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 = $('
').appendTo(this.input_area); + if (this.df.max_height) { + $(this.quill_container).css({'max-height': this.df.max_height, 'overflow': 'auto'}); + } this.quill = new Quill(this.quill_container[0], this.get_quill_options()); this.bind_events(); } diff --git a/frappe/public/js/frappe/form/grid_pagination.js b/frappe/public/js/frappe/form/grid_pagination.js index 76a5f7b50b..2be708a87b 100644 --- a/frappe/public/js/frappe/form/grid_pagination.js +++ b/frappe/public/js/frappe/form/grid_pagination.js @@ -66,7 +66,7 @@ export default class GridPagination { } // only allow numbers from 0-9 and up, down, left, right arrow keys - if (charCode > 31 && (charCode < 48 || charCode > 57) && + if (charCode > 31 && (charCode < 48 || charCode > 57) && ![37, 38, 39, 40].includes(charCode)) { return false; } diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 2e9c6f970a..221a120a18 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -615,6 +615,7 @@ export default class GridRow { if (!this.doc) { $col.attr("title", txt); } + df.fieldname && $col.static_area.toggleClass('reqd', Boolean(df.reqd)); $col.df = df; $col.column_index = ci; diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index da642b7ca5..90516b7c0a 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -148,6 +148,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) { }); if (frm.is_new() && frm.meta.autoname === 'Prompt' && !frm.doc.__newname) { + has_errors = true; error_fields = [__('Name'), ...error_fields]; } diff --git a/frappe/public/js/frappe/list/list_view_select.js b/frappe/public/js/frappe/list/list_view_select.js index c89815d200..54e88ea05b 100644 --- a/frappe/public/js/frappe/list/list_view_select.js +++ b/frappe/public/js/frappe/list/list_view_select.js @@ -150,7 +150,7 @@ frappe.views.ListViewSelect = class ListViewSelect { const views_wrapper = this.sidebar.sidebar.find(".views-section"); views_wrapper.find(".sidebar-label").html(`${__(view)}`); const $dropdown = views_wrapper.find(".views-dropdown"); - + let placeholder = `${__("Select {0}", [__(view)])}`; let html = ``; diff --git a/frappe/public/js/frappe/ui/filters/field_select.js b/frappe/public/js/frappe/ui/filters/field_select.js index 0bdb9085f0..8f6d3ab89d 100644 --- a/frappe/public/js/frappe/ui/filters/field_select.js +++ b/frappe/public/js/frappe/ui/filters/field_select.js @@ -112,9 +112,9 @@ frappe.ui.FieldSelect = class FieldSelect { // main table var main_table_fields = std_filters.concat(frappe.meta.docfield_list[me.doctype]); $.each(frappe.utils.sort(main_table_fields, "label", "string"), function(i, df) { - let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ? + let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ? me.parent_doctype : me.doctype; - + // show fields where user has read access and if report hide flag is not set if (frappe.perm.has_perm(doctype, df.permlevel, "read")) me.add_field_option(df); @@ -132,9 +132,9 @@ frappe.ui.FieldSelect = class FieldSelect { } $.each(frappe.utils.sort(child_table_fields, "label", "string"), function(i, df) { - let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ? + let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ? me.parent_doctype : me.doctype; - + // show fields where user has read access and if report hide flag is not set if (frappe.perm.has_perm(doctype, df.permlevel, "read")) me.add_field_option(df); diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index dc75239ed5..ff55f5578f 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -244,7 +244,7 @@ Object.assign(frappe.utils, { }; return String(txt).replace( - /[&<>"'`=/]/g, + /[&<>"'`=/]/g, char => escape_html_mapping[char] || char ); }, @@ -262,7 +262,7 @@ Object.assign(frappe.utils, { }; return String(txt).replace( - /&|<|>|"|'|/|`|=/g, + /&|<|>|"|'|/|`|=/g, char => unescape_html_mapping[char] || char ); }, @@ -1435,7 +1435,7 @@ Object.assign(frappe.utils, { // for link titles frappe._link_titles = {}; } - + frappe._link_titles[doctype + "::" + name] = value; }, diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.js b/frappe/public/js/frappe/views/kanban/kanban_board.js index dac14936a0..58d58b27fc 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_board.js +++ b/frappe/public/js/frappe/views/kanban/kanban_board.js @@ -150,18 +150,6 @@ frappe.provide("frappe.views"); } updater.set({ cards: cards }); }, - update_doc: function(updater, doc, card) { - var state = this; - return frappe.call({ - method: method_prefix + "update_doc", - args: { doc: doc }, - freeze: true - }).then(function(r) { - var updated_doc = r.message; - var updated_card = prepare_card(card, state, updated_doc); - fluxify.doAction('update_card', updated_card); - }); - }, update_order_for_single_card: function(updater, card) { // cache original order const _cards = this.cards.slice(); diff --git a/frappe/public/js/frappe/views/reports/print_tree.html b/frappe/public/js/frappe/views/reports/print_tree.html index 9300c8df64..817c0c1e9f 100644 --- a/frappe/public/js/frappe/views/reports/print_tree.html +++ b/frappe/public/js/frappe/views/reports/print_tree.html @@ -10,14 +10,14 @@ - +