@@ -13,5 +13,6 @@ | |||
"root_login": "root", | |||
"root_password": "travis", | |||
"host_name": "http://test_site:8000", | |||
"monitor": 1, | |||
"server_script_enabled": true | |||
} |
@@ -239,7 +239,6 @@ def init(site: str, sites_path: str = ".", new_site: bool = False) -> None: | |||
local.jloader = None | |||
local.cache = {} | |||
local.document_cache = {} | |||
local.meta_cache = {} | |||
local.form_dict = _dict() | |||
local.preload_assets = {"style": [], "script": []} | |||
local.session = _dict() | |||
@@ -1065,21 +1064,9 @@ def set_value(doctype, docname, fieldname, value=None): | |||
return frappe.client.set_value(doctype, docname, fieldname, value) | |||
@overload | |||
def get_cached_doc(doctype, docname, _allow_dict=True) -> dict: | |||
... | |||
@overload | |||
def get_cached_doc(*args, **kwargs) -> "Document": | |||
... | |||
def get_cached_doc(*args, **kwargs): | |||
allow_dict = kwargs.pop("_allow_dict", False) | |||
def _respond(doc, from_redis=False): | |||
if not allow_dict and isinstance(doc, dict): | |||
if isinstance(doc, dict): | |||
local.document_cache[key] = doc = get_doc(doc) | |||
elif from_redis: | |||
@@ -1103,6 +1090,12 @@ def get_cached_doc(*args, **kwargs): | |||
if not key: | |||
key = get_document_cache_key(doc.doctype, doc.name) | |||
_set_document_in_cache(key, doc) | |||
return doc | |||
def _set_document_in_cache(key: str, doc: "Document") -> None: | |||
local.document_cache[key] = doc | |||
# Avoid setting in local.cache since we're already using local.document_cache above | |||
@@ -1112,8 +1105,6 @@ def get_cached_doc(*args, **kwargs): | |||
except Exception: | |||
cache().hset("document_cache", key, doc.as_dict(), cache_locally=False) | |||
return doc | |||
def can_cache_doc(args) -> str | None: | |||
""" | |||
@@ -1152,7 +1143,7 @@ def get_cached_value( | |||
doctype: str, name: str, fieldname: str = "name", as_dict: bool = False | |||
) -> Any: | |||
try: | |||
doc = get_cached_doc(doctype, name, _allow_dict=True) | |||
doc = get_cached_doc(doctype, name) | |||
except DoesNotExistError: | |||
clear_last_message() | |||
return | |||
@@ -1188,13 +1179,9 @@ def get_doc(*args, **kwargs) -> "Document": | |||
doc = frappe.model.document.get_doc(*args, **kwargs) | |||
# Replace cache | |||
if key := can_cache_doc(args): | |||
if key in local.document_cache: | |||
local.document_cache[key] = doc | |||
if cache().hexists("document_cache", key): | |||
cache().hset("document_cache", key, doc.as_dict()) | |||
# Replace cache if stale one exists | |||
if (key := can_cache_doc(args)) and cache().hexists("document_cache", key): | |||
_set_document_in_cache(key, doc) | |||
return doc | |||
@@ -59,8 +59,8 @@ user_cache_keys = ( | |||
) | |||
doctype_cache_keys = ( | |||
"meta", | |||
"form_meta", | |||
"doctype_meta", | |||
"doctype_form_meta", | |||
"table_columns", | |||
"last_modified", | |||
"linked_doctypes", | |||
@@ -117,9 +117,6 @@ def clear_doctype_cache(doctype=None): | |||
clear_controller_cache(doctype) | |||
cache = frappe.cache() | |||
if getattr(frappe.local, "meta_cache") and (doctype in frappe.local.meta_cache): | |||
del frappe.local.meta_cache[doctype] | |||
for key in ("is_table", "doctype_modules", "document_cache"): | |||
cache.delete_value(key) | |||
@@ -881,6 +881,7 @@ def run_ui_tests( | |||
"@4tw/cypress-drag-drop@^2", | |||
"cypress-real-events", | |||
"@testing-library/cypress@^8", | |||
"@testing-library/dom@8.17.1", | |||
"@cypress/code-coverage@^3", | |||
] | |||
) | |||
@@ -414,10 +414,6 @@ class DocType(Document): | |||
if not frappe.flags.in_install and hasattr(self, "before_update"): | |||
self.sync_global_search() | |||
# clear from local cache | |||
if self.name in frappe.local.meta_cache: | |||
del frappe.local.meta_cache[self.name] | |||
clear_linked_doctype_cache() | |||
def setup_autoincrement_and_sequence(self): | |||
@@ -8,6 +8,7 @@ import frappe | |||
from frappe.desk.form.load import get_attachments | |||
from frappe.desk.query_report import generate_report_result | |||
from frappe.model.document import Document | |||
from frappe.monitor import add_data_to_monitor | |||
from frappe.utils import gzip_compress, gzip_decompress | |||
from frappe.utils.background_jobs import enqueue | |||
@@ -25,6 +26,8 @@ def run_background(prepared_report): | |||
instance = frappe.get_doc("Prepared Report", prepared_report) | |||
report = frappe.get_doc("Report", instance.ref_report_doctype) | |||
add_data_to_monitor(report=instance.ref_report_doctype) | |||
try: | |||
report.custom_columns = [] | |||
@@ -23,8 +23,10 @@ class RQWorker(Document): | |||
start = cint(args.get("start")) or 0 | |||
page_length = cint(args.get("page_length")) or 20 | |||
workers = get_workers()[start : start + page_length] | |||
return [serialize_worker(worker) for worker in workers] | |||
workers = get_workers() | |||
valid_workers = [w for w in workers if w.pid][start : start + page_length] | |||
return [serialize_worker(worker) for worker in valid_workers] | |||
@staticmethod | |||
def get_count(args) -> int: | |||
@@ -178,6 +178,7 @@ frappe.ui.form.on("Customize Form", { | |||
fieldname: "module", | |||
options: "Module Def", | |||
label: __("Module to Export"), | |||
reqd: 1, | |||
}, | |||
{ | |||
fieldtype: "Check", | |||
@@ -64,11 +64,9 @@ def getdoctype(doctype, with_parent=False, cached_timestamp=None): | |||
parent_dt = None | |||
# with parent (called from report builder) | |||
if with_parent: | |||
parent_dt = frappe.model.meta.get_parent_dt(doctype) | |||
if parent_dt: | |||
docs = get_meta_bundle(parent_dt) | |||
frappe.response["parent_dt"] = parent_dt | |||
if with_parent and (parent_dt := frappe.model.meta.get_parent_dt(doctype)): | |||
docs = get_meta_bundle(parent_dt) | |||
frappe.response["parent_dt"] = parent_dt | |||
if not docs: | |||
docs = get_meta_bundle(doctype) | |||
@@ -35,12 +35,10 @@ ASSET_KEYS = ( | |||
def get_meta(doctype, cached=True): | |||
# don't cache for developer mode as js files, templates may be edited | |||
if cached and not frappe.conf.developer_mode: | |||
meta = frappe.cache().hget("form_meta", doctype) | |||
if meta: | |||
meta = FormMeta(meta) | |||
else: | |||
meta = frappe.cache().hget("doctype_form_meta", doctype) | |||
if not meta: | |||
meta = FormMeta(doctype) | |||
frappe.cache().hset("form_meta", doctype, meta.as_dict()) | |||
frappe.cache().hset("doctype_form_meta", doctype, meta) | |||
else: | |||
meta = FormMeta(doctype) | |||
@@ -5,6 +5,7 @@ import json | |||
import frappe | |||
from frappe.desk.form.load import run_onload | |||
from frappe.monitor import add_data_to_monitor | |||
@frappe.whitelist() | |||
@@ -25,6 +26,8 @@ def savedocs(doc, action): | |||
run_onload(doc) | |||
send_updated_docs(doc) | |||
add_data_to_monitor(doctype=doc.doctype, action=action) | |||
frappe.msgprint(frappe._("Saved"), indicator="green", alert=True) | |||
@@ -2,11 +2,10 @@ | |||
# License: MIT. See LICENSE | |||
import json | |||
import os | |||
import frappe | |||
from frappe.geo.country_info import get_country_info | |||
from frappe.translate import get_dict, send_translations, set_default_language | |||
from frappe.translate import get_messages_for_boot, send_translations, set_default_language | |||
from frappe.utils import cint, strip | |||
from frappe.utils.password import update_password | |||
@@ -290,15 +289,7 @@ def load_messages(language): | |||
frappe.clear_cache() | |||
set_default_language(get_language_code(language)) | |||
frappe.db.commit() | |||
m = get_dict("page", "setup-wizard") | |||
for path in frappe.get_hooks("setup_wizard_requires"): | |||
# common folder `assets` served from `sites/` | |||
js_file_path = os.path.abspath(frappe.get_site_path("..", *path.strip("/").split("/"))) | |||
m.update(get_dict("jsfile", js_file_path)) | |||
m.update(get_dict("boot")) | |||
send_translations(m) | |||
send_translations(get_messages_for_boot()) | |||
return frappe.local.lang | |||
@@ -12,6 +12,7 @@ from frappe import _ | |||
from frappe.core.utils import ljust_list | |||
from frappe.model.utils import render_include | |||
from frappe.modules import get_module_path, scrub | |||
from frappe.monitor import add_data_to_monitor | |||
from frappe.permissions import get_role_permissions | |||
from frappe.translate import send_translations | |||
from frappe.utils import ( | |||
@@ -254,6 +255,8 @@ def run( | |||
result["add_total_row"] = report.add_total_row and not result.get("skip_total_row", False) | |||
add_data_to_monitor(report=report) | |||
return result | |||
@@ -40,21 +40,31 @@ from frappe.model.workflow import get_workflow_name | |||
from frappe.modules import load_doctype_module | |||
from frappe.utils import cast, cint, cstr | |||
DEFAULT_FIELD_LABELS = { | |||
"name": lambda: _("ID"), | |||
"creation": lambda: _("Created On"), | |||
"docstatus": lambda: _("Document Status"), | |||
"idx": lambda: _("Index"), | |||
"modified": lambda: _("Last Updated On"), | |||
"modified_by": lambda: _("Last Updated By"), | |||
"owner": lambda: _("Created By"), | |||
"_user_tags": lambda: _("Tags"), | |||
"_liked_by": lambda: _("Liked By"), | |||
"_comments": lambda: _("Comments"), | |||
"_assign": lambda: _("Assigned To"), | |||
} | |||
def get_meta(doctype, cached=True) -> "Meta": | |||
if cached: | |||
if not frappe.local.meta_cache.get(doctype): | |||
meta = frappe.cache().hget("meta", doctype) | |||
if meta: | |||
meta = Meta(meta) | |||
else: | |||
meta = Meta(doctype) | |||
frappe.cache().hset("meta", doctype, meta.as_dict()) | |||
frappe.local.meta_cache[doctype] = meta | |||
if not cached: | |||
return Meta(doctype) | |||
return frappe.local.meta_cache[doctype] | |||
else: | |||
return load_meta(doctype) | |||
if meta := frappe.cache().hget("doctype_meta", doctype): | |||
return meta | |||
meta = Meta(doctype) | |||
frappe.cache().hset("doctype_meta", doctype, meta) | |||
return meta | |||
def load_meta(doctype): | |||
@@ -86,7 +96,7 @@ def load_doctype_from_file(doctype): | |||
class Meta(Document): | |||
_metaclass = True | |||
default_fields = list(default_fields)[1:] | |||
special_doctypes = ( | |||
special_doctypes = { | |||
"DocField", | |||
"DocPerm", | |||
"DocType", | |||
@@ -94,24 +104,25 @@ class Meta(Document): | |||
"DocType Action", | |||
"DocType Link", | |||
"DocType State", | |||
) | |||
} | |||
standard_set_once_fields = [ | |||
frappe._dict(fieldname="creation", fieldtype="Datetime"), | |||
frappe._dict(fieldname="owner", fieldtype="Data"), | |||
] | |||
def __init__(self, doctype): | |||
self._fields = {} | |||
# from cache | |||
if isinstance(doctype, dict): | |||
super().__init__(doctype) | |||
self.init_field_map() | |||
return | |||
elif isinstance(doctype, Document): | |||
if isinstance(doctype, Document): | |||
super().__init__(doctype.as_dict()) | |||
self.process() | |||
else: | |||
super().__init__("DocType", doctype) | |||
self.process() | |||
self.process() | |||
def load_from_db(self): | |||
try: | |||
@@ -126,10 +137,12 @@ class Meta(Document): | |||
# don't process for special doctypes | |||
# prevent's circular dependency | |||
if self.name in self.special_doctypes: | |||
self.init_field_map() | |||
return | |||
self.add_custom_fields() | |||
self.apply_property_setters() | |||
self.init_field_map() | |||
self.sort_fields() | |||
self.get_valid_columns() | |||
self.set_custom_permissions() | |||
@@ -233,36 +246,24 @@ class Meta(Document): | |||
def get_field(self, fieldname): | |||
"""Return docfield from meta""" | |||
if not self._fields: | |||
for f in self.get("fields"): | |||
self._fields[f.fieldname] = f | |||
return self._fields.get(fieldname) | |||
def has_field(self, fieldname): | |||
"""Returns True if fieldname exists""" | |||
return True if self.get_field(fieldname) else False | |||
return fieldname in self._fields | |||
def get_label(self, fieldname): | |||
"""Get label of the given fieldname""" | |||
df = self.get_field(fieldname) | |||
if df: | |||
label = df.label | |||
else: | |||
label = { | |||
"name": _("ID"), | |||
"creation": _("Created On"), | |||
"docstatus": _("Document Status"), | |||
"idx": _("Index"), | |||
"modified": _("Last Updated On"), | |||
"modified_by": _("Last Updated By"), | |||
"owner": _("Created By"), | |||
"_user_tags": _("Tags"), | |||
"_liked_by": _("Liked By"), | |||
"_comments": _("Comments"), | |||
"_assign": _("Assigned To"), | |||
}.get(fieldname) or _("No Label") | |||
return label | |||
if df := self.get_field(fieldname): | |||
return df.label | |||
if fieldname in DEFAULT_FIELD_LABELS: | |||
return DEFAULT_FIELD_LABELS[fieldname]() | |||
return _("No Label") | |||
def get_options(self, fieldname): | |||
return self.get_field(fieldname).options | |||
@@ -273,12 +274,9 @@ class Meta(Document): | |||
if df.fieldtype == "Link": | |||
return df.options | |||
elif df.fieldtype == "Dynamic Link": | |||
if df.fieldtype == "Dynamic Link": | |||
return self.get_options(df.options) | |||
else: | |||
return None | |||
def get_search_fields(self): | |||
search_fields = self.search_fields or "name" | |||
search_fields = [d.strip() for d in search_fields.split(",")] | |||
@@ -340,8 +338,9 @@ class Meta(Document): | |||
def is_translatable(self, fieldname): | |||
"""Return true of false given a field""" | |||
field = self.get_field(fieldname) | |||
return field and field.translatable | |||
if field := self.get_field(fieldname): | |||
return field.translatable | |||
def get_workflow(self): | |||
return get_workflow_name(self.name) | |||
@@ -349,11 +348,10 @@ class Meta(Document): | |||
def get_naming_series_options(self) -> list[str]: | |||
"""Get list naming series options.""" | |||
field = self.get_field("naming_series") | |||
if field: | |||
if field := self.get_field("naming_series"): | |||
options = field.options or "" | |||
return options.split("\n") | |||
return [] | |||
def add_custom_fields(self): | |||
@@ -450,6 +448,9 @@ class Meta(Document): | |||
self.set(fieldname, new_list) | |||
def init_field_map(self): | |||
self._fields = {field.fieldname: field for field in self.fields} | |||
def sort_fields(self): | |||
"""sort on basis of insert_after""" | |||
custom_fields = sorted(self.get_custom_fields(), key=lambda df: df.idx) | |||
@@ -666,10 +667,17 @@ def is_single(doctype): | |||
def get_parent_dt(dt): | |||
parent_dt = frappe.get_all( | |||
"DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=dt), limit=1 | |||
if not frappe.is_table(dt): | |||
return "" | |||
return ( | |||
frappe.db.get_value( | |||
"DocField", | |||
{"fieldtype": ("in", frappe.model.table_fields), "options": dt}, | |||
"parent", | |||
) | |||
or "" | |||
) | |||
return parent_dt and parent_dt[0].parent or "" | |||
def set_fieldname(field_id, fieldname): | |||
@@ -25,6 +25,13 @@ def stop(response=None): | |||
frappe.local.monitor.dump(response) | |||
def add_data_to_monitor(**kwargs) -> None: | |||
"""Add additional custom key-value pairs along with monitor log. | |||
Note: Key-value pairs should be simple JSON exportable types.""" | |||
if hasattr(frappe.local, "monitor"): | |||
frappe.local.monitor.add_custom_data(**kwargs) | |||
def log_file(): | |||
return os.path.join(frappe.utils.get_bench_path(), "logs", "monitor.json.log") | |||
@@ -71,6 +78,10 @@ class Monitor: | |||
waitdiff = self.data.timestamp - job.enqueued_at | |||
self.data.job.wait = int(waitdiff.total_seconds() * 1000000) | |||
def add_custom_data(self, **kwargs): | |||
if self.data: | |||
self.data.update(kwargs) | |||
def dump(self, response=None): | |||
try: | |||
timediff = datetime.utcnow() - self.data.timestamp | |||
@@ -94,7 +105,7 @@ class Monitor: | |||
def store(self): | |||
if frappe.cache().llen(MONITOR_REDIS_KEY) > MONITOR_MAX_ENTRIES: | |||
frappe.cache().ltrim(MONITOR_REDIS_KEY, 1, -1) | |||
serialized = json.dumps(self.data, sort_keys=True, default=str) | |||
serialized = json.dumps(self.data, sort_keys=True, default=str, separators=(",", ":")) | |||
frappe.cache().rpush(MONITOR_REDIS_KEY, serialized) | |||
@@ -15,7 +15,7 @@ frappe.dashboard_utils = { | |||
let chart_filter_html = `<div class="${button_class} ${filter_class} btn-group dropdown pull-right"> | |||
<a data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | |||
<button class="btn btn-secondary btn-xs"> | |||
${icon_html} | |||
${icon_html} | |||
<span class="filter-label">${__(filter.label)}</span> | |||
${frappe.utils.icon("select", "xs")} | |||
</button> | |||
@@ -26,16 +26,21 @@ frappe.dashboard_utils = { | |||
options_html = filter.options | |||
.map( | |||
(option, i) => | |||
// TODO: Make option translatable - be careful, since the text of the a tag is later used to perform some action | |||
`<li> | |||
<a class="dropdown-item" data-fieldname="${filter.fieldnames[i]}">${option}</a> | |||
<a class="dropdown-item" data-fieldname="${ | |||
filter.fieldnames[i] | |||
}" data-option="${encodeURIComponent(option)}">${__(option)}</a> | |||
</li>` | |||
) | |||
.join(""); | |||
} else { | |||
// TODO: Make option translatable - be careful, since the text of the a tag is later used to perform some action | |||
options_html = filter.options | |||
.map((option) => `<li><a class="dropdown-item">${option}</a></li>`) | |||
.map( | |||
(option) => | |||
`<li><a class="dropdown-item" data-option="${encodeURIComponent( | |||
option | |||
)}">${__(option)}</a></li>` | |||
) | |||
.join(""); | |||
} | |||
@@ -54,8 +59,8 @@ frappe.dashboard_utils = { | |||
fieldname = $el.attr("data-fieldname"); | |||
} | |||
let selected_item = $el.text(); | |||
$el.parents(`.${button_class}`).find(".filter-label").text(selected_item); | |||
let selected_item = decodeURIComponent($el.data("option")); | |||
$el.parents(`.${button_class}`).find(".filter-label").html(__(selected_item)); | |||
filter.action(selected_item, fieldname); | |||
}); | |||
}); | |||
@@ -117,13 +117,13 @@ export default class OnboardingWidget extends Widget { | |||
const set_description = () => { | |||
let content = step.description | |||
? frappe.markdown(step.description) | |||
: `<h1>${step.title}</h1>`; | |||
: `<h1>${__(step.title)}</h1>`; | |||
if (step.action === "Create Entry") { | |||
// add a secondary action to view list | |||
content += `<p> | |||
<a href='/app/${frappe.router.slug(step.reference_document)}'> | |||
${__("Show {0} List", [step.reference_document])}</a> | |||
${__("Show {0} List", [__(step.reference_document)])}</a> | |||
</p>`; | |||
} | |||
@@ -1,3 +1,4 @@ | |||
from datetime import time | |||
from enum import Enum | |||
from pypika.functions import * | |||
@@ -35,6 +36,9 @@ Match = ImportMapper({db_type_is.MARIADB: MATCH, db_type_is.POSTGRES: TO_TSVECTO | |||
class _PostgresTimestamp(ArithmeticExpression): | |||
def __init__(self, datepart, timepart, alias=None): | |||
"""Postgres would need both datepart and timepart to be a string for concatenation""" | |||
if isinstance(timepart, time) or isinstance(datepart, time): | |||
timepart, datepart = str(timepart), str(datepart) | |||
if isinstance(datepart, str): | |||
datepart = Cast(datepart, "date") | |||
if isinstance(timepart, str): | |||
@@ -1,11 +1,11 @@ | |||
from datetime import timedelta | |||
from datetime import time, timedelta | |||
from typing import Any | |||
from pypika.queries import QueryBuilder | |||
from pypika.terms import Criterion, Function, ValueWrapper | |||
from pypika.utils import format_alias_sql | |||
from frappe.utils.data import format_timedelta | |||
from frappe.utils.data import format_time, format_timedelta | |||
class NamedParameterWrapper: | |||
@@ -55,9 +55,12 @@ class ParameterizedValueWrapper(ValueWrapper): | |||
value_sql = self.get_value_sql(quote_char=quote_char, **kwargs) | |||
sql = param_wrapper.get_sql(param_value=value_sql, **kwargs) | |||
else: | |||
# * BUG: pypika doesen't parse timedeltas | |||
# * BUG: pypika doesen't parse timedeltas and datetime.time | |||
if isinstance(self.value, timedelta): | |||
self.value = format_timedelta(self.value) | |||
elif isinstance(self.value, time): | |||
self.value = format_time(self.value) | |||
sql = self.get_value_sql( | |||
quote_char=quote_char, | |||
secondary_quote_char=secondary_quote_char, | |||
@@ -413,6 +413,14 @@ class TestCommands(BaseTestCommands): | |||
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 ( | |||
@@ -16,14 +16,21 @@ query. This test can be written like this. | |||
>>> get_controller("User") | |||
""" | |||
import time | |||
import unittest | |||
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed | |||
import frappe | |||
from frappe.frappeclient import FrappeClient | |||
from frappe.model.base_document import get_controller | |||
from frappe.query_builder.utils import db_type_is | |||
from frappe.tests.test_query_builder import run_only_if | |||
from frappe.tests.utils import FrappeTestCase | |||
from frappe.website.path_resolver import PathResolver | |||
@run_only_if(db_type_is.MARIADB) | |||
class TestPerformance(FrappeTestCase): | |||
def reset_request_specific_caches(self): | |||
# To simulate close to request level of handling | |||
@@ -33,6 +40,8 @@ class TestPerformance(FrappeTestCase): | |||
frappe.clear_cache() | |||
def setUp(self) -> None: | |||
self.HOST = frappe.utils.get_site_url(frappe.local.site) | |||
self.reset_request_specific_caches() | |||
def test_meta_caching(self): | |||
@@ -55,6 +64,36 @@ class TestPerformance(FrappeTestCase): | |||
with self.assertQueryCount(0): | |||
doc.get_invalid_links() | |||
@retry( | |||
retry=retry_if_exception_type(AssertionError), | |||
stop=stop_after_attempt(3), | |||
wait=wait_fixed(0.5), | |||
reraise=True, | |||
) | |||
def test_req_per_seconds_basic(self): | |||
"""Ideally should be ran against gunicorn worker, though I have not seen any difference | |||
when using werkzeug's run_simple for synchronous requests.""" | |||
EXPECTED_RPS = 55 # measured on GHA | |||
FAILURE_THREASHOLD = 0.1 | |||
req_count = 1000 | |||
client = FrappeClient(self.HOST, "Administrator", self.ADMIN_PASSWORD) | |||
start = time.perf_counter() | |||
for _ in range(req_count): | |||
client.get_list("ToDo", limit_page_length=1) | |||
end = time.perf_counter() | |||
rps = req_count / (end - start) | |||
print(f"Completed {req_count} in {end - start} @ {rps} requests per seconds") | |||
self.assertGreaterEqual( | |||
rps, | |||
EXPECTED_RPS * (1 - FAILURE_THREASHOLD), | |||
f"Possible performance regression in basic /api/Resource list requests", | |||
) | |||
@unittest.skip("Not implemented") | |||
def test_homepage_resolver(self): | |||
paths = ["/", "/app"] | |||
@@ -1,5 +1,6 @@ | |||
import unittest | |||
from collections.abc import Callable | |||
from datetime import time | |||
import frappe | |||
from frappe.query_builder import Case | |||
@@ -74,6 +75,26 @@ class TestCustomFunctionsMariaDB(FrappeTestCase): | |||
str(select_query).lower(), | |||
) | |||
def test_time(self): | |||
note = frappe.qb.DocType("Note") | |||
self.assertEqual( | |||
"TIMESTAMP('2021-01-01','00:00:21')", CombineDatetime("2021-01-01", time(0, 0, 21)).get_sql() | |||
) | |||
select_query = frappe.qb.from_(note).select( | |||
CombineDatetime(note.posting_date, note.posting_time) | |||
) | |||
self.assertIn("select timestamp(`posting_date`,`posting_time`)", str(select_query).lower()) | |||
select_query = select_query.where( | |||
CombineDatetime(note.posting_date, note.posting_time) | |||
>= CombineDatetime("2021-01-01", time(0, 0, 1)) | |||
) | |||
self.assertIn( | |||
"timestamp(`posting_date`,`posting_time`)>=timestamp('2021-01-01','00:00:01')", | |||
str(select_query).lower(), | |||
) | |||
def test_cast(self): | |||
note = frappe.qb.DocType("Note") | |||
self.assertEqual("CONCAT(name,'')", Cast_(note.name, "varchar").get_sql()) | |||
@@ -141,6 +162,28 @@ class TestCustomFunctionsPostgres(FrappeTestCase): | |||
'"tabnote"."posting_date"+"tabnote"."posting_time" "timestamp"', str(select_query).lower() | |||
) | |||
def test_time(self): | |||
note = frappe.qb.DocType("Note") | |||
self.assertEqual( | |||
"CAST('2021-01-01' AS DATE)+CAST('00:00:21' AS TIME)", | |||
CombineDatetime("2021-01-01", time(0, 0, 21)).get_sql(), | |||
) | |||
select_query = frappe.qb.from_(note).select( | |||
CombineDatetime(note.posting_date, note.posting_time) | |||
) | |||
self.assertIn('select "posting_date"+"posting_time"', str(select_query).lower()) | |||
select_query = select_query.where( | |||
CombineDatetime(note.posting_date, note.posting_time) | |||
>= CombineDatetime("2021-01-01", time(0, 0, 1)) | |||
) | |||
self.assertIn( | |||
"""where "posting_date"+"posting_time">=cast('2021-01-01' as date)+cast('00:00:01' as time)""", | |||
str(select_query).lower(), | |||
) | |||
def test_cast(self): | |||
note = frappe.qb.DocType("Note") | |||
self.assertEqual("CAST(name AS VARCHAR)", Cast_(note.name, "varchar").get_sql()) | |||
@@ -26,9 +26,9 @@ class FrappeTestCase(unittest.TestCase): | |||
@classmethod | |||
def setUpClass(cls) -> None: | |||
cls.TEST_SITE = getattr(frappe.local, "site", None) or cls.TEST_SITE | |||
cls.ADMIN_PASSWORD = frappe.get_conf(cls.TEST_SITE).admin_password | |||
# flush changes done so far to avoid flake | |||
frappe.db.commit() | |||
frappe.db.begin() | |||
if cls.SHOW_TRANSACTION_COMMIT_WARNINGS: | |||
frappe.db.add_before_commit(_commit_watcher) | |||
@@ -459,7 +459,7 @@ def get_site_base_path(): | |||
def get_site_path(*path): | |||
return get_path(base=get_site_base_path(), *path) | |||
return get_path(*path, base=get_site_base_path()) | |||
def get_files_path(*path, **kwargs): | |||
@@ -45,6 +45,7 @@ URL_NOTATION_PATTERN = re.compile( | |||
) # background-image: url('/assets/...') | |||
DURATION_PATTERN = re.compile(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$") | |||
HTML_TAG_PATTERN = re.compile("<[^>]+>") | |||
MARIADB_SPECIFIC_COMMENT = re.compile(r"#.*") | |||
class Weekday(Enum): | |||
@@ -1809,8 +1810,11 @@ def sanitize_column(column_name: str) -> None: | |||
from frappe import _ | |||
regex = re.compile("^.*[,'();].*") | |||
column_name = sqlparse.format(column_name, strip_comments=True, keyword_case="lower") | |||
if frappe.db and frappe.db.db_type == "mariadb": | |||
# strip mariadb specific comments which are like python single line comments | |||
column_name = MARIADB_SPECIFIC_COMMENT.sub("", column_name) | |||
blacklisted_keywords = [ | |||
"select", | |||
"create", | |||
@@ -1826,6 +1830,7 @@ def sanitize_column(column_name: str) -> None: | |||
def _raise_exception(): | |||
frappe.throw(_("Invalid field name {0}").format(column_name), frappe.DataError) | |||
regex = re.compile("^.*[,'();].*") | |||
if "ifnull" in column_name: | |||
if regex.match(column_name): | |||
# to avoid and, or | |||