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/cypress/integration/control_autocomplete.js b/cypress/integration/control_autocomplete.js new file mode 100644 index 0000000000..3bf3e829f9 --- /dev/null +++ b/cypress/integration/control_autocomplete.js @@ -0,0 +1,57 @@ +context('Control Autocomplete', () => { + before(() => { + cy.login(); + cy.visit('/app/website'); + }); + + function get_dialog_with_autocomplete(options) { + cy.visit('/app/website'); + return cy.dialog({ + title: 'Autocomplete', + fields: [ + { + 'label': 'Select an option', + 'fieldname': 'autocomplete', + 'fieldtype': 'Autocomplete', + 'options': options || ['Option 1', 'Option 2', 'Option 3'], + } + ] + }); + } + + it('should set the valid value', () => { + get_dialog_with_autocomplete().as('dialog'); + + cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input'); + cy.wait(1000); + cy.get('@input').type('2', { delay: 300 }); + cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible'); + cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 }); + cy.get('.frappe-control[data-fieldname=autocomplete] input').blur(); + cy.get('@dialog').then(dialog => { + let value = dialog.get_value('autocomplete'); + expect(value).to.eq('Option 2'); + dialog.clear(); + }); + }); + + it('should set the valid value with different label', () => { + const options_with_label = [ + { label: "Option 1", value: "option_1" }, + { label: "Option 2", value: "option_2" } + ]; + get_dialog_with_autocomplete(options_with_label).as('dialog'); + + cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input'); + cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible'); + cy.get('@input').type('2', { delay: 300 }); + cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 }); + cy.get('.frappe-control[data-fieldname=autocomplete] input').blur(); + cy.get('@dialog').then(dialog => { + let value = dialog.get_value('autocomplete'); + expect(value).to.eq('option_2'); + dialog.clear(); + }); + }); + +}); diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 1684f26d49..c5d2257d75 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -19,36 +19,38 @@ from frappe.exceptions import SiteNotSpecifiedError @click.option('--db-type', default='mariadb', type=click.Choice(['mariadb', 'postgres']), help='Optional "postgres" or "mariadb". Default is "mariadb"') @click.option('--db-host', help='Database Host') @click.option('--db-port', type=int, help='Database Port') -@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB') -@click.option('--mariadb-root-password', help='Root password for MariaDB') +@click.option('--db-root-username', '--mariadb-root-username', help='Root username for MariaDB or PostgreSQL, Default is "root"') +@click.option('--db-root-password', '--mariadb-root-password', help='Root password for MariaDB or PostgreSQL') @click.option('--no-mariadb-socket', is_flag=True, default=False, help='Set MariaDB host to % and use TCP/IP Socket instead of using the UNIX Socket') @click.option('--admin-password', help='Administrator password for new site', default=None) @click.option('--verbose', is_flag=True, default=False, help='Verbose') @click.option('--force', help='Force restore if site/database already exists', is_flag=True, default=False) @click.option('--source_sql', help='Initiate database with a SQL file') @click.option('--install-app', multiple=True, help='Install app after installation') -def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None, - verbose=False, install_apps=None, source_sql=None, force=None, no_mariadb_socket=False, - install_app=None, db_name=None, db_password=None, db_type=None, db_host=None, db_port=None): +@click.option('--set-default', is_flag=True, default=False, help='Set the new site as default site') +def new_site(site, db_root_username=None, db_root_password=None, admin_password=None, + verbose=False, install_apps=None, source_sql=None, force=None, no_mariadb_socket=False, + install_app=None, db_name=None, db_password=None, db_type=None, db_host=None, db_port=None, + set_default=False): "Create a new site" from frappe.installer import _new_site frappe.init(site=site, new_site=True) - _new_site(db_name, site, mariadb_root_username=mariadb_root_username, - mariadb_root_password=mariadb_root_password, admin_password=admin_password, - verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force, - no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host, - db_port=db_port, new_site=True) + _new_site(db_name, site, db_root_username=db_root_username, + db_root_password=db_root_password, admin_password=admin_password, + verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force, + no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host, + db_port=db_port, new_site=True) - if len(frappe.utils.get_sites()) == 1: + if set_default: use(site) @click.command('restore') @click.argument('sql-file-path') -@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB') -@click.option('--mariadb-root-password', help='Root password for MariaDB') +@click.option('--db-root-username', '--mariadb-root-username', help='Root username for MariaDB or PostgreSQL, Default is "root"') +@click.option('--db-root-password', '--mariadb-root-password', help='Root password for MariaDB or PostgreSQL') @click.option('--db-name', help='Database name for site in case it is a new one') @click.option('--admin-password', help='Administrator password for new site') @click.option('--install-app', multiple=True, help='Install app after installation') @@ -57,7 +59,7 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin @click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended') @click.option('--encryption-key', help='Backup encryption key') @pass_context -def restore(context, sql_file_path, encryption_key=None, mariadb_root_username=None, mariadb_root_password=None, +def restore(context, sql_file_path, encryption_key=None, db_root_username=None, db_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None): "Restore site database from an sql file" @@ -150,8 +152,8 @@ def restore(context, sql_file_path, encryption_key=None, mariadb_root_username=N try: - _new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username, - mariadb_root_password=mariadb_root_password, admin_password=admin_password, + _new_site(frappe.conf.db_name, site, db_root_username=db_root_username, + db_root_password=db_root_password, admin_password=admin_password, verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name, force=True, db_type=frappe.conf.db_type) @@ -290,16 +292,16 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None): @click.command('reinstall') @click.option('--admin-password', help='Administrator Password for reinstalled site') -@click.option('--mariadb-root-username', help='Root username for MariaDB') -@click.option('--mariadb-root-password', help='Root password for MariaDB') +@click.option('--db-root-username', '--mariadb-root-username', help='Root username for MariaDB or PostgreSQL, Default is "root"') +@click.option('--db-root-password', '--mariadb-root-password', help='Root password for MariaDB or PostgreSQL') @click.option('--yes', is_flag=True, default=False, help='Pass --yes to skip confirmation') @pass_context -def reinstall(context, admin_password=None, mariadb_root_username=None, mariadb_root_password=None, yes=False): +def reinstall(context, admin_password=None, db_root_username=None, db_root_password=None, yes=False): "Reinstall site ie. wipe all data and start over" site = get_site(context) - _reinstall(site, admin_password, mariadb_root_username, mariadb_root_password, yes, verbose=context.verbose) + _reinstall(site, admin_password, db_root_username, db_root_password, yes, verbose=context.verbose) -def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_root_password=None, yes=False, verbose=False): +def _reinstall(site, admin_password=None, db_root_username=None, db_root_password=None, yes=False, verbose=False): from frappe.installer import _new_site if not yes: @@ -319,7 +321,7 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro frappe.init(site=site) _new_site(frappe.conf.db_name, site, verbose=verbose, force=True, reinstall=True, install_apps=installed, - mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, + db_root_username=db_root_username, db_root_password=db_root_password, admin_password=admin_password) @click.command('install-app') @@ -656,16 +658,16 @@ def uninstall(context, app, dry_run, yes, no_backup, force): @click.command('drop-site') @click.argument('site') -@click.option('--root-login', default='root') -@click.option('--root-password') +@click.option('--db-root-username', '--mariadb-root-username', '--root-login', help='Root username for MariaDB or PostgreSQL, Default is "root"') +@click.option('--db-root-password', '--mariadb-root-password', '--root-password', help='Root password for MariaDB or PostgreSQL') @click.option('--archived-sites-path') @click.option('--no-backup', is_flag=True, default=False) @click.option('--force', help='Force drop-site even if an error is encountered', is_flag=True, default=False) -def drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False, no_backup=False): - _drop_site(site, root_login, root_password, archived_sites_path, force, no_backup) +def drop_site(site, db_root_username='root', db_root_password=None, archived_sites_path=None, force=False, no_backup=False): + _drop_site(site, db_root_username, db_root_password, archived_sites_path, force, no_backup) -def _drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False, no_backup=False): +def _drop_site(site, db_root_username=None, db_root_password=None, archived_sites_path=None, force=False, no_backup=False): "Remove site from database and filesystem" from frappe.database import drop_user_and_database from frappe.utils.backups import scheduled_backup @@ -690,7 +692,7 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path= click.echo("\n".join(messages)) sys.exit(1) - drop_user_and_database(frappe.conf.db_name, root_login, root_password) + drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password) archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites') 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/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 6eb8cf347f..3267429298 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -99,7 +99,7 @@ "label": "Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", + "options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", "reqd": 1, "search_index": 1 }, @@ -547,7 +547,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-27 21:22:20.529072", + "modified": "2022-02-14 11:56:19.812863", "modified_by": "Administrator", "module": "Core", "name": "DocField", 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 bf63afa5c5..a077956d71 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -314,23 +314,24 @@ 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" } ] - result = add_total_row(result, columns, meta=None, report_settings=report_settings) + result = add_total_row(result, columns, meta=None, is_tree=report_settings['tree'], + parent_field=report_settings['parent_field']) self.assertEqual(result[-1][0], "Total") self.assertEqual(result[-1][1], 200) self.assertEqual(result[-1][2], 150.50) diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index e51dfda14b..f09829a688 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -122,7 +122,7 @@ "label": "Field Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", + "options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", "reqd": 1 }, { @@ -431,7 +431,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-01-27 21:47:01.065556", + "modified": "2022-02-14 15:42:21.885999", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index f1b6ab40ed..81cd38ff87 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -600,4 +600,4 @@ ALLOWED_FIELDTYPE_CHANGE = ( ('Code', 'Geolocation'), ('Table', 'Table MultiSelect')) -ALLOWED_OPTIONS_CHANGE = ('Read Only', 'HTML', 'Select', 'Data') +ALLOWED_OPTIONS_CHANGE = ('Read Only', 'HTML', 'Data') diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 5906cd3bcf..1cc4c9f623 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -85,7 +85,7 @@ "label": "Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", + "options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", "reqd": 1, "search_index": 1 }, @@ -450,7 +450,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-02-08 19:38:16.111199", + "modified": "2022-02-25 16:01:12.616736", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", @@ -460,4 +460,4 @@ "sort_field": "modified", "sort_order": "ASC", "states": [] -} +} \ No newline at end of file diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index de28dad900..b5971e236e 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -52,7 +52,8 @@ class MariaDBDatabase(Database): 'Barcode': ('longtext', ''), 'Geolocation': ('longtext', ''), 'Duration': ('decimal', '21,9'), - 'Icon': ('varchar', self.VARCHAR_LEN) + 'Icon': ('varchar', self.VARCHAR_LEN), + 'Autocomplete': ('varchar', self.VARCHAR_LEN), } def get_connection(self): diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index a3266242a5..b0793fcbf0 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -62,7 +62,8 @@ class PostgresDatabase(Database): 'Barcode': ('text', ''), 'Geolocation': ('text', ''), 'Duration': ('decimal', '21,9'), - 'Icon': ('varchar', self.VARCHAR_LEN) + 'Icon': ('varchar', self.VARCHAR_LEN), + 'Autocomplete': ('varchar', self.VARCHAR_LEN), } def get_connection(self): diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index 19ba681237..b3b2e0fd41 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -4,7 +4,7 @@ import frappe def setup_database(force, source_sql=None, verbose=False): - root_conn = get_root_connection() + root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password) root_conn.commit() root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(frappe.conf.db_name)) root_conn.sql("DROP USER IF EXISTS {0}".format(frappe.conf.db_name)) @@ -70,7 +70,7 @@ def import_db_from_sql(source_sql=None, verbose=False): print(f"\nSTDOUT by psql:\n{restore_proc.stdout.decode()}\nImported from Database File: {source_sql}") def setup_help_database(help_db_name): - root_conn = get_root_connection() + root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password) root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(help_db_name)) root_conn.sql("DROP USER IF EXISTS {0}".format(help_db_name)) root_conn.sql("CREATE DATABASE `{0}`".format(help_db_name)) diff --git a/frappe/database/query.py b/frappe/database/query.py index 587378b32f..15ab85ff56 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -244,7 +244,13 @@ class Query: _operator = OPERATOR_MAP[value[0]] conditions = conditions.where(_operator(Field(key), value[1])) else: - conditions = conditions.where(_operator(Field(key), value)) + if value is not None: + conditions = conditions.where(_operator(Field(key), value)) + else: + _table = conditions._from[0] + field = getattr(_table, key) + conditions = conditions.where(field.isnull()) + conditions = self.add_conditions(conditions, **kwargs) return conditions 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/desk/form/load.py b/frappe/desk/form/load.py index 4d35ebf5e8..b5dfacb1d6 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -11,8 +11,10 @@ from frappe.model.utils.user_settings import get_user_settings from frappe.permissions import get_doc_permissions from frappe.desk.form.document_follow import is_document_followed from frappe import _ +from frappe import _dict from urllib.parse import quote + @frappe.whitelist() def getdoc(doctype, name, user=None): """ @@ -50,8 +52,11 @@ def getdoc(doctype, name, user=None): doc.add_seen() set_link_titles(doc) + if frappe.response.docs is None: + frappe.response = _dict({"docs": []}) frappe.response.docs.append(doc) + @frappe.whitelist() def getdoctype(doctype, with_parent=False, cached_timestamp=None): """load doctype""" diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 74101a6e1f..0c32e886f4 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -392,7 +392,7 @@ def make_records(records, debug=False): doc.flags.ignore_mandatory = True try: - doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + doc.insert(ignore_permissions=True) frappe.db.commit() except frappe.DuplicateEntryError as e: diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 9ed956e986..a0b0aec255 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -73,7 +73,7 @@ def get_report_result(report, filters): return res @frappe.read_only() -def generate_report_result(report, filters=None, user=None, custom_columns=None, report_settings=None): +def generate_report_result(report, filters=None, user=None, custom_columns=None, is_tree=False, parent_field=None): user = user or frappe.session.user filters = filters or [] @@ -108,7 +108,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None, result = get_filtered_data(report.ref_doctype, columns, result, user) if cint(report.add_total_row) and result and not skip_total_row: - result = add_total_row(result, columns, report_settings=report_settings) + result = add_total_row(result, columns, is_tree=is_tree, parent_field=parent_field) return { "result": result, @@ -210,7 +210,7 @@ def get_script(report_name): @frappe.whitelist() @frappe.read_only() -def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, report_settings=None): +def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, is_tree=False, parent_field=None): report = get_report_doc(report_name) if not user: user = frappe.session.user @@ -238,7 +238,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False, cust dn = "" result = get_prepared_report_result(report, filters, dn, user) else: - result = generate_report_result(report, filters, user, custom_columns, report_settings) + result = generate_report_result(report, filters, user, custom_columns, is_tree, parent_field) result["add_total_row"] = report.add_total_row and not result.get( "skip_total_row", False @@ -435,18 +435,9 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi return result, column_widths -def add_total_row(result, columns, meta=None, report_settings=None): +def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None): total_row = [""] * len(columns) has_percent = [] - is_tree = False - parent_field = '' - - if report_settings: - if isinstance(report_settings, (str,)): - report_settings = json.loads(report_settings) - - is_tree = report_settings.get('tree') - parent_field = report_settings.get('parent_field') for i, col in enumerate(columns): fieldtype, options, fieldname = None, None, None 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/installer.py b/frappe/installer.py index 20db451d26..6ebab95a7d 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -14,8 +14,8 @@ from frappe.defaults import _clear_cache def _new_site( db_name, site, - mariadb_root_username=None, - mariadb_root_password=None, + db_root_username=None, + db_root_password=None, admin_password=None, verbose=False, install_apps=None, @@ -60,8 +60,8 @@ def _new_site( installing = touch_file(get_site_path("locks", "installing.lock")) install_db( - root_login=mariadb_root_username, - root_password=mariadb_root_password, + root_login=db_root_username, + root_password=db_root_password, db_name=db_name, admin_password=admin_password, verbose=verbose, @@ -92,7 +92,7 @@ def _new_site( print("*** Scheduler is", scheduler_status, "***") -def install_db(root_login="root", root_password=None, db_name=None, source_sql=None, +def install_db(root_login=None, root_password=None, db_name=None, source_sql=None, admin_password=None, verbose=True, force=0, site_config=None, reinstall=False, db_password=None, db_type=None, db_host=None, db_port=None, no_mariadb_socket=False): import frappe.database @@ -101,6 +101,11 @@ def install_db(root_login="root", root_password=None, db_name=None, source_sql=N if not db_type: db_type = frappe.conf.db_type or 'mariadb' + if not root_login and db_type == 'mariadb': + root_login='root' + elif not root_login and db_type == 'postgres': + root_login='postgres' + make_conf(db_name, site_config=site_config, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port) frappe.flags.in_install_db = True 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/__init__.py b/frappe/model/__init__.py index be9496c85b..ab792d90e5 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -35,7 +35,8 @@ data_fieldtypes = ( 'Barcode', 'Geolocation', 'Duration', - 'Icon' + 'Icon', + 'Autocomplete', ) attachment_fieldtypes = ( diff --git a/frappe/model/document.py b/frappe/model/document.py index cb36c18b47..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) @@ -1154,7 +1154,7 @@ class Document(BaseDocument): for f in hooks: add_to_return_value(self, f(self, method, *args, **kwargs)) - return self._return_value + return self.__dict__.pop("_return_value", None) return runner 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/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index 1bc0ffeb8a..4e66ed6642 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -11,7 +11,26 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui set_options() { if (this.df.options) { let options = this.df.options || []; - this._data = this.parse_options(options); + this.set_data(options); + } + } + + format_for_input(value) { + if (value == null) { + return ""; + } else if (this._data && this._data.length) { + const item = this._data.find(i => i.value == value); + return item ? item.label : value; + } else { + return value; + } + } + + get_input_value() { + if (this.$input) { + const label = this.$input.val(); + const item = this._data?.find(i => i.label == label); + return item ? item.value : label; } } @@ -23,7 +42,7 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui autoFirst: true, list: this.get_data(), data: function(item) { - if (!(item instanceof Object)) { + if (typeof item !== 'object') { var d = { value: item }; item = d; } @@ -65,6 +84,18 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui }; } + init_option_cache() { + if (!this.$input.cache) { + this.$input.cache = {}; + } + if (!this.$input.cache[this.doctype]) { + this.$input.cache[this.doctype] = {}; + } + if (!this.$input.cache[this.doctype][this.df.fieldname]) { + this.$input.cache[this.doctype][this.df.fieldname] = {}; + } + } + setup_awesomplete() { this.awesomplete = new Awesomplete( this.input, @@ -75,12 +106,18 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui .find('.awesomplete ul') .css('min-width', '100%'); - this.$input.on( - 'input', - frappe.utils.debounce(() => { + this.init_option_cache(); + + this.$input.on('input', frappe.utils.debounce((e) => { + const cached_options = this.$input.cache[this.doctype][this.df.fieldname][e.target.value]; + if (cached_options && cached_options.length) { + this.set_data(cached_options); + } else if (this.get_query || this.df.get_query) { + this.execute_query_if_exists(e.target.value); + } else { this.awesomplete.list = this.get_data(); - }, 500) - ); + } + }, 500)); this.$input.on('focus', () => { if (!this.$input.val()) { @@ -89,6 +126,17 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui } }); + this.$input.on("blur", () => { + if(this.selected) { + this.selected = false; + return; + } + var value = this.get_input_value(); + if(value!==this.last_value) { + this.parse_validate_and_set_in_model(value); + } + }); + this.$input.on("awesomplete-open", () => { this.autocomplete_open = true; }); @@ -127,6 +175,75 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui return options; } + execute_query_if_exists(term) { + const args = { txt: term }; + let get_query = this.get_query || this.df.get_query; + + if (!get_query) { + return; + } + + let set_nulls = function(obj) { + $.each(obj, function(key, value) { + if (value !== undefined) { + obj[key] = value; + } + }); + return obj; + }; + + let process_query_object = function(obj) { + if (obj.query) { + args.query = obj.query; + } + + if (obj.params) { + set_nulls(obj.params); + Object.assign(args, obj.params); + } + + // turn off value translation + if (obj.translate_values !== undefined) { + this.translate_values = obj.translate_values; + } + }; + + if ($.isPlainObject(get_query)) { + process_query_object(get_query); + } else if (typeof get_query === "string") { + args.query = get_query; + } else { + // get_query by function + var q = get_query( + (this.frm && this.frm.doc) || this.doc, + this.doctype, + this.docname + ); + + if (typeof q === "string") { + // returns a string + args.query = q; + } else if ($.isPlainObject(q)) { + // returns an object + process_query_object(q); + } + } + + if (args.query) { + frappe.call({ + method: args.query, + args: args, + callback: ({ message }) => { + if(!this.$input.is(":focus")) { + return; + } + this.$input.cache[this.doctype][this.df.fieldname][term] = message; + this.set_data(message); + } + }) + } + } + get_data() { return this._data || []; } 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/formatters.js b/frappe/public/js/frappe/form/formatters.js index c39c4046b4..2b0f996661 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -21,6 +21,9 @@ frappe.form.formatters = { } return value==null ? "" : value; }, + Autocomplete: function(value) { + return __(frappe.form.formatters["Data"](value)); + }, Select: function(value) { return __(frappe.form.formatters["Data"](value)); }, 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 cd8bde1f57..221a120a18 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -183,21 +183,20 @@ export default class GridRow { render_template() { this.set_row_index(); - if(this.row_display) { + if (this.row_display) { this.row_display.remove(); } // row index - if(this.doc) { - if(!this.row_index) { - this.row_index = $('
'+this.row_check_html+'
').appendTo(this.row); - } + if (!this.row_index) { + this.row_index = $(`
${this.row_check_html}
`).appendTo(this.row); + } + + if (this.doc) { this.row_index.find('span').html(this.doc.idx); } - this.row_display = $('
'+ - +'
').appendTo(this.row) + this.row_display = $('
').appendTo(this.row) .html(frappe.render(this.grid.template, { doc: this.doc ? frappe.get_format_helper(this.doc) : null, frm: this.frm, @@ -616,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/ui/link_preview.js b/frappe/public/js/frappe/ui/link_preview.js index 328cd23716..a6a2273161 100644 --- a/frappe/public/js/frappe/ui/link_preview.js +++ b/frappe/public/js/frappe/ui/link_preview.js @@ -73,7 +73,7 @@ frappe.ui.LinkPreview = class { } this.popover_timeout = setTimeout(() => { - if (this.popover) { + if (this.popover && this.popover.options) { let new_content = this.get_popover_html(preview_data); this.popover.options.content = new_content; } else { 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 @@ - +