diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 9c0a1f4ce2..01b5407489 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -83,4 +83,4 @@ jobs: - uses: actions/checkout@v3 - run: | pip install pip-audit - pip-audit ${GITHUB_WORKSPACE} --ignore-vuln GHSA-hcpj-qp55-gfph + pip-audit ${GITHUB_WORKSPACE} diff --git a/cypress/integration/form_builder.js b/cypress/integration/form_builder.js index 46e62b2697..84494ddebf 100644 --- a/cypress/integration/form_builder.js +++ b/cypress/integration/form_builder.js @@ -244,7 +244,7 @@ context("Form Builder", () => { let first_field = ".tab-content.active .section-columns-container:first .column:first .field:first"; - cy.get(".fields-container .field[title='Check']").drag(first_field, { + cy.get(".fields-container .field[title='Data']").drag(first_field, { target: { x: 100, y: 10 }, }); diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 1c0207ce4b..fbbdde8e03 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -109,7 +109,8 @@ def new_site( "--with-public-files", help="Restores the public files of the site, given path to its tar file" ) @click.option( - "--with-private-files", help="Restores the private files of the site, given path to its tar file" + "--with-private-files", + help="Restores the private files of the site, given path to its tar file", ) @click.option( "--force", @@ -191,7 +192,8 @@ def _restore( fg="red", ) click.secho( - "Use `bench partial-restore` to restore a partial backup to an existing site.", fg="yellow" + "Use `bench partial-restore` to restore a partial backup to an existing site.", + fg="yellow", ) _backup.decryption_rollback() sys.exit(1) @@ -222,7 +224,8 @@ def _restore( fg="red", ) click.secho( - "Use `bench partial-restore` to restore a partial backup to an existing site.", fg="yellow" + "Use `bench partial-restore` to restore a partial backup to an existing site.", + fg="yellow", ) _backup.decryption_rollback() sys.exit(1) @@ -324,7 +327,8 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None): # Check for full backup file if "Partial Backup" not in header: click.secho( - "Full backup file detected.Use `bench restore` to restore a Frappe Site.", fg="red" + "Full backup file detected.Use `bench restore` to restore a Frappe Site.", + fg="red", ) _backup.decryption_rollback() sys.exit(1) @@ -355,7 +359,8 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None): # Check for Full backup file. if "Partial Backup" not in header: click.secho( - "Full Backup file detected.Use `bench restore` to restore a Frappe Site.", fg="red" + "Full Backup file detected.Use `bench restore` to restore a Frappe Site.", + fg="red", ) _backup.decryption_rollback() sys.exit(1) @@ -391,7 +396,12 @@ def reinstall( def _reinstall( - site, admin_password=None, db_root_username=None, db_root_password=None, yes=False, verbose=False + site, + admin_password=None, + db_root_username=None, + db_root_password=None, + yes=False, + verbose=False, ): from frappe.installer import _new_site from frappe.utils.synchronization import filelock @@ -719,7 +729,10 @@ def use(site, sites_path="."): @click.option("--backup-path-private-files", default=None, help="Set path for saving private file") @click.option("--backup-path-conf", default=None, help="Set path for saving config file") @click.option( - "--ignore-backup-conf", default=False, is_flag=True, help="Ignore excludes/includes set in config" + "--ignore-backup-conf", + default=False, + is_flag=True, + help="Ignore excludes/includes set in config", ) @click.option("--verbose", default=False, is_flag=True, help="Add verbosity") @click.option("--compress", default=False, is_flag=True, help="Compress private and public files") @@ -774,7 +787,8 @@ def backup( continue if frappe.get_system_settings("encrypt_backup") and frappe.get_site_config().encryption_key: click.secho( - "Backup encryption is turned on. Please note the backup encryption key.", fg="yellow" + "Backup encryption is turned on. Please note the backup encryption key.", + fg="yellow", ) odb.print_summary() @@ -1120,14 +1134,31 @@ def stop_recording(context): @click.option( "--bind-tls", is_flag=True, default=False, help="Returns a reference to the https tunnel." ) +@click.option( + "--use-default-authtoken", + is_flag=True, + default=False, + help="Use the auth token present in ngrok's config.", +) @pass_context -def start_ngrok(context, bind_tls): +def start_ngrok(context, bind_tls, use_default_authtoken): """Start a ngrok tunnel to your local development server.""" from pyngrok import ngrok site = get_site(context) frappe.init(site=site) + ngrok_authtoken = frappe.conf.ngrok_authtoken + if not use_default_authtoken: + if not ngrok_authtoken: + click.echo( + f"\n{click.style('ngrok_authtoken', fg='yellow')} not found in site config.\n" + "Please register for a free ngrok account at: https://dashboard.ngrok.com/signup and place the obtained authtoken in the site config.", + ) + sys.exit(1) + + ngrok.set_auth_token(ngrok_authtoken) + port = frappe.conf.http_port or frappe.conf.webserver_port tunnel = ngrok.connect(addr=str(port), host_header=site, bind_tls=bind_tls) print(f"Public URL: {tunnel.public_url}") diff --git a/frappe/core/api/file.py b/frappe/core/api/file.py index c2354516a8..e3e6a9de08 100644 --- a/frappe/core/api/file.py +++ b/frappe/core/api/file.py @@ -13,7 +13,7 @@ def unzip_file(name: str): @frappe.whitelist() -def get_attached_images(doctype: str, names: list[str]) -> frappe._dict: +def get_attached_images(doctype: str, names: list[str] | str) -> frappe._dict: """get list of image urls attached in form returns {name: ['image.jpg', 'image.png']}""" diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index e3f8ffd503..e1bb23b388 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -195,10 +195,12 @@ class DocType(Document): def set_default_in_list_view(self): """Set default in-list-view for first 4 mandatory fields""" + not_allowed_in_list_view = get_fields_not_allowed_in_list_view(self.meta) + if not [d.fieldname for d in self.fields if d.in_list_view]: cnt = 0 for d in self.fields: - if d.reqd and not d.hidden and not d.fieldtype in table_fields: + if d.reqd and not d.hidden and not d.fieldtype in not_allowed_in_list_view: d.in_list_view = 1 cnt += 1 if cnt == 4: @@ -1446,10 +1448,7 @@ def validate_fields(meta): fields = meta.get("fields") fieldname_list = [d.fieldname for d in fields] - not_allowed_in_list_view = list(copy.copy(no_value_fields)) - not_allowed_in_list_view.append("Attach Image") - if meta.istable: - not_allowed_in_list_view.remove("Button") + not_allowed_in_list_view = get_fields_not_allowed_in_list_view(meta) for d in fields: if not d.permlevel: @@ -1490,6 +1489,14 @@ def validate_fields(meta): check_image_field(meta) +def get_fields_not_allowed_in_list_view(meta) -> list[str]: + not_allowed_in_list_view = list(copy.copy(no_value_fields)) + not_allowed_in_list_view.append("Attach Image") + if meta.istable: + not_allowed_in_list_view.remove("Button") + return not_allowed_in_list_view + + def validate_permissions_for_doctype(doctype, for_remove=False, alert=False): """Validates if permissions are set correctly.""" doctype = frappe.get_doc("DocType", doctype) diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 2e74fd3a6a..e8226d4f9d 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -722,6 +722,28 @@ class TestDocType(FrappeTestCase): self.assertEqual(frappe.get_meta(doctype).get_field(field).default, "DELETETHIS") frappe.delete_doc("DocType", doctype) + def test_not_in_list_view_for_not_allowed_mandatory_field(self): + doctype = new_doctype( + fields=[ + { + "fieldname": "cover_image", + "fieldtype": "Attach Image", + "label": "Cover Image", + "reqd": 1, # mandatory + }, + { + "fieldname": "book_name", + "fieldtype": "Data", + "label": "Book Name", + "reqd": 1, # mandatory + }, + ], + ).insert() + + self.assertFalse(doctype.fields[0].in_list_view) + self.assertTrue(doctype.fields[1].in_list_view) + frappe.delete_doc("DocType", doctype.name) + def new_doctype( name: str | None = None, @@ -759,8 +781,7 @@ def new_doctype( } ) - if fields: - for f in fields: - doc.append("fields", f) + if fields and len(fields) > 0: + doc.set("fields", fields) return doc diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index f9d7adceef..ddafd0e9fd 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -39,6 +39,8 @@ "allow_login_using_mobile_number", "allow_login_using_user_name", "disable_user_pass_login", + "login_with_email_link", + "login_with_email_link_expiry", "allow_error_traceback", "strip_exif_metadata_from_uploaded_images", "allow_older_web_view_links", @@ -416,11 +418,11 @@ "label": "Send document Web View link in email" }, { - "collapsible": 1, - "fieldname": "prepared_report_section", - "fieldtype": "Section Break", - "label": "Reports" - }, + "collapsible": 1, + "fieldname": "prepared_report_section", + "fieldtype": "Section Break", + "label": "Reports" + }, { "default": "Frappe", "description": "The application name will be used in the Login page.", @@ -504,12 +506,26 @@ "fieldname": "disable_user_pass_login", "fieldtype": "Check", "label": "Disable Username/Password Login" + }, + { + "default": "0", + "description": "Allow users to log in without a password, using a login link sent to their email", + "fieldname": "login_with_email_link", + "fieldtype": "Check", + "label": "Login with email link" + }, + { + "default": "10", + "depends_on": "login_with_email_link", + "fieldname": "login_with_email_link_expiry", + "fieldtype": "Int", + "label": "Login with email link expiry (in minutes)" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2022-11-28 17:57:05.099512", + "modified": "2022-12-20 21:45:37.651668", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/email/doctype/email_template/email_template.json b/frappe/email/doctype/email_template/email_template.json index c6ec971da4..00f1428475 100644 --- a/frappe/email/doctype/email_template/email_template.json +++ b/frappe/email/doctype/email_template/email_template.json @@ -57,18 +57,16 @@ ], "icon": "fa fa-comment", "links": [], - "modified": "2022-01-04 14:12:50.321633", + "modified": "2023-01-02 03:56:48.437280", "modified_by": "Administrator", "module": "Email", "name": "Email Template", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { - "create": 1, "read": 1, - "role": "All", - "share": 1, - "write": 1 + "role": "All" }, { "create": 1, @@ -85,5 +83,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/geo/country_info.py b/frappe/geo/country_info.py index 2aefa27170..3267149d4c 100644 --- a/frappe/geo/country_info.py +++ b/frappe/geo/country_info.py @@ -5,6 +5,7 @@ import json # all country info import os +from functools import lru_cache import frappe from frappe.utils.momentjs import get_all_timezones @@ -27,8 +28,13 @@ def get_all(): return all_data -@frappe.whitelist() +@frappe.whitelist(allow_guest=True) def get_country_timezone_info(): + return _get_country_timezone_info() + + +@lru_cache(maxsize=2) +def _get_country_timezone_info(): return {"country_info": get_all(), "all_timezones": get_all_timezones()} diff --git a/frappe/model/document.py b/frappe/model/document.py index 773ee4a764..7222cf4ad6 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -178,6 +178,7 @@ class Document(BaseDocument): "*", as_dict=True, order_by="idx asc", + for_update=self.flags.for_update, ) or [] ) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 1fa1340024..69a2cbd11e 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -111,12 +111,6 @@ class Meta(Document): ] def __init__(self, doctype): - # from cache - if isinstance(doctype, dict): - super().__init__(doctype) - self.init_field_caches() - return - if isinstance(doctype, Document): super().__init__(doctype.as_dict()) else: @@ -135,7 +129,7 @@ class Meta(Document): def process(self): # don't process for special doctypes - # prevent's circular dependency + # prevents circular dependency if self.name in self.special_doctypes: self.init_field_caches() return diff --git a/frappe/public/js/form_builder/components/Column.vue b/frappe/public/js/form_builder/components/Column.vue index 3f108c06ba..a8f1f84118 100644 --- a/frappe/public/js/form_builder/components/Column.vue +++ b/frappe/public/js/form_builder/components/Column.vue @@ -1,6 +1,7 @@