Merge branch 'develop' into refactor-jinja-hook
This commit is contained in:
commit
6cb64934a8
66 changed files with 917 additions and 632 deletions
|
|
@ -143,6 +143,7 @@
|
|||
"Cypress": true,
|
||||
"cy": true,
|
||||
"it": true,
|
||||
"describe": true,
|
||||
"expect": true,
|
||||
"context": true,
|
||||
"before": true,
|
||||
|
|
|
|||
7
.github/workflows/ci-tests.yml
vendored
7
.github/workflows/ci-tests.yml
vendored
|
|
@ -149,9 +149,10 @@ jobs:
|
|||
run: |
|
||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
pip install coveralls==2.2.0
|
||||
pip install coverage==4.5.4
|
||||
coveralls
|
||||
pip install coveralls==3.0.1
|
||||
pip install coverage==5.5
|
||||
coveralls --service=github
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
||||
COVERALLS_SERVICE_NAME: github
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ context('Form', () => {
|
|||
});
|
||||
it('create a new form', () => {
|
||||
cy.visit('/app/todo/new');
|
||||
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
|
||||
cy.fill_field('description', 'this is a test todo', 'Text Editor');
|
||||
cy.wait(300);
|
||||
cy.get('.page-title').should('contain', 'Not Saved');
|
||||
cy.intercept({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
context('Relative Timeframe', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
context('Table MultiSelect', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ if PY2:
|
|||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
__version__ = '13.0.0-dev'
|
||||
__version__ = '13.1.0'
|
||||
|
||||
__title__ = "Frappe Framework"
|
||||
|
||||
|
|
@ -975,7 +975,7 @@ def get_pymodule_path(modulename, *joins):
|
|||
:param *joins: Join additional path elements using `os.path.join`."""
|
||||
if not "public" in joins:
|
||||
joins = [scrub(part) for part in joins]
|
||||
return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__), *joins)
|
||||
return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__ or ''), *joins)
|
||||
|
||||
def get_module_list(app_name):
|
||||
"""Get list of modules for given all via `app/modules.txt`."""
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ class LoginManager:
|
|||
self.make_session()
|
||||
self.set_user_info()
|
||||
|
||||
@frappe.whitelist()
|
||||
def login(self):
|
||||
# clear cache
|
||||
frappe.clear_cache(user = frappe.form_dict.get('usr'))
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ global_cache_keys = ("app_hooks", "installed_apps", 'all_apps',
|
|||
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
|
||||
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
|
||||
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts',
|
||||
'sitemap_routes', 'db_tables') + doctype_map_keys
|
||||
'sitemap_routes', 'db_tables', 'server_script_autocompletion_items') + doctype_map_keys
|
||||
|
||||
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
|
||||
"defaults", "user_permissions", "home_page", "linked_with",
|
||||
|
|
|
|||
22
frappe/change_log/v13/v13_1_0.md
Normal file
22
frappe/change_log/v13/v13_1_0.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Version 13.1.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- Automated mail notifications will be shown in timeline ([#12693](https://github.com/frappe/frappe/pull/12693))
|
||||
- Introduced Client Script for List views ([#12590](https://github.com/frappe/frappe/pull/12590))
|
||||
- Introduced language switcher for guest users on website navbar ([#12813](https://github.com/frappe/frappe/pull/12813))
|
||||
- Option to give submit permission while sharing a document ([#12799](https://github.com/frappe/frappe/pull/12799))
|
||||
- Added option to set `autoname` in Customize Form ([#12413](https://github.com/frappe/frappe/pull/12413))
|
||||
- Virtual DocType ([#12121](https://github.com/frappe/frappe/pull/12121))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Workspace fixes ([#12650](https://github.com/frappe/frappe/pull/12650)) ([#12655](https://github.com/frappe/frappe/pull/12655)) ([#12869](https://github.com/frappe/frappe/pull/12869))
|
||||
- Fixed an issue where select options were not getting updated in Grid ([#12839](https://github.com/frappe/frappe/pull/12839))
|
||||
- Webform Fixes ([#12630](https://github.com/frappe/frappe/pull/12630)) ([#12756](https://github.com/frappe/frappe/pull/12756)) ([#12819](https://github.com/frappe/frappe/pull/12819))
|
||||
- Fixed timespan filter for next and last timespans ([#12509](https://github.com/frappe/frappe/pull/12509))
|
||||
- System Notification fixes ([#12719](https://github.com/frappe/frappe/pull/12719))
|
||||
- Design Fixes ([#12669](https://github.com/frappe/frappe/pull/12669)) ([#12591](https://github.com/frappe/frappe/pull/12591)) ([#12557](https://github.com/frappe/frappe/pull/12557)) ([#12751](https://github.com/frappe/frappe/pull/12751)) ([#12864](https://github.com/frappe/frappe/pull/12864))
|
||||
- Fixed Multi-column paste in grid ([#12861](https://github.com/frappe/frappe/pull/12861))
|
||||
- Fixed grid validation ([#12744](https://github.com/frappe/frappe/pull/12744))
|
||||
- Fixed currency value formatting in dashboard chart ([#12613](https://github.com/frappe/frappe/pull/12613))
|
||||
|
|
@ -62,11 +62,24 @@ def popen(command, *args, **kwargs):
|
|||
if env:
|
||||
env = dict(environ, **env)
|
||||
|
||||
def set_low_prio():
|
||||
import psutil
|
||||
if psutil.LINUX:
|
||||
psutil.Process().nice(19)
|
||||
psutil.Process().ionice(psutil.IOPRIO_CLASS_IDLE)
|
||||
elif psutil.WINDOWS:
|
||||
psutil.Process().nice(psutil.IDLE_PRIORITY_CLASS)
|
||||
psutil.Process().ionice(psutil.IOPRIO_VERYLOW)
|
||||
else:
|
||||
psutil.Process().nice(19)
|
||||
# ionice not supported
|
||||
|
||||
proc = subprocess.Popen(command,
|
||||
stdout=None if output else subprocess.PIPE,
|
||||
stderr=None if output else subprocess.PIPE,
|
||||
shell=shell,
|
||||
cwd=cwd,
|
||||
preexec_fn=set_low_prio,
|
||||
env=env
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,22 +18,33 @@ def _is_scheduler_enabled():
|
|||
|
||||
return enable_scheduler
|
||||
|
||||
@click.command('trigger-scheduler-event')
|
||||
@click.argument('event')
|
||||
|
||||
@click.command("trigger-scheduler-event", help="Trigger a scheduler event")
|
||||
@click.argument("event")
|
||||
@pass_context
|
||||
def trigger_scheduler_event(context, event):
|
||||
"Trigger a scheduler event"
|
||||
import frappe.utils.scheduler
|
||||
|
||||
exit_code = 0
|
||||
|
||||
for site in context.sites:
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
frappe.utils.scheduler.trigger(site, event, now=True)
|
||||
try:
|
||||
frappe.get_doc("Scheduled Job Type", {"method": event}).execute()
|
||||
except frappe.DoesNotExistError:
|
||||
click.secho(f"Event {event} does not exist!", fg="red")
|
||||
exit_code = 1
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
@click.command('enable-scheduler')
|
||||
@pass_context
|
||||
def enable_scheduler(context):
|
||||
|
|
|
|||
|
|
@ -676,10 +676,8 @@ def start_ngrok(context):
|
|||
frappe.init(site=site)
|
||||
|
||||
port = frappe.conf.http_port or frappe.conf.webserver_port
|
||||
public_url = ngrok.connect(port=port, options={
|
||||
'host_header': site
|
||||
})
|
||||
print(f'Public URL: {public_url}')
|
||||
tunnel = ngrok.connect(addr=str(port), host_header=site)
|
||||
print(f'Public URL: {tunnel.public_url}')
|
||||
print('Inspect logs at http://localhost:4040')
|
||||
|
||||
ngrok_process = ngrok.get_ngrok_process()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import click
|
|||
import frappe
|
||||
from frappe.commands import get_site, pass_context
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
from frappe.utils import get_bench_path, update_progress_bar
|
||||
from frappe.utils import get_bench_path, update_progress_bar, cint
|
||||
|
||||
|
||||
@click.command('build')
|
||||
|
|
@ -567,11 +567,14 @@ def run_ui_tests(context, app, headless=False):
|
|||
|
||||
node_bin = subprocess.getoutput("npm bin")
|
||||
cypress_path = "{0}/cypress".format(node_bin)
|
||||
plugin_path = "{0}/cypress-file-upload".format(node_bin)
|
||||
plugin_path = "{0}/../cypress-file-upload".format(node_bin)
|
||||
|
||||
# check if cypress in path...if not, install it.
|
||||
if not (os.path.exists(cypress_path) or os.path.exists(plugin_path)) \
|
||||
or not subprocess.getoutput("npm view cypress version").startswith("6."):
|
||||
if not (
|
||||
os.path.exists(cypress_path)
|
||||
and os.path.exists(plugin_path)
|
||||
and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6
|
||||
):
|
||||
# install cypress
|
||||
click.secho("Installing Cypress...", fg="yellow")
|
||||
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile")
|
||||
|
|
|
|||
|
|
@ -20,6 +20,6 @@ frappe.listview_settings['Communication'] = {
|
|||
},
|
||||
|
||||
primary_action: function() {
|
||||
new frappe.views.CommunicationComposer({ doc: {} });
|
||||
new frappe.views.CommunicationComposer();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@
|
|||
"show_preview_popup",
|
||||
"show_name_in_global_search",
|
||||
"email_settings_sb",
|
||||
"default_email_template",
|
||||
"column_break_51",
|
||||
"email_append_to",
|
||||
"sender_field",
|
||||
"subject_field",
|
||||
|
|
@ -535,6 +537,16 @@
|
|||
"fieldname": "is_virtual",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Virtual"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_email_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Email Template",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_51",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-bolt",
|
||||
|
|
@ -616,7 +628,7 @@
|
|||
"link_fieldname": "reference_doctype"
|
||||
}
|
||||
],
|
||||
"modified": "2021-02-17 20:18:06.212232",
|
||||
"modified": "2021-04-16 12:26:41.031135",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
|
|
|
|||
|
|
@ -325,9 +325,8 @@ def get_group_by_field(args, doctype):
|
|||
if args['aggregate_function'] == 'count':
|
||||
group_by_field = 'count(*) as _aggregate_column'
|
||||
else:
|
||||
group_by_field = '{0}(`tab{1}`.{2}) as _aggregate_column'.format(
|
||||
group_by_field = '{0}({1}) as _aggregate_column'.format(
|
||||
args.aggregate_function,
|
||||
doctype,
|
||||
args.aggregate_on
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
|
||||
import frappe, json
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import now_datetime, get_datetime
|
||||
from datetime import datetime
|
||||
from croniter import croniter
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_datetime, now_datetime
|
||||
from frappe.utils.background_jobs import enqueue, get_jobs
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ frappe.ui.form.on('Server Script', {
|
|||
if (frm.doc.script_type != 'Scheduler Event') {
|
||||
frm.dashboard.hide();
|
||||
}
|
||||
|
||||
frm.call('get_autocompletion_items')
|
||||
.then(r => r.message)
|
||||
.then(items => {
|
||||
frm.set_df_property('script', 'autocompletions', items);
|
||||
});
|
||||
},
|
||||
|
||||
setup_help(frm) {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import ast
|
||||
from types import FunctionType, MethodType, ModuleType
|
||||
from typing import Dict, List
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.safe_exec import safe_exec
|
||||
from frappe.utils.safe_exec import get_safe_globals, safe_exec, NamespaceDict
|
||||
from frappe import _
|
||||
|
||||
|
||||
|
|
@ -122,6 +123,51 @@ class ServerScript(Document):
|
|||
if locals["conditions"]:
|
||||
return locals["conditions"]
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_autocompletion_items(self):
|
||||
"""Generates a list of a autocompletion strings from the context dict
|
||||
that is used while executing a Server Script.
|
||||
|
||||
Returns:
|
||||
list: Returns list of autocompletion items.
|
||||
For e.g., ["frappe.utils.cint", "frappe.db.get_all", ...]
|
||||
"""
|
||||
def get_keys(obj):
|
||||
out = []
|
||||
for key in obj:
|
||||
if key.startswith('_'):
|
||||
continue
|
||||
value = obj[key]
|
||||
if isinstance(value, (NamespaceDict, dict)) and value:
|
||||
if key == 'form_dict':
|
||||
out.append(['form_dict', 7])
|
||||
continue
|
||||
for subkey, score in get_keys(value):
|
||||
fullkey = f'{key}.{subkey}'
|
||||
out.append([fullkey, score])
|
||||
else:
|
||||
if isinstance(value, type) and issubclass(value, Exception):
|
||||
score = 0
|
||||
elif isinstance(value, ModuleType):
|
||||
score = 10
|
||||
elif isinstance(value, (FunctionType, MethodType)):
|
||||
score = 9
|
||||
elif isinstance(value, type):
|
||||
score = 8
|
||||
elif isinstance(value, dict):
|
||||
score = 7
|
||||
else:
|
||||
score = 6
|
||||
out.append([key, score])
|
||||
return out
|
||||
|
||||
items = frappe.cache().get_value('server_script_autocompletion_items')
|
||||
if not items:
|
||||
items = get_keys(get_safe_globals())
|
||||
items = [{'value': d[0], 'score': d[1]} for d in items]
|
||||
frappe.cache().set_value('server_script_autocompletion_items', items)
|
||||
return items
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def setup_scheduler_events(script_name, frequency):
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@
|
|||
"show_preview_popup",
|
||||
"image_view",
|
||||
"email_settings_section",
|
||||
"default_email_template",
|
||||
"column_break_26",
|
||||
"email_append_to",
|
||||
"sender_field",
|
||||
"subject_field",
|
||||
|
|
@ -264,6 +266,16 @@
|
|||
"label": "Actions",
|
||||
"options": "DocType Action"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_email_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Email Template",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_26",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "naming_section",
|
||||
|
|
@ -283,7 +295,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-16 15:22:11.108256",
|
||||
"modified": "2021-03-22 12:27:15.462727",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
@ -304,4 +316,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -491,6 +491,7 @@ doctype_properties = {
|
|||
'allow_auto_repeat': 'Check',
|
||||
'allow_import': 'Check',
|
||||
'show_preview_popup': 'Check',
|
||||
'default_email_template': 'Data',
|
||||
'email_append_to': 'Check',
|
||||
'subject_field': 'Data',
|
||||
'sender_field': 'Data',
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
import warnings
|
||||
|
||||
import pymysql
|
||||
from pymysql.times import TimeDelta
|
||||
from pymysql.constants import ER, FIELD_TYPE
|
||||
from pymysql.converters import conversions
|
||||
from pymysql.constants import ER, FIELD_TYPE
|
||||
from pymysql.converters import conversions, escape_string
|
||||
|
||||
from frappe.utils import get_datetime, cstr, UnicodeWithAttrs
|
||||
import frappe
|
||||
from frappe.database.database import Database
|
||||
from six import PY2, binary_type, text_type, string_types
|
||||
from frappe.database.mariadb.schema import MariaDBTable
|
||||
from frappe.utils import UnicodeWithAttrs, cstr, get_datetime
|
||||
|
||||
|
||||
class MariaDBDatabase(Database):
|
||||
|
|
@ -72,22 +68,20 @@ class MariaDBDatabase(Database):
|
|||
conversions.update({
|
||||
FIELD_TYPE.NEWDECIMAL: float,
|
||||
FIELD_TYPE.DATETIME: get_datetime,
|
||||
UnicodeWithAttrs: conversions[text_type]
|
||||
UnicodeWithAttrs: conversions[str]
|
||||
})
|
||||
|
||||
if PY2:
|
||||
conversions.update({
|
||||
TimeDelta: conversions[binary_type]
|
||||
})
|
||||
|
||||
if usessl:
|
||||
conn = pymysql.connect(self.host, self.user or '', self.password or '',
|
||||
port=self.port, charset='utf8mb4', use_unicode = True, ssl=ssl_params,
|
||||
conv = conversions, local_infile = frappe.conf.local_infile)
|
||||
else:
|
||||
conn = pymysql.connect(self.host, self.user or '', self.password or '',
|
||||
port=self.port, charset='utf8mb4', use_unicode = True, conv = conversions,
|
||||
local_infile = frappe.conf.local_infile)
|
||||
conn = pymysql.connect(
|
||||
user=self.user or '',
|
||||
password=self.password or '',
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
charset='utf8mb4',
|
||||
use_unicode=True,
|
||||
ssl=ssl_params if usessl else None,
|
||||
conv=conversions,
|
||||
local_infile=frappe.conf.local_infile
|
||||
)
|
||||
|
||||
# MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1
|
||||
# # self._conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_OFF)
|
||||
|
|
@ -111,7 +105,7 @@ class MariaDBDatabase(Database):
|
|||
def escape(s, percent=True):
|
||||
"""Excape quotes and percent in given string."""
|
||||
# pymysql expects unicode argument to escape_string with Python 3
|
||||
s = frappe.as_unicode(pymysql.escape_string(frappe.as_unicode(s)), "utf-8").replace("`", "\\`")
|
||||
s = frappe.as_unicode(escape_string(frappe.as_unicode(s)), "utf-8").replace("`", "\\`")
|
||||
|
||||
# NOTE separating % escape, because % escape should only be done when using LIKE operator
|
||||
# or when you use python format string to generate query that already has a %s
|
||||
|
|
@ -260,7 +254,7 @@ class MariaDBDatabase(Database):
|
|||
ADD INDEX `%s`(%s)""" % (table_name, index_name, ", ".join(fields)))
|
||||
|
||||
def add_unique(self, doctype, fields, constraint_name=None):
|
||||
if isinstance(fields, string_types):
|
||||
if isinstance(fields, str):
|
||||
fields = [fields]
|
||||
if not constraint_name:
|
||||
constraint_name = "unique_" + "_".join(fields)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ frappe.ui.form.on('Notification Settings', {
|
|||
|
||||
refresh: (frm) => {
|
||||
if (frappe.user.has_role('System Manager')) {
|
||||
frm.add_custom_button('Go to Notification Settings List', () => {
|
||||
frm.add_custom_button(__('Go to Notification Settings List'), () => {
|
||||
frappe.set_route('List', 'Notification Settings');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,13 +126,14 @@ def setup_group_by(data):
|
|||
if data.group_by:
|
||||
if data.aggregate_function.lower() not in ('count', 'sum', 'avg'):
|
||||
frappe.throw(_('Invalid aggregate function'))
|
||||
if '`' in data.aggregate_on:
|
||||
raise_invalid_field(data.aggregate_on)
|
||||
data.fields.append('{aggregate_function}(`tab{doctype}`.`{aggregate_on}`) AS _aggregate_column'.format(**data))
|
||||
if data.aggregate_on:
|
||||
data.fields.append(data.aggregate_on)
|
||||
|
||||
data.pop('aggregate_on')
|
||||
if frappe.db.has_column(data.aggregate_on_doctype, data.aggregate_on_field):
|
||||
data.fields.append('{aggregate_function}(`tab{aggregate_on_doctype}`.`{aggregate_on_field}`) AS _aggregate_column'.format(**data))
|
||||
else:
|
||||
raise_invalid_field(data.aggregate_on_field)
|
||||
|
||||
data.pop('aggregate_on_doctype')
|
||||
data.pop('aggregate_on_field')
|
||||
data.pop('aggregate_function')
|
||||
|
||||
def raise_invalid_field(fieldname):
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters):
|
|||
return out
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_children(doctype, parent=''):
|
||||
def get_children(doctype, parent='', **filters):
|
||||
return _get_children(doctype, parent)
|
||||
|
||||
def _get_children(doctype, parent='', ignore_permissions=False):
|
||||
|
|
@ -66,7 +66,7 @@ def add_node():
|
|||
doc.save()
|
||||
|
||||
def make_tree_args(**kwarg):
|
||||
del kwarg['cmd']
|
||||
kwarg.pop('cmd', None)
|
||||
|
||||
doctype = kwarg['doctype']
|
||||
parent_field = 'parent_' + doctype.lower().replace(' ', '_')
|
||||
|
|
|
|||
|
|
@ -1,18 +1,27 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import six
|
||||
from six import iteritems, text_type
|
||||
from six.moves import range
|
||||
import time, _socket, poplib, imaplib, email, email.utils, datetime, chardet, re
|
||||
from email_reply_parser import EmailReplyParser
|
||||
import datetime
|
||||
import email
|
||||
import email.utils
|
||||
import imaplib
|
||||
import poplib
|
||||
import re
|
||||
import time
|
||||
from email.header import decode_header
|
||||
|
||||
import _socket
|
||||
import chardet
|
||||
import six
|
||||
from email_reply_parser import EmailReplyParser
|
||||
|
||||
import frappe
|
||||
from frappe import _, safe_decode, safe_encode
|
||||
from frappe.utils import (extract_email_id, convert_utc_to_user_timezone, now,
|
||||
cint, cstr, strip, markdown, parse_addr)
|
||||
from frappe.core.doctype.file.file import get_random_filename, MaxFileSizeReachedError
|
||||
from frappe.core.doctype.file.file import (MaxFileSizeReachedError,
|
||||
get_random_filename)
|
||||
from frappe.utils import (cint, convert_utc_to_user_timezone, cstr,
|
||||
extract_email_id, markdown, now, parse_addr, strip)
|
||||
|
||||
|
||||
class EmailSizeExceededError(frappe.ValidationError): pass
|
||||
class EmailTimeoutError(frappe.ValidationError): pass
|
||||
|
|
@ -337,7 +346,7 @@ class EmailServer:
|
|||
return
|
||||
|
||||
self.imap.select("Inbox")
|
||||
for uid, operation in iteritems(uid_list):
|
||||
for uid, operation in uid_list.items():
|
||||
if not uid: continue
|
||||
|
||||
op = "+FLAGS" if operation == "Read" else "-FLAGS"
|
||||
|
|
@ -473,7 +482,7 @@ class Email:
|
|||
self.html_content += markdown(text_content)
|
||||
|
||||
def get_charset(self, part):
|
||||
"""Detect chartset."""
|
||||
"""Detect charset."""
|
||||
charset = part.get_content_charset()
|
||||
if not charset:
|
||||
charset = chardet.detect(safe_encode(cstr(part)))['encoding']
|
||||
|
|
@ -484,7 +493,7 @@ class Email:
|
|||
charset = self.get_charset(part)
|
||||
|
||||
try:
|
||||
return text_type(part.get_payload(decode=True), str(charset), "ignore")
|
||||
return str(part.get_payload(decode=True), str(charset), "ignore")
|
||||
except LookupError:
|
||||
return part.get_payload()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,22 +2,23 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import dropbox
|
||||
import json
|
||||
import frappe
|
||||
import os
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size, get_chunk_site
|
||||
from frappe.integrations.utils import make_post_request
|
||||
from frappe.utils import (cint, get_request_site_address,
|
||||
get_files_path, get_backups_path, get_url, encode)
|
||||
from frappe.utils.backups import new_backup
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from six.moves.urllib.parse import urlparse, parse_qs
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import dropbox
|
||||
from rq.timeouts import JobTimeoutException
|
||||
from six import text_type
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.integrations.offsite_backup_utils import (get_chunk_site,
|
||||
get_latest_backup_file, send_email, validate_file_size)
|
||||
from frappe.integrations.utils import make_post_request
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (cint, encode, get_backups_path, get_files_path,
|
||||
get_request_site_address, get_url)
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.backups import new_backup
|
||||
|
||||
ignore_list = [".DS_Store"]
|
||||
|
||||
|
|
@ -91,7 +92,10 @@ def backup_to_dropbox(upload_db_backup=True):
|
|||
dropbox_settings['access_token'] = access_token['oauth2_token']
|
||||
set_dropbox_access_token(access_token['oauth2_token'])
|
||||
|
||||
dropbox_client = dropbox.Dropbox(dropbox_settings['access_token'], timeout=None)
|
||||
dropbox_client = dropbox.Dropbox(
|
||||
oauth2_access_token=dropbox_settings['access_token'],
|
||||
timeout=None
|
||||
)
|
||||
|
||||
if upload_db_backup:
|
||||
if frappe.flags.create_new_backup:
|
||||
|
|
@ -127,7 +131,7 @@ def upload_from_folder(path, is_private, dropbox_folder, dropbox_client, did_not
|
|||
else:
|
||||
response = frappe._dict({"entries": []})
|
||||
|
||||
path = text_type(path)
|
||||
path = str(path)
|
||||
|
||||
for f in frappe.get_all("File", filters={"is_folder": 0, "is_private": is_private,
|
||||
"uploaded_to_dropbox": 0}, fields=['file_url', 'name', 'file_name']):
|
||||
|
|
@ -286,11 +290,11 @@ def get_redirect_url():
|
|||
def get_dropbox_authorize_url():
|
||||
app_details = get_dropbox_settings(redirect_uri=True)
|
||||
dropbox_oauth_flow = dropbox.DropboxOAuth2Flow(
|
||||
app_details["app_key"],
|
||||
app_details["app_secret"],
|
||||
app_details["redirect_uri"],
|
||||
{},
|
||||
"dropbox-auth-csrf-token"
|
||||
consumer_key=app_details["app_key"],
|
||||
redirect_uri=app_details["redirect_uri"],
|
||||
session={},
|
||||
csrf_token_session_key="dropbox-auth-csrf-token",
|
||||
consumer_secret=app_details["app_secret"]
|
||||
)
|
||||
|
||||
auth_url = dropbox_oauth_flow.start()
|
||||
|
|
@ -307,13 +311,13 @@ def dropbox_auth_finish(return_access_token=False):
|
|||
close = '<p class="text-muted">' + _('Please close this window') + '</p>'
|
||||
|
||||
dropbox_oauth_flow = dropbox.DropboxOAuth2Flow(
|
||||
app_details["app_key"],
|
||||
app_details["app_secret"],
|
||||
app_details["redirect_uri"],
|
||||
{
|
||||
consumer_key=app_details["app_key"],
|
||||
redirect_uri=app_details["redirect_uri"],
|
||||
session={
|
||||
'dropbox-auth-csrf-token': callback.state
|
||||
},
|
||||
"dropbox-auth-csrf-token"
|
||||
csrf_token_session_key="dropbox-auth-csrf-token",
|
||||
consumer_secret=app_details["app_secret"]
|
||||
)
|
||||
|
||||
if callback.state or callback.code:
|
||||
|
|
|
|||
|
|
@ -2,22 +2,23 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import requests
|
||||
import googleapiclient.discovery
|
||||
import google.oauth2.credentials
|
||||
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_request_site_address
|
||||
from googleapiclient.errors import HttpError
|
||||
from frappe.utils.password import set_encrypted_password
|
||||
from frappe.utils import add_days, get_datetime, get_weekdays, now_datetime, add_to_date, get_time_zone
|
||||
from dateutil import parser
|
||||
from datetime import datetime, timedelta
|
||||
from six.moves.urllib.parse import quote
|
||||
from urllib.parse import quote
|
||||
|
||||
import google.oauth2.credentials
|
||||
import requests
|
||||
from dateutil import parser
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.integrations.doctype.google_settings.google_settings import get_auth_url
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (add_days, add_to_date, get_datetime,
|
||||
get_request_site_address, get_time_zone, get_weekdays, now_datetime)
|
||||
from frappe.utils.password import set_encrypted_password
|
||||
|
||||
SCOPES = "https://www.googleapis.com/auth/calendar"
|
||||
|
||||
|
|
@ -171,7 +172,12 @@ def get_google_calendar_object(g_calendar):
|
|||
}
|
||||
|
||||
credentials = google.oauth2.credentials.Credentials(**credentials_dict)
|
||||
google_calendar = googleapiclient.discovery.build("calendar", "v3", credentials=credentials)
|
||||
google_calendar = build(
|
||||
serviceName="calendar",
|
||||
version="v3",
|
||||
credentials=credentials,
|
||||
static_discovery=False
|
||||
)
|
||||
|
||||
check_google_calendar(account, google_calendar)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import requests
|
||||
import googleapiclient.discovery
|
||||
import google.oauth2.credentials
|
||||
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
import google.oauth2.credentials
|
||||
import requests
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
from frappe.utils import get_request_site_address
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.integrations.doctype.google_settings.google_settings import get_auth_url
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_request_site_address
|
||||
|
||||
SCOPES = "https://www.googleapis.com/auth/contacts"
|
||||
|
||||
|
|
@ -118,7 +118,12 @@ def get_google_contacts_object(g_contact):
|
|||
}
|
||||
|
||||
credentials = google.oauth2.credentials.Credentials(**credentials_dict)
|
||||
google_contacts = googleapiclient.discovery.build("people", "v1", credentials=credentials)
|
||||
google_contacts = build(
|
||||
serviceName="people",
|
||||
version="v1",
|
||||
credentials=credentials,
|
||||
static_discovery=False
|
||||
)
|
||||
|
||||
return google_contacts, account
|
||||
|
||||
|
|
|
|||
|
|
@ -2,27 +2,29 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import requests
|
||||
import googleapiclient.discovery
|
||||
import google.oauth2.credentials
|
||||
import os
|
||||
from urllib.parse import quote
|
||||
|
||||
from frappe import _
|
||||
from googleapiclient.errors import HttpError
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_request_site_address
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from six.moves.urllib.parse import quote
|
||||
import google.oauth2.credentials
|
||||
import requests
|
||||
from apiclient.http import MediaFileUpload
|
||||
from frappe.utils import get_backups_path, get_bench_path
|
||||
from frappe.utils.backups import new_backup
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.integrations.doctype.google_settings.google_settings import get_auth_url
|
||||
from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size
|
||||
from frappe.integrations.offsite_backup_utils import (get_latest_backup_file,
|
||||
send_email, validate_file_size)
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (get_backups_path, get_bench_path,
|
||||
get_request_site_address)
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.backups import new_backup
|
||||
|
||||
SCOPES = "https://www.googleapis.com/auth/drive"
|
||||
|
||||
|
||||
class GoogleDrive(Document):
|
||||
|
||||
def validate(self):
|
||||
|
|
@ -126,7 +128,12 @@ def get_google_drive_object():
|
|||
}
|
||||
|
||||
credentials = google.oauth2.credentials.Credentials(**credentials_dict)
|
||||
google_drive = googleapiclient.discovery.build("drive", "v3", credentials=credentials)
|
||||
google_drive = build(
|
||||
serviceName="drive",
|
||||
version="v3",
|
||||
credentials=credentials,
|
||||
static_discovery=False
|
||||
)
|
||||
|
||||
return google_drive, account
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class TestTokenCache(unittest.TestCase):
|
|||
def setUp(self):
|
||||
self.token_cache = frappe.get_last_doc('Token Cache')
|
||||
self.token_cache.update({'connected_app': frappe.get_last_doc('Connected App').name})
|
||||
self.token_cache.save()
|
||||
self.token_cache.save(ignore_permissions=True)
|
||||
|
||||
def test_get_auth_header(self):
|
||||
self.token_cache.get_auth_header()
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ def get_token(*args, **kwargs):
|
|||
}
|
||||
|
||||
id_token_encoded = jwt.encode(id_token, client_secret, algorithm='HS256', headers=id_token_header)
|
||||
out.update({"id_token": str(id_token_encoded)})
|
||||
out.update({"id_token": frappe.safe_decode(id_token_encoded)})
|
||||
|
||||
frappe.local.response = out
|
||||
|
||||
|
|
|
|||
|
|
@ -159,9 +159,10 @@ frappe.ui.form.Control = Class.extend({
|
|||
},
|
||||
validate_and_set_in_model: function(value, e) {
|
||||
var me = this;
|
||||
if(this.inside_change_event) {
|
||||
if (this.inside_change_event || this.get_model_value() === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.inside_change_event = true;
|
||||
var set = function(value) {
|
||||
me.inside_change_event = false;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,57 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
|||
const input_value = this.get_input_value();
|
||||
this.parse_validate_and_set_in_model(input_value);
|
||||
}, 300));
|
||||
|
||||
// setup autocompletion when it is set the first time
|
||||
Object.defineProperty(this.df, 'autocompletions', {
|
||||
get() {
|
||||
return this._autocompletions || [];
|
||||
},
|
||||
set: (value) => {
|
||||
this.setup_autocompletion();
|
||||
this.df._autocompletions = value;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setup_autocompletion() {
|
||||
if (this._autocompletion_setup) return;
|
||||
|
||||
const ace = window.ace;
|
||||
const get_autocompletions = () => this.df.autocompletions;
|
||||
|
||||
ace.config.loadModule("ace/ext/language_tools", langTools => {
|
||||
this.editor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true
|
||||
});
|
||||
|
||||
langTools.addCompleter({
|
||||
getCompletions: function(editor, session, pos, prefix, callback) {
|
||||
if (prefix.length === 0) {
|
||||
callback(null, []);
|
||||
return;
|
||||
}
|
||||
let autocompletions = get_autocompletions();
|
||||
if (autocompletions.length) {
|
||||
callback(
|
||||
null,
|
||||
autocompletions.map(a => {
|
||||
if (typeof a === 'string') {
|
||||
a = { value: a };
|
||||
}
|
||||
return {
|
||||
name: 'frappe',
|
||||
value: a.value,
|
||||
score: a.score
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
this._autocompletion_setup = true;
|
||||
},
|
||||
|
||||
refresh_height() {
|
||||
|
|
|
|||
|
|
@ -462,9 +462,10 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
|
|||
if(this.frm && this.frm.fetch_dict[df.fieldname]) {
|
||||
fetch = this.frm.fetch_dict[df.fieldname].columns.join(', ');
|
||||
}
|
||||
|
||||
// if default and no fetch, no need to validate
|
||||
if (!fetch && df.__default_value && df.__default_value===value) return value;
|
||||
if (!fetch && df.__default_value && df.__default_value===value) {
|
||||
resolve(value);
|
||||
}
|
||||
|
||||
this.fetch_and_validate_link(resolve, df, doctype, docname, value, fetch);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ frappe.ui.form.ControlMultiSelect = frappe.ui.form.ControlAutocomplete.extend({
|
|||
let data;
|
||||
if(this.df.get_data) {
|
||||
data = this.df.get_data();
|
||||
this.set_data(data);
|
||||
if (data) this.set_data(data);
|
||||
} else {
|
||||
data = this._super();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ frappe.ui.form.ControlTableMultiSelect = frappe.ui.form.ControlLink.extend({
|
|||
this._rows_list = this.rows.map(row => row[link_field.fieldname]);
|
||||
return this.rows;
|
||||
},
|
||||
get_model_value() {
|
||||
let value = this._super();
|
||||
return value ? value.filter(d => !d.__islocal) : value;
|
||||
},
|
||||
validate(value) {
|
||||
const rows = (value || []).slice();
|
||||
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
|
||||
// bind links
|
||||
transactions_area_body.find(".badge-link").on('click', function() {
|
||||
me.open_document_list($(this).parent());
|
||||
me.open_document_list($(this).closest('.document-link'));
|
||||
});
|
||||
|
||||
// bind reports
|
||||
|
|
|
|||
|
|
@ -360,6 +360,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
grid_obj.grid.grid_pagination.go_to_page(1, true);
|
||||
});
|
||||
frappe.ui.form.close_grid_form();
|
||||
this.viewers && this.viewers.parent.empty();
|
||||
this.docname = docname;
|
||||
this.setup_docinfo_change_listener();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ frappe.ui.form.FormViewers = class FormViewers {
|
|||
|
||||
refresh() {
|
||||
let users = this.frm.get_docinfo()['viewers'];
|
||||
if (!users || !users.current || !users.current.length) {
|
||||
this.parent.empty();
|
||||
return;
|
||||
}
|
||||
|
||||
let currently_viewing = users.current.filter(user => user != frappe.session.user);
|
||||
let avatar_group = frappe.avatar_group(currently_viewing, 5, {'align': 'left', 'overlap': true});
|
||||
this.parent.empty().append(avatar_group);
|
||||
|
|
|
|||
|
|
@ -194,7 +194,10 @@ export default class Grid {
|
|||
}
|
||||
|
||||
tasks.push(() => {
|
||||
if (dirty) this.refresh();
|
||||
if (dirty) {
|
||||
this.refresh();
|
||||
this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
|
||||
}
|
||||
});
|
||||
|
||||
frappe.run_serially(tasks);
|
||||
|
|
@ -210,6 +213,7 @@ export default class Grid {
|
|||
this.frm.doc[this.df.fieldname] = [];
|
||||
$(this.parent).find('.rows').empty();
|
||||
this.grid_rows = [];
|
||||
this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
|
||||
|
||||
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').prop('checked', 0);
|
||||
this.refresh();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export default class GridRow {
|
|||
this.on_grid_fields = [];
|
||||
$.extend(this, opts);
|
||||
if (this.doc && this.parent_df.options) {
|
||||
frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields);
|
||||
this.docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
|
||||
}
|
||||
this.columns = {};
|
||||
|
|
@ -555,6 +556,15 @@ export default class GridRow {
|
|||
this.grid_form.render();
|
||||
this.row.toggle(false);
|
||||
// this.form_panel.toggle(true);
|
||||
|
||||
if (this.grid.cannot_add_rows || (this.grid.df && this.grid.df.cannot_add_rows)) {
|
||||
this.wrapper.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row')
|
||||
.addClass('hidden');
|
||||
} else {
|
||||
this.wrapper.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row')
|
||||
.removeClass('hidden');
|
||||
}
|
||||
|
||||
frappe.dom.freeze("", "dark");
|
||||
if (cur_frm) cur_frm.cur_grid = this;
|
||||
this.wrapper.addClass("grid-row-open");
|
||||
|
|
|
|||
|
|
@ -211,7 +211,6 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
|
||||
make_viewers() {
|
||||
if (this.frm.viewers) {
|
||||
this.frm.viewers.parent.empty();
|
||||
return;
|
||||
}
|
||||
this.frm.viewers = new frappe.ui.form.FormViewers({
|
||||
|
|
|
|||
|
|
@ -177,7 +177,9 @@ $.extend(frappe.model, {
|
|||
// Use User Permission value when only when it has a single value
|
||||
user_default = user_defaults[0];
|
||||
}
|
||||
} else if (!user_default) {
|
||||
}
|
||||
|
||||
if (!user_default) {
|
||||
user_default = frappe.defaults.get_user_default(df.fieldname);
|
||||
} else if (
|
||||
!user_default &&
|
||||
|
|
|
|||
|
|
@ -38,14 +38,14 @@ $.extend(frappe.meta, {
|
|||
frappe.meta.docfield_list[df.parent].push(df);
|
||||
},
|
||||
|
||||
make_docfield_copy_for: function(doctype, docname) {
|
||||
make_docfield_copy_for: function(doctype, docname, docfield_list=null) {
|
||||
var c = frappe.meta.docfield_copy;
|
||||
if(!c[doctype])
|
||||
c[doctype] = {};
|
||||
if(!c[doctype][docname])
|
||||
c[doctype][docname] = {};
|
||||
|
||||
var docfield_list = frappe.meta.docfield_list[doctype] || [];
|
||||
docfield_list = docfield_list || frappe.meta.docfield_list[doctype] || [];
|
||||
for(var i=0, j=docfield_list.length; i<j; i++) {
|
||||
var df = docfield_list[i];
|
||||
c[doctype][docname][df.fieldname || df.label] = copy_dict(df);
|
||||
|
|
|
|||
|
|
@ -313,7 +313,8 @@ frappe.ui.GroupBy = class {
|
|||
|
||||
Object.assign(args, {
|
||||
with_comment_count: false,
|
||||
aggregate_on: this.aggregate_on || 'name',
|
||||
aggregate_on_field: this.aggregate_on_field || 'name',
|
||||
aggregate_on_doctype: this.aggregate_on_doctype || this.doctype,
|
||||
aggregate_function: this.aggregate_function || 'count',
|
||||
group_by: this.report_view.group_by || null,
|
||||
order_by: '_aggregate_column desc',
|
||||
|
|
|
|||
|
|
@ -2,73 +2,55 @@
|
|||
// MIT License. See license.txt
|
||||
|
||||
frappe.last_edited_communication = {};
|
||||
frappe.standard_replies = {};
|
||||
frappe.separator_element = '<div>---</div>';
|
||||
const separator_element = '<div>---</div>';
|
||||
|
||||
frappe.views.CommunicationComposer = Class.extend({
|
||||
init: function(opts) {
|
||||
frappe.views.CommunicationComposer = class {
|
||||
constructor(opts) {
|
||||
$.extend(this, opts);
|
||||
if (!this.doc) {
|
||||
this.doc = this.frm && this.frm.doc || {};
|
||||
}
|
||||
|
||||
this.make();
|
||||
},
|
||||
make: function() {
|
||||
var me = this;
|
||||
}
|
||||
|
||||
make() {
|
||||
const me = this;
|
||||
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
title: (this.title || this.subject || __("New Email")),
|
||||
no_submit_on_enter: true,
|
||||
fields: this.get_fields(),
|
||||
primary_action_label: __("Send"),
|
||||
size: 'large',
|
||||
primary_action: function() {
|
||||
me.delete_saved_draft();
|
||||
primary_action() {
|
||||
me.send_action();
|
||||
},
|
||||
secondary_action_label: __("Discard"),
|
||||
secondary_action() {
|
||||
me.dialog.hide();
|
||||
me.clear_cache();
|
||||
},
|
||||
size: 'large',
|
||||
minimizable: true
|
||||
});
|
||||
|
||||
this.dialog.sections[0].wrapper.addClass('to_section');
|
||||
|
||||
['recipients', 'cc', 'bcc'].forEach(field => {
|
||||
this.dialog.fields_dict[field].get_data = function() {
|
||||
const data = me.dialog.fields_dict[field].get_value();
|
||||
const txt = data.match(/[^,\s*]*$/)[0] || '';
|
||||
let options = [];
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.email.get_contact_list",
|
||||
args: {
|
||||
txt: txt,
|
||||
},
|
||||
callback: (r) => {
|
||||
options = r.message;
|
||||
me.dialog.fields_dict[field].set_data(options);
|
||||
}
|
||||
});
|
||||
return options;
|
||||
}
|
||||
});
|
||||
|
||||
this.prepare();
|
||||
this.dialog.show();
|
||||
|
||||
if (this.frm) {
|
||||
$(document).trigger('form-typing', [this.frm]);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.cc || this.bcc) {
|
||||
this.toggle_more_options(true);
|
||||
}
|
||||
},
|
||||
|
||||
get_fields: function() {
|
||||
let contactList = [];
|
||||
let fields = [
|
||||
get_fields() {
|
||||
const fields = [
|
||||
{
|
||||
label: __("To"),
|
||||
fieldtype: "MultiSelect",
|
||||
reqd: 0,
|
||||
fieldname: "recipients",
|
||||
options: contactList
|
||||
},
|
||||
{
|
||||
fieldtype: "Button",
|
||||
|
|
@ -87,13 +69,11 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
label: __("CC"),
|
||||
fieldtype: "MultiSelect",
|
||||
fieldname: "cc",
|
||||
options: contactList
|
||||
},
|
||||
{
|
||||
label: __("BCC"),
|
||||
fieldtype: "MultiSelect",
|
||||
fieldname: "bcc",
|
||||
options: contactList
|
||||
},
|
||||
{
|
||||
label: __("Email Template"),
|
||||
|
|
@ -163,78 +143,83 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
);
|
||||
});
|
||||
|
||||
if (frappe.boot.email_accounts && email_accounts.length > 1) {
|
||||
fields = [
|
||||
{
|
||||
label: __("From"),
|
||||
fieldtype: "Select",
|
||||
reqd: 1,
|
||||
fieldname: "sender",
|
||||
options: email_accounts.map(function(e) {
|
||||
return e.email_id;
|
||||
})
|
||||
}
|
||||
].concat(fields);
|
||||
if (email_accounts.length > 1) {
|
||||
fields.unshift({
|
||||
label: __("From"),
|
||||
fieldtype: "Select",
|
||||
reqd: 1,
|
||||
fieldname: "sender",
|
||||
options: email_accounts.map(function(e) {
|
||||
return e.email_id;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return fields;
|
||||
},
|
||||
}
|
||||
|
||||
toggle_more_options(show_options) {
|
||||
show_options = show_options || this.dialog.fields_dict.more_options.df.hidden;
|
||||
this.dialog.set_df_property('more_options', 'hidden', !show_options);
|
||||
let label = frappe.utils.icon(show_options ? 'up-line': 'down');
|
||||
this.dialog.get_field('option_toggle_button').set_label(label);
|
||||
},
|
||||
|
||||
prepare: function() {
|
||||
const label = frappe.utils.icon(show_options ? 'up-line': 'down');
|
||||
this.dialog.get_field('option_toggle_button').set_label(label);
|
||||
}
|
||||
|
||||
prepare() {
|
||||
this.setup_multiselect_queries();
|
||||
this.setup_subject_and_recipients();
|
||||
this.setup_print_language();
|
||||
this.setup_print();
|
||||
this.setup_attach();
|
||||
this.setup_email();
|
||||
this.setup_last_edited_communication();
|
||||
this.setup_email_template();
|
||||
this.setup_last_edited_communication();
|
||||
this.set_values();
|
||||
}
|
||||
|
||||
this.dialog.set_value("recipients", this.recipients || '');
|
||||
this.dialog.set_value("cc", this.cc || '');
|
||||
this.dialog.set_value("bcc", this.bcc || '');
|
||||
setup_multiselect_queries() {
|
||||
['recipients', 'cc', 'bcc'].forEach(field => {
|
||||
this.dialog.fields_dict[field].get_data = () => {
|
||||
const data = this.dialog.fields_dict[field].get_value();
|
||||
const txt = data.match(/[^,\s*]*$/)[0] || '';
|
||||
|
||||
if(this.dialog.fields_dict.sender) {
|
||||
this.dialog.fields_dict.sender.set_value(this.sender || '');
|
||||
}
|
||||
this.dialog.fields_dict.subject.set_value(
|
||||
frappe.utils.html2text(this.subject) || ''
|
||||
);
|
||||
frappe.call({
|
||||
method: "frappe.email.get_contact_list",
|
||||
args: {txt},
|
||||
callback: (r) => {
|
||||
this.dialog.fields_dict[field].set_data(r.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
this.setup_earlier_reply();
|
||||
},
|
||||
|
||||
setup_subject_and_recipients: function() {
|
||||
setup_subject_and_recipients() {
|
||||
this.subject = this.subject || "";
|
||||
|
||||
if(!this.forward && !this.recipients && this.last_email) {
|
||||
if (!this.forward && !this.recipients && this.last_email) {
|
||||
this.recipients = this.last_email.sender;
|
||||
this.cc = this.last_email.cc;
|
||||
this.bcc = this.last_email.bcc;
|
||||
}
|
||||
|
||||
if(!this.forward && !this.recipients) {
|
||||
if (!this.forward && !this.recipients) {
|
||||
this.recipients = this.frm && this.frm.timeline.get_recipient();
|
||||
}
|
||||
|
||||
if(!this.subject && this.frm) {
|
||||
if (!this.subject && this.frm) {
|
||||
// get subject from last communication
|
||||
var last = this.frm.timeline.get_last_email();
|
||||
const last = this.frm.timeline.get_last_email();
|
||||
|
||||
if(last) {
|
||||
if (last) {
|
||||
this.subject = last.subject;
|
||||
if(!this.recipients) {
|
||||
if (!this.recipients) {
|
||||
this.recipients = last.sender;
|
||||
}
|
||||
|
||||
// prepend "Re:"
|
||||
if(strip(this.subject.toLowerCase().split(":")[0])!="re") {
|
||||
if (strip(this.subject.toLowerCase().split(":")[0])!="re") {
|
||||
this.subject = __("Re: {0}", [this.subject]);
|
||||
}
|
||||
}
|
||||
|
|
@ -251,7 +236,7 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
// always add an identifier to catch a reply
|
||||
// some email clients (outlook) may not send the message id to identify
|
||||
// the thread. So as a backup we use the name of the document as identifier
|
||||
let identifier = `#${this.frm.doc.name}`;
|
||||
const identifier = `#${this.frm.doc.name}`;
|
||||
if (!this.subject.includes(identifier)) {
|
||||
this.subject = `${this.subject} (${identifier})`;
|
||||
}
|
||||
|
|
@ -260,33 +245,25 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
if (this.frm && !this.recipients) {
|
||||
this.recipients = this.frm.doc[this.frm.email_field];
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
setup_email_template: function() {
|
||||
var me = this;
|
||||
setup_email_template() {
|
||||
const me = this;
|
||||
|
||||
this.dialog.fields_dict["email_template"].df.onchange = () => {
|
||||
var email_template = me.dialog.fields_dict.email_template.get_value();
|
||||
const email_template = me.dialog.fields_dict.email_template.get_value();
|
||||
if (!email_template) return;
|
||||
|
||||
var prepend_reply = function(reply) {
|
||||
if(me.reply_added===email_template) {
|
||||
return;
|
||||
}
|
||||
var content_field = me.dialog.fields_dict.content;
|
||||
var subject_field = me.dialog.fields_dict.subject;
|
||||
var content = content_field.get_value() || "";
|
||||
var subject = subject_field.get_value() || "";
|
||||
function prepend_reply(reply) {
|
||||
if (me.reply_added === email_template) return;
|
||||
|
||||
var parts = content.split('<!-- salutation-ends -->');
|
||||
const content_field = me.dialog.fields_dict.content;
|
||||
const subject_field = me.dialog.fields_dict.subject;
|
||||
|
||||
if(parts.length===2) {
|
||||
content = [reply.message, "<br>", parts[1]];
|
||||
} else {
|
||||
content = [reply.message, "<br>", content];
|
||||
}
|
||||
|
||||
content_field.set_value(content.join(''));
|
||||
let content = content_field.get_value() || "";
|
||||
content = content.split('<!-- salutation-ends -->')[1] || content;
|
||||
|
||||
content_field.set_value(`${reply.message}<br>${content}`);
|
||||
subject_field.set_value(reply.subject);
|
||||
|
||||
me.reply_added = email_template;
|
||||
|
|
@ -296,86 +273,107 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
method: 'frappe.email.doctype.email_template.email_template.get_email_template',
|
||||
args: {
|
||||
template_name: email_template,
|
||||
doc: me.frm.doc,
|
||||
doc: me.doc,
|
||||
_lang: me.dialog.get_value("language_sel")
|
||||
},
|
||||
callback: function(r) {
|
||||
callback(r) {
|
||||
prepend_reply(r.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
setup_last_edited_communication: function() {
|
||||
var me = this;
|
||||
if (!this.doc){
|
||||
if (cur_frm){
|
||||
this.doc = cur_frm.doctype;
|
||||
}else{
|
||||
this.doc = "Inbox";
|
||||
}
|
||||
}
|
||||
if (cur_frm && cur_frm.docname) {
|
||||
this.key = cur_frm.docname;
|
||||
setup_last_edited_communication() {
|
||||
if (this.frm) {
|
||||
this.doctype = this.frm.doctype;
|
||||
this.key = this.frm.docname;
|
||||
} else {
|
||||
this.key = "Inbox";
|
||||
this.doctype = this.key = "Inbox";
|
||||
}
|
||||
if(this.last_email) {
|
||||
|
||||
if (this.last_email) {
|
||||
this.key = this.key + ":" + this.last_email.name;
|
||||
}
|
||||
if(this.subject){
|
||||
|
||||
if (this.subject) {
|
||||
this.key = this.key + ":" + this.subject;
|
||||
}
|
||||
this.dialog.onhide = function() {
|
||||
var last_edited_communication = me.get_last_edited_communication();
|
||||
$.extend(last_edited_communication, {
|
||||
sender: me.dialog.get_value("sender"),
|
||||
recipients: me.dialog.get_value("recipients"),
|
||||
cc: me.dialog.get_value("cc"),
|
||||
bcc: me.dialog.get_value("bcc"),
|
||||
subject: me.dialog.get_value("subject"),
|
||||
content: me.dialog.get_value("content"),
|
||||
});
|
||||
|
||||
if (me.frm) {
|
||||
$(document).trigger("form-stopped-typing", [me.frm]);
|
||||
this.dialog.on_hide = () => {
|
||||
$.extend(
|
||||
this.get_last_edited_communication(true),
|
||||
this.dialog.get_values(true)
|
||||
);
|
||||
|
||||
if (this.frm) {
|
||||
$(document).trigger("form-stopped-typing", [this.frm]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get_last_edited_communication(clear) {
|
||||
if (!frappe.last_edited_communication[this.doctype]) {
|
||||
frappe.last_edited_communication[this.doctype] = {};
|
||||
}
|
||||
|
||||
if (clear || !frappe.last_edited_communication[this.doctype][this.key]) {
|
||||
frappe.last_edited_communication[this.doctype][this.key] = {};
|
||||
}
|
||||
|
||||
return frappe.last_edited_communication[this.doctype][this.key];
|
||||
}
|
||||
|
||||
async set_values() {
|
||||
for (const fieldname of ["recipients", "cc", "bcc", "sender"]) {
|
||||
await this.dialog.set_value(fieldname, this[fieldname] || "");
|
||||
}
|
||||
|
||||
const subject = frappe.utils.html2text(this.subject) || '';
|
||||
await this.dialog.set_value("subject", subject);
|
||||
|
||||
await this.set_values_from_last_edited_communication();
|
||||
await this.set_content();
|
||||
|
||||
// set default email template for the first email in a document
|
||||
if (this.frm && !this.is_a_reply && !this.content_set) {
|
||||
const email_template = this.frm.meta.default_email_template || '';
|
||||
await this.dialog.set_value("email_template", email_template);
|
||||
}
|
||||
|
||||
for (const fieldname of ['email_template', 'cc', 'bcc']) {
|
||||
if (this.dialog.get_value(fieldname)) {
|
||||
this.toggle_more_options(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.dialog.on_page_show = function() {
|
||||
if (!me.txt) {
|
||||
var last_edited_communication = me.get_last_edited_communication();
|
||||
if(last_edited_communication.content) {
|
||||
me.dialog.set_value("sender", last_edited_communication.sender || "");
|
||||
me.dialog.set_value("subject", last_edited_communication.subject || "");
|
||||
me.dialog.set_value("recipients", last_edited_communication.recipients || "");
|
||||
me.dialog.set_value("cc", last_edited_communication.cc || "");
|
||||
me.dialog.set_value("bcc", last_edited_communication.bcc || "");
|
||||
me.dialog.set_value("content", last_edited_communication.content || "");
|
||||
}
|
||||
}
|
||||
async set_values_from_last_edited_communication() {
|
||||
if (this.txt) return;
|
||||
|
||||
const last_edited = this.get_last_edited_communication();
|
||||
if (!last_edited.content) return;
|
||||
|
||||
// prevent re-triggering of email template
|
||||
if (last_edited.email_template) {
|
||||
const template_field = this.dialog.fields_dict.email_template;
|
||||
await template_field.set_model_value(last_edited.email_template);
|
||||
delete last_edited.email_template;
|
||||
}
|
||||
|
||||
},
|
||||
await this.dialog.set_values(last_edited);
|
||||
this.content_set = true;
|
||||
}
|
||||
|
||||
get_last_edited_communication: function() {
|
||||
if (!frappe.last_edited_communication[this.doc]) {
|
||||
frappe.last_edited_communication[this.doc] = {};
|
||||
}
|
||||
selected_format() {
|
||||
return (
|
||||
this.dialog.fields_dict.select_print_format.input.value
|
||||
|| this.frm && this.frm.meta.default_print_format
|
||||
|| "Standard"
|
||||
);
|
||||
}
|
||||
|
||||
if(!frappe.last_edited_communication[this.doc][this.key]) {
|
||||
frappe.last_edited_communication[this.doc][this.key] = {};
|
||||
}
|
||||
|
||||
return frappe.last_edited_communication[this.doc][this.key];
|
||||
},
|
||||
|
||||
selected_format: function() {
|
||||
return this.dialog.fields_dict.select_print_format.input.value || (this.frm && this.frm.meta.default_print_format) || "Standard";
|
||||
},
|
||||
|
||||
get_print_format: function(format) {
|
||||
get_print_format(format) {
|
||||
if (!format) {
|
||||
format = this.selected_format();
|
||||
}
|
||||
|
|
@ -385,21 +383,18 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
setup_print_language: function() {
|
||||
var doc = this.doc || cur_frm.doc;
|
||||
var fields = this.dialog.fields_dict;
|
||||
setup_print_language() {
|
||||
const fields = this.dialog.fields_dict;
|
||||
|
||||
//Load default print language from doctype
|
||||
this.lang_code = doc.language
|
||||
|
||||
if (!this.lang_code && this.get_print_format().default_print_language) {
|
||||
this.lang_code = this.get_print_format().default_print_language;
|
||||
}
|
||||
this.lang_code = this.doc.language
|
||||
|| this.get_print_format().default_print_language
|
||||
|| frappe.boot.lang;
|
||||
|
||||
//On selection of language retrieve language code
|
||||
var me = this;
|
||||
const me = this;
|
||||
$(fields.language_sel.input).change(function(){
|
||||
me.lang_code = this.value
|
||||
})
|
||||
|
|
@ -412,11 +407,11 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
if (this.lang_code) {
|
||||
$(fields.language_sel.input).val(this.lang_code);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
setup_print: function() {
|
||||
setup_print() {
|
||||
// print formats
|
||||
var fields = this.dialog.fields_dict;
|
||||
const fields = this.dialog.fields_dict;
|
||||
|
||||
// toggle print format
|
||||
$(fields.attach_document_print.input).click(function() {
|
||||
|
|
@ -426,8 +421,8 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
// select print format
|
||||
$(fields.select_print_format.wrapper).toggle(false);
|
||||
|
||||
if (cur_frm) {
|
||||
const print_formats = frappe.meta.get_print_formats(cur_frm.meta.name);
|
||||
if (this.frm) {
|
||||
const print_formats = frappe.meta.get_print_formats(this.frm.meta.name);
|
||||
$(fields.select_print_format.input)
|
||||
.empty()
|
||||
.add_options(print_formats)
|
||||
|
|
@ -436,11 +431,11 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
$(fields.attach_document_print.wrapper).toggle(false);
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
setup_attach: function() {
|
||||
var fields = this.dialog.fields_dict;
|
||||
var attach = $(fields.select_attachments.wrapper);
|
||||
setup_attach() {
|
||||
const fields = this.dialog.fields_dict;
|
||||
const attach = $(fields.select_attachments.wrapper);
|
||||
|
||||
if (!this.attachments) {
|
||||
this.attachments = [];
|
||||
|
|
@ -483,9 +478,9 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
.find(".add-more-attachments button")
|
||||
.on('click', () => new frappe.ui.FileUploader(args));
|
||||
this.render_attachment_rows();
|
||||
},
|
||||
}
|
||||
|
||||
render_attachment_rows: function(attachment) {
|
||||
render_attachment_rows(attachment) {
|
||||
const select_attachments = this.dialog.fields_dict.select_attachments;
|
||||
const attachment_rows = $(select_attachments.wrapper).find(".attach-list");
|
||||
if (attachment) {
|
||||
|
|
@ -509,7 +504,7 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
get_attachment_row(attachment, checked) {
|
||||
return $(`<p class="checkbox flex">
|
||||
|
|
@ -526,56 +521,55 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
${frappe.utils.icon('link-url')}
|
||||
</a>
|
||||
</p>`);
|
||||
},
|
||||
}
|
||||
|
||||
setup_email: function() {
|
||||
setup_email() {
|
||||
// email
|
||||
var fields = this.dialog.fields_dict;
|
||||
const fields = this.dialog.fields_dict;
|
||||
|
||||
if(this.attach_document_print) {
|
||||
if (this.attach_document_print) {
|
||||
$(fields.attach_document_print.input).click();
|
||||
$(fields.select_print_format.wrapper).toggle(true);
|
||||
}
|
||||
|
||||
$(fields.send_me_a_copy.input).on('click', () => {
|
||||
// update send me a copy (make it sticky)
|
||||
let val = fields.send_me_a_copy.get_value();
|
||||
const val = fields.send_me_a_copy.get_value();
|
||||
frappe.db.set_value('User', frappe.session.user, 'send_me_a_copy', val);
|
||||
frappe.boot.user.send_me_a_copy = val;
|
||||
});
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
send_action: function() {
|
||||
var me = this;
|
||||
var btn = me.dialog.get_primary_btn();
|
||||
send_action() {
|
||||
const me = this;
|
||||
const btn = me.dialog.get_primary_btn();
|
||||
const form_values = this.get_values();
|
||||
if (!form_values) return;
|
||||
|
||||
var form_values = this.get_values();
|
||||
if(!form_values) return;
|
||||
|
||||
var selected_attachments =
|
||||
const selected_attachments =
|
||||
$.map($(me.dialog.wrapper).find("[data-file-name]:checked"), function (element) {
|
||||
return $(element).attr("data-file-name");
|
||||
});
|
||||
|
||||
|
||||
if(form_values.attach_document_print) {
|
||||
if (form_values.attach_document_print) {
|
||||
me.send_email(btn, form_values, selected_attachments, null, form_values.select_print_format || "");
|
||||
} else {
|
||||
me.send_email(btn, form_values, selected_attachments);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
get_values: function() {
|
||||
var form_values = this.dialog.get_values();
|
||||
get_values() {
|
||||
const form_values = this.dialog.get_values();
|
||||
|
||||
// cc
|
||||
for ( var i=0, l=this.dialog.fields.length; i < l; i++ ) {
|
||||
var df = this.dialog.fields[i];
|
||||
for (let i = 0, l = this.dialog.fields.length; i < l; i++) {
|
||||
const df = this.dialog.fields[i];
|
||||
|
||||
if ( df.is_cc_checkbox ) {
|
||||
if (df.is_cc_checkbox) {
|
||||
// concat in cc
|
||||
if ( form_values[df.fieldname] ) {
|
||||
if (form_values[df.fieldname]) {
|
||||
form_values.cc = ( form_values.cc ? (form_values.cc + ", ") : "" ) + df.fieldname;
|
||||
form_values.bcc = ( form_values.bcc ? (form_values.bcc + ", ") : "" ) + df.fieldname;
|
||||
}
|
||||
|
|
@ -585,22 +579,27 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
}
|
||||
|
||||
return form_values;
|
||||
},
|
||||
}
|
||||
|
||||
save_as_draft: function() {
|
||||
save_as_draft() {
|
||||
if (this.dialog && this.frm) {
|
||||
let message = this.dialog.get_value('content');
|
||||
message = message.split(frappe.separator_element)[0];
|
||||
message = message.split(separator_element)[0];
|
||||
localforage.setItem(this.frm.doctype + this.frm.docname, message).catch(e => {
|
||||
if (e) {
|
||||
// silently fail
|
||||
console.log(e); // eslint-disable-line
|
||||
console.warn('[Communication] localStorage is full. Cannot save message as draft'); // eslint-disable-line
|
||||
console.warn('[Communication] IndexedDB is full. Cannot save message as draft'); // eslint-disable-line
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
clear_cache() {
|
||||
this.delete_saved_draft();
|
||||
this.get_last_edited_communication(true);
|
||||
}
|
||||
|
||||
delete_saved_draft() {
|
||||
if (this.dialog && this.frm) {
|
||||
|
|
@ -608,28 +607,28 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
if (e) {
|
||||
// silently fail
|
||||
console.log(e); // eslint-disable-line
|
||||
console.warn('[Communication] localStorage is full. Cannot save message as draft'); // eslint-disable-line
|
||||
console.warn('[Communication] IndexedDB is full. Cannot save message as draft'); // eslint-disable-line
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
send_email: function(btn, form_values, selected_attachments, print_html, print_format) {
|
||||
var me = this;
|
||||
me.dialog.hide();
|
||||
send_email(btn, form_values, selected_attachments, print_html, print_format) {
|
||||
const me = this;
|
||||
this.dialog.hide();
|
||||
|
||||
if(!form_values.recipients) {
|
||||
if (!form_values.recipients) {
|
||||
frappe.msgprint(__("Enter Email Recipient(s)"));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!form_values.attach_document_print) {
|
||||
if (!form_values.attach_document_print) {
|
||||
print_html = null;
|
||||
print_format = null;
|
||||
}
|
||||
|
||||
|
||||
if(cur_frm && !frappe.model.can_email(me.doc.doctype, cur_frm)) {
|
||||
if (this.frm && !frappe.model.can_email(this.doc.doctype, this.frm)) {
|
||||
frappe.msgprint(__("You are not allowed to send emails related to this document"));
|
||||
return;
|
||||
}
|
||||
|
|
@ -650,28 +649,29 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
send_me_a_copy: form_values.send_me_a_copy,
|
||||
print_format: print_format,
|
||||
sender: form_values.sender,
|
||||
sender_full_name: form_values.sender?frappe.user.full_name():undefined,
|
||||
sender_full_name: form_values.sender
|
||||
? frappe.user.full_name()
|
||||
: undefined,
|
||||
email_template: form_values.email_template,
|
||||
attachments: selected_attachments,
|
||||
_lang : me.lang_code,
|
||||
read_receipt:form_values.send_read_receipt,
|
||||
print_letterhead: me.is_print_letterhead_checked(),
|
||||
},
|
||||
btn: btn,
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
btn,
|
||||
callback(r) {
|
||||
if (!r.exc) {
|
||||
frappe.utils.play_sound("email");
|
||||
|
||||
if(r.message["emails_not_sent_to"]) {
|
||||
if (r.message["emails_not_sent_to"]) {
|
||||
frappe.msgprint(__("Email not sent to {0} (unsubscribed / disabled)",
|
||||
[ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) );
|
||||
}
|
||||
|
||||
if ((frappe.last_edited_communication[me.doc] || {})[me.key]) {
|
||||
delete frappe.last_edited_communication[me.doc][me.key];
|
||||
}
|
||||
if (cur_frm) {
|
||||
cur_frm.reload_doc();
|
||||
me.clear_cache();
|
||||
|
||||
if (me.frm) {
|
||||
me.frm.reload_doc();
|
||||
}
|
||||
|
||||
// try the success callback if it exists
|
||||
|
|
@ -679,7 +679,7 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
try {
|
||||
me.success(r);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e); // eslint-disable-line
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -691,113 +691,115 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
try {
|
||||
me.error(r);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e); // eslint-disable-line
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
is_print_letterhead_checked: function() {
|
||||
is_print_letterhead_checked() {
|
||||
if (this.frm && $(this.frm.wrapper).find('.form-print-wrapper').is(':visible')){
|
||||
return $(this.frm.wrapper).find('.print-letterhead').prop('checked') ? 1 : 0;
|
||||
} else {
|
||||
return (frappe.model.get_doc(":Print Settings", "Print Settings") ||
|
||||
{ with_letterhead: 1 }).with_letterhead ? 1 : 0;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
get_default_outgoing_email_account_signature: function() {
|
||||
return frappe.db.get_value('Email Account', { 'default_outgoing': 1, 'add_signature': 1 }, 'signature');
|
||||
},
|
||||
async set_content() {
|
||||
if (this.content_set) return;
|
||||
|
||||
setup_earlier_reply: async function() {
|
||||
let fields = this.dialog.fields_dict;
|
||||
let signature = frappe.boot.user.email_signature || "";
|
||||
|
||||
if (!signature) {
|
||||
const res = await this.get_default_outgoing_email_account_signature();
|
||||
signature = "<!-- signature-included -->" + res.message.signature;
|
||||
let message = this.txt || "";
|
||||
if (!message && this.frm) {
|
||||
const { doctype, docname } = this.frm;
|
||||
message = await localforage.getItem(doctype + docname) || "";
|
||||
}
|
||||
|
||||
if (signature && !frappe.utils.is_html(signature)) {
|
||||
signature = signature.replace(/\n/g, "<br>");
|
||||
if (message) {
|
||||
this.content_set = true;
|
||||
}
|
||||
|
||||
if(this.txt) {
|
||||
this.message = this.txt + (this.message ? ("<br><br>" + this.message) : "");
|
||||
} else {
|
||||
// saved draft in localStorage
|
||||
const { doctype, docname } = this.frm || {};
|
||||
if (doctype && docname) {
|
||||
this.message = await localforage.getItem(doctype + docname) || '';
|
||||
}
|
||||
}
|
||||
|
||||
if(this.real_name) {
|
||||
this.message = '<p>'+__('Dear') +' '
|
||||
+ this.real_name + ",</p><!-- salutation-ends --><br>" + (this.message || "");
|
||||
}
|
||||
|
||||
if(this.message && signature && this.message.includes(signature)) {
|
||||
signature = "";
|
||||
}
|
||||
|
||||
let reply = (this.message || "") + (signature ? ("<br>" + signature) : "");
|
||||
let content = '';
|
||||
|
||||
if (this.is_a_reply === 'undefined') {
|
||||
this.is_a_reply = true;
|
||||
message += await this.get_signature();
|
||||
if (this.real_name && !message.includes("<!-- salutation-ends -->")) {
|
||||
message = `<p>${__('Dear')} ${this.real_name},</p>
|
||||
<!-- salutation-ends --><br>${message}`;
|
||||
}
|
||||
|
||||
if (this.is_a_reply) {
|
||||
let last_email = this.last_email;
|
||||
|
||||
if (!last_email) {
|
||||
last_email = this.frm && this.frm.timeline.get_last_email(true);
|
||||
}
|
||||
|
||||
if (!last_email) return;
|
||||
|
||||
let last_email_content = last_email.original_comment || last_email.content;
|
||||
|
||||
// convert the email context to text as we are enclosing
|
||||
// this inside <blockquote>
|
||||
last_email_content = this.html2text(last_email_content).replace(/\n/g, '<br>');
|
||||
|
||||
// clip last email for a maximum of 20k characters
|
||||
// to prevent the email content from getting too large
|
||||
if (last_email_content.length > 20 * 1024) {
|
||||
last_email_content += '<div>' + __('Message clipped') + '</div>' + last_email_content;
|
||||
last_email_content = last_email_content.slice(0, 20 * 1024);
|
||||
}
|
||||
|
||||
let communication_date = last_email.communication_date || last_email.creation;
|
||||
content = `
|
||||
${reply}
|
||||
<div><br></div>
|
||||
${frappe.separator_element || ''}
|
||||
<p>${__("On {0}, {1} wrote:", [frappe.datetime.global_date_format(communication_date) , last_email.sender])}</p>
|
||||
<blockquote>
|
||||
${last_email_content}
|
||||
</blockquote>
|
||||
`;
|
||||
} else {
|
||||
content = reply;
|
||||
message += this.get_earlier_reply();
|
||||
}
|
||||
fields.content.set_value(content);
|
||||
},
|
||||
|
||||
html2text: function(html) {
|
||||
await this.dialog.set_value("content", message);
|
||||
}
|
||||
|
||||
async get_signature() {
|
||||
let signature = frappe.boot.user.email_signature;
|
||||
|
||||
if (!signature) {
|
||||
const response = await frappe.db.get_value(
|
||||
'Email Account',
|
||||
{'default_outgoing': 1, 'add_signature': 1},
|
||||
'signature'
|
||||
);
|
||||
|
||||
signature = response.message.signature;
|
||||
}
|
||||
|
||||
if (!signature) return "";
|
||||
|
||||
if (!frappe.utils.is_html(signature)) {
|
||||
signature = signature.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
return "<br>" + signature;
|
||||
}
|
||||
|
||||
get_earlier_reply() {
|
||||
const last_email = (
|
||||
this.last_email
|
||||
|| this.frm && this.frm.timeline.get_last_email(true)
|
||||
);
|
||||
|
||||
if (!last_email) return "";
|
||||
let last_email_content = last_email.original_comment || last_email.content;
|
||||
|
||||
// convert the email context to text as we are enclosing
|
||||
// this inside <blockquote>
|
||||
last_email_content = this.html2text(last_email_content).replace(/\n/g, '<br>');
|
||||
|
||||
// clip last email for a maximum of 20k characters
|
||||
// to prevent the email content from getting too large
|
||||
if (last_email_content.length > 20 * 1024) {
|
||||
last_email_content += '<div>' + __('Message clipped') + '</div>' + last_email_content;
|
||||
last_email_content = last_email_content.slice(0, 20 * 1024);
|
||||
}
|
||||
|
||||
const communication_date = frappe.datetime.global_date_format(
|
||||
last_email.communication_date || last_email.creation
|
||||
);
|
||||
|
||||
return `
|
||||
<div><br></div>
|
||||
${separator_element || ''}
|
||||
<p>
|
||||
${__("On {0}, {1} wrote:", [communication_date, last_email.sender])}
|
||||
</p>
|
||||
<blockquote>
|
||||
${last_email_content}
|
||||
</blockquote>
|
||||
`;
|
||||
}
|
||||
|
||||
html2text(html) {
|
||||
// convert HTML to text and try and preserve whitespace
|
||||
var d = document.createElement( 'div' );
|
||||
const d = document.createElement( 'div' );
|
||||
d.innerHTML = html.replace(/<\/div>/g, '<br></div>') // replace end of blocks
|
||||
.replace(/<\/p>/g, '<br></p>') // replace end of paragraphs
|
||||
.replace(/<br>/g, '\n');
|
||||
let text = d.textContent;
|
||||
|
||||
// replace multiple empty lines with just one
|
||||
return text.replace(/\n{3,}/g, '\n\n');
|
||||
return d.textContent.replace(/\n{3,}/g, '\n\n');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -204,9 +204,7 @@ frappe.views.InboxView = class InboxView extends frappe.views.ListView {
|
|||
};
|
||||
frappe.new_doc('Email Account');
|
||||
} else {
|
||||
new frappe.views.CommunicationComposer({
|
||||
doc: {}
|
||||
});
|
||||
new frappe.views.CommunicationComposer();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -306,6 +306,7 @@ frappe.provide("frappe.views");
|
|||
store.on('change:cur_list', setup_restore_columns);
|
||||
store.on('change:columns', setup_restore_columns);
|
||||
store.on('change:empty_state', show_empty_state);
|
||||
fluxify.doAction('update_order');
|
||||
}
|
||||
|
||||
function prepare() {
|
||||
|
|
|
|||
|
|
@ -169,6 +169,9 @@
|
|||
// Other Colors
|
||||
--sidebar-select-color: var(--gray-200);
|
||||
|
||||
--scrollbar-thumb-color: var(--gray-400);
|
||||
--scrollbar-track-color: var(--gray-200);
|
||||
|
||||
--shadow-inset: inset 0px -1px 0px var(--gray-300);
|
||||
--border-color: var(--gray-100);
|
||||
--dark-border-color: var(--gray-300);
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@
|
|||
|
||||
--sidebar-select-color: var(--gray-800);
|
||||
|
||||
--scrollbar-thumb-color: var(--gray-600);
|
||||
--scrollbar-track-color: var(--gray-700);
|
||||
|
||||
--shadow-inset: var(--fg-color);
|
||||
--border-color: var(--gray-700);
|
||||
--dark-border-color: var(--gray-600);
|
||||
|
|
@ -75,6 +78,8 @@
|
|||
// input
|
||||
--input-disabled-bg: none;
|
||||
|
||||
color-scheme: dark;
|
||||
|
||||
.frappe-card {
|
||||
.btn-default {
|
||||
background-color: var(--bg-color);
|
||||
|
|
|
|||
|
|
@ -754,7 +754,28 @@ body {
|
|||
.layout-side-section, .layout-main-section-wrapper {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-right: 25px;
|
||||
scrollbar-color: var(--gray-200) transparent;
|
||||
[data-theme="dark"] & {
|
||||
scrollbar-color: var(--gray-800) transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--gray-200);
|
||||
[data-theme="dark"] & {
|
||||
background: var(--gray-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-side-section {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.desk-sidebar {
|
||||
margin-bottom: var(--margin-2xl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
@import "mobile";
|
||||
@import "form";
|
||||
@import "print_preview";
|
||||
@import "scrollbar";
|
||||
@import "navbar";
|
||||
@import "../common/modal";
|
||||
@import "slides";
|
||||
|
|
|
|||
29
frappe/public/scss/desk/scrollbar.scss
Normal file
29
frappe/public/scss/desk/scrollbar.scss
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* Works on Firefox */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color);
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
|
||||
/* Works on Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb-color);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track,
|
||||
*::-webkit-scrollbar-corner {
|
||||
background: var(--scrollbar-track-color);
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
width: unset;
|
||||
height: unset;
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
window.is_chat_enabled = {{ chat_enable }};
|
||||
</script>
|
||||
</head>
|
||||
<body frappe-session-status="{{ 'logged-in' if frappe.session.user != 'Guest' else 'logged-out'}}" data-path="{{ path | e }}" {%- if template and template.endswith('.md') %} frappe-content-type="markdown" {% endif -%} class="{{ body_class or ''}}">
|
||||
<body frappe-session-status="{{ 'logged-in' if frappe.session.user != 'Guest' else 'logged-out'}}" data-path="{{ path | e }}" {%- if template and template.endswith('.md') %} frappe-content-type="markdown" {%- endif %} class="{{ body_class or ''}}">
|
||||
{% include "public/icons/timeless/symbol-defs.svg" %}
|
||||
{%- block banner -%}
|
||||
{% include "templates/includes/banner_extension.html" ignore missing %}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ def get_dict(fortype, name=None):
|
|||
messages.extend(get_server_messages(app))
|
||||
messages = deduplicate_messages(messages)
|
||||
|
||||
messages += frappe.db.sql("""select "navbar", item_label from `tabNavbar Item` where item_label is not null""")
|
||||
messages += frappe.db.sql("""select 'navbar', item_label from `tabNavbar Item` where item_label is not null""")
|
||||
messages = get_messages_from_include_files()
|
||||
messages += frappe.db.sql("select 'Print Format:', name from `tabPrint Format`")
|
||||
messages += frappe.db.sql("select 'DocType:', name from tabDocType")
|
||||
|
|
@ -518,8 +518,13 @@ def get_messages_from_file(path):
|
|||
apps_path = get_bench_dir()
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as sourcefile:
|
||||
try:
|
||||
file_contents = sourcefile.read()
|
||||
except Exception:
|
||||
print("Could not scan file for translation: {0}".format(path))
|
||||
return []
|
||||
data = [(os.path.relpath(path, apps_path), message, context, line) \
|
||||
for line, message, context in extract_messages_from_code(sourcefile.read())]
|
||||
for line, message, context in extract_messages_from_code(file_contents)]
|
||||
return data
|
||||
else:
|
||||
# print "Translate: {0} missing".format(os.path.abspath(path))
|
||||
|
|
@ -601,11 +606,23 @@ def write_csv_file(path, app_messages, lang_dict):
|
|||
from csv import writer
|
||||
with open(path, 'w', newline='') as msgfile:
|
||||
w = writer(msgfile, lineterminator='\n')
|
||||
for p, m in app_messages:
|
||||
t = lang_dict.get(m, '')
|
||||
|
||||
for app_message in app_messages:
|
||||
context = None
|
||||
if len(app_message) == 2:
|
||||
path, message = app_message
|
||||
elif len(app_message) == 3:
|
||||
path, message, lineno = app_message
|
||||
elif len(app_message) == 4:
|
||||
path, message, context, lineno = app_message
|
||||
else:
|
||||
continue
|
||||
|
||||
t = lang_dict.get(message, '')
|
||||
# strip whitespaces
|
||||
t = re.sub('{\s?([0-9]+)\s?}', "{\g<1>}", t)
|
||||
w.writerow([p if p else '', m, t])
|
||||
translated_string = re.sub('{\s?([0-9]+)\s?}', "{\g<1>}", t)
|
||||
if translated_string:
|
||||
w.writerow([message, translated_string, context])
|
||||
|
||||
def get_untranslated(lang, untranslated_file, get_all=False):
|
||||
"""Returns all untranslated strings for a language and writes in a file
|
||||
|
|
@ -827,7 +844,7 @@ def get_all_languages(with_language_name=False):
|
|||
return frappe.db.sql_list('select name from tabLanguage')
|
||||
|
||||
def get_all_language_with_name():
|
||||
return frappe.db.get_all('language', ['language_code', 'language_name'])
|
||||
return frappe.db.get_all('Language', ['language_code', 'language_name'])
|
||||
|
||||
if not frappe.db:
|
||||
frappe.connect()
|
||||
|
|
|
|||
|
|
@ -307,14 +307,23 @@ def unesc(s, esc_chars):
|
|||
s = s.replace(esc_str, c)
|
||||
return s
|
||||
|
||||
def execute_in_shell(cmd, verbose=0):
|
||||
def execute_in_shell(cmd, verbose=0, low_priority=False):
|
||||
# using Popen instead of os.system - as recommended by python docs
|
||||
import tempfile
|
||||
from subprocess import Popen
|
||||
|
||||
with tempfile.TemporaryFile() as stdout:
|
||||
with tempfile.TemporaryFile() as stderr:
|
||||
p = Popen(cmd, shell=True, stdout=stdout, stderr=stderr)
|
||||
kwargs = {
|
||||
"shell": True,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr
|
||||
}
|
||||
|
||||
if low_priority:
|
||||
kwargs["preexec_fn"] = lambda: os.nice(10)
|
||||
|
||||
p = Popen(cmd, **kwargs)
|
||||
p.wait()
|
||||
|
||||
stdout.seek(0)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import click
|
|||
# imports - module imports
|
||||
import frappe
|
||||
from frappe import _, conf
|
||||
from frappe.utils import get_file_size, get_url, now, now_datetime
|
||||
from frappe.utils import get_file_size, get_url, now, now_datetime, cint
|
||||
|
||||
# backup variable for backwards compatibility
|
||||
verbose = False
|
||||
|
|
@ -315,8 +315,6 @@ class BackupGenerator:
|
|||
print(template.format(_type.title(), info["path"], info["size"]))
|
||||
|
||||
def backup_files(self):
|
||||
import subprocess
|
||||
|
||||
for folder in ("public", "private"):
|
||||
files_path = frappe.get_site_path(folder, "files")
|
||||
backup_path = (
|
||||
|
|
@ -327,12 +325,12 @@ class BackupGenerator:
|
|||
cmd_string = "tar cf - {1} | gzip > {0}"
|
||||
else:
|
||||
cmd_string = "tar -cf {0} {1}"
|
||||
output = subprocess.check_output(
|
||||
cmd_string.format(backup_path, files_path), shell=True
|
||||
)
|
||||
|
||||
if self.verbose and output:
|
||||
print(output.decode("utf8"))
|
||||
frappe.utils.execute_in_shell(
|
||||
cmd_string.format(backup_path, files_path),
|
||||
verbose=self.verbose,
|
||||
low_priority=True
|
||||
)
|
||||
|
||||
def copy_site_config(self):
|
||||
site_config_backup_path = self.backup_path_conf
|
||||
|
|
@ -436,7 +434,7 @@ class BackupGenerator:
|
|||
if self.verbose:
|
||||
print(command + "\n")
|
||||
|
||||
err, out = frappe.utils.execute_in_shell(command)
|
||||
frappe.utils.execute_in_shell(command, low_priority=True)
|
||||
|
||||
def send_email(self):
|
||||
"""
|
||||
|
|
@ -474,29 +472,6 @@ download only after 24 hours.""" % {
|
|||
return recipient_list
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_backup():
|
||||
"""
|
||||
This function is executed when the user clicks on
|
||||
Toos > Download Backup
|
||||
"""
|
||||
delete_temp_backups()
|
||||
odb = BackupGenerator(
|
||||
frappe.conf.db_name,
|
||||
frappe.conf.db_name,
|
||||
frappe.conf.db_password,
|
||||
db_host=frappe.db.host,
|
||||
db_type=frappe.conf.db_type,
|
||||
db_port=frappe.conf.db_port,
|
||||
)
|
||||
odb.get_backup()
|
||||
recipient_list = odb.send_email()
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Download link for your backup will be emailed on the following email address: {0}"
|
||||
).format(", ".join(recipient_list))
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def fetch_latest_backups(partial=False):
|
||||
"""Fetches paths of the latest backup taken in the last 30 days
|
||||
|
|
@ -570,7 +545,7 @@ def new_backup(
|
|||
force=False,
|
||||
verbose=False,
|
||||
):
|
||||
delete_temp_backups(older_than=frappe.conf.keep_backups_for_hours or 24)
|
||||
delete_temp_backups()
|
||||
odb = BackupGenerator(
|
||||
frappe.conf.db_name,
|
||||
frappe.conf.db_name,
|
||||
|
|
@ -595,8 +570,9 @@ def new_backup(
|
|||
|
||||
def delete_temp_backups(older_than=24):
|
||||
"""
|
||||
Cleans up the backup_link_path directory by deleting files older than 24 hours
|
||||
Cleans up the backup_link_path directory by deleting older files
|
||||
"""
|
||||
older_than = cint(frappe.conf.keep_backups_for_hours) or older_than
|
||||
backup_path = get_backup_path()
|
||||
if os.path.exists(backup_path):
|
||||
file_list = os.listdir(get_backup_path())
|
||||
|
|
|
|||
|
|
@ -64,8 +64,6 @@ def get_oauth2_authorize_url(provider, redirect_to):
|
|||
|
||||
state = { "site": frappe.utils.get_url(), "token": frappe.generate_hash(), "redirect_to": redirect_to }
|
||||
|
||||
frappe.cache().set_value("{0}:{1}".format(provider, state["token"]), True, expires_in_sec=120)
|
||||
|
||||
# relative to absolute url
|
||||
data = {
|
||||
"redirect_uri": get_redirect_uri(provider),
|
||||
|
|
@ -176,11 +174,6 @@ def login_oauth_user(data=None, provider=None, state=None, email_id=None, key=No
|
|||
frappe.respond_as_web_page(_("Invalid Request"), _("Token is missing"), http_status_code=417)
|
||||
return
|
||||
|
||||
token = frappe.cache().get_value("{0}:{1}".format(provider, state["token"]), expires=True)
|
||||
if not token:
|
||||
frappe.respond_as_web_page(_("Invalid Request"), _("Invalid Token"), http_status_code=417)
|
||||
return
|
||||
|
||||
user = get_email(data)
|
||||
|
||||
if not user:
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
import re
|
||||
from io import BytesIO
|
||||
|
||||
import openpyxl
|
||||
import xlrd
|
||||
import re
|
||||
from openpyxl.styles import Font
|
||||
from openpyxl import load_workbook
|
||||
from openpyxl.styles import Font
|
||||
from openpyxl.utils import get_column_letter
|
||||
from six import BytesIO, string_types
|
||||
|
||||
import frappe
|
||||
|
||||
ILLEGAL_CHARACTERS_RE = re.compile(r'[\000-\010]|[\013-\014]|[\016-\037]')
|
||||
|
||||
|
||||
# return xlsx file object
|
||||
def make_xlsx(data, sheet_name, wb=None, column_widths=None):
|
||||
column_widths = column_widths or []
|
||||
|
|
@ -31,12 +32,12 @@ def make_xlsx(data, sheet_name, wb=None, column_widths=None):
|
|||
for row in data:
|
||||
clean_row = []
|
||||
for item in row:
|
||||
if isinstance(item, string_types) and (sheet_name not in ['Data Import Template', 'Data Export']):
|
||||
if isinstance(item, str) and (sheet_name not in ['Data Import Template', 'Data Export']):
|
||||
value = handle_html(item)
|
||||
else:
|
||||
value = item
|
||||
|
||||
if isinstance(item, string_types) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None):
|
||||
if isinstance(item, str) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None):
|
||||
# Remove illegal characters from the string
|
||||
value = re.sub(ILLEGAL_CHARACTERS_RE, '', value)
|
||||
|
||||
|
|
@ -80,12 +81,12 @@ def handle_html(data):
|
|||
|
||||
return value
|
||||
|
||||
|
||||
def read_xlsx_file_from_attached_file(file_url=None, fcontent=None, filepath=None):
|
||||
if file_url:
|
||||
_file = frappe.get_doc("File", {"file_url": file_url})
|
||||
filename = _file.get_full_path()
|
||||
elif fcontent:
|
||||
from io import BytesIO
|
||||
filename = BytesIO(fcontent)
|
||||
elif filepath:
|
||||
filename = filepath
|
||||
|
|
@ -102,6 +103,7 @@ def read_xlsx_file_from_attached_file(file_url=None, fcontent=None, filepath=Non
|
|||
rows.append(tmp_list)
|
||||
return rows
|
||||
|
||||
|
||||
def read_xls_file_from_attached_file(content):
|
||||
book = xlrd.open_workbook(file_contents=content)
|
||||
sheets = book.sheets()
|
||||
|
|
@ -111,6 +113,7 @@ def read_xls_file_from_attached_file(content):
|
|||
rows.append(sheet.row_values(i))
|
||||
return rows
|
||||
|
||||
|
||||
def build_xlsx_response(data, filename):
|
||||
xlsx_file = make_xlsx(data, filename)
|
||||
# write out response as a xlsx type
|
||||
|
|
|
|||
|
|
@ -2,17 +2,18 @@
|
|||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import requests
|
||||
import googleapiclient.discovery
|
||||
import google.oauth2.credentials
|
||||
|
||||
from frappe import _
|
||||
from urllib.parse import quote
|
||||
|
||||
import google.oauth2.credentials
|
||||
import requests
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
from frappe.utils import get_request_site_address
|
||||
from six.moves.urllib.parse import quote
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.integrations.doctype.google_settings.google_settings import get_auth_url
|
||||
from frappe.utils import get_request_site_address
|
||||
|
||||
SCOPES = "https://www.googleapis.com/auth/indexing"
|
||||
|
||||
|
|
@ -82,7 +83,12 @@ def get_google_indexing_object():
|
|||
}
|
||||
|
||||
credentials = google.oauth2.credentials.Credentials(**credentials_dict)
|
||||
google_indexing = googleapiclient.discovery.build("indexing", "v3", credentials=credentials)
|
||||
google_indexing = build(
|
||||
serviceName="indexing",
|
||||
version="v3",
|
||||
credentials=credentials,
|
||||
static_discovery=False
|
||||
)
|
||||
|
||||
return google_indexing
|
||||
|
||||
|
|
|
|||
|
|
@ -33,20 +33,12 @@ frappe.ui.form.on('Website Settings', {
|
|||
frm.fields_dict.top_bar_items.grid.update_docfield_property(
|
||||
'parent_label', 'options', frm.events.get_parent_options(frm, "top_bar_items")
|
||||
);
|
||||
|
||||
if ($(frm.fields_dict.top_bar_items.grid.wrapper).find(".grid-row-open")) {
|
||||
frm.fields_dict.top_bar_items.grid.refresh();
|
||||
}
|
||||
},
|
||||
|
||||
set_parent_label_options_footer: function(frm) {
|
||||
frm.fields_dict.footer_items.grid.update_docfield_property(
|
||||
'parent_label', 'options', frm.events.get_parent_options(frm, "top_bar_items")
|
||||
'parent_label', 'options', frm.events.get_parent_options(frm, "footer_items")
|
||||
);
|
||||
|
||||
if ($(frm.fields_dict.footer_items.grid.wrapper).find(".grid-row-open")) {
|
||||
frm.fields_dict.footer_items.grid.refresh();
|
||||
}
|
||||
},
|
||||
|
||||
authorize_api_indexing_access: function(frm) {
|
||||
|
|
@ -122,10 +114,18 @@ frappe.ui.form.on('Website Settings', {
|
|||
});
|
||||
|
||||
frappe.ui.form.on('Top Bar Item', {
|
||||
top_bar_items_delete(frm) {
|
||||
frm.events.set_parent_label_options(frm);
|
||||
},
|
||||
|
||||
footer_items_add(frm, cdt, cdn) {
|
||||
frappe.model.set_value(cdt, cdn, 'right', 0);
|
||||
},
|
||||
|
||||
footer_items_delete(frm) {
|
||||
frm.events.set_parent_label_options_footer(frm);
|
||||
},
|
||||
|
||||
parent_label: function(frm, doctype, name) {
|
||||
frm.events.set_parent_options(frm, doctype, name);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -95,14 +95,6 @@ def login_via_frappe(code, state):
|
|||
def login_via_office365(code, state):
|
||||
login_via_oauth2_id_token("office_365", code, state, decoder=decoder_compat)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_oauth_user(data=None, provider=None, state=None, email_id=None, key=None, generate_login_token=False):
|
||||
if not ((data and provider and state) or (email_id and key)):
|
||||
frappe.respond_as_web_page(_("Invalid Request"), _("Missing parameters for login"), http_status_code=417)
|
||||
return
|
||||
|
||||
_login_oauth_user(data, provider, state, email_id, key, generate_login_token)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_token(login_token):
|
||||
sid = frappe.cache().get_value("login_token:{0}".format(login_token), expires=True)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
"driver.js": "^0.9.8",
|
||||
"express": "^4.17.1",
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"frappe-charts": "^2.0.0-rc11",
|
||||
"frappe-charts": "^2.0.0-rc13",
|
||||
"frappe-datatable": "^1.15.3",
|
||||
"frappe-gantt": "^0.5.0",
|
||||
"fuse.js": "^3.4.6",
|
||||
|
|
|
|||
152
requirements.txt
152
requirements.txt
|
|
@ -1,79 +1,79 @@
|
|||
Babel==2.6.0
|
||||
beautifulsoup4==4.8.2
|
||||
bleach-whitelist==0.0.10
|
||||
bleach==3.3.0
|
||||
boto3==1.10.18
|
||||
braintree==3.57.1
|
||||
chardet==3.0.4
|
||||
Click==7.0
|
||||
coverage==4.5.4
|
||||
croniter==0.3.31
|
||||
cryptography==3.3.2
|
||||
dropbox==9.1.0
|
||||
email-reply-parser==0.5.9
|
||||
Faker==2.0.4
|
||||
Babel~=2.9.0
|
||||
beautifulsoup4~=4.9.3
|
||||
bleach-whitelist~=0.0.11
|
||||
bleach~=3.3.0
|
||||
boto3~=1.17.53
|
||||
braintree~=4.8.0
|
||||
chardet~=4.0.0
|
||||
Click~=7.1.2
|
||||
coverage~=5.5
|
||||
croniter~=1.0.11
|
||||
cryptography~=3.4.7
|
||||
dropbox~=11.7.0
|
||||
email-reply-parser~=0.5.12
|
||||
Faker~=8.1.0
|
||||
future==0.18.2
|
||||
gitdb2==2.0.6;python_version<'3.4'
|
||||
GitPython==2.1.15
|
||||
git-url-parse==1.2.2
|
||||
google-api-python-client==1.9.3
|
||||
google-auth-httplib2==0.0.3
|
||||
google-auth-oauthlib==0.4.1
|
||||
google-auth==1.18.0
|
||||
googlemaps==3.1.1
|
||||
gunicorn==19.10.0
|
||||
html2text==2016.9.19
|
||||
html5lib==1.0.1
|
||||
ipython==7.14.0
|
||||
jedi==0.17.2 # not directly required. Pinned to fix upstream issue with ipython.
|
||||
Jinja2==2.11.3
|
||||
ldap3==2.7
|
||||
markdown2==2.4.0
|
||||
git-url-parse~=1.2.2
|
||||
gitdb~=4.0.7
|
||||
GitPython~=3.1.14
|
||||
google-api-python-client~=2.2.0
|
||||
google-auth-httplib2~=0.1.0
|
||||
google-auth-oauthlib~=0.4.4
|
||||
google-auth~=1.29.0
|
||||
googlemaps~=4.4.5
|
||||
gunicorn~=20.1.0
|
||||
html2text==2020.1.16
|
||||
html5lib~=1.1
|
||||
ipython~=7.16.1
|
||||
jedi==0.17.2 # not directly required. Pinned to fix upstream IPython issue (https://github.com/ipython/ipython/issues/12740)
|
||||
Jinja2~=2.11.3
|
||||
ldap3~=2.9
|
||||
markdown2~=2.4.0
|
||||
maxminddb-geolite2==2018.703
|
||||
ndg-httpsclient==0.5.1
|
||||
num2words==0.5.10
|
||||
oauthlib==3.1.0
|
||||
openpyxl==2.6.4
|
||||
passlib==1.7.3
|
||||
pdfkit==0.6.1
|
||||
Pillow>=8.0.0
|
||||
premailer==3.6.1
|
||||
psutil==5.7.2
|
||||
psycopg2-binary==2.8.4
|
||||
pyasn1==0.4.8
|
||||
PyJWT==1.7.1
|
||||
PyMySQL==0.9.3
|
||||
pyngrok==4.1.6
|
||||
pyOpenSSL==19.1.0
|
||||
pyotp==2.3.0
|
||||
PyPDF2==1.26.0
|
||||
pypng==0.0.20
|
||||
PyQRCode==1.2.1
|
||||
python-dateutil==2.8.1
|
||||
pytz==2019.3
|
||||
PyYAML==5.4
|
||||
rauth==0.7.3
|
||||
redis==3.5.3
|
||||
requests-oauthlib==1.3.0
|
||||
requests==2.23.0
|
||||
RestrictedPython==5.0
|
||||
rq>=1.1.0
|
||||
schedule==0.6.0
|
||||
semantic-version==2.8.4
|
||||
simple-chalk==0.1.0
|
||||
six==1.14.0
|
||||
sqlparse==0.2.4
|
||||
stripe==2.40.0
|
||||
terminaltables==3.1.0
|
||||
unittest-xml-reporting==2.5.2
|
||||
urllib3==1.25.9
|
||||
watchdog==0.8.0
|
||||
Werkzeug==0.16.1
|
||||
Whoosh==2.7.4
|
||||
xlrd==1.2.0
|
||||
zxcvbn-python==4.4.24
|
||||
pycryptodome==3.9.8
|
||||
paytmchecksum==1.7.0
|
||||
wrapt==1.10.11
|
||||
razorpay==1.2.0
|
||||
ndg-httpsclient~=0.5.1
|
||||
num2words~=0.5.10
|
||||
oauthlib~=3.1.0
|
||||
openpyxl~=3.0.7
|
||||
passlib~=1.7.4
|
||||
paytmchecksum~=1.7.0
|
||||
pdfkit~=0.6.1
|
||||
Pillow~=8.2.0
|
||||
premailer~=3.8.0
|
||||
psutil~=5.8.0
|
||||
psycopg2-binary~=2.8.6
|
||||
pyasn1~=0.4.8
|
||||
pycryptodome~=3.10.1
|
||||
PyJWT~=1.7.1
|
||||
PyMySQL~=1.0.2
|
||||
pyngrok~=5.0.5
|
||||
pyOpenSSL~=20.0.1
|
||||
pyotp~=2.6.0
|
||||
PyPDF2~=1.26.0
|
||||
pypng~=0.0.20
|
||||
PyQRCode~=1.2.1
|
||||
python-dateutil~=2.8.1
|
||||
pytz==2021.1
|
||||
PyYAML~=5.4.1
|
||||
rauth~=0.7.3
|
||||
razorpay~=1.2.0
|
||||
redis~=3.5.3
|
||||
requests-oauthlib~=1.3.0
|
||||
requests~=2.25.1
|
||||
RestrictedPython~=5.1
|
||||
rq~=1.8.0
|
||||
rsa>=4.1 # not directly required, pinned by Snyk to avoid a vulnerability
|
||||
schedule~=1.1.0
|
||||
semantic-version~=2.8.5
|
||||
simple-chalk~=0.1.0
|
||||
six~=1.15.0
|
||||
sqlparse~=0.4.1
|
||||
stripe~=2.56.0
|
||||
terminaltables~=3.1.0
|
||||
unittest-xml-reporting~=3.0.4
|
||||
urllib3~=1.26.4
|
||||
watchdog~=2.0.2
|
||||
Werkzeug~=0.16.1
|
||||
Whoosh~=2.7.4
|
||||
wrapt~=1.12.1
|
||||
xlrd~=2.0.1
|
||||
zxcvbn-python~=4.4.24
|
||||
|
|
|
|||
|
|
@ -2699,10 +2699,10 @@ fragment-cache@^0.2.1:
|
|||
dependencies:
|
||||
map-cache "^0.2.2"
|
||||
|
||||
frappe-charts@^2.0.0-rc11:
|
||||
version "2.0.0-rc11"
|
||||
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc11.tgz#0724fa0d43593362c075c3805ebbbe1a608fcef7"
|
||||
integrity sha512-DY3tThT1lNGcJlRMOtIhnILtSm5h1iKysWhZAyj7yrGiOnOWbZpYx/NZzXZYwtRrWwMlYiLX2ylV76qo31ONsg==
|
||||
frappe-charts@^2.0.0-rc13:
|
||||
version "2.0.0-rc13"
|
||||
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc13.tgz#fdb251d7ae311c41e38f90a3ae108070ec6b9072"
|
||||
integrity sha512-Bv7IfllIrjRbKWHn5b769dOSenqdBixAr6m5kurf8ZUOJSLOgK4HOXItJ7BA8n9PvviH9/k5DaloisjLM2Bm1w==
|
||||
|
||||
frappe-datatable@^1.15.3:
|
||||
version "1.15.3"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue