diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 633c5fcfe2..96e9be8b3c 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -16,3 +16,6 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85 # Refactor "not a in b" -> "a not in b" 745297a49d516e5e3c4bb3e1b0c4235e7d31165d + +# Clean up whitespace +b2fc959307c7c79f5584625569d5aed04133ba13 diff --git a/.github/workflows/semgrep.yml b/.github/workflows/linters.yml similarity index 68% rename from .github/workflows/semgrep.yml rename to .github/workflows/linters.yml index 325411cf5c..443ee45bf7 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/linters.yml @@ -1,15 +1,24 @@ -name: Semgrep +name: Linters on: pull_request: { } jobs: - semgrep: + + linters: name: Frappe Linter runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install and Run Pre-commit + uses: pre-commit/action@v2.0.3 + - name: Download Semgrep rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..f3c3447cb3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,23 @@ +exclude: 'node_modules|.git' +default_stages: [commit] +fail_fast: false + + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: trailing-whitespace + files: "frappe.*" + exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" + - id: check-yaml + - id: no-commit-to-branch + args: ['--branch', 'develop'] + - id: check-merge-conflict + - id: check-ast + + +ci: + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/codecov.yml b/codecov.yml index bc59416d2f..1326403cfe 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,7 +3,6 @@ codecov: coverage: status: - patch: off project: default: false server: diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index f085709945..f89eb31cc8 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -166,7 +166,7 @@ class Importer: if not self.data_import.status == "Partial Success": self.data_import.db_set("status", "Partial Success") - + # commit after every successful import frappe.db.commit() diff --git a/frappe/core/doctype/doctype/boilerplate/test_controller._py b/frappe/core/doctype/doctype/boilerplate/test_controller._py index 5f4150ce9b..83a38c493d 100644 --- a/frappe/core/doctype/doctype/boilerplate/test_controller._py +++ b/frappe/core/doctype/doctype/boilerplate/test_controller._py @@ -2,7 +2,8 @@ # See license.txt # import frappe -import unittest +from frappe.tests.utils import FrappeTestCase -class Test{classname}(unittest.TestCase): + +class Test{classname}(FrappeTestCase): pass diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index 7d434ff166..a077956d71 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -314,19 +314,19 @@ result = [ { "parent_column": "Parent 1", "column_1": 200, - "column_2": 150.50 + "column_2": 150.50 }, { "parent_column": "Child 1", "column_1": 100, "column_2": 75.25, - "parent_value": "Parent 1" + "parent_value": "Parent 1" }, { "parent_column": "Child 2", "column_1": 100, "column_2": 75.25, - "parent_value": "Parent 1" + "parent_value": "Parent 1" } ] diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index e0d2cab8ef..0b93786e8e 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -495,7 +495,7 @@ frappe.ui.form.on('Dashboard Chart', { set_parent_document_type: async function(frm) { let document_type = frm.doc.document_type; - let doc_is_table = document_type && + let doc_is_table = document_type && (await frappe.db.get_value('DocType', document_type, 'istable')).message.istable; frm.set_df_property('parent_document_type', 'hidden', !doc_is_table); diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index d6390d7613..3f3fc0ff8a 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -16,7 +16,7 @@ frappe.ui.form.on('Form Tour', { frm.add_custom_button(__('Show Tour'), async () => { const issingle = await check_if_single(frm.doc.reference_doctype); let route_changed = null; - + if (issingle) { route_changed = frappe.set_route('Form', frm.doc.reference_doctype); } else if (frm.doc.first_document) { diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py index 155a925fcf..97f529a061 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.py +++ b/frappe/desk/doctype/kanban_board/kanban_board.py @@ -76,26 +76,6 @@ def archive_restore_column(board_name, column_title, status): return doc.columns -@frappe.whitelist() -def update_doc(doc): - '''Updates the doc when card is edited''' - doc = json.loads(doc) - - try: - to_update = doc - doctype = doc['doctype'] - docname = doc['name'] - doc = frappe.get_doc(doctype, docname) - doc.update(to_update) - doc.save() - except: - return { - 'doc': doc, - 'exc': frappe.utils.get_traceback() - } - return doc - - @frappe.whitelist() def update_order(board_name, order): '''Save the order of cards in columns''' diff --git a/frappe/desk/doctype/todo/todo_calendar.js b/frappe/desk/doctype/todo/todo_calendar.js index 4545846cf9..8ba020fac1 100644 --- a/frappe/desk/doctype/todo/todo_calendar.js +++ b/frappe/desk/doctype/todo/todo_calendar.js @@ -24,7 +24,7 @@ frappe.views.calendar["ToDo"] = { "options": "reference_type", "label": __("Task") } - + ], get_events_method: "frappe.desk.calendar.get_events" }; diff --git a/frappe/desk/doctype/workspace/workspace.js b/frappe/desk/doctype/workspace/workspace.js index 5377470343..3f912127fc 100644 --- a/frappe/desk/doctype/workspace/workspace.js +++ b/frappe/desk/doctype/workspace/workspace.js @@ -9,7 +9,7 @@ frappe.ui.form.on('Workspace', { refresh: function(frm) { frm.enable_save(); - if (frm.doc.for_user || (frm.doc.public && !frm.has_perm('write') && + if (frm.doc.for_user || (frm.doc.public && !frm.has_perm('write') && !frappe.user.has_role('Workspace Manager'))) { frm.trigger('disable_form'); } diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index b40f517350..f0a3531ae4 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -176,9 +176,9 @@ def update_page(name, title, icon, parent, public): doc = frappe.get_doc("Workspace", name) - filters = { + filters = { 'parent_page': doc.title, - 'public': doc.public + 'public': doc.public } child_docs = frappe.get_list("Workspace", filters=filters) @@ -255,7 +255,7 @@ def delete_page(page): def sort_pages(sb_public_items, sb_private_items): if not loads(sb_public_items) and not loads(sb_private_items): return - + sb_public_items = loads(sb_public_items) sb_private_items = loads(sb_private_items) @@ -292,7 +292,7 @@ def last_sequence_id(doc): if not doc_exists: return 0 - return frappe.db.get_list('Workspace', + return frappe.db.get_list('Workspace', fields=['sequence_id'], filters={ 'public': doc.public, diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 3fd96bdb6b..2b62530847 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -61,7 +61,7 @@ def get_context(context): """) def validate_standard(self): - if self.is_standard and not frappe.conf.developer_mode: + if self.is_standard and self.enabled and not frappe.conf.developer_mode: frappe.throw(_('Cannot edit Standard Notification. To edit, please disable this and duplicate it')) def validate_condition(self): diff --git a/frappe/email/receive.py b/frappe/email/receive.py index b8156d5d9b..8aa32fc1a5 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -630,7 +630,7 @@ class InboundMail(Email): if self.reference_document(): data['reference_doctype'] = self.reference_document().doctype data['reference_name'] = self.reference_document().name - else: + else: if append_to and append_to != 'Communication': reference_doc = self._create_reference_document(append_to) if reference_doc: diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.js b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.js index 1343faecc4..6915c5c582 100644 --- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.js +++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.js @@ -3,6 +3,6 @@ frappe.ui.form.on('Razorpay Settings', { refresh: function(frm) { - + } }); \ No newline at end of file diff --git a/frappe/model/document.py b/frappe/model/document.py index 37e70e8126..dc0fd2caf0 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -471,7 +471,7 @@ class Document(BaseDocument): # We'd probably want the creation and owner to be set via API # or Data import at some point, that'd have to be handled here - if self.is_new() and not (frappe.flags.in_patch or frappe.flags.in_migrate): + if self.is_new() and not (frappe.flags.in_install or frappe.flags.in_patch or frappe.flags.in_migrate): self.creation = self.modified self.owner = self.modified_by @@ -860,14 +860,14 @@ class Document(BaseDocument): def run_method(self, method, *args, **kwargs): """run standard triggers, plus those in hooks""" - if "flags" in kwargs: - del kwargs["flags"] - if hasattr(self, method) and hasattr(getattr(self, method), "__call__"): - fn = lambda self, *args, **kwargs: getattr(self, method)(*args, **kwargs) - else: - # hack! to run hooks even if method does not exist - fn = lambda self, *args, **kwargs: None + def fn(self, *args, **kwargs): + method_object = getattr(self, method, None) + + # Cannot have a field with same name as method + # If method found in __dict__, expect it to be callable + if method in self.__dict__ or callable(method_object): + return method_object(*args, **kwargs) fn.__name__ = str(method) out = Document.hook(fn)(self, *args, **kwargs) diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py index 7b635ac940..0a23d5b0f4 100644 --- a/frappe/modules/patch_handler.py +++ b/frappe/modules/patch_handler.py @@ -37,6 +37,7 @@ patches by using INI like file format: import configparser import time from enum import Enum +from textwrap import dedent, indent from typing import List, Optional import frappe @@ -148,21 +149,36 @@ def run_single(patchmodule=None, method=None, methodargs=None, force=False): def execute_patch(patchmodule, method=None, methodargs=None): """execute the patch""" block_user(True) - frappe.db.begin() + + if patchmodule.startswith("execute:"): + has_patch_file = False + patch = patchmodule.split("execute:")[1] + docstring = "" + else: + has_patch_file = True + patch = f"{patchmodule.split()[0]}.execute" + _patch = frappe.get_attr(patch) + docstring = _patch.__doc__ or "" + + if docstring: + docstring = "\n" + indent(dedent(docstring), "\t") + + print(f"Executing {patchmodule or methodargs} in {frappe.local.site} ({frappe.db.cur_db_name}){docstring}") + start_time = time.time() + frappe.db.begin() try: - print('Executing {patch} in {site} ({db})'.format(patch=patchmodule or str(methodargs), - site=frappe.local.site, db=frappe.db.cur_db_name)) if patchmodule: if patchmodule.startswith("finally:"): # run run patch at the end frappe.flags.final_patches.append(patchmodule) else: - if patchmodule.startswith("execute:"): - exec(patchmodule.split("execute:")[1],globals()) + if has_patch_file: + _patch() else: - frappe.get_attr(patchmodule.split()[0] + ".execute")() + exec(patch, globals()) update_patch_log(patchmodule) + elif method: method(**methodargs) @@ -174,7 +190,7 @@ def execute_patch(patchmodule, method=None, methodargs=None): frappe.db.commit() end_time = time.time() block_user(False) - print('Success: Done in {time}s'.format(time = round(end_time - start_time, 3))) + print(f"Success: Done in {round(end_time - start_time, 3)}s") return True diff --git a/frappe/patches.txt b/frappe/patches.txt index 0d2a6162c2..c889d9a4da 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -189,6 +189,7 @@ frappe.patches.v14_0.update_workspace2 # 20.09.2021 frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 frappe.patches.v14_0.transform_todo_schema frappe.patches.v14_0.remove_post_and_post_comment +frappe.patches.v14_0.reset_creation_datetime [post_model_sync] frappe.patches.v14_0.drop_data_import_legacy diff --git a/frappe/patches/v12_0/set_correct_url_in_files.py b/frappe/patches/v12_0/set_correct_url_in_files.py index 4f820c1b24..4613f88694 100644 --- a/frappe/patches/v12_0/set_correct_url_in_files.py +++ b/frappe/patches/v12_0/set_correct_url_in_files.py @@ -15,7 +15,7 @@ def execute(): for file in files: file_path = file.file_url file_name = file_path.split('/')[-1] - + if not file_path.startswith(('/private/', '/files/')): continue diff --git a/frappe/patches/v14_0/reset_creation_datetime.py b/frappe/patches/v14_0/reset_creation_datetime.py new file mode 100644 index 0000000000..54eb6c65af --- /dev/null +++ b/frappe/patches/v14_0/reset_creation_datetime.py @@ -0,0 +1,41 @@ +import glob +import json +import frappe +import os +from frappe.query_builder import DocType as _DocType + + +def execute(): + """Resetting creation datetimes for DocTypes""" + DocType = _DocType("DocType") + doctype_jsons = glob.glob( + os.path.join("..", "apps", "frappe", "frappe", "**", "doctype", "**", "*.json") + ) + + frappe_modules = frappe.get_all( + "Module Def", filters={"app_name": "frappe"}, pluck="name" + ) + site_doctypes = frappe.get_all( + "DocType", + filters={"module": ("in", frappe_modules), "custom": False}, + fields=["name", "creation"], + ) + + for dt_path in doctype_jsons: + with open(dt_path) as f: + try: + file_schema = frappe._dict(json.load(f)) + except Exception: + continue + + if not file_schema.name: + continue + + _site_schema = [x for x in site_doctypes if x.name == file_schema.name] + if not _site_schema: + continue + + if file_schema.creation != _site_schema[0].creation: + frappe.qb.update(DocType).set( + DocType.creation, file_schema.creation + ).where(DocType.name == file_schema.name).run() diff --git a/frappe/public/js/frappe/form/controls/date.js b/frappe/public/js/frappe/form/controls/date.js index 48f4f3b5ee..0f80371706 100644 --- a/frappe/public/js/frappe/form/controls/date.js +++ b/frappe/public/js/frappe/form/controls/date.js @@ -160,7 +160,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat get_df_options() { let df_options = this.df.options; if (!df_options) return {}; - + let options = {}; if (typeof df_options === 'string') { try { diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js index d8fb4bb0e9..5b7cf9421e 100644 --- a/frappe/public/js/frappe/form/controls/table.js +++ b/frappe/public/js/frappe/form/controls/table.js @@ -92,7 +92,7 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control if (frappe.model.no_value_type.includes(field.fieldtype)) { return false; } - + const is_field_matching = () => { return ( field.fieldname.toLowerCase() === field_name || diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index f9ee15692c..faf803ee54 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -88,6 +88,9 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for make_quill_editor() { if (this.quill) return; this.quill_container = $('