diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 57e2b1a1be..f9d7adceef 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -33,7 +33,6 @@ "allow_guests_to_upload_files", "security", "session_expiry", - "session_expiry_mobile", "document_share_key_expiry", "column_break_13", "deny_multiple_sessions", @@ -211,13 +210,6 @@ "fieldtype": "Data", "label": "Session Expiry (idle timeout)" }, - { - "default": "720:00", - "description": "In Hours", - "fieldname": "session_expiry_mobile", - "fieldtype": "Data", - "label": "Session Expiry Mobile" - }, { "fieldname": "column_break_13", "fieldtype": "Column Break" @@ -517,7 +509,7 @@ "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2022-11-20 17:57:05.099512", + "modified": "2022-11-28 17:57:05.099512", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index 1fc27ca114..0f7a7f0819 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -20,11 +20,10 @@ class SystemSettings(Document): elif not enable_password_policy: self.minimum_password_score = "" - for key in ("session_expiry", "session_expiry_mobile"): - if self.get(key): - parts = self.get(key).split(":") - if len(parts) != 2 or not (cint(parts[0]) or cint(parts[1])): - frappe.throw(_("Session Expiry must be in format {0}").format("hh:mm")) + if self.session_expiry: + parts = self.session_expiry.split(":") + if len(parts) != 2 or not (cint(parts[0]) or cint(parts[1])): + frappe.throw(_("Session Expiry must be in format {0}").format("hh:mm")) if self.enable_two_factor_auth: if self.two_factor_method == "SMS": diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index 70b37dfcf8..efeeaaf935 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -252,7 +252,6 @@ CREATE TABLE `tabSessions` ( `sessiondata` longtext, `ipaddress` varchar(16) DEFAULT NULL, `lastupdate` datetime(6) DEFAULT NULL, - `device` varchar(255) DEFAULT 'desktop', `status` varchar(20) DEFAULT NULL, KEY `sid` (`sid`) ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/frappe/database/mariadb/setup_db.py b/frappe/database/mariadb/setup_db.py index ec31923f93..67f809abf7 100644 --- a/frappe/database/mariadb/setup_db.py +++ b/frappe/database/mariadb/setup_db.py @@ -135,9 +135,9 @@ def check_compatible_versions(): version = get_mariadb_version() version_tuple = tuple(int(v) for v in version[0].split(".")) - if version_tuple < (10, 3): + if version_tuple < (10, 6): click.secho( - f"Warning: MariaDB version {version} is less than 10.3 which is not supported by Frappe", + f"Warning: MariaDB version {version} is less than 10.6 which is not supported by Frappe", fg="yellow", ) elif version_tuple >= (10, 9): diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index 7ce3cecff8..37605be0f6 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -256,7 +256,6 @@ CREATE TABLE "tabSessions" ( "sessiondata" text, "ipaddress" varchar(16) DEFAULT NULL, "lastupdate" timestamp(6) DEFAULT NULL, - "device" varchar(255) DEFAULT 'desktop', "status" varchar(20) DEFAULT NULL ); diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index ee975c8326..b5b58ebfa3 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -156,6 +156,9 @@ class FormMeta(Meta): list_script = "" form_script = "" for script in client_scripts: + if not script.script: + continue + if script.view == "List": list_script += f""" // {script.name} @@ -163,7 +166,7 @@ class FormMeta(Meta): """ - if script.view == "Form": + elif script.view == "Form": form_script += f""" // {script.name} {script.script} diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index a1e8a9368f..b4a51ffaf3 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -319,7 +319,7 @@ def format_duration_fields(data: frappe._dict) -> None: continue for row in data.result: - index = col.fieldname if isinstance(row, dict) else i + index = col.get("fieldname") if isinstance(row, dict) else i if row[index]: row[index] = format_duration(row[index]) diff --git a/frappe/geo/doctype/country/country.py b/frappe/geo/doctype/country/country.py index 5738a1837d..8b1ec1364f 100644 --- a/frappe/geo/doctype/country/country.py +++ b/frappe/geo/doctype/country/country.py @@ -29,6 +29,8 @@ def get_countries_and_currencies(): countries = [] currencies = [] + added_currencies = set() + for name, country in data.items(): country = frappe._dict(country) countries.append( @@ -42,7 +44,9 @@ def get_countries_and_currencies(): time_zones="\n".join(country.timezones or []), ) ) - if country.currency: + if country.currency and country.currency not in added_currencies: + added_currencies.add(country.currency) + currencies.append( frappe.get_doc( doctype="Currency", diff --git a/frappe/model/document.py b/frappe/model/document.py index b81aa79cde..e6f90b2260 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -708,17 +708,16 @@ class Document(BaseDocument): d.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields) def get_permlevel_access(self, permission_type="write"): - if not hasattr(self, "_has_access_to"): - self._has_access_to = {} - - self._has_access_to[permission_type] = [] + allowed_permlevels = [] roles = frappe.get_roles() - for perm in self.get_permissions(): - if perm.role in roles and perm.get(permission_type): - if perm.permlevel not in self._has_access_to[permission_type]: - self._has_access_to[permission_type].append(perm.permlevel) - return self._has_access_to[permission_type] + for perm in self.get_permissions(): + if ( + perm.role in roles and perm.get(permission_type) and perm.permlevel not in allowed_permlevels + ): + allowed_permlevels.append(perm.permlevel) + + return allowed_permlevels def has_permlevel_access_to(self, fieldname, df=None, permission_type="read"): if not df: diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 99553f470c..146676652e 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -644,6 +644,8 @@ $.extend(frappe.model, { doctype: doctype, name: docname, }, + freeze: true, + freeze_message: __("Deleting {0}...", [title]), callback: function (r, rt) { if (!r.exc) { frappe.utils.play_sound("delete"); diff --git a/frappe/public/js/frappe/ui/toolbar/fuzzy_match.js b/frappe/public/js/frappe/ui/toolbar/fuzzy_match.js index 765377a0b4..9f86b03744 100644 --- a/frappe/public/js/frappe/ui/toolbar/fuzzy_match.js +++ b/frappe/public/js/frappe/ui/toolbar/fuzzy_match.js @@ -14,7 +14,7 @@ // J�rgen Tjern� - async helper // Anurag Awasthi - updated to 0.2.0 -const SEQUENTIAL_BONUS = 15; // bonus for adjacent matches +const SEQUENTIAL_BONUS = 25; // bonus for adjacent matches const SEPARATOR_BONUS = 30; // bonus if match occurs after a separator const CAMEL_BONUS = 30; // bonus if match is uppercase and prev is lower const FIRST_LETTER_BONUS = 15; // bonus if the first letter is matched diff --git a/frappe/public/js/frappe/web_form/web_form.js b/frappe/public/js/frappe/web_form/web_form.js index 52d417f025..980df7129b 100644 --- a/frappe/public/js/frappe/web_form/web_form.js +++ b/frappe/public/js/frappe/web_form/web_form.js @@ -379,7 +379,6 @@ export default class WebForm extends frappe.ui.FieldGroup { args: { data: this.doc, web_form: this.name, - docname: this.doc.name, for_payment, }, callback: (response) => { diff --git a/frappe/query_builder/docs.md b/frappe/query_builder/docs.md new file mode 100644 index 0000000000..c1f170b3d4 --- /dev/null +++ b/frappe/query_builder/docs.md @@ -0,0 +1,117 @@ +# This documentation is added for query builder and related files. + +## Related Files + +- [builder](./builder.py) +- [custom](./custom.py) +- [functions](./functions.py) +- [terms](./terms.py) +- [query](../database/query.py) + +### Builder + +Database specefic classes are declared which are then selected during init to give either postgres or mariadb dialects. + +### Functions and Custom + +These file handle any custom function which needs to be either added or handled sperately by the different dialects which are not supported yet by pypika directly. + +### Terms + +The inherent terms or specefic classes of pypika builder are handled and declared here all the parameterization goes through this module (custom parameterization is also implemeted here). + +### Raw Query Generation Examples + +Check out some examples [here](https://frappeframework.com/docs/v14/user/en/api/query-builder) + +

Query

+ +## Goal + +```sql +select `name` from `tabUser` +``` + +## There are 3 major ways to reach this goal + +### 1. Direct SQL (Boring / Unsafe / inconsistent) + +```python +frappe.db.sql("select `name` from `tabUser`") +``` + +### 2. SQL through direct Query Builder objects + +```python +from frappe.query_builder import Field + +frappe.qb.from_("User").select(Field("name")) + +``` + +### 3. Through the database API (Which performs the second method under the hood) + +```python +frappe.db.get_values("User", fieldname="name", filters={}) +``` + +This module is used to support the 3rd way of query generation in frappe. +The database module is completely powered by this query module. +This module is also where the query `Engine` resides which is the class responsible for the handling of various filter & field notations. + +- Interating with the existing Database API. + - The old database API was running on raw sql generation in order to bridge the gap between the added new support and raw sql strings this intermediate module was added. + +This module supports almost all the features present in db_query which powers `frappe.get_all` and `frappe.get_list` + +Supporting all the features with the previous filter notations and the field notations few features were added -: + +1. Dict Query + + - To support this + + ```python + frappe.db.get_values("ToDo", fieldname="name", filters={"description": "Something Random"}) + ``` + + and many other possible caveats to the dict representation such as + + ```python + frappe.db.get_values("User", fieldname="name", + filters={"name": ("like", "admin%")}) + + frappe.db.get_values("ToDo", fieldname="name", filters={"description": ("in", ["somso%", "someome"])}) + ``` + +2. Misc Query + + - To support this + + ```python + frappe.db.get_values("ToDo", fieldname="name", filters=["description", "=", "someone"]) + ``` + + Along with other possible list filter use cases including implicit joins + +3. Criterion Query + + - To support Inherent Query Builder objects + + ```python + from frappe.query_builder import Field + + frappe.db.get_values("User", fieldname="name", filters=Field("name") == "Administrator") + + ``` + + and all the pypika filters and functions. + +## Things to be implemented in the `Engine` + +### 1. Support for Permissions + +As of now query builder has no concept of permissions and moving towards a singular database API this needs to be added in the `Engine`. + +### 2. Implementing the missing features which are present in `frappe.get_list` and `frappe.get_all` (do we even need so much magic?) + +Moving to a singular Database API (database.py + db_query.py) all the support present in `get_list` and `get_all` needs to be present in the new `Engine` as well however this creates alot of security cracks, so moving the a *new and more restrictive version* of the database API with backward compatibility perhaps would be the right way to go? diff --git a/frappe/sessions.py b/frappe/sessions.py index 20891db2e6..9c739f3a96 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -32,12 +32,11 @@ def clear(): frappe.response["message"] = _("Cache Cleared") -def clear_sessions(user=None, keep_current=False, device=None, force=False): +def clear_sessions(user=None, keep_current=False, force=False): """Clear other sessions of the current user. Called at login / logout :param user: user name (default: current user) :param keep_current: keep current session (default: false) - :param device: delete sessions of this device (default: desktop, mobile) :param force: triggered by the user (default false) """ @@ -45,35 +44,26 @@ def clear_sessions(user=None, keep_current=False, device=None, force=False): if force: reason = "Force Logged out by the user" - for sid in get_sessions_to_clear(user, keep_current, device): + for sid in get_sessions_to_clear(user, keep_current): delete_session(sid, reason=reason) -def get_sessions_to_clear(user=None, keep_current=False, device=None): +def get_sessions_to_clear(user=None, keep_current=False): """Returns sessions of the current user. Called at login / logout :param user: user name (default: current user) :param keep_current: keep current session (default: false) - :param device: delete sessions of this device (default: desktop, mobile) """ if not user: user = frappe.session.user - if not device: - device = ("desktop", "mobile") - - if not isinstance(device, (tuple, list)): - device = (device,) - offset = 0 if user == frappe.session.user: simultaneous_sessions = frappe.db.get_value("User", user, "simultaneous_sessions") or 1 offset = simultaneous_sessions - 1 session = DocType("Sessions") - session_id = frappe.qb.from_(session).where( - (session.user == user) & (session.device.isin(device)) - ) + session_id = frappe.qb.from_(session).where(session.user == user) if keep_current: session_id = session_id.where(session.sid != frappe.session.sid) @@ -121,25 +111,18 @@ def clear_all_sessions(reason=None): def get_expired_sessions(): """Returns list of expired sessions""" + sessions = DocType("Sessions") - expired = [] - for device in ("desktop", "mobile"): - expired.extend( - frappe.db.get_values( - sessions, - filters=( - PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})") - > get_expiry_period_for_query(device) - ) - & (sessions.device == device), - fieldname="sid", - order_by=None, - pluck=True, - ) - ) - - return expired + return frappe.db.get_values( + sessions, + filters=( + PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})") > get_expiry_period_for_query() + ), + fieldname="sid", + order_by=None, + pluck=True, + ) def clear_expired_sessions(): @@ -218,14 +201,13 @@ def generate_csrf_token(): class Session: - __slots__ = ("user", "device", "user_type", "full_name", "data", "time_diff", "sid") + __slots__ = ("user", "user_type", "full_name", "data", "time_diff", "sid") def __init__(self, user, resume=False, full_name=None, user_type=None): self.sid = cstr( frappe.form_dict.get("sid") or unquote(frappe.request.cookies.get("sid", "Guest")) ) self.user = user - self.device = frappe.form_dict.get("device") or "desktop" self.user_type = user_type self.full_name = full_name self.data = frappe._dict({"data": frappe._dict({})}) @@ -257,10 +239,9 @@ class Session: self.data.data.update( { "last_updated": frappe.utils.now(), - "session_expiry": get_expiry_period(self.device), + "session_expiry": get_expiry_period(), "full_name": self.full_name, "user_type": self.user_type, - "device": self.device, "session_country": get_geo_ip_country(frappe.local.request_ip) if frappe.local.request_ip else None, @@ -289,9 +270,9 @@ class Session: def insert_session_record(self): frappe.db.sql( """insert into `tabSessions` - (`sessiondata`, `user`, `lastupdate`, `sid`, `status`, `device`) - values (%s , %s, NOW(), %s, 'Active', %s)""", - (str(self.data["data"]), self.data["user"], self.data["sid"], self.device), + (`sessiondata`, `user`, `lastupdate`, `sid`, `status`) + values (%s , %s, NOW(), %s, 'Active')""", + (str(self.data["data"]), self.data["user"], self.data["sid"]), ) # also add to memcache @@ -308,7 +289,6 @@ class Session: self.data.update({"data": data, "user": data.user, "sid": self.sid}) self.user = data.user validate_ip_address(self.user) - self.device = data.device else: self.start_as_guest() @@ -359,22 +339,11 @@ class Session: def get_session_data_from_db(self): sessions = DocType("Sessions") - - self.device = ( - frappe.db.get_value( - sessions, - filters=sessions.sid == self.sid, - fieldname="device", - order_by=None, - ) - or "desktop" - ) rec = frappe.db.get_values( sessions, filters=(sessions.sid == self.sid) & ( - PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})") - < get_expiry_period_for_query(self.device) + PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})") < get_expiry_period_for_query() ), fieldname=["user", "sessiondata"], order_by=None, @@ -437,29 +406,23 @@ class Session: return updated_in_db -def get_expiry_period_for_query(device=None): +def get_expiry_period_for_query(): if frappe.db.db_type == "postgres": - return get_expiry_period(device) + return get_expiry_period() else: - return get_expiry_in_seconds(device=device) + return get_expiry_in_seconds() -def get_expiry_in_seconds(expiry=None, device=None): +def get_expiry_in_seconds(expiry=None): if not expiry: - expiry = get_expiry_period(device) + expiry = get_expiry_period() + parts = expiry.split(":") return (cint(parts[0]) * 3600) + (cint(parts[1]) * 60) + cint(parts[2]) -def get_expiry_period(device="desktop"): - if device == "mobile": - key = "session_expiry_mobile" - default = "720:00:00" - else: - key = "session_expiry" - default = "06:00:00" - - exp_sec = frappe.defaults.get_global_default(key) or default +def get_expiry_period(): + exp_sec = frappe.defaults.get_global_default("session_expiry") or "06:00:00" # incase seconds is missing if len(exp_sec.split(":")) == 2: diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 8365947e6c..60dd0396de 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -19,7 +19,6 @@ login.bind_events = function () { args.cmd = "login"; args.usr = frappe.utils.xss_sanitise(($("#login_email").val() || "").trim()); args.pwd = $("#login_password").val(); - args.device = "desktop"; if (!args.usr || !args.pwd) { frappe.msgprint('{{ _("Both login and password required") }}'); return false; @@ -73,7 +72,6 @@ login.bind_events = function () { args.cmd = "{{ ldap_settings.method }}"; args.usr = ($("#login_email").val() || "").trim(); args.pwd = $("#login_password").val(); - args.device = "desktop"; if (!args.usr || !args.pwd) { login.set_status('{{ _("Both login and password required") }}', 'red'); return false; diff --git a/frappe/utils/install.py b/frappe/utils/install.py index 0d5abd52e8..cd15c957f9 100644 --- a/frappe/utils/install.py +++ b/frappe/utils/install.py @@ -169,6 +169,7 @@ def before_tests(): if not int(frappe.db.get_single_value("System Settings", "setup_complete") or 0): complete_setup_wizard() + frappe.db.set_value("Website Settings", "Website Settings", "disable_signup", 0) frappe.db.commit() frappe.clear_cache() diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index d102ac2fd8..73b2dc2331 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -342,7 +342,7 @@ def get_context(context): return False if self.apply_document_permissions: - return frappe.get_doc(doctype, name).has_permission() + return frappe.get_doc(doctype, name).has_permission(permtype=ptype) # owner matches elif frappe.db.get_value(doctype, name, "owner") == frappe.session.user: @@ -365,7 +365,7 @@ def get_web_form_module(doc): @frappe.whitelist(allow_guest=True) @rate_limit(key="web_form", limit=5, seconds=60, methods=["POST"]) -def accept(web_form, data, docname=None): +def accept(web_form, data): """Save the web form""" data = frappe._dict(json.loads(data)) @@ -373,19 +373,20 @@ def accept(web_form, data, docname=None): files_to_delete = [] web_form = frappe.get_doc("Web Form", web_form) + doctype = web_form.doc_type if data.name and not web_form.allow_edit: frappe.throw(_("You are not allowed to update this Web Form Document")) frappe.flags.in_web_form = True - meta = frappe.get_meta(data.doctype) + meta = frappe.get_meta(doctype) - if docname: + if data.name: # update - doc = frappe.get_doc(data.doctype, docname) + doc = frappe.get_doc(doctype, data.name) else: # insert - doc = frappe.new_doc(data.doctype) + doc = frappe.new_doc(doctype) # set values for field in web_form.web_form_fields: @@ -406,7 +407,7 @@ def accept(web_form, data, docname=None): doc.set(fieldname, value) if doc.name: - if web_form.has_web_form_permission(doc.doctype, doc.name, "write"): + if web_form.has_web_form_permission(doctype, doc.name, "write"): doc.save(ignore_permissions=True) else: # only if permissions are present @@ -428,7 +429,7 @@ def accept(web_form, data, docname=None): # remove earlier attached file (if exists) if doc.get(fieldname): - remove_file_by_url(doc.get(fieldname), doctype=doc.doctype, name=doc.name) + remove_file_by_url(doc.get(fieldname), doctype=doctype, name=doc.name) # save new file filename, dataurl = filedata.split(",", 1) @@ -436,7 +437,7 @@ def accept(web_form, data, docname=None): { "doctype": "File", "file_name": filename, - "attached_to_doctype": doc.doctype, + "attached_to_doctype": doctype, "attached_to_name": doc.name, "content": dataurl, "decode": True, @@ -452,7 +453,7 @@ def accept(web_form, data, docname=None): if files_to_delete: for f in files_to_delete: if f: - remove_file_by_url(f, doctype=doc.doctype, name=doc.name) + remove_file_by_url(f, doctype=doctype, name=doc.name) frappe.flags.web_form_doc = doc return doc diff --git a/frappe/website/doctype/website_settings/website_settings.json b/frappe/website/doctype/website_settings/website_settings.json index 29827afd80..3bf7e4cb30 100644 --- a/frappe/website/doctype/website_settings/website_settings.json +++ b/frappe/website/doctype/website_settings/website_settings.json @@ -247,7 +247,7 @@ "read_only": 1 }, { - "default": "0", + "default": "1", "description": "Disable Customer Signup link in Login page", "fieldname": "disable_signup", "fieldtype": "Check", @@ -476,7 +476,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-10-18 09:50:24.621839", + "modified": "2022-12-05 04:17:56.478757", "modified_by": "Administrator", "module": "Website", "name": "Website Settings",