diff --git a/.flake8 b/.flake8 index 4b852abd7c..2de7a154c9 100644 --- a/.flake8 +++ b/.flake8 @@ -1,37 +1,74 @@ [flake8] ignore = + B001, + B007, + B009, + B010, + B950, + E101, + E111, + E114, + E116, + E117, E121, + E122, + E123, + E124, + E125, E126, E127, E128, + E131, + E201, + E202, E203, + E211, + E221, + E222, + E223, + E224, E225, E226, + E228, E231, E241, + E242, E251, E261, + E262, E265, + E266, + E271, + E272, + E273, + E274, + E301, E302, E303, E305, + E306, E402, E501, + E502, + E701, + E702, + E703, E741, + F401, + F403, + F405, + W191, W291, W292, W293, W391, W503, W504, - F403, - B007, - B950, - W191, - E124, # closing bracket, irritating while writing QB code - E131, # continuation line unaligned for hanging indent - E123, # closing bracket does not match indentation of opening bracket's line - E101, # ensured by use of black + E711, + E129, + F841, + E713, + E712, max-line-length = 200 -exclude=.github/helper/semgrep_rules +exclude=,test_*.py diff --git a/.github/helper/flake8.conf b/.github/helper/flake8.conf deleted file mode 100644 index 20d4b912ca..0000000000 --- a/.github/helper/flake8.conf +++ /dev/null @@ -1,75 +0,0 @@ -[flake8] -ignore = - B001, - B007, - B009, - B010, - B950, - E101, - E111, - E114, - E116, - E117, - E121, - E122, - E123, - E124, - E125, - E126, - E127, - E128, - E131, - E201, - E202, - E203, - E211, - E221, - E222, - E223, - E224, - E225, - E226, - E228, - E231, - E241, - E242, - E251, - E261, - E262, - E265, - E266, - E271, - E272, - E273, - E274, - E301, - E302, - E303, - E305, - E306, - E402, - E501, - E502, - E701, - E702, - E703, - E741, - F401, - F403, - F405, - W191, - W291, - W292, - W293, - W391, - W503, - W504, - E711, - E129, - F841, - E713, - E712, - - -max-line-length = 200 -exclude=.github/helper/semgrep_rules,test_*.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 27fae671c9..0783e94457 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,7 +59,6 @@ repos: hooks: - id: flake8 additional_dependencies: ['flake8-bugbear',] - args: ['--config', '.github/helper/flake8.conf'] ci: autoupdate_schedule: weekly diff --git a/.stylelintrc b/.stylelintrc deleted file mode 100644 index 1e05d1fb41..0000000000 --- a/.stylelintrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": ["stylelint-config-recommended"], - "plugins": ["stylelint-scss"], - "rules": { - "at-rule-no-unknown": null, - "scss/at-rule-no-unknown": true, - "no-descending-specificity": null - } -} \ No newline at end of file diff --git a/bandit.yml b/bandit.yml deleted file mode 100644 index b8560e97c8..0000000000 --- a/bandit.yml +++ /dev/null @@ -1 +0,0 @@ -skips: ['E0203', 'B605', 'B404', 'B603', 'B607'] \ No newline at end of file diff --git a/frappe/__init__.py b/frappe/__init__.py index c03b87be1c..84a27642a9 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1018,19 +1018,15 @@ def get_precision( return get_field_precision(get_meta(doctype).get_field(fieldname), doc, currency) -def generate_hash(txt: str | None = None, length: int | None = None) -> str: - """Generates random hash for given text + current timestamp + random string.""" - import hashlib - import time +def generate_hash(txt: str | None = None, length: int = 56) -> str: + """Generate random hash using best available randomness source.""" + import math + import secrets - from .utils import random_string + if not length: + length = 56 - digest = hashlib.sha224( - ((txt or "") + repr(time.time()) + repr(random_string(8))).encode() - ).hexdigest() - if length: - digest = digest[:length] - return digest + return secrets.token_hex(math.ceil(length / 2))[:length] def reset_metadata_version(): diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index e3bf669630..611592531d 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -432,7 +432,7 @@ class DataExporter: row[_column_start_end.start + i + 1] = value def build_response_as_excel(self): - filename = frappe.generate_hash("", 10) + filename = frappe.generate_hash(length=10) with open(filename, "wb") as f: f.write(cstr(self.writer.getvalue()).encode("utf-8")) f = open(filename) diff --git a/frappe/core/doctype/data_import/test_importer.py b/frappe/core/doctype/data_import/test_importer.py index af8c711ab5..978f5792dd 100644 --- a/frappe/core/doctype/data_import/test_importer.py +++ b/frappe/core/doctype/data_import/test_importer.py @@ -97,7 +97,7 @@ class TestImporter(FrappeTestCase): def test_data_import_update(self): existing_doc = frappe.get_doc( doctype=doctype_name, - title=frappe.generate_hash(doctype_name, 8), + title=frappe.generate_hash(length=8), table_field_1=[{"child_title": "child title to update"}], ) existing_doc.save() diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 3722e5d1fa..2e74fd3a6a 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -670,6 +670,9 @@ class TestDocType(FrappeTestCase): self.assertEqual(test_json.test_json_field["hello"], "world") + def test_no_delete_doc(self): + self.assertRaises(frappe.ValidationError, frappe.delete_doc, "DocType", "Address") + @patch.dict(frappe.conf, {"developer_mode": 1}) def test_custom_field_deletion(self): """Custom child tables whose doctype doesn't exist should be auto deleted.""" diff --git a/frappe/database/database.py b/frappe/database/database.py index 3cb47e853a..47ca451289 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -7,7 +7,7 @@ import random import re import string import traceback -from contextlib import contextmanager +from contextlib import contextmanager, suppress from time import time from pypika.dialects import MySQLQueryBuilder, PostgreSQLQueryBuilder @@ -29,7 +29,8 @@ from frappe.exceptions import DoesNotExistError, ImplicitCommitError from frappe.model.utils.link_count import flush_local_link_count from frappe.query_builder.functions import Count from frappe.utils import cast as cast_fieldtype -from frappe.utils import get_datetime, get_table_name, getdate, now, sbool +from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool +from frappe.utils.deprecations import deprecated, deprecation_warning IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE) INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*") @@ -114,6 +115,17 @@ class Database: self._cursor = self._conn.cursor() frappe.local.rollback_observers = [] + try: + if execution_timeout := get_query_execution_timeout(): + self.set_execution_timeout(execution_timeout) + except Exception as e: + frappe.logger("database").warning(f"Couldn't set execution timeout {e}") + + def set_execution_timeout(self, seconds: int): + """Set session speicifc timeout on exeuction of statements. + If any statement takes more time it will be killed along with entire transaction.""" + raise NotImplementedError + def use(self, db_name): """`USE` db_name.""" self._conn.select_db(db_name) @@ -262,6 +274,11 @@ class Database: if pluck: return [r[0] for r in self.last_result] + if as_utf8: + deprecation_warning("as_utf8 parameter is deprecated and will be removed in version 15.") + if formatted: + deprecation_warning("formatted parameter is deprecated and will be removed in version 15.") + # scrub output if required if as_dict: ret = self.fetch_as_dict(formatted, as_utf8) @@ -380,10 +397,13 @@ class Database: def fetch_as_dict(self, formatted=0, as_utf8=0) -> list[frappe._dict]: """Internal. Converts results to dict.""" result = self.last_result - ret = [] if result: keys = [column[0] for column in self._cursor.description] + if not as_utf8: + return [frappe._dict(zip(keys, row)) for row in result] + + ret = [] for r in result: values = [] for value in r: @@ -418,6 +438,9 @@ class Database: @staticmethod def convert_to_lists(res, formatted=0, as_utf8=0): """Convert tuple output to lists (internal).""" + if not as_utf8: + return [[value for value in row] for row in res] + nres = [] for r in res: nr = [] @@ -826,6 +849,7 @@ class Database: ).run(debug=debug, run=run, as_dict=as_dict) return {} + @deprecated def update(self, *args, **kwargs): """Update multiple values. Alias for `set_value`.""" return self.set_value(*args, **kwargs) @@ -865,6 +889,9 @@ class Database: modified_by = modified_by or frappe.session.user to_update.update({"modified": modified, "modified_by": modified_by}) + if for_update: + deprecation_warning("for_update parameter is deprecated and will be removed in v15.") + if is_single_doctype: frappe.db.delete( "Singles", filters={"field": ("in", tuple(to_update)), "doctype": dt}, debug=debug @@ -896,10 +923,12 @@ class Database: del self.value_cache[dt] @staticmethod + @deprecated def set(doc, field, val): """Set value in document. **Avoid**""" doc.db_set(field, val) + @deprecated def touch(self, doctype, docname): """Update the modified timestamp of this document.""" modified = now() @@ -1222,6 +1251,7 @@ class Database: """ return self.sql_ddl(f"truncate `{get_table_name(doctype)}`") + @deprecated def clear_table(self, doctype): return self.truncate(doctype) @@ -1340,3 +1370,28 @@ def savepoint(catch: type | tuple[type, ...] = Exception): frappe.db.rollback(save_point=savepoint) else: frappe.db.release_savepoint(savepoint) + + +def get_query_execution_timeout() -> int: + """Get execution timeout based on current timeout in different contexts. + + HTTP requests: HTTP timeout or a default (300) + Background jobs: Job timeout + Console/Commands: No timeout = 0. + + Note: Timeout adds 1.5x as "safety factor" + """ + from rq import get_current_job + + if not frappe.conf.get("enable_db_statement_timeout"): + return 0 + + # Zero means no timeout, which is the default value in db. + timeout = 0 + with suppress(Exception): + if getattr(frappe.local, "request", None): + timeout = frappe.conf.http_timeout or 300 + elif job := get_current_job(): + timeout = job.timeout + + return int(cint(timeout) * 1.5) diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 1df9877eb1..322c355357 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -68,6 +68,10 @@ class MariaDBExceptionUtil: def is_syntax_error(e: pymysql.Error) -> bool: return e.args[0] == ER.PARSE_ERROR + @staticmethod + def is_statement_timeout(e: pymysql.Error) -> bool: + return e.args[0] == 1969 + @staticmethod def is_data_too_long(e: pymysql.Error) -> bool: return e.args[0] == ER.DATA_TOO_LONG @@ -102,6 +106,9 @@ class MariaDBConnectionUtil: def create_connection(self): return pymysql.connect(**self.get_connection_settings()) + def set_execution_timeout(self, seconds: int): + self.sql("set session max_statement_time = %s", int(seconds)) + def get_connection_settings(self) -> dict: conn_settings = { "host": self.host, diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 3b3612c0e4..d082afceaf 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -99,6 +99,10 @@ class PostgresExceptionUtil: def is_duplicate_fieldname(e): return getattr(e, "pgcode", None) == DUPLICATE_COLUMN + @staticmethod + def is_statement_timeout(e): + return PostgresDatabase.is_timedout(e) or isinstance(e, frappe.QueryTimeoutError) + @staticmethod def is_data_too_long(e): return getattr(e, "pgcode", None) == STRING_DATA_RIGHT_TRUNCATION @@ -161,6 +165,10 @@ class PostgresDatabase(PostgresExceptionUtil, Database): return conn + def set_execution_timeout(self, seconds: int): + # Postgres expects milliseconds as input + self.sql("set local statement_timeout = %s", int(seconds) * 1000) + def escape(self, s, percent=True): """Escape quotes and percent in given string.""" if isinstance(s, bytes): diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 679b052baf..b24ab21455 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -91,7 +91,7 @@ def validate_args(data): def validate_fields(data): wildcard = update_wildcard_field_param(data) - for field in data.fields or []: + for field in list(data.fields or []): fieldname = extract_fieldname(field) if is_standard(fieldname): continue diff --git a/frappe/installer.py b/frappe/installer.py index 4f1755c2a0..2a6c29a17f 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -402,7 +402,7 @@ def _delete_modules(modules: list[str], dry_run: bool) -> list[str]: if not dry_run: if doctype.issingle: - frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True) + frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True, force=True) else: drop_doctypes.append(doctype.name) @@ -460,7 +460,7 @@ def _delete_doctypes(doctypes: list[str], dry_run: bool) -> None: for doctype in set(doctypes): print(f"* dropping Table for '{doctype}'...") if not dry_run: - frappe.delete_doc("DocType", doctype, ignore_on_trash=True) + frappe.delete_doc("DocType", doctype, ignore_on_trash=True, force=True) frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{doctype}`") diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index d1120cc22d..5e8a12c345 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -92,6 +92,8 @@ def delete_doc( else: doc = frappe.get_doc(doctype, name) + if not (doc.custom or frappe.conf.developer_mode or frappe.flags.in_patch or force): + frappe.throw(_("Standard DocType can not be deleted.")) update_flags(doc, flags, ignore_permissions) check_permission_and_not_submitted(doc) diff --git a/frappe/model/document.py b/frappe/model/document.py index 8dcc57e827..d438544e70 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -142,9 +142,14 @@ class Document(BaseDocument): self._fix_numeric_types() else: + get_value_kwargs = {"for_update": self.flags.for_update, "as_dict": True} + if not isinstance(self.name, (dict, list)): + get_value_kwargs["order_by"] = None + d = frappe.db.get_value( - self.doctype, self.name, "*", as_dict=1, for_update=self.flags.for_update + doctype=self.doctype, filters=self.name, fieldname="*", **get_value_kwargs ) + if not d: frappe.throw( _("{0} {1} not found").format(_(self.doctype), self.name), frappe.DoesNotExistError @@ -744,12 +749,13 @@ class Document(BaseDocument): Will also validate document transitions (Save > Submit > Cancel) calling `self.check_docstatus_transition`.""" - self.load_doc_before_save() + self.load_doc_before_save(raise_exception=True) self._action = "save" - previous = self.get_doc_before_save() + previous = self._doc_before_save - if not previous or self.meta.get("is_virtual"): + # previous is None for new document insert + if not previous: self.check_docstatus_transition(0) return @@ -1048,7 +1054,7 @@ class Document(BaseDocument): self.set_title_field() - def load_doc_before_save(self): + def load_doc_before_save(self, *, raise_exception: bool = False): """load existing document from db before saving""" self._doc_before_save = None @@ -1059,6 +1065,9 @@ class Document(BaseDocument): try: self._doc_before_save = frappe.get_doc(self.doctype, self.name, for_update=True) except frappe.DoesNotExistError: + if raise_exception: + raise + frappe.clear_last_message() def run_post_save_methods(self): diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 6f7db5cf9d..93be2204b4 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -278,7 +278,7 @@ def make_autoname(key="", doctype="", doc=""): DE/09/01/00001 where 09 is the year, 01 is the month and 00001 is the series """ if key == "hash": - return frappe.generate_hash(doctype, 10) + return frappe.generate_hash(length=10) series = NamingSeries(key) return series.generate_next_name(doc) diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 923fbc1b3b..8338157996 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -1,12 +1,17 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import json +from typing import TYPE_CHECKING, Union import frappe from frappe import _ from frappe.model.docstatus import DocStatus from frappe.utils import cint +if TYPE_CHECKING: + from frappe.model.document import Document + from frappe.workflow.doctype.workflow.workflow import Workflow + class WorkflowStateError(frappe.ValidationError): pass @@ -32,20 +37,22 @@ def get_workflow_name(doctype): @frappe.whitelist() -def get_transitions(doc, workflow=None, raise_exception=False): +def get_transitions( + doc: Union["Document", str, dict], workflow: "Workflow" = None, raise_exception: bool = False +) -> list[dict]: """Return list of possible transitions for the given doc""" - doc = frappe.get_doc(frappe.parse_json(doc)) + from frappe.model.document import Document + + if not isinstance(doc, Document): + doc = frappe.get_doc(frappe.parse_json(doc)) + doc.load_from_db() if doc.is_new(): return [] - doc.load_from_db() + doc.check_permission("read") - frappe.has_permission(doc, "read", throw=True) - roles = frappe.get_roles() - - if not workflow: - workflow = get_workflow(doc.doctype) + workflow = workflow or get_workflow(doc.doctype) current_state = doc.get(workflow.workflow_state_field) if not current_state: @@ -55,11 +62,14 @@ def get_transitions(doc, workflow=None, raise_exception=False): frappe.throw(_("Workflow State not set"), WorkflowStateError) transitions = [] + roles = frappe.get_roles() + for transition in workflow.transitions: if transition.state == current_state and transition.allowed in roles: if not is_transition_condition_satisfied(transition, doc): continue transitions.append(transition.as_dict()) + return transitions @@ -79,7 +89,7 @@ def get_workflow_safe_globals(): ) -def is_transition_condition_satisfied(transition, doc): +def is_transition_condition_satisfied(transition, doc) -> bool: if not transition.condition: return True else: @@ -198,7 +208,7 @@ def validate_workflow(doc): ) -def get_workflow(doctype): +def get_workflow(doctype) -> "Workflow": return frappe.get_doc("Workflow", get_workflow_name(doctype)) diff --git a/frappe/patches/v11_0/remove_skip_for_doctype.py b/frappe/patches/v11_0/remove_skip_for_doctype.py index e7c1d71a0a..b3471ca4e8 100644 --- a/frappe/patches/v11_0/remove_skip_for_doctype.py +++ b/frappe/patches/v11_0/remove_skip_for_doctype.py @@ -60,7 +60,7 @@ def execute(): # Maintain sequence (name, user, allow, for_value, applicable_for, apply_to_all_doctypes, creation, modified) new_user_permissions_list.append( ( - frappe.generate_hash("", 10), + frappe.generate_hash(length=10), user_permission.user, user_permission.allow, user_permission.for_value, diff --git a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py index 1a369b4e12..7283760c23 100644 --- a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py +++ b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py @@ -27,7 +27,7 @@ def execute(): email_values.append( ( 1, - frappe.generate_hash(contact_detail.email_id, 10), + frappe.generate_hash(length=10), contact_detail.email_id, "email_ids", "Contact", @@ -44,7 +44,7 @@ def execute(): phone_values.append( ( phone_counter, - frappe.generate_hash(contact_detail.email_id, 10), + frappe.generate_hash(length=10), contact_detail.phone, "phone_nos", "Contact", @@ -63,7 +63,7 @@ def execute(): phone_values.append( ( phone_counter, - frappe.generate_hash(contact_detail.email_id, 10), + frappe.generate_hash(length=10), contact_detail.mobile_no, "phone_nos", "Contact", diff --git a/frappe/patches/v12_0/setup_tags.py b/frappe/patches/v12_0/setup_tags.py index 6bff8d3dac..cb0d46a45d 100644 --- a/frappe/patches/v12_0/setup_tags.py +++ b/frappe/patches/v12_0/setup_tags.py @@ -28,7 +28,7 @@ def execute(): tag_list.append((tag.strip(), time, time, "Administrator")) - tag_link_name = frappe.generate_hash(_user_tags.name + tag.strip() + doctype.name, 10) + tag_link_name = frappe.generate_hash(length=10) tag_links.append( (tag_link_name, doctype.name, _user_tags.name, tag.strip(), time, time, "Administrator") ) diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 66ff5107a1..52d0026e37 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1179,7 +1179,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.$result.on("click", ".list-row, .image-view-header, .file-header", (e) => { const $target = $(e.target); // tick checkbox if Ctrl/Meta key is pressed - if (e.ctrlKey || (e.metaKey && !$target.is("a"))) { + if ((e.ctrlKey || e.metaKey) && !$target.is("a")) { const $list_row = $(e.currentTarget); const $check = $list_row.find(".list-row-checkbox"); $check.prop("checked", !$check.prop("checked")); diff --git a/frappe/public/js/frappe/list/list_view_select.js b/frappe/public/js/frappe/list/list_view_select.js index 0e7033cf9a..5265ace340 100644 --- a/frappe/public/js/frappe/list/list_view_select.js +++ b/frappe/public/js/frappe/list/list_view_select.js @@ -315,7 +315,7 @@ frappe.views.ListViewSelect = class ListViewSelect { accounts.forEach((account) => { let email_account = account.email_id == "All Accounts" ? "All Accounts" : account.email_account; - let route = `/app/communication/inbox/${email_account}`; + let route = `/app/communication/view/inbox/${email_account}`; let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes( account.email_id ) diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js index 4e67272f88..e381f332a9 100644 --- a/frappe/public/js/frappe/ui/page.js +++ b/frappe/public/js/frappe/ui/page.js @@ -327,13 +327,14 @@ frappe.ui.Page = class Page { //--- Menu --// - add_menu_item(label, click, standard, shortcut) { + add_menu_item(label, click, standard, shortcut, show_parent) { return this.add_dropdown_item({ label, click, standard, parent: this.menu, shortcut, + show_parent, }); } @@ -424,7 +425,7 @@ frappe.ui.Page = class Page { icon = null, }) { if (show_parent) { - parent.parent().removeClass("hide"); + parent.parent().removeClass("hide hidden-xl"); } let $link = this.is_in_group_button_dropdown(parent, "li > a.grey-link > span", label); @@ -602,8 +603,11 @@ frappe.ui.Page = class Page { }; // Add actions as menu item in Mobile View let menu_item_label = group ? `${group} > ${label}` : label; - let menu_item = this.add_menu_item(menu_item_label, _action, false); + let menu_item = this.add_menu_item(menu_item_label, _action, false, false, false); menu_item.parent().addClass("hidden-xl"); + if (this.menu_btn_group.hasClass("hide")) { + this.menu_btn_group.removeClass("hide").addClass("hidden-xl"); + } if (group) { var $group = this.get_or_add_inner_group_button(group); diff --git a/frappe/public/js/frappe/ui/toolbar/about.js b/frappe/public/js/frappe/ui/toolbar/about.js index e23706eff1..69cbbfaba0 100644 --- a/frappe/public/js/frappe/ui/toolbar/about.js +++ b/frappe/public/js/frappe/ui/toolbar/about.js @@ -19,6 +19,8 @@ frappe.ui.misc.about = function () { Facebook: https://facebook.com/erpnext

Twitter: https://twitter.com/erpnext

+

+ YouTube: https://www.youtube.com/@erpnextofficial


${__("Installed Apps")}

${__("Loading versions...")}
diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index 53a3300a94..74560cebbc 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -40,7 +40,6 @@ frappe.breadcrumbs = { type: type, }; } - this.all[frappe.breadcrumbs.current_page()] = obj; this.update(); }, diff --git a/frappe/templates/includes/navbar/navbar_items.html b/frappe/templates/includes/navbar/navbar_items.html index 8a10751441..99a40a02a0 100644 --- a/frappe/templates/includes/navbar/navbar_items.html +++ b/frappe/templates/includes/navbar/navbar_items.html @@ -3,7 +3,7 @@ {% if parent %} -{%- set dropdown_id = 'id-' + frappe.utils.generate_hash('Dropdown', 12) -%} +{%- set dropdown_id = 'id-' + frappe.utils.generate_hash(length=12) -%}