Merge branch 'develop' into ldap-multiple-email

This commit is contained in:
Shariq Ansari 2022-12-05 22:28:51 +05:30 committed by GitHub
commit 2201601170
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 187 additions and 111 deletions

View file

@ -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",

View file

@ -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":

View file

@ -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;

View file

@ -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):

View file

@ -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
);

View file

@ -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}

View file

@ -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])

View file

@ -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",

View file

@ -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:

View file

@ -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");

View file

@ -14,7 +14,7 @@
// J<>rgen Tjern<72> - 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

View file

@ -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) => {

View file

@ -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)
<H2 align="center">Query</H2>
## 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?

View file

@ -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:

View file

@ -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;

View file

@ -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()

View file

@ -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

View file

@ -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",