diff --git a/cypress/integration/timeline_email.js b/cypress/integration/timeline_email.js index dfe80e0019..1b7634d211 100644 --- a/cypress/integration/timeline_email.js +++ b/cypress/integration/timeline_email.js @@ -19,7 +19,7 @@ context('Timeline Email', () => { cy.get('.list-row > .level-left > .list-subject').eq(0).click(); //Creating a new email - cy.get('.timeline-actions > .btn').click(); + cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click(); cy.fill_field('recipients', 'test@example.com', 'MultiSelect'); cy.get('.modal.show > .modal-dialog > .modal-content > .modal-body > :nth-child(1) > .form-layout > .form-page > :nth-child(3) > .section-body > .form-column > form > [data-fieldtype="Text Editor"] > .form-group > .control-input-wrapper > .control-input > .ql-container > .ql-editor').type('Test Mail'); @@ -57,11 +57,11 @@ context('Timeline Email', () => { cy.wait(500); //To check if the discard button functionality in email is working correctly - cy.get('.timeline-actions > .btn').click(); + cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click(); cy.fill_field('recipients', 'test@example.com', 'MultiSelect'); cy.get('.modal-footer > .standard-actions > .btn-secondary').contains('Discard').click(); cy.wait(500); - cy.get('.timeline-actions > .btn').click(); + cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click(); cy.wait(500); cy.get_field('recipients', 'MultiSelect').should('have.text', ''); cy.get('.modal-header:visible > .modal-actions > .btn-modal-close > .icon').click(); diff --git a/frappe/__init__.py b/frappe/__init__.py index 08c0f794b3..c6cbfead43 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -12,6 +12,8 @@ Read the documentation: https://frappeframework.com/docs """ import os, warnings +STANDARD_USERS = ('Guest', 'Administrator') + _dev_server = os.environ.get('DEV_SERVER', False) if _dev_server: @@ -100,7 +102,7 @@ def as_unicode(text, encoding='utf-8'): '''Convert to unicode if required''' if isinstance(text, str): return text - elif text==None: + elif text is None: return '' elif isinstance(text, bytes): return str(text, encoding) @@ -121,6 +123,7 @@ def set_user_lang(user, user_language=None): local.lang = get_user_lang(user) # local-globals + db = local("db") qb = local("qb") conf = local("conf") @@ -291,7 +294,7 @@ def get_conf(site=None): class init_site: def __init__(self, site=None): - '''If site==None, initialize it for empty site ('') to load common_site_config.json''' + '''If site is None, initialize it for empty site ('') to load common_site_config.json''' self.site = site or '' def __enter__(self): @@ -443,7 +446,7 @@ def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None, msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide, as_list=as_list) def emit_js(js, user=False, **kwargs): - if user == False: + if user is False: user = session.user publish_realtime('eval_js', js, user=user, **kwargs) @@ -1658,7 +1661,7 @@ def local_cache(namespace, key, generator, regenerate_if_none=False): if key not in local.cache[namespace]: local.cache[namespace][key] = generator() - elif local.cache[namespace][key]==None and regenerate_if_none: + elif local.cache[namespace][key] is None and regenerate_if_none: # if key exists but the previous result was None local.cache[namespace][key] = generator() diff --git a/frappe/auth.py b/frappe/auth.py index 078a6bb165..d4778eb0c1 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -111,7 +111,8 @@ class LoginManager: self.user_type = None if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": - if self.login()==False: return + if self.login() is False: + return self.resume = False # run login triggers @@ -250,8 +251,7 @@ class LoginManager: if not self.user: return - from frappe.core.doctype.user.user import STANDARD_USERS - if self.user in STANDARD_USERS: + if self.user in frappe.STANDARD_USERS: return False reset_pwd_after_days = cint(frappe.db.get_single_value("System Settings", diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index a8c75bffd9..90099eebb6 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -272,7 +272,7 @@ def apply(doc=None, method=None, doctype=None, name=None): for todo in todos_to_close: _todo = frappe.get_doc("ToDo", todo) _todo.status = "Closed" - _todo.save() + _todo.save(ignore_permissions=True) break else: diff --git a/frappe/boot.py b/frappe/boot.py index e671d8b37d..d5d992343a 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -17,7 +17,7 @@ from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_p from frappe.model.base_document import get_controller from frappe.social.doctype.post.post import frequently_visited_links from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo -from frappe.utils import get_time_zone +from frappe.utils import get_time_zone, add_user_info def get_bootinfo(): """build and return boot info""" @@ -222,17 +222,14 @@ def load_translations(bootinfo): bootinfo["__messages"] = messages def get_user_info(): - user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image', 'gender', - 'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type', 'time_zone'], - filters=dict(enabled=1)) + # get info for current user + user_info = frappe._dict() + add_user_info(frappe.session.user, user_info) - user_info_map = {d.name: d for d in user_info} + if frappe.session.user == 'Administrator' and user_info.Administrator.email: + user_info[user_info.Administrator.email] = user_info.Administrator - admin_data = user_info_map.get('Administrator') - if admin_data: - user_info_map[admin_data.email] = admin_data - - return user_info_map + return user_info def get_user(bootinfo): """get user info""" diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 0df8878da4..94a845639b 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -148,7 +148,7 @@ def build_table_count_cache(): data = ( frappe.qb.from_(information_schema.tables).select(table_name, table_rows) ).run(as_dict=True) - counts = {d.get('name').lstrip('tab'): d.get('count', None) for d in data} + counts = {d.get('name').replace('tab', '', 1): d.get('count', None) for d in data} _cache.set_value("information_schema:counts", counts) return counts diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 677325e02d..62488525b0 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -952,7 +952,7 @@ def trim_database(context, dry_run, format, no_backup): doctype_tables = frappe.get_all("DocType", pluck="name") for x in database_tables: - doctype = x.lstrip("tab") + doctype = x.replace("tab", "", 1) if not (doctype in doctype_tables or x.startswith("__") or x in STANDARD_TABLES): TABLES_TO_DROP.append(x) @@ -966,7 +966,7 @@ def trim_database(context, dry_run, format, no_backup): odb = scheduled_backup( ignore_conf=False, - include_doctypes=",".join(x.lstrip("tab") for x in TABLES_TO_DROP), + include_doctypes=",".join(x.replace("tab", "", 1) for x in TABLES_TO_DROP), ignore_files=True, force=True, ) diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index c5cf67ba57..79570d5048 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -314,7 +314,7 @@ class DataExporter: .where(child_doctype_table.parentfield == c["parentfield"]) .orderby(child_doctype_table.idx) ) - for ci, child in enumerate(data_row.run()): + for ci, child in enumerate(data_row.run(as_dict=True)): self.add_data_row(rows, c['doctype'], c['parentfield'], child, ci) for row in rows: diff --git a/frappe/core/doctype/data_export/test_data_exporter.py b/frappe/core/doctype/data_export/test_data_exporter.py new file mode 100644 index 0000000000..8d05707cf1 --- /dev/null +++ b/frappe/core/doctype/data_export/test_data_exporter.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# License: MIT. See LICENSE +import unittest +import frappe +from frappe.core.doctype.data_export.exporter import DataExporter + +class TestDataExporter(unittest.TestCase): + def setUp(self): + self.doctype_name = 'Test DocType for Export Tool' + self.doc_name = 'Test Data for Export Tool' + self.create_doctype_if_not_exists(doctype_name=self.doctype_name) + self.create_test_data() + + def create_doctype_if_not_exists(self, doctype_name, force=False): + """ + Helper Function for setting up doctypes + """ + if force: + frappe.delete_doc_if_exists('DocType', doctype_name) + frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name) + + if frappe.db.exists('DocType', doctype_name): + return + + # Child Table 1 + table_1_name = 'Child 1 of ' + doctype_name + frappe.get_doc({ + 'doctype': 'DocType', + 'name': table_1_name, + 'module': 'Custom', + 'custom': 1, + 'istable': 1, + 'fields': [ + {'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'}, + {'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'}, + ] + }).insert() + + # Main Table + frappe.get_doc({ + 'doctype': 'DocType', + 'name': doctype_name, + 'module': 'Custom', + 'custom': 1, + 'autoname': 'field:title', + 'fields': [ + {'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'}, + {'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'}, + {'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name}, + ], + 'permissions': [ + {'role': 'System Manager'} + ] + }).insert() + + def create_test_data(self, force=False): + """ + Helper Function creating test data + """ + if force: + frappe.delete_doc(self.doctype_name, self.doc_name) + + if not frappe.db.exists(self.doctype_name, self.doc_name): + self.doc = frappe.get_doc( + doctype=self.doctype_name, + title=self.doc_name, + number="100", + table_field_1=[ + {"child_title": "Child Title 1", "child_number": "50"}, + {"child_title": "Child Title 2", "child_number": "51"}, + ] + ).insert() + else: + self.doc = frappe.get_doc(self.doctype_name, self.doc_name) + + def test_export_content(self): + exp = DataExporter(doctype=self.doctype_name, file_type='CSV') + exp.build_response() + + self.assertEqual(frappe.response['type'],'csv') + self.assertEqual(frappe.response['doctype'], self.doctype_name) + self.assertTrue(frappe.response['result']) + self.assertIn('Child Title 1\",50',frappe.response['result']) + self.assertIn('Child Title 2\",51',frappe.response['result']) + + def test_export_type(self): + for type in ['csv', 'Excel']: + with self.subTest(type=type): + exp = DataExporter(doctype=self.doctype_name, file_type=type) + exp.build_response() + + self.assertEqual(frappe.response['doctype'], self.doctype_name) + self.assertTrue(frappe.response['result']) + + if type == 'csv': + self.assertEqual(frappe.response['type'],'csv') + elif type == 'Excel': + self.assertEqual(frappe.response['type'],'binary') + self.assertEqual(frappe.response['filename'], self.doctype_name+'.xlsx') # 'Test DocType for Export Tool.xlsx') + self.assertTrue(frappe.response['filecontent']) + + def tearDown(self): + pass + diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py index 8013f9df6f..5c445fd058 100644 --- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py @@ -5,6 +5,7 @@ import frappe from frappe.model.document import Document from frappe.utils.data import evaluate_filters +from frappe.model.naming import parse_naming_series from frappe import _ class DocumentNamingRule(Document): @@ -27,7 +28,9 @@ class DocumentNamingRule(Document): return counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0 - doc.name = self.prefix + ('%0'+str(self.prefix_digits)+'d') % (counter + 1) + naming_series = parse_naming_series(self.prefix, doc=doc) + + doc.name = naming_series + ('%0'+str(self.prefix_digits)+'d') % (counter + 1) frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1) @frappe.whitelist() diff --git a/frappe/core/doctype/language/language.py b/frappe/core/doctype/language/language.py index 511c8ddeb6..69942ffd6d 100644 --- a/frappe/core/doctype/language/language.py +++ b/frappe/core/doctype/language/language.py @@ -39,7 +39,8 @@ def sync_languages(): frappe.get_doc({ 'doctype': 'Language', 'language_code': l['code'], - 'language_name': l['name'] + 'language_name': l['name'], + 'enabled': 1, }).insert() def update_language_names(): diff --git a/frappe/core/doctype/payment_gateway/payment_gateway.json b/frappe/core/doctype/payment_gateway/payment_gateway.json index b97d72c771..f548b0dffb 100644 --- a/frappe/core/doctype/payment_gateway/payment_gateway.json +++ b/frappe/core/doctype/payment_gateway/payment_gateway.json @@ -1,154 +1,57 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:gateway", - "beta": 0, - "creation": "2015-12-15 22:26:45.221162", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "autoname": "field:gateway", + "creation": "2022-01-24 21:09:47.229371", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "gateway", + "gateway_settings", + "gateway_controller" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gateway", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Gateway", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "gateway", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Gateway", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gateway_settings", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Gateway Settings", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "gateway_settings", + "fieldtype": "Link", + "label": "Gateway Settings", + "options": "DocType", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gateway_controller", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Gateway Controller", - "length": 0, - "no_copy": 0, - "options": "gateway_settings", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "gateway_controller", + "fieldtype": "Dynamic Link", + "label": "Gateway Controller", + "options": "gateway_settings", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-02-05 14:24:33.526645", - "modified_by": "Administrator", - "module": "Core", - "name": "Payment Gateway", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2022-01-24 21:17:03.864719", + "modified_by": "Administrator", + "module": "Core", + "name": "Payment Gateway", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "create": 1, + "delete": 1, + "read": 1, + "role": "System Manager", + "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json index ba82e023a9..e370082fb5 100644 --- a/frappe/core/doctype/role/role.json +++ b/frappe/core/doctype/role/role.json @@ -12,6 +12,7 @@ "restrict_to_domain", "column_break_4", "disabled", + "is_custom", "desk_access", "two_factor_auth", "navigation_settings_section", @@ -24,8 +25,7 @@ "form_settings_section", "form_sidebar", "timeline", - "dashboard", - "is_custom" + "dashboard" ], "fields": [ { @@ -148,7 +148,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-10-08 14:06:55.729364", + "modified": "2022-01-12 20:18:18.496230", "modified_by": "Administrator", "module": "Core", "name": "Role", @@ -170,5 +170,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "ASC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index ef7845d3b0..f1ccc25c6e 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -19,7 +19,7 @@ from frappe.core.doctype.user_type.user_type import user_linked_with_permission_ from frappe.query_builder import DocType -STANDARD_USERS = ("Guest", "Administrator") +STANDARD_USERS = frappe.STANDARD_USERS class User(Document): __new_password = None @@ -344,7 +344,7 @@ class User(Document): frappe.sendmail(recipients=self.email, sender=sender, subject=subject, template=template, args=args, header=[subject, "green"], - delayed=(not now) if now!=None else self.flags.delay_emails, retry=3) + delayed=(not now) if now is not None else self.flags.delay_emails, retry=3) def a_system_manager_should_exist(self): if not self.get_other_system_managers(): diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py index c1fd678141..661ac932e7 100644 --- a/frappe/core/doctype/user_type/user_type.py +++ b/frappe/core/doctype/user_type/user_type.py @@ -37,16 +37,14 @@ class UserType(Document): return modules = frappe.get_all("DocType", - fields=["module"], filters={"name": ("in", [d.document_type for d in self.user_doctypes])}, distinct=True, + pluck="module", ) - self.set('user_type_modules', []) - for row in modules: - self.append('user_type_modules', { - 'module': row.module - }) + self.set("user_type_modules", []) + for module in modules: + self.append("user_type_modules", {"module": module}) def validate_document_type_limit(self): limit = frappe.conf.get('user_type_doctype_limit', {}).get(frappe.scrub(self.name)) diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 24a5d1358b..488c468025 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -377,7 +377,7 @@ class CustomizeForm(Document): def make_property_setter(self, prop, value, property_type, fieldname=None, apply_on=None, row_name = None): - delete_property_setter(self.doc_type, prop, fieldname) + delete_property_setter(self.doc_type, prop, fieldname, row_name) property_value = self.get_existing_property_value(prop, fieldname) diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py index 7f40be9725..0a65aa6f5d 100644 --- a/frappe/custom/doctype/property_setter/property_setter.py +++ b/frappe/custom/doctype/property_setter/property_setter.py @@ -19,7 +19,7 @@ class PropertySetter(Document): def validate(self): self.validate_fieldtype_change() if self.is_new(): - delete_property_setter(self.doc_type, self.property, self.field_name) + delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name) # clear cache frappe.clear_cache(doctype = self.doc_type) @@ -91,11 +91,13 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for property_setter.insert() return property_setter -def delete_property_setter(doc_type, property, field_name=None): +def delete_property_setter(doc_type, property, field_name=None, row_name=None): """delete other property setters on this, if this is new""" - filters = dict(doc_type = doc_type, property=property) + filters = dict(doc_type=doc_type, property=property) if field_name: filters['field_name'] = field_name + if row_name: + filters["row_name"] = row_name frappe.db.delete('Property Setter', filters) diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 6b827a4e89..de28dad900 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -245,9 +245,16 @@ class MariaDBDatabase(Database): column_name as 'name', column_type as 'type', column_default as 'default', - column_key = 'MUL' as 'index', + COALESCE( + (select 1 + from information_schema.statistics + where table_name="{table_name}" + and column_name=columns.column_name + and NON_UNIQUE=1 + limit 1 + ), 0) as 'index', column_key = 'UNI' as 'unique' - from information_schema.columns + from information_schema.columns as columns where table_name = '{table_name}' '''.format(table_name=table_name), as_dict=1) def has_index(self, table_name, index_name): diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py index 5768a2f23d..07bb4d5d7c 100644 --- a/frappe/database/mariadb/schema.py +++ b/frappe/database/mariadb/schema.py @@ -58,18 +58,34 @@ class MariaDBTable(DBTable): modify_column_query.append("MODIFY `{}` {}".format(col.fieldname, col.get_definition())) for col in self.add_index: - # if index key not exists - if not frappe.db.sql("SHOW INDEX FROM `%s` WHERE key_name = %s" % - (self.table_name, '%s'), col.fieldname): - add_index_query.append("ADD INDEX `{}`(`{}`)".format(col.fieldname, col.fieldname)) + # if index key does not exists + if not frappe.db.has_index(self.table_name, col.fieldname + '_index'): + add_index_query.append("ADD INDEX `{}_index`(`{}`)".format(col.fieldname, col.fieldname)) - for col in self.drop_index: + for col in self.drop_index + self.drop_unique: if col.fieldname != 'name': # primary key + current_column = self.current_columns.get(col.fieldname.lower()) + unique_constraint_changed = current_column.unique != col.unique + if unique_constraint_changed and not col.unique: + # nosemgrep + unique_index_record = frappe.db.sql(""" + SHOW INDEX FROM `{0}` + WHERE Key_name=%s + AND Non_unique=0 + """.format(self.table_name), (col.fieldname), as_dict=1) + if unique_index_record: + drop_index_query.append("DROP INDEX `{}`".format(unique_index_record[0].Key_name)) + index_constraint_changed = current_column.index != col.set_index # if index key exists - if frappe.db.sql("""SHOW INDEX FROM `{0}` - WHERE key_name=%s - AND Non_unique=%s""".format(self.table_name), (col.fieldname, col.unique)): - drop_index_query.append("drop index `{}`".format(col.fieldname)) + if index_constraint_changed and not col.set_index: + # nosemgrep + index_record = frappe.db.sql(""" + SHOW INDEX FROM `{0}` + WHERE Key_name=%s + AND Non_unique=1 + """.format(self.table_name), (col.fieldname + '_index'), as_dict=1) + if index_record: + drop_index_query.append("DROP INDEX `{}`".format(index_record[0].Key_name)) try: for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]: diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 0ce6fbb265..d5495c6879 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -74,10 +74,16 @@ class PostgresDatabase(Database): return conn def escape(self, s, percent=True): - """Excape quotes and percent in given string.""" + """Escape quotes and percent in given string.""" if isinstance(s, bytes): s = s.decode('utf-8') + # MariaDB's driver treats None as an empty string + # So Postgres should do the same + + if s is None: + s = '' + if percent: s = s.replace("%", "%%") @@ -302,18 +308,20 @@ class PostgresDatabase(Database): WHEN 'timestamp without time zone' THEN 'timestamp' ELSE a.data_type END AS type, - COUNT(b.indexdef) AS Index, + BOOL_OR(b.index) AS index, SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default, BOOL_OR(b.unique) AS unique FROM information_schema.columns a LEFT JOIN - (SELECT indexdef, tablename, indexdef LIKE '%UNIQUE INDEX%' AS unique + (SELECT indexdef, tablename, + indexdef LIKE '%UNIQUE INDEX%' AS unique, + indexdef NOT LIKE '%UNIQUE INDEX%' AS index FROM pg_indexes WHERE tablename='{table_name}') b - ON SUBSTRING(b.indexdef, '\(.*\)') LIKE CONCAT('%', a.column_name, '%') + ON SUBSTRING(b.indexdef, '(.*)') LIKE CONCAT('%', a.column_name, '%') WHERE a.table_name = '{table_name}' - GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length;''' - .format(table_name=table_name), as_dict=1) + GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length; + '''.format(table_name=table_name), as_dict=1) def get_database_list(self, target): return [d[0] for d in self.sql("SELECT datname FROM pg_database;")] diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py index 58153ca6ce..a2d5be0b70 100644 --- a/frappe/database/postgres/schema.py +++ b/frappe/database/postgres/schema.py @@ -11,8 +11,6 @@ class PostgresTable(DBTable): column_defs = self.get_column_definitions() if column_defs: add_text += ',\n'.join(column_defs) - # index - # index_defs = self.get_index_definitions() # TODO: set docstatus length # create table frappe.db.sql("""create table `%s` ( @@ -28,8 +26,25 @@ class PostgresTable(DBTable): idx bigint not null default '0', %s)""".format(varchar_len=frappe.db.VARCHAR_LEN) % (self.table_name, add_text)) + self.create_indexes() frappe.db.commit() + def create_indexes(self): + create_index_query = "" + for key, col in self.columns.items(): + if (col.set_index + and col.fieldtype in frappe.db.type_map + and frappe.db.type_map.get(col.fieldtype)[0] + not in ('text', 'longtext')): + create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( + index_name=col.fieldname, + table_name=self.table_name, + field=col.fieldname + ) + if create_index_query: + # nosemgrep + frappe.db.sql(create_index_query) + def alter(self): for col in self.columns.values(): col.build_for_alter_table(self.current_columns.get(col.fieldname.lower())) @@ -52,8 +67,8 @@ class PostgresTable(DBTable): query.append("ALTER COLUMN `{0}` TYPE {1} {2}".format( col.fieldname, get_definition(col.fieldtype, precision=col.precision, length=col.length), - using_clause) - ) + using_clause + )) for col in self.set_default: if col.fieldname=="name": @@ -73,37 +88,54 @@ class PostgresTable(DBTable): query.append("ALTER COLUMN `{}` SET DEFAULT {}".format(col.fieldname, col_default)) - create_index_query = "" + create_contraint_query = "" for col in self.add_index: # if index key not exists - create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( + create_contraint_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( index_name=col.fieldname, table_name=self.table_name, field=col.fieldname) - drop_index_query = "" + for col in self.add_unique: + # if index key not exists + create_contraint_query += 'CREATE UNIQUE INDEX IF NOT EXISTS "unique_{index_name}" ON `{table_name}`(`{field}`);'.format( + index_name=col.fieldname, + table_name=self.table_name, + field=col.fieldname + ) + + drop_contraint_query = "" for col in self.drop_index: # primary key if col.fieldname != 'name': # if index key exists - if not frappe.db.has_index(self.table_name, col.fieldname): - drop_index_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname) + drop_contraint_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname) - if query: - try: + for col in self.drop_unique: + # primary key + if col.fieldname != 'name': + # if index key exists + drop_contraint_query += 'DROP INDEX IF EXISTS "unique_{}" ;'.format(col.fieldname) + try: + if query: final_alter_query = "ALTER TABLE `{}` {}".format(self.table_name, ", ".join(query)) - if final_alter_query: frappe.db.sql(final_alter_query) - if create_index_query: frappe.db.sql(create_index_query) - if drop_index_query: frappe.db.sql(drop_index_query) - except Exception as e: - # sanitize - if frappe.db.is_duplicate_fieldname(e): - frappe.throw(str(e)) - elif frappe.db.is_duplicate_entry(e): - fieldname = str(e).split("'")[-2] - frappe.throw(_("""{0} field cannot be set as unique in {1}, - as there are non-unique existing values""".format( - fieldname, self.table_name))) - raise e - else: - raise e + # nosemgrep + frappe.db.sql(final_alter_query) + if create_contraint_query: + # nosemgrep + frappe.db.sql(create_contraint_query) + if drop_contraint_query: + # nosemgrep + frappe.db.sql(drop_contraint_query) + except Exception as e: + # sanitize + if frappe.db.is_duplicate_fieldname(e): + frappe.throw(str(e)) + elif frappe.db.is_duplicate_entry(e): + fieldname = str(e).split("'")[-2] + frappe.throw( + _("{0} field cannot be set as unique in {1}, as there are non-unique existing values") + .format(fieldname, self.table_name) + ) + else: + raise e diff --git a/frappe/database/query.py b/frappe/database/query.py index 6d2be5fa25..587378b32f 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -308,7 +308,7 @@ class Permission: doctype = [doctype] for dt in doctype: - dt = re.sub("tab", "", dt) + dt = re.sub("^tab", "", dt) if not frappe.has_permission( dt, "select", diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 10582eff8f..1767f90af0 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -21,6 +21,7 @@ class DBTable: self.change_name = [] self.add_unique = [] self.add_index = [] + self.drop_unique = [] self.drop_index = [] self.set_default = [] @@ -219,8 +220,10 @@ class DbColumn: self.table.change_type.append(self) # unique - if((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')): + if ((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')): self.table.add_unique.append(self) + elif (current_def['unique'] and not self.unique): + self.table.drop_unique.append(self) # default if (self.default_changed(current_def) @@ -230,9 +233,7 @@ class DbColumn: self.table.set_default.append(self) # index should be applied or dropped irrespective of type change - if ((current_def['index'] and not self.set_index and not self.unique) - or (current_def['unique'] and not self.unique)): - # to drop unique you have to drop index + if (current_def['index'] and not self.set_index): self.table.drop_index.append(self) elif (not current_def['index'] and self.set_index) and not (column_type in ('text', 'longtext')): diff --git a/frappe/defaults.py b/frappe/defaults.py index eb98db449f..e249ef2099 100644 --- a/frappe/defaults.py +++ b/frappe/defaults.py @@ -126,7 +126,7 @@ def set_default(key, value, parent, parenttype="__default"): "defkey": key, "parent": parent }) - if value != None: + if value is not None: add_default(key, value, parent) else: _clear_cache(parent) @@ -187,7 +187,7 @@ def get_defaults_for(parent="__default"): """get all defaults""" defaults = frappe.cache().hget("defaults", parent) - if defaults==None: + if defaults is None: # sort descending because first default must get precedence table = DocType("DefaultValue") res = frappe.qb.from_(table).where( diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py index b512ca175c..a0523d90cd 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.py +++ b/frappe/desk/doctype/bulk_update/bulk_update.py @@ -7,6 +7,7 @@ from frappe.model.document import Document from frappe import _ from frappe.utils import cint + class BulkUpdate(Document): pass @@ -22,7 +23,7 @@ def update(doctype, field, value, condition='', limit=500): frappe.throw(_('; not allowed in condition')) docnames = frappe.db.sql_list( - '''select name from `tab{0}`{1} limit 0, {2}'''.format(doctype, condition, limit) + '''select name from `tab{0}`{1} limit {2} offset 0'''.format(doctype, condition, limit) ) data = {} data[field] = value diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index 0dfd458a37..ac62796dc2 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -1,23 +1,33 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2019, Frappe Technologies and contributors +# Copyright (c) 2022, Frappe Technologies and contributors # License: MIT. See LICENSE -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 json + import frappe from frappe import _ -import json +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 +from frappe.query_builder import DocType + class Dashboard(Document): def on_update(self): if self.is_default: # make all other dashboards non-default - frappe.db.sql('''update - tabDashboard set is_default = 0 where name != %s''', self.name) + DashBoard = DocType("Dashboard") + + frappe.qb.update(DashBoard).set( + DashBoard.is_default, 0 + ).where( + DashBoard.name != self.name + ).run() if frappe.conf.developer_mode and self.is_standard: - export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module) + export_to_files( + record_list=[["Dashboard", self.name, f"{self.module} Dashboard"]], + record_module=self.module + ) def validate(self): if not frappe.conf.developer_mode and self.is_standard: diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 0e644c3cf5..38b671d629 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -94,30 +94,78 @@ def get_docinfo(doc=None, doctype=None, name=None): automated_messages = filter(lambda x: x['communication_type'] == 'Automated Message', all_communications) communications_except_auto_messages = filter(lambda x: x['communication_type'] != 'Automated Message', all_communications) - frappe.response["docinfo"] = { + docinfo = frappe._dict(user_info = {}) + + add_comments(doc, docinfo) + + docinfo.update({ "attachments": get_attachments(doc.doctype, doc.name), - "attachment_logs": get_comments(doc.doctype, doc.name, 'attachment'), "communications": communications_except_auto_messages, "automated_messages": automated_messages, - 'comments': get_comments(doc.doctype, doc.name), 'total_comments': len(json.loads(doc.get('_comments') or '[]')), 'versions': get_versions(doc), "assignments": get_assignments(doc.doctype, doc.name), - "assignment_logs": get_comments(doc.doctype, doc.name, 'assignment'), "permissions": get_doc_permissions(doc), "shared": frappe.share.get_users(doc.doctype, doc.name), - "info_logs": get_comments(doc.doctype, doc.name, comment_type=['Info', 'Edit', 'Label']), - "share_logs": get_comments(doc.doctype, doc.name, 'share'), - "like_logs": get_comments(doc.doctype, doc.name, 'Like'), - "workflow_logs": get_comments(doc.doctype, doc.name, comment_type="Workflow"), "views": get_view_logs(doc.doctype, doc.name), "energy_point_logs": get_point_logs(doc.doctype, doc.name), "additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name), "milestones": get_milestones(doc.doctype, doc.name), "is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user), "tags": get_tags(doc.doctype, doc.name), - "document_email": get_document_email(doc.doctype, doc.name) - } + "document_email": get_document_email(doc.doctype, doc.name), + }) + + update_user_info(docinfo) + + frappe.response["docinfo"] = docinfo + +def add_comments(doc, docinfo): + # divide comments into separate lists + docinfo.comments = [] + docinfo.shared = [] + docinfo.assignment_logs = [] + docinfo.attachment_logs = [] + docinfo.info_logs = [] + docinfo.like_logs = [] + docinfo.workflow_logs = [] + + comments = frappe.get_all("Comment", + fields=["name", "creation", "content", "owner", "comment_type"], + filters={ + "reference_doctype": doc.doctype, + "reference_name": doc.name + } + ) + + for c in comments: + if c.comment_type == "Comment": + c.content = frappe.utils.markdown(c.content) + docinfo.comments.append(c) + + elif c.comment_type in ('Shared', 'Unshared'): + docinfo.shared.append(c) + + elif c.comment_type in ('Assignment Completed', 'Assigned'): + docinfo.assignment_logs.append(c) + + elif c.comment_type in ('Attachment', 'Attachment Removed'): + docinfo.attachment_logs.append(c) + + elif c.comment_type in ('Info', 'Edit', 'Label'): + docinfo.info_logs.append(c) + + elif c.comment_type == "Like": + docinfo.like_logs.append(c) + + elif c.comment_type == "Workflow": + docinfo.workflow_logs.append(c) + + frappe.utils.add_user_info(c.owner, docinfo.user_info) + + + return comments + def get_milestones(doctype, name): return frappe.db.get_all('Milestone', fields = ['creation', 'owner', 'track_field', 'value'], @@ -252,7 +300,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= return communications def get_assignments(dt, dn): - cl = frappe.get_all("ToDo", + return frappe.get_all("ToDo", fields=['name', 'allocated_to as owner', 'description', 'status'], filters={ 'reference_type': dt, @@ -260,8 +308,6 @@ def get_assignments(dt, dn): 'status': ('!=', 'Cancelled'), }) - return cl - @frappe.whitelist() def get_badge_info(doctypes, filters): filters = json.loads(filters) @@ -319,3 +365,24 @@ def get_additional_timeline_content(doctype, docname): contents.extend(frappe.get_attr(method)(doctype, docname) or []) return contents + +def update_user_info(docinfo): + for d in docinfo.communications: + frappe.utils.add_user_info(d.sender, docinfo.user_info) + + for d in docinfo.shared: + frappe.utils.add_user_info(d.user, docinfo.user_info) + + for d in docinfo.assignments: + frappe.utils.add_user_info(d.owner, docinfo.user_info) + + for d in docinfo.views: + frappe.utils.add_user_info(d.owner, docinfo.user_info) + +@frappe.whitelist() +def get_user_info_for_viewers(users): + user_info = {} + for user in json.loads(users): + frappe.utils.add_user_info(user, user_info) + + return user_info diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py index e2e2c4c155..7a9c211c3c 100644 --- a/frappe/desk/moduleview.py +++ b/frappe/desk/moduleview.py @@ -524,7 +524,7 @@ def get_last_modified(doctype): raise # hack: save as -1 so that it is cached - if last_modified==None: + if last_modified is None: last_modified = -1 return last_modified diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index b5f0c5043c..b42d8c58b7 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -388,7 +388,6 @@ def make_records(records, debug=False): # LOG every success and failure for record in records: - doctype = record.get("doctype") condition = record.get('__condition') @@ -405,6 +404,7 @@ def make_records(records, debug=False): try: doc.insert(ignore_permissions=True) + frappe.db.commit() except frappe.DuplicateEntryError as e: # print("Failed to insert duplicate {0} {1}".format(doctype, doc.name)) @@ -417,6 +417,7 @@ def make_records(records, debug=False): raise except Exception as e: + frappe.db.rollback() exception = record.get('__exception') if exception: config = _dict(exception) diff --git a/frappe/desk/page/user_profile/user_profile.py b/frappe/desk/page/user_profile/user_profile.py index 73df6d78cb..0d91fd0d91 100644 --- a/frappe/desk/page/user_profile/user_profile.py +++ b/frappe/desk/page/user_profile/user_profile.py @@ -30,7 +30,7 @@ def get_energy_points_percentage_chart_data(user, field): as_list = True) return { - "labels": [r[0] for r in result if r[0] != None], + "labels": [r[0] for r in result if r[0] is not None], "datasets": [{ "values": [r[1] for r in result] }] diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index fb150e4bea..e81ed0767b 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -12,7 +12,7 @@ from io import StringIO from frappe.core.doctype.access_log.access_log import make_access_log from frappe.utils import cstr, format_duration from frappe.model.base_document import get_controller - +from frappe.utils import add_user_info @frappe.whitelist() @frappe.read_only() @@ -219,6 +219,8 @@ def compress(data, args=None): """separate keys and values""" from frappe.desk.query_report import add_total_row + user_info = {} + if not data: return data if args is None: args = {} @@ -230,13 +232,19 @@ def compress(data, args=None): new_row.append(row.get(key)) values.append(new_row) + # add user info for assignments (avatar) + if row._assign: + for user in json.loads(row._assign): + add_user_info(user, user_info) + if args.get("add_total_row"): meta = frappe.get_meta(args.doctype) values = add_total_row(values, keys, meta) return { "keys": keys, - "values": values + "values": values, + "user_info": user_info } @frappe.whitelist() diff --git a/frappe/desk/search.py b/frappe/desk/search.py index db88e6ec52..95397070ae 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -107,7 +107,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, else: filters.append([doctype, f[0], "=", f[1]]) - if filters==None: + if filters is None: filters = [] or_filters = [] diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index f40c135653..7e3efb5d48 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -4,6 +4,7 @@ import frappe from frappe import _ + @frappe.whitelist() def get_all_nodes(doctype, label, parent, tree_method, **filters): '''Recursively gets all data from tree nodes''' @@ -40,8 +41,8 @@ def get_children(doctype, parent='', **filters): def _get_children(doctype, parent='', ignore_permissions=False): parent_field = 'parent_' + doctype.lower().replace(' ', '_') - filters = [['ifnull(`{0}`,"")'.format(parent_field), '=', parent], - ['docstatus', '<' ,'2']] + filters = [["ifnull(`{0}`,'')".format(parent_field), '=', parent], + ['docstatus', '<' ,2]] meta = frappe.get_meta(doctype) diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index d89a3d83be..9730004065 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -475,28 +475,20 @@ class QueueBuilder: if self._unsubscribed_user_emails is not None: return self._unsubscribed_user_emails - all_ids = tuple(set(self.recipients + self.cc)) - - unsubscribed = frappe.db.sql_list(''' - SELECT - distinct email - from - `tabEmail Unsubscribe` - where - email in %(all_ids)s - and ( - ( - reference_doctype = %(reference_doctype)s - and reference_name = %(reference_name)s - ) - or global_unsubscribe = 1 - ) - ''', { - 'all_ids': all_ids, - 'reference_doctype': self.reference_doctype, - 'reference_name': self.reference_name, - }) - + all_ids = list(set(self.recipients + self.cc)) + + EmailUnsubscribe = frappe.qb.DocType("Email Unsubscribe") + + unsubscribed = (frappe.qb.from_(EmailUnsubscribe) + .select(EmailUnsubscribe.email) + .where(EmailUnsubscribe.email.isin(all_ids) & + ( + ( + (EmailUnsubscribe.reference_doctype == self.reference_doctype) & (EmailUnsubscribe.reference_name == self.reference_name) + ) | EmailUnsubscribe.global_unsubscribe == 1 + ) + ).distinct() + ).run(pluck=True) self._unsubscribed_user_emails = unsubscribed or [] return self._unsubscribed_user_emails diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 4f4ed6d48e..dd64d0df80 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -27,11 +27,7 @@ from frappe.utils.html_utils import clean_email_html # fix due to a python bug in poplib that limits it to 2048 poplib._MAXLINE = 20480 -imaplib._MAXLINE = 20480 -# fix due to a python bug in poplib that limits it to 2048 -poplib._MAXLINE = 20480 -imaplib._MAXLINE = 20480 class EmailSizeExceededError(frappe.ValidationError): pass diff --git a/frappe/installer.py b/frappe/installer.py old mode 100755 new mode 100644 index b50fa4a3b5..b24dd7db41 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -154,7 +154,7 @@ def install_app(name, verbose=False, set_as_patched=True): for before_install in app_hooks.before_install or []: out = frappe.get_attr(before_install)() - if out==False: + if out is False: return if name != "frappe": diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index b5330f4d4f..b66cd9014b 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -96,7 +96,7 @@ }, { "fieldname": "authorization_uri", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Authorization URI", "mandatory_depends_on": "eval:doc.redirect_uri" }, @@ -139,7 +139,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2021-05-10 05:03:06.296863", + "modified": "2022-01-07 05:28:45.073041", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index eeef552a8a..11e97a38b9 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -172,7 +172,7 @@ class BaseDocument(object): ... }) """ - if value==None: + if value is None: value={} if isinstance(value, (dict, BaseDocument)): if not self.__dict__.get(key): @@ -272,7 +272,7 @@ class BaseDocument(object): )): d[fieldname] = str(d[fieldname]) - if d[fieldname] == None and ignore_nulls: + if d[fieldname] is None and ignore_nulls: del d[fieldname] return d @@ -646,8 +646,6 @@ class BaseDocument(object): value, comma_options)) def _validate_data_fields(self): - from frappe.core.doctype.user.user import STANDARD_USERS - # data_field options defined in frappe.model.data_field_options for data_field in self.meta.get_data_fields(): data = self.get(data_field.fieldname) @@ -658,7 +656,7 @@ class BaseDocument(object): continue if data_field_options == "Email": - if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS): + if (self.owner in frappe.STANDARD_USERS) and (data in frappe.STANDARD_USERS): continue for email_address in frappe.utils.split_emails(data): frappe.utils.validate_email_address(email_address, throw=True) @@ -768,7 +766,9 @@ class BaseDocument(object): else: self_value = self.get_value(key) - + # Postgres stores values as `datetime.time`, MariaDB as `timedelta` + if isinstance(self_value, datetime.timedelta) and isinstance(db_value, datetime.time): + db_value = datetime.timedelta(hours=db_value.hour, minutes=db_value.minute, seconds=db_value.second, microseconds=db_value.microsecond) if self_value != db_value: frappe.throw(_("Not allowed to change {0} after submission").format(df.label), frappe.UpdateAfterSubmitError) @@ -1008,15 +1008,12 @@ def _filter(data, filters, limit=None): _filters[f] = fval for d in data: - add = True for f, fval in _filters.items(): if not frappe.compare(getattr(d, f, None), fval[0], fval[1]): - add = False break - - if add: + else: out.append(d) - if limit and (len(out)-1)==limit: + if limit and len(out) >= limit: break return out diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index cb2c2af898..79be261981 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -130,6 +130,11 @@ class DatabaseQuery(object): args.fields = 'distinct ' + args.fields args.order_by = '' # TODO: recheck for alternative + # Postgres requires any field that appears in the select clause to also + # appear in the order by and group by clause + if frappe.db.db_type == 'postgres' and args.order_by and args.group_by: + args = self.prepare_select_args(args) + query = """select %(fields)s from %(tables)s %(conditions)s @@ -203,6 +208,19 @@ class DatabaseQuery(object): return args + def prepare_select_args(self, args): + order_field = re.sub(r"\ order\ by\ |\ asc|\ ASC|\ desc|\ DESC", "", args.order_by) + + if order_field not in args.fields: + extracted_column = order_column = order_field.replace("`", "") + if "." in extracted_column: + extracted_column = extracted_column.split(".")[1] + + args.fields += f", MAX({extracted_column}) as `{order_column}`" + args.order_by = args.order_by.replace(order_field, f"`{order_column}`") + + return args + def parse_args(self): """Convert fields and filters from strings to list, dicts""" if isinstance(self.fields, str): @@ -527,7 +545,7 @@ class DatabaseQuery(object): elif f.operator.lower() in ("like", "not like") or (isinstance(f.value, str) and (not df or df.fieldtype not in ["Float", "Int", "Currency", "Percent", "Check"])): - value = "" if f.value==None else f.value + value = "" if f.value is None else f.value fallback = "''" if f.operator.lower() in ("like", "not like") and isinstance(value, str): diff --git a/frappe/model/document.py b/frappe/model/document.py index e25469c68a..db4b7703ba 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -188,6 +188,8 @@ class Document(BaseDocument): is not set. :param permtype: one of `read`, `write`, `submit`, `cancel`, `delete`""" + import frappe.permissions + if self.flags.ignore_permissions: return True return frappe.permissions.has_permission(self.doctype, permtype, self, verbose=verbose) @@ -209,13 +211,13 @@ class Document(BaseDocument): self.flags.notifications_executed = [] - if ignore_permissions!=None: + if ignore_permissions is not None: self.flags.ignore_permissions = ignore_permissions - if ignore_links!=None: + if ignore_links is not None: self.flags.ignore_links = ignore_links - if ignore_mandatory!=None: + if ignore_mandatory is not None: self.flags.ignore_mandatory = ignore_mandatory self.set("__islocal", True) @@ -295,7 +297,7 @@ class Document(BaseDocument): self.flags.notifications_executed = [] - if ignore_permissions!=None: + if ignore_permissions is not None: self.flags.ignore_permissions = ignore_permissions self.flags.ignore_version = frappe.flags.in_test if ignore_version is None else ignore_version @@ -439,7 +441,7 @@ class Document(BaseDocument): values = self.as_dict() # format values for key, value in values.items(): - if value==None: + if value is None: values[key] = "" return values @@ -487,7 +489,7 @@ class Document(BaseDocument): frappe.flags.currently_saving.append((self.doctype, self.name)) def set_docstatus(self): - if self.docstatus==None: + if self.docstatus is None: self.docstatus=0 for d in self.get_all_children(): @@ -885,14 +887,14 @@ class Document(BaseDocument): if (frappe.flags.in_import and frappe.flags.mute_emails) or frappe.flags.in_patch or frappe.flags.in_install: return - if self.flags.notifications_executed==None: + if self.flags.notifications_executed is None: self.flags.notifications_executed = [] from frappe.email.doctype.notification.notification import evaluate_alert - if self.flags.notifications == None: + if self.flags.notifications is None: alerts = frappe.cache().hget('notifications', self.doctype) - if alerts==None: + if alerts is None: alerts = frappe.get_all('Notification', fields=['name', 'event', 'method'], filters={'enabled': 1, 'document_type': self.doctype}) frappe.cache().hset('notifications', self.doctype, alerts) diff --git a/frappe/model/dynamic_links.py b/frappe/model/dynamic_links.py index 7311b39b30..03f616ef60 100644 --- a/frappe/model/dynamic_links.py +++ b/frappe/model/dynamic_links.py @@ -32,7 +32,7 @@ def get_dynamic_link_map(for_delete=False): Note: Will not map single doctypes ''' - if getattr(frappe.local, 'dynamic_link_map', None)==None or frappe.flags.in_test: + if getattr(frappe.local, 'dynamic_link_map', None) is None or frappe.flags.in_test: # Build from scratch dynamic_link_map = {} for df in get_dynamic_links(): diff --git a/frappe/patches.txt b/frappe/patches.txt index af7e4d6e3f..16ae349941 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -123,6 +123,9 @@ frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats frappe.patches.v12_0.remove_example_email_thread_notify execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() frappe.patches.v12_0.set_correct_url_in_files +execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) +execute:frappe.reload_doc('custom', 'doctype', 'property_setter') +frappe.patches.v13_0.remove_invalid_options_for_data_fields frappe.patches.v13_0.website_theme_custom_scss frappe.patches.v13_0.make_user_type frappe.patches.v13_0.set_existing_dashboard_charts_as_public @@ -153,7 +156,6 @@ frappe.patches.v13_0.rename_notification_fields frappe.patches.v13_0.remove_duplicate_navbar_items frappe.patches.v13_0.set_social_icons frappe.patches.v12_0.set_default_password_reset_limit -execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) frappe.patches.v13_0.set_route_for_blog_category frappe.patches.v13_0.enable_custom_script frappe.patches.v13_0.update_newsletter_content_type @@ -179,7 +181,6 @@ frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings frappe.patches.v13_0.remove_twilio_settings frappe.patches.v12_0.rename_uploaded_files_with_proper_name frappe.patches.v13_0.queryreport_columns -execute:frappe.reload_doc('core', 'doctype', 'doctype') frappe.patches.v13_0.jinja_hook frappe.patches.v13_0.update_notification_channel_if_empty frappe.patches.v13_0.set_first_day_of_the_week diff --git a/frappe/patches/v11_0/remove_skip_for_doctype.py b/frappe/patches/v11_0/remove_skip_for_doctype.py index 1bbe74bb6d..6e66c75f68 100644 --- a/frappe/patches/v11_0/remove_skip_for_doctype.py +++ b/frappe/patches/v11_0/remove_skip_for_doctype.py @@ -33,7 +33,7 @@ def execute(): continue skip_for_doctype = user_permission.skip_for_doctype.split('\n') else: # while migrating from v10 -> v11 - if skip_for_doctype_map.get((user_permission.allow, user_permission.user)) == None: + if skip_for_doctype_map.get((user_permission.allow, user_permission.user)) is None: skip_for_doctype = get_doctypes_to_skip(user_permission.allow, user_permission.user) # cache skip for doctype for same user and doctype skip_for_doctype_map[(user_permission.allow, user_permission.user)] = skip_for_doctype diff --git a/frappe/patches/v13_0/remove_invalid_options_for_data_fields.py b/frappe/patches/v13_0/remove_invalid_options_for_data_fields.py new file mode 100644 index 0000000000..90e4b3c5c6 --- /dev/null +++ b/frappe/patches/v13_0/remove_invalid_options_for_data_fields.py @@ -0,0 +1,17 @@ +# Copyright (c) 2022, Frappe and Contributors +# License: MIT. See LICENSE + + +import frappe +from frappe.model import data_field_options + + +def execute(): + custom_field = frappe.qb.DocType('Custom Field') + (frappe.qb + .update(custom_field) + .set(custom_field.options, None) + .where( + (custom_field.fieldtype == "Data") + & (custom_field.options.notin(data_field_options))) + ).run() diff --git a/frappe/permissions.py b/frappe/permissions.py index 5faaf7dcfb..af17faba01 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -23,7 +23,7 @@ def print_has_permission_check_logs(func): frappe.flags['has_permission_check_logs'] = [] result = func(*args, **kwargs) self_perm_check = True if not kwargs.get('user') else kwargs.get('user') == frappe.session.user - raise_exception = False if kwargs.get('raise_exception') == False else True + raise_exception = False if kwargs.get('raise_exception') is False else True # print only if access denied # and if user is checking his own permission @@ -559,7 +559,9 @@ def filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc= return (allowed_doc, default_doc) if with_default_doc else allowed_doc def push_perm_check_log(log): - if frappe.flags.get('has_permission_check_logs') == None: return + if frappe.flags.get('has_permission_check_logs') is None: + return + frappe.flags.get('has_permission_check_logs').append(_(log)) def has_child_table_permission(child_doctype, ptype="read", child_doc=None, diff --git a/frappe/public/images/ui-states/empty-app-state.svg b/frappe/public/images/ui-states/empty-app-state.svg new file mode 100644 index 0000000000..b7e346f310 --- /dev/null +++ b/frappe/public/images/ui-states/empty-app-state.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 202cee645a..51ada70948 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -214,19 +214,20 @@ frappe.Application = class Application { email_password_prompt(email_account,user,i) { var me = this; + const email_id = email_account[i]["email_id"]; let d = new frappe.ui.Dialog({ title: __('Password missing in Email Account'), fields: [ { 'fieldname': 'password', 'fieldtype': 'Password', - 'label': __('Please enter the password for: {0}', [email_account[i]["email_id"]]), + 'label': __('Please enter the password for: {0}', [email_id], "Email Account"), 'reqd': 1 }, { "fieldname": "submit", "fieldtype": "Button", - "label": __("Submit") + "label": __("Submit", null, "Submit password for Email Account") } ] }); diff --git a/frappe/public/js/frappe/form/controls/date.js b/frappe/public/js/frappe/form/controls/date.js index 78eb3832cc..7ad1887d62 100644 --- a/frappe/public/js/frappe/form/controls/date.js +++ b/frappe/public/js/frappe/form/controls/date.js @@ -10,14 +10,16 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat this.set_t_for_today(); } set_formatted_input(value) { + if (value === "Today") { + value = this.get_now_date(); + } + super.set_formatted_input(value); if (this.timepicker_only) return; if (!this.datepicker) return; if (!value) { this.datepicker.clear(); return; - } else if (value === "Today") { - value = this.get_now_date(); } let should_refresh = this.last_value && this.last_value !== value; @@ -78,7 +80,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat } get_start_date() { - return new Date(this.get_now_date()); + return this.get_now_date(); } set_datepicker() { @@ -117,7 +119,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat this.datepicker.update('position', position); } get_now_date() { - return frappe.datetime.convert_to_system_tz(frappe.datetime.now_date(true)); + return frappe.datetime.convert_to_system_tz(frappe.datetime.now_date(true), false).toDate(); } set_t_for_today() { var me = this; diff --git a/frappe/public/js/frappe/form/controls/date_range.js b/frappe/public/js/frappe/form/controls/date_range.js index 727e9d55c2..170404f575 100644 --- a/frappe/public/js/frappe/form/controls/date_range.js +++ b/frappe/public/js/frappe/form/controls/date_range.js @@ -11,7 +11,8 @@ frappe.ui.form.ControlDateRange = class ControlDateRange extends frappe.ui.form. language: "en", range: true, autoClose: true, - toggleSelected: false + toggleSelected: false, + firstDay: frappe.datetime.get_first_day_of_the_week_index() }; this.datepicker_options.dateFormat = (frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd'); diff --git a/frappe/public/js/frappe/form/controls/markdown_editor.js b/frappe/public/js/frappe/form/controls/markdown_editor.js index d9ba2df261..5acf4bd467 100644 --- a/frappe/public/js/frappe/form/controls/markdown_editor.js +++ b/frappe/public/js/frappe/form/controls/markdown_editor.js @@ -32,7 +32,9 @@ frappe.ui.form.ControlMarkdownEditor = class ControlMarkdownEditor extends frapp } set_language() { - this.df.options = 'Markdown'; + if (!this.df.options) { + this.df.options = 'Markdown'; + } super.set_language(); } diff --git a/frappe/public/js/frappe/form/footer/base_timeline.js b/frappe/public/js/frappe/form/footer/base_timeline.js index beeba16459..ba7a4eb565 100644 --- a/frappe/public/js/frappe/form/footer/base_timeline.js +++ b/frappe/public/js/frappe/form/footer/base_timeline.js @@ -12,8 +12,11 @@ class BaseTimeline { this.wrapper = this.timeline_wrapper; this.timeline_items_wrapper = $(`
`); this.timeline_actions_wrapper = $(` -
-
+
+
+
+
+
`); @@ -37,7 +40,7 @@ class BaseTimeline { ${label} `); action_btn.click(action); - this.timeline_actions_wrapper.append(action_btn); + this.timeline_actions_wrapper.find('.action-buttons').append(action_btn); return action_btn; } diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index f278d1b64b..d440874f36 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -77,12 +77,14 @@ class FormTimeline extends BaseTimeline { const message = __("Add to this activity by mailing to {0}", [link.bold()]); this.document_email_link_wrapper = $(` -