* Handle inconsistencies in type handling in DatabaseQuery & Database APIs * Update incompatible queries with frappe.qb notation * Fixed use cases discovered by failing ERPNext CI tests fix: db independent syntax for user_type fix: handle postgres datetime values feat: add ability to auto commit on db inserts feat: add ability to escape underscore in postgres fix: handle missing data in test runner bootstrapping fix: db independent syntax for queries fix: refactor to use qb fix: update cache for language fix: use pluck in email_queue Co-authored-by: gavin <gavin18d@gmail.com> fix: don't auto insert on tests for make_property_setter fix: remove auto_commit in custom_field insertion fix: remove auto_commit functionality fix: review comments fix: revert link validation fix: style suggestion for readability Co-authored-by: gavin <gavin18d@gmail.com> fix: revert .lower() in link validation fix: add rollback for setup_wizard Revert "fix: add rollback for setup_wizard" This reverts commitversion-1483b3b0913d
. Revert "feat: add ability to escape underscore in postgres" This reverts commit8ed9c2aa33
. fix: more concise representation of order fields Co-authored-by: gavin <gavin18d@gmail.com>
@@ -36,13 +36,19 @@ class UserType(Document): | |||||
if not self.user_doctypes: | if not self.user_doctypes: | ||||
return | return | ||||
modules = frappe.get_all('DocType', fields=['distinct module as module'], | |||||
filters={'name': ('in', [d.document_type for d in self.user_doctypes])}, group_by='module') | |||||
DocType = frappe.qb.DocType("DocType") | |||||
document_types = [d.document_type for d in self.user_doctypes] or [''] | |||||
modules = (frappe.qb.from_(DocType) | |||||
.select(DocType.module) | |||||
.where(DocType.name.isin(document_types)) | |||||
.groupby(DocType.module) | |||||
.distinct()).run() | |||||
self.set('user_type_modules', []) | self.set('user_type_modules', []) | ||||
for row in modules: | for row in modules: | ||||
self.append('user_type_modules', { | self.append('user_type_modules', { | ||||
'module': row.module | |||||
'module': row[0] | |||||
}) | }) | ||||
def validate_document_type_limit(self): | def validate_document_type_limit(self): | ||||
@@ -74,9 +74,15 @@ class PostgresDatabase(Database): | |||||
return conn | return conn | ||||
def escape(self, s, percent=True): | def escape(self, s, percent=True): | ||||
"""Excape quotes and percent in given string.""" | |||||
"""Escape quotes and percent in given string.""" | |||||
if isinstance(s, bytes): | if isinstance(s, bytes): | ||||
s = s.decode('utf-8') | s = s.decode('utf-8') | ||||
# MariaDB's driver treats None as an empty string | |||||
# So Postgres should do the same | |||||
if s is None: | |||||
s = '' | |||||
if percent: | if percent: | ||||
s = s.replace("%", "%%") | s = s.replace("%", "%%") | ||||
@@ -7,6 +7,7 @@ from frappe.model.document import Document | |||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils import cint | from frappe.utils import cint | ||||
class BulkUpdate(Document): | class BulkUpdate(Document): | ||||
pass | pass | ||||
@@ -22,7 +23,7 @@ def update(doctype, field, value, condition='', limit=500): | |||||
frappe.throw(_('; not allowed in condition')) | frappe.throw(_('; not allowed in condition')) | ||||
docnames = frappe.db.sql_list( | docnames = frappe.db.sql_list( | ||||
'''select name from `tab{0}`{1} limit 0, {2}'''.format(doctype, condition, limit) | |||||
'''select name from `tab{0}`{1} limit {2} offset 0'''.format(doctype, condition, limit) | |||||
) | ) | ||||
data = {} | data = {} | ||||
data[field] = value | data[field] = value | ||||
@@ -13,8 +13,9 @@ class Dashboard(Document): | |||||
def on_update(self): | def on_update(self): | ||||
if self.is_default: | if self.is_default: | ||||
# make all other dashboards non-default | # make all other dashboards non-default | ||||
frappe.db.sql('''update | |||||
`tabDashboard` set is_default = 0 where name != %s''', self.name) | |||||
DashBoard = frappe.qb.DocType("Dashboard") | |||||
frappe.qb.update(DashBoard).set(DashBoard.is_default, 0).where(DashBoard.name != self.name).run() | |||||
if frappe.conf.developer_mode and self.is_standard: | if frappe.conf.developer_mode and self.is_standard: | ||||
export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module) | export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module) | ||||
@@ -151,7 +151,7 @@ def update_system_settings(args): | |||||
system_settings = frappe.get_doc("System Settings", "System Settings") | system_settings = frappe.get_doc("System Settings", "System Settings") | ||||
system_settings.update({ | system_settings.update({ | ||||
"country": args.get("country"), | "country": args.get("country"), | ||||
"language": get_language_code(args.get("language")), | |||||
"language": get_language_code(args.get("language")) or 'en', | |||||
"time_zone": args.get("timezone"), | "time_zone": args.get("timezone"), | ||||
"float_precision": 3, | "float_precision": 3, | ||||
'date_format': frappe.db.get_value("Country", args.get("country"), "date_format"), | 'date_format': frappe.db.get_value("Country", args.get("country"), "date_format"), | ||||
@@ -417,6 +417,7 @@ def make_records(records, debug=False): | |||||
raise | raise | ||||
except Exception as e: | except Exception as e: | ||||
frappe.db.rollback() | |||||
exception = record.get('__exception') | exception = record.get('__exception') | ||||
if exception: | if exception: | ||||
config = _dict(exception) | config = _dict(exception) | ||||
@@ -4,6 +4,7 @@ | |||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_all_nodes(doctype, label, parent, tree_method, **filters): | def get_all_nodes(doctype, label, parent, tree_method, **filters): | ||||
'''Recursively gets all data from tree nodes''' | '''Recursively gets all data from tree nodes''' | ||||
@@ -40,8 +41,8 @@ def get_children(doctype, parent='', **filters): | |||||
def _get_children(doctype, parent='', ignore_permissions=False): | def _get_children(doctype, parent='', ignore_permissions=False): | ||||
parent_field = 'parent_' + doctype.lower().replace(' ', '_') | parent_field = 'parent_' + doctype.lower().replace(' ', '_') | ||||
filters = [['ifnull(`{0}`,"")'.format(parent_field), '=', parent], | |||||
['docstatus', '<' ,'2']] | |||||
filters = [["ifnull(`{0}`,'')".format(parent_field), '=', parent], | |||||
['docstatus', '<' ,2]] | |||||
meta = frappe.get_meta(doctype) | meta = frappe.get_meta(doctype) | ||||
@@ -475,28 +475,20 @@ class QueueBuilder: | |||||
if self._unsubscribed_user_emails is not None: | if self._unsubscribed_user_emails is not None: | ||||
return self._unsubscribed_user_emails | return self._unsubscribed_user_emails | ||||
all_ids = tuple(set(self.recipients + self.cc)) | |||||
unsubscribed = frappe.db.sql_list(''' | |||||
SELECT | |||||
distinct email | |||||
from | |||||
`tabEmail Unsubscribe` | |||||
where | |||||
email in %(all_ids)s | |||||
and ( | |||||
( | |||||
reference_doctype = %(reference_doctype)s | |||||
and reference_name = %(reference_name)s | |||||
) | |||||
or global_unsubscribe = 1 | |||||
) | |||||
''', { | |||||
'all_ids': all_ids, | |||||
'reference_doctype': self.reference_doctype, | |||||
'reference_name': self.reference_name, | |||||
}) | |||||
all_ids = list(set(self.recipients + self.cc)) | |||||
EmailUnsubscribe = frappe.qb.DocType("Email Unsubscribe") | |||||
unsubscribed = (frappe.qb.from_(EmailUnsubscribe) | |||||
.select(EmailUnsubscribe.email) | |||||
.where(EmailUnsubscribe.email.isin(all_ids) & | |||||
( | |||||
( | |||||
(EmailUnsubscribe.reference_doctype == self.reference_doctype) & (EmailUnsubscribe.reference_name == self.reference_name) | |||||
) | EmailUnsubscribe.global_unsubscribe == 1 | |||||
) | |||||
).distinct() | |||||
).run(pluck=True) | |||||
self._unsubscribed_user_emails = unsubscribed or [] | self._unsubscribed_user_emails = unsubscribed or [] | ||||
return self._unsubscribed_user_emails | return self._unsubscribed_user_emails | ||||
@@ -771,7 +771,9 @@ class BaseDocument(object): | |||||
else: | else: | ||||
self_value = self.get_value(key) | self_value = self.get_value(key) | ||||
# Postgres stores values as `datetime.time`, MariaDB as `timedelta` | |||||
if isinstance(self_value, datetime.timedelta) and isinstance(db_value, datetime.time): | |||||
db_value = datetime.timedelta(hours=db_value.hour, minutes=db_value.minute, seconds=db_value.second, microseconds=db_value.microsecond) | |||||
if self_value != db_value: | if self_value != db_value: | ||||
frappe.throw(_("Not allowed to change {0} after submission").format(df.label), | frappe.throw(_("Not allowed to change {0} after submission").format(df.label), | ||||
frappe.UpdateAfterSubmitError) | frappe.UpdateAfterSubmitError) | ||||
@@ -127,19 +127,18 @@ class DatabaseQuery(object): | |||||
if self.distinct: | if self.distinct: | ||||
args.fields = 'distinct ' + args.fields | args.fields = 'distinct ' + args.fields | ||||
args.order_by = '' # TODO: recheck for alternative | args.order_by = '' # TODO: recheck for alternative | ||||
# Postgres requires any field that appears in the select clause to also | |||||
# appear in the order by and group by clause | |||||
if args.order_by and args.group_by: | if args.order_by and args.group_by: | ||||
order_field = args.order_by | order_field = args.order_by | ||||
for r in (" order by ", " asc", " ASC", " desc", " DESC"): | for r in (" order by ", " asc", " ASC", " desc", " DESC"): | ||||
order_field = order_field.replace(r, "") | order_field = order_field.replace(r, "") | ||||
if not order_field in args.fields: | |||||
order_fieldm = order_field.replace("`", "") | |||||
if "." in order_fieldm: | |||||
args.fields += ", MAX(" + order_fieldm.split(".")[1] + ") as `" + order_fieldm + "`" | |||||
else: | |||||
args.fields += ", MAX(" + order_fieldm + ") as `" + order_fieldm + "`" | |||||
if order_field not in args.fields: | |||||
order_fieldm = order_field.replace("`", '"') | |||||
args.fields += f", MAX({order_fieldm}) as `{order_fieldm}`" | |||||
args.order_by = args.order_by.replace(order_field, "`" + order_fieldm + "`") | args.order_by = args.order_by.replace(order_field, "`" + order_fieldm + "`") | ||||
query = """select %(fields)s | query = """select %(fields)s | ||||
@@ -335,7 +335,10 @@ def make_test_records_for_doctype(doctype, verbose=0, force=False): | |||||
frappe.local.test_objects[doctype] += test_module._make_test_records(verbose) | frappe.local.test_objects[doctype] += test_module._make_test_records(verbose) | ||||
elif hasattr(test_module, "test_records"): | elif hasattr(test_module, "test_records"): | ||||
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose, force) | |||||
if doctype in frappe.local.test_objects: | |||||
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose, force) | |||||
else: | |||||
frappe.local.test_objects[doctype] = make_test_objects(doctype, test_module.test_records, verbose, force) | |||||
else: | else: | ||||
test_records = frappe.get_test_records(doctype) | test_records = frappe.get_test_records(doctype) | ||||
@@ -197,6 +197,7 @@ class TestWebsite(unittest.TestCase): | |||||
frappe.cache().delete_key('app_hooks') | frappe.cache().delete_key('app_hooks') | ||||
def test_printview_page(self): | def test_printview_page(self): | ||||
frappe.db.value_cache[('DocType', 'Language', 'name')] = (('Language',),) | |||||
content = get_response_content('/Language/ru') | content = get_response_content('/Language/ru') | ||||
self.assertIn('<div class="print-format">', content) | self.assertIn('<div class="print-format">', content) | ||||
self.assertIn('<div>Language</div>', content) | self.assertIn('<div>Language</div>', content) | ||||
@@ -98,6 +98,9 @@ def get_timedelta(time: Optional[str] = None) -> Optional[datetime.timedelta]: | |||||
def to_timedelta(time_str): | def to_timedelta(time_str): | ||||
from dateutil import parser | from dateutil import parser | ||||
if isinstance(time_str, datetime.time): | |||||
time_str = str(time_str) | |||||
if isinstance(time_str, str): | if isinstance(time_str, str): | ||||
t = parser.parse(time_str) | t = parser.parse(time_str) | ||||
return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond) | return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond) | ||||
@@ -35,9 +35,13 @@ def get_random(doctype, filters=None, doc=False): | |||||
condition = " where " + " and ".join(condition) | condition = " where " + " and ".join(condition) | ||||
else: | else: | ||||
condition = "" | condition = "" | ||||
out = frappe.db.sql("""select name from `tab%s` %s | |||||
order by RAND() limit 0,1""" % (doctype, condition)) | |||||
out = frappe.db.multisql({ | |||||
'mariadb': """select name from `tab%s` %s | |||||
order by RAND() limit 1 offset 0""" % (doctype, condition), | |||||
'postgres': """select name from `tab%s` %s | |||||
order by RANDOM() limit 1 offset 0""" % (doctype, condition) | |||||
}) | |||||
out = out and out[0][0] or None | out = out and out[0][0] or None | ||||
@@ -16,6 +16,9 @@ import frappe | |||||
from frappe import _ | from frappe import _ | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.query_builder import DocType, Order | from frappe.query_builder import DocType, Order | ||||
from pypika import CustomFunction | |||||
Replace = CustomFunction('REPLACE', ['input', 'substr_to_replace', 'substr_to_replace_with']) | |||||
class NestedSetRecursionError(frappe.ValidationError): pass | class NestedSetRecursionError(frappe.ValidationError): pass | ||||
class NestedSetMultipleRootsError(frappe.ValidationError): pass | class NestedSetMultipleRootsError(frappe.ValidationError): pass | ||||
@@ -88,7 +91,7 @@ def update_move_node(doc, parent_field): | |||||
if parent: | if parent: | ||||
new_parent = frappe.db.sql("""select lft, rgt from `tab{0}` | new_parent = frappe.db.sql("""select lft, rgt from `tab{0}` | ||||
where name = %s for update""".format(doc.doctype), parent, as_dict=1)[0] | |||||
where name = %s for update""".format(doc.doctype), parent, as_dict=1)[0] | |||||
validate_loop(doc.doctype, doc.name, new_parent.lft, new_parent.rgt) | validate_loop(doc.doctype, doc.name, new_parent.lft, new_parent.rgt) | ||||
@@ -107,8 +110,7 @@ def update_move_node(doc, parent_field): | |||||
if parent: | if parent: | ||||
new_parent = frappe.db.sql("""select lft, rgt from `tab%s` | new_parent = frappe.db.sql("""select lft, rgt from `tab%s` | ||||
where name = %s for update""" % (doc.doctype, '%s'), parent, as_dict=1)[0] | |||||
where name = %s for update""" % (doc.doctype, '%s'), parent, as_dict=1)[0] | |||||
# set parent lft, rgt | # set parent lft, rgt | ||||
frappe.db.sql("""update `tab{0}` set rgt = rgt + %s | frappe.db.sql("""update `tab{0}` set rgt = rgt + %s | ||||
@@ -134,6 +136,7 @@ def update_move_node(doc, parent_field): | |||||
frappe.db.sql("""update `tab{0}` set lft = -lft + %s, rgt = -rgt + %s | frappe.db.sql("""update `tab{0}` set lft = -lft + %s, rgt = -rgt + %s | ||||
where lft < 0""".format(doc.doctype), (new_diff, new_diff)) | where lft < 0""".format(doc.doctype), (new_diff, new_diff)) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def rebuild_tree(doctype, parent_field): | def rebuild_tree(doctype, parent_field): | ||||
""" | """ | ||||
@@ -148,13 +151,12 @@ def rebuild_tree(doctype, parent_field): | |||||
right = 1 | right = 1 | ||||
table = DocType(doctype) | table = DocType(doctype) | ||||
column = getattr(table, parent_field) | column = getattr(table, parent_field) | ||||
result = ( | result = ( | ||||
frappe.qb.from_(table) | frappe.qb.from_(table) | ||||
.where( | .where( | ||||
(column == "") | (column.isnull()) | (column == "") | (column.isnull()) | ||||
) | ) | ||||
.orderby(table.name, order=Order.asc) | |||||
.orderby(Replace(table.name, '_', ''), order=Order.asc) | |||||
.select(table.name) | .select(table.name) | ||||
).run() | ).run() | ||||
@@ -177,7 +179,7 @@ def rebuild_node(doctype, parent, left, parent_field): | |||||
column = getattr(table, parent_field) | column = getattr(table, parent_field) | ||||
result = ( | result = ( | ||||
frappe.qb.from_(table).where(column == parent).select(table.name) | |||||
frappe.qb.from_(table).where(column == parent).select(table.name).orderby(Replace(table.name, '_', ''), order=Order.asc) | |||||
).run() | ).run() | ||||
for r in result: | for r in result: | ||||
@@ -125,7 +125,7 @@ def json_handler(obj): | |||||
# serialize date | # serialize date | ||||
import collections.abc | import collections.abc | ||||
if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)): | |||||
if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime, datetime.time)): | |||||
return str(obj) | return str(obj) | ||||
elif isinstance(obj, decimal.Decimal): | elif isinstance(obj, decimal.Decimal): | ||||
@@ -58,15 +58,18 @@ class TestBlogPost(unittest.TestCase): | |||||
category_page_link = list(soup.find_all('a', href=re.compile(blog.blog_category)))[0] | category_page_link = list(soup.find_all('a', href=re.compile(blog.blog_category)))[0] | ||||
category_page_url = category_page_link["href"] | category_page_url = category_page_link["href"] | ||||
cached_value = frappe.db.value_cache[('DocType', 'Blog Post', 'name')] | |||||
frappe.db.value_cache[('DocType', 'Blog Post', 'name')] = (('Blog Post',),) | |||||
# Visit the category page (by following the link found in above stage) | # Visit the category page (by following the link found in above stage) | ||||
set_request(path=category_page_url) | set_request(path=category_page_url) | ||||
category_page_response = get_response() | category_page_response = get_response() | ||||
category_page_html = frappe.safe_decode(category_page_response.get_data()) | category_page_html = frappe.safe_decode(category_page_response.get_data()) | ||||
# Category page should contain the blog post title | # Category page should contain the blog post title | ||||
self.assertIn(blog.title, category_page_html) | self.assertIn(blog.title, category_page_html) | ||||
# Cleanup | # Cleanup | ||||
frappe.db.value_cache[('DocType', 'Blog Post', 'name')] = cached_value | |||||
frappe.delete_doc("Blog Post", blog.name) | frappe.delete_doc("Blog Post", blog.name) | ||||
frappe.delete_doc("Blog Category", blog.blog_category) | frappe.delete_doc("Blog Category", blog.blog_category) | ||||