Merge branch 'develop' into permlevel-apis
This commit is contained in:
commit
4be74bc013
42 changed files with 1043 additions and 1017 deletions
1
.flake8
1
.flake8
|
|
@ -69,6 +69,7 @@ ignore =
|
|||
F841,
|
||||
E713,
|
||||
E712,
|
||||
B028,
|
||||
|
||||
max-line-length = 200
|
||||
exclude=,test_*.py
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import click
|
|||
from werkzeug.local import Local, release_local
|
||||
|
||||
from frappe.query_builder import (
|
||||
get_qb_engine,
|
||||
get_query,
|
||||
get_query_builder,
|
||||
patch_query_aggregation,
|
||||
patch_query_execute,
|
||||
|
|
@ -244,7 +244,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False) -> None:
|
|||
local.session = _dict()
|
||||
local.dev_server = _dev_server
|
||||
local.qb = get_query_builder(local.conf.db_type or "mariadb")
|
||||
local.qb.engine = get_qb_engine()
|
||||
local.qb.get_query = get_query
|
||||
setup_module_map()
|
||||
|
||||
if not _qb_patched.get(local.conf.db_type):
|
||||
|
|
@ -770,7 +770,12 @@ def is_whitelisted(method):
|
|||
|
||||
is_guest = session["user"] == "Guest"
|
||||
if method not in whitelisted or is_guest and method not in guest_methods:
|
||||
throw(_("Not permitted"), PermissionError)
|
||||
summary = _("You are not permitted to access this resource.")
|
||||
detail = _("Function {0} is not whitelisted.").format(
|
||||
bold(f"{method.__module__}.{method.__name__}")
|
||||
)
|
||||
msg = f"<details><summary>{summary}</summary>{detail}</details>"
|
||||
throw(msg, PermissionError, title="Method Not Allowed")
|
||||
|
||||
if is_guest and method not in xss_safe_methods:
|
||||
# strictly sanitize form_dict
|
||||
|
|
@ -1399,23 +1404,37 @@ def get_all_apps(with_internal_apps=True, sites_path=None):
|
|||
|
||||
|
||||
@request_cache
|
||||
def get_installed_apps(sort=False, frappe_last=False):
|
||||
"""Get list of installed apps in current site."""
|
||||
def get_installed_apps(sort=False, frappe_last=False, *, _ensure_on_bench=False):
|
||||
"""
|
||||
Get list of installed apps in current site.
|
||||
|
||||
:param sort: [DEPRECATED] Sort installed apps based on the sequence in sites/apps.txt
|
||||
:param frappe_last: [DEPRECATED] Keep frappe last. Do not use this, reverse the app list instead.
|
||||
:param ensure_on_bench: Only return apps that are present on bench.
|
||||
"""
|
||||
from frappe.utils.deprecations import deprecation_warning
|
||||
|
||||
if getattr(flags, "in_install_db", True):
|
||||
return []
|
||||
|
||||
if not db:
|
||||
connect()
|
||||
|
||||
if not local.all_apps:
|
||||
local.all_apps = cache().get_value("all_apps", get_all_apps)
|
||||
|
||||
installed = json.loads(db.get_global("installed_apps") or "[]")
|
||||
|
||||
if sort:
|
||||
if not local.all_apps:
|
||||
local.all_apps = cache().get_value("all_apps", get_all_apps)
|
||||
|
||||
deprecation_warning("`sort` argument is deprecated and will be removed in v15.")
|
||||
installed = [app for app in local.all_apps if app in installed]
|
||||
|
||||
if _ensure_on_bench:
|
||||
all_apps = cache().get_value("all_apps", get_all_apps)
|
||||
installed = [app for app in installed if app in all_apps]
|
||||
|
||||
if frappe_last:
|
||||
deprecation_warning("`frappe_last` argument is deprecated and will be removed in v15.")
|
||||
if "frappe" in installed:
|
||||
installed.remove("frappe")
|
||||
installed.append("frappe")
|
||||
|
|
@ -1445,7 +1464,7 @@ def _load_app_hooks(app_name: str | None = None):
|
|||
import types
|
||||
|
||||
hooks = {}
|
||||
apps = [app_name] if app_name else get_installed_apps(sort=True)
|
||||
apps = [app_name] if app_name else get_installed_apps(_ensure_on_bench=True)
|
||||
|
||||
for app in apps:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ frappe.ui.form.on("File", {
|
|||
|
||||
preview_file: function (frm) {
|
||||
let $preview = "";
|
||||
let file_name = frm.doc.file_name.split("?")[0];
|
||||
let file_extension = file_name.split(".").pop()?.toLowerCase();
|
||||
|
||||
if (frappe.utils.is_image_file(frm.doc.file_url)) {
|
||||
$preview = $(`<div class="img_preview">
|
||||
|
|
@ -40,7 +42,7 @@ frappe.ui.form.on("File", {
|
|||
${__("Your browser does not support the video element.")}
|
||||
</video>
|
||||
</div>`);
|
||||
} else if (frm.doc.file_name.split("?")[0].endsWith(".pdf")) {
|
||||
} else if (file_extension === "pdf") {
|
||||
$preview = $(`<div class="img_preview">
|
||||
<object style="background:#323639;" width="100%">
|
||||
<embed
|
||||
|
|
@ -51,7 +53,7 @@ frappe.ui.form.on("File", {
|
|||
>
|
||||
</object>
|
||||
</div>`);
|
||||
} else if (frm.doc.file_name.split("?")[0].endsWith(".mp3")) {
|
||||
} else if (file_extension === "mp3") {
|
||||
$preview = $(`<div class="img_preview">
|
||||
<audio width="480" height="60" controls>
|
||||
<source src="${frm.doc.file_url}" type="audio/mpeg">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,62 @@
|
|||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Installed Applications", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
refresh: function (frm) {
|
||||
frm.add_custom_button(__("Update Hooks Resolution Order"), () => {
|
||||
frm.trigger("show_update_order_dialog");
|
||||
});
|
||||
},
|
||||
|
||||
show_update_order_dialog() {
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Update Hooks Resolution Order"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "apps",
|
||||
fieldtype: "Table",
|
||||
label: __("Installed Apps"),
|
||||
cannot_add_rows: true,
|
||||
cannot_delete_rows: true,
|
||||
in_place_edit: true,
|
||||
data: [],
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "app_name",
|
||||
label: __("App Name"),
|
||||
in_list_view: 1,
|
||||
read_only: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
primary_action: function () {
|
||||
const new_order = this.get_values()["apps"].map((row) => row.app_name);
|
||||
frappe.call({
|
||||
method: "frappe.core.doctype.installed_applications.installed_applications.update_installed_apps_order",
|
||||
freeze: true,
|
||||
args: {
|
||||
new_order: new_order,
|
||||
},
|
||||
});
|
||||
this.hide();
|
||||
},
|
||||
primary_action_label: __("Update Order"),
|
||||
});
|
||||
|
||||
frappe
|
||||
.xcall(
|
||||
"frappe.core.doctype.installed_applications.installed_applications.get_installed_app_order"
|
||||
)
|
||||
.then((data) => {
|
||||
data.forEach((app) => {
|
||||
dialog.fields_dict.apps.df.data.push({
|
||||
app_name: app,
|
||||
});
|
||||
});
|
||||
|
||||
dialog.fields_dict.apps.grid.refresh();
|
||||
dialog.show();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class InvalidAppOrder(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InstalledApplications(Document):
|
||||
def update_versions(self):
|
||||
self.delete_key("installed_applications")
|
||||
|
|
@ -18,3 +25,51 @@ class InstalledApplications(Document):
|
|||
},
|
||||
)
|
||||
self.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_installed_apps_order(new_order: list[str] | str):
|
||||
"""Change the ordering of `installed_apps` global
|
||||
|
||||
This list is used to resolve hooks and by default it's order of installation on site.
|
||||
|
||||
Sometimes it might not be the ordering you want, so thie function is provided to override it.
|
||||
"""
|
||||
frappe.only_for("System Manager")
|
||||
|
||||
if isinstance(new_order, str):
|
||||
new_order = json.loads(new_order)
|
||||
|
||||
frappe.local.request_cache and frappe.local.request_cache.clear()
|
||||
existing_order = frappe.get_installed_apps(_ensure_on_bench=True)
|
||||
|
||||
if set(existing_order) != set(new_order) or not isinstance(new_order, list):
|
||||
frappe.throw(
|
||||
_("You are only allowed to update order, do not remove or add apps."), exc=InvalidAppOrder
|
||||
)
|
||||
|
||||
# Ensure frappe is always first regardless of user's preference.
|
||||
if "frappe" in new_order:
|
||||
new_order.remove("frappe")
|
||||
new_order.insert(0, "frappe")
|
||||
|
||||
frappe.db.set_global("installed_apps", json.dumps(new_order))
|
||||
|
||||
_create_version_log_for_change(existing_order, new_order)
|
||||
|
||||
|
||||
def _create_version_log_for_change(old, new):
|
||||
version = frappe.new_doc("Version")
|
||||
version.ref_doctype = "DefaultValue"
|
||||
version.docname = "installed_apps"
|
||||
version.data = frappe.as_json({"changed": [["current", json.dumps(old), json.dumps(new)]]})
|
||||
version.flags.ignore_links = True # This is a fake doctype
|
||||
version.flags.ignore_permissions = True
|
||||
version.insert()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_installed_app_order() -> list[str]:
|
||||
frappe.only_for("System Manager")
|
||||
|
||||
return frappe.get_installed_apps(_ensure_on_bench=True)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
# import frappe
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.installed_applications.installed_applications import (
|
||||
InvalidAppOrder,
|
||||
update_installed_apps_order,
|
||||
)
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestInstalledApplications(FrappeTestCase):
|
||||
pass
|
||||
def test_order_change(self):
|
||||
update_installed_apps_order(["frappe"])
|
||||
self.assertRaises(InvalidAppOrder, update_installed_apps_order, [])
|
||||
self.assertRaises(InvalidAppOrder, update_installed_apps_order, ["frappe", "deepmind"])
|
||||
|
|
|
|||
|
|
@ -305,12 +305,10 @@ class User(Document):
|
|||
.from_(user_role_doctype)
|
||||
.select(user_doctype.name)
|
||||
.where(user_role_doctype.role == "System Manager")
|
||||
.where(user_doctype.docstatus < 2)
|
||||
.where(user_doctype.enabled == 1)
|
||||
.where(user_role_doctype.parent == user_doctype.name)
|
||||
.where(user_role_doctype.parent.notin(["Administrator", self.name]))
|
||||
.limit(1)
|
||||
.distinct()
|
||||
).run()
|
||||
|
||||
def get_fullname(self):
|
||||
|
|
|
|||
|
|
@ -7,25 +7,24 @@
|
|||
"allow_print": 0,
|
||||
"apply_document_permissions": 0,
|
||||
"breadcrumbs": "[{\"title\": _(\"My Account\"), \"route\": \"me\"}]",
|
||||
"client_script": "frappe.web_form.after_load = () => {\n if (window.location.pathname.endsWith(\"/new\") && frappe.session.user) {\n let current_path = window.location.href;\n window.location.href = current_path.replace(\"/new\", \"/\" + frappe.session.user);\n }\n}",
|
||||
"creation": "2016-09-19 05:16:59.242754",
|
||||
"doc_type": "User",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Form",
|
||||
"idx": 0,
|
||||
"introduction_text": "",
|
||||
"is_multi_step_form": 0,
|
||||
"is_standard": 1,
|
||||
"list_columns": [],
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2022-07-18 16:51:19.796411",
|
||||
"modified": "2023-01-18 10:26:26.766414",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "edit-profile",
|
||||
"owner": "Administrator",
|
||||
"published": 1,
|
||||
"route": "update-profile",
|
||||
"route_to_success_link": 0,
|
||||
"show_attachments": 0,
|
||||
"show_list": 0,
|
||||
"show_sidebar": 0,
|
||||
|
|
|
|||
|
|
@ -622,7 +622,7 @@ class Database:
|
|||
return [map(values.get, fields)]
|
||||
|
||||
else:
|
||||
r = frappe.qb.engine.get_query(
|
||||
r = frappe.qb.get_query(
|
||||
"Singles",
|
||||
filters={"field": ("in", tuple(fields)), "doctype": doctype},
|
||||
fields=["field", "value"],
|
||||
|
|
@ -655,7 +655,7 @@ class Database:
|
|||
# Get coulmn and value of the single doctype Accounts Settings
|
||||
account_settings = frappe.db.get_singles_dict("Accounts Settings")
|
||||
"""
|
||||
queried_result = frappe.qb.engine.get_query(
|
||||
queried_result = frappe.qb.get_query(
|
||||
"Singles",
|
||||
filters={"doctype": doctype},
|
||||
fields=["field", "value"],
|
||||
|
|
@ -761,7 +761,7 @@ class Database:
|
|||
if cache and fieldname in self.value_cache[doctype]:
|
||||
return self.value_cache[doctype][fieldname]
|
||||
|
||||
val = frappe.qb.engine.get_query(
|
||||
val = frappe.qb.get_query(
|
||||
table="Singles",
|
||||
filters={"doctype": doctype, "field": fieldname},
|
||||
fields="value",
|
||||
|
|
@ -801,10 +801,10 @@ class Database:
|
|||
distinct=False,
|
||||
limit=None,
|
||||
):
|
||||
query = frappe.qb.engine.get_query(
|
||||
query = frappe.qb.get_query(
|
||||
table=doctype,
|
||||
filters=filters,
|
||||
orderby=order_by,
|
||||
order_by=order_by,
|
||||
for_update=for_update,
|
||||
fields=fields,
|
||||
distinct=distinct,
|
||||
|
|
@ -830,15 +830,14 @@ class Database:
|
|||
as_dict=False,
|
||||
):
|
||||
if names := list(filter(None, names)):
|
||||
return frappe.qb.engine.get_query(
|
||||
return frappe.qb.get_query(
|
||||
doctype,
|
||||
fields=field,
|
||||
filters=names,
|
||||
order_by=order_by,
|
||||
pluck=pluck,
|
||||
distinct=distinct,
|
||||
limit=limit,
|
||||
).run(debug=debug, run=run, as_dict=as_dict)
|
||||
).run(debug=debug, run=run, as_dict=as_dict, pluck=pluck)
|
||||
return {}
|
||||
|
||||
def set_value(
|
||||
|
|
@ -887,7 +886,7 @@ class Database:
|
|||
field, val, modified=modified, modified_by=modified_by, update_modified=update_modified
|
||||
)
|
||||
|
||||
query = frappe.qb.engine.build_conditions(table=dt, filters=dn, update=True)
|
||||
query = frappe.qb.get_query(table=dt, filters=dn, update=True)
|
||||
|
||||
if isinstance(dn, str):
|
||||
frappe.clear_document_cache(dt, dn)
|
||||
|
|
@ -1052,9 +1051,9 @@ class Database:
|
|||
cache_count = frappe.cache().get_value(f"doctype:count:{dt}")
|
||||
if cache_count is not None:
|
||||
return cache_count
|
||||
count = frappe.qb.engine.get_query(
|
||||
table=dt, filters=filters, fields=Count("*"), distinct=distinct
|
||||
).run(debug=debug)[0][0]
|
||||
count = frappe.qb.get_query(table=dt, filters=filters, fields=Count("*"), distinct=distinct).run(
|
||||
debug=debug
|
||||
)[0][0]
|
||||
if not filters and cache:
|
||||
frappe.cache().set_value(f"doctype:count:{dt}", count, expires_in_sec=86400)
|
||||
return count
|
||||
|
|
@ -1195,7 +1194,7 @@ class Database:
|
|||
Doctype name can be passed directly, it will be pre-pended with `tab`.
|
||||
"""
|
||||
filters = filters or kwargs.get("conditions")
|
||||
query = frappe.qb.engine.build_conditions(table=doctype, filters=filters).delete()
|
||||
query = frappe.qb.get_query(table=doctype, filters=filters, delete=True)
|
||||
if "debug" not in kwargs:
|
||||
kwargs["debug"] = debug
|
||||
return query.run(**kwargs)
|
||||
|
|
|
|||
138
frappe/database/operator_map.py
Normal file
138
frappe/database/operator_map.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import operator
|
||||
from typing import Callable
|
||||
|
||||
import frappe
|
||||
from frappe.database.utils import NestedSetHierarchy
|
||||
from frappe.model.db_query import get_timespan_date_range
|
||||
from frappe.query_builder import Field
|
||||
|
||||
|
||||
def like(key: Field, value: str) -> frappe.qb:
|
||||
"""Wrapper method for `LIKE`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (str): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `LIKE`
|
||||
"""
|
||||
return key.like(value)
|
||||
|
||||
|
||||
def func_in(key: Field, value: list | tuple) -> frappe.qb:
|
||||
"""Wrapper method for `IN`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (Union[int, str]): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `IN`
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
value = value.split(",")
|
||||
return key.isin(value)
|
||||
|
||||
|
||||
def not_like(key: Field, value: str) -> frappe.qb:
|
||||
"""Wrapper method for `NOT LIKE`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (str): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `NOT LIKE`
|
||||
"""
|
||||
return key.not_like(value)
|
||||
|
||||
|
||||
def func_not_in(key: Field, value: list | tuple | str):
|
||||
"""Wrapper method for `NOT IN`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (Union[int, str]): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `NOT IN`
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
value = value.split(",")
|
||||
return key.notin(value)
|
||||
|
||||
|
||||
def func_regex(key: Field, value: str) -> frappe.qb:
|
||||
"""Wrapper method for `REGEX`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (str): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `REGEX`
|
||||
"""
|
||||
return key.regex(value)
|
||||
|
||||
|
||||
def func_between(key: Field, value: list | tuple) -> frappe.qb:
|
||||
"""Wrapper method for `BETWEEN`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (Union[int, str]): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `BETWEEN`
|
||||
"""
|
||||
return key[slice(*value)]
|
||||
|
||||
|
||||
def func_is(key, value):
|
||||
"Wrapper for IS"
|
||||
return key.isnotnull() if value.lower() == "set" else key.isnull()
|
||||
|
||||
|
||||
def func_timespan(key: Field, value: str) -> frappe.qb:
|
||||
"""Wrapper method for `TIMESPAN`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (str): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `TIMESPAN`
|
||||
"""
|
||||
|
||||
return func_between(key, get_timespan_date_range(value))
|
||||
|
||||
|
||||
# default operators
|
||||
OPERATOR_MAP: dict[str, Callable] = {
|
||||
"+": operator.add,
|
||||
"=": operator.eq,
|
||||
"-": operator.sub,
|
||||
"!=": operator.ne,
|
||||
"<": operator.lt,
|
||||
">": operator.gt,
|
||||
"<=": operator.le,
|
||||
"=<": operator.le,
|
||||
">=": operator.ge,
|
||||
"=>": operator.ge,
|
||||
"/": operator.truediv,
|
||||
"*": operator.mul,
|
||||
"in": func_in,
|
||||
"not in": func_not_in,
|
||||
"like": like,
|
||||
"not like": not_like,
|
||||
"regex": func_regex,
|
||||
"between": func_between,
|
||||
"is": func_is,
|
||||
"timespan": func_timespan,
|
||||
"nested_set": NestedSetHierarchy,
|
||||
# TODO: Add support for custom operators (WIP) - via filters_config hooks
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -34,6 +34,14 @@ def is_pypika_function_object(field: str) -> bool:
|
|||
return getattr(field, "__module__", None) == "pypika.functions" or isinstance(field, Function)
|
||||
|
||||
|
||||
def get_doctype_name(table_name: str) -> str:
|
||||
if table_name.startswith(("tab", "`tab", '"tab')):
|
||||
table_name = table_name.replace("tab", "", 1)
|
||||
table_name = table_name.replace("`", "")
|
||||
table_name = table_name.replace('"', "")
|
||||
return table_name
|
||||
|
||||
|
||||
class LazyString:
|
||||
def _setup(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
|
|||
if txt:
|
||||
search_conditions = [numberCard[field].like(f"%{txt}%") for field in searchfields]
|
||||
|
||||
condition_query = frappe.qb.engine.build_conditions(doctype, filters)
|
||||
condition_query = frappe.qb.get_query(doctype, filters=filters)
|
||||
|
||||
return (
|
||||
condition_query.select(numberCard.name, numberCard.label, numberCard.document_type)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d
|
|||
ToDo = DocType("ToDo")
|
||||
User = DocType("User")
|
||||
count = Count("*").as_("count")
|
||||
filtered_records = frappe.qb.engine.build_conditions(doctype, current_filters).select("name")
|
||||
filtered_records = frappe.qb.get_query(doctype, filters=current_filters, fields=["name"])
|
||||
|
||||
return (
|
||||
frappe.qb.from_(ToDo)
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
|
|||
}
|
||||
|
||||
get_setup_slides_filtered_by_domain() {
|
||||
var filtered_slides = [];
|
||||
let filtered_slides = [];
|
||||
frappe.setup.slides.forEach(function (slide) {
|
||||
if (frappe.setup.domains) {
|
||||
let active_domains = frappe.setup.domains;
|
||||
|
|
@ -329,7 +329,7 @@ frappe.setup.SetupWizardSlide = class SetupWizardSlide extends frappe.ui.Slide {
|
|||
}
|
||||
|
||||
set_init_values() {
|
||||
var me = this;
|
||||
let me = this;
|
||||
// set values from frappe.setup.values
|
||||
if (frappe.wizard.values && this.fields) {
|
||||
this.fields.forEach(function (f) {
|
||||
|
|
@ -348,7 +348,7 @@ frappe.setup.slides_settings = [
|
|||
{
|
||||
// Welcome (language) slide
|
||||
name: "welcome",
|
||||
title: __("Hello!"),
|
||||
title: __("Welcome"),
|
||||
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -418,16 +418,9 @@ frappe.setup.slides_settings = [
|
|||
{
|
||||
// Profile slide
|
||||
name: "user",
|
||||
title: __("The First User: You"),
|
||||
title: __("Let's setup your account"),
|
||||
icon: "fa fa-user",
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Attach Image",
|
||||
fieldname: "attach_user_image",
|
||||
label: __("Attach Your Picture"),
|
||||
is_private: 0,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
fieldname: "full_name",
|
||||
label: __("Full Name"),
|
||||
|
|
@ -456,15 +449,6 @@ frappe.setup.slides_settings = [
|
|||
[frappe.boot.user.first_name, frappe.boot.user.last_name].join(" ").trim()
|
||||
);
|
||||
}
|
||||
|
||||
var user_image = frappe.get_cookie("user_image");
|
||||
var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper;
|
||||
|
||||
if (user_image) {
|
||||
$attach_user_image.find(".missing-image").toggle(false);
|
||||
$attach_user_image.find("img").attr("src", decodeURIComponent(user_image));
|
||||
$attach_user_image.find(".img-container").toggle(true);
|
||||
}
|
||||
delete slide.form.fields_dict.email;
|
||||
} else {
|
||||
slide.form.fields_dict.email.df.reqd = 1;
|
||||
|
|
@ -484,7 +468,7 @@ frappe.setup.slides_settings = [
|
|||
let email = frappe.setup.data.email;
|
||||
slide.form.fields_dict.email.set_input(email);
|
||||
if (frappe.get_gravatar(email, 200)) {
|
||||
var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper;
|
||||
let $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper;
|
||||
$attach_user_image.find(".missing-image").toggle(false);
|
||||
$attach_user_image.find("img").attr("src", frappe.get_gravatar(email, 200));
|
||||
$attach_user_image.find(".img-container").toggle(true);
|
||||
|
|
@ -569,7 +553,7 @@ frappe.setup.utils = {
|
|||
.on("change", function () {
|
||||
clearTimeout(slide.language_call_timeout);
|
||||
slide.language_call_timeout = setTimeout(() => {
|
||||
var lang = $(this).val() || "English";
|
||||
let lang = $(this).val() || "English";
|
||||
frappe._messages = {};
|
||||
frappe.call({
|
||||
method: "frappe.desk.page.setup_wizard.setup_wizard.load_messages",
|
||||
|
|
@ -595,9 +579,9 @@ frappe.setup.utils = {
|
|||
Bind a slide's country, timezone and currency fields
|
||||
*/
|
||||
slide.get_input("country").on("change", function () {
|
||||
var country = slide.get_input("country").val();
|
||||
var $timezone = slide.get_input("timezone");
|
||||
var data = frappe.setup.data.regional_data;
|
||||
let country = slide.get_input("country").val();
|
||||
let $timezone = slide.get_input("timezone");
|
||||
let data = frappe.setup.data.regional_data;
|
||||
|
||||
$timezone.empty();
|
||||
|
||||
|
|
@ -618,12 +602,12 @@ frappe.setup.utils = {
|
|||
});
|
||||
|
||||
slide.get_input("currency").on("change", function () {
|
||||
var currency = slide.get_input("currency").val();
|
||||
let currency = slide.get_input("currency").val();
|
||||
if (!currency) return;
|
||||
frappe.model.with_doc("Currency", currency, function () {
|
||||
frappe.provide("locals.:Currency." + currency);
|
||||
var currency_doc = frappe.model.get_doc("Currency", currency);
|
||||
var number_format = currency_doc.number_format;
|
||||
let currency_doc = frappe.model.get_doc("Currency", currency);
|
||||
let number_format = currency_doc.number_format;
|
||||
if (number_format === "#.###") {
|
||||
number_format = "#.###,##";
|
||||
} else if (number_format === "#,###") {
|
||||
|
|
|
|||
|
|
@ -67,27 +67,29 @@ frappe.email_defaults_pop = {
|
|||
};
|
||||
|
||||
function oauth_access(frm) {
|
||||
return frappe.call({
|
||||
method: "frappe.email.oauth.oauth_access",
|
||||
args: {
|
||||
email_account: frm.doc.name,
|
||||
service: frm.doc.service || "",
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
window.open(r.message.url, "_self");
|
||||
}
|
||||
},
|
||||
frappe.model.with_doc("Connected App", frm.doc.connected_app, () => {
|
||||
const connected_app = frappe.get_doc("Connected App", frm.doc.connected_app);
|
||||
return frappe.call({
|
||||
doc: connected_app,
|
||||
method: "initiate_web_application_flow",
|
||||
args: {
|
||||
success_uri: window.location.pathname,
|
||||
user: frm.doc.connected_user,
|
||||
},
|
||||
callback: function (r) {
|
||||
window.open(r.message, "_self");
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function set_default_max_attachment_size(frm, field) {
|
||||
if (frm.doc.__islocal && !frm.doc[field]) {
|
||||
function set_default_max_attachment_size(frm) {
|
||||
if (frm.doc.__islocal && !frm.doc["attachment_limit"]) {
|
||||
frappe.call({
|
||||
method: "frappe.core.api.file.get_max_file_size",
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frm.set_value(field, Number(r.message) / (1024 * 1024));
|
||||
frm.set_value("attachment_limit", Number(r.message) / (1024 * 1024));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -104,8 +106,6 @@ frappe.ui.form.on("Email Account", {
|
|||
frm.set_value(key, value);
|
||||
});
|
||||
}
|
||||
frm.events.show_gmail_message_for_less_secure_apps(frm);
|
||||
frm.events.toggle_auth_method(frm);
|
||||
},
|
||||
|
||||
use_imap: function (frm) {
|
||||
|
|
@ -133,12 +133,6 @@ frappe.ui.form.on("Email Account", {
|
|||
},
|
||||
|
||||
onload: function (frm) {
|
||||
if (frappe.utils.get_query_params().successful_authorization === "1") {
|
||||
frappe.show_alert(__("Successfully Authorized"));
|
||||
// FIXME: find better alternative
|
||||
window.history.replaceState(null, "", window.location.pathname);
|
||||
}
|
||||
|
||||
frm.set_df_property("append_to", "only_select", true);
|
||||
frm.set_query(
|
||||
"append_to",
|
||||
|
|
@ -153,15 +147,13 @@ frappe.ui.form.on("Email Account", {
|
|||
frm.add_child("imap_folder", { folder_name: "INBOX" });
|
||||
frm.refresh_field("imap_folder");
|
||||
}
|
||||
frm.toggle_display(["auth_method"], frm.doc.service === "GMail");
|
||||
set_default_max_attachment_size(frm, "attachment_limit");
|
||||
set_default_max_attachment_size(frm);
|
||||
frm.events.show_oauth_authorization_message(frm);
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.events.enable_incoming(frm);
|
||||
frm.events.notify_if_unreplied(frm);
|
||||
frm.events.show_gmail_message_for_less_secure_apps(frm);
|
||||
frm.events.show_oauth_authorization_message(frm);
|
||||
|
||||
if (frappe.route_flags.delete_user_from_locals && frappe.route_flags.linked_user) {
|
||||
delete frappe.route_flags.delete_user_from_locals;
|
||||
|
|
@ -169,47 +161,31 @@ frappe.ui.form.on("Email Account", {
|
|||
}
|
||||
},
|
||||
|
||||
after_save(frm) {
|
||||
if (frm.doc.auth_method === "OAuth" && !frm.doc.refresh_token) {
|
||||
oauth_access(frm);
|
||||
}
|
||||
},
|
||||
|
||||
toggle_auth_method: function (frm) {
|
||||
if (frm.doc.service !== "GMail") {
|
||||
frm.toggle_display(["auth_method"], false);
|
||||
frm.doc.auth_method = "Basic";
|
||||
} else {
|
||||
frm.toggle_display(["auth_method"], true);
|
||||
}
|
||||
},
|
||||
|
||||
show_gmail_message_for_less_secure_apps: function (frm) {
|
||||
frm.dashboard.clear_headline();
|
||||
let msg = __(
|
||||
"GMail will only work if you enable 2-step authentication and use app-specific password OR use OAuth."
|
||||
);
|
||||
let cta = __("Read the step by step guide here.");
|
||||
msg += ` <a target="_blank" href="https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/email/email_account_setup_with_gmail">${cta}</a>`;
|
||||
if (frm.doc.service === "GMail") {
|
||||
frm.dashboard.set_headline_alert(msg);
|
||||
}
|
||||
authorize_api_access: function (frm) {
|
||||
oauth_access(frm);
|
||||
},
|
||||
|
||||
show_oauth_authorization_message(frm) {
|
||||
if (frm.doc.auth_method === "OAuth" && !frm.doc.refresh_token) {
|
||||
let msg = __(
|
||||
'OAuth has been enabled but not authorised. Please use "Authorise API Access" button to do the same.'
|
||||
);
|
||||
frm.dashboard.clear_headline();
|
||||
frm.dashboard.set_headline_alert(msg, "yellow");
|
||||
if (frm.doc.auth_method === "OAuth" && frm.doc.connected_app) {
|
||||
frappe.call({
|
||||
method: "frappe.integrations.doctype.connected_app.connected_app.has_token",
|
||||
args: {
|
||||
connected_app: frm.doc.connected_app,
|
||||
connected_user: frm.doc.connected_user,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.message) {
|
||||
let msg = __(
|
||||
'OAuth has been enabled but not authorised. Please use "Authorise API Access" button to do the same.'
|
||||
);
|
||||
frm.dashboard.clear_headline();
|
||||
frm.dashboard.set_headline_alert(msg, "yellow");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
authorize_api_access: function (frm) {
|
||||
oauth_access(frm);
|
||||
},
|
||||
|
||||
domain: frappe.utils.debounce((frm) => {
|
||||
if (frm.doc.domain) {
|
||||
frappe.call({
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
"awaiting_password",
|
||||
"ascii_encode_password",
|
||||
"column_break_10",
|
||||
"refresh_token",
|
||||
"access_token",
|
||||
"connected_app",
|
||||
"connected_user",
|
||||
"login_id_is_different",
|
||||
"login_id",
|
||||
"mailbox_settings",
|
||||
|
|
@ -203,7 +203,6 @@
|
|||
"label": "Use SSL"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:!doc.domain && doc.enable_incoming",
|
||||
"description": "Ignore attachments over this size",
|
||||
"fetch_from": "domain.attachment_limit",
|
||||
|
|
@ -577,25 +576,11 @@
|
|||
"label": "IMAP Details"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.service === \"GMail\" && doc.auth_method === \"OAuth\" && !doc.__islocal && !doc.__unsaved",
|
||||
"depends_on": "eval: doc.auth_method === \"OAuth\" && !doc.__islocal && !doc.__unsaved",
|
||||
"fieldname": "authorize_api_access",
|
||||
"fieldtype": "Button",
|
||||
"label": "Authorize API Access"
|
||||
},
|
||||
{
|
||||
"fieldname": "refresh_token",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Refresh Token",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "access_token",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Access Token",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "Basic",
|
||||
"fieldname": "auth_method",
|
||||
|
|
@ -610,12 +595,28 @@
|
|||
"fieldname": "use_starttls",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use STARTTLS"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.auth_method === \"OAuth\"",
|
||||
"fieldname": "connected_app",
|
||||
"fieldtype": "Link",
|
||||
"label": "Connected App",
|
||||
"mandatory_depends_on": "eval: doc.auth_method === \"OAuth\"",
|
||||
"options": "Connected App"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.auth_method === \"OAuth\"",
|
||||
"fieldname": "connected_user",
|
||||
"fieldtype": "Link",
|
||||
"label": "Connected User",
|
||||
"mandatory_depends_on": "eval: doc.auth_method === \"OAuth\"",
|
||||
"options": "User"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-inbox",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-23 00:31:05.305462",
|
||||
"modified": "2022-12-28 14:56:18.754804",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Account",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ from frappe.utils import cint, comma_or, cstr, parse_addr, validate_email_addres
|
|||
from frappe.utils.background_jobs import enqueue, get_jobs
|
||||
from frappe.utils.error import raise_error_on_no_output
|
||||
from frappe.utils.jinja import render_template
|
||||
from frappe.utils.password import decrypt, encrypt
|
||||
from frappe.utils.user import get_system_managers
|
||||
|
||||
|
||||
|
|
@ -83,23 +82,16 @@ class EmailAccount(Document):
|
|||
return
|
||||
|
||||
use_oauth = self.auth_method == "OAuth"
|
||||
validate_oauth = use_oauth and not (self.is_new() and not self.get_oauth_token())
|
||||
self.use_starttls = cint(self.use_imap and self.use_starttls and not self.use_ssl)
|
||||
|
||||
if getattr(self, "service", "") != "GMail" and use_oauth:
|
||||
self.auth_method = "Basic"
|
||||
use_oauth = False
|
||||
|
||||
if use_oauth:
|
||||
# no need for awaiting password for oauth
|
||||
self.awaiting_password = 0
|
||||
self.password = None
|
||||
|
||||
elif self.refresh_token:
|
||||
# clear access & refresh token
|
||||
self.refresh_token = self.access_token = None
|
||||
|
||||
if not frappe.local.flags.in_install and not self.awaiting_password:
|
||||
if self.refresh_token or self.password or self.smtp_server in ("127.0.0.1", "localhost"):
|
||||
if validate_oauth or self.password or self.smtp_server in ("127.0.0.1", "localhost"):
|
||||
if self.enable_incoming:
|
||||
self.get_incoming_server()
|
||||
self.no_failed = 0
|
||||
|
|
@ -188,6 +180,7 @@ class EmailAccount(Document):
|
|||
if frappe.cache().get_value("workers:no-internet") == True:
|
||||
return None
|
||||
|
||||
oauth_token = self.get_oauth_token()
|
||||
args = frappe._dict(
|
||||
{
|
||||
"email_account_name": self.email_account_name,
|
||||
|
|
@ -196,14 +189,12 @@ class EmailAccount(Document):
|
|||
"use_ssl": self.use_ssl,
|
||||
"use_starttls": self.use_starttls,
|
||||
"username": getattr(self, "login_id", None) or self.email_id,
|
||||
"service": getattr(self, "service", ""),
|
||||
"use_imap": self.use_imap,
|
||||
"email_sync_rule": email_sync_rule,
|
||||
"incoming_port": get_port(self),
|
||||
"initial_sync_count": self.initial_sync_count or 100,
|
||||
"use_oauth": self.auth_method == "OAuth",
|
||||
"refresh_token": decrypt(self.refresh_token) if self.refresh_token else None,
|
||||
"access_token": decrypt(self.access_token) if self.access_token else None,
|
||||
"access_token": oauth_token.get_password("access_token") if oauth_token else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -392,8 +383,6 @@ class EmailAccount(Document):
|
|||
},
|
||||
"name": {"conf_names": ("email_sender_name",), "default": "Frappe"},
|
||||
"auth_method": {"conf_names": ("auth_method"), "default": "Basic"},
|
||||
"access_token": {"conf_names": ("mail_access_token")},
|
||||
"refresh_token": {"conf_names": ("mail_refresh_token")},
|
||||
"from_site_config": {"default": True},
|
||||
}
|
||||
|
||||
|
|
@ -401,15 +390,13 @@ class EmailAccount(Document):
|
|||
for doc_field_name, d in field_to_conf_name_map.items():
|
||||
conf_names, default = d.get("conf_names") or [], d.get("default")
|
||||
value = [frappe.conf.get(k) for k in conf_names if frappe.conf.get(k)]
|
||||
|
||||
if doc_field_name in ("refresh_token", "access_token"):
|
||||
account_details[doc_field_name] = value and encrypt(value[0])
|
||||
else:
|
||||
account_details[doc_field_name] = (value and value[0]) or default
|
||||
account_details[doc_field_name] = (value and value[0]) or default
|
||||
|
||||
return account_details
|
||||
|
||||
def sendmail_config(self):
|
||||
oauth_token = self.get_oauth_token()
|
||||
|
||||
return {
|
||||
"email_account": self.name,
|
||||
"server": self.smtp_server,
|
||||
|
|
@ -418,10 +405,8 @@ class EmailAccount(Document):
|
|||
"password": self._password,
|
||||
"use_ssl": cint(self.use_ssl_for_outgoing),
|
||||
"use_tls": cint(self.use_tls),
|
||||
"service": getattr(self, "service", ""),
|
||||
"use_oauth": self.auth_method == "OAuth",
|
||||
"refresh_token": decrypt(self.refresh_token) if self.refresh_token else None,
|
||||
"access_token": decrypt(self.access_token) if self.access_token else None,
|
||||
"access_token": oauth_token.get_password("access_token") if oauth_token else None,
|
||||
}
|
||||
|
||||
def get_smtp_server(self):
|
||||
|
|
@ -681,6 +666,11 @@ class EmailAccount(Document):
|
|||
except Exception:
|
||||
self.log_error("Unable to add to Sent folder")
|
||||
|
||||
def get_oauth_token(self):
|
||||
if self.auth_method == "OAuth":
|
||||
connected_app = frappe.get_doc("Connected App", self.connected_app)
|
||||
return connected_app.get_active_token(self.connected_user)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_append_to(
|
||||
|
|
@ -776,25 +766,29 @@ def notify_unreplied():
|
|||
|
||||
def pull(now=False):
|
||||
"""Will be called via scheduler, pull emails from all enabled Email accounts."""
|
||||
from frappe.integrations.doctype.connected_app.connected_app import has_token
|
||||
|
||||
if frappe.cache().get_value("workers:no-internet") == True:
|
||||
if test_internet():
|
||||
frappe.cache().set_value("workers:no-internet", False)
|
||||
else:
|
||||
return
|
||||
return
|
||||
|
||||
doctype = frappe.qb.DocType("Email Account")
|
||||
email_accounts = (
|
||||
frappe.qb.from_(doctype)
|
||||
.select(doctype.name)
|
||||
.select(doctype.name, doctype.auth_method, doctype.connected_app, doctype.connected_user)
|
||||
.where(doctype.enable_incoming == 1)
|
||||
.where(
|
||||
(doctype.awaiting_password == 0)
|
||||
| ((doctype.auth_method == "OAuth") & (doctype.refresh_token.isnotnull()))
|
||||
)
|
||||
.where(doctype.awaiting_password == 0)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
for email_account in email_accounts:
|
||||
if email_account.auth_method == "OAuth" and not has_token(
|
||||
email_account.connected_app, email_account.connected_user
|
||||
):
|
||||
# don't try to pull from accounts which dont have access token (for Oauth)
|
||||
continue
|
||||
|
||||
if now:
|
||||
pull_from_email_account(email_account.name)
|
||||
|
||||
|
|
@ -917,7 +911,7 @@ def remove_user_email_inbox(email_account):
|
|||
@frappe.whitelist()
|
||||
def set_email_password(email_account, password):
|
||||
account = frappe.get_doc("Email Account", email_account)
|
||||
if account.awaiting_password and not account.auth_method == "OAuth":
|
||||
if account.awaiting_password and account.auth_method != "OAuth":
|
||||
account.awaiting_password = 0
|
||||
account.password = password
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -2,15 +2,8 @@ import base64
|
|||
from imaplib import IMAP4
|
||||
from poplib import POP3
|
||||
from smtplib import SMTP
|
||||
from urllib.parse import quote
|
||||
|
||||
import frappe
|
||||
from frappe.integrations.google_oauth import GoogleOAuth
|
||||
from frappe.utils.password import encrypt
|
||||
|
||||
|
||||
class OAuthenticationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Oauth:
|
||||
|
|
@ -20,46 +13,32 @@ class Oauth:
|
|||
email_account: str,
|
||||
email: str,
|
||||
access_token: str,
|
||||
refresh_token: str,
|
||||
service: str,
|
||||
mechanism: str = "XOAUTH2",
|
||||
) -> None:
|
||||
|
||||
self.email_account = email_account
|
||||
self.email = email
|
||||
self.service = service
|
||||
self._mechanism = mechanism
|
||||
self._conn = conn
|
||||
self._access_token = access_token
|
||||
self._refresh_token = refresh_token
|
||||
|
||||
self._validate()
|
||||
|
||||
def _validate(self) -> None:
|
||||
if self.service != "GMail":
|
||||
raise NotImplementedError(
|
||||
f"Service {self.service} currently doesn't have oauth implementation."
|
||||
)
|
||||
|
||||
if not self._refresh_token:
|
||||
if not self._access_token:
|
||||
frappe.throw(
|
||||
frappe._("Please Authorize OAuth."),
|
||||
OAuthenticationError,
|
||||
frappe._("OAuth Error"),
|
||||
frappe._("Please Authorize OAuth for Email Account {}").format(self.email_account),
|
||||
title=frappe._("OAuth Error"),
|
||||
)
|
||||
|
||||
@property
|
||||
def _auth_string(self) -> str:
|
||||
return f"user={self.email}\1auth=Bearer {self._access_token}\1\1"
|
||||
|
||||
def connect(self, _retry: int = 0) -> None:
|
||||
"""Connection method with retry on exception for Oauth"""
|
||||
def connect(self) -> None:
|
||||
try:
|
||||
if isinstance(self._conn, POP3):
|
||||
res = self._connect_pop()
|
||||
|
||||
if not res.startswith(b"+OK"):
|
||||
raise
|
||||
self._connect_pop()
|
||||
|
||||
elif isinstance(self._conn, IMAP4):
|
||||
self._connect_imap()
|
||||
|
|
@ -68,100 +47,29 @@ class Oauth:
|
|||
# SMTP
|
||||
self._connect_smtp()
|
||||
|
||||
except Exception as e:
|
||||
# maybe the access token expired - refreshing
|
||||
access_token = self._refresh_access_token()
|
||||
except Exception:
|
||||
frappe.log_error(
|
||||
"Email Connection Error - Authentication Failed",
|
||||
reference_doctype="Email Account",
|
||||
reference_name=self.email_account,
|
||||
)
|
||||
# raising a bare exception here as we have a lot of exception handling present
|
||||
# where the connect method is called from - hence just logging and raising.
|
||||
raise
|
||||
|
||||
if not access_token or _retry > 0:
|
||||
frappe.log_error(
|
||||
"OAuth Error - Authentication Failed", str(e), "Email Account", self.email_account
|
||||
)
|
||||
# raising a bare exception here as we have a lot of exception handling present
|
||||
# where the connect method is called from - hence just logging and raising.
|
||||
raise
|
||||
|
||||
self._access_token = access_token
|
||||
self.connect(_retry + 1)
|
||||
|
||||
def _connect_pop(self) -> bytes:
|
||||
# poplib doesn't have AUTH command implementation
|
||||
def _connect_pop(self) -> None:
|
||||
# NOTE: poplib doesn't have AUTH command implementation
|
||||
res = self._conn._shortcmd(
|
||||
"AUTH {} {}".format(
|
||||
self._mechanism, base64.b64encode(bytes(self._auth_string, "utf-8")).decode("utf-8")
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
if not res.startswith(b"+OK"):
|
||||
raise
|
||||
|
||||
def _connect_imap(self) -> None:
|
||||
self._conn.authenticate(self._mechanism, lambda x: self._auth_string)
|
||||
|
||||
def _connect_smtp(self) -> None:
|
||||
self._conn.auth(self._mechanism, lambda x: self._auth_string, initial_response_ok=False)
|
||||
|
||||
def _refresh_access_token(self) -> str:
|
||||
"""Refreshes access token via calling `refresh_access_token` method of oauth service object"""
|
||||
service_obj = self._get_service_object()
|
||||
access_token = service_obj.refresh_access_token(self._refresh_token).get("access_token")
|
||||
|
||||
if access_token:
|
||||
# set the new access token in db
|
||||
frappe.db.set_value(
|
||||
"Email Account",
|
||||
self.email_account,
|
||||
"access_token",
|
||||
encrypt(access_token),
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
return access_token
|
||||
|
||||
def _get_service_object(self):
|
||||
"""Get Oauth service object"""
|
||||
|
||||
return {
|
||||
"GMail": GoogleOAuth("mail", validate=False),
|
||||
}[self.service]
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def oauth_access(email_account: str, service: str):
|
||||
"""Used as a default endpoint/caller for all oauth services.
|
||||
Returns authorization url for redirection"""
|
||||
|
||||
if not service:
|
||||
frappe.throw(frappe._("No Service is selected. Please select one and try again!"))
|
||||
|
||||
if service == "GMail":
|
||||
return authorize_google_access(email_account)
|
||||
|
||||
raise NotImplementedError(f"Service {service} currently doesn't have oauth implementation.")
|
||||
|
||||
|
||||
def authorize_google_access(email_account: str, code: str = None):
|
||||
"""Facilitates google oauth for email.
|
||||
This is invoked 2 times - first time when user clicks `Authorize API Access` for getting the authorization url
|
||||
and second time for setting the refresh and access token in db when google redirects back with oauth code."""
|
||||
|
||||
doctype = "Email Account"
|
||||
oauth_obj = GoogleOAuth("mail")
|
||||
|
||||
if not code:
|
||||
return oauth_obj.get_authentication_url(
|
||||
{
|
||||
"redirect": f"/app/Form/{quote(doctype)}/{quote(email_account)}",
|
||||
"success_query_param": "successful_authorization=1",
|
||||
"email_account": email_account,
|
||||
},
|
||||
)
|
||||
|
||||
res = oauth_obj.authorize(code)
|
||||
frappe.db.set_value(
|
||||
doctype,
|
||||
email_account,
|
||||
{
|
||||
"refresh_token": encrypt(res.get("refresh_token")),
|
||||
"access_token": encrypt(res.get("access_token")),
|
||||
},
|
||||
update_modified=False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -109,8 +109,6 @@ class EmailServer:
|
|||
self.settings.email_account,
|
||||
self.settings.username,
|
||||
self.settings.access_token,
|
||||
self.settings.refresh_token,
|
||||
self.settings.service,
|
||||
).connect()
|
||||
|
||||
else:
|
||||
|
|
@ -142,8 +140,6 @@ class EmailServer:
|
|||
self.settings.email_account,
|
||||
self.settings.username,
|
||||
self.settings.access_token,
|
||||
self.settings.refresh_token,
|
||||
self.settings.service,
|
||||
).connect()
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -54,9 +54,7 @@ class SMTPServer:
|
|||
use_tls=None,
|
||||
use_ssl=None,
|
||||
use_oauth=0,
|
||||
refresh_token=None,
|
||||
access_token=None,
|
||||
service=None,
|
||||
):
|
||||
self.login = login
|
||||
self.email_account = email_account
|
||||
|
|
@ -66,9 +64,7 @@ class SMTPServer:
|
|||
self.use_tls = use_tls
|
||||
self.use_ssl = use_ssl
|
||||
self.use_oauth = use_oauth
|
||||
self.refresh_token = refresh_token
|
||||
self.access_token = access_token
|
||||
self.service = service
|
||||
self._session = None
|
||||
|
||||
if not self.server:
|
||||
|
|
@ -112,9 +108,7 @@ class SMTPServer:
|
|||
self.secure_session(_session)
|
||||
|
||||
if self.use_oauth:
|
||||
Oauth(
|
||||
_session, self.email_account, self.login, self.access_token, self.refresh_token, self.service
|
||||
).connect()
|
||||
Oauth(_session, self.email_account, self.login, self.access_token).connect()
|
||||
|
||||
elif self.password:
|
||||
res = _session.login(str(self.login or ""), str(self.password or ""))
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ if any((os.getenv("CI"), frappe.conf.developer_mode, frappe.conf.allow_tests)):
|
|||
# Disable mandatory TLS in developer mode and tests
|
||||
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
||||
|
||||
os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1"
|
||||
|
||||
|
||||
class ConnectedApp(Document):
|
||||
"""Connect to a remote oAuth Server. Retrieve and store user's access token
|
||||
|
|
@ -57,7 +59,7 @@ class ConnectedApp(Document):
|
|||
def initiate_web_application_flow(self, user=None, success_uri=None):
|
||||
"""Return an authorization URL for the user. Save state in Token Cache."""
|
||||
user = user or frappe.session.user
|
||||
oauth = self.get_oauth2_session(init=True)
|
||||
oauth = self.get_oauth2_session(user, init=True)
|
||||
query_params = self.get_query_params()
|
||||
authorization_url, state = oauth.authorization_url(self.authorization_uri, **query_params)
|
||||
token_cache = self.get_token_cache(user)
|
||||
|
|
@ -102,8 +104,27 @@ class ConnectedApp(Document):
|
|||
def get_query_params(self):
|
||||
return {param.key: param.value for param in self.query_parameters}
|
||||
|
||||
def get_active_token(self, user=None):
|
||||
user = user or frappe.session.user
|
||||
token_cache = self.get_token_cache(user)
|
||||
if token_cache and token_cache.is_expired():
|
||||
oauth_session = self.get_oauth2_session(user)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
try:
|
||||
token = oauth_session.refresh_token(
|
||||
body=f"redirect_uri={self.redirect_uri}",
|
||||
token_url=self.token_uri,
|
||||
)
|
||||
except Exception:
|
||||
self.log_error("Token Refresh Error")
|
||||
return None
|
||||
|
||||
token_cache.update_data(token)
|
||||
|
||||
return token_cache
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["GET"], allow_guest=True)
|
||||
def callback(code=None, state=None):
|
||||
"""Handle client's code.
|
||||
|
||||
|
|
@ -111,8 +132,6 @@ def callback(code=None, state=None):
|
|||
transmit a code that can be used by the local server to obtain an access
|
||||
token.
|
||||
"""
|
||||
if frappe.request.method != "GET":
|
||||
frappe.throw(_("Invalid request method: {}").format(frappe.request.method))
|
||||
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.local.response["type"] = "redirect"
|
||||
|
|
@ -136,9 +155,16 @@ def callback(code=None, state=None):
|
|||
code=code,
|
||||
client_secret=connected_app.get_password("client_secret"),
|
||||
include_client_id=True,
|
||||
**query_params
|
||||
**query_params,
|
||||
)
|
||||
token_cache.update_data(token)
|
||||
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = token_cache.get("success_uri") or connected_app.get_url()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def has_token(connected_app, connected_user=None):
|
||||
app = frappe.get_doc("Connected App", connected_app)
|
||||
token_cache = app.get_token_cache(connected_user or frappe.session.user)
|
||||
return bool(token_cache and token_cache.get_password("access_token", False))
|
||||
|
|
|
|||
|
|
@ -86,10 +86,11 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-11-13 13:35:53.714352",
|
||||
"modified": "2023-01-01 21:01:24.405729",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Token Cache",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
@ -106,5 +107,5 @@
|
|||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
|
@ -50,16 +52,18 @@ class TokenCache(Document):
|
|||
return self
|
||||
|
||||
def get_expires_in(self):
|
||||
expiry_time = frappe.utils.get_datetime(self.modified) + timedelta(self.expires_in)
|
||||
return (datetime.now() - expiry_time).total_seconds()
|
||||
modified = frappe.utils.get_datetime(self.modified)
|
||||
expiry_utc = modified.astimezone(pytz.utc) + timedelta(seconds=self.expires_in)
|
||||
now_utc = datetime.utcnow().replace(tzinfo=pytz.utc)
|
||||
return cint((expiry_utc - now_utc).total_seconds())
|
||||
|
||||
def is_expired(self):
|
||||
return self.get_expires_in() < 0
|
||||
|
||||
def get_json(self):
|
||||
return {
|
||||
"access_token": self.get_password("access_token", ""),
|
||||
"refresh_token": self.get_password("refresh_token", ""),
|
||||
"access_token": self.get_password("access_token", False),
|
||||
"refresh_token": self.get_password("refresh_token", False),
|
||||
"expires_in": self.get_expires_in(),
|
||||
"token_type": self.token_type,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ def enqueue_webhook(doc, webhook) -> None:
|
|||
webhook: Webhook = frappe.get_doc("Webhook", webhook.get("name"))
|
||||
headers = get_webhook_headers(doc, webhook)
|
||||
data = get_webhook_data(doc, webhook)
|
||||
r = None
|
||||
|
||||
for i in range(3):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -220,3 +220,4 @@ frappe.patches.v14_0.add_manage_subscriptions_in_navbar_settings
|
|||
frappe.patches.v14_0.update_attachment_comment
|
||||
frappe.patches.v15_0.set_contact_full_name
|
||||
execute:frappe.delete_doc("Page", "activity", force=1)
|
||||
frappe.patches.v14_0.disable_email_accounts_with_oauth
|
||||
|
|
|
|||
36
frappe/patches/v14_0/disable_email_accounts_with_oauth.py
Normal file
36
frappe/patches/v14_0/disable_email_accounts_with_oauth.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import frappe
|
||||
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
|
||||
|
||||
|
||||
def execute():
|
||||
if not frappe.get_value("Email Account", {"auth_method": "OAuth"}):
|
||||
return
|
||||
|
||||
# Setting awaiting password to 1 for email accounts where Oauth is enabled.
|
||||
# This is done so that people can resetup their email accounts with connected app mechanism.
|
||||
frappe.db.set_value("Email Account", {"auth_method": "OAuth"}, "awaiting_password", 1)
|
||||
|
||||
message = "Email Accounts with auth method as OAuth have been disabled.\
|
||||
Please re-setup your OAuth based email accounts with the connected app mechanism to re-enable them."
|
||||
|
||||
if sysmanagers := get_system_managers():
|
||||
make_notification_logs(
|
||||
{
|
||||
"type": "Alert",
|
||||
"subject": frappe._(message),
|
||||
},
|
||||
sysmanagers,
|
||||
)
|
||||
|
||||
|
||||
def get_system_managers():
|
||||
user_doctype = frappe.qb.DocType("User").as_("user")
|
||||
user_role_doctype = frappe.qb.DocType("Has Role").as_("user_role")
|
||||
return (
|
||||
frappe.qb.from_(user_doctype)
|
||||
.from_(user_role_doctype)
|
||||
.select(user_doctype.email)
|
||||
.where(user_role_doctype.role == "System Manager")
|
||||
.where(user_doctype.enabled == 1)
|
||||
.where(user_role_doctype.parent == user_doctype.name)
|
||||
).run(pluck=True)
|
||||
|
|
@ -75,7 +75,7 @@ class WidgetDialog {
|
|||
|
||||
this.filters = [];
|
||||
|
||||
this.generate_filter_from_json();
|
||||
this.generate_filter_from_json && this.generate_filter_from_json();
|
||||
|
||||
this.filter_group = new frappe.ui.FilterGroup({
|
||||
parent: this.dialog.get_field("filter_area").$wrapper,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from frappe.query_builder.terms import ParameterizedFunction, ParameterizedValue
|
|||
from frappe.query_builder.utils import (
|
||||
Column,
|
||||
DocType,
|
||||
get_qb_engine,
|
||||
get_query,
|
||||
get_query_builder,
|
||||
patch_query_aggregation,
|
||||
patch_query_execute,
|
||||
|
|
|
|||
|
|
@ -119,9 +119,9 @@ class Cast_(Function):
|
|||
|
||||
def _aggregate(function, dt, fieldname, filters, **kwargs):
|
||||
return (
|
||||
frappe.qb.engine.build_conditions(dt, filters)
|
||||
.select(function(PseudoColumn(fieldname)))
|
||||
.run(**kwargs)[0][0]
|
||||
frappe.qb.get_query(dt, filters=filters, fields=[function(PseudoColumn(fieldname))]).run(
|
||||
**kwargs
|
||||
)[0][0]
|
||||
or 0
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ from enum import Enum
|
|||
from importlib import import_module
|
||||
from typing import Any, Callable, get_type_hints
|
||||
|
||||
from pypika import Query
|
||||
from pypika.queries import Column
|
||||
from pypika.queries import Column, QueryBuilder
|
||||
from pypika.terms import PseudoColumn
|
||||
|
||||
import frappe
|
||||
|
|
@ -55,10 +54,10 @@ def get_query_builder(type_of_db: str) -> Postgres | MariaDB:
|
|||
return picks[db]
|
||||
|
||||
|
||||
def get_qb_engine():
|
||||
def get_query(*args, **kwargs) -> QueryBuilder:
|
||||
from frappe.database.query import Engine
|
||||
|
||||
return Engine()
|
||||
return Engine().get_query(*args, **kwargs)
|
||||
|
||||
|
||||
def get_attr(method_string):
|
||||
|
|
|
|||
|
|
@ -143,6 +143,9 @@ class BaseTestCommands(FrappeTestCase):
|
|||
|
||||
@classmethod
|
||||
def execute(self, command, kwargs=None):
|
||||
# tests might have written to DB which wont be visible to commands until we end current transaction
|
||||
frappe.db.commit()
|
||||
|
||||
site = {"site": frappe.local.site}
|
||||
cmd_input = None
|
||||
if kwargs:
|
||||
|
|
@ -165,6 +168,9 @@ class BaseTestCommands(FrappeTestCase):
|
|||
self.stderr = clean(self._proc.stderr)
|
||||
self.returncode = clean(self._proc.returncode)
|
||||
|
||||
# Commands might have written to DB which wont be visible until we end current transaction
|
||||
frappe.db.rollback()
|
||||
|
||||
@classmethod
|
||||
def setup_test_site(cls):
|
||||
cmd_config = {
|
||||
|
|
@ -300,6 +306,7 @@ class TestCommands(BaseTestCommands):
|
|||
frappe.local.cache = {}
|
||||
self.assertEqual(frappe.recorder.status(), False)
|
||||
|
||||
@unittest.skip("Poorly written, relied on app name being absent in apps.txt")
|
||||
def test_remove_from_installed_apps(self):
|
||||
app = "test_remove_app"
|
||||
add_to_installed_apps(app)
|
||||
|
|
@ -407,20 +414,16 @@ class TestCommands(BaseTestCommands):
|
|||
self.execute("bench --site {site} set-password Administrator test1")
|
||||
self.assertEqual(self.returncode, 0)
|
||||
self.assertEqual(check_password("Administrator", "test1"), "Administrator")
|
||||
# to release the lock taken by check_password
|
||||
frappe.db.commit()
|
||||
|
||||
self.execute("bench --site {site} set-admin-password test2")
|
||||
self.assertEqual(self.returncode, 0)
|
||||
self.assertEqual(check_password("Administrator", "test2"), "Administrator")
|
||||
frappe.db.commit()
|
||||
|
||||
# Reset it back to original password
|
||||
original_password = frappe.conf.admin_password or "admin"
|
||||
self.execute("bench --site {site} set-admin-password %s" % original_password)
|
||||
self.assertEqual(self.returncode, 0)
|
||||
self.assertEqual(check_password("Administrator", original_password), "Administrator")
|
||||
frappe.db.commit()
|
||||
|
||||
@skipIf(
|
||||
not (
|
||||
|
|
|
|||
|
|
@ -545,7 +545,7 @@ class TestDB(FrappeTestCase):
|
|||
self.assertEqual((frappe.db.count("Note")), 2)
|
||||
|
||||
# simple filters
|
||||
self.assertEqual((frappe.db.count("Note", ["title", "=", "note1"])), 1)
|
||||
self.assertEqual((frappe.db.count("Note", [["title", "=", "note1"]])), 1)
|
||||
|
||||
frappe.get_doc(doctype="Note", title="note3", content="something other").insert()
|
||||
|
||||
|
|
|
|||
|
|
@ -258,9 +258,7 @@ class TestReportview(FrappeTestCase):
|
|||
)
|
||||
|
||||
def test_none_filter(self):
|
||||
query = frappe.qb.engine.get_query(
|
||||
"DocType", fields="name", filters={"restrict_to_domain": None}
|
||||
)
|
||||
query = frappe.qb.get_query("DocType", fields="name", filters={"restrict_to_domain": None})
|
||||
sql = str(query).replace("`", "").replace('"', "")
|
||||
condition = "restrict_to_domain IS NULL"
|
||||
self.assertIn(condition, sql)
|
||||
|
|
|
|||
|
|
@ -56,30 +56,28 @@ class TestQuery(FrappeTestCase):
|
|||
@run_only_if(db_type_is.MARIADB)
|
||||
def test_multiple_tables_in_filters(self):
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
["*"],
|
||||
[
|
||||
["BOM Update Log", "name", "like", "f%"],
|
||||
["DocField", "name", "like", "f%"],
|
||||
["DocType", "parent", "=", "something"],
|
||||
],
|
||||
).get_sql(),
|
||||
"SELECT * FROM `tabDocType` LEFT JOIN `tabBOM Update Log` ON `tabBOM Update Log`.`parent`=`tabDocType`.`name` AND `tabBOM Update Log`.`parenttype`='DocType' WHERE `tabBOM Update Log`.`name` LIKE 'f%' AND `tabDocType`.`parent`='something'",
|
||||
"SELECT `tabDocType`.* FROM `tabDocType` LEFT JOIN `tabDocField` ON `tabDocField`.`parent`=`tabDocType`.`name` AND `tabDocField`.`parenttype`='DocType' WHERE `tabDocField`.`name` LIKE 'f%' AND `tabDocType`.`parent`='something'",
|
||||
)
|
||||
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
def test_string_fields(self):
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
"User", fields="name, email", filters={"name": "Administrator"}
|
||||
).get_sql(),
|
||||
frappe.qb.get_query("User", fields="name, email", filters={"name": "Administrator"}).get_sql(),
|
||||
frappe.qb.from_("User")
|
||||
.select(Field("name"), Field("email"))
|
||||
.where(Field("name") == "Administrator")
|
||||
.get_sql(),
|
||||
)
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
"User", fields=["`name`, `email`"], filters={"name": "Administrator"}
|
||||
).get_sql(),
|
||||
frappe.qb.from_("User")
|
||||
|
|
@ -89,7 +87,7 @@ class TestQuery(FrappeTestCase):
|
|||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
"User", fields=["`tabUser`.`name`", "`tabUser`.`email`"], filters={"name": "Administrator"}
|
||||
).run(),
|
||||
frappe.qb.from_("User")
|
||||
|
|
@ -99,7 +97,7 @@ class TestQuery(FrappeTestCase):
|
|||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
"User",
|
||||
fields=["`tabUser`.`name` as owner", "`tabUser`.`email`"],
|
||||
filters={"name": "Administrator"},
|
||||
|
|
@ -111,7 +109,7 @@ class TestQuery(FrappeTestCase):
|
|||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
"User", fields=["`tabUser`.`name`, Count(`name`) as count"], filters={"name": "Administrator"}
|
||||
).run(),
|
||||
frappe.qb.from_("User")
|
||||
|
|
@ -121,7 +119,7 @@ class TestQuery(FrappeTestCase):
|
|||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
"User",
|
||||
fields=["`tabUser`.`name`, Count(`name`) as `count`"],
|
||||
filters={"name": "Administrator"},
|
||||
|
|
@ -133,7 +131,7 @@ class TestQuery(FrappeTestCase):
|
|||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
"User", fields="`tabUser`.`name`, Count(`name`) as `count`", filters={"name": "Administrator"}
|
||||
).run(),
|
||||
frappe.qb.from_("User")
|
||||
|
|
@ -144,38 +142,34 @@ class TestQuery(FrappeTestCase):
|
|||
|
||||
def test_functions_fields(self):
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query("User", fields="Count(name)", filters={}).get_sql(),
|
||||
frappe.qb.get_query("User", fields="Count(name)", filters={}).get_sql(),
|
||||
frappe.qb.from_("User").select(Count(Field("name"))).get_sql(),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query("User", fields=["Count(name)", "Max(name)"], filters={}).get_sql(),
|
||||
frappe.qb.get_query("User", fields=["Count(name)", "Max(name)"], filters={}).get_sql(),
|
||||
frappe.qb.from_("User").select(Count(Field("name")), Max(Field("name"))).get_sql(),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
"User", fields=["abs(name-email)", "Count(name)"], filters={}
|
||||
).get_sql(),
|
||||
frappe.qb.get_query("User", fields=["abs(name-email)", "Count(name)"], filters={}).get_sql(),
|
||||
frappe.qb.from_("User")
|
||||
.select(Abs(Field("name") - Field("email")), Count(Field("name")))
|
||||
.get_sql(),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query("User", fields=[Count("*")], filters={}).get_sql(),
|
||||
frappe.qb.get_query("User", fields=[Count("*")], filters={}).get_sql(),
|
||||
frappe.qb.from_("User").select(Count("*")).get_sql(),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
"User", fields="timestamp(creation, modified)", filters={}
|
||||
).get_sql(),
|
||||
frappe.qb.get_query("User", fields="timestamp(creation, modified)", filters={}).get_sql(),
|
||||
frappe.qb.from_("User").select(Timestamp(Field("creation"), Field("modified"))).get_sql(),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
"User", fields="Count(name) as count, Max(email) as max_email", filters={}
|
||||
).get_sql(),
|
||||
frappe.qb.from_("User")
|
||||
|
|
@ -186,85 +180,175 @@ class TestQuery(FrappeTestCase):
|
|||
def test_qb_fields(self):
|
||||
user_doctype = frappe.qb.DocType("User")
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
user_doctype, fields=[user_doctype.name, user_doctype.email], filters={}
|
||||
).get_sql(),
|
||||
frappe.qb.from_(user_doctype).select(user_doctype.name, user_doctype.email).get_sql(),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(user_doctype, fields=user_doctype.email, filters={}).get_sql(),
|
||||
frappe.qb.get_query(user_doctype, fields=user_doctype.email, filters={}).get_sql(),
|
||||
frappe.qb.from_(user_doctype).select(user_doctype.email).get_sql(),
|
||||
)
|
||||
|
||||
def test_aliasing(self):
|
||||
user_doctype = frappe.qb.DocType("User")
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
user_doctype, fields=["name as owner", "email as id"], filters={}
|
||||
).get_sql(),
|
||||
frappe.qb.get_query("User", fields=["name as owner", "email as id"], filters={}).get_sql(),
|
||||
frappe.qb.from_(user_doctype)
|
||||
.select(user_doctype.name.as_("owner"), user_doctype.email.as_("id"))
|
||||
.get_sql(),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
user_doctype, fields="name as owner, email as id", filters={}
|
||||
).get_sql(),
|
||||
frappe.qb.get_query(user_doctype, fields="name as owner, email as id", filters={}).get_sql(),
|
||||
frappe.qb.from_(user_doctype)
|
||||
.select(user_doctype.name.as_("owner"), user_doctype.email.as_("id"))
|
||||
.get_sql(),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
user_doctype, fields=["Count(name) as count", "email as id"], filters={}
|
||||
).get_sql(),
|
||||
frappe.qb.from_(user_doctype)
|
||||
.select(user_doctype.email.as_("id"), Count(Field("name")).as_("count"))
|
||||
.select(Count(Field("name")).as_("count"), user_doctype.email.as_("id"))
|
||||
.get_sql(),
|
||||
)
|
||||
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
def test_filters(self):
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
"User", filters={"IfNull(name, " ")": ("<", Now())}, fields=["Max(name)"]
|
||||
).run(),
|
||||
frappe.qb.from_("User").select(Max(Field("name"))).where(Ifnull("name", "") < Now()).run(),
|
||||
)
|
||||
|
||||
def test_implicit_join_query(self):
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
filters={"module.app_name": "frappe"},
|
||||
).get_sql(),
|
||||
"SELECT `tabDocType`.`name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module` WHERE `tabModule Def`.`app_name`='frappe'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
filters={"module.app_name": ("like", "frap%")},
|
||||
).get_sql(),
|
||||
"SELECT `tabDocType`.`name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module` WHERE `tabModule Def`.`app_name` LIKE 'frap%'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
filters={"permissions.role": "System Manager"},
|
||||
).get_sql(),
|
||||
"SELECT `tabDocType`.`name` FROM `tabDocType` LEFT JOIN `tabDocPerm` ON `tabDocPerm`.`parent`=`tabDocType`.`name` AND `tabDocPerm`.`parenttype`='DocType' WHERE `tabDocPerm`.`role`='System Manager'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["module"],
|
||||
filters="",
|
||||
).get_sql(),
|
||||
"SELECT `module` FROM `tabDocType` WHERE `name`=''".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
filters=["ToDo", "Note"],
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name` IN ('ToDo','Note')".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
filters={"name": ("in", [])},
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name` IN ('')".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
filters=[1, 2, 3],
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name` IN (1,2,3)".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
filters=[],
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType`".replace("`", '"' if frappe.db.db_type == "postgres" else "`"),
|
||||
)
|
||||
|
||||
def test_implicit_join_query(self):
|
||||
self.maxDiff = None
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.get_query(
|
||||
"Note",
|
||||
filters={"name": "Test Note Title"},
|
||||
fields=["name", "`tabNote Seen By`.`user` as seen_by"],
|
||||
).get_sql(),
|
||||
"SELECT `tabNote`.`name`,`tabNote Seen By`.`user` seen_by FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'".replace(
|
||||
"SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
"Note",
|
||||
filters={"name": "Test Note Title"},
|
||||
fields=["name", "`tabNote Seen By`.`user` as seen_by", "`tabNote Seen By`.`idx` as idx"],
|
||||
).get_sql(),
|
||||
"SELECT `tabNote`.`name`,`tabNote Seen By`.`user` seen_by,`tabNote Seen By`.`idx` idx FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'".replace(
|
||||
"SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by`,`tabNote Seen By`.`idx` `idx` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
frappe.qb.get_query(
|
||||
"Note",
|
||||
filters={"name": "Test Note Title"},
|
||||
fields=["name", "seen_by.user as seen_by", "`tabNote Seen By`.`idx` as idx"],
|
||||
).get_sql(),
|
||||
"SELECT `tabNote`.`name`,`tabNote Seen By`.`user` seen_by,`tabNote Seen By`.`idx` idx FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'".replace(
|
||||
"SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by`,`tabNote Seen By`.`idx` `idx` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name", "module.app_name as app_name"],
|
||||
).get_sql(),
|
||||
"SELECT `tabDocType`.`name`,`tabModule Def`.`app_name` `app_name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module`".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
)
|
||||
|
|
@ -272,40 +356,40 @@ class TestQuery(FrappeTestCase):
|
|||
@run_only_if(db_type_is.MARIADB)
|
||||
def test_comment_stripping(self):
|
||||
self.assertNotIn(
|
||||
"email", frappe.qb.engine.get_query("User", fields=["name", "#email"], filters={}).get_sql()
|
||||
"email", frappe.qb.get_query("User", fields=["name", "#email"], filters={}).get_sql()
|
||||
)
|
||||
|
||||
def test_nestedset(self):
|
||||
frappe.db.sql("delete from `tabDocType` where `name` = 'Test Tree DocType'")
|
||||
frappe.db.sql_ddl("drop table if exists `tabTest Tree DocType`")
|
||||
create_tree_docs()
|
||||
descendants_result = frappe.qb.engine.get_query(
|
||||
descendants_result = frappe.qb.get_query(
|
||||
"Test Tree DocType",
|
||||
fields=["name"],
|
||||
filters={"name": ("descendants of", "Parent 1")},
|
||||
orderby="modified",
|
||||
order_by="modified desc",
|
||||
).run(as_list=1)
|
||||
|
||||
# Format decendants result
|
||||
descendants_result = list(itertools.chain.from_iterable(descendants_result))
|
||||
self.assertListEqual(descendants_result, get_descendants_of("Test Tree DocType", "Parent 1"))
|
||||
|
||||
ancestors_result = frappe.qb.engine.get_query(
|
||||
ancestors_result = frappe.qb.get_query(
|
||||
"Test Tree DocType",
|
||||
fields=["name"],
|
||||
filters={"name": ("ancestors of", "Child 2")},
|
||||
orderby="modified",
|
||||
order_by="modified desc",
|
||||
).run(as_list=1)
|
||||
|
||||
# Format ancestors result
|
||||
ancestors_result = list(itertools.chain.from_iterable(ancestors_result))
|
||||
self.assertListEqual(ancestors_result, get_ancestors_of("Test Tree DocType", "Child 2"))
|
||||
|
||||
not_descendants_result = frappe.qb.engine.get_query(
|
||||
not_descendants_result = frappe.qb.get_query(
|
||||
"Test Tree DocType",
|
||||
fields=["name"],
|
||||
filters={"name": ("not descendants of", "Parent 1")},
|
||||
orderby="modified",
|
||||
order_by="modified desc",
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertListEqual(
|
||||
|
|
@ -317,11 +401,11 @@ class TestQuery(FrappeTestCase):
|
|||
),
|
||||
)
|
||||
|
||||
not_ancestors_result = frappe.qb.engine.get_query(
|
||||
not_ancestors_result = frappe.qb.get_query(
|
||||
"Test Tree DocType",
|
||||
fields=["name"],
|
||||
filters={"name": ("not ancestors of", "Child 2")},
|
||||
orderby="modified",
|
||||
order_by="modified desc",
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertListEqual(
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ from frappe.utils.data import (
|
|||
cast,
|
||||
cstr,
|
||||
duration_to_seconds,
|
||||
expand_relative_urls,
|
||||
get_datetime,
|
||||
get_first_day_of_week,
|
||||
get_time,
|
||||
|
|
@ -920,6 +921,21 @@ class TestMiscUtils(FrappeTestCase):
|
|||
self.assertEqual(safe_json_loads("{ /}"), "{ /}")
|
||||
self.assertEqual(safe_json_loads("12"), 12) # this is a quirk
|
||||
|
||||
def test_url_expansion(self):
|
||||
unchanged_links = [
|
||||
"<a href='tel:12345432'>My Phone</a>)",
|
||||
"<a href='mailto:hello@example.com'>My Email</a>)",
|
||||
"<a href='data:hello@example.com'>Data</a>)",
|
||||
]
|
||||
for link in unchanged_links:
|
||||
self.assertEqual(link, expand_relative_urls(link))
|
||||
|
||||
site = get_url()
|
||||
|
||||
transforms = [("<a href='/about'>About</a>)", f"<a href='{site}/about'>About</a>)")]
|
||||
for input, output in transforms:
|
||||
self.assertEqual(output, expand_relative_urls(input))
|
||||
|
||||
|
||||
class TestTypingValidations(FrappeTestCase):
|
||||
ERR_REGEX = f"^Argument '.*' should be of type '.*' but got '.*' instead.$"
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ def get_versions():
|
|||
}
|
||||
}"""
|
||||
versions = {}
|
||||
for app in frappe.get_installed_apps(sort=True):
|
||||
for app in frappe.get_installed_apps(_ensure_on_bench=True):
|
||||
app_hooks = frappe.get_hooks(app_name=app)
|
||||
versions[app] = {
|
||||
"title": app_hooks.get("app_title")[0],
|
||||
|
|
|
|||
|
|
@ -1885,7 +1885,7 @@ def expand_relative_urls(html: str) -> str:
|
|||
def _expand_relative_urls(match):
|
||||
to_expand = list(match.groups())
|
||||
|
||||
if not to_expand[2].startswith("mailto") and not to_expand[2].startswith("data:"):
|
||||
if not to_expand[2].startswith(("mailto", "data:", "tel:")):
|
||||
if not to_expand[2].startswith("/"):
|
||||
to_expand[2] = "/" + to_expand[2]
|
||||
to_expand.insert(2, url)
|
||||
|
|
|
|||
|
|
@ -24,10 +24,13 @@ def get_monthly_results(
|
|||
date_format = "%m-%Y" if frappe.db.db_type != "postgres" else "MM-YYYY"
|
||||
|
||||
return dict(
|
||||
frappe.qb.engine.build_conditions(table=goal_doctype, filters=filters)
|
||||
.select(
|
||||
DateFormat(Table[date_col], date_format).as_("month_year"),
|
||||
Function(aggregation, goal_field),
|
||||
frappe.qb.get_query(
|
||||
table=goal_doctype,
|
||||
fields=[
|
||||
DateFormat(Table[date_col], date_format).as_("month_year"),
|
||||
Function(aggregation, goal_field),
|
||||
],
|
||||
filters=filters,
|
||||
)
|
||||
.groupby("month_year")
|
||||
.run()
|
||||
|
|
|
|||
|
|
@ -112,8 +112,9 @@ def get_jloader():
|
|||
|
||||
apps = frappe.get_hooks("template_apps")
|
||||
if not apps:
|
||||
apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps(sort=True)
|
||||
apps.reverse()
|
||||
apps = list(
|
||||
reversed(frappe.local.flags.web_pages_apps or frappe.get_installed_apps(_ensure_on_bench=True))
|
||||
)
|
||||
|
||||
if "frappe" not in apps:
|
||||
apps.append("frappe")
|
||||
|
|
|
|||
|
|
@ -120,14 +120,14 @@ def read_multi_pdf(output):
|
|||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def download_pdf(
|
||||
doctype, name, format=None, doc=None, no_letterhead=0, language=None, letter_head=None
|
||||
doctype, name, format=None, doc=None, no_letterhead=0, language=None, letterhead=None
|
||||
):
|
||||
doc = doc or frappe.get_doc(doctype, name)
|
||||
validate_print_permission(doc)
|
||||
|
||||
with print_language(language):
|
||||
pdf_file = frappe.get_print(
|
||||
doctype, name, format, doc=doc, as_pdf=True, letterhead=letter_head, no_letterhead=no_letterhead
|
||||
doctype, name, format, doc=doc, as_pdf=True, letterhead=letterhead, no_letterhead=no_letterhead
|
||||
)
|
||||
|
||||
frappe.local.response.filename = "{name}.pdf".format(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<span class="my-account-item-link">
|
||||
<a href="/update-profile?name={{ user }}">
|
||||
<a href="/update-profile/{{ user }}">
|
||||
<svg class="edit-profile-icon icon icon-md">
|
||||
<use href="#icon-edit">
|
||||
</use>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue