@@ -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): | |||
"""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): | |||
"""Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files.""" | |||
@@ -1252,9 +1251,10 @@ def get_newargs(fn, kwargs): | |||
if hasattr(fn, 'fnargs'): | |||
fnargs = fn.fnargs | |||
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 = {} | |||
for a in kwargs: | |||
@@ -1266,7 +1266,7 @@ def get_newargs(fn, kwargs): | |||
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). | |||
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, | |||
'value': args.value, | |||
'property_type': args.property_type or "Data", | |||
'is_system_generated': is_system_generated, | |||
'__islocal': 1 | |||
}) | |||
ps.flags.ignore_validate = ignore_validate | |||
@@ -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 | |||
import frappe | |||
from frappe import _ | |||
from frappe.utils import get_fullname, now | |||
from frappe.model.document import Document | |||
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.functions import Now | |||
from pypika.terms import PseudoColumn | |||
from frappe.utils import get_fullname, now | |||
class ActivityLog(Document): | |||
def before_insert(self): | |||
@@ -49,5 +48,5 @@ def clear_activity_logs(days=None): | |||
days = 90 | |||
doctype = DocType("Activity Log") | |||
frappe.db.delete(doctype, filters=( | |||
doctype.creation < PseudoColumn(f"({Now() - Interval(days=days)})") | |||
)) | |||
doctype.creation < (Now() - Interval(days=days)) | |||
)) |
@@ -7,7 +7,6 @@ from frappe import _ | |||
from frappe.model.document import Document | |||
from frappe.query_builder import DocType, Interval | |||
from frappe.query_builder.functions import Now | |||
from pypika.terms import PseudoColumn | |||
class LogSettings(Document): | |||
@@ -19,7 +18,7 @@ class LogSettings(Document): | |||
def clear_error_logs(self): | |||
table = DocType("Error Log") | |||
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): | |||
@@ -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 | |||
# import frappe | |||
from datetime import datetime | |||
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): | |||
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) |
@@ -7,6 +7,7 @@ | |||
"document_type": "Setup", | |||
"engine": "InnoDB", | |||
"field_order": [ | |||
"is_system_generated", | |||
"dt", | |||
"module", | |||
"label", | |||
@@ -425,13 +426,20 @@ | |||
"fieldtype": "Link", | |||
"label": "Module (for export)", | |||
"options": "Module Def" | |||
}, | |||
{ | |||
"default": "0", | |||
"fieldname": "is_system_generated", | |||
"fieldtype": "Check", | |||
"label": "Is System Generated", | |||
"read_only": 1 | |||
} | |||
], | |||
"icon": "fa fa-glass", | |||
"idx": 1, | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2022-02-14 15:42:21.885999", | |||
"modified": "2022-02-28 22:22:54.893269", | |||
"modified_by": "Administrator", | |||
"module": "Custom", | |||
"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, "") != ""): | |||
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) | |||
if not df.fieldname and df.label: | |||
df.fieldname = frappe.scrub(df.label) | |||
@@ -130,8 +130,7 @@ def create_custom_field(doctype, df, ignore_validate=False): | |||
"permlevel": 0, | |||
"fieldtype": 'Data', | |||
"hidden": 0, | |||
# Looks like we always use this programatically? | |||
# "is_standard": 1 | |||
"is_system_generated": is_system_generated | |||
}) | |||
custom_field.update(df) | |||
custom_field.flags.ignore_validate = ignore_validate | |||
@@ -246,7 +246,8 @@ frappe.ui.form.on("Customize Form Field", { | |||
}, | |||
fields_add: function(frm, 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, | |||
"value": value, | |||
"property_type": property_type | |||
}) | |||
}, is_system_generated=False) | |||
def get_existing_property_value(self, property_name, fieldname=None): | |||
# 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={ | |||
'doc_type': doctype, | |||
'field_name': ['!=', 'naming_series'], | |||
'property': ['!=', 'options'] | |||
'property': ['!=', 'options'], | |||
'is_system_generated': False | |||
}, pluck='name') | |||
for setter in setters: | |||
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) | |||
doctype_properties = { | |||
@@ -6,6 +6,7 @@ | |||
"document_type": "Setup", | |||
"engine": "InnoDB", | |||
"field_order": [ | |||
"is_system_generated", | |||
"help", | |||
"sb0", | |||
"doctype_or_field", | |||
@@ -103,13 +104,20 @@ | |||
{ | |||
"fieldname": "section_break_9", | |||
"fieldtype": "Section Break" | |||
}, | |||
{ | |||
"default": "0", | |||
"fieldname": "is_system_generated", | |||
"fieldtype": "Check", | |||
"label": "Is System Generated", | |||
"read_only": 1 | |||
} | |||
], | |||
"icon": "fa fa-glass", | |||
"idx": 1, | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2021-12-14 14:15:41.929071", | |||
"modified": "2022-02-28 22:24:12.377693", | |||
"modified_by": "Administrator", | |||
"module": "Custom", | |||
"name": "Property Setter", | |||
@@ -10,7 +10,7 @@ import re | |||
import string | |||
from contextlib import contextmanager | |||
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 | |||
@@ -561,7 +561,7 @@ class Database(object): | |||
def 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. | |||
:param doctype: DocType of the single object | |||
@@ -919,8 +919,8 @@ class Database(object): | |||
return dn | |||
if isinstance(dt, dict): | |||
_dt = dt.pop("doctype") | |||
dt, dn = _dt, dt | |||
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) | |||
@@ -72,7 +72,8 @@ class ToDo(Document): | |||
assignments = frappe.get_all("ToDo", filters={ | |||
"reference_type": self.reference_type, | |||
"reference_name": self.reference_name, | |||
"status": ("!=", "Cancelled") | |||
"status": ("!=", "Cancelled"), | |||
"allocated_to": ("is", "set") | |||
}, pluck="allocated_to") | |||
assignments.reverse() | |||
@@ -312,6 +312,7 @@ def get_assignments(dt, dn): | |||
'reference_type': dt, | |||
'reference_name': dn, | |||
'status': ('!=', 'Cancelled'), | |||
'allocated_to': ("is", "set") | |||
}) | |||
@frappe.whitelist() | |||
@@ -9,7 +9,6 @@ from frappe import _, is_whitelisted | |||
import re | |||
import wrapt | |||
UNTRANSLATED_DOCTYPES = ["DocType", "Role"] | |||
def sanitize_searchfield(searchfield): | |||
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 = [] | |||
translated_search_doctypes = frappe.get_hooks("translated_search_doctypes") | |||
# build from doctype | |||
if txt: | |||
search_fields = ["name"] | |||
@@ -125,7 +125,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||
for f in search_fields: | |||
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"])): | |||
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' | |||
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 | |||
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, | |||
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 | |||
values = ( | |||
v for v in values | |||
@@ -111,7 +111,6 @@ class EmailQueue(Document): | |||
""" Send emails to recipients. | |||
""" | |||
if not self.can_send_now(): | |||
frappe.db.rollback() | |||
return | |||
with SendMailContext(self, is_background_task) as ctx: | |||
@@ -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 | |||
import frappe | |||
from frappe import msgprint, _ | |||
from frappe.utils.verified_command import get_signed_params, verify_request | |||
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): | |||
"""Get count of emails sent from a specific email account. | |||
@@ -162,15 +164,16 @@ def get_queue(): | |||
by priority desc, creation asc | |||
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. | |||
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: | |||
frappe.db.delete("Email Queue", {"name": ("in", email_queues)}) | |||
@@ -110,3 +110,6 @@ class InvalidAuthorizationPrefix(CSRFTokenError): pass | |||
class InvalidAuthorizationToken(CSRFTokenError): pass | |||
class InvalidDatabaseFile(ValidationError): 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): | |||
"""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 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) | |||
else: | |||
if isinstance(docs, str): | |||
docs = json.loads(docs) | |||
docs = frappe.parse_json(docs) | |||
doc = frappe.get_doc(docs) | |||
doc._original_modified = doc.modified | |||
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() | |||
try: | |||
args = json.loads(args) | |||
args = frappe.parse_json(args) | |||
except ValueError: | |||
args = args | |||
pass | |||
method_obj = getattr(doc, method) | |||
fn = getattr(method_obj, '__func__', method_obj) | |||
is_whitelisted(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"): | |||
response = doc.run_method(method) | |||
@@ -383,3 +383,5 @@ global_search_doctypes = { | |||
{"doctype": "Web Form"} | |||
] | |||
} | |||
translated_search_doctypes = ["DocType", "Role", "Country", "Gender", "Salutation"] |
@@ -5,10 +5,11 @@ import json | |||
import os | |||
import sys | |||
from collections import OrderedDict | |||
from typing import List, Dict | |||
from typing import List, Dict, Tuple | |||
import frappe | |||
from frappe.defaults import _clear_cache | |||
from frappe.utils import is_git_url | |||
def _new_site( | |||
@@ -34,7 +35,6 @@ def _new_site( | |||
from frappe.commands.scheduler import _is_scheduler_enabled | |||
from frappe.utils import get_site_path, scheduler, touch_file | |||
if not force and os.path.exists(site): | |||
print("Site {0} already exists".format(site)) | |||
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 | |||
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): | |||
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs | |||
from frappe.model.sync import sync_for | |||
@@ -140,7 +220,8 @@ def install_app(name, verbose=False, set_as_patched=True): | |||
# install pre-requisites | |||
if 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.clear_cache() | |||
@@ -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.remove_db_aggregation | |||
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 |
@@ -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() | |||
) |
@@ -526,11 +526,13 @@ export default { | |||
error: true | |||
}); | |||
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() { | |||
@@ -37,8 +37,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro | |||
if(this.frm) { | |||
me.parse_validate_and_set_in_model(null); | |||
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.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; | |||
} | |||
on_upload_complete(attachment) { | |||
async on_upload_complete(attachment) { | |||
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.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save(); | |||
} | |||
@@ -28,6 +28,24 @@ frappe._.get_data_uri = element => { | |||
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. | |||
* | |||
@@ -45,6 +63,9 @@ frappe.ui.Capture = class { | |||
constructor(options = {}) { | |||
this.options = frappe.ui.Capture.OPTIONS; | |||
this.set_options(options); | |||
this.facing_mode = "environment"; | |||
this.images = []; | |||
} | |||
set_options(options) { | |||
@@ -53,74 +74,229 @@ frappe.ui.Capture = class { | |||
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() { | |||
@@ -148,11 +324,11 @@ frappe.ui.Capture.OPTIONS = { | |||
frappe.ui.Capture.ERR_MESSAGE = __('Unable to load camera.'); | |||
frappe.ui.Capture.TEMPLATE = ` | |||
<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> | |||
`; |
@@ -29,7 +29,7 @@ frappe.views.CalendarView = class CalendarView extends frappe.views.ListView { | |||
.then(() => { | |||
this.page_title = __('{0} Calendar', [this.page_title]); | |||
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; | |||
return new Promise(resolve => { | |||
if (calendar_name === 'Default') { | |||
if (calendar_name === 'default') { | |||
Object.assign(options, frappe.views.calendar[this.doctype]); | |||
resolve(options); | |||
} else { | |||
frappe.model.with_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, { | |||
field_map: { | |||
id: "name", | |||
@@ -460,3 +460,10 @@ button.data-pill { | |||
justify-content: space-between; | |||
align-items: center; | |||
} | |||
.capture-remove-btn { | |||
position: absolute; | |||
top: 0; | |||
right: 0; | |||
cursor: pointer; | |||
} |
@@ -4,6 +4,7 @@ import frappe, unittest | |||
import frappe.desk.form.assign_to | |||
from frappe.desk.listview import get_group_by_count | |||
from frappe.automation.doctype.assignment_rule.test_assignment_rule import make_note | |||
from frappe.desk.form.load import get_assignments | |||
class TestAssign(unittest.TestCase): | |||
def test_assign(self): | |||
@@ -55,6 +56,17 @@ class TestAssign(unittest.TestCase): | |||
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): | |||
return frappe.desk.form.assign_to.add({ | |||
@@ -327,7 +327,13 @@ class TestDB(unittest.TestCase): | |||
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) | |||
self.assertEqual(frappe.db.exists({"doctype": dt, "name": ("like", "Admin%")}), 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) | |||
@@ -2,6 +2,7 @@ | |||
# License: MIT. See LICENSE | |||
import io | |||
import os | |||
import json | |||
import unittest | |||
from datetime import date, datetime, time, timedelta | |||
@@ -14,13 +15,14 @@ import pytz | |||
from PIL import Image | |||
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 validate_email_address, validate_url | |||
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.image import optimize_image, strip_exif_data | |||
from frappe.utils.response import json_handler | |||
from frappe.installer import parse_app_name | |||
class TestFilters(unittest.TestCase): | |||
@@ -510,3 +512,13 @@ class TestLinkTitle(unittest.TestCase): | |||
todo.delete() | |||
user.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")) |
@@ -918,3 +918,8 @@ def add_user_info(user, user_info): | |||
email = info.email, | |||
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)) |
@@ -333,6 +333,13 @@ app_license = "{app_license}" | |||
# "{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 _ | |||
@@ -13,7 +13,7 @@ from frappe.utils import get_sites | |||
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 | |||
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. | |||
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. | |||
stream_only (bool, optional): Whether to stream logs only to stderr (True) or use log files (False). Defaults to False. | |||
Returns: | |||
<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 | |||
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) | |||
logger.addHandler(handler) | |||
if site: | |||
if site and not stream_only: | |||
sitelog_filename = os.path.join(site, "logs", logfile) | |||
site_handler = RotatingFileHandler(sitelog_filename, maxBytes=max_size, backupCount=file_count) | |||
site_handler.setFormatter(formatter) | |||