@@ -978,8 +978,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa | |||||
def delete_doc_if_exists(doctype, name, force=0): | def delete_doc_if_exists(doctype, name, force=0): | ||||
"""Delete document if exists.""" | """Delete document if exists.""" | ||||
if db.exists(doctype, name): | |||||
delete_doc(doctype, name, force=force) | |||||
delete_doc(doctype, name, force=force, ignore_missing=True) | |||||
def reload_doctype(doctype, force=False, reset_permissions=False): | def reload_doctype(doctype, force=False, reset_permissions=False): | ||||
"""Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files.""" | """Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files.""" | ||||
@@ -1252,9 +1251,10 @@ def get_newargs(fn, kwargs): | |||||
if hasattr(fn, 'fnargs'): | if hasattr(fn, 'fnargs'): | ||||
fnargs = fn.fnargs | fnargs = fn.fnargs | ||||
else: | else: | ||||
fnargs = inspect.getfullargspec(fn).args | |||||
fnargs.extend(inspect.getfullargspec(fn).kwonlyargs) | |||||
varkw = inspect.getfullargspec(fn).varkw | |||||
fullargspec = inspect.getfullargspec(fn) | |||||
fnargs = fullargspec.args | |||||
fnargs.extend(fullargspec.kwonlyargs) | |||||
varkw = fullargspec.varkw | |||||
newargs = {} | newargs = {} | ||||
for a in kwargs: | for a in kwargs: | ||||
@@ -1266,7 +1266,7 @@ def get_newargs(fn, kwargs): | |||||
return newargs | return newargs | ||||
def make_property_setter(args, ignore_validate=False, validate_fields_for_doctype=True): | |||||
def make_property_setter(args, ignore_validate=False, validate_fields_for_doctype=True, is_system_generated=True): | |||||
"""Create a new **Property Setter** (for overriding DocType and DocField properties). | """Create a new **Property Setter** (for overriding DocType and DocField properties). | ||||
If doctype is not specified, it will create a property setter for all fields with the | If doctype is not specified, it will create a property setter for all fields with the | ||||
@@ -1297,6 +1297,7 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp | |||||
'property': args.property, | 'property': args.property, | ||||
'value': args.value, | 'value': args.value, | ||||
'property_type': args.property_type or "Data", | 'property_type': args.property_type or "Data", | ||||
'is_system_generated': is_system_generated, | |||||
'__islocal': 1 | '__islocal': 1 | ||||
}) | }) | ||||
ps.flags.ignore_validate = ignore_validate | ps.flags.ignore_validate = ignore_validate | ||||
@@ -1464,7 +1465,7 @@ def get_list(doctype, *args, **kwargs): | |||||
:param fields: List of fields or `*`. | :param fields: List of fields or `*`. | ||||
:param filters: List of filters (see example). | :param filters: List of filters (see example). | ||||
:param order_by: Order By e.g. `modified desc`. | :param order_by: Order By e.g. `modified desc`. | ||||
:param limit_page_start: Start results at record #. Default 0. | |||||
:param limit_start: Start results at record #. Default 0. | |||||
:param limit_page_length: No of records in the page. Default 20. | :param limit_page_length: No of records in the page. Default 20. | ||||
Example usage: | Example usage: | ||||
@@ -1,15 +1,14 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2017, Frappe Technologies and contributors | |||||
# Copyright (c) 2022, Frappe Technologies and contributors | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils import get_fullname, now | |||||
from frappe.model.document import Document | |||||
from frappe.core.utils import set_timeline_doc | from frappe.core.utils import set_timeline_doc | ||||
import frappe | |||||
from frappe.model.document import Document | |||||
from frappe.query_builder import DocType, Interval | from frappe.query_builder import DocType, Interval | ||||
from frappe.query_builder.functions import Now | from frappe.query_builder.functions import Now | ||||
from pypika.terms import PseudoColumn | |||||
from frappe.utils import get_fullname, now | |||||
class ActivityLog(Document): | class ActivityLog(Document): | ||||
def before_insert(self): | def before_insert(self): | ||||
@@ -49,5 +48,5 @@ def clear_activity_logs(days=None): | |||||
days = 90 | days = 90 | ||||
doctype = DocType("Activity Log") | doctype = DocType("Activity Log") | ||||
frappe.db.delete(doctype, filters=( | frappe.db.delete(doctype, filters=( | ||||
doctype.creation < PseudoColumn(f"({Now() - Interval(days=days)})") | |||||
)) | |||||
doctype.creation < (Now() - Interval(days=days)) | |||||
)) |
@@ -4,8 +4,8 @@ import unittest | |||||
from urllib.parse import quote | from urllib.parse import quote | ||||
import frappe | import frappe | ||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue | |||||
from frappe.core.doctype.communication.communication import get_emails | from frappe.core.doctype.communication.communication import get_emails | ||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue | |||||
test_records = frappe.get_test_records('Communication') | test_records = frappe.get_test_records('Communication') | ||||
@@ -202,7 +202,7 @@ class TestCommunication(unittest.TestCase): | |||||
self.assertIn(("Note", note.name), doc_links) | self.assertIn(("Note", note.name), doc_links) | ||||
def parse_emails(self): | |||||
def test_parse_emails(self): | |||||
emails = get_emails( | emails = get_emails( | ||||
[ | [ | ||||
'comm_recipient+DocType+DocName@example.com', | 'comm_recipient+DocType+DocName@example.com', | ||||
@@ -382,7 +382,7 @@ class TestFile(unittest.TestCase): | |||||
}).insert(ignore_permissions=True) | }).insert(ignore_permissions=True) | ||||
test_file.make_thumbnail() | test_file.make_thumbnail() | ||||
self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg') | |||||
self.assertEqual(test_file.thumbnail_url, '/files/image_small.jpg') | |||||
# test web image without extension | # test web image without extension | ||||
test_file = frappe.get_doc({ | test_file = frappe.get_doc({ | ||||
@@ -399,7 +399,7 @@ class TestFile(unittest.TestCase): | |||||
test_file.reload() | test_file.reload() | ||||
test_file.file_url = "/files/image_small.jpg" | test_file.file_url = "/files/image_small.jpg" | ||||
test_file.make_thumbnail(suffix="xs", crop=True) | test_file.make_thumbnail(suffix="xs", crop=True) | ||||
self.assertEquals(test_file.thumbnail_url, '/files/image_small_xs.jpg') | |||||
self.assertEqual(test_file.thumbnail_url, '/files/image_small_xs.jpg') | |||||
frappe.clear_messages() | frappe.clear_messages() | ||||
test_file.db_set('thumbnail_url', None) | test_file.db_set('thumbnail_url', None) | ||||
@@ -407,7 +407,7 @@ class TestFile(unittest.TestCase): | |||||
test_file.file_url = frappe.utils.get_url('unknown.jpg') | test_file.file_url = frappe.utils.get_url('unknown.jpg') | ||||
test_file.make_thumbnail(suffix="xs") | test_file.make_thumbnail(suffix="xs") | ||||
self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found") | self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found") | ||||
self.assertEquals(test_file.thumbnail_url, None) | |||||
self.assertEqual(test_file.thumbnail_url, None) | |||||
def test_file_unzip(self): | def test_file_unzip(self): | ||||
file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip') | file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip') | ||||
@@ -7,7 +7,6 @@ from frappe import _ | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.query_builder import DocType, Interval | from frappe.query_builder import DocType, Interval | ||||
from frappe.query_builder.functions import Now | from frappe.query_builder.functions import Now | ||||
from pypika.terms import PseudoColumn | |||||
class LogSettings(Document): | class LogSettings(Document): | ||||
@@ -19,7 +18,7 @@ class LogSettings(Document): | |||||
def clear_error_logs(self): | def clear_error_logs(self): | ||||
table = DocType("Error Log") | table = DocType("Error Log") | ||||
frappe.db.delete(table, filters=( | frappe.db.delete(table, filters=( | ||||
table.creation < PseudoColumn(f"({Now() - Interval(days=self.clear_error_log_after)})") | |||||
table.creation < (Now() - Interval(days=self.clear_error_log_after)) | |||||
)) | )) | ||||
def clear_activity_logs(self): | def clear_activity_logs(self): | ||||
@@ -1,8 +1,107 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2020, Frappe Technologies and Contributors | |||||
# Copyright (c) 2022, Frappe Technologies and Contributors | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
# import frappe | |||||
from datetime import datetime | |||||
import unittest | import unittest | ||||
import frappe | |||||
from frappe.utils import now_datetime, add_to_date | |||||
from frappe.core.doctype.log_settings.log_settings import run_log_clean_up | |||||
class TestLogSettings(unittest.TestCase): | class TestLogSettings(unittest.TestCase): | ||||
pass | |||||
@classmethod | |||||
def setUpClass(cls): | |||||
cls.savepoint = "TestLogSettings" | |||||
# SAVEPOINT can only be used in transaction blocks and we don't wan't to take chances | |||||
frappe.db.begin() | |||||
frappe.db.savepoint(cls.savepoint) | |||||
frappe.db.set_single_value( | |||||
"Log Settings", | |||||
{ | |||||
"clear_error_log_after": 1, | |||||
"clear_activity_log_after": 1, | |||||
"clear_email_queue_after": 1, | |||||
}, | |||||
) | |||||
@classmethod | |||||
def tearDownClass(cls): | |||||
frappe.db.rollback(save_point=cls.savepoint) | |||||
def setUp(self) -> None: | |||||
if self._testMethodName == "test_delete_logs": | |||||
self.datetime = frappe._dict() | |||||
self.datetime.current = now_datetime() | |||||
self.datetime.past = add_to_date(self.datetime.current, days=-4) | |||||
setup_test_logs(self.datetime.past) | |||||
def tearDown(self) -> None: | |||||
if self._testMethodName == "test_delete_logs": | |||||
del self.datetime | |||||
def test_delete_logs(self): | |||||
# make sure test data is present | |||||
activity_log_count = frappe.db.count( | |||||
"Activity Log", {"creation": ("<=", self.datetime.past)} | |||||
) | |||||
error_log_count = frappe.db.count( | |||||
"Error Log", {"creation": ("<=", self.datetime.past)} | |||||
) | |||||
email_queue_count = frappe.db.count( | |||||
"Email Queue", {"creation": ("<=", self.datetime.past)} | |||||
) | |||||
self.assertNotEqual(activity_log_count, 0) | |||||
self.assertNotEqual(error_log_count, 0) | |||||
self.assertNotEqual(email_queue_count, 0) | |||||
# run clean up job | |||||
run_log_clean_up() | |||||
# test if logs are deleted | |||||
activity_log_count = frappe.db.count( | |||||
"Activity Log", {"creation": ("<", self.datetime.past)} | |||||
) | |||||
error_log_count = frappe.db.count( | |||||
"Error Log", {"creation": ("<", self.datetime.past)} | |||||
) | |||||
email_queue_count = frappe.db.count( | |||||
"Email Queue", {"creation": ("<", self.datetime.past)} | |||||
) | |||||
self.assertEqual(activity_log_count, 0) | |||||
self.assertEqual(error_log_count, 0) | |||||
self.assertEqual(email_queue_count, 0) | |||||
def setup_test_logs(past: datetime) -> None: | |||||
activity_log = frappe.get_doc( | |||||
{ | |||||
"doctype": "Activity Log", | |||||
"subject": "Test subject", | |||||
"full_name": "test user2", | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
activity_log.db_set("creation", past) | |||||
error_log = frappe.get_doc( | |||||
{ | |||||
"doctype": "Error Log", | |||||
"method": "test_method", | |||||
"error": "traceback", | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
error_log.db_set("creation", past) | |||||
doc1 = frappe.get_doc( | |||||
{ | |||||
"doctype": "Email Queue", | |||||
"sender": "test1@example.com", | |||||
"message": "This is a test email1", | |||||
"priority": 1, | |||||
"expose_recipients": "test@receiver.com", | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
doc1.db_set("creation", past) |
@@ -253,8 +253,8 @@ class User(Document): | |||||
self.email_new_password(new_password) | self.email_new_password(new_password) | ||||
except frappe.OutgoingEmailError: | except frappe.OutgoingEmailError: | ||||
print(frappe.get_traceback()) | |||||
pass # email server not set, don't send email | |||||
# email server not set, don't send email | |||||
frappe.log_error(frappe.get_traceback()) | |||||
@Document.hook | @Document.hook | ||||
def validate_reset_password(self): | def validate_reset_password(self): | ||||
@@ -7,6 +7,7 @@ | |||||
"document_type": "Setup", | "document_type": "Setup", | ||||
"engine": "InnoDB", | "engine": "InnoDB", | ||||
"field_order": [ | "field_order": [ | ||||
"is_system_generated", | |||||
"dt", | "dt", | ||||
"module", | "module", | ||||
"label", | "label", | ||||
@@ -425,13 +426,20 @@ | |||||
"fieldtype": "Link", | "fieldtype": "Link", | ||||
"label": "Module (for export)", | "label": "Module (for export)", | ||||
"options": "Module Def" | "options": "Module Def" | ||||
}, | |||||
{ | |||||
"default": "0", | |||||
"fieldname": "is_system_generated", | |||||
"fieldtype": "Check", | |||||
"label": "Is System Generated", | |||||
"read_only": 1 | |||||
} | } | ||||
], | ], | ||||
"icon": "fa fa-glass", | "icon": "fa fa-glass", | ||||
"idx": 1, | "idx": 1, | ||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2022-02-14 15:42:21.885999", | |||||
"modified": "2022-02-28 22:22:54.893269", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Custom", | "module": "Custom", | ||||
"name": "Custom Field", | "name": "Custom Field", | ||||
@@ -119,7 +119,7 @@ def create_custom_field_if_values_exist(doctype, df): | |||||
frappe.db.count(dt=doctype, filters=IfNull(df.fieldname, "") != ""): | frappe.db.count(dt=doctype, filters=IfNull(df.fieldname, "") != ""): | ||||
create_custom_field(doctype, df) | create_custom_field(doctype, df) | ||||
def create_custom_field(doctype, df, ignore_validate=False): | |||||
def create_custom_field(doctype, df, ignore_validate=False, is_system_generated=True): | |||||
df = frappe._dict(df) | df = frappe._dict(df) | ||||
if not df.fieldname and df.label: | if not df.fieldname and df.label: | ||||
df.fieldname = frappe.scrub(df.label) | df.fieldname = frappe.scrub(df.label) | ||||
@@ -130,8 +130,7 @@ def create_custom_field(doctype, df, ignore_validate=False): | |||||
"permlevel": 0, | "permlevel": 0, | ||||
"fieldtype": 'Data', | "fieldtype": 'Data', | ||||
"hidden": 0, | "hidden": 0, | ||||
# Looks like we always use this programatically? | |||||
# "is_standard": 1 | |||||
"is_system_generated": is_system_generated | |||||
}) | }) | ||||
custom_field.update(df) | custom_field.update(df) | ||||
custom_field.flags.ignore_validate = ignore_validate | custom_field.flags.ignore_validate = ignore_validate | ||||
@@ -242,7 +242,8 @@ frappe.ui.form.on("Customize Form Field", { | |||||
}, | }, | ||||
fields_add: function(frm, cdt, cdn) { | fields_add: function(frm, cdt, cdn) { | ||||
var f = frappe.model.get_doc(cdt, cdn); | var f = frappe.model.get_doc(cdt, cdn); | ||||
f.is_custom_field = 1; | |||||
f.is_system_generated = false; | |||||
f.is_custom_field = true; | |||||
} | } | ||||
}); | }); | ||||
@@ -402,7 +402,7 @@ class CustomizeForm(Document): | |||||
"property": prop, | "property": prop, | ||||
"value": value, | "value": value, | ||||
"property_type": property_type | "property_type": property_type | ||||
}) | |||||
}, is_system_generated=False) | |||||
def get_existing_property_value(self, property_name, fieldname=None): | def get_existing_property_value(self, property_name, fieldname=None): | ||||
# check if there is any need to make property setter! | # check if there is any need to make property setter! | ||||
@@ -487,12 +487,21 @@ def reset_customization(doctype): | |||||
setters = frappe.get_all("Property Setter", filters={ | setters = frappe.get_all("Property Setter", filters={ | ||||
'doc_type': doctype, | 'doc_type': doctype, | ||||
'field_name': ['!=', 'naming_series'], | 'field_name': ['!=', 'naming_series'], | ||||
'property': ['!=', 'options'] | |||||
'property': ['!=', 'options'], | |||||
'is_system_generated': False | |||||
}, pluck='name') | }, pluck='name') | ||||
for setter in setters: | for setter in setters: | ||||
frappe.delete_doc("Property Setter", setter) | frappe.delete_doc("Property Setter", setter) | ||||
custom_fields = frappe.get_all("Custom Field", filters={ | |||||
'dt': doctype, | |||||
'is_system_generated': False | |||||
}, pluck='name') | |||||
for field in custom_fields: | |||||
frappe.delete_doc("Custom Field", field) | |||||
frappe.clear_cache(doctype=doctype) | frappe.clear_cache(doctype=doctype) | ||||
doctype_properties = { | doctype_properties = { | ||||
@@ -6,6 +6,7 @@ | |||||
"document_type": "Setup", | "document_type": "Setup", | ||||
"engine": "InnoDB", | "engine": "InnoDB", | ||||
"field_order": [ | "field_order": [ | ||||
"is_system_generated", | |||||
"help", | "help", | ||||
"sb0", | "sb0", | ||||
"doctype_or_field", | "doctype_or_field", | ||||
@@ -103,13 +104,20 @@ | |||||
{ | { | ||||
"fieldname": "section_break_9", | "fieldname": "section_break_9", | ||||
"fieldtype": "Section Break" | "fieldtype": "Section Break" | ||||
}, | |||||
{ | |||||
"default": "0", | |||||
"fieldname": "is_system_generated", | |||||
"fieldtype": "Check", | |||||
"label": "Is System Generated", | |||||
"read_only": 1 | |||||
} | } | ||||
], | ], | ||||
"icon": "fa fa-glass", | "icon": "fa fa-glass", | ||||
"idx": 1, | "idx": 1, | ||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2021-12-14 14:15:41.929071", | |||||
"modified": "2022-02-28 22:24:12.377693", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Custom", | "module": "Custom", | ||||
"name": "Property Setter", | "name": "Property Setter", | ||||
@@ -10,7 +10,7 @@ import re | |||||
import string | import string | ||||
from contextlib import contextmanager | from contextlib import contextmanager | ||||
from time import time | from time import time | ||||
from typing import Dict, List, Tuple, Union | |||||
from typing import Dict, List, Optional, Tuple, Union | |||||
from pypika.terms import Criterion, NullValue, PseudoColumn | from pypika.terms import Criterion, NullValue, PseudoColumn | ||||
@@ -119,6 +119,9 @@ class Database(object): | |||||
if not run: | if not run: | ||||
return query | return query | ||||
# remove \n \t from start and end of query | |||||
query = re.sub(r'^\s*|\s*$', '', query) | |||||
if re.search(r'ifnull\(', query, flags=re.IGNORECASE): | if re.search(r'ifnull\(', query, flags=re.IGNORECASE): | ||||
# replaces ifnull in query with coalesce | # replaces ifnull in query with coalesce | ||||
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE) | query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE) | ||||
@@ -384,7 +387,7 @@ class Database(object): | |||||
""" | """ | ||||
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug, | ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug, | ||||
order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct) | |||||
order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct, limit=1) | |||||
if not run: | if not run: | ||||
return ret | return ret | ||||
@@ -393,7 +396,7 @@ class Database(object): | |||||
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False, | def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False, | ||||
debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, | debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, | ||||
run=True, pluck=False, distinct=False): | |||||
run=True, pluck=False, distinct=False, limit=None): | |||||
"""Returns multiple document properties. | """Returns multiple document properties. | ||||
:param doctype: DocType name. | :param doctype: DocType name. | ||||
@@ -423,14 +426,15 @@ class Database(object): | |||||
if isinstance(filters, list): | if isinstance(filters, list): | ||||
out = self._get_value_for_many_names( | out = self._get_value_for_many_names( | ||||
doctype, | |||||
filters, | |||||
fieldname, | |||||
order_by, | |||||
doctype=doctype, | |||||
names=filters, | |||||
field=fieldname, | |||||
order_by=order_by, | |||||
debug=debug, | debug=debug, | ||||
run=run, | run=run, | ||||
pluck=pluck, | pluck=pluck, | ||||
distinct=distinct, | distinct=distinct, | ||||
limit=limit, | |||||
) | ) | ||||
else: | else: | ||||
@@ -444,17 +448,18 @@ class Database(object): | |||||
if order_by: | if order_by: | ||||
order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by | order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by | ||||
out = self._get_values_from_table( | out = self._get_values_from_table( | ||||
fields, | |||||
filters, | |||||
doctype, | |||||
as_dict, | |||||
debug, | |||||
order_by, | |||||
update, | |||||
fields=fields, | |||||
filters=filters, | |||||
doctype=doctype, | |||||
as_dict=as_dict, | |||||
debug=debug, | |||||
order_by=order_by, | |||||
update=update, | |||||
for_update=for_update, | for_update=for_update, | ||||
run=run, | run=run, | ||||
pluck=pluck, | pluck=pluck, | ||||
distinct=distinct | |||||
distinct=distinct, | |||||
limit=limit, | |||||
) | ) | ||||
except Exception as e: | except Exception as e: | ||||
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)): | if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)): | ||||
@@ -556,7 +561,7 @@ class Database(object): | |||||
def get_list(*args, **kwargs): | def get_list(*args, **kwargs): | ||||
return frappe.get_list(*args, **kwargs) | return frappe.get_list(*args, **kwargs) | ||||
def set_single_value(self, doctype, fieldname, value, *args, **kwargs): | |||||
def set_single_value(self, doctype: str, fieldname: Union[str, Dict], value: Optional[Union[str, int]] = None, *args, **kwargs): | |||||
"""Set field value of Single DocType. | """Set field value of Single DocType. | ||||
:param doctype: DocType of the single object | :param doctype: DocType of the single object | ||||
@@ -623,6 +628,7 @@ class Database(object): | |||||
run=True, | run=True, | ||||
pluck=False, | pluck=False, | ||||
distinct=False, | distinct=False, | ||||
limit=None, | |||||
): | ): | ||||
field_objects = [] | field_objects = [] | ||||
@@ -641,6 +647,7 @@ class Database(object): | |||||
field_objects=field_objects, | field_objects=field_objects, | ||||
fields=fields, | fields=fields, | ||||
distinct=distinct, | distinct=distinct, | ||||
limit=limit, | |||||
) | ) | ||||
if ( | if ( | ||||
fields == "*" | fields == "*" | ||||
@@ -654,7 +661,7 @@ class Database(object): | |||||
) | ) | ||||
return r | return r | ||||
def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False): | |||||
def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False, limit=None): | |||||
names = list(filter(None, names)) | names = list(filter(None, names)) | ||||
if names: | if names: | ||||
return self.get_all( | return self.get_all( | ||||
@@ -667,6 +674,7 @@ class Database(object): | |||||
as_list=1, | as_list=1, | ||||
run=run, | run=run, | ||||
distinct=distinct, | distinct=distinct, | ||||
limit_page_length=limit | |||||
) | ) | ||||
else: | else: | ||||
return {} | return {} | ||||
@@ -882,27 +890,39 @@ class Database(object): | |||||
return self.sql("select name from `tab{doctype}` limit 1".format(doctype=doctype)) | return self.sql("select name from `tab{doctype}` limit 1".format(doctype=doctype)) | ||||
def exists(self, dt, dn=None, cache=False): | def exists(self, dt, dn=None, cache=False): | ||||
"""Returns true if document exists. | |||||
"""Return the document name of a matching document, or None. | |||||
:param dt: DocType name. | |||||
:param dn: Document name or filter dict.""" | |||||
if isinstance(dt, str): | |||||
if dt!="DocType" and dt==dn: | |||||
return True # single always exists (!) | |||||
try: | |||||
return self.get_value(dt, dn, "name", cache=cache) | |||||
except Exception: | |||||
return None | |||||
Note: `cache` only works if `dt` and `dn` are of type `str`. | |||||
elif isinstance(dt, dict) and dt.get('doctype'): | |||||
try: | |||||
conditions = [] | |||||
for d in dt: | |||||
if d == 'doctype': continue | |||||
conditions.append([d, '=', dt[d]]) | |||||
return self.get_all(dt['doctype'], filters=conditions, as_list=1) | |||||
except Exception: | |||||
return None | |||||
## Examples | |||||
Pass doctype and docname (only in this case we can cache the result) | |||||
``` | |||||
exists("User", "jane@example.org", cache=True) | |||||
``` | |||||
Pass a dict of filters including the `"doctype"` key: | |||||
``` | |||||
exists({"doctype": "User", "full_name": "Jane Doe"}) | |||||
``` | |||||
Pass the doctype and a dict of filters: | |||||
``` | |||||
exists("User", {"full_name": "Jane Doe"}) | |||||
``` | |||||
""" | |||||
if dt != "DocType" and dt == dn: | |||||
# single always exists (!) | |||||
return dn | |||||
if isinstance(dt, dict): | |||||
dt = dt.copy() # don't modify the original dict | |||||
dt, dn = dt.pop("doctype"), dt | |||||
return self.get_value(dt, dn, ignore=True, cache=cache) | |||||
def count(self, dt, filters=None, debug=False, cache=False): | def count(self, dt, filters=None, debug=False, cache=False): | ||||
"""Returns `COUNT(*)` for given DocType and filters.""" | """Returns `COUNT(*)` for given DocType and filters.""" | ||||
@@ -72,7 +72,8 @@ class ToDo(Document): | |||||
assignments = frappe.get_all("ToDo", filters={ | assignments = frappe.get_all("ToDo", filters={ | ||||
"reference_type": self.reference_type, | "reference_type": self.reference_type, | ||||
"reference_name": self.reference_name, | "reference_name": self.reference_name, | ||||
"status": ("!=", "Cancelled") | |||||
"status": ("!=", "Cancelled"), | |||||
"allocated_to": ("is", "set") | |||||
}, pluck="allocated_to") | }, pluck="allocated_to") | ||||
assignments.reverse() | assignments.reverse() | ||||
@@ -312,6 +312,7 @@ def get_assignments(dt, dn): | |||||
'reference_type': dt, | 'reference_type': dt, | ||||
'reference_name': dn, | 'reference_name': dn, | ||||
'status': ('!=', 'Cancelled'), | 'status': ('!=', 'Cancelled'), | ||||
'allocated_to': ("is", "set") | |||||
}) | }) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@@ -9,7 +9,6 @@ from frappe import _, is_whitelisted | |||||
import re | import re | ||||
import wrapt | import wrapt | ||||
UNTRANSLATED_DOCTYPES = ["DocType", "Role"] | |||||
def sanitize_searchfield(searchfield): | def sanitize_searchfield(searchfield): | ||||
blacklisted_keywords = ['select', 'delete', 'drop', 'update', 'case', 'and', 'or', 'like'] | blacklisted_keywords = ['select', 'delete', 'drop', 'update', 'case', 'and', 'or', 'like'] | ||||
@@ -114,6 +113,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||||
or_filters = [] | or_filters = [] | ||||
translated_search_doctypes = frappe.get_hooks("translated_search_doctypes") | |||||
# build from doctype | # build from doctype | ||||
if txt: | if txt: | ||||
search_fields = ["name"] | search_fields = ["name"] | ||||
@@ -125,7 +125,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||||
for f in search_fields: | for f in search_fields: | ||||
fmeta = meta.get_field(f.strip()) | fmeta = meta.get_field(f.strip()) | ||||
if (doctype not in UNTRANSLATED_DOCTYPES) and (f == "name" or (fmeta and fmeta.fieldtype in ["Data", "Text", "Small Text", "Long Text", | |||||
if (doctype not in translated_search_doctypes) and (f == "name" or (fmeta and fmeta.fieldtype in ["Data", "Text", "Small Text", "Long Text", | |||||
"Link", "Select", "Read Only", "Text Editor"])): | "Link", "Select", "Read Only", "Text Editor"])): | ||||
or_filters.append([doctype, f.strip(), "like", "%{0}%".format(txt)]) | or_filters.append([doctype, f.strip(), "like", "%{0}%".format(txt)]) | ||||
@@ -160,7 +160,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||||
ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read' | ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read' | ||||
ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype)) | ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype)) | ||||
if doctype in UNTRANSLATED_DOCTYPES: | |||||
if doctype in translated_search_doctypes: | |||||
page_length = None | page_length = None | ||||
values = frappe.get_list(doctype, | values = frappe.get_list(doctype, | ||||
@@ -175,7 +175,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||||
as_list=not as_dict, | as_list=not as_dict, | ||||
strict=False) | strict=False) | ||||
if doctype in UNTRANSLATED_DOCTYPES: | |||||
if doctype in translated_search_doctypes: | |||||
# Filtering the values array so that query is included in very element | # Filtering the values array so that query is included in very element | ||||
values = ( | values = ( | ||||
v for v in values | v for v in values | ||||
@@ -15,8 +15,6 @@ from frappe.utils.csvutils import to_csv | |||||
from frappe.utils.xlsxutils import make_xlsx | from frappe.utils.xlsxutils import make_xlsx | ||||
from frappe.desk.query_report import build_xlsx_data | from frappe.desk.query_report import build_xlsx_data | ||||
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3 | |||||
class AutoEmailReport(Document): | class AutoEmailReport(Document): | ||||
def autoname(self): | def autoname(self): | ||||
@@ -46,6 +44,8 @@ class AutoEmailReport(Document): | |||||
def validate_report_count(self): | def validate_report_count(self): | ||||
'''check that there are only 3 enabled reports per user''' | '''check that there are only 3 enabled reports per user''' | ||||
count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0] | count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0] | ||||
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3 | |||||
if count > max_reports_per_user + (-1 if self.flags.in_insert else 0): | if count > max_reports_per_user + (-1 if self.flags.in_insert else 0): | ||||
frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user)) | frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user)) | ||||
@@ -111,7 +111,6 @@ class EmailQueue(Document): | |||||
""" Send emails to recipients. | """ Send emails to recipients. | ||||
""" | """ | ||||
if not self.can_send_now(): | if not self.can_send_now(): | ||||
frappe.db.rollback() | |||||
return | return | ||||
with SendMailContext(self, is_background_task) as ctx: | with SendMailContext(self, is_background_task) as ctx: | ||||
@@ -240,7 +240,7 @@ class TestNotification(unittest.TestCase): | |||||
self.assertTrue(email_queue) | self.assertTrue(email_queue) | ||||
# check if description is changed after alert since set_property_after_alert is set | # check if description is changed after alert since set_property_after_alert is set | ||||
self.assertEquals(todo.description, 'Changed by Notification') | |||||
self.assertEqual(todo.description, 'Changed by Notification') | |||||
recipients = [d.recipient for d in email_queue.recipients] | recipients = [d.recipient for d in email_queue.recipients] | ||||
self.assertTrue('test2@example.com' in recipients) | self.assertTrue('test2@example.com' in recipients) | ||||
@@ -1,10 +1,12 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from frappe import msgprint, _ | from frappe import msgprint, _ | ||||
from frappe.utils.verified_command import get_signed_params, verify_request | from frappe.utils.verified_command import get_signed_params, verify_request | ||||
from frappe.utils import get_url, now_datetime, cint | from frappe.utils import get_url, now_datetime, cint | ||||
from frappe.query_builder import DocType, Interval | |||||
from frappe.query_builder.functions import Now | |||||
def get_emails_sent_this_month(email_account=None): | def get_emails_sent_this_month(email_account=None): | ||||
"""Get count of emails sent from a specific email account. | """Get count of emails sent from a specific email account. | ||||
@@ -162,15 +164,16 @@ def get_queue(): | |||||
by priority desc, creation asc | by priority desc, creation asc | ||||
limit 500''', { 'now': now_datetime() }, as_dict=True) | limit 500''', { 'now': now_datetime() }, as_dict=True) | ||||
def clear_outbox(days=None): | |||||
def clear_outbox(days: int = None) -> None: | |||||
"""Remove low priority older than 31 days in Outbox or configured in Log Settings. | """Remove low priority older than 31 days in Outbox or configured in Log Settings. | ||||
Note: Used separate query to avoid deadlock | Note: Used separate query to avoid deadlock | ||||
""" | """ | ||||
if not days: | |||||
days=31 | |||||
days = days or 31 | |||||
email_queue = DocType("Email Queue") | |||||
email_queues = frappe.db.sql_list("""SELECT `name` FROM `tabEmail Queue` | |||||
WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '{0}' DAY)""".format(days)) | |||||
email_queues = frappe.qb.from_(email_queue).select(email_queue.name).where( | |||||
email_queue.modified < (Now() - Interval(days=days)) | |||||
).run(pluck=True) | |||||
if email_queues: | if email_queues: | ||||
frappe.db.delete("Email Queue", {"name": ("in", email_queues)}) | frappe.db.delete("Email Queue", {"name": ("in", email_queues)}) | ||||
@@ -110,3 +110,6 @@ class InvalidAuthorizationPrefix(CSRFTokenError): pass | |||||
class InvalidAuthorizationToken(CSRFTokenError): pass | class InvalidAuthorizationToken(CSRFTokenError): pass | ||||
class InvalidDatabaseFile(ValidationError): pass | class InvalidDatabaseFile(ValidationError): pass | ||||
class ExecutableNotFound(FileNotFoundError): pass | class ExecutableNotFound(FileNotFoundError): pass | ||||
class InvalidRemoteException(Exception): | |||||
pass |
@@ -225,11 +225,10 @@ def ping(): | |||||
def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): | def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): | ||||
"""run a whitelisted controller method""" | """run a whitelisted controller method""" | ||||
import json | |||||
import inspect | |||||
from inspect import getfullargspec | |||||
if not args: | |||||
args = arg or "" | |||||
if not args and arg: | |||||
args = arg | |||||
if dt: # not called from a doctype (from a page) | if dt: # not called from a doctype (from a page) | ||||
if not dn: | if not dn: | ||||
@@ -237,9 +236,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): | |||||
doc = frappe.get_doc(dt, dn) | doc = frappe.get_doc(dt, dn) | ||||
else: | else: | ||||
if isinstance(docs, str): | |||||
docs = json.loads(docs) | |||||
docs = frappe.parse_json(docs) | |||||
doc = frappe.get_doc(docs) | doc = frappe.get_doc(docs) | ||||
doc._original_modified = doc.modified | doc._original_modified = doc.modified | ||||
doc.check_if_latest() | doc.check_if_latest() | ||||
@@ -248,16 +245,16 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): | |||||
throw_permission_error() | throw_permission_error() | ||||
try: | try: | ||||
args = json.loads(args) | |||||
args = frappe.parse_json(args) | |||||
except ValueError: | except ValueError: | ||||
args = args | |||||
pass | |||||
method_obj = getattr(doc, method) | method_obj = getattr(doc, method) | ||||
fn = getattr(method_obj, '__func__', method_obj) | fn = getattr(method_obj, '__func__', method_obj) | ||||
is_whitelisted(fn) | is_whitelisted(fn) | ||||
is_valid_http_method(fn) | is_valid_http_method(fn) | ||||
fnargs = inspect.getfullargspec(method_obj).args | |||||
fnargs = getfullargspec(method_obj).args | |||||
if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"): | if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"): | ||||
response = doc.run_method(method) | response = doc.run_method(method) | ||||
@@ -383,3 +383,5 @@ global_search_doctypes = { | |||||
{"doctype": "Web Form"} | {"doctype": "Web Form"} | ||||
] | ] | ||||
} | } | ||||
translated_search_doctypes = ["DocType", "Role", "Country", "Gender", "Salutation"] |
@@ -5,10 +5,11 @@ import json | |||||
import os | import os | ||||
import sys | import sys | ||||
from collections import OrderedDict | from collections import OrderedDict | ||||
from typing import List, Dict | |||||
from typing import List, Dict, Tuple | |||||
import frappe | import frappe | ||||
from frappe.defaults import _clear_cache | from frappe.defaults import _clear_cache | ||||
from frappe.utils import is_git_url | |||||
def _new_site( | def _new_site( | ||||
@@ -34,7 +35,6 @@ def _new_site( | |||||
from frappe.commands.scheduler import _is_scheduler_enabled | from frappe.commands.scheduler import _is_scheduler_enabled | ||||
from frappe.utils import get_site_path, scheduler, touch_file | from frappe.utils import get_site_path, scheduler, touch_file | ||||
if not force and os.path.exists(site): | if not force and os.path.exists(site): | ||||
print("Site {0} already exists".format(site)) | print("Site {0} already exists".format(site)) | ||||
sys.exit(1) | sys.exit(1) | ||||
@@ -124,6 +124,86 @@ def install_db(root_login=None, root_password=None, db_name=None, source_sql=Non | |||||
frappe.flags.in_install_db = False | frappe.flags.in_install_db = False | ||||
def find_org(org_repo: str) -> Tuple[str, str]: | |||||
""" find the org a repo is in | |||||
find_org() | |||||
ref -> https://github.com/frappe/bench/blob/develop/bench/utils/__init__.py#L390 | |||||
:param org_repo: | |||||
:type org_repo: str | |||||
:raises InvalidRemoteException: if the org is not found | |||||
:return: organisation and repository | |||||
:rtype: Tuple[str, str] | |||||
""" | |||||
from frappe.exceptions import InvalidRemoteException | |||||
import requests | |||||
for org in ["frappe", "erpnext"]: | |||||
res = requests.head(f"https://api.github.com/repos/{org}/{org_repo}") | |||||
if res.ok: | |||||
return org, org_repo | |||||
raise InvalidRemoteException | |||||
def fetch_details_from_tag(_tag: str) -> Tuple[str, str, str]: | |||||
""" parse org, repo, tag from string | |||||
fetch_details_from_tag() | |||||
ref -> https://github.com/frappe/bench/blob/develop/bench/utils/__init__.py#L403 | |||||
:param _tag: input string | |||||
:type _tag: str | |||||
:return: organisation, repostitory, tag | |||||
:rtype: Tuple[str, str, str] | |||||
""" | |||||
app_tag = _tag.split("@") | |||||
org_repo = app_tag[0].split("/") | |||||
try: | |||||
repo, tag = app_tag | |||||
except ValueError: | |||||
repo, tag = app_tag + [None] | |||||
try: | |||||
org, repo = org_repo | |||||
except Exception: | |||||
org, repo = find_org(org_repo[0]) | |||||
return org, repo, tag | |||||
def parse_app_name(name: str) -> str: | |||||
"""parse repo name from name | |||||
__setup_details_from_git() | |||||
ref -> https://github.com/frappe/bench/blob/develop/bench/app.py#L114 | |||||
:param name: git tag | |||||
:type name: str | |||||
:return: repository name | |||||
:rtype: str | |||||
""" | |||||
name = name.rstrip("/") | |||||
if os.path.exists(name): | |||||
repo = os.path.split(name)[-1] | |||||
elif is_git_url(name): | |||||
if name.startswith("git@") or name.startswith("ssh://"): | |||||
_repo = name.split(":")[1].rsplit("/", 1)[1] | |||||
else: | |||||
_repo = name.rsplit("/", 2)[2] | |||||
repo = _repo.split(".")[0] | |||||
else: | |||||
_, repo, _ = fetch_details_from_tag(name) | |||||
return repo | |||||
def install_app(name, verbose=False, set_as_patched=True): | def install_app(name, verbose=False, set_as_patched=True): | ||||
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs | from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs | ||||
from frappe.model.sync import sync_for | from frappe.model.sync import sync_for | ||||
@@ -140,7 +220,8 @@ def install_app(name, verbose=False, set_as_patched=True): | |||||
# install pre-requisites | # install pre-requisites | ||||
if app_hooks.required_apps: | if app_hooks.required_apps: | ||||
for app in app_hooks.required_apps: | for app in app_hooks.required_apps: | ||||
install_app(app, verbose=verbose) | |||||
name = parse_app_name(name) | |||||
install_app(name, verbose=verbose) | |||||
frappe.flags.in_install = name | frappe.flags.in_install = name | ||||
frappe.clear_cache() | frappe.clear_cache() | ||||
@@ -963,10 +963,13 @@ class BaseDocument(object): | |||||
from frappe.model.meta import get_default_df | from frappe.model.meta import get_default_df | ||||
df = get_default_df(fieldname) | df = get_default_df(fieldname) | ||||
if not currency and df: | |||||
currency = self.get(df.get("options")) | |||||
if not frappe.db.exists('Currency', currency, cache=True): | |||||
currency = None | |||||
if ( | |||||
df.fieldtype == "Currency" | |||||
and not currency | |||||
and (currency_field := df.get("options")) | |||||
and (currency_value := self.get(currency_field)) | |||||
): | |||||
currency = frappe.db.get_value('Currency', currency_value, cache=True) | |||||
val = self.get(fieldname) | val = self.get(fieldname) | ||||
@@ -197,4 +197,5 @@ frappe.patches.v14_0.copy_mail_data #08.03.21 | |||||
frappe.patches.v14_0.update_github_endpoints #08-11-2021 | frappe.patches.v14_0.update_github_endpoints #08-11-2021 | ||||
frappe.patches.v14_0.remove_db_aggregation | frappe.patches.v14_0.remove_db_aggregation | ||||
frappe.patches.v14_0.update_color_names_in_kanban_board_column | frappe.patches.v14_0.update_color_names_in_kanban_board_column | ||||
frappe.patches.v14_0.update_is_system_generated_flag | |||||
frappe.patches.v14_0.update_auto_account_deletion_duration | frappe.patches.v14_0.update_auto_account_deletion_duration |
@@ -0,0 +1,17 @@ | |||||
import frappe | |||||
def execute(): | |||||
# assuming all customization generated by Admin is system generated customization | |||||
custom_field = frappe.qb.DocType("Custom Field") | |||||
( | |||||
frappe.qb.update(custom_field) | |||||
.set(custom_field.is_system_generated, True) | |||||
.where(custom_field.owner == 'Administrator').run() | |||||
) | |||||
property_setter = frappe.qb.DocType("Property Setter") | |||||
( | |||||
frappe.qb.update(property_setter) | |||||
.set(property_setter.is_system_generated, True) | |||||
.where(property_setter.owner == 'Administrator').run() | |||||
) |
@@ -594,4 +594,4 @@ def is_parent_valid(child_doctype, parent_doctype): | |||||
from frappe.core.utils import find | from frappe.core.utils import find | ||||
parent_meta = frappe.get_meta(parent_doctype) | parent_meta = frappe.get_meta(parent_doctype) | ||||
child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype) | child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype) | ||||
return not parent_meta.istable and child_table_field_exists | |||||
return not parent_meta.istable and child_table_field_exists |
@@ -526,11 +526,13 @@ export default { | |||||
error: true | error: true | ||||
}); | }); | ||||
capture.show(); | capture.show(); | ||||
capture.submit(data_url => { | |||||
let filename = `capture_${frappe.datetime.now_datetime().replaceAll(/[: -]/g, '_')}.png`; | |||||
this.url_to_file(data_url, filename, 'image/png').then((file) => | |||||
this.add_files([file]) | |||||
); | |||||
capture.submit(data_urls => { | |||||
data_urls.forEach(data_url => { | |||||
let filename = `capture_${frappe.datetime.now_datetime().replaceAll(/[: -]/g, '_')}.png`; | |||||
this.url_to_file(data_url, filename, 'image/png').then((file) => | |||||
this.add_files([file]) | |||||
); | |||||
}); | |||||
}); | }); | ||||
}, | }, | ||||
show_google_drive_picker() { | show_google_drive_picker() { | ||||
@@ -37,8 +37,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro | |||||
if(this.frm) { | if(this.frm) { | ||||
me.parse_validate_and_set_in_model(null); | me.parse_validate_and_set_in_model(null); | ||||
me.refresh(); | me.refresh(); | ||||
me.frm.attachments.remove_attachment_by_filename(me.value, function() { | |||||
me.parse_validate_and_set_in_model(null); | |||||
me.frm.attachments.remove_attachment_by_filename(me.value, async () => { | |||||
await me.parse_validate_and_set_in_model(null); | |||||
me.refresh(); | me.refresh(); | ||||
me.frm.doc.docstatus == 1 ? me.frm.save('Update') : me.frm.save(); | me.frm.doc.docstatus == 1 ? me.frm.save('Update') : me.frm.save(); | ||||
}); | }); | ||||
@@ -110,9 +110,9 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro | |||||
return this.value || null; | return this.value || null; | ||||
} | } | ||||
on_upload_complete(attachment) { | |||||
async on_upload_complete(attachment) { | |||||
if(this.frm) { | if(this.frm) { | ||||
this.parse_validate_and_set_in_model(attachment.file_url); | |||||
await this.parse_validate_and_set_in_model(attachment.file_url); | |||||
this.frm.attachments.update_attachment(attachment); | this.frm.attachments.update_attachment(attachment); | ||||
this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save(); | this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save(); | ||||
} | } | ||||
@@ -454,7 +454,10 @@ class FormTimeline extends BaseTimeline { | |||||
let edit_box = this.make_editable(edit_wrapper); | let edit_box = this.make_editable(edit_wrapper); | ||||
let content_wrapper = comment_wrapper.find('.content'); | let content_wrapper = comment_wrapper.find('.content'); | ||||
let more_actions_wrapper = comment_wrapper.find('.more-actions'); | let more_actions_wrapper = comment_wrapper.find('.more-actions'); | ||||
if (frappe.model.can_delete("Comment")) { | |||||
if (frappe.model.can_delete("Comment") && ( | |||||
frappe.session.user == doc.owner || | |||||
frappe.user.has_role("System Manager") | |||||
)) { | |||||
const delete_option = $(` | const delete_option = $(` | ||||
<li> | <li> | ||||
<a class="dropdown-item"> | <a class="dropdown-item"> | ||||
@@ -375,7 +375,7 @@ export default class ListSettings { | |||||
let me = this; | let me = this; | ||||
if (me.removed_fields) { | if (me.removed_fields) { | ||||
me.removed_fields.concat(fields); | |||||
me.removed_fields = me.removed_fields.concat(fields); | |||||
} else { | } else { | ||||
me.removed_fields = fields; | me.removed_fields = fields; | ||||
} | } | ||||
@@ -28,6 +28,24 @@ frappe._.get_data_uri = element => { | |||||
return data_uri; | return data_uri; | ||||
}; | }; | ||||
function get_file_input() { | |||||
let input = document.createElement("input"); | |||||
input.setAttribute("type", "file"); | |||||
input.setAttribute("accept", "image/*"); | |||||
input.setAttribute("multiple", ""); | |||||
return input; | |||||
} | |||||
function read(file) { | |||||
return new Promise((resolve, reject) => { | |||||
const reader = new FileReader(); | |||||
reader.onload = () => resolve(reader.result); | |||||
reader.onerror = reject; | |||||
reader.readAsDataURL(file); | |||||
}); | |||||
} | |||||
/** | /** | ||||
* @description Frappe's Capture object. | * @description Frappe's Capture object. | ||||
* | * | ||||
@@ -45,6 +63,9 @@ frappe.ui.Capture = class { | |||||
constructor(options = {}) { | constructor(options = {}) { | ||||
this.options = frappe.ui.Capture.OPTIONS; | this.options = frappe.ui.Capture.OPTIONS; | ||||
this.set_options(options); | this.set_options(options); | ||||
this.facing_mode = "environment"; | |||||
this.images = []; | |||||
} | } | ||||
set_options(options) { | set_options(options) { | ||||
@@ -53,74 +74,229 @@ frappe.ui.Capture = class { | |||||
return this; | return this; | ||||
} | } | ||||
render() { | |||||
return navigator.mediaDevices.getUserMedia({ video: true }).then(stream => { | |||||
this.stream = stream; | |||||
show() { | |||||
this.build_dialog(); | |||||
this.dialog = new frappe.ui.Dialog({ | |||||
title: this.options.title, | |||||
animate: this.options.animate, | |||||
on_hide: () => this.stop_media_stream() | |||||
}); | |||||
if (frappe.is_mobile()) { | |||||
this.show_for_mobile(); | |||||
} else { | |||||
this.show_for_desktop(); | |||||
} | |||||
} | |||||
this.dialog.get_close_btn().on('click', () => { | |||||
this.hide(); | |||||
}); | |||||
build_dialog() { | |||||
let me = this; | |||||
me.dialog = new frappe.ui.Dialog({ | |||||
title: this.options.title, | |||||
animate: this.options.animate, | |||||
fields: [ | |||||
{ | |||||
fieldtype: "HTML", | |||||
fieldname: "capture" | |||||
}, | |||||
{ | |||||
fieldtype: "HTML", | |||||
fieldname: "total_count" | |||||
} | |||||
], | |||||
on_hide: this.stop_media_stream() | |||||
}); | |||||
const set_take_photo_action = () => { | |||||
this.dialog.set_primary_action(__('Take Photo'), () => { | |||||
const data_url = frappe._.get_data_uri(video); | |||||
$e.find('.fc-p').attr('src', data_url); | |||||
me.$template = $(frappe.ui.Capture.TEMPLATE); | |||||
$e.find('.fc-s').hide(); | |||||
$e.find('.fc-p').show(); | |||||
let field = me.dialog.get_field("capture"); | |||||
$(field.wrapper).html(me.$template); | |||||
this.dialog.set_secondary_action_label(__('Retake')); | |||||
this.dialog.get_secondary_btn().show(); | |||||
me.dialog.get_close_btn().on('click', () => { | |||||
me.hide(); | |||||
}); | |||||
} | |||||
this.dialog.set_primary_action(__('Submit'), () => { | |||||
this.hide(); | |||||
if (this.callback) this.callback(data_url); | |||||
}); | |||||
}); | |||||
}; | |||||
show_for_mobile() { | |||||
let me = this; | |||||
if (!me.input) { | |||||
me.input = get_file_input(); | |||||
} | |||||
set_take_photo_action(); | |||||
me.input.onchange = async () => { | |||||
for (let file of me.input.files) { | |||||
let f = await read(file); | |||||
me.images.push(f); | |||||
} | |||||
this.dialog.set_secondary_action(() => { | |||||
$e.find('.fc-p').hide(); | |||||
$e.find('.fc-s').show(); | |||||
me.render_preview(); | |||||
me.dialog.show(); | |||||
}; | |||||
me.input.click(); | |||||
} | |||||
show_for_desktop() { | |||||
let me = this; | |||||
this.dialog.get_secondary_btn().hide(); | |||||
this.dialog.get_primary_btn().off('click'); | |||||
set_take_photo_action(); | |||||
this.render_stream() | |||||
.then(() => { | |||||
me.dialog.show(); | |||||
}) | |||||
.catch(err => { | |||||
if (me.options.error) { | |||||
frappe.show_alert(frappe.ui.Capture.ERR_MESSAGE, 3); | |||||
} | |||||
throw err; | |||||
}); | }); | ||||
} | |||||
render_stream() { | |||||
let me = this; | |||||
let constraints = { | |||||
video: { | |||||
facingMode: this.facing_mode | |||||
} | |||||
}; | |||||
this.dialog.get_secondary_btn().hide(); | |||||
return navigator.mediaDevices.getUserMedia(constraints).then(stream => { | |||||
me.stream = stream; | |||||
me.dialog.custom_actions.empty(); | |||||
me.dialog.get_primary_btn().off('click'); | |||||
me.setup_take_photo_action(); | |||||
me.setup_preview_action(); | |||||
me.setup_toggle_camera(); | |||||
me.$template.find('.fc-stream-container').show(); | |||||
me.$template.find('.fc-preview-container').hide(); | |||||
me.video = me.$template.find('video')[0]; | |||||
me.video.srcObject = me.stream; | |||||
me.video.load(); | |||||
me.video.play(); | |||||
}); | |||||
} | |||||
const $e = $(frappe.ui.Capture.TEMPLATE); | |||||
render_preview() { | |||||
this.stop_media_stream(); | |||||
this.$template.find('.fc-stream-container').hide(); | |||||
this.$template.find('.fc-preview-container').show(); | |||||
this.dialog.get_primary_btn().off('click'); | |||||
const video = $e.find('video')[0]; | |||||
video.srcObject = this.stream; | |||||
video.play(); | |||||
const $container = $(this.dialog.body); | |||||
let images = ``; | |||||
$container.html($e); | |||||
this.images.forEach((image, idx) => { | |||||
images += ` | |||||
<div class="mt-1 p-1 rounded col-md-3 col-sm-4 col-xs-4" data-idx="${idx}"> | |||||
<span class="capture-remove-btn" data-idx="${idx}"> | |||||
${frappe.utils.icon("close", "lg")} | |||||
</span> | |||||
<img class="rounded" src="${image}" data-idx="${idx}"> | |||||
</div> | |||||
`; | |||||
}); | }); | ||||
this.$template.find('.fc-preview-container').empty(); | |||||
$(this.$template.find('.fc-preview-container')).html( | |||||
`<div class="row"> | |||||
${images} | |||||
</div>` | |||||
); | |||||
this.setup_capture_action(); | |||||
this.setup_submit_action(); | |||||
this.setup_remove_action(); | |||||
this.update_count(); | |||||
this.dialog.custom_actions.empty(); | |||||
} | } | ||||
show() { | |||||
this.render() | |||||
.then(() => { | |||||
this.dialog.show(); | |||||
}) | |||||
.catch(err => { | |||||
if (this.options.error) { | |||||
frappe.show_alert(frappe.ui.Capture.ERR_MESSAGE, 3); | |||||
} | |||||
setup_take_photo_action() { | |||||
let me = this; | |||||
throw err; | |||||
this.dialog.set_primary_action(__('Take Photo'), () => { | |||||
const data_url = frappe._.get_data_uri(me.video); | |||||
me.images.push(data_url); | |||||
me.setup_preview_action(); | |||||
me.update_count(); | |||||
}); | |||||
} | |||||
setup_preview_action() { | |||||
let me = this; | |||||
if (!this.images.length) { | |||||
return; | |||||
} | |||||
this.dialog.set_secondary_action_label(__("Preview")); | |||||
this.dialog.set_secondary_action(() => { | |||||
me.dialog.get_primary_btn().off('click'); | |||||
me.render_preview(); | |||||
}); | |||||
} | |||||
setup_remove_action() { | |||||
let me = this; | |||||
let elements = this.$template[0].getElementsByClassName("capture-remove-btn"); | |||||
elements.forEach(el => { | |||||
el.onclick = () => { | |||||
let idx = parseInt(el.getAttribute("data-idx")); | |||||
me.images.splice(idx, 1); | |||||
me.render_preview(); | |||||
}; | |||||
}); | |||||
} | |||||
update_count() { | |||||
let field = this.dialog.get_field("total_count"); | |||||
let msg = `${__("Total Images")}: <b>${this.images.length}`; | |||||
if (this.images.length === 0) { | |||||
msg = __("No Images"); | |||||
} | |||||
$(field.wrapper).html(` | |||||
<div class="row mt-2"> | |||||
<div class="offset-4 col-4 d-flex justify-content-center">${msg}</b></div> | |||||
</div> | |||||
`); | |||||
} | |||||
setup_toggle_camera() { | |||||
let me = this; | |||||
this.dialog.add_custom_action(__("Switch Camera"), () => { | |||||
me.facing_mode = me.facing_mode == "environment" ? "user" : "environment"; | |||||
frappe.show_alert({ | |||||
message: __("Switching Camera") | |||||
}); | }); | ||||
me.stop_media_stream(); | |||||
me.render_stream(); | |||||
}, "btn-switch"); | |||||
} | |||||
setup_capture_action() { | |||||
let me = this; | |||||
this.dialog.set_secondary_action_label(__("Capture")); | |||||
this.dialog.set_secondary_action(() => { | |||||
if (frappe.is_mobile()) { | |||||
me.show_for_mobile(); | |||||
} else { | |||||
me.render_stream(); | |||||
} | |||||
}); | |||||
} | |||||
setup_submit_action() { | |||||
let me = this; | |||||
this.dialog.set_primary_action(__('Submit'), () => { | |||||
me.hide(); | |||||
if (me.callback) { | |||||
me.callback(me.images); | |||||
} | |||||
}); | |||||
} | } | ||||
hide() { | hide() { | ||||
@@ -148,11 +324,11 @@ frappe.ui.Capture.OPTIONS = { | |||||
frappe.ui.Capture.ERR_MESSAGE = __('Unable to load camera.'); | frappe.ui.Capture.ERR_MESSAGE = __('Unable to load camera.'); | ||||
frappe.ui.Capture.TEMPLATE = ` | frappe.ui.Capture.TEMPLATE = ` | ||||
<div class="frappe-capture"> | <div class="frappe-capture"> | ||||
<div class="panel panel-default"> | |||||
<div class="embed-responsive embed-responsive-16by9"> | |||||
<img class="fc-p embed-responsive-item" style="object-fit: contain; display: none;"/> | |||||
<video class="fc-s embed-responsive-item">${frappe.ui.Capture.ERR_MESSAGE}</video> | |||||
</div> | |||||
<div class="embed-responsive embed-responsive-16by9 fc-stream-container"> | |||||
<video class="fc-stream embed-responsive-item">${frappe.ui.Capture.ERR_MESSAGE}</video> | |||||
</div> | |||||
<div class="fc-preview-container px-2" style="display: none;"> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
`; | `; |
@@ -47,13 +47,17 @@ frappe.ui.Page = class Page { | |||||
} | } | ||||
setup_scroll_handler() { | setup_scroll_handler() { | ||||
window.addEventListener('scroll', () => { | |||||
if (document.documentElement.scrollTop) { | |||||
$('.page-head').toggleClass('drop-shadow', true); | |||||
let last_scroll = 0; | |||||
window.addEventListener('scroll', frappe.utils.throttle(() => { | |||||
$('.page-head').toggleClass('drop-shadow', !!document.documentElement.scrollTop); | |||||
let current_scroll = document.documentElement.scrollTop; | |||||
if (current_scroll > 0 && last_scroll <= current_scroll) { | |||||
$('.page-head').css("top", "-15px"); | |||||
} else { | } else { | ||||
$('.page-head').removeClass('drop-shadow'); | |||||
$('.page-head').css("top", "var(--navbar-height)"); | |||||
} | } | ||||
}); | |||||
last_scroll = current_scroll; | |||||
}), 500); | |||||
} | } | ||||
get_empty_state(title, message, primary_action) { | get_empty_state(title, message, primary_action) { | ||||
@@ -231,7 +231,7 @@ Object.assign(frappe.utils, { | |||||
if (tt && (tt.substr(0, 1)===">" || tt.substr(0, 4)===">")) { | if (tt && (tt.substr(0, 1)===">" || tt.substr(0, 4)===">")) { | ||||
part.push(t); | part.push(t); | ||||
} else { | } else { | ||||
out.concat(part); | |||||
out = out.concat(part); | |||||
out.push(t); | out.push(t); | ||||
part = []; | part = []; | ||||
} | } | ||||
@@ -29,7 +29,7 @@ frappe.views.CalendarView = class CalendarView extends frappe.views.ListView { | |||||
.then(() => { | .then(() => { | ||||
this.page_title = __('{0} Calendar', [this.page_title]); | this.page_title = __('{0} Calendar', [this.page_title]); | ||||
this.calendar_settings = frappe.views.calendar[this.doctype] || {}; | this.calendar_settings = frappe.views.calendar[this.doctype] || {}; | ||||
this.calendar_name = frappe.utils.to_title_case(frappe.get_route()[3] || ''); | |||||
this.calendar_name = frappe.get_route()[3]; | |||||
}); | }); | ||||
} | } | ||||
@@ -72,12 +72,17 @@ frappe.views.CalendarView = class CalendarView extends frappe.views.ListView { | |||||
const calendar_name = this.calendar_name; | const calendar_name = this.calendar_name; | ||||
return new Promise(resolve => { | return new Promise(resolve => { | ||||
if (calendar_name === 'Default') { | |||||
if (calendar_name === 'default') { | |||||
Object.assign(options, frappe.views.calendar[this.doctype]); | Object.assign(options, frappe.views.calendar[this.doctype]); | ||||
resolve(options); | resolve(options); | ||||
} else { | } else { | ||||
frappe.model.with_doc('Calendar View', calendar_name, () => { | frappe.model.with_doc('Calendar View', calendar_name, () => { | ||||
const doc = frappe.get_doc('Calendar View', calendar_name); | const doc = frappe.get_doc('Calendar View', calendar_name); | ||||
if (!doc) { | |||||
frappe.show_alert(__("{0} is not a valid Calendar. Redirecting to default Calendar.", [calendar_name.bold()])); | |||||
frappe.set_route("List", this.doctype, "Calendar", "default"); | |||||
return; | |||||
} | |||||
Object.assign(options, { | Object.assign(options, { | ||||
field_map: { | field_map: { | ||||
id: "name", | id: "name", | ||||
@@ -1026,7 +1026,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||||
} | } | ||||
if (!docfield || docfield.report_hide) return; | if (!docfield || docfield.report_hide) return; | ||||
let title = __(docfield ? docfield.label : toTitle(fieldname)); | |||||
let title = __(docfield.label); | |||||
if (doctype !== this.doctype) { | if (doctype !== this.doctype) { | ||||
title += ` (${__(doctype)})`; | title += ` (${__(doctype)})`; | ||||
} | } | ||||
@@ -16,7 +16,8 @@ export default class WebFormList { | |||||
if (this.table) { | if (this.table) { | ||||
Array.from(this.table.tBodies).forEach(tbody => tbody.remove()); | Array.from(this.table.tBodies).forEach(tbody => tbody.remove()); | ||||
let check = document.getElementById('select-all'); | let check = document.getElementById('select-all'); | ||||
check.checked = false; | |||||
if (check) | |||||
check.checked = false; | |||||
} | } | ||||
this.rows = []; | this.rows = []; | ||||
this.page_length = 20; | this.page_length = 20; | ||||
@@ -131,9 +132,39 @@ export default class WebFormList { | |||||
this.make_table_head(); | this.make_table_head(); | ||||
} | } | ||||
this.append_rows(this.data); | |||||
this.wrapper.appendChild(this.table); | |||||
if (this.data.length) { | |||||
this.append_rows(this.data); | |||||
this.wrapper.appendChild(this.table); | |||||
} else { | |||||
let new_button = ""; | |||||
let empty_state = document.createElement("div"); | |||||
empty_state.classList.add("no-result", "text-muted", "flex", "justify-center", "align-center"); | |||||
frappe.has_permission(this.doctype, "", "create", () => { | |||||
new_button = ` | |||||
<a | |||||
class="btn btn-primary btn-sm btn-new-doc hidden-xs" | |||||
href="${window.location.pathname}?new=1"> | |||||
${__("Create a new {0}", [__(this.doctype)])} | |||||
</a> | |||||
`; | |||||
empty_state.innerHTML = ` | |||||
<div class="text-center"> | |||||
<div> | |||||
<img | |||||
src="/assets/frappe/images/ui-states/list-empty-state.svg" | |||||
alt="Generic Empty State" | |||||
class="null-state"> | |||||
</div> | |||||
<p class="small mb-2">${__("No {0} found", [__(this.doctype)])}</p> | |||||
${new_button} | |||||
</div> | |||||
`; | |||||
this.wrapper.appendChild(empty_state); | |||||
}); | |||||
} | |||||
} | } | ||||
make_table_head() { | make_table_head() { | ||||
@@ -212,8 +243,7 @@ export default class WebFormList { | |||||
"btn", | "btn", | ||||
"btn-secondary", | "btn-secondary", | ||||
"btn-sm", | "btn-sm", | ||||
"ml-2", | |||||
"text-white" | |||||
"ml-2" | |||||
); | ); | ||||
} | } | ||||
else if (type == "danger") { | else if (type == "danger") { | ||||
@@ -460,3 +460,10 @@ button.data-pill { | |||||
justify-content: space-between; | justify-content: space-between; | ||||
align-items: center; | align-items: center; | ||||
} | } | ||||
.capture-remove-btn { | |||||
position: absolute; | |||||
top: 0; | |||||
right: 0; | |||||
cursor: pointer; | |||||
} |
@@ -88,6 +88,7 @@ | |||||
top: var(--navbar-height); | top: var(--navbar-height); | ||||
background: var(--bg-color); | background: var(--bg-color); | ||||
margin-bottom: 5px; | margin-bottom: 5px; | ||||
transition: 0.5s top; | |||||
.page-head-content { | .page-head-content { | ||||
height: var(--page-head-height); | height: var(--page-head-height); | ||||
} | } | ||||
@@ -355,7 +355,7 @@ body[data-route^="Module"] .main-menu { | |||||
display: none; | display: none; | ||||
} | } | ||||
input { | |||||
input:not([data-fieldtype='Check']) { | |||||
background: var(--control-bg-on-gray); | background: var(--control-bg-on-gray); | ||||
} | } | ||||
@@ -311,3 +311,16 @@ h5.modal-title { | |||||
.empty-list-icon { | .empty-list-icon { | ||||
height: 70px; | height: 70px; | ||||
} | } | ||||
.null-state { | |||||
height: 60px; | |||||
width: auto; | |||||
margin-bottom: var(--margin-md); | |||||
img { | |||||
fill: var(--fg-color); | |||||
} | |||||
} | |||||
.no-result { | |||||
min-height: #{"calc(100vh - 284px)"}; | |||||
} |
@@ -4,6 +4,7 @@ import frappe, unittest | |||||
import frappe.desk.form.assign_to | import frappe.desk.form.assign_to | ||||
from frappe.desk.listview import get_group_by_count | from frappe.desk.listview import get_group_by_count | ||||
from frappe.automation.doctype.assignment_rule.test_assignment_rule import make_note | from frappe.automation.doctype.assignment_rule.test_assignment_rule import make_note | ||||
from frappe.desk.form.load import get_assignments | |||||
class TestAssign(unittest.TestCase): | class TestAssign(unittest.TestCase): | ||||
def test_assign(self): | def test_assign(self): | ||||
@@ -55,6 +56,17 @@ class TestAssign(unittest.TestCase): | |||||
frappe.db.rollback() | frappe.db.rollback() | ||||
def test_assignment_removal(self): | |||||
todo = frappe.get_doc({"doctype":"ToDo", "description": "test"}).insert() | |||||
if not frappe.db.exists("User", "test@example.com"): | |||||
frappe.get_doc({"doctype":"User", "email":"test@example.com", "first_name":"Test"}).insert() | |||||
new_todo = assign(todo, "test@example.com") | |||||
# remove assignment | |||||
frappe.db.set_value("ToDo", new_todo[0].name, "allocated_to", "") | |||||
self.assertFalse(get_assignments("ToDo", todo.name)) | |||||
def assign(doc, user): | def assign(doc, user): | ||||
return frappe.desk.form.assign_to.add({ | return frappe.desk.form.assign_to.add({ | ||||
@@ -7,12 +7,12 @@ class TestBaseDocument(unittest.TestCase): | |||||
def test_docstatus(self): | def test_docstatus(self): | ||||
doc = BaseDocument({"docstatus": 0}) | doc = BaseDocument({"docstatus": 0}) | ||||
self.assertTrue(doc.docstatus.is_draft()) | self.assertTrue(doc.docstatus.is_draft()) | ||||
self.assertEquals(doc.docstatus, 0) | |||||
self.assertEqual(doc.docstatus, 0) | |||||
doc.docstatus = 1 | doc.docstatus = 1 | ||||
self.assertTrue(doc.docstatus.is_submitted()) | self.assertTrue(doc.docstatus.is_submitted()) | ||||
self.assertEquals(doc.docstatus, 1) | |||||
self.assertEqual(doc.docstatus, 1) | |||||
doc.docstatus = 2 | doc.docstatus = 2 | ||||
self.assertTrue(doc.docstatus.is_cancelled()) | self.assertTrue(doc.docstatus.is_cancelled()) | ||||
self.assertEquals(doc.docstatus, 2) | |||||
self.assertEqual(doc.docstatus, 2) |
@@ -14,7 +14,7 @@ from frappe.database.database import Database | |||||
from frappe.query_builder import Field | from frappe.query_builder import Field | ||||
from frappe.query_builder.functions import Concat_ws | from frappe.query_builder.functions import Concat_ws | ||||
from frappe.tests.test_query_builder import db_type_is, run_only_if | from frappe.tests.test_query_builder import db_type_is, run_only_if | ||||
from frappe.utils import add_days, now, random_string | |||||
from frappe.utils import add_days, now, random_string, cint | |||||
from frappe.utils.testutils import clear_custom_fields | from frappe.utils.testutils import clear_custom_fields | ||||
@@ -84,6 +84,27 @@ class TestDB(unittest.TestCase): | |||||
), | ), | ||||
) | ) | ||||
def test_get_value_limits(self): | |||||
# check both dict and list style filters | |||||
filters = [{"enabled": 1}, [["enabled", "=", 1]]] | |||||
for filter in filters: | |||||
self.assertEqual(1, len(frappe.db.get_values("User", filters=filter, limit=1))) | |||||
# count of last touched rows as per DB-API 2.0 https://peps.python.org/pep-0249/#rowcount | |||||
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount)) | |||||
self.assertEqual(2, len(frappe.db.get_values("User", filters=filter, limit=2))) | |||||
self.assertGreaterEqual(2, cint(frappe.db._cursor.rowcount)) | |||||
# without limits length == count | |||||
self.assertEqual(len(frappe.db.get_values("User", filters=filter)), | |||||
frappe.db.count("User", filter)) | |||||
frappe.db.get_value("User", filters=filter) | |||||
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount)) | |||||
frappe.db.exists("User", filter) | |||||
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount)) | |||||
def test_escape(self): | def test_escape(self): | ||||
frappe.db.escape("香港濟生堂製藥有限公司 - IT".encode("utf-8")) | frappe.db.escape("香港濟生堂製藥有限公司 - IT".encode("utf-8")) | ||||
@@ -301,6 +322,20 @@ class TestDB(unittest.TestCase): | |||||
# recover transaction to continue other tests | # recover transaction to continue other tests | ||||
raise Exception | raise Exception | ||||
def test_exists(self): | |||||
dt, dn = "User", "Administrator" | |||||
self.assertEqual(frappe.db.exists(dt, dn, cache=True), dn) | |||||
self.assertEqual(frappe.db.exists(dt, dn), dn) | |||||
self.assertEqual(frappe.db.exists(dt, {"name": ("=", dn)}), dn) | |||||
filters = {"doctype": dt, "name": ("like", "Admin%")} | |||||
self.assertEqual(frappe.db.exists(filters), dn) | |||||
self.assertEqual( | |||||
filters["doctype"], dt | |||||
) # make sure that doctype was not removed from filters | |||||
self.assertEqual(frappe.db.exists(dt, [["name", "=", dn]]), dn) | |||||
@run_only_if(db_type_is.MARIADB) | @run_only_if(db_type_is.MARIADB) | ||||
class TestDDLCommandsMaria(unittest.TestCase): | class TestDDLCommandsMaria(unittest.TestCase): | ||||
@@ -357,7 +392,7 @@ class TestDDLCommandsMaria(unittest.TestCase): | |||||
WHERE Key_name = '{index_name}'; | WHERE Key_name = '{index_name}'; | ||||
""" | """ | ||||
) | ) | ||||
self.assertEquals(len(indexs_in_table), 2) | |||||
self.assertEqual(len(indexs_in_table), 2) | |||||
class TestDBSetValue(unittest.TestCase): | class TestDBSetValue(unittest.TestCase): | ||||
@@ -561,7 +596,7 @@ class TestDDLCommandsPost(unittest.TestCase): | |||||
AND indexname = '{index_name}' ; | AND indexname = '{index_name}' ; | ||||
""", | """, | ||||
) | ) | ||||
self.assertEquals(len(indexs_in_table), 1) | |||||
self.assertEqual(len(indexs_in_table), 1) | |||||
@run_only_if(db_type_is.POSTGRES) | @run_only_if(db_type_is.POSTGRES) | ||||
def test_modify_query(self): | def test_modify_query(self): | ||||
@@ -246,7 +246,7 @@ class TestDocument(unittest.TestCase): | |||||
'fields': [ | 'fields': [ | ||||
{'label': 'Currency', 'fieldname': 'currency', 'reqd': 1, 'fieldtype': 'Currency'}, | {'label': 'Currency', 'fieldname': 'currency', 'reqd': 1, 'fieldtype': 'Currency'}, | ||||
] | ] | ||||
}).insert() | |||||
}).insert(ignore_if_duplicate=True) | |||||
frappe.delete_doc_if_exists("Currency", "INR", 1) | frappe.delete_doc_if_exists("Currency", "INR", 1) | ||||
@@ -260,15 +260,19 @@ class TestDocument(unittest.TestCase): | |||||
'doctype': 'Test Formatted', | 'doctype': 'Test Formatted', | ||||
'currency': 100000 | 'currency': 100000 | ||||
}) | }) | ||||
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') | |||||
self.assertEqual(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') | |||||
# should work even if options aren't set in df | |||||
# and currency param is not passed | |||||
self.assertIn("0", d.get_formatted("currency")) | |||||
def test_limit_for_get(self): | def test_limit_for_get(self): | ||||
doc = frappe.get_doc("DocType", "DocType") | doc = frappe.get_doc("DocType", "DocType") | ||||
# assuming DocType has more than 3 Data fields | # assuming DocType has more than 3 Data fields | ||||
self.assertEquals(len(doc.get("fields", limit=3)), 3) | |||||
self.assertEqual(len(doc.get("fields", limit=3)), 3) | |||||
# limit with filters | # limit with filters | ||||
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3) | |||||
self.assertEqual(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3) | |||||
def test_virtual_fields(self): | def test_virtual_fields(self): | ||||
"""Virtual fields are accessible via API and Form views, whenever .as_dict is invoked | """Virtual fields are accessible via API and Form views, whenever .as_dict is invoked | ||||
@@ -70,10 +70,10 @@ class TestSearch(unittest.TestCase): | |||||
result = frappe.response['results'] | result = frappe.response['results'] | ||||
# Check whether the result is sorted or not | # Check whether the result is sorted or not | ||||
self.assertEquals(self.parent_doctype_name, result[0]['value']) | |||||
self.assertEqual(self.parent_doctype_name, result[0]['value']) | |||||
# Check whether searching for parent also list out children | # Check whether searching for parent also list out children | ||||
self.assertEquals(len(result), len(self.child_doctypes_names) + 1) | |||||
self.assertEqual(len(result), len(self.child_doctypes_names) + 1) | |||||
#Search for the word "pay", part of the word "pays" (country) in french. | #Search for the word "pay", part of the word "pays" (country) in french. | ||||
def test_link_search_in_foreign_language(self): | def test_link_search_in_foreign_language(self): | ||||
@@ -2,6 +2,7 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import io | import io | ||||
import os | |||||
import json | import json | ||||
import unittest | import unittest | ||||
from datetime import date, datetime, time, timedelta | from datetime import date, datetime, time, timedelta | ||||
@@ -14,13 +15,14 @@ import pytz | |||||
from PIL import Image | from PIL import Image | ||||
import frappe | import frappe | ||||
from frappe.utils import ceil, evaluate_filters, floor, format_timedelta | |||||
from frappe.utils import ceil, evaluate_filters, floor, format_timedelta, get_bench_path | |||||
from frappe.utils import get_url, money_in_words, parse_timedelta, scrub_urls | from frappe.utils import get_url, money_in_words, parse_timedelta, scrub_urls | ||||
from frappe.utils import validate_email_address, validate_url | from frappe.utils import validate_email_address, validate_url | ||||
from frappe.utils.data import cast, get_time, get_timedelta, nowtime, now_datetime, validate_python_code | from frappe.utils.data import cast, get_time, get_timedelta, nowtime, now_datetime, validate_python_code | ||||
from frappe.utils.diff import _get_value_from_version, get_version_diff, version_query | from frappe.utils.diff import _get_value_from_version, get_version_diff, version_query | ||||
from frappe.utils.image import optimize_image, strip_exif_data | from frappe.utils.image import optimize_image, strip_exif_data | ||||
from frappe.utils.response import json_handler | from frappe.utils.response import json_handler | ||||
from frappe.installer import parse_app_name | |||||
class TestFilters(unittest.TestCase): | class TestFilters(unittest.TestCase): | ||||
@@ -510,3 +512,13 @@ class TestLinkTitle(unittest.TestCase): | |||||
todo.delete() | todo.delete() | ||||
user.delete() | user.delete() | ||||
prop_setter.delete() | prop_setter.delete() | ||||
class TestAppParser(unittest.TestCase): | |||||
def test_app_name_parser(self): | |||||
bench_path = get_bench_path() | |||||
frappe_app = os.path.join(bench_path, "apps", "frappe") | |||||
self.assertEqual("frappe", parse_app_name(frappe_app)) | |||||
self.assertEqual("healthcare", parse_app_name("healthcare")) | |||||
self.assertEqual("healthcare", parse_app_name("https://github.com/frappe/healthcare.git")) | |||||
self.assertEqual("healthcare", parse_app_name("git@github.com:frappe/healthcare.git")) | |||||
self.assertEqual("healthcare", parse_app_name("frappe/healthcare@develop")) |
@@ -650,8 +650,6 @@ def extract_messages_from_code(code): | |||||
if isinstance(e, InvalidIncludePath): | if isinstance(e, InvalidIncludePath): | ||||
frappe.clear_last_message() | frappe.clear_last_message() | ||||
pass | |||||
messages = [] | messages = [] | ||||
pattern = r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)" | pattern = r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)" | ||||
@@ -918,3 +918,8 @@ def add_user_info(user, user_info): | |||||
email = info.email, | email = info.email, | ||||
time_zone = info.time_zone | time_zone = info.time_zone | ||||
) | ) | ||||
def is_git_url(url: str) -> bool: | |||||
# modified to allow without the tailing .git from https://github.com/jonschlinkert/is-git-url.git | |||||
pattern = r"(?:git|ssh|https?|\w*@[-\w.]+):(\/\/)?(.*?)(\.git)?(\/?|\#[-\d\w._]+?)$" | |||||
return bool(re.match(pattern, url)) |
@@ -15,7 +15,7 @@ import click | |||||
# imports - module imports | # imports - module imports | ||||
import frappe | import frappe | ||||
from frappe import _, conf | |||||
from frappe import conf | |||||
from frappe.utils import get_file_size, get_url, now, now_datetime, cint | from frappe.utils import get_file_size, get_url, now, now_datetime, cint | ||||
from frappe.utils.password import get_encryption_key | from frappe.utils.password import get_encryption_key | ||||
@@ -505,7 +505,7 @@ download only after 24 hours.""" % { | |||||
datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded""" | datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded""" | ||||
) | ) | ||||
frappe.sendmail(recipients=recipient_list, msg=msg, subject=subject) | |||||
frappe.sendmail(recipients=recipient_list, message=msg, subject=subject) | |||||
return recipient_list | return recipient_list | ||||
@@ -779,7 +779,7 @@ if __name__ == "__main__": | |||||
db_type=db_type, | db_type=db_type, | ||||
db_port=db_port, | db_port=db_port, | ||||
) | ) | ||||
odb.send_email("abc.sql.gz") | |||||
odb.send_email() | |||||
if cmd == "delete_temp_backups": | if cmd == "delete_temp_backups": | ||||
delete_temp_backups() | delete_temp_backups() |
@@ -333,6 +333,13 @@ app_license = "{app_license}" | |||||
# "{app_name}.auth.validate" | # "{app_name}.auth.validate" | ||||
# ] | # ] | ||||
# Translation | |||||
# -------------------------------- | |||||
# Make link fields search translated document names for these DocTypes | |||||
# Recommended only for DocTypes which have limited documents with untranslated names | |||||
# For example: Role, Gender, etc. | |||||
# translated_search_doctypes = [] | |||||
""" | """ | ||||
desktop_template = """from frappe import _ | desktop_template = """from frappe import _ | ||||
@@ -13,7 +13,7 @@ from frappe.utils import get_sites | |||||
default_log_level = logging.DEBUG | default_log_level = logging.DEBUG | ||||
def get_logger(module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20): | |||||
def get_logger(module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20, stream_only=False): | |||||
"""Application Logger for your given module | """Application Logger for your given module | ||||
Args: | Args: | ||||
@@ -23,6 +23,7 @@ def get_logger(module=None, with_more_info=False, allow_site=True, filter=None, | |||||
filter (function, optional): Add a filter function for your logger. Defaults to None. | filter (function, optional): Add a filter function for your logger. Defaults to None. | ||||
max_size (int, optional): Max file size of each log file in bytes. Defaults to 100_000. | max_size (int, optional): Max file size of each log file in bytes. Defaults to 100_000. | ||||
file_count (int, optional): Max count of log files to be retained via Log Rotation. Defaults to 20. | file_count (int, optional): Max count of log files to be retained via Log Rotation. Defaults to 20. | ||||
stream_only (bool, optional): Whether to stream logs only to stderr (True) or use log files (False). Defaults to False. | |||||
Returns: | Returns: | ||||
<class 'logging.Logger'>: Returns a Python logger object with Site and Bench level logging capabilities. | <class 'logging.Logger'>: Returns a Python logger object with Site and Bench level logging capabilities. | ||||
@@ -54,11 +55,14 @@ def get_logger(module=None, with_more_info=False, allow_site=True, filter=None, | |||||
logger.propagate = False | logger.propagate = False | ||||
formatter = logging.Formatter("%(asctime)s %(levelname)s {0} %(message)s".format(module)) | formatter = logging.Formatter("%(asctime)s %(levelname)s {0} %(message)s".format(module)) | ||||
handler = RotatingFileHandler(log_filename, maxBytes=max_size, backupCount=file_count) | |||||
if stream_only: | |||||
handler = logging.StreamHandler() | |||||
else: | |||||
handler = RotatingFileHandler(log_filename, maxBytes=max_size, backupCount=file_count) | |||||
handler.setFormatter(formatter) | handler.setFormatter(formatter) | ||||
logger.addHandler(handler) | logger.addHandler(handler) | ||||
if site: | |||||
if site and not stream_only: | |||||
sitelog_filename = os.path.join(site, "logs", logfile) | sitelog_filename = os.path.join(site, "logs", logfile) | ||||
site_handler = RotatingFileHandler(sitelog_filename, maxBytes=max_size, backupCount=file_count) | site_handler = RotatingFileHandler(sitelog_filename, maxBytes=max_size, backupCount=file_count) | ||||
site_handler.setFormatter(formatter) | site_handler.setFormatter(formatter) | ||||
@@ -227,7 +227,6 @@ class NestedSet(Document): | |||||
update_nsm(self) | update_nsm(self) | ||||
except frappe.DoesNotExistError: | except frappe.DoesNotExistError: | ||||
if self.flags.on_rollback: | if self.flags.on_rollback: | ||||
pass | |||||
frappe.message_log.pop() | frappe.message_log.pop() | ||||
else: | else: | ||||
raise | raise | ||||