diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..24f122a8d4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# Root editor config file +root = true + +# Common settings +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# python, js indentation settings +[{*.py,*.js}] +indent_style = tab +indent_size = 4 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..26bb7ab280 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community Forum + url: https://discuss.erpnext.com/ + about: For general QnA, discussions and community help. diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py index 3fc14ba61b..08d1d1aa9c 100644 --- a/.github/helper/documentation.py +++ b/.github/helper/documentation.py @@ -21,8 +21,8 @@ def docs_link_exists(body): if word.startswith('http') and uri_validator(word): parsed_url = urlparse(word) if parsed_url.netloc == "github.com": - _, org, repo, _type, ref = parsed_url.path.split('/') - if org == "frappe" and repo in docs_repos: + parts = parsed_url.path.split('/') + if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos: return True diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index 2b1102f681..afee9b98bb 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -44,6 +44,20 @@ frappe.ui.form.on('Auto Repeat', { // auto repeat schedule frappe.auto_repeat.render_schedule(frm); + + frm.trigger('toggle_submit_on_creation'); + }, + + reference_doctype: function(frm) { + frm.trigger('toggle_submit_on_creation'); + }, + + toggle_submit_on_creation: function(frm) { + // submit on creation checkbox + frappe.model.with_doctype(frm.doc.reference_doctype, () => { + let meta = frappe.get_meta(frm.doc.reference_doctype); + frm.toggle_display('submit_on_creation', meta.is_submittable); + }); }, template: function(frm) { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json index 87cf423e2c..5ff4cbeead 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.json +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json @@ -13,6 +13,7 @@ "section_break_3", "reference_doctype", "reference_document", + "submit_on_creation", "column_break_5", "start_date", "end_date", @@ -201,10 +202,16 @@ "depends_on": "eval:doc.frequency==='Weekly';", "fieldname": "section_break_12", "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "submit_on_creation", + "fieldtype": "Check", + "label": "Submit on Creation" } ], "links": [], - "modified": "2020-11-10 22:44:51.815740", + "modified": "2020-12-10 10:43:13.449172", "modified_by": "Administrator", "module": "Automation", "name": "Auto Repeat", diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index c47b672595..205d49df56 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -24,6 +24,7 @@ class AutoRepeat(Document): def validate(self): self.update_status() self.validate_reference_doctype() + self.validate_submit_on_creation() self.validate_dates() self.validate_email_id() self.validate_auto_repeat_days() @@ -64,6 +65,11 @@ class AutoRepeat(Document): if not frappe.get_meta(self.reference_doctype).allow_auto_repeat: frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype)) + def validate_submit_on_creation(self): + if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable: + frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format( + frappe.bold('Submit on Creation'))) + def validate_dates(self): if frappe.flags.in_patch: return @@ -160,6 +166,9 @@ class AutoRepeat(Document): self.update_doc(new_doc, reference_doc) new_doc.insert(ignore_permissions = True) + if self.submit_on_creation: + new_doc.submit() + return new_doc def update_doc(self, new_doc, reference_doc): @@ -170,7 +179,7 @@ class AutoRepeat(Document): if new_doc.meta.get_field('auto_repeat'): new_doc.set('auto_repeat', self.name) - for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']: + for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']: if new_doc.meta.get_field(fieldname): new_doc.set(fieldname, reference_doc.get(fieldname)) diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index 3cd10e8a61..a9bfbb1cf8 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -158,6 +158,25 @@ class TestAutoRepeat(unittest.TestCase): doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2)) self.assertEqual(getdate(doc.next_schedule_date), current_date) + def test_submit_on_creation(self): + doctype = 'Test Submittable DocType' + create_submittable_doctype(doctype) + + current_date = getdate() + submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert() + submittable_doc.submit() + doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name, + start_date=add_days(current_date, -1), submit_on_creation=1) + + data = get_auto_repeat_entries(current_date) + create_repeated_entries(data) + docnames = frappe.db.get_all(doc.reference_doctype, + filters={'auto_repeat': doc.name}, + fields=['docstatus'], + limit=1 + ) + self.assertEquals(docnames[0].docstatus, 1) + def make_auto_repeat(**args): args = frappe._dict(args) @@ -165,6 +184,7 @@ def make_auto_repeat(**args): 'doctype': 'Auto Repeat', 'reference_doctype': args.reference_doctype or 'ToDo', 'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'), + 'submit_on_creation': args.submit_on_creation or 0, 'frequency': args.frequency or 'Daily', 'start_date': args.start_date or add_days(today(), -1), 'end_date': args.end_date or "", @@ -176,3 +196,34 @@ def make_auto_repeat(**args): }).insert(ignore_permissions=True) return doc + + +def create_submittable_doctype(doctype): + if frappe.db.exists('DocType', doctype): + return + else: + doc = frappe.get_doc({ + 'doctype': 'DocType', + '__newname': doctype, + 'module': 'Custom', + 'custom': 1, + 'is_submittable': 1, + 'fields': [{ + 'fieldname': 'test', + 'label': 'Test', + 'fieldtype': 'Data' + }], + 'permissions': [{ + 'role': 'System Manager', + 'read': 1, + 'write': 1, + 'create': 1, + 'delete': 1, + 'submit': 1, + 'cancel': 1, + 'amend': 1 + }] + }).insert() + + doc.allow_auto_repeat = 1 + doc.save() \ No newline at end of file diff --git a/frappe/chat/util/util.py b/frappe/chat/util/util.py index 5aa80a85ae..82df6dd127 100644 --- a/frappe/chat/util/util.py +++ b/frappe/chat/util/util.py @@ -1,27 +1,21 @@ from __future__ import unicode_literals +# imports - standard imports +import json +from collections.abc import MutableMapping, MutableSequence, Sequence + # imports - third-party imports import requests - -# imports - compatibility imports -import six - -# imports - standard imports -from collections import Sequence, MutableSequence, Mapping, MutableMapping -if six.PY2: - from urlparse import urlparse # PY2 -else: - from urllib.parse import urlparse # PY3 -import json +from urllib.parse import urlparse # imports - module imports -from frappe.model.document import Document -from frappe.exceptions import DuplicateEntryError -from frappe import _dict import frappe +from frappe.exceptions import DuplicateEntryError +from frappe.model.document import Document session = frappe.session + def get_user_doc(user = None): if isinstance(user, Document): return user @@ -38,12 +32,12 @@ def squashify(what): return what def safe_json_loads(*args): - results = [ ] + results = [] for arg in args: try: arg = json.loads(arg) - except Exception as e: + except Exception: pass results.append(arg) @@ -81,7 +75,7 @@ def dictify(arg): for i, a in enumerate(arg): arg[i] = dictify(a) elif isinstance(arg, MutableMapping): - arg = _dict(arg) + arg = frappe._dict(arg) return arg @@ -113,4 +107,4 @@ def get_emojis(): emojis = resp.json() redis.hset('frappe_emojis', 'emojis', emojis) - return dictify(emojis) \ No newline at end of file + return dictify(emojis) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index bc65aa178c..4a631be3ac 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -100,13 +100,11 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas # Extract public and/or private files to the restored site, if user has given the path if with_public_files: - with_public_files = os.path.join(base_path, with_public_files) - public = extract_files(site, with_public_files, 'public') + public = extract_files(site, with_public_files) os.remove(public) if with_private_files: - with_private_files = os.path.join(base_path, with_private_files) - private = extract_files(site, with_private_files, 'private') + private = extract_files(site, with_private_files) os.remove(private) # Removing temporarily created file diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index cb3d06a29a..cce5968f9c 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -394,7 +394,10 @@ class DocType(Document): frappe.db.sql("""update tabSingles set value=%s where doctype=%s and field='name' and value = %s""", (new, new, old)) else: - frappe.db.sql("rename table `tab%s` to `tab%s`" % (old, new)) + frappe.db.multisql({ + "mariadb": f"RENAME TABLE `tab{old}` TO `tab{new}`", + "postgres": f"ALTER TABLE `tab{old}` RENAME TO `tab{new}`" + }) def rename_files_and_folders(self, old, new): # move files diff --git a/frappe/core/doctype/domain_settings/domain_settings.js b/frappe/core/doctype/domain_settings/domain_settings.js index 1428727993..7178cb4cd6 100644 --- a/frappe/core/doctype/domain_settings/domain_settings.js +++ b/frappe/core/doctype/domain_settings/domain_settings.js @@ -18,6 +18,9 @@ frappe.ui.form.on('Domain Settings', { checked: active_domains.includes(domain) }; }); + }, + on_change: () => { + frm.dirty(); } }, render_input: true diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 48247860b3..8614740d26 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -30,7 +30,7 @@ import frappe from frappe import _, conf from frappe.model.document import Document from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip - +from frappe.utils.image import strip_exif_data class MaxFileSizeReachedError(frappe.ValidationError): pass @@ -456,6 +456,7 @@ class File(Document): def save_file(self, content=None, decode=False, ignore_existing_file_check=False): file_exists = False self.content = content + if decode: if isinstance(content, text_type): self.content = content.encode("utf-8") @@ -466,10 +467,19 @@ class File(Document): if not self.is_private: self.is_private = 0 - self.file_size = self.check_max_file_size() - self.content_hash = get_content_hash(self.content) + self.content_type = mimetypes.guess_type(self.file_name)[0] + + self.file_size = self.check_max_file_size() + + if ( + self.content_type and "image" in self.content_type + and frappe.get_system_settings("strip_exif_metadata_from_uploaded_images") + ): + self.content = strip_exif_data(self.content, self.content_type) + self.content_hash = get_content_hash(self.content) + duplicate_file = None # check if a file exists with the same content hash and is also in the same folder (public or private) @@ -633,7 +643,12 @@ def get_extension(filename, extn, content): return extn def get_local_image(file_url): - file_path = frappe.get_site_path("public", file_url.lstrip("/")) + if file_url.startswith("/private"): + file_url_path = (file_url.lstrip("/"), ) + else: + file_url_path = ("public", file_url.lstrip("/")) + + file_path = frappe.get_site_path(*file_url_path) try: image = Image.open(file_path) diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index 78ef2d0509..a317d69166 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -48,29 +48,33 @@ frappe.ui.form.on('Server Script', { setup_help(frm) { frm.get_field('help_html').html(` -
+Add logic for standard doctype events like Before Insert, After Submit, etc.
+
+
# set property
if "test" in doc.description:
- doc.status = 'Closed'
+ doc.status = 'Closed'
# validate
if "validate" in doc.description:
- raise frappe.ValidationError
+ raise frappe.ValidationError
# auto create another document
-if doc.allocted_to:
- frappe.get_doc(dict(
- doctype = 'ToDo'
- owner = doc.allocated_to,
- description = doc.subject
- )).insert()
-
+if doc.allocated_to:
+ frappe.get_doc(dict(
+ doctype = 'ToDo'
+ owner = doc.allocated_to,
+ description = doc.subject
+ )).insert()
+
+
+
Respond to /api/method/<method-name> calls, just like whitelisted methods
# respond to API
@@ -79,6 +83,21 @@ if frappe.form_dict.message == "ping":
else:
frappe.response['message'] = "ok"
+
+Add conditions to the where clause of list queries.
+
+# generate dynamic conditions and set it in the conditions variable
+tenant_id = frappe.db.get_value(...)
+conditions = 'tenant_id = {}'.format(tenant_id)
+
+# resulting select query
+select name from \`tabPerson\`
+where tenant_id = 2
+order by creation desc
+
`);
}
diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json
index 420f96ec2f..94a48f196c 100644
--- a/frappe/core/doctype/server_script/server_script.json
+++ b/frappe/core/doctype/server_script/server_script.json
@@ -24,7 +24,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Script Type",
- "options": "DocType Event\nScheduler Event\nAPI",
+ "options": "DocType Event\nScheduler Event\nPermission Query\nAPI",
"reqd": 1
},
{
@@ -35,7 +35,7 @@
"reqd": 1
},
{
- "depends_on": "eval:doc.script_type==='DocType Event'",
+ "depends_on": "eval:['DocType Event', 'Permission Query'].includes(doc.script_type)",
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
@@ -88,7 +88,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-11-11 12:39:41.391052",
+ "modified": "2020-12-03 22:42:02.708148",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 839b784651..88d68dba14 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -4,6 +4,8 @@
from __future__ import unicode_literals
+import ast
+
import frappe
from frappe.model.document import Document
from frappe.utils.safe_exec import safe_exec
@@ -11,9 +13,9 @@ from frappe import _
class ServerScript(Document):
- @staticmethod
- def validate():
+ def validate(self):
frappe.only_for('Script Manager', True)
+ ast.parse(self.script)
@staticmethod
def on_update():
@@ -41,6 +43,12 @@ class ServerScript(Document):
# wrong report type!
raise frappe.DoesNotExistError
+ def get_permission_query_conditions(self, user):
+ locals = {"user": user, "conditions": ""}
+ safe_exec(self.script, None, locals)
+ if locals["conditions"]:
+ return locals["conditions"]
+
@frappe.whitelist()
def setup_scheduler_events(script_name, frequency):
method = frappe.scrub('{0}-{1}'.format(script_name, frequency))
diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py
index e03504f30b..4dc4f12b34 100644
--- a/frappe/core/doctype/server_script/server_script_utils.py
+++ b/frappe/core/doctype/server_script/server_script_utils.py
@@ -50,6 +50,9 @@ def get_server_script_map():
# },
# '_api': {
# '[path]': '[server script]'
+ # },
+ # 'permission_query': {
+ # 'DocType': '[server script]'
# }
# }
if frappe.flags.in_patch and not frappe.db.table_exists('Server Script'):
@@ -57,16 +60,20 @@ def get_server_script_map():
script_map = frappe.cache().get_value('server_script_map')
if script_map is None:
- script_map = {}
+ script_map = {
+ 'permission_query': {}
+ }
enabled_server_scripts = frappe.get_all('Server Script',
fields=('name', 'reference_doctype', 'doctype_event','api_method', 'script_type'),
filters={'disabled': 0})
for script in enabled_server_scripts:
if script.script_type == 'DocType Event':
script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name)
+ elif script.script_type == 'Permission Query':
+ script_map['permission_query'][script.reference_doctype] = script.name
else:
script_map.setdefault('_api', {})[script.api_method] = script.name
frappe.cache().set_value('server_script_map', script_map)
- return script_map
\ No newline at end of file
+ return script_map
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index 3356e584af..957cbbf72d 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -45,6 +45,22 @@ frappe.response['message'] = 'hello'
allow_guest = 1,
script = '''
frappe.flags = 'hello'
+'''
+ ),
+ dict(
+ name='test_permission_query',
+ script_type = 'Permission Query',
+ reference_doctype = 'ToDo',
+ script = '''
+conditions = '1 = 1'
+'''),
+ dict(
+ name='test_invalid_namespace_method',
+ script_type = 'DocType Event',
+ doctype_event = 'Before Insert',
+ reference_doctype = 'Note',
+ script = '''
+frappe.method_that_doesnt_exist("do some magic")
'''
)
]
@@ -85,3 +101,12 @@ class TestServerScript(unittest.TestCase):
def test_api_return(self):
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')
+
+ def test_permission_query(self):
+ self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', return_query=1))
+ self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list))
+
+ def test_attribute_error(self):
+ """Raise AttributeError if method not found in Namespace"""
+ note = frappe.get_doc({"doctype": "Note", "title": "Test Note: Server Script"})
+ self.assertRaises(AttributeError, note.insert)
diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js
index b6514dea9f..c0c9074cbc 100644
--- a/frappe/core/doctype/system_settings/system_settings.js
+++ b/frappe/core/doctype/system_settings/system_settings.js
@@ -1,37 +1,36 @@
-frappe.ui.form.on("System Settings", "refresh", function(frm) {
- frappe.call({
- method: "frappe.core.doctype.system_settings.system_settings.load",
- callback: function(data) {
- frappe.all_timezones = data.message.timezones;
- frm.set_df_property("time_zone", "options", frappe.all_timezones);
+frappe.ui.form.on("System Settings", {
+ refresh: function(frm) {
+ frappe.call({
+ method: "frappe.core.doctype.system_settings.system_settings.load",
+ callback: function(data) {
+ frappe.all_timezones = data.message.timezones;
+ frm.set_df_property("time_zone", "options", frappe.all_timezones);
- $.each(data.message.defaults, function(key, val) {
- frm.set_value(key, val);
- frappe.sys_defaults[key] = val;
- })
+ $.each(data.message.defaults, function(key, val) {
+ frm.set_value(key, val);
+ frappe.sys_defaults[key] = val;
+ });
+ }
+ });
+ },
+ enable_password_policy: function(frm) {
+ if (frm.doc.enable_password_policy == 0) {
+ frm.set_value("minimum_password_score", "");
+ } else {
+ frm.set_value("minimum_password_score", "2");
}
- });
-});
-
-frappe.ui.form.on("System Settings", "enable_password_policy", function(frm) {
- if(frm.doc.enable_password_policy == 0){
- frm.set_value("minimum_password_score", "");
- } else {
- frm.set_value("minimum_password_score", "2");
- }
-});
-
-frappe.ui.form.on("System Settings", "enable_two_factor_auth", function(frm) {
- if(frm.doc.enable_two_factor_auth == 0){
- frm.set_value("bypass_2fa_for_retricted_ip_users", 0);
- frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0);
- }
-});
-
-frappe.ui.form.on("System Settings", "enable_prepared_report_auto_deletion", function(frm) {
- if (frm.doc.enable_prepared_report_auto_deletion) {
- if (!frm.doc.prepared_report_expiry_period) {
- frm.set_value('prepared_report_expiry_period', 7);
+ },
+ enable_two_factor_auth: function(frm) {
+ if (frm.doc.enable_two_factor_auth == 0) {
+ frm.set_value("bypass_2fa_for_retricted_ip_users", 0);
+ frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0);
+ }
+ },
+ enable_prepared_report_auto_deletion: function(frm) {
+ if (frm.doc.enable_prepared_report_auto_deletion) {
+ if (!frm.doc.prepared_report_expiry_period) {
+ frm.set_value('prepared_report_expiry_period', 7);
+ }
}
}
});
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 17f97b3e1a..79fb84923a 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -37,6 +37,7 @@
"allow_login_using_mobile_number",
"allow_login_using_user_name",
"allow_error_traceback",
+ "strip_exif_metadata_from_uploaded_images",
"password_settings",
"logout_on_password_reset",
"force_user_to_reset_password",
@@ -460,12 +461,18 @@
"fieldname": "prepared_report_section",
"fieldtype": "Section Break",
"label": "Prepared Report"
+ },
+ {
+ "default": "1",
+ "fieldname": "strip_exif_metadata_from_uploaded_images",
+ "fieldtype": "Check",
+ "label": "Strip EXIF tags from uploaded images"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2020-08-12 14:35:45.214327",
+ "modified": "2020-11-30 18:52:22.161391",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 2d220b864c..17343573ed 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -81,6 +81,11 @@ frappe.ui.form.on("Customize Form", {
} else {
f._sortable = false;
}
+ if (f.fieldtype == "Table") {
+ frm.add_custom_button(f.options, function() {
+ frm.set_value('doc_type', f.options);
+ }, __('Customize Child Table'));
+ }
});
frm.fields_dict.fields.grid.refresh();
},
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index cf674082ab..82513783c7 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -39,7 +39,7 @@ class CustomizeForm(Document):
translation = self.get_name_translation()
self.label = translation.translated_text if translation else ''
- self.create_auto_repeat_custom_field_if_requried(meta)
+ self.create_auto_repeat_custom_field_if_required(meta)
# NOTE doc (self) is sent to clientside by run_method
@@ -74,19 +74,25 @@ class CustomizeForm(Document):
for d in meta.get(fieldname):
self.append(fieldname, d)
- def create_auto_repeat_custom_field_if_requried(self, meta):
+ def create_auto_repeat_custom_field_if_required(self, meta):
+ '''
+ Create auto repeat custom field if it's not already present
+ '''
if self.allow_auto_repeat:
- if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.doc_type}) and \
- not frappe.db.exists('DocField', {'fieldname': 'auto_repeat', 'parent': self.name}):
- insert_after = self.fields[len(self.fields) - 1].fieldname
- df = dict(
- fieldname='auto_repeat',
- label='Auto Repeat',
- fieldtype='Link',
- options='Auto Repeat',
- insert_after=insert_after,
- read_only=1, no_copy=1, print_hide=1)
- create_custom_field(self.doc_type, df)
+ all_fields = [df.fieldname for df in meta.fields]
+
+ if "auto_repeat" in all_fields:
+ return
+
+ insert_after = self.fields[len(self.fields) - 1].fieldname
+ create_custom_field(self.doc_type, dict(
+ fieldname='auto_repeat',
+ label='Auto Repeat',
+ fieldtype='Link',
+ options='Auto Repeat',
+ insert_after=insert_after,
+ read_only=1, no_copy=1, print_hide=1
+ ))
def get_name_translation(self):
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index 15b0bed699..a52efd01e3 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -233,7 +233,7 @@ CREATE TABLE `tabDocType` (
DROP TABLE IF EXISTS `tabSeries`;
CREATE TABLE `tabSeries` (
- `name` varchar(100) DEFAULT NULL,
+ `name` varchar(100),
`current` int(10) NOT NULL DEFAULT 0,
PRIMARY KEY(`name`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index b12bcfe27d..fa03bf8f80 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -5,6 +5,7 @@
from __future__ import unicode_literals
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
+from frappe.config import get_modules_from_all_apps_for_user
import frappe
from frappe import _
import json
@@ -42,6 +43,24 @@ class Dashboard(Document):
except ValueError as error:
frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
+
+def get_permission_query_conditions(user):
+ if not user:
+ user = frappe.session.user
+
+ if user == 'Administrator':
+ return
+
+ roles = frappe.get_roles(user)
+ if "System Manager" in roles:
+ return None
+
+ allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
+ module_condition = '`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL'.format(
+ allowed_modules=','.join(allowed_modules))
+
+ return module_condition
+
@frappe.whitelist()
def get_permitted_charts(dashboard_name):
permitted_charts = []
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 7e2d952928..2fa36b5514 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -7,17 +7,18 @@ import frappe
from frappe import _
import datetime
import json
-from frappe.utils.dashboard import cache_source, get_from_date_from_timespan
-from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate,\
- get_datetime, cint, now_datetime
+from frappe.utils.dashboard import cache_source
+from frappe.utils import nowdate, getdate, get_datetime, cint, now_datetime
+from frappe.utils.dateutils import\
+ get_period, get_period_beginning, get_from_date_from_timespan, get_dates_from_timegrain
from frappe.model.naming import append_number_if_name_exists
from frappe.boot import get_allowed_reports
+from frappe.config import get_modules_from_all_apps_for_user
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
def get_permission_query_conditions(user):
-
if not user:
user = frappe.session.user
@@ -30,9 +31,11 @@ def get_permission_query_conditions(user):
doctype_condition = False
report_condition = False
+ module_condition = False
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
allowed_reports = [frappe.db.escape(key) if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()]
+ allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
if allowed_doctypes:
doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format(
@@ -40,18 +43,24 @@ def get_permission_query_conditions(user):
if allowed_reports:
report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format(
allowed_reports=','.join(allowed_reports))
+ if allowed_modules:
+ module_condition = '''`tabDashboard Chart`.`module` in ({allowed_modules})
+ or `tabDashboard Chart`.`module` is NULL'''.format(
+ allowed_modules=','.join(allowed_modules))
return '''
- (`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
- and {doctype_condition})
- or
- (`tabDashboard Chart`.`chart_type` = 'Report'
- and {report_condition})
- '''.format(
- doctype_condition=doctype_condition,
- report_condition=report_condition
- )
-
+ ((`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
+ and {doctype_condition})
+ or
+ (`tabDashboard Chart`.`chart_type` = 'Report'
+ and {report_condition}))
+ and
+ ({module_condition})
+ '''.format(
+ doctype_condition=doctype_condition,
+ report_condition=report_condition,
+ module_condition=module_condition
+ )
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)
@@ -156,6 +165,7 @@ def add_chart_to_dashboard(args):
def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
if not from_date:
from_date = get_from_date_from_timespan(to_date, timespan)
+ from_date = get_period_beginning(from_date, timegrain)
if not to_date:
to_date = now_datetime()
@@ -185,7 +195,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
result = get_result(data, timegrain, from_date, to_date)
chart_config = {
- "labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
+ "labels": [get_period(r[0], timegrain) for r in result],
"datasets": [{
"name": chart.name,
"values": [r[1] for r in result]
@@ -279,16 +289,8 @@ def get_aggregate_function(chart_type):
def get_result(data, timegrain, from_date, to_date):
- start_date = getdate(from_date)
- end_date = getdate(to_date)
-
- result = [[start_date, 0.0]]
-
- while start_date < end_date:
- next_date = get_next_expected_date(start_date, timegrain)
- result.append([next_date, 0.0])
- start_date = next_date
-
+ dates = get_dates_from_timegrain(from_date, to_date, timegrain)
+ result = [[date, 0] for date in dates]
data_index = 0
if data:
for i, d in enumerate(result):
@@ -298,65 +300,6 @@ def get_result(data, timegrain, from_date, to_date):
return result
-def get_next_expected_date(date, timegrain):
- next_date = None
- # given date is always assumed to be the period ending date
- next_date = get_period_ending(add_to_date(date, days=1), timegrain)
- return getdate(next_date)
-
-def get_period_ending(date, timegrain):
- date = getdate(date)
- if timegrain == 'Daily':
- pass
- elif timegrain == 'Weekly':
- date = get_week_ending(date)
- elif timegrain == 'Monthly':
- date = get_month_ending(date)
- elif timegrain == 'Quarterly':
- date = get_quarter_ending(date)
- elif timegrain == 'Yearly':
- date = get_year_ending(date)
-
- return getdate(date)
-
-def get_week_ending(date):
- # week starts on monday
- from datetime import timedelta
- start = date - timedelta(days = date.weekday())
- end = start + timedelta(days=6)
-
- return end
-
-def get_month_ending(date):
- month_of_the_year = int(date.strftime('%m'))
- # first day of next month (note month starts from 1)
-
- date = add_to_date('{}-01-01'.format(date.year), months = month_of_the_year)
- # last day of this month
- return add_to_date(date, days=-1)
-
-def get_quarter_ending(date):
- date = getdate(date)
-
- # find the earliest quarter ending date that is after
- # the given date
- for month in (3, 6, 9, 12):
- quarter_end_month = getdate('{}-{}-01'.format(date.year, month))
- quarter_end_date = getdate(get_last_day(quarter_end_month))
- if date <= quarter_end_date:
- date = quarter_end_date
- break
-
- return date
-
-def get_year_ending(date):
- ''' returns year ending of the given date '''
-
- # first day of next year (note year starts from 1)
- date = add_to_date('{}-01-01'.format(date.year), months = 12)
- # last day of this month
- return add_to_date(date, days=-1)
-
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
index 5e39998e62..3c37ad4a09 100644
--- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
@@ -5,8 +5,8 @@ from __future__ import unicode_literals
import unittest, frappe
from frappe.utils import getdate, formatdate, get_last_day
-from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get,
- get_period_ending)
+from frappe.utils.dateutils import get_period_ending, get_period
+from frappe.desk.doctype.dashboard_chart.dashboard_chart import get
from datetime import datetime
from dateutil.relativedelta import relativedelta
@@ -53,15 +53,11 @@ class TestDashboardChart(unittest.TestCase):
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name='Test Dashboard Chart', refresh=1)
- self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
- if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
- cur_date += relativedelta(months=1)
-
- for idx in range(1, 13):
+ for idx in range(13):
month = get_last_day(cur_date)
month = formatdate(month.strftime('%Y-%m-%d'))
- self.assertEqual(result.get('labels')[idx], month)
+ self.assertEqual(result.get('labels')[idx], get_period(month))
cur_date += relativedelta(months=1)
frappe.db.rollback()
@@ -87,15 +83,11 @@ class TestDashboardChart(unittest.TestCase):
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name ='Test Empty Dashboard Chart', refresh=1)
- self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
- if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
- cur_date += relativedelta(months=1)
-
- for idx in range(1, 13):
+ for idx in range(13):
month = get_last_day(cur_date)
month = formatdate(month.strftime('%Y-%m-%d'))
- self.assertEqual(result.get('labels')[idx], month)
+ self.assertEqual(result.get('labels')[idx], get_period(month))
cur_date += relativedelta(months=1)
frappe.db.rollback()
@@ -124,15 +116,11 @@ class TestDashboardChart(unittest.TestCase):
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1)
- self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
- if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
- cur_date += relativedelta(months=1)
-
- for idx in range(1, 13):
+ for idx in range(13):
month = get_last_day(cur_date)
month = formatdate(month.strftime('%Y-%m-%d'))
- self.assertEqual(result.get('labels')[idx], month)
+ self.assertEqual(result.get('labels')[idx], get_period(month))
cur_date += relativedelta(months=1)
# only 1 data point with value
@@ -183,13 +171,12 @@ class TestDashboardChart(unittest.TestCase):
timeseries = 1
)).insert()
- result = get(chart_name ='Test Daily Dashboard Chart', refresh = 1)
+ result = get(chart_name = 'Test Daily Dashboard Chart', refresh = 1)
self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 300.0, 0.0, 100.0, 0.0])
self.assertEqual(
result.get('labels'),
- [formatdate('2019-01-06'), formatdate('2019-01-07'), formatdate('2019-01-08'),\
- formatdate('2019-01-09'), formatdate('2019-01-10'), formatdate('2019-01-11')]
+ ['06-01-19', '07-01-19', '08-01-19', '09-01-19', '10-01-19', '11-01-19']
)
frappe.db.rollback()
@@ -218,7 +205,10 @@ class TestDashboardChart(unittest.TestCase):
result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1)
self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0])
- self.assertEqual(result.get('labels'), [formatdate('2018-12-30'), formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
+ self.assertEqual(
+ result.get('labels'),
+ ['30-12-18', '06-01-19', '13-01-19', '20-01-19']
+ )
frappe.db.rollback()
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index d4a2b00c57..6bddd09fc7 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -8,6 +8,7 @@ from frappe.model.document import Document
from frappe.utils import cint
from frappe.model.naming import append_number_if_name_exists
from frappe.modules.export_file import export_to_files
+from frappe.config import get_modules_from_all_apps_for_user
class NumberCard(Document):
def autoname(self):
@@ -33,16 +34,24 @@ def get_permission_query_conditions(user=None):
return None
doctype_condition = False
+ module_condition = False
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
+ allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
if allowed_doctypes:
doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format(
allowed_doctypes=','.join(allowed_doctypes))
+ if allowed_modules:
+ module_condition = '''`tabNumber Card`.`module` in ({allowed_modules})
+ or `tabNumber Card`.`module` is NULL'''.format(
+ allowed_modules=','.join(allowed_modules))
return '''
- {doctype_condition}
- '''.format(doctype_condition=doctype_condition)
+ {doctype_condition}
+ and
+ {module_condition}
+ '''.format(doctype_condition=doctype_condition, module_condition=module_condition)
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)
diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py
index 3aa3a4fa88..66164948f2 100644
--- a/frappe/desk/form/document_follow.py
+++ b/frappe/desk/form/document_follow.py
@@ -21,7 +21,7 @@ def follow_document(doctype, doc_name, user, force=False):
avoided for some doctype
follow only if track changes are set to 1
'''
- if (doctype in ("Communication", "ToDo", "Email Unsubscribe", "File", "Comment")
+ if (doctype in ("Communication", "ToDo", "Email Unsubscribe", "File", "Comment", "Email Account", "Email Domain")
or doctype in log_types):
return
diff --git a/frappe/email/doctype/email_template/email_template.json b/frappe/email/doctype/email_template/email_template.json
index 0d0922f16f..dc73acacc1 100644
--- a/frappe/email/doctype/email_template/email_template.json
+++ b/frappe/email/doctype/email_template/email_template.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
@@ -8,6 +9,8 @@
"engine": "InnoDB",
"field_order": [
"subject",
+ "use_html",
+ "response_html",
"response",
"owner",
"section_break_4",
@@ -22,11 +25,12 @@
"reqd": 1
},
{
+ "depends_on": "eval:!doc.use_html",
"fieldname": "response",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Response",
- "reqd": 1
+ "mandatory_depends_on": "eval:!doc.use_html"
},
{
"default": "user",
@@ -45,10 +49,24 @@
"fieldtype": "HTML",
"label": "Email Reply Help",
"options": "Order Overdue\n\nTransaction {{ name }} has exceeded Due Date. Please take necessary action.\n\nDetails\n\n- Customer: {{ customer }}\n- Amount: {{ grand_total }}\n\n\nThe fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)
\n\nTemplates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.
\n" + }, + { + "default": "0", + "fieldname": "use_html", + "fieldtype": "Check", + "label": "Use HTML" + }, + { + "depends_on": "eval:doc.use_html", + "fieldname": "response_html", + "fieldtype": "Code", + "label": "Response ", + "options": "HTML" } ], "icon": "fa fa-comment", - "modified": "2019-10-30 14:15:00.956347", + "links": [], + "modified": "2020-11-30 14:12:50.321633", "modified_by": "Administrator", "module": "Email", "name": "Email Template", diff --git a/frappe/email/doctype/email_template/email_template.py b/frappe/email/doctype/email_template/email_template.py index 2743032331..6708e9dd3f 100644 --- a/frappe/email/doctype/email_template/email_template.py +++ b/frappe/email/doctype/email_template/email_template.py @@ -9,7 +9,29 @@ from six import string_types class EmailTemplate(Document): def validate(self): - validate_template(self.response) + if self.use_html: + validate_template(self.response_html) + else: + validate_template(self.response) + + def get_formatted_subject(self, doc): + return frappe.render_template(self.subject, doc) + + def get_formatted_response(self, doc): + if self.use_html: + return frappe.render_template(self.response_html, doc) + + return frappe.render_template(self.response, doc) + + def get_formatted_email(self, doc): + if isinstance(doc, string_types): + doc = json.loads(doc) + + return { + "subject" : self.get_formatted_subject(doc), + "message" : self.get_formatted_response(doc) + } + @frappe.whitelist() def get_email_template(template_name, doc): @@ -18,5 +40,4 @@ def get_email_template(template_name, doc): doc = json.loads(doc) email_template = frappe.get_doc("Email Template", template_name) - return {"subject" : frappe.render_template(email_template.subject, doc), - "message" : frappe.render_template(email_template.response, doc)} \ No newline at end of file + return email_template.get_formatted_email(doc) \ No newline at end of file diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index a4d60706eb..2791ebb75b 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -85,11 +85,11 @@ class Newsletter(WebsiteGenerator): self.db_set("scheduled_to_send", len(self.recipients)) def get_message(self): - + if self.content_type == "HTML": + return frappe.render_template(self.message_html, {"doc": self.as_dict()}) return { 'Rich Text': self.message, - 'Markdown': markdown(self.message_md), - 'HTML': self.message_html + 'Markdown': markdown(self.message_md) }[self.content_type or 'Rich Text'] def get_recipients(self): diff --git a/frappe/email/doctype/notification/notification.json b/frappe/email/doctype/notification/notification.json index 73a84e1d3e..c1c877efd4 100644 --- a/frappe/email/doctype/notification/notification.json +++ b/frappe/email/doctype/notification/notification.json @@ -207,7 +207,7 @@ "label": "Value To Be Set" }, { - "depends_on": "eval:in_list(['Email', 'SMS'], doc.channel)", + "depends_on": "eval:doc.channel !=\"Slack\"", "fieldname": "column_break_5", "fieldtype": "Section Break", "label": "Recipients" @@ -281,7 +281,7 @@ "icon": "fa fa-envelope", "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-28 11:04:54.955567", + "modified": "2020-11-24 14:25:43.245677", "modified_by": "Administrator", "module": "Email", "name": "Notification", diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 75281d427e..2ea7a3785e 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -181,6 +181,7 @@ def get_context(context): 'document_type': doc.doctype, 'document_name': doc.name, 'subject': subject, + 'from_user': doc.modified_by or doc.owner, 'email_content': frappe.render_template(self.message, context), 'attached_file': attachments and json.dumps(attachments[0]) } diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index f53b835757..9ba81fa146 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -210,10 +210,9 @@ class SMTPServer: try: if self.use_ssl: if not self.port: - self.smtp_port = 465 + self.port = 465 - self._sess = smtplib.SMTP_SSL((self.server or "").encode('utf-8'), - cint(self.port) or None) + self._sess = smtplib.SMTP_SSL((self.server or ""), cint(self.port)) else: if self.use_tls and not self.port: self.port = 587 diff --git a/frappe/email/test_smtp.py b/frappe/email/test_smtp.py new file mode 100644 index 0000000000..869d708430 --- /dev/null +++ b/frappe/email/test_smtp.py @@ -0,0 +1,25 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: The MIT License + +import unittest +from frappe.email.smtp import SMTPServer + +class TestSMTP(unittest.TestCase): + def test_smtp_ssl_session(self): + for port in [None, 0, 465, "465"]: + make_server(port, 1, 0) + + def test_smtp_tls_session(self): + for port in [None, 0, 587, "587"]: + make_server(port, 0, 1) + + +def make_server(port, ssl, tls): + server = SMTPServer( + server = "smtp.gmail.com", + port = port, + use_ssl = ssl, + use_tls = tls + ) + + server.sess \ No newline at end of file diff --git a/frappe/hooks.py b/frappe/hooks.py index d8c8cd841c..3d7ae0abb4 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -94,6 +94,7 @@ permission_query_conditions = { "User": "frappe.core.doctype.user.user.get_permission_query_conditions", "Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions", "Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions", + "Dashboard": "frappe.desk.doctype.dashboard.dashboard.get_permission_query_conditions", "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions", "Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions", "Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions", diff --git a/frappe/installer.py b/frappe/installer.py index 6a77e5e713..a11c8dfbfa 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -262,11 +262,11 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) for record in frappe.get_all(doctype, filters={"module": module_name}, pluck="name"): print(f"* removing {doctype} '{record}'...") if not dry_run: - frappe.delete_doc(doctype, record, ignore_on_trash=True) + frappe.delete_doc(doctype, record, ignore_on_trash=True, force=True) print(f"* removing Module Def '{module_name}'...") if not dry_run: - frappe.delete_doc("Module Def", module_name, ignore_on_trash=True) + frappe.delete_doc("Module Def", module_name, ignore_on_trash=True, force=True) for doctype in set(drop_doctypes): print(f"* dropping Table for '{doctype}'...") @@ -440,20 +440,11 @@ def extract_sql_from_archive(sql_file_path): Returns: str: Path of the decompressed SQL file """ + from frappe.utils import get_bench_relative_path + sql_file_path = get_bench_relative_path(sql_file_path) # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file - if not os.path.exists(sql_file_path): - base_path = '..' - sql_file_path = os.path.join(base_path, sql_file_path) - if not os.path.exists(sql_file_path): - print('Invalid path {0}'.format(sql_file_path[3:])) - sys.exit(1) - elif sql_file_path.startswith(os.sep): - base_path = os.sep - else: - base_path = '.' - if sql_file_path.endswith('sql.gz'): - decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path)) + decompressed_file_name = extract_sql_gzip(sql_file_path) else: decompressed_file_name = sql_file_path @@ -475,9 +466,12 @@ def extract_sql_gzip(sql_gz_path): return decompressed_file -def extract_files(site_name, file_path, folder_name): +def extract_files(site_name, file_path): import shutil import subprocess + from frappe.utils import get_bench_relative_path + + file_path = get_bench_relative_path(file_path) # Need to do frappe.init to maintain the site locals frappe.init(site=site_name) diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index c8dfc52c95..a750c8328c 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -1,41 +1,50 @@ from __future__ import unicode_literals -import frappe, json -from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer + +import hashlib +import json +from urllib.parse import quote, urlencode, urlparse + +import jwt from oauthlib.oauth2 import FatalClientError, OAuth2Error -from werkzeug import url_fix -from six.moves.urllib.parse import quote, urlencode, urlparse -from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings + +import frappe from frappe import _ +from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer +from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings def get_oauth_server(): if not getattr(frappe.local, 'oauth_server', None): oauth_validator = OAuthWebRequestValidator() - frappe.local.oauth_server = WebApplicationServer(oauth_validator) + frappe.local.oauth_server = WebApplicationServer(oauth_validator) return frappe.local.oauth_server -def get_urlparams_from_kwargs(param_kwargs): +def sanitize_kwargs(param_kwargs): arguments = param_kwargs - if arguments.get("data"): - arguments.pop("data") - if arguments.get("cmd"): - arguments.pop("cmd") + arguments.pop('data', None) + arguments.pop('cmd', None) - return urlencode(arguments) + return arguments @frappe.whitelist() def approve(*args, **kwargs): r = frappe.request - uri = url_fix(r.url.replace("+"," ")) - http_method = r.method - body = r.get_data() - headers = r.headers try: - scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(uri, http_method, body, headers) + scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request( + r.url, + r.method, + r.get_data(), + r.headers + ) - headers, body, status = get_oauth_server().create_authorization_response(uri=frappe.flags.oauth_credentials['redirect_uri'], \ - body=body, headers=headers, scopes=scopes, credentials=frappe.flags.oauth_credentials) + headers, body, status = get_oauth_server().create_authorization_response( + uri=frappe.flags.oauth_credentials['redirect_uri'], + body=r.get_data(), + headers=r.headers, + scopes=scopes, + credentials=frappe.flags.oauth_credentials + ) uri = headers.get('Location', None) frappe.local.response["type"] = "redirect" @@ -47,34 +56,28 @@ def approve(*args, **kwargs): return e @frappe.whitelist(allow_guest=True) -def authorize(*args, **kwargs): - #Fetch provider URL from settings - oauth_settings = get_oauth_settings() - params = get_urlparams_from_kwargs(kwargs) - request_url = urlparse(frappe.request.url) - success_url = request_url.scheme + "://" + request_url.netloc + "/api/method/frappe.integrations.oauth2.approve?" + params +def authorize(**kwargs): + success_url = "/api/method/frappe.integrations.oauth2.approve?" + encode_params(sanitize_kwargs(kwargs)) failure_url = frappe.form_dict["redirect_uri"] + "?error=access_denied" - if frappe.session['user']=='Guest': + if frappe.session.user == 'Guest': #Force login, redirect to preauth again. frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = "/login?redirect-to=/api/method/frappe.integrations.oauth2.authorize?" + quote(params.replace("+"," ")) - - elif frappe.session['user']!='Guest': + frappe.local.response["location"] = "/login?" + encode_params({'redirect-to': frappe.request.url}) + else: try: r = frappe.request - uri = url_fix(r.url) - http_method = r.method - body = r.get_data() - headers = r.headers - - scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(uri, http_method, body, headers) + scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request( + r.url, + r.method, + r.get_data(), + r.headers + ) skip_auth = frappe.db.get_value("OAuth Client", frappe.flags.oauth_credentials['client_id'], "skip_authorization") unrevoked_tokens = frappe.get_all("OAuth Bearer Token", filters={"status":"Active"}) - if skip_auth or (oauth_settings["skip_authorization"] == "Auto" and len(unrevoked_tokens)): - + if skip_auth or (get_oauth_settings().skip_authorization == "Auto" and unrevoked_tokens): frappe.local.response["type"] = "redirect" frappe.local.response["location"] = success_url else: @@ -87,7 +90,6 @@ def authorize(*args, **kwargs): }) resp_html = frappe.render_template("templates/includes/oauth_confirmation.html", response_html_params) frappe.respond_as_web_page("Confirm Access", resp_html) - except FatalClientError as e: return e except OAuth2Error as e: @@ -95,20 +97,20 @@ def authorize(*args, **kwargs): @frappe.whitelist(allow_guest=True) def get_token(*args, **kwargs): - r = frappe.request - - uri = url_fix(r.url) - http_method = r.method - body = r.form - headers = r.headers - #Check whether frappe server URL is set frappe_server_url = frappe.db.get_value("Social Login Key", "frappe", "base_url") or None if not frappe_server_url: frappe.throw(_("Please set Base URL in Social Login Key for Frappe")) try: - headers, body, status = get_oauth_server().create_token_response(uri, http_method, body, headers, frappe.flags.oauth_credentials) + r = frappe.request + headers, body, status = get_oauth_server().create_token_response( + r.url, + r.method, + r.form, + r.headers, + frappe.flags.oauth_credentials + ) out = frappe._dict(json.loads(body)) if not out.error and "openid" in out.scope: token_user = frappe.db.get_value("OAuth Bearer Token", out.access_token, "user") @@ -116,7 +118,7 @@ def get_token(*args, **kwargs): client_secret = frappe.db.get_value("OAuth Client", token_client, "client_secret") if token_user in ["Guest", "Administrator"]: frappe.throw(_("Logged in as Guest or Administrator")) - import hashlib + id_token_header = { "typ":"jwt", "alg":"HS256" @@ -128,9 +130,10 @@ def get_token(*args, **kwargs): "iss": frappe_server_url, "at_hash": frappe.oauth.calculate_at_hash(out.access_token, hashlib.sha256) } - import jwt + 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": str(id_token_encoded)}) + frappe.local.response = out except FatalClientError as e: @@ -140,12 +143,12 @@ def get_token(*args, **kwargs): @frappe.whitelist(allow_guest=True) def revoke_token(*args, **kwargs): r = frappe.request - uri = url_fix(r.url) - http_method = r.method - body = r.form - headers = r.headers - - headers, body, status = get_oauth_server().create_revocation_response(uri, headers=headers, body=body, http_method=http_method) + headers, body, status = get_oauth_server().create_revocation_response( + r.url, + headers=r.headers, + body=r.form, + http_method=r.method + ) frappe.local.response['http_status_code'] = status if status == 200: @@ -174,15 +177,22 @@ def openid_profile(*args, **kwargs): "email": name, "picture": picture }) - + frappe.local.response = user_profile def validate_url(url_string): try: result = urlparse(url_string) - if result.scheme and result.scheme in ["http", "https", "ftp", "ftps"]: - return True - else: - return False + return result.scheme and result.scheme in ["http", "https", "ftp", "ftps"] except: - return False \ No newline at end of file + return False + +def encode_params(params): + """ + Encode a dict of params into a query string. + + Use `quote_via=urllib.parse.quote` so that whitespaces will be encoded as + `%20` instead of as `+`. This is needed because oauthlib cannot handle `+` + as a whitespace. + """ + return urlencode(params, quote_via=quote) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 0a219b4253..5d86b3bac8 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -802,12 +802,12 @@ class BaseDocument(object): if translated: val = _(val) - if absolute_value and isinstance(val, (int, float)): - val = abs(self.get(fieldname)) - if not doc: doc = getattr(self, "parent_doc", None) or self + if (absolute_value or doc.get('absolute_value')) and isinstance(val, (int, float)): + val = abs(self.get(fieldname)) + return format_value(val, df=df, doc=doc, currency=currency) def is_print_hide(self, fieldname, df=None, for_print=True): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index ace9b04cec..b936251b50 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -18,6 +18,7 @@ from frappe.client import check_parent_permission from frappe.model.utils.user_settings import get_user_settings, update_user_settings from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, get_timespan_date_range from frappe.model.meta import get_table_columns +from frappe.core.doctype.server_script.server_script_utils import get_server_script_map class DatabaseQuery(object): def __init__(self, doctype, user=None): @@ -683,15 +684,23 @@ class DatabaseQuery(object): self.match_filters.append(match_filters) def get_permission_query_conditions(self): + conditions = [] condition_methods = frappe.get_hooks("permission_query_conditions", {}).get(self.doctype, []) if condition_methods: - conditions = [] for method in condition_methods: c = frappe.call(frappe.get_attr(method), self.user) if c: conditions.append(c) - return " and ".join(conditions) if conditions else None + permision_script_name = get_server_script_map().get("permission_query").get(self.doctype) + if permision_script_name: + script = frappe.get_doc("Server Script", permision_script_name) + condition = script.get_permission_query_conditions(self.user) + if condition: + conditions.append(condition) + + return " and ".join(conditions) if conditions else "" + def run_custom_query(self, query): if '%(key)s' in query: diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 8c17a5b19b..c740d495c1 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -209,7 +209,8 @@ class Meta(Document): 'owner': _('Created By'), 'modified_by': _('Modified By'), 'creation': _('Created On'), - 'modified': _('Last Modified On') + 'modified': _('Last Modified On'), + '_assign': _('Assigned To') }.get(fieldname) or _('No Label') return label diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 7a2129e76e..35fbf94dc6 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -1,14 +1,15 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals, print_function +from __future__ import print_function, unicode_literals + import frappe from frappe import _, bold -from frappe.utils import cint -from frappe.model.naming import validate_name from frappe.model.dynamic_links import get_dynamic_link_map -from frappe.utils.password import rename_password +from frappe.model.naming import validate_name from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data +from frappe.utils import cint +from frappe.utils.password import rename_password @frappe.whitelist() @@ -42,7 +43,6 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F force = cint(force) merge = cint(merge) - meta = frappe.get_meta(doctype) # call before_rename @@ -249,8 +249,17 @@ def update_link_field_values(link_fields, old, new, doctype): # or no longer exists pass else: - # because the table hasn't been renamed yet! - parent = field['parent'] if field['parent']!=new else old + parent = field['parent'] + + # Handles the case where one of the link fields belongs to + # the DocType being renamed. + # Here this field could have the current DocType as its value too. + + # In this case while updating link field value, the field's parent + # or the current DocType table name hasn't been renamed yet, + # so consider it's old name. + if parent == new and doctype == "DocType": + parent = old frappe.db.sql(""" update `tab{table_name}` set `{fieldname}`=%s @@ -306,8 +315,7 @@ def get_link_fields(doctype): def update_options_for_fieldtype(fieldtype, old, new): if frappe.conf.developer_mode: - for name in frappe.db.sql_list("""select parent from - tabDocField where options=%s""", old): + for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"): doctype = frappe.get_doc("DocType", name) save = False for f in doctype.fields: @@ -413,20 +421,21 @@ def update_parenttype_values(old, new): child_doctypes += custom_child_doctypes fields = [d['fieldname'] for d in child_doctypes] - property_setter_child_doctypes = frappe.db.sql("""\ - select value as options from `tabProperty Setter` - where doc_type=%s and property='options' and - field_name in ("%s")""" % ('%s', '", "'.join(fields)), - (new,)) + property_setter_child_doctypes = frappe.get_all( + "Property Setter", + filters={ + "doc_type": new, + "property": "options", + "field_name": ("in", fields) + }, + pluck="value" + ) + child_doctypes = list(d['options'] for d in child_doctypes) child_doctypes += property_setter_child_doctypes - child_doctypes = (d['options'] for d in child_doctypes) for doctype in child_doctypes: - frappe.db.sql("""\ - update `tab%s` set parenttype=%s - where parenttype=%s""" % (doctype, '%s', '%s'), - (new, old)) + frappe.db.sql(f"update `tab{doctype}` set parenttype=%s where parenttype=%s", (new, old)) def rename_dynamic_links(doctype, old, new): for df in get_dynamic_link_map().get(doctype, []): @@ -482,60 +491,30 @@ def bulk_rename(doctype, rows=None, via_console = False): return rename_log def update_linked_doctypes(doctype, docname, linked_to, value, ignore_doctypes=None): - """ - linked_doctype_info_list = list formed by get_fetch_fields() function - docname = Master DocType's name in which modification are made - value = Value for the field thats set in other DocType's by fetching from Master DocType - """ - linked_doctype_info_list = get_fetch_fields(doctype, linked_to, ignore_doctypes) + from frappe.model.utils.rename_doc import update_linked_doctypes + show_deprecation_warning("update_linked_doctypes") + + return update_linked_doctypes( + doctype=doctype, + docname=docname, + linked_to=linked_to, + value=value, + ignore_doctypes=ignore_doctypes, + ) - for d in linked_doctype_info_list: - frappe.db.sql(""" - update - `tab{doctype}` - set - {linked_to_fieldname} = "{value}" - where - {master_fieldname} = {docname} - and {linked_to_fieldname} != "{value}" - """.format( - doctype = d['doctype'], - linked_to_fieldname = d['linked_to_fieldname'], - value = value, - master_fieldname = d['master_fieldname'], - docname = frappe.db.escape(docname) - )) def get_fetch_fields(doctype, linked_to, ignore_doctypes=None): - """ - doctype = Master DocType in which the changes are being made - linked_to = DocType name of the field thats being updated in Master + from frappe.model.utils.rename_doc import get_fetch_fields + show_deprecation_warning("get_fetch_fields") - This function fetches list of all DocType where both doctype and linked_to is found - as link fields. - Forms a list of dict in the form - - [{doctype: , master_fieldname: , linked_to_fieldname: ] - where - doctype = DocType where changes need to be made - master_fieldname = Fieldname where options = doctype - linked_to_fieldname = Fieldname where options = linked_to - """ + return get_fetch_fields( + doctype=doctype, linked_to=linked_to, ignore_doctypes=ignore_doctypes + ) - master_list = get_link_fields(doctype) - linked_to_list = get_link_fields(linked_to) - out = [] - - from itertools import product - product_list = product(master_list, linked_to_list) - - for d in product_list: - linked_doctype_info = frappe._dict() - if d[0]['parent'] == d[1]['parent'] \ - and (not ignore_doctypes or d[0]['parent'] not in ignore_doctypes) \ - and not d[1]['issingle']: - linked_doctype_info['doctype'] = d[0]['parent'] - linked_doctype_info['master_fieldname'] = d[0]['fieldname'] - linked_doctype_info['linked_to_fieldname'] = d[1]['fieldname'] - out.append(linked_doctype_info) - - return out +def show_deprecation_warning(funct): + from click import secho + message = ( + f"Function frappe.model.rename_doc.{funct} has been deprecated and " + "moved to the frappe.model.utils.rename_doc" + ) + secho(message, fg="yellow") diff --git a/frappe/model/utils/rename_doc.py b/frappe/model/utils/rename_doc.py new file mode 100644 index 0000000000..bf71d36a42 --- /dev/null +++ b/frappe/model/utils/rename_doc.py @@ -0,0 +1,58 @@ +from itertools import product + +import frappe +from frappe.model.rename_doc import get_link_fields + + +def update_linked_doctypes(doctype, docname, linked_to, value, ignore_doctypes=None): + """ + linked_doctype_info_list = list formed by get_fetch_fields() function + docname = Master DocType's name in which modification are made + value = Value for the field thats set in other DocType's by fetching from Master DocType + """ + linked_doctype_info_list = get_fetch_fields(doctype, linked_to, ignore_doctypes) + + for d in linked_doctype_info_list: + frappe.db.set_value( + d.doctype, + { + d.master_fieldname : docname, + d.linked_to_fieldname : ("!=", value), + }, + d.linked_to_fieldname, + value, + ) + + +def get_fetch_fields(doctype, linked_to, ignore_doctypes=None): + """ + doctype = Master DocType in which the changes are being made + linked_to = DocType name of the field thats being updated in Master + This function fetches list of all DocType where both doctype and linked_to is found + as link fields. + Forms a list of dict in the form - + [{doctype: , master_fieldname: , linked_to_fieldname: ] + where + doctype = DocType where changes need to be made + master_fieldname = Fieldname where options = doctype + linked_to_fieldname = Fieldname where options = linked_to + """ + + out = [] + master_list = get_link_fields(doctype) + linked_to_list = get_link_fields(linked_to) + product_list = product(master_list, linked_to_list) + + for d in product_list: + linked_doctype_info = frappe._dict() + if ( + d[0]["parent"] == d[1]["parent"] + and (not ignore_doctypes or d[0]["parent"] not in ignore_doctypes) + and not d[1]["issingle"] + ): + linked_doctype_info.doctype = d[0]["parent"] + linked_doctype_info.master_fieldname = d[0]["fieldname"] + linked_doctype_info.linked_to_fieldname = d[1]["fieldname"] + out.append(linked_doctype_info) + + return out diff --git a/frappe/oauth.py b/frappe/oauth.py index bf225ac118..09af5ad809 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -148,7 +148,7 @@ class OAuthWebRequestValidator(RequestValidator): print("Failed body authentication: Application %s does not exist".format(cid=request.client_id)) cookie_dict = get_cookie_dict_from_headers(request) - user_id = unquote(cookie_dict['user_id']) if 'user_id' in cookie_dict else "Guest" + user_id = unquote(cookie_dict.get('user_id').value) if 'user_id' in cookie_dict else "Guest" return frappe.session.user == user_id def authenticate_client_id(self, client_id, request, *args, **kwargs): diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index e6599b2496..9ef5652dda 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -19,6 +19,7 @@ frappe.ui.form.on("Print Format", { } frm.trigger('render_buttons'); frm.toggle_display('standard', frappe.boot.developer_mode); + frm.trigger('hide_absolute_value_field'); }, render_buttons: function (frm) { frm.page.clear_inner_toolbar(); @@ -58,5 +59,20 @@ frappe.ui.form.on("Print Format", { frm.set_value('show_section_headings', value); frm.set_value('line_breaks', value); frm.trigger('render_buttons'); + }, + doc_type: function (frm) { + frm.trigger('hide_absolute_value_field'); + }, + hide_absolute_value_field: function (frm) { + // TODO: make it work with frm.doc.doc_type + // Problem: frm isn't updated in some random cases + const doctype = locals[frm.doc.doctype][frm.doc.name]; + if (doctype) { + frappe.model.with_doctype(doctype, () => { + const meta = frappe.get_meta(doctype); + const has_int_float_currency_field = meta.fields.filter(df => in_list(['Int', 'Float', 'Currency'], df.fieldtype)); + frm.toggle_display('absolute_value', has_int_float_currency_field.length); + }); + } } -}) +}); diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 63448ccc39..6e64e802c9 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -22,6 +22,7 @@ "align_labels_right", "show_section_headings", "line_breaks", + "absolute_value", "column_break_11", "font", "css_section", @@ -196,13 +197,21 @@ "fieldtype": "Check", "hidden": 1, "label": "Print Format Builder" + }, + { + "default": "0", + "depends_on": "doc_type", + "description": "If checked, negative numberic values of Currency, Quantity or Count would be shown as positive", + "fieldname": "absolute_value", + "fieldtype": "Check", + "label": "Show absolute values" } ], "icon": "fa fa-print", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-27 18:27:58.307070", + "modified": "2020-12-10 18:58:55.598269", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js index 6c17cb4351..477cfb0786 100644 --- a/frappe/public/js/frappe/data_import/import_preview.js +++ b/frappe/public/js/frappe/data_import/import_preview.js @@ -101,6 +101,7 @@ frappe.data_import.ImportPreview = class ImportPreview { .replace('%H', 'HH') .replace('%M', 'mm') .replace('%S', 'ss') + .replace('%b', 'Mon') : null; let column_title = ` diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index c8ed29fb76..5fa7a9dbcb 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -148,7 +148,6 @@ frappe.Application = Class.extend({ user: frappe.session.user }, callback: function(r) { - console.log(r); if(r.message.show_alert){ frappe.show_alert({ indicator: 'red', diff --git a/frappe/public/js/frappe/form/controls/comment.js b/frappe/public/js/frappe/form/controls/comment.js index a64df56bca..d00c915065 100644 --- a/frappe/public/js/frappe/form/controls/comment.js +++ b/frappe/public/js/frappe/form/controls/comment.js @@ -60,7 +60,7 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({ update_state() { const value = this.get_value(); - if (strip_html(value).trim() != "") { + if (strip_html(value).trim() != "" || value.includes('img')) { this.button.removeClass('btn-default').addClass('btn-primary'); } else { this.button.addClass('btn-default').removeClass('btn-primary'); diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index 4db2553bd1..401de2ed5d 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -22,27 +22,9 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ this.has_input = true; this.bind_change_event(); this.setup_autoname_check(); - if (this.df.options == 'Phone') { - this.setup_phone(); - } // somehow this event does not bubble up to document // after v7, if you can debug, remove this }, - setup_phone() { - if (frappe.phone_call.handler) { - this.$wrapper.find('.control-input') - .append(` - - - - - `) - .find('.phone-btn') - .click(() => { - frappe.phone_call.handler(this.get_value(), this.frm); - }); - } - }, setup_autoname_check: function() { if (!this.df.parent) return; this.meta = frappe.get_meta(this.df.parent); diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 84f34d4757..159ab8a61b 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -30,7 +30,7 @@ frappe.ui.form.Timeline = class Timeline { render_input: true, only_input: true, on_submit: (val) => { - if(strip_html(val).trim() != "") { + if (strip_html(val).trim() != "" || val.includes('img')) { this.insert_comment(val, this.comment_area.button); } } @@ -547,10 +547,7 @@ frappe.ui.form.Timeline = class Timeline { log.color = 'dark'; log.sender = log.owner; log.comment_type = 'Milestone'; - log.content = __('{0} changed {1} to {2}', [ - frappe.user.full_name(log.owner).bold(), - frappe.meta.get_label(this.frm.doctype, log.track_field), - log.value.bold()]); + log.content = __('{0} changed {1} to {2}', [ frappe.user.full_name(log.owner).bold(), frappe.meta.get_label(this.frm.doctype, log.track_field), log.value.bold()]); return log; }); return milestones; @@ -613,11 +610,7 @@ frappe.ui.form.Timeline = class Timeline { const field_display_status = frappe.perm.get_field_display_status(df, null, me.frm.perm); if (field_display_status === 'Read' || field_display_status === 'Write') { - parts.push(__('{0} from {1} to {2}', [ - __(df.label), - me.format_content_for_timeline(p[1]), - me.format_content_for_timeline(p[2]) - ])); + parts.push(__('{0} from {1} to {2}', [ __(df.label), me.format_content_for_timeline(p[1]), me.format_content_for_timeline(p[2])])); } } } @@ -648,13 +641,7 @@ frappe.ui.form.Timeline = class Timeline { null, me.frm.perm); if (field_display_status === 'Read' || field_display_status === 'Write') { - parts.push(__('{0} from {1} to {2} in row #{3}', [ - frappe.meta.get_label(me.frm.fields_dict[row[0]].grid.doctype, - p[0]), - me.format_content_for_timeline(p[1]), - me.format_content_for_timeline(p[2]), - row[1] - ])); + parts.push(__('{0} from {1} to {2} in row #{3}', [ frappe.meta.get_label( me.frm.fields_dict[row[0]].grid.doctype, p[0]), me.format_content_for_timeline(p[1]), me.format_content_for_timeline(p[2]), row[1] ])); } } return parts.length < 3; @@ -691,8 +678,7 @@ frappe.ui.form.Timeline = class Timeline { return p; }); if (parts.length) { - out.push(me.get_version_comment(version, __("{0} rows for {1}", - [__(key), parts.join(', ')]))); + out.push(me.get_version_comment(version, __("{0} rows for {1}", [__(key), parts.join(', ')]))); } } }); diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 827fbfdee6..ec9cee9c39 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -373,6 +373,7 @@ export default class GridRow { // no text editor in grid if (df.fieldtype=='Text Editor') { + df = Object.assign({}, df); df.fieldtype = 'Text'; } diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index 7be7fc5baa..f7a5982b96 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -306,6 +306,7 @@ $.extend(frappe.model, { selected_children: opts.frm ? opts.frm.get_selected() : null }, freeze: true, + freeze_message: opts.freeze_message || '', callback: function(r) { if(!r.exc) { frappe.model.sync(r.message); diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 308d9bd5f8..1d302215dd 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -103,6 +103,31 @@ $.extend(frappe.model, { return docfield[0]; }, + get_from_localstorage: function(doctype) { + if (localStorage["_doctype:" + doctype]) { + return JSON.parse(localStorage["_doctype:" + doctype]); + } + }, + + set_in_localstorage: function(doctype, docs) { + try { + localStorage["_doctype:" + doctype] = JSON.stringify(docs); + } catch(e) { + // if quota is exceeded, clear local storage and set item + console.warn("localStorage quota exceeded, clearing doctype cache") + frappe.model.clear_local_storage(); + localStorage["_doctype:" + doctype] = JSON.stringify(docs); + } + }, + + clear_local_storage: function() { + for(var key in localStorage) { + if (key.startsWith("_doctype:")) { + localStorage.removeItem(key); + } + } + }, + with_doctype: function(doctype, callback, async) { if(locals.DocType[doctype]) { callback && callback(); @@ -110,13 +135,15 @@ $.extend(frappe.model, { let cached_timestamp = null; let cached_doc = null; - if(localStorage["_doctype:" + doctype]) { - let cached_docs = JSON.parse(localStorage["_doctype:" + doctype]); + let cached_docs = frappe.model.get_from_localstorage(doctype) + + if (cached_docs) { cached_doc = cached_docs.filter(doc => doc.name === doctype)[0]; if(cached_doc) { cached_timestamp = cached_doc.modified; } } + return frappe.call({ method:'frappe.desk.form.load.getdoctype', type: "GET", @@ -134,7 +161,7 @@ $.extend(frappe.model, { if(r.message=="use_cache") { frappe.model.sync(cached_doc); } else { - localStorage["_doctype:" + doctype] = JSON.stringify(r.docs); + frappe.model.set_in_localstorage(doctype, r.docs) } frappe.model.init_doctype(doctype); diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js index 9ff4ade761..0a145b098b 100644 --- a/frappe/public/js/frappe/utils/common.js +++ b/frappe/public/js/frappe/utils/common.js @@ -234,11 +234,11 @@ frappe.utils.xss_sanitise = function (string, options) { strategies: ['html', 'js'] // use all strategies. } const HTML_ESCAPE_MAP = { - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/' + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' }; const REGEX_SCRIPT = / {% else %}An unexpected error occurred while authorizing {{ client_id }}.
+{{ _("An unexpected error occurred while authorizing {}.").format(client_id) }}