@@ -19,7 +19,7 @@ context('Timeline Email', () => { | |||
cy.get('.list-row > .level-left > .list-subject').eq(0).click(); | |||
//Creating a new email | |||
cy.get('.timeline-actions > .btn').click(); | |||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click(); | |||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect'); | |||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-body > :nth-child(1) > .form-layout > .form-page > :nth-child(3) > .section-body > .form-column > form > [data-fieldtype="Text Editor"] > .form-group > .control-input-wrapper > .control-input > .ql-container > .ql-editor').type('Test Mail'); | |||
@@ -57,11 +57,11 @@ context('Timeline Email', () => { | |||
cy.wait(500); | |||
//To check if the discard button functionality in email is working correctly | |||
cy.get('.timeline-actions > .btn').click(); | |||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click(); | |||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect'); | |||
cy.get('.modal-footer > .standard-actions > .btn-secondary').contains('Discard').click(); | |||
cy.wait(500); | |||
cy.get('.timeline-actions > .btn').click(); | |||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click(); | |||
cy.wait(500); | |||
cy.get_field('recipients', 'MultiSelect').should('have.text', ''); | |||
cy.get('.modal-header:visible > .modal-actions > .btn-modal-close > .icon').click(); | |||
@@ -12,6 +12,8 @@ Read the documentation: https://frappeframework.com/docs | |||
""" | |||
import os, warnings | |||
STANDARD_USERS = ('Guest', 'Administrator') | |||
_dev_server = os.environ.get('DEV_SERVER', False) | |||
if _dev_server: | |||
@@ -100,7 +102,7 @@ def as_unicode(text, encoding='utf-8'): | |||
'''Convert to unicode if required''' | |||
if isinstance(text, str): | |||
return text | |||
elif text==None: | |||
elif text is None: | |||
return '' | |||
elif isinstance(text, bytes): | |||
return str(text, encoding) | |||
@@ -121,6 +123,7 @@ def set_user_lang(user, user_language=None): | |||
local.lang = get_user_lang(user) | |||
# local-globals | |||
db = local("db") | |||
qb = local("qb") | |||
conf = local("conf") | |||
@@ -291,7 +294,7 @@ def get_conf(site=None): | |||
class init_site: | |||
def __init__(self, site=None): | |||
'''If site==None, initialize it for empty site ('') to load common_site_config.json''' | |||
'''If site is None, initialize it for empty site ('') to load common_site_config.json''' | |||
self.site = site or '' | |||
def __enter__(self): | |||
@@ -443,7 +446,7 @@ def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None, | |||
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide, as_list=as_list) | |||
def emit_js(js, user=False, **kwargs): | |||
if user == False: | |||
if user is False: | |||
user = session.user | |||
publish_realtime('eval_js', js, user=user, **kwargs) | |||
@@ -1658,7 +1661,7 @@ def local_cache(namespace, key, generator, regenerate_if_none=False): | |||
if key not in local.cache[namespace]: | |||
local.cache[namespace][key] = generator() | |||
elif local.cache[namespace][key]==None and regenerate_if_none: | |||
elif local.cache[namespace][key] is None and regenerate_if_none: | |||
# if key exists but the previous result was None | |||
local.cache[namespace][key] = generator() | |||
@@ -111,7 +111,8 @@ class LoginManager: | |||
self.user_type = None | |||
if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": | |||
if self.login()==False: return | |||
if self.login() is False: | |||
return | |||
self.resume = False | |||
# run login triggers | |||
@@ -250,8 +251,7 @@ class LoginManager: | |||
if not self.user: | |||
return | |||
from frappe.core.doctype.user.user import STANDARD_USERS | |||
if self.user in STANDARD_USERS: | |||
if self.user in frappe.STANDARD_USERS: | |||
return False | |||
reset_pwd_after_days = cint(frappe.db.get_single_value("System Settings", | |||
@@ -272,7 +272,7 @@ def apply(doc=None, method=None, doctype=None, name=None): | |||
for todo in todos_to_close: | |||
_todo = frappe.get_doc("ToDo", todo) | |||
_todo.status = "Closed" | |||
_todo.save() | |||
_todo.save(ignore_permissions=True) | |||
break | |||
else: | |||
@@ -17,7 +17,7 @@ from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_p | |||
from frappe.model.base_document import get_controller | |||
from frappe.social.doctype.post.post import frequently_visited_links | |||
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo | |||
from frappe.utils import get_time_zone | |||
from frappe.utils import get_time_zone, add_user_info | |||
def get_bootinfo(): | |||
"""build and return boot info""" | |||
@@ -222,17 +222,14 @@ def load_translations(bootinfo): | |||
bootinfo["__messages"] = messages | |||
def get_user_info(): | |||
user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image', 'gender', | |||
'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type', 'time_zone'], | |||
filters=dict(enabled=1)) | |||
# get info for current user | |||
user_info = frappe._dict() | |||
add_user_info(frappe.session.user, user_info) | |||
user_info_map = {d.name: d for d in user_info} | |||
if frappe.session.user == 'Administrator' and user_info.Administrator.email: | |||
user_info[user_info.Administrator.email] = user_info.Administrator | |||
admin_data = user_info_map.get('Administrator') | |||
if admin_data: | |||
user_info_map[admin_data.email] = admin_data | |||
return user_info_map | |||
return user_info | |||
def get_user(bootinfo): | |||
"""get user info""" | |||
@@ -148,7 +148,7 @@ def build_table_count_cache(): | |||
data = ( | |||
frappe.qb.from_(information_schema.tables).select(table_name, table_rows) | |||
).run(as_dict=True) | |||
counts = {d.get('name').lstrip('tab'): d.get('count', None) for d in data} | |||
counts = {d.get('name').replace('tab', '', 1): d.get('count', None) for d in data} | |||
_cache.set_value("information_schema:counts", counts) | |||
return counts | |||
@@ -952,7 +952,7 @@ def trim_database(context, dry_run, format, no_backup): | |||
doctype_tables = frappe.get_all("DocType", pluck="name") | |||
for x in database_tables: | |||
doctype = x.lstrip("tab") | |||
doctype = x.replace("tab", "", 1) | |||
if not (doctype in doctype_tables or x.startswith("__") or x in STANDARD_TABLES): | |||
TABLES_TO_DROP.append(x) | |||
@@ -966,7 +966,7 @@ def trim_database(context, dry_run, format, no_backup): | |||
odb = scheduled_backup( | |||
ignore_conf=False, | |||
include_doctypes=",".join(x.lstrip("tab") for x in TABLES_TO_DROP), | |||
include_doctypes=",".join(x.replace("tab", "", 1) for x in TABLES_TO_DROP), | |||
ignore_files=True, | |||
force=True, | |||
) | |||
@@ -314,7 +314,7 @@ class DataExporter: | |||
.where(child_doctype_table.parentfield == c["parentfield"]) | |||
.orderby(child_doctype_table.idx) | |||
) | |||
for ci, child in enumerate(data_row.run()): | |||
for ci, child in enumerate(data_row.run(as_dict=True)): | |||
self.add_data_row(rows, c['doctype'], c['parentfield'], child, ci) | |||
for row in rows: | |||
@@ -0,0 +1,105 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2019, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import unittest | |||
import frappe | |||
from frappe.core.doctype.data_export.exporter import DataExporter | |||
class TestDataExporter(unittest.TestCase): | |||
def setUp(self): | |||
self.doctype_name = 'Test DocType for Export Tool' | |||
self.doc_name = 'Test Data for Export Tool' | |||
self.create_doctype_if_not_exists(doctype_name=self.doctype_name) | |||
self.create_test_data() | |||
def create_doctype_if_not_exists(self, doctype_name, force=False): | |||
""" | |||
Helper Function for setting up doctypes | |||
""" | |||
if force: | |||
frappe.delete_doc_if_exists('DocType', doctype_name) | |||
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name) | |||
if frappe.db.exists('DocType', doctype_name): | |||
return | |||
# Child Table 1 | |||
table_1_name = 'Child 1 of ' + doctype_name | |||
frappe.get_doc({ | |||
'doctype': 'DocType', | |||
'name': table_1_name, | |||
'module': 'Custom', | |||
'custom': 1, | |||
'istable': 1, | |||
'fields': [ | |||
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'}, | |||
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'}, | |||
] | |||
}).insert() | |||
# Main Table | |||
frappe.get_doc({ | |||
'doctype': 'DocType', | |||
'name': doctype_name, | |||
'module': 'Custom', | |||
'custom': 1, | |||
'autoname': 'field:title', | |||
'fields': [ | |||
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'}, | |||
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'}, | |||
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name}, | |||
], | |||
'permissions': [ | |||
{'role': 'System Manager'} | |||
] | |||
}).insert() | |||
def create_test_data(self, force=False): | |||
""" | |||
Helper Function creating test data | |||
""" | |||
if force: | |||
frappe.delete_doc(self.doctype_name, self.doc_name) | |||
if not frappe.db.exists(self.doctype_name, self.doc_name): | |||
self.doc = frappe.get_doc( | |||
doctype=self.doctype_name, | |||
title=self.doc_name, | |||
number="100", | |||
table_field_1=[ | |||
{"child_title": "Child Title 1", "child_number": "50"}, | |||
{"child_title": "Child Title 2", "child_number": "51"}, | |||
] | |||
).insert() | |||
else: | |||
self.doc = frappe.get_doc(self.doctype_name, self.doc_name) | |||
def test_export_content(self): | |||
exp = DataExporter(doctype=self.doctype_name, file_type='CSV') | |||
exp.build_response() | |||
self.assertEqual(frappe.response['type'],'csv') | |||
self.assertEqual(frappe.response['doctype'], self.doctype_name) | |||
self.assertTrue(frappe.response['result']) | |||
self.assertIn('Child Title 1\",50',frappe.response['result']) | |||
self.assertIn('Child Title 2\",51',frappe.response['result']) | |||
def test_export_type(self): | |||
for type in ['csv', 'Excel']: | |||
with self.subTest(type=type): | |||
exp = DataExporter(doctype=self.doctype_name, file_type=type) | |||
exp.build_response() | |||
self.assertEqual(frappe.response['doctype'], self.doctype_name) | |||
self.assertTrue(frappe.response['result']) | |||
if type == 'csv': | |||
self.assertEqual(frappe.response['type'],'csv') | |||
elif type == 'Excel': | |||
self.assertEqual(frappe.response['type'],'binary') | |||
self.assertEqual(frappe.response['filename'], self.doctype_name+'.xlsx') # 'Test DocType for Export Tool.xlsx') | |||
self.assertTrue(frappe.response['filecontent']) | |||
def tearDown(self): | |||
pass | |||
@@ -5,6 +5,7 @@ | |||
import frappe | |||
from frappe.model.document import Document | |||
from frappe.utils.data import evaluate_filters | |||
from frappe.model.naming import parse_naming_series | |||
from frappe import _ | |||
class DocumentNamingRule(Document): | |||
@@ -27,7 +28,9 @@ class DocumentNamingRule(Document): | |||
return | |||
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0 | |||
doc.name = self.prefix + ('%0'+str(self.prefix_digits)+'d') % (counter + 1) | |||
naming_series = parse_naming_series(self.prefix, doc=doc) | |||
doc.name = naming_series + ('%0'+str(self.prefix_digits)+'d') % (counter + 1) | |||
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1) | |||
@frappe.whitelist() | |||
@@ -39,7 +39,8 @@ def sync_languages(): | |||
frappe.get_doc({ | |||
'doctype': 'Language', | |||
'language_code': l['code'], | |||
'language_name': l['name'] | |||
'language_name': l['name'], | |||
'enabled': 1, | |||
}).insert() | |||
def update_language_names(): | |||
@@ -1,154 +1,57 @@ | |||
{ | |||
"allow_copy": 0, | |||
"allow_guest_to_view": 0, | |||
"allow_import": 0, | |||
"allow_rename": 0, | |||
"autoname": "field:gateway", | |||
"beta": 0, | |||
"creation": "2015-12-15 22:26:45.221162", | |||
"custom": 0, | |||
"docstatus": 0, | |||
"doctype": "DocType", | |||
"document_type": "", | |||
"editable_grid": 1, | |||
"actions": [], | |||
"autoname": "field:gateway", | |||
"creation": "2022-01-24 21:09:47.229371", | |||
"doctype": "DocType", | |||
"editable_grid": 1, | |||
"engine": "InnoDB", | |||
"field_order": [ | |||
"gateway", | |||
"gateway_settings", | |||
"gateway_controller" | |||
], | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "gateway", | |||
"fieldtype": "Data", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"label": "Gateway", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 1, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
"fieldname": "gateway", | |||
"fieldtype": "Data", | |||
"in_list_view": 1, | |||
"label": "Gateway", | |||
"reqd": 1, | |||
"unique": 1 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "gateway_settings", | |||
"fieldtype": "Link", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Gateway Settings", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "DocType", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
"fieldname": "gateway_settings", | |||
"fieldtype": "Link", | |||
"label": "Gateway Settings", | |||
"options": "DocType", | |||
"reqd": 1 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "gateway_controller", | |||
"fieldtype": "Dynamic Link", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Gateway Controller", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "gateway_settings", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
"fieldname": "gateway_controller", | |||
"fieldtype": "Dynamic Link", | |||
"label": "Gateway Controller", | |||
"options": "gateway_settings", | |||
"reqd": 1 | |||
} | |||
], | |||
"has_web_view": 0, | |||
"hide_heading": 0, | |||
"hide_toolbar": 0, | |||
"idx": 0, | |||
"image_view": 0, | |||
"in_create": 1, | |||
"is_submittable": 0, | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2018-02-05 14:24:33.526645", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Payment Gateway", | |||
"name_case": "", | |||
"owner": "Administrator", | |||
], | |||
"links": [], | |||
"modified": "2022-01-24 21:17:03.864719", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Payment Gateway", | |||
"naming_rule": "By fieldname", | |||
"owner": "Administrator", | |||
"permissions": [ | |||
{ | |||
"amend": 0, | |||
"apply_user_permissions": 0, | |||
"cancel": 0, | |||
"create": 0, | |||
"delete": 0, | |||
"email": 0, | |||
"export": 0, | |||
"if_owner": 0, | |||
"import": 0, | |||
"permlevel": 0, | |||
"print": 0, | |||
"read": 1, | |||
"report": 0, | |||
"role": "System Manager", | |||
"set_user_permissions": 0, | |||
"share": 0, | |||
"submit": 0, | |||
"write": 0 | |||
"create": 1, | |||
"delete": 1, | |||
"read": 1, | |||
"role": "System Manager", | |||
"write": 1 | |||
} | |||
], | |||
"quick_entry": 1, | |||
"read_only": 0, | |||
"read_only_onload": 0, | |||
"show_name_in_global_search": 0, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"track_changes": 0, | |||
"track_seen": 0 | |||
], | |||
"quick_entry": 1, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [] | |||
} |
@@ -12,6 +12,7 @@ | |||
"restrict_to_domain", | |||
"column_break_4", | |||
"disabled", | |||
"is_custom", | |||
"desk_access", | |||
"two_factor_auth", | |||
"navigation_settings_section", | |||
@@ -24,8 +25,7 @@ | |||
"form_settings_section", | |||
"form_sidebar", | |||
"timeline", | |||
"dashboard", | |||
"is_custom" | |||
"dashboard" | |||
], | |||
"fields": [ | |||
{ | |||
@@ -148,7 +148,7 @@ | |||
"idx": 1, | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2021-10-08 14:06:55.729364", | |||
"modified": "2022-01-12 20:18:18.496230", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Role", | |||
@@ -170,5 +170,6 @@ | |||
"quick_entry": 1, | |||
"sort_field": "modified", | |||
"sort_order": "ASC", | |||
"states": [], | |||
"track_changes": 1 | |||
} |
@@ -19,7 +19,7 @@ from frappe.core.doctype.user_type.user_type import user_linked_with_permission_ | |||
from frappe.query_builder import DocType | |||
STANDARD_USERS = ("Guest", "Administrator") | |||
STANDARD_USERS = frappe.STANDARD_USERS | |||
class User(Document): | |||
__new_password = None | |||
@@ -344,7 +344,7 @@ class User(Document): | |||
frappe.sendmail(recipients=self.email, sender=sender, subject=subject, | |||
template=template, args=args, header=[subject, "green"], | |||
delayed=(not now) if now!=None else self.flags.delay_emails, retry=3) | |||
delayed=(not now) if now is not None else self.flags.delay_emails, retry=3) | |||
def a_system_manager_should_exist(self): | |||
if not self.get_other_system_managers(): | |||
@@ -37,16 +37,14 @@ class UserType(Document): | |||
return | |||
modules = frappe.get_all("DocType", | |||
fields=["module"], | |||
filters={"name": ("in", [d.document_type for d in self.user_doctypes])}, | |||
distinct=True, | |||
pluck="module", | |||
) | |||
self.set('user_type_modules', []) | |||
for row in modules: | |||
self.append('user_type_modules', { | |||
'module': row.module | |||
}) | |||
self.set("user_type_modules", []) | |||
for module in modules: | |||
self.append("user_type_modules", {"module": module}) | |||
def validate_document_type_limit(self): | |||
limit = frappe.conf.get('user_type_doctype_limit', {}).get(frappe.scrub(self.name)) | |||
@@ -377,7 +377,7 @@ class CustomizeForm(Document): | |||
def make_property_setter(self, prop, value, property_type, fieldname=None, | |||
apply_on=None, row_name = None): | |||
delete_property_setter(self.doc_type, prop, fieldname) | |||
delete_property_setter(self.doc_type, prop, fieldname, row_name) | |||
property_value = self.get_existing_property_value(prop, fieldname) | |||
@@ -19,7 +19,7 @@ class PropertySetter(Document): | |||
def validate(self): | |||
self.validate_fieldtype_change() | |||
if self.is_new(): | |||
delete_property_setter(self.doc_type, self.property, self.field_name) | |||
delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name) | |||
# clear cache | |||
frappe.clear_cache(doctype = self.doc_type) | |||
@@ -91,11 +91,13 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for | |||
property_setter.insert() | |||
return property_setter | |||
def delete_property_setter(doc_type, property, field_name=None): | |||
def delete_property_setter(doc_type, property, field_name=None, row_name=None): | |||
"""delete other property setters on this, if this is new""" | |||
filters = dict(doc_type = doc_type, property=property) | |||
filters = dict(doc_type=doc_type, property=property) | |||
if field_name: | |||
filters['field_name'] = field_name | |||
if row_name: | |||
filters["row_name"] = row_name | |||
frappe.db.delete('Property Setter', filters) | |||
@@ -245,9 +245,16 @@ class MariaDBDatabase(Database): | |||
column_name as 'name', | |||
column_type as 'type', | |||
column_default as 'default', | |||
column_key = 'MUL' as 'index', | |||
COALESCE( | |||
(select 1 | |||
from information_schema.statistics | |||
where table_name="{table_name}" | |||
and column_name=columns.column_name | |||
and NON_UNIQUE=1 | |||
limit 1 | |||
), 0) as 'index', | |||
column_key = 'UNI' as 'unique' | |||
from information_schema.columns | |||
from information_schema.columns as columns | |||
where table_name = '{table_name}' '''.format(table_name=table_name), as_dict=1) | |||
def has_index(self, table_name, index_name): | |||
@@ -58,18 +58,34 @@ class MariaDBTable(DBTable): | |||
modify_column_query.append("MODIFY `{}` {}".format(col.fieldname, col.get_definition())) | |||
for col in self.add_index: | |||
# if index key not exists | |||
if not frappe.db.sql("SHOW INDEX FROM `%s` WHERE key_name = %s" % | |||
(self.table_name, '%s'), col.fieldname): | |||
add_index_query.append("ADD INDEX `{}`(`{}`)".format(col.fieldname, col.fieldname)) | |||
# if index key does not exists | |||
if not frappe.db.has_index(self.table_name, col.fieldname + '_index'): | |||
add_index_query.append("ADD INDEX `{}_index`(`{}`)".format(col.fieldname, col.fieldname)) | |||
for col in self.drop_index: | |||
for col in self.drop_index + self.drop_unique: | |||
if col.fieldname != 'name': # primary key | |||
current_column = self.current_columns.get(col.fieldname.lower()) | |||
unique_constraint_changed = current_column.unique != col.unique | |||
if unique_constraint_changed and not col.unique: | |||
# nosemgrep | |||
unique_index_record = frappe.db.sql(""" | |||
SHOW INDEX FROM `{0}` | |||
WHERE Key_name=%s | |||
AND Non_unique=0 | |||
""".format(self.table_name), (col.fieldname), as_dict=1) | |||
if unique_index_record: | |||
drop_index_query.append("DROP INDEX `{}`".format(unique_index_record[0].Key_name)) | |||
index_constraint_changed = current_column.index != col.set_index | |||
# if index key exists | |||
if frappe.db.sql("""SHOW INDEX FROM `{0}` | |||
WHERE key_name=%s | |||
AND Non_unique=%s""".format(self.table_name), (col.fieldname, col.unique)): | |||
drop_index_query.append("drop index `{}`".format(col.fieldname)) | |||
if index_constraint_changed and not col.set_index: | |||
# nosemgrep | |||
index_record = frappe.db.sql(""" | |||
SHOW INDEX FROM `{0}` | |||
WHERE Key_name=%s | |||
AND Non_unique=1 | |||
""".format(self.table_name), (col.fieldname + '_index'), as_dict=1) | |||
if index_record: | |||
drop_index_query.append("DROP INDEX `{}`".format(index_record[0].Key_name)) | |||
try: | |||
for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]: | |||
@@ -74,10 +74,16 @@ class PostgresDatabase(Database): | |||
return conn | |||
def escape(self, s, percent=True): | |||
"""Excape quotes and percent in given string.""" | |||
"""Escape quotes and percent in given string.""" | |||
if isinstance(s, bytes): | |||
s = s.decode('utf-8') | |||
# MariaDB's driver treats None as an empty string | |||
# So Postgres should do the same | |||
if s is None: | |||
s = '' | |||
if percent: | |||
s = s.replace("%", "%%") | |||
@@ -302,18 +308,20 @@ class PostgresDatabase(Database): | |||
WHEN 'timestamp without time zone' THEN 'timestamp' | |||
ELSE a.data_type | |||
END AS type, | |||
COUNT(b.indexdef) AS Index, | |||
BOOL_OR(b.index) AS index, | |||
SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default, | |||
BOOL_OR(b.unique) AS unique | |||
FROM information_schema.columns a | |||
LEFT JOIN | |||
(SELECT indexdef, tablename, indexdef LIKE '%UNIQUE INDEX%' AS unique | |||
(SELECT indexdef, tablename, | |||
indexdef LIKE '%UNIQUE INDEX%' AS unique, | |||
indexdef NOT LIKE '%UNIQUE INDEX%' AS index | |||
FROM pg_indexes | |||
WHERE tablename='{table_name}') b | |||
ON SUBSTRING(b.indexdef, '\(.*\)') LIKE CONCAT('%', a.column_name, '%') | |||
ON SUBSTRING(b.indexdef, '(.*)') LIKE CONCAT('%', a.column_name, '%') | |||
WHERE a.table_name = '{table_name}' | |||
GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length;''' | |||
.format(table_name=table_name), as_dict=1) | |||
GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length; | |||
'''.format(table_name=table_name), as_dict=1) | |||
def get_database_list(self, target): | |||
return [d[0] for d in self.sql("SELECT datname FROM pg_database;")] | |||
@@ -11,8 +11,6 @@ class PostgresTable(DBTable): | |||
column_defs = self.get_column_definitions() | |||
if column_defs: add_text += ',\n'.join(column_defs) | |||
# index | |||
# index_defs = self.get_index_definitions() | |||
# TODO: set docstatus length | |||
# create table | |||
frappe.db.sql("""create table `%s` ( | |||
@@ -28,8 +26,25 @@ class PostgresTable(DBTable): | |||
idx bigint not null default '0', | |||
%s)""".format(varchar_len=frappe.db.VARCHAR_LEN) % (self.table_name, add_text)) | |||
self.create_indexes() | |||
frappe.db.commit() | |||
def create_indexes(self): | |||
create_index_query = "" | |||
for key, col in self.columns.items(): | |||
if (col.set_index | |||
and col.fieldtype in frappe.db.type_map | |||
and frappe.db.type_map.get(col.fieldtype)[0] | |||
not in ('text', 'longtext')): | |||
create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( | |||
index_name=col.fieldname, | |||
table_name=self.table_name, | |||
field=col.fieldname | |||
) | |||
if create_index_query: | |||
# nosemgrep | |||
frappe.db.sql(create_index_query) | |||
def alter(self): | |||
for col in self.columns.values(): | |||
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower())) | |||
@@ -52,8 +67,8 @@ class PostgresTable(DBTable): | |||
query.append("ALTER COLUMN `{0}` TYPE {1} {2}".format( | |||
col.fieldname, | |||
get_definition(col.fieldtype, precision=col.precision, length=col.length), | |||
using_clause) | |||
) | |||
using_clause | |||
)) | |||
for col in self.set_default: | |||
if col.fieldname=="name": | |||
@@ -73,37 +88,54 @@ class PostgresTable(DBTable): | |||
query.append("ALTER COLUMN `{}` SET DEFAULT {}".format(col.fieldname, col_default)) | |||
create_index_query = "" | |||
create_contraint_query = "" | |||
for col in self.add_index: | |||
# if index key not exists | |||
create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( | |||
create_contraint_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( | |||
index_name=col.fieldname, | |||
table_name=self.table_name, | |||
field=col.fieldname) | |||
drop_index_query = "" | |||
for col in self.add_unique: | |||
# if index key not exists | |||
create_contraint_query += 'CREATE UNIQUE INDEX IF NOT EXISTS "unique_{index_name}" ON `{table_name}`(`{field}`);'.format( | |||
index_name=col.fieldname, | |||
table_name=self.table_name, | |||
field=col.fieldname | |||
) | |||
drop_contraint_query = "" | |||
for col in self.drop_index: | |||
# primary key | |||
if col.fieldname != 'name': | |||
# if index key exists | |||
if not frappe.db.has_index(self.table_name, col.fieldname): | |||
drop_index_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname) | |||
drop_contraint_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname) | |||
if query: | |||
try: | |||
for col in self.drop_unique: | |||
# primary key | |||
if col.fieldname != 'name': | |||
# if index key exists | |||
drop_contraint_query += 'DROP INDEX IF EXISTS "unique_{}" ;'.format(col.fieldname) | |||
try: | |||
if query: | |||
final_alter_query = "ALTER TABLE `{}` {}".format(self.table_name, ", ".join(query)) | |||
if final_alter_query: frappe.db.sql(final_alter_query) | |||
if create_index_query: frappe.db.sql(create_index_query) | |||
if drop_index_query: frappe.db.sql(drop_index_query) | |||
except Exception as e: | |||
# sanitize | |||
if frappe.db.is_duplicate_fieldname(e): | |||
frappe.throw(str(e)) | |||
elif frappe.db.is_duplicate_entry(e): | |||
fieldname = str(e).split("'")[-2] | |||
frappe.throw(_("""{0} field cannot be set as unique in {1}, | |||
as there are non-unique existing values""".format( | |||
fieldname, self.table_name))) | |||
raise e | |||
else: | |||
raise e | |||
# nosemgrep | |||
frappe.db.sql(final_alter_query) | |||
if create_contraint_query: | |||
# nosemgrep | |||
frappe.db.sql(create_contraint_query) | |||
if drop_contraint_query: | |||
# nosemgrep | |||
frappe.db.sql(drop_contraint_query) | |||
except Exception as e: | |||
# sanitize | |||
if frappe.db.is_duplicate_fieldname(e): | |||
frappe.throw(str(e)) | |||
elif frappe.db.is_duplicate_entry(e): | |||
fieldname = str(e).split("'")[-2] | |||
frappe.throw( | |||
_("{0} field cannot be set as unique in {1}, as there are non-unique existing values") | |||
.format(fieldname, self.table_name) | |||
) | |||
else: | |||
raise e |
@@ -308,7 +308,7 @@ class Permission: | |||
doctype = [doctype] | |||
for dt in doctype: | |||
dt = re.sub("tab", "", dt) | |||
dt = re.sub("^tab", "", dt) | |||
if not frappe.has_permission( | |||
dt, | |||
"select", | |||
@@ -21,6 +21,7 @@ class DBTable: | |||
self.change_name = [] | |||
self.add_unique = [] | |||
self.add_index = [] | |||
self.drop_unique = [] | |||
self.drop_index = [] | |||
self.set_default = [] | |||
@@ -219,8 +220,10 @@ class DbColumn: | |||
self.table.change_type.append(self) | |||
# unique | |||
if((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')): | |||
if ((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')): | |||
self.table.add_unique.append(self) | |||
elif (current_def['unique'] and not self.unique): | |||
self.table.drop_unique.append(self) | |||
# default | |||
if (self.default_changed(current_def) | |||
@@ -230,9 +233,7 @@ class DbColumn: | |||
self.table.set_default.append(self) | |||
# index should be applied or dropped irrespective of type change | |||
if ((current_def['index'] and not self.set_index and not self.unique) | |||
or (current_def['unique'] and not self.unique)): | |||
# to drop unique you have to drop index | |||
if (current_def['index'] and not self.set_index): | |||
self.table.drop_index.append(self) | |||
elif (not current_def['index'] and self.set_index) and not (column_type in ('text', 'longtext')): | |||
@@ -126,7 +126,7 @@ def set_default(key, value, parent, parenttype="__default"): | |||
"defkey": key, | |||
"parent": parent | |||
}) | |||
if value != None: | |||
if value is not None: | |||
add_default(key, value, parent) | |||
else: | |||
_clear_cache(parent) | |||
@@ -187,7 +187,7 @@ def get_defaults_for(parent="__default"): | |||
"""get all defaults""" | |||
defaults = frappe.cache().hget("defaults", parent) | |||
if defaults==None: | |||
if defaults is None: | |||
# sort descending because first default must get precedence | |||
table = DocType("DefaultValue") | |||
res = frappe.qb.from_(table).where( | |||
@@ -7,6 +7,7 @@ from frappe.model.document import Document | |||
from frappe import _ | |||
from frappe.utils import cint | |||
class BulkUpdate(Document): | |||
pass | |||
@@ -22,7 +23,7 @@ def update(doctype, field, value, condition='', limit=500): | |||
frappe.throw(_('; not allowed in condition')) | |||
docnames = frappe.db.sql_list( | |||
'''select name from `tab{0}`{1} limit 0, {2}'''.format(doctype, condition, limit) | |||
'''select name from `tab{0}`{1} limit {2} offset 0'''.format(doctype, condition, limit) | |||
) | |||
data = {} | |||
data[field] = value | |||
@@ -1,23 +1,33 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2019, Frappe Technologies and contributors | |||
# Copyright (c) 2022, Frappe Technologies and contributors | |||
# License: MIT. See LICENSE | |||
from frappe.model.document import Document | |||
from frappe.modules.export_file import export_to_files | |||
from frappe.config import get_modules_from_all_apps_for_user | |||
import json | |||
import frappe | |||
from frappe import _ | |||
import json | |||
from frappe.config import get_modules_from_all_apps_for_user | |||
from frappe.model.document import Document | |||
from frappe.modules.export_file import export_to_files | |||
from frappe.query_builder import DocType | |||
class Dashboard(Document): | |||
def on_update(self): | |||
if self.is_default: | |||
# make all other dashboards non-default | |||
frappe.db.sql('''update | |||
tabDashboard set is_default = 0 where name != %s''', self.name) | |||
DashBoard = DocType("Dashboard") | |||
frappe.qb.update(DashBoard).set( | |||
DashBoard.is_default, 0 | |||
).where( | |||
DashBoard.name != self.name | |||
).run() | |||
if frappe.conf.developer_mode and self.is_standard: | |||
export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module) | |||
export_to_files( | |||
record_list=[["Dashboard", self.name, f"{self.module} Dashboard"]], | |||
record_module=self.module | |||
) | |||
def validate(self): | |||
if not frappe.conf.developer_mode and self.is_standard: | |||
@@ -94,30 +94,78 @@ def get_docinfo(doc=None, doctype=None, name=None): | |||
automated_messages = filter(lambda x: x['communication_type'] == 'Automated Message', all_communications) | |||
communications_except_auto_messages = filter(lambda x: x['communication_type'] != 'Automated Message', all_communications) | |||
frappe.response["docinfo"] = { | |||
docinfo = frappe._dict(user_info = {}) | |||
add_comments(doc, docinfo) | |||
docinfo.update({ | |||
"attachments": get_attachments(doc.doctype, doc.name), | |||
"attachment_logs": get_comments(doc.doctype, doc.name, 'attachment'), | |||
"communications": communications_except_auto_messages, | |||
"automated_messages": automated_messages, | |||
'comments': get_comments(doc.doctype, doc.name), | |||
'total_comments': len(json.loads(doc.get('_comments') or '[]')), | |||
'versions': get_versions(doc), | |||
"assignments": get_assignments(doc.doctype, doc.name), | |||
"assignment_logs": get_comments(doc.doctype, doc.name, 'assignment'), | |||
"permissions": get_doc_permissions(doc), | |||
"shared": frappe.share.get_users(doc.doctype, doc.name), | |||
"info_logs": get_comments(doc.doctype, doc.name, comment_type=['Info', 'Edit', 'Label']), | |||
"share_logs": get_comments(doc.doctype, doc.name, 'share'), | |||
"like_logs": get_comments(doc.doctype, doc.name, 'Like'), | |||
"workflow_logs": get_comments(doc.doctype, doc.name, comment_type="Workflow"), | |||
"views": get_view_logs(doc.doctype, doc.name), | |||
"energy_point_logs": get_point_logs(doc.doctype, doc.name), | |||
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name), | |||
"milestones": get_milestones(doc.doctype, doc.name), | |||
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user), | |||
"tags": get_tags(doc.doctype, doc.name), | |||
"document_email": get_document_email(doc.doctype, doc.name) | |||
} | |||
"document_email": get_document_email(doc.doctype, doc.name), | |||
}) | |||
update_user_info(docinfo) | |||
frappe.response["docinfo"] = docinfo | |||
def add_comments(doc, docinfo): | |||
# divide comments into separate lists | |||
docinfo.comments = [] | |||
docinfo.shared = [] | |||
docinfo.assignment_logs = [] | |||
docinfo.attachment_logs = [] | |||
docinfo.info_logs = [] | |||
docinfo.like_logs = [] | |||
docinfo.workflow_logs = [] | |||
comments = frappe.get_all("Comment", | |||
fields=["name", "creation", "content", "owner", "comment_type"], | |||
filters={ | |||
"reference_doctype": doc.doctype, | |||
"reference_name": doc.name | |||
} | |||
) | |||
for c in comments: | |||
if c.comment_type == "Comment": | |||
c.content = frappe.utils.markdown(c.content) | |||
docinfo.comments.append(c) | |||
elif c.comment_type in ('Shared', 'Unshared'): | |||
docinfo.shared.append(c) | |||
elif c.comment_type in ('Assignment Completed', 'Assigned'): | |||
docinfo.assignment_logs.append(c) | |||
elif c.comment_type in ('Attachment', 'Attachment Removed'): | |||
docinfo.attachment_logs.append(c) | |||
elif c.comment_type in ('Info', 'Edit', 'Label'): | |||
docinfo.info_logs.append(c) | |||
elif c.comment_type == "Like": | |||
docinfo.like_logs.append(c) | |||
elif c.comment_type == "Workflow": | |||
docinfo.workflow_logs.append(c) | |||
frappe.utils.add_user_info(c.owner, docinfo.user_info) | |||
return comments | |||
def get_milestones(doctype, name): | |||
return frappe.db.get_all('Milestone', fields = ['creation', 'owner', 'track_field', 'value'], | |||
@@ -252,7 +300,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= | |||
return communications | |||
def get_assignments(dt, dn): | |||
cl = frappe.get_all("ToDo", | |||
return frappe.get_all("ToDo", | |||
fields=['name', 'allocated_to as owner', 'description', 'status'], | |||
filters={ | |||
'reference_type': dt, | |||
@@ -260,8 +308,6 @@ def get_assignments(dt, dn): | |||
'status': ('!=', 'Cancelled'), | |||
}) | |||
return cl | |||
@frappe.whitelist() | |||
def get_badge_info(doctypes, filters): | |||
filters = json.loads(filters) | |||
@@ -319,3 +365,24 @@ def get_additional_timeline_content(doctype, docname): | |||
contents.extend(frappe.get_attr(method)(doctype, docname) or []) | |||
return contents | |||
def update_user_info(docinfo): | |||
for d in docinfo.communications: | |||
frappe.utils.add_user_info(d.sender, docinfo.user_info) | |||
for d in docinfo.shared: | |||
frappe.utils.add_user_info(d.user, docinfo.user_info) | |||
for d in docinfo.assignments: | |||
frappe.utils.add_user_info(d.owner, docinfo.user_info) | |||
for d in docinfo.views: | |||
frappe.utils.add_user_info(d.owner, docinfo.user_info) | |||
@frappe.whitelist() | |||
def get_user_info_for_viewers(users): | |||
user_info = {} | |||
for user in json.loads(users): | |||
frappe.utils.add_user_info(user, user_info) | |||
return user_info |
@@ -524,7 +524,7 @@ def get_last_modified(doctype): | |||
raise | |||
# hack: save as -1 so that it is cached | |||
if last_modified==None: | |||
if last_modified is None: | |||
last_modified = -1 | |||
return last_modified | |||
@@ -388,7 +388,6 @@ def make_records(records, debug=False): | |||
# LOG every success and failure | |||
for record in records: | |||
doctype = record.get("doctype") | |||
condition = record.get('__condition') | |||
@@ -405,6 +404,7 @@ def make_records(records, debug=False): | |||
try: | |||
doc.insert(ignore_permissions=True) | |||
frappe.db.commit() | |||
except frappe.DuplicateEntryError as e: | |||
# print("Failed to insert duplicate {0} {1}".format(doctype, doc.name)) | |||
@@ -417,6 +417,7 @@ def make_records(records, debug=False): | |||
raise | |||
except Exception as e: | |||
frappe.db.rollback() | |||
exception = record.get('__exception') | |||
if exception: | |||
config = _dict(exception) | |||
@@ -30,7 +30,7 @@ def get_energy_points_percentage_chart_data(user, field): | |||
as_list = True) | |||
return { | |||
"labels": [r[0] for r in result if r[0] != None], | |||
"labels": [r[0] for r in result if r[0] is not None], | |||
"datasets": [{ | |||
"values": [r[1] for r in result] | |||
}] | |||
@@ -12,7 +12,7 @@ from io import StringIO | |||
from frappe.core.doctype.access_log.access_log import make_access_log | |||
from frappe.utils import cstr, format_duration | |||
from frappe.model.base_document import get_controller | |||
from frappe.utils import add_user_info | |||
@frappe.whitelist() | |||
@frappe.read_only() | |||
@@ -219,6 +219,8 @@ def compress(data, args=None): | |||
"""separate keys and values""" | |||
from frappe.desk.query_report import add_total_row | |||
user_info = {} | |||
if not data: return data | |||
if args is None: | |||
args = {} | |||
@@ -230,13 +232,19 @@ def compress(data, args=None): | |||
new_row.append(row.get(key)) | |||
values.append(new_row) | |||
# add user info for assignments (avatar) | |||
if row._assign: | |||
for user in json.loads(row._assign): | |||
add_user_info(user, user_info) | |||
if args.get("add_total_row"): | |||
meta = frappe.get_meta(args.doctype) | |||
values = add_total_row(values, keys, meta) | |||
return { | |||
"keys": keys, | |||
"values": values | |||
"values": values, | |||
"user_info": user_info | |||
} | |||
@frappe.whitelist() | |||
@@ -107,7 +107,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||
else: | |||
filters.append([doctype, f[0], "=", f[1]]) | |||
if filters==None: | |||
if filters is None: | |||
filters = [] | |||
or_filters = [] | |||
@@ -4,6 +4,7 @@ | |||
import frappe | |||
from frappe import _ | |||
@frappe.whitelist() | |||
def get_all_nodes(doctype, label, parent, tree_method, **filters): | |||
'''Recursively gets all data from tree nodes''' | |||
@@ -40,8 +41,8 @@ def get_children(doctype, parent='', **filters): | |||
def _get_children(doctype, parent='', ignore_permissions=False): | |||
parent_field = 'parent_' + doctype.lower().replace(' ', '_') | |||
filters = [['ifnull(`{0}`,"")'.format(parent_field), '=', parent], | |||
['docstatus', '<' ,'2']] | |||
filters = [["ifnull(`{0}`,'')".format(parent_field), '=', parent], | |||
['docstatus', '<' ,2]] | |||
meta = frappe.get_meta(doctype) | |||
@@ -475,28 +475,20 @@ class QueueBuilder: | |||
if self._unsubscribed_user_emails is not None: | |||
return self._unsubscribed_user_emails | |||
all_ids = tuple(set(self.recipients + self.cc)) | |||
unsubscribed = frappe.db.sql_list(''' | |||
SELECT | |||
distinct email | |||
from | |||
`tabEmail Unsubscribe` | |||
where | |||
email in %(all_ids)s | |||
and ( | |||
( | |||
reference_doctype = %(reference_doctype)s | |||
and reference_name = %(reference_name)s | |||
) | |||
or global_unsubscribe = 1 | |||
) | |||
''', { | |||
'all_ids': all_ids, | |||
'reference_doctype': self.reference_doctype, | |||
'reference_name': self.reference_name, | |||
}) | |||
all_ids = list(set(self.recipients + self.cc)) | |||
EmailUnsubscribe = frappe.qb.DocType("Email Unsubscribe") | |||
unsubscribed = (frappe.qb.from_(EmailUnsubscribe) | |||
.select(EmailUnsubscribe.email) | |||
.where(EmailUnsubscribe.email.isin(all_ids) & | |||
( | |||
( | |||
(EmailUnsubscribe.reference_doctype == self.reference_doctype) & (EmailUnsubscribe.reference_name == self.reference_name) | |||
) | EmailUnsubscribe.global_unsubscribe == 1 | |||
) | |||
).distinct() | |||
).run(pluck=True) | |||
self._unsubscribed_user_emails = unsubscribed or [] | |||
return self._unsubscribed_user_emails | |||
@@ -27,11 +27,7 @@ from frappe.utils.html_utils import clean_email_html | |||
# fix due to a python bug in poplib that limits it to 2048 | |||
poplib._MAXLINE = 20480 | |||
imaplib._MAXLINE = 20480 | |||
# fix due to a python bug in poplib that limits it to 2048 | |||
poplib._MAXLINE = 20480 | |||
imaplib._MAXLINE = 20480 | |||
class EmailSizeExceededError(frappe.ValidationError): pass | |||
@@ -154,7 +154,7 @@ def install_app(name, verbose=False, set_as_patched=True): | |||
for before_install in app_hooks.before_install or []: | |||
out = frappe.get_attr(before_install)() | |||
if out==False: | |||
if out is False: | |||
return | |||
if name != "frappe": | |||
@@ -96,7 +96,7 @@ | |||
}, | |||
{ | |||
"fieldname": "authorization_uri", | |||
"fieldtype": "Data", | |||
"fieldtype": "Small Text", | |||
"label": "Authorization URI", | |||
"mandatory_depends_on": "eval:doc.redirect_uri" | |||
}, | |||
@@ -139,7 +139,7 @@ | |||
"link_fieldname": "connected_app" | |||
} | |||
], | |||
"modified": "2021-05-10 05:03:06.296863", | |||
"modified": "2022-01-07 05:28:45.073041", | |||
"modified_by": "Administrator", | |||
"module": "Integrations", | |||
"name": "Connected App", | |||
@@ -172,7 +172,7 @@ class BaseDocument(object): | |||
... | |||
}) | |||
""" | |||
if value==None: | |||
if value is None: | |||
value={} | |||
if isinstance(value, (dict, BaseDocument)): | |||
if not self.__dict__.get(key): | |||
@@ -272,7 +272,7 @@ class BaseDocument(object): | |||
)): | |||
d[fieldname] = str(d[fieldname]) | |||
if d[fieldname] == None and ignore_nulls: | |||
if d[fieldname] is None and ignore_nulls: | |||
del d[fieldname] | |||
return d | |||
@@ -646,8 +646,6 @@ class BaseDocument(object): | |||
value, comma_options)) | |||
def _validate_data_fields(self): | |||
from frappe.core.doctype.user.user import STANDARD_USERS | |||
# data_field options defined in frappe.model.data_field_options | |||
for data_field in self.meta.get_data_fields(): | |||
data = self.get(data_field.fieldname) | |||
@@ -658,7 +656,7 @@ class BaseDocument(object): | |||
continue | |||
if data_field_options == "Email": | |||
if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS): | |||
if (self.owner in frappe.STANDARD_USERS) and (data in frappe.STANDARD_USERS): | |||
continue | |||
for email_address in frappe.utils.split_emails(data): | |||
frappe.utils.validate_email_address(email_address, throw=True) | |||
@@ -768,7 +766,9 @@ class BaseDocument(object): | |||
else: | |||
self_value = self.get_value(key) | |||
# Postgres stores values as `datetime.time`, MariaDB as `timedelta` | |||
if isinstance(self_value, datetime.timedelta) and isinstance(db_value, datetime.time): | |||
db_value = datetime.timedelta(hours=db_value.hour, minutes=db_value.minute, seconds=db_value.second, microseconds=db_value.microsecond) | |||
if self_value != db_value: | |||
frappe.throw(_("Not allowed to change {0} after submission").format(df.label), | |||
frappe.UpdateAfterSubmitError) | |||
@@ -1008,15 +1008,12 @@ def _filter(data, filters, limit=None): | |||
_filters[f] = fval | |||
for d in data: | |||
add = True | |||
for f, fval in _filters.items(): | |||
if not frappe.compare(getattr(d, f, None), fval[0], fval[1]): | |||
add = False | |||
break | |||
if add: | |||
else: | |||
out.append(d) | |||
if limit and (len(out)-1)==limit: | |||
if limit and len(out) >= limit: | |||
break | |||
return out |
@@ -130,6 +130,11 @@ class DatabaseQuery(object): | |||
args.fields = 'distinct ' + args.fields | |||
args.order_by = '' # TODO: recheck for alternative | |||
# Postgres requires any field that appears in the select clause to also | |||
# appear in the order by and group by clause | |||
if frappe.db.db_type == 'postgres' and args.order_by and args.group_by: | |||
args = self.prepare_select_args(args) | |||
query = """select %(fields)s | |||
from %(tables)s | |||
%(conditions)s | |||
@@ -203,6 +208,19 @@ class DatabaseQuery(object): | |||
return args | |||
def prepare_select_args(self, args): | |||
order_field = re.sub(r"\ order\ by\ |\ asc|\ ASC|\ desc|\ DESC", "", args.order_by) | |||
if order_field not in args.fields: | |||
extracted_column = order_column = order_field.replace("`", "") | |||
if "." in extracted_column: | |||
extracted_column = extracted_column.split(".")[1] | |||
args.fields += f", MAX({extracted_column}) as `{order_column}`" | |||
args.order_by = args.order_by.replace(order_field, f"`{order_column}`") | |||
return args | |||
def parse_args(self): | |||
"""Convert fields and filters from strings to list, dicts""" | |||
if isinstance(self.fields, str): | |||
@@ -527,7 +545,7 @@ class DatabaseQuery(object): | |||
elif f.operator.lower() in ("like", "not like") or (isinstance(f.value, str) and | |||
(not df or df.fieldtype not in ["Float", "Int", "Currency", "Percent", "Check"])): | |||
value = "" if f.value==None else f.value | |||
value = "" if f.value is None else f.value | |||
fallback = "''" | |||
if f.operator.lower() in ("like", "not like") and isinstance(value, str): | |||
@@ -188,6 +188,8 @@ class Document(BaseDocument): | |||
is not set. | |||
:param permtype: one of `read`, `write`, `submit`, `cancel`, `delete`""" | |||
import frappe.permissions | |||
if self.flags.ignore_permissions: | |||
return True | |||
return frappe.permissions.has_permission(self.doctype, permtype, self, verbose=verbose) | |||
@@ -209,13 +211,13 @@ class Document(BaseDocument): | |||
self.flags.notifications_executed = [] | |||
if ignore_permissions!=None: | |||
if ignore_permissions is not None: | |||
self.flags.ignore_permissions = ignore_permissions | |||
if ignore_links!=None: | |||
if ignore_links is not None: | |||
self.flags.ignore_links = ignore_links | |||
if ignore_mandatory!=None: | |||
if ignore_mandatory is not None: | |||
self.flags.ignore_mandatory = ignore_mandatory | |||
self.set("__islocal", True) | |||
@@ -295,7 +297,7 @@ class Document(BaseDocument): | |||
self.flags.notifications_executed = [] | |||
if ignore_permissions!=None: | |||
if ignore_permissions is not None: | |||
self.flags.ignore_permissions = ignore_permissions | |||
self.flags.ignore_version = frappe.flags.in_test if ignore_version is None else ignore_version | |||
@@ -439,7 +441,7 @@ class Document(BaseDocument): | |||
values = self.as_dict() | |||
# format values | |||
for key, value in values.items(): | |||
if value==None: | |||
if value is None: | |||
values[key] = "" | |||
return values | |||
@@ -487,7 +489,7 @@ class Document(BaseDocument): | |||
frappe.flags.currently_saving.append((self.doctype, self.name)) | |||
def set_docstatus(self): | |||
if self.docstatus==None: | |||
if self.docstatus is None: | |||
self.docstatus=0 | |||
for d in self.get_all_children(): | |||
@@ -885,14 +887,14 @@ class Document(BaseDocument): | |||
if (frappe.flags.in_import and frappe.flags.mute_emails) or frappe.flags.in_patch or frappe.flags.in_install: | |||
return | |||
if self.flags.notifications_executed==None: | |||
if self.flags.notifications_executed is None: | |||
self.flags.notifications_executed = [] | |||
from frappe.email.doctype.notification.notification import evaluate_alert | |||
if self.flags.notifications == None: | |||
if self.flags.notifications is None: | |||
alerts = frappe.cache().hget('notifications', self.doctype) | |||
if alerts==None: | |||
if alerts is None: | |||
alerts = frappe.get_all('Notification', fields=['name', 'event', 'method'], | |||
filters={'enabled': 1, 'document_type': self.doctype}) | |||
frappe.cache().hset('notifications', self.doctype, alerts) | |||
@@ -32,7 +32,7 @@ def get_dynamic_link_map(for_delete=False): | |||
Note: Will not map single doctypes | |||
''' | |||
if getattr(frappe.local, 'dynamic_link_map', None)==None or frappe.flags.in_test: | |||
if getattr(frappe.local, 'dynamic_link_map', None) is None or frappe.flags.in_test: | |||
# Build from scratch | |||
dynamic_link_map = {} | |||
for df in get_dynamic_links(): | |||
@@ -123,6 +123,9 @@ frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats | |||
frappe.patches.v12_0.remove_example_email_thread_notify | |||
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() | |||
frappe.patches.v12_0.set_correct_url_in_files | |||
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) | |||
execute:frappe.reload_doc('custom', 'doctype', 'property_setter') | |||
frappe.patches.v13_0.remove_invalid_options_for_data_fields | |||
frappe.patches.v13_0.website_theme_custom_scss | |||
frappe.patches.v13_0.make_user_type | |||
frappe.patches.v13_0.set_existing_dashboard_charts_as_public | |||
@@ -153,7 +156,6 @@ frappe.patches.v13_0.rename_notification_fields | |||
frappe.patches.v13_0.remove_duplicate_navbar_items | |||
frappe.patches.v13_0.set_social_icons | |||
frappe.patches.v12_0.set_default_password_reset_limit | |||
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) | |||
frappe.patches.v13_0.set_route_for_blog_category | |||
frappe.patches.v13_0.enable_custom_script | |||
frappe.patches.v13_0.update_newsletter_content_type | |||
@@ -179,7 +181,6 @@ frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings | |||
frappe.patches.v13_0.remove_twilio_settings | |||
frappe.patches.v12_0.rename_uploaded_files_with_proper_name | |||
frappe.patches.v13_0.queryreport_columns | |||
execute:frappe.reload_doc('core', 'doctype', 'doctype') | |||
frappe.patches.v13_0.jinja_hook | |||
frappe.patches.v13_0.update_notification_channel_if_empty | |||
frappe.patches.v13_0.set_first_day_of_the_week | |||
@@ -33,7 +33,7 @@ def execute(): | |||
continue | |||
skip_for_doctype = user_permission.skip_for_doctype.split('\n') | |||
else: # while migrating from v10 -> v11 | |||
if skip_for_doctype_map.get((user_permission.allow, user_permission.user)) == None: | |||
if skip_for_doctype_map.get((user_permission.allow, user_permission.user)) is None: | |||
skip_for_doctype = get_doctypes_to_skip(user_permission.allow, user_permission.user) | |||
# cache skip for doctype for same user and doctype | |||
skip_for_doctype_map[(user_permission.allow, user_permission.user)] = skip_for_doctype | |||
@@ -0,0 +1,17 @@ | |||
# Copyright (c) 2022, Frappe and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe.model import data_field_options | |||
def execute(): | |||
custom_field = frappe.qb.DocType('Custom Field') | |||
(frappe.qb | |||
.update(custom_field) | |||
.set(custom_field.options, None) | |||
.where( | |||
(custom_field.fieldtype == "Data") | |||
& (custom_field.options.notin(data_field_options))) | |||
).run() |
@@ -23,7 +23,7 @@ def print_has_permission_check_logs(func): | |||
frappe.flags['has_permission_check_logs'] = [] | |||
result = func(*args, **kwargs) | |||
self_perm_check = True if not kwargs.get('user') else kwargs.get('user') == frappe.session.user | |||
raise_exception = False if kwargs.get('raise_exception') == False else True | |||
raise_exception = False if kwargs.get('raise_exception') is False else True | |||
# print only if access denied | |||
# and if user is checking his own permission | |||
@@ -559,7 +559,9 @@ def filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc= | |||
return (allowed_doc, default_doc) if with_default_doc else allowed_doc | |||
def push_perm_check_log(log): | |||
if frappe.flags.get('has_permission_check_logs') == None: return | |||
if frappe.flags.get('has_permission_check_logs') is None: | |||
return | |||
frappe.flags.get('has_permission_check_logs').append(_(log)) | |||
def has_child_table_permission(child_doctype, ptype="read", child_doc=None, | |||
@@ -0,0 +1,6 @@ | |||
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
<path d="M26.4844 25.3281V16.0781" stroke="#F56B6B" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | |||
<path d="M42.6719 25.3281V16.0781" stroke="#F56B6B" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | |||
<path d="M34.5781 50.7656C30.8982 50.7656 27.3691 49.3038 24.767 46.7017C22.165 44.0997 20.7031 40.5705 20.7031 36.8906V25.3281H48.4531V36.8906C48.4531 40.5705 46.9913 44.0997 44.3892 46.7017C41.7872 49.3038 38.258 50.7656 34.5781 50.7656Z" stroke="#98A1A9" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | |||
<path d="M57.7032 58.8594C63.3462 53.4851 66.9411 46.3131 67.8703 38.576C68.7994 30.8388 67.0046 23.0197 62.7944 16.4622C58.5842 9.90464 52.2215 5.01829 44.7997 2.64279C37.3778 0.267296 29.3604 0.550997 22.125 3.44515C14.8896 6.33929 8.8882 11.6632 5.15204 18.5018C1.41588 25.3405 0.178293 33.267 1.65196 40.9191C3.12562 48.5713 7.21851 55.4712 13.2273 60.4332C19.236 65.3952 26.7855 68.1094 34.5782 68.1094V56.5469" stroke="#98A1A9" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | |||
</svg> |
@@ -214,19 +214,20 @@ frappe.Application = class Application { | |||
email_password_prompt(email_account,user,i) { | |||
var me = this; | |||
const email_id = email_account[i]["email_id"]; | |||
let d = new frappe.ui.Dialog({ | |||
title: __('Password missing in Email Account'), | |||
fields: [ | |||
{ | |||
'fieldname': 'password', | |||
'fieldtype': 'Password', | |||
'label': __('Please enter the password for: <b>{0}</b>', [email_account[i]["email_id"]]), | |||
'label': __('Please enter the password for: <b>{0}</b>', [email_id], "Email Account"), | |||
'reqd': 1 | |||
}, | |||
{ | |||
"fieldname": "submit", | |||
"fieldtype": "Button", | |||
"label": __("Submit") | |||
"label": __("Submit", null, "Submit password for Email Account") | |||
} | |||
] | |||
}); | |||
@@ -10,14 +10,16 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat | |||
this.set_t_for_today(); | |||
} | |||
set_formatted_input(value) { | |||
if (value === "Today") { | |||
value = this.get_now_date(); | |||
} | |||
super.set_formatted_input(value); | |||
if (this.timepicker_only) return; | |||
if (!this.datepicker) return; | |||
if (!value) { | |||
this.datepicker.clear(); | |||
return; | |||
} else if (value === "Today") { | |||
value = this.get_now_date(); | |||
} | |||
let should_refresh = this.last_value && this.last_value !== value; | |||
@@ -78,7 +80,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat | |||
} | |||
get_start_date() { | |||
return new Date(this.get_now_date()); | |||
return this.get_now_date(); | |||
} | |||
set_datepicker() { | |||
@@ -117,7 +119,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat | |||
this.datepicker.update('position', position); | |||
} | |||
get_now_date() { | |||
return frappe.datetime.convert_to_system_tz(frappe.datetime.now_date(true)); | |||
return frappe.datetime.convert_to_system_tz(frappe.datetime.now_date(true), false).toDate(); | |||
} | |||
set_t_for_today() { | |||
var me = this; | |||
@@ -11,7 +11,8 @@ frappe.ui.form.ControlDateRange = class ControlDateRange extends frappe.ui.form. | |||
language: "en", | |||
range: true, | |||
autoClose: true, | |||
toggleSelected: false | |||
toggleSelected: false, | |||
firstDay: frappe.datetime.get_first_day_of_the_week_index() | |||
}; | |||
this.datepicker_options.dateFormat = | |||
(frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd'); | |||
@@ -32,7 +32,9 @@ frappe.ui.form.ControlMarkdownEditor = class ControlMarkdownEditor extends frapp | |||
} | |||
set_language() { | |||
this.df.options = 'Markdown'; | |||
if (!this.df.options) { | |||
this.df.options = 'Markdown'; | |||
} | |||
super.set_language(); | |||
} | |||
@@ -12,8 +12,11 @@ class BaseTimeline { | |||
this.wrapper = this.timeline_wrapper; | |||
this.timeline_items_wrapper = $(`<div class="timeline-items">`); | |||
this.timeline_actions_wrapper = $(` | |||
<div class="timeline-actions"> | |||
<div class="timeline-dot"></div> | |||
<div class="timeline-items timeline-actions"> | |||
<div class="timeline-item"> | |||
<div class="timeline-dot"></div> | |||
<div class="timeline-content action-buttons"></div> | |||
</div> | |||
</div> | |||
`); | |||
@@ -37,7 +40,7 @@ class BaseTimeline { | |||
${label} | |||
</button>`); | |||
action_btn.click(action); | |||
this.timeline_actions_wrapper.append(action_btn); | |||
this.timeline_actions_wrapper.find('.action-buttons').append(action_btn); | |||
return action_btn; | |||
} | |||
@@ -77,12 +77,14 @@ class FormTimeline extends BaseTimeline { | |||
const message = __("Add to this activity by mailing to {0}", [link.bold()]); | |||
this.document_email_link_wrapper = $(` | |||
<div class="document-email-link-container"> | |||
<div class="timeline-item"> | |||
<div class="timeline-dot"></div> | |||
<span class="ellipsis">${message}</span> | |||
<div class="timeline-content"> | |||
<span>${message}</span> | |||
</div> | |||
</div> | |||
`); | |||
this.timeline_wrapper.append(this.document_email_link_wrapper); | |||
this.timeline_actions_wrapper.append(this.document_email_link_wrapper); | |||
this.document_email_link_wrapper | |||
.find('.document-email-link') | |||
@@ -943,7 +943,10 @@ frappe.ui.form.Form = class FrappeForm { | |||
// re-enable buttons | |||
resolve(); | |||
} | |||
frappe.throw (__("No permission to '{0}' {1}", [__(action), __(this.doc.doctype)])); | |||
frappe.throw( | |||
__("No permission to '{0}' {1}", [__(action), __(this.doc.doctype)], "{0} = verb, {1} = object") | |||
); | |||
} | |||
} | |||
@@ -27,19 +27,40 @@ frappe.ui.form.FormViewers.set_users = function(data, type) { | |||
const users = data.users || []; | |||
const new_users = users.filter(user => !past_users.includes(user)); | |||
frappe.model.set_docinfo(doctype, docname, type, { | |||
past: past_users.concat(new_users), | |||
new: new_users, | |||
current: users | |||
}); | |||
if ( | |||
cur_frm && | |||
cur_frm.doc && | |||
cur_frm.doc.doctype === doctype && | |||
cur_frm.doc.name == docname && | |||
cur_frm.viewers | |||
) { | |||
cur_frm.viewers.refresh(true, type); | |||
if (new_users.length===0) return; | |||
const set_and_refresh = () => { | |||
const info = { | |||
past: past_users.concat(new_users), | |||
new: new_users, | |||
current: users | |||
}; | |||
frappe.model.set_docinfo(doctype, docname, type, info); | |||
if ( | |||
cur_frm && | |||
cur_frm.doc && | |||
cur_frm.doc.doctype === doctype && | |||
cur_frm.doc.name == docname && | |||
cur_frm.viewers | |||
) { | |||
cur_frm.viewers.refresh(true, type); | |||
} | |||
}; | |||
let unknown_users = []; | |||
for (let user of users) { | |||
if (!frappe.boot.user_info[user]) unknown_users.push(user); | |||
} | |||
if (unknown_users.length===0) { | |||
set_and_refresh(); | |||
} else { | |||
// load additional user info | |||
frappe.xcall('frappe.desk.form.load.get_user_info_for_viewers', {users: unknown_users}).then((data) => { | |||
Object.assign(frappe.boot.user_info, data); | |||
set_and_refresh(); | |||
}); | |||
} | |||
}; |
@@ -7,12 +7,12 @@ frappe.ui.form.save = function (frm, action, callback, btn) { | |||
$(btn).prop("disabled", true); | |||
// specified here because there are keyboard shortcuts to save | |||
var working_label = { | |||
"Save": __("Saving"), | |||
"Submit": __("Submitting"), | |||
"Update": __("Updating"), | |||
"Amend": __("Amending"), | |||
"Cancel": __("Cancelling") | |||
const working_label = { | |||
"Save": __("Saving", null, "Freeze message while saving a document"), | |||
"Submit": __("Submitting", null, "Freeze message while submitting a document"), | |||
"Update": __("Updating", null, "Freeze message while updating a document"), | |||
"Amend": __("Amending", null, "Freeze message while amending a document"), | |||
"Cancel": __("Cancelling", null, "Freeze message while cancelling a document"), | |||
}[toTitle(action)]; | |||
var freeze_message = working_label ? __(working_label) : ""; | |||
@@ -154,8 +154,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) { | |||
if (error_fields.length) { | |||
let meta = frappe.get_meta(doc.doctype); | |||
if (meta.istable) { | |||
var message = __('Mandatory fields required in table {0}, Row {1}', | |||
[__(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label).bold(), doc.idx]); | |||
const table_label = __(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label).bold(); | |||
var message = __('Mandatory fields required in table {0}, Row {1}', [table_label, doc.idx]); | |||
} else { | |||
var message = __('Mandatory fields required in {0}', [__(doc.doctype)]); | |||
} | |||
@@ -276,4 +276,3 @@ frappe.ui.form.update_calling_link = (newdoc) => { | |||
frappe._from_link = null; | |||
} | |||
} | |||
@@ -484,6 +484,11 @@ frappe.views.BaseList = class BaseList { | |||
prepare_data(r) { | |||
let data = r.message || {}; | |||
// extract user_info for assignments | |||
Object.assign(frappe.boot.user_info, data.user_info); | |||
delete data.user_info; | |||
data = !Array.isArray(data) | |||
? frappe.utils.dict(data.keys, data.values) | |||
: data; | |||
@@ -200,7 +200,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
frappe.render_template("list_view_permission_restrictions", { | |||
condition_list: match_rules_list, | |||
}), | |||
__("Restrictions") | |||
__("Restrictions", null, "Title of message showing restrictions in list view") | |||
); | |||
} | |||
@@ -255,8 +255,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
set_primary_action() { | |||
if (this.can_create) { | |||
const doctype_name = __(frappe.router.doctype_layout) || __(this.doctype); | |||
// Better style would be __("Add {0}", [doctype_name], "Primary action in list view") | |||
// Keeping it like this to not disrupt existing translations | |||
const label = `${__("Add", null, "Primary action in list view")} ${doctype_name}`; | |||
this.page.set_primary_action( | |||
`${__("Add")} ${frappe.router.doctype_layout || __(this.doctype)}`, | |||
label, | |||
() => { | |||
if (this.settings.primary_action) { | |||
this.settings.primary_action(); | |||
@@ -320,9 +325,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
setup_freeze_area() { | |||
this.$freeze = $( | |||
`<div class="freeze flex justify-center align-center text-muted">${__( | |||
"Loading" | |||
)}...</div>` | |||
`<div class="freeze flex justify-center align-center text-muted"> | |||
${__("Loading")}... | |||
</div>` | |||
).hide(); | |||
this.$result.append(this.$freeze); | |||
} | |||
@@ -460,8 +465,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
? __("No {0} found", [__(this.doctype)]) | |||
: __("You haven't created a {0} yet", [__(this.doctype)]); | |||
let new_button_label = filters && filters.length | |||
? __("Create a new {0}", [__(this.doctype)]) | |||
: __("Create your first {0}", [__(this.doctype)]); | |||
? __("Create a new {0}", [__(this.doctype)], "Create a new document from list view") | |||
: __("Create your first {0}", [__(this.doctype)], "Create a new document from list view"); | |||
let empty_state_image = | |||
this.settings.empty_state_image || | |||
"/assets/frappe/images/ui-states/list-empty-state.svg"; | |||
@@ -469,7 +474,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const new_button = this.can_create | |||
? `<p><button class="btn btn-primary btn-sm btn-new-doc hidden-xs"> | |||
${new_button_label} | |||
</button> <button class="btn btn-primary btn-new-doc visible-xs">${__('Create New')}</button></p>` | |||
</button> <button class="btn btn-primary btn-new-doc visible-xs"> | |||
${__("Create New", null, "Create a new document from list view")} | |||
</button></p>` | |||
: ""; | |||
return `<div class="msg-box no-border"> | |||
@@ -486,7 +493,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
if (this.list_view_settings && !this.list_view_settings.disable_count) { | |||
this.$result | |||
.find(".list-count") | |||
.html(`<span>${__("Refreshing")}...</span>`); | |||
.html(`<span>${__("Refreshing", null, "Document count in list view")}...</span>`); | |||
} | |||
} | |||
@@ -1081,14 +1088,14 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
frappe.ui.keys.add_shortcut({ | |||
shortcut: "down", | |||
action: () => handle_navigation("down"), | |||
description: __("Navigate list down"), | |||
description: __("Navigate list down", null, "Description of a list view shortcut"), | |||
page: this.page, | |||
}); | |||
frappe.ui.keys.add_shortcut({ | |||
shortcut: "up", | |||
action: () => handle_navigation("up"), | |||
description: __("Navigate list up"), | |||
description: __("Navigate list up", null, "Description of a list view shortcut"), | |||
page: this.page, | |||
}); | |||
@@ -1100,7 +1107,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
check_row($list_row); | |||
focus_next(); | |||
}, | |||
description: __("Select multiple list items"), | |||
description: __("Select multiple list items", null, "Description of a list view shortcut"), | |||
page: this.page, | |||
}); | |||
@@ -1112,7 +1119,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
check_row($list_row); | |||
focus_prev(); | |||
}, | |||
description: __("Select multiple list items"), | |||
description: __("Select multiple list items", null, "Description of a list view shortcut"), | |||
page: this.page, | |||
}); | |||
@@ -1126,7 +1133,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
} | |||
return false; | |||
}, | |||
description: __("Open list item"), | |||
description: __("Open list item", null, "Description of a list view shortcut"), | |||
page: this.page, | |||
}); | |||
@@ -1140,7 +1147,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
} | |||
return false; | |||
}, | |||
description: __("Select list item"), | |||
description: __("Select list item", null, "Description of a list view shortcut"), | |||
page: this.page, | |||
}); | |||
} | |||
@@ -1515,7 +1522,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
if (frappe.model.can_import(doctype, null, this.meta)) { | |||
items.push({ | |||
label: __("Import"), | |||
label: __("Import", null, "Button in list view menu"), | |||
action: () => | |||
frappe.set_route("list", "data-import", { | |||
reference_doctype: doctype, | |||
@@ -1526,7 +1533,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
if (frappe.model.can_set_user_permissions(doctype)) { | |||
items.push({ | |||
label: __("User Permissions"), | |||
label: __("User Permissions", null, "Button in list view menu"), | |||
action: () => | |||
frappe.set_route("list", "user-permission", { | |||
allow: doctype, | |||
@@ -1537,7 +1544,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
if (frappe.user_roles.includes("System Manager")) { | |||
items.push({ | |||
label: __("Role Permissions Manager"), | |||
label: __("Role Permissions Manager", null, "Button in list view menu"), | |||
action: () => | |||
frappe.set_route("permission-manager", { | |||
doctype, | |||
@@ -1546,7 +1553,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
}); | |||
items.push({ | |||
label: __("Customize"), | |||
label: __("Customize", null, "Button in list view menu"), | |||
action: () => { | |||
if (!this.meta) return; | |||
if (this.meta.custom) { | |||
@@ -1563,7 +1570,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
} | |||
items.push({ | |||
label: __("Toggle Sidebar"), | |||
label: __("Toggle Sidebar", null, "Button in list view menu"), | |||
action: () => this.toggle_side_bar(), | |||
condition: () => !this.hide_sidebar, | |||
standard: true, | |||
@@ -1571,7 +1578,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
}); | |||
items.push({ | |||
label: __("Share URL"), | |||
label: __("Share URL", null, "Button in list view menu"), | |||
action: () => this.share_url(), | |||
standard: true, | |||
shortcut: "Ctrl+L", | |||
@@ -1583,7 +1590,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
) { | |||
// edit doctype | |||
items.push({ | |||
label: __("Edit DocType"), | |||
label: __("Edit DocType", null, "Button in list view menu"), | |||
action: () => frappe.set_route("form", "doctype", doctype), | |||
standard: true, | |||
}); | |||
@@ -1591,7 +1598,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
if (frappe.user.has_role("System Manager")) { | |||
items.push({ | |||
label: __("List Settings"), | |||
label: __("List Settings", null, "Button in list view menu"), | |||
action: () => this.show_list_settings(), | |||
standard: true, | |||
}); | |||
@@ -1682,7 +1689,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
// utility | |||
const bulk_assignment = () => { | |||
return { | |||
label: __("Assign To"), | |||
label: __("Assign To", null, "Button in list view actions menu"), | |||
action: () => { | |||
this.disable_list_update = true; | |||
bulk_operations.assign( | |||
@@ -1700,7 +1707,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const bulk_assignment_rule = () => { | |||
return { | |||
label: __("Apply Assignment Rule"), | |||
label: __("Apply Assignment Rule", null, "Button in list view actions menu"), | |||
action: () => { | |||
this.disable_list_update = true; | |||
bulk_operations.apply_assignment_rule( | |||
@@ -1718,7 +1725,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const bulk_add_tags = () => { | |||
return { | |||
label: __("Add Tags"), | |||
label: __("Add Tags", null, "Button in list view actions menu"), | |||
action: () => { | |||
this.disable_list_update = true; | |||
bulk_operations.add_tags( | |||
@@ -1736,7 +1743,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const bulk_printing = () => { | |||
return { | |||
label: __("Print"), | |||
label: __("Print", null, "Button in list view actions menu"), | |||
action: () => bulk_operations.print(this.get_checked_items()), | |||
standard: true, | |||
}; | |||
@@ -1744,13 +1751,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const bulk_delete = () => { | |||
return { | |||
label: __("Delete"), | |||
label: __("Delete", null, "Button in list view actions menu"), | |||
action: () => { | |||
const docnames = this.get_checked_items(true).map( | |||
(docname) => docname.toString() | |||
); | |||
frappe.confirm( | |||
__("Delete {0} items permanently?", [docnames.length]), | |||
__("Delete {0} items permanently?", [docnames.length], "Title of confirmation dialog"), | |||
() => { | |||
this.disable_list_update = true; | |||
bulk_operations.delete(docnames, () => { | |||
@@ -1767,12 +1774,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const bulk_cancel = () => { | |||
return { | |||
label: __("Cancel"), | |||
label: __("Cancel", null, "Button in list view actions menu"), | |||
action: () => { | |||
const docnames = this.get_checked_items(true); | |||
if (docnames.length > 0) { | |||
frappe.confirm( | |||
__("Cancel {0} documents?", [docnames.length]), | |||
__("Cancel {0} documents?", [docnames.length], "Title of confirmation dialog"), | |||
() => { | |||
this.disable_list_update = true; | |||
bulk_operations.submit_or_cancel( | |||
@@ -1793,12 +1800,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const bulk_submit = () => { | |||
return { | |||
label: __("Submit"), | |||
label: __("Submit", null, "Button in list view actions menu"), | |||
action: () => { | |||
const docnames = this.get_checked_items(true); | |||
if (docnames.length > 0) { | |||
frappe.confirm( | |||
__("Submit {0} documents?", [docnames.length]), | |||
__("Submit {0} documents?", [docnames.length], "Title of confirmation dialog"), | |||
() => { | |||
this.disable_list_update = true; | |||
bulk_operations.submit_or_cancel( | |||
@@ -1820,7 +1827,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const bulk_edit = () => { | |||
return { | |||
label: __("Edit"), | |||
label: __("Edit", null, "Button in list view actions menu"), | |||
action: () => { | |||
let field_mappings = {}; | |||
@@ -1850,7 +1857,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const bulk_export = () => { | |||
return { | |||
label: __("Export"), | |||
label: __("Export", null, "Button in list view actions menu"), | |||
action: () => { | |||
const docnames = this.get_checked_items(true); | |||
@@ -1,7 +1,7 @@ | |||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
// MIT License. See license.txt | |||
$.extend(frappe.model, { | |||
Object.assign(frappe.model, { | |||
docinfo: {}, | |||
sync: function(r) { | |||
/* docs: | |||
@@ -33,22 +33,28 @@ $.extend(frappe.model, { | |||
} | |||
if(d.localname) { | |||
frappe.model.new_names[d.localname] = d.name; | |||
$(document).trigger('rename', [d.doctype, d.localname, d.name]); | |||
delete locals[d.doctype][d.localname]; | |||
// update docinfo to new dict keys | |||
if(i===0) { | |||
frappe.model.docinfo[d.doctype][d.name] = frappe.model.docinfo[d.doctype][d.localname]; | |||
frappe.model.docinfo[d.doctype][d.localname] = undefined; | |||
} | |||
frappe.model.rename_after_save(d, i); | |||
} | |||
} | |||
} | |||
frappe.model.sync_docinfo(r); | |||
}, | |||
rename_after_save: (d, i) => { | |||
frappe.model.new_names[d.localname] = d.name; | |||
$(document).trigger('rename', [d.doctype, d.localname, d.name]); | |||
delete locals[d.doctype][d.localname]; | |||
// update docinfo to new dict keys | |||
if(i===0) { | |||
frappe.model.docinfo[d.doctype][d.name] = frappe.model.docinfo[d.doctype][d.localname]; | |||
frappe.model.docinfo[d.doctype][d.localname] = undefined; | |||
} | |||
}, | |||
sync_docinfo: (r) => { | |||
// set docinfo (comments, assign, attachments) | |||
if(r.docinfo) { | |||
var doc; | |||
@@ -62,10 +68,14 @@ $.extend(frappe.model, { | |||
frappe.model.docinfo[doc.doctype] = {}; | |||
frappe.model.docinfo[doc.doctype][doc.name] = r.docinfo; | |||
} | |||
// copy values to frappe.boot.user_info | |||
Object.assign(frappe.boot.user_info, r.docinfo.user_info); | |||
} | |||
return r.docs; | |||
}, | |||
add_to_locals: function(doc) { | |||
if(!locals[doc.doctype]) | |||
locals[doc.doctype] = {}; | |||
@@ -100,6 +110,7 @@ $.extend(frappe.model, { | |||
} | |||
} | |||
}, | |||
update_in_locals: function(doc) { | |||
// update values in the existing local doc instead of replacing | |||
let local_doc = locals[doc.doctype][doc.name]; | |||
@@ -291,11 +291,18 @@ frappe.request.call = function(opts) { | |||
}) | |||
.fail(function(xhr, textStatus) { | |||
try { | |||
if (xhr.responseText) { | |||
var data = JSON.parse(xhr.responseText); | |||
if (data.exception) { | |||
// frappe.exceptions.CustomError -> CustomError | |||
var exception = data.exception.split('.').at(-1); | |||
if (xhr.getResponseHeader('content-type') == 'application/json' && xhr.responseText) { | |||
var data; | |||
try { | |||
data = JSON.parse(xhr.responseText); | |||
} catch (e) { | |||
console.log("Unable to parse reponse text"); | |||
console.log(xhr.responseText); | |||
console.log(e); | |||
} | |||
if (data && data.exception) { | |||
// frappe.exceptions.CustomError: (1024, ...) -> CustomError | |||
var exception = data.exception.split('.').at(-1).split(':').at(0); | |||
var exception_handler = exception_handlers[exception]; | |||
if (exception_handler) { | |||
exception_handler(data); | |||
@@ -282,7 +282,7 @@ frappe.router = { | |||
resolve(); | |||
}); | |||
}, 100); | |||
}); | |||
}).finally(() => frappe.route_flags = {}); | |||
}, | |||
get_route_from_arguments(route) { | |||
@@ -374,8 +374,9 @@ frappe.router = { | |||
// change the URL and call the router | |||
if (window.location.pathname !== url) { | |||
// push state so the browser looks fine | |||
history.pushState(null, null, url); | |||
// push/replace state so the browser looks fine | |||
const method = frappe.route_flags.replace_route ? "replaceState" : "pushState"; | |||
history[method](null, null, url); | |||
// now process the route | |||
this.route(); | |||
@@ -57,8 +57,10 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { | |||
// show footer | |||
this.action = this.action || { primary: { }, secondary: { } }; | |||
if (this.primary_action || (this.action.primary && this.action.primary.onsubmit)) { | |||
this.set_primary_action(this.primary_action_label || this.action.primary.label || __("Submit"), | |||
this.primary_action || this.action.primary.onsubmit); | |||
this.set_primary_action( | |||
this.primary_action_label || this.action.primary.label || __("Submit", null, "Primary action in dialog"), | |||
this.primary_action || this.action.primary.onsubmit | |||
); | |||
} | |||
if (this.secondary_action) { | |||
@@ -63,7 +63,7 @@ frappe.warn = function(title, message_html, proceed_action, primary_label, is_mi | |||
if (proceed_action) proceed_action(); | |||
d.hide(); | |||
}, | |||
secondary_action_label: __("Cancel"), | |||
secondary_action_label: __("Cancel", null, "Secondary button in warning dialog"), | |||
secondary_action: () => d.hide(), | |||
minimizable: is_minimizable | |||
}); | |||
@@ -365,7 +365,7 @@ frappe.show_alert = frappe.toast = function(message, seconds=7, actions={}) { | |||
let indicator_icon_map = { | |||
'orange': "solid-warning", | |||
'yellow': "solid-warning", | |||
'blue': "solid-success", | |||
'blue': "solid-info", | |||
'green': "solid-success", | |||
'red': "solid-error" | |||
}; | |||
@@ -387,8 +387,10 @@ frappe.show_alert = frappe.toast = function(message, seconds=7, actions={}) { | |||
icon = 'solid-info'; | |||
} | |||
const indicator = message.indicator || 'blue'; | |||
const div = $(` | |||
<div class="alert desk-alert"> | |||
<div class="alert desk-alert ${indicator}" role="alert"> | |||
<div class="alert-message-container"> | |||
<div class="alert-title-container"> | |||
<div>${frappe.utils.icon(icon, 'lg')}</div> | |||
@@ -398,7 +400,8 @@ frappe.show_alert = frappe.toast = function(message, seconds=7, actions={}) { | |||
</div> | |||
<div class="alert-body" style="display: none"></div> | |||
<a class="close">${frappe.utils.icon('close-alt')}</a> | |||
</div>`); | |||
</div> | |||
`); | |||
div.hide().appendTo("#alert-container").show(); | |||
@@ -54,7 +54,7 @@ frappe.ui.DiffView = class DiffView { | |||
fieldname: "diff", | |||
}, | |||
], | |||
size: "large", | |||
size: "extra-large", | |||
}); | |||
return dialog; | |||
} | |||
@@ -2,14 +2,6 @@ frappe.user_info = function(uid) { | |||
if(!uid) | |||
uid = frappe.session.user; | |||
if(uid.toLowerCase()==="bot") { | |||
return { | |||
fullname: __("Bot"), | |||
image: "/assets/frappe/images/ui/bot.png", | |||
abbr: "B" | |||
}; | |||
} | |||
if(!(frappe.boot.user_info && frappe.boot.user_info[uid])) { | |||
var user_info = {fullname: uid || "Unknown"}; | |||
} else { | |||
@@ -22,29 +14,6 @@ frappe.user_info = function(uid) { | |||
return user_info; | |||
}; | |||
frappe.ui.set_user_background = function(src, selector, style) { | |||
if(!selector) selector = "#page-desktop"; | |||
if(!style) style = "Fill Screen"; | |||
if(src) { | |||
if (window.cordova && src.indexOf("http") === -1) { | |||
src = frappe.base_url + src; | |||
} | |||
var background = repl('background: url("%(src)s") center center;', {src: src}); | |||
} else { | |||
var background = "background-color: #4B4C9D;"; | |||
} | |||
frappe.dom.set_style(repl('%(selector)s { \ | |||
%(background)s \ | |||
background-attachment: fixed; \ | |||
%(style)s \ | |||
}', { | |||
selector:selector, | |||
background:background, | |||
style: style==="Fill Screen" ? "background-size: cover;" : "" | |||
})); | |||
}; | |||
frappe.provide('frappe.user'); | |||
$.extend(frappe.user, { | |||
@@ -98,6 +98,7 @@ frappe.views.FormFactory = class FormFactory extends frappe.views.Factory { | |||
if (new_name===name) { | |||
this.render(doctype_layout, name); | |||
} else { | |||
frappe.route_flags.replace_route = true; | |||
frappe.set_route("Form", doctype_layout, new_name); | |||
} | |||
} | |||
@@ -337,6 +337,7 @@ frappe.provide("frappe.views"); | |||
function bind_events() { | |||
bind_add_column(); | |||
bind_clickdrag(); | |||
} | |||
function setup_sortable() { | |||
@@ -392,6 +393,45 @@ frappe.provide("frappe.views"); | |||
}); | |||
} | |||
function bind_clickdrag() { | |||
let isDown = false; | |||
let startX; | |||
let scrollLeft; | |||
let draggable = self.$kanban_board[0]; | |||
draggable.addEventListener('mousedown', (e) => { | |||
// don't trigger scroll if one of the ancestors of the | |||
// clicked element matches any of these selectors | |||
let ignoreEl = [ | |||
'.kanban-column .kanban-column-header', | |||
'.kanban-column .add-card', | |||
'.kanban-column .kanban-card.new-card-area', | |||
'.kanban-card-wrapper', | |||
]; | |||
if (ignoreEl.some((el) => e.target.closest(el))) return; | |||
isDown = true; | |||
draggable.classList.add('clickdrag-active'); | |||
startX = e.pageX - draggable.offsetLeft; | |||
scrollLeft = draggable.scrollLeft; | |||
}); | |||
draggable.addEventListener('mouseleave', () => { | |||
isDown = false; | |||
draggable.classList.remove('clickdrag-active'); | |||
}); | |||
draggable.addEventListener('mouseup', () => { | |||
isDown = false; | |||
draggable.classList.remove('clickdrag-active'); | |||
}); | |||
draggable.addEventListener('mousemove', (e) => { | |||
if (!isDown) return; | |||
e.preventDefault(); | |||
const x = e.pageX - draggable.offsetLeft; | |||
const walk = (x - startX); | |||
draggable.scrollLeft = scrollLeft - walk; | |||
}); | |||
} | |||
function setup_restore_columns() { | |||
var cur_list = store.getState().cur_list; | |||
var columns = store.getState().columns; | |||
@@ -340,7 +340,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||
options: columns_in_picker | |||
}, | |||
{ | |||
label: __('Insert Column Before {0}', [datatabe_col.docfield.label.bold()]), | |||
label: __('Insert Column Before {0}', [__(datatabe_col.docfield.label).bold()]), | |||
fieldname: 'insert_before', | |||
fieldtype: 'Check' | |||
} | |||
@@ -789,7 +789,10 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||
} else { | |||
this.fields.splice(col_index, 0, field); | |||
} | |||
frappe.show_alert(__('Also adding the dependent currency field {0}', [field[0].bold()])); | |||
const field_label = frappe.meta.get_label(doctype, field[0]); | |||
frappe.show_alert( | |||
__('Also adding the dependent currency field {0}', [__(field_label).bold()]) | |||
); | |||
} | |||
} | |||
@@ -799,7 +802,10 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||
const field = [col, doctype]; | |||
this.fields.push(field); | |||
this.refresh(); | |||
frappe.show_alert(__('Also adding the status dependency field {0}', [field[0].bold()])); | |||
const field_label = frappe.meta.get_label(doctype, field[0]); | |||
frappe.show_alert( | |||
__('Also adding the status dependency field {0}', [__(field_label).bold()]) | |||
); | |||
} | |||
} | |||
@@ -160,17 +160,17 @@ export default class WebForm extends frappe.ui.FieldGroup { | |||
} | |||
setup_primary_action() { | |||
this.add_button_to_header(this.button_label || "Save", "primary", () => | |||
this.add_button_to_header(this.button_label || __("Save", null, "Button in web form"), "primary", () => | |||
this.save() | |||
); | |||
this.add_button_to_footer(this.button_label || "Save", "primary", () => | |||
this.add_button_to_footer(this.button_label || __("Save", null, "Button in web form"), "primary", () => | |||
this.save() | |||
); | |||
} | |||
setup_cancel_button() { | |||
this.add_button_to_header(__("Cancel"), "light", () => this.cancel()); | |||
this.add_button_to_header(__("Cancel", null, "Button in web form"), "light", () => this.cancel()); | |||
} | |||
setup_delete_button() { | |||
@@ -216,16 +216,18 @@ export default class WebForm extends frappe.ui.FieldGroup { | |||
let message = ''; | |||
if (invalid_values.length) { | |||
message += __('Invalid values for fields:') + '<br><br><ul><li>' + invalid_values.join('<li>') + '</ul>'; | |||
message += __('Invalid values for fields:', null, 'Error message in web form'); | |||
message += '<br><br><ul><li>' + invalid_values.join('<li>') + '</ul>'; | |||
} | |||
if (errors.length) { | |||
message += __('Mandatory fields required:') + '<br><br><ul><li>' + errors.join('<li>') + '</ul>'; | |||
message += __('Mandatory fields required:', null, 'Error message in web form'); | |||
message += '<br><br><ul><li>' + errors.join('<li>') + '</ul>'; | |||
} | |||
if (invalid_values.length || errors.length) { | |||
frappe.msgprint({ | |||
title: __('Error'), | |||
title: __('Error', null, 'Title of error message in web form'), | |||
message: message, | |||
indicator: 'orange' | |||
}); | |||
@@ -139,8 +139,6 @@ export default class WebFormList { | |||
make_table_head() { | |||
// Create Heading | |||
let thead = this.table.createTHead(); | |||
thead.style.backgroundColor = "#f7fafc"; | |||
thead.style.color = "#8d99a6"; | |||
let row = thead.insertRow(); | |||
let th = document.createElement("th"); | |||
@@ -0,0 +1,12 @@ | |||
// Color overrides for https://getbootstrap.com/docs/4.0/components/alerts | |||
$alert-types: info, success, warning, danger; | |||
.alert { | |||
@each $alert-type in $alert-types { | |||
&.alert-#{$alert-type} { | |||
color: var(--alert-text-#{$alert-type}); | |||
background-color: var(--alert-bg-#{$alert-type}); | |||
border: none; | |||
} | |||
} | |||
} |
@@ -165,6 +165,16 @@ | |||
--bg-pink: var(--pink-50); | |||
--bg-cyan: var(--cyan-50); | |||
//font sizes | |||
--text-xs: 11px; | |||
--text-sm: 12px; | |||
--text-md: 13px; | |||
--text-base: 14px; | |||
--text-lg: 16px; | |||
--text-xl: 18px; | |||
--text-2xl: 20px; | |||
--text-3xl: 22px; | |||
--text-on-blue: var(--blue-600); | |||
--text-on-light-blue: var(--blue-500); | |||
--text-on-dark-blue: var(--blue-700); | |||
@@ -179,6 +189,16 @@ | |||
--text-on-pink: var(--pink-500); | |||
--text-on-cyan: var(--cyan-600); | |||
// alert colors | |||
--alert-text-danger: var(--red-600); | |||
--alert-text-warning: var(--yellow-700); | |||
--alert-text-info: var(--blue-700); | |||
--alert-text-success: var(--green-700); | |||
--alert-bg-danger: var(--red-50); | |||
--alert-bg-warning: var(--yellow-50); | |||
--alert-bg-info: var(--blue-50); | |||
--alert-bg-success: var(--green-50); | |||
// Layout Colors | |||
--bg-color: var(--gray-50); | |||
--fg-color: white; | |||
@@ -4,15 +4,6 @@ $input-height: 28px !default; | |||
:root, | |||
[data-theme="light"] { | |||
--text-xs: 11px; | |||
--text-sm: 12px; | |||
--text-md: 13px; | |||
--text-base: 14px; | |||
--text-lg: 16px; | |||
--text-xl: 18px; | |||
--text-2xl: 20px; | |||
--text-3xl: 22px; | |||
// breakpoints | |||
--xxl-width: map-get($grid-breakpoints, '2xl'); | |||
--xl-width: map-get($grid-breakpoints, 'xl'); | |||
@@ -63,6 +63,16 @@ | |||
--text-on-light-gray: var(--gray-100); | |||
--text-on-purple: var(--purple-100); | |||
// alert colors | |||
--alert-text-danger: var(--red-300); | |||
--alert-text-warning: var(--yellow-300); | |||
--alert-text-info: var(--blue-300); | |||
--alert-text-success: var(--green-300); | |||
--alert-bg-danger: var(--red-900); | |||
--alert-bg-warning: var(--yellow-900); | |||
--alert-bg-info: var(--blue-900); | |||
--alert-bg-success: var(--green-900); | |||
--sidebar-select-color: var(--gray-800); | |||
--scrollbar-thumb-color: var(--gray-600); | |||
@@ -87,6 +87,11 @@ | |||
} | |||
} | |||
.dt-cell__resize-handle { | |||
right: -3px !important; | |||
left: unset !important; | |||
} | |||
.dt-row.dt-row-totalRow { | |||
font-weight: bold; | |||
} | |||
@@ -2,6 +2,7 @@ | |||
@import "../common/mixins.scss"; | |||
@import "../common/global.scss"; | |||
@import "../common/icons.scss"; | |||
@import "../common/alert.scss"; | |||
@import "~bootstrap/scss/bootstrap"; | |||
@import "global"; | |||
@@ -164,7 +164,7 @@ | |||
// To compensate for perceived centering | |||
.null-state { | |||
height: 85px; | |||
height: 60px; | |||
width: auto; | |||
margin-bottom: var(--margin-md); | |||
img { | |||
@@ -57,35 +57,6 @@ $threshold: 34; | |||
} | |||
} | |||
} | |||
.timeline-actions { | |||
display: inline-flex; | |||
width: 100%; | |||
margin-bottom: var(--timeline-item-bottom-margin); | |||
padding: var(--padding-sm); | |||
position: relative; | |||
.action-btn { | |||
margin-left: var(--margin-md); | |||
display: flex; | |||
align-items: center; | |||
line-height: var(--text-xl); | |||
.icon { | |||
margin-right: var(--margin-xs); | |||
} | |||
} | |||
.action-btn:first-of-type { | |||
margin-left: var(--timeline-item-left-margin); | |||
} | |||
} | |||
.document-email-link-container { | |||
@extend .ellipsis; | |||
position: relative; | |||
padding: var(--padding-sm); | |||
font-size: var(--text-sm); | |||
margin-bottom: var(--timeline-item-bottom-margin); | |||
span:first-of-type { | |||
margin-left: var(--timeline-item-left-margin); | |||
} | |||
} | |||
.timeline-item { | |||
font-size: var(--text-md); | |||
position: relative; | |||
@@ -94,6 +65,23 @@ $threshold: 34; | |||
color: var(--text-color); | |||
font-weight: var(--text-bold); | |||
} | |||
.action-buttons { | |||
display: inline-flex; | |||
white-space: nowrap; | |||
overflow: auto; | |||
.action-btn { | |||
margin-left: var(--margin-md); | |||
display: flex; | |||
align-items: center; | |||
line-height: var(--text-xl); | |||
.icon { | |||
margin-right: var(--margin-xs); | |||
} | |||
} | |||
.action-btn:first-of-type { | |||
margin-left: 0; | |||
} | |||
} | |||
.timeline-content { | |||
max-width: var(--timeline-content-max-width); | |||
padding: var(--padding-sm); | |||
@@ -9,7 +9,27 @@ | |||
} | |||
} | |||
#alert-container .desk-alert { | |||
.desk-alert { | |||
&.red { | |||
--toast-bg: var(--alert-bg-danger); | |||
} | |||
&.yellow { | |||
--toast-bg: var(--alert-bg-warning); | |||
} | |||
&.orange { | |||
--toast-bg: var(--alert-bg-warning); | |||
} | |||
&.blue { | |||
--toast-bg: var(--alert-bg-info); | |||
} | |||
&.green { | |||
--toast-bg: var(--alert-bg-success); | |||
} | |||
box-shadow: var(--modal-shadow); | |||
width: 400px; | |||
min-height: 50px; | |||
@@ -46,7 +66,7 @@ | |||
.alert-subtitle { | |||
font-size: var(--text-md); | |||
padding-left: 34px; | |||
color: var(--text-muted); | |||
color: var(--text-light); | |||
} | |||
} | |||
@@ -1,7 +1,9 @@ | |||
@import "./desk/variables"; | |||
body { | |||
background-color: var(--bg-light-gray); | |||
@include media-breakpoint-up(sm) { | |||
background-color: var(--bg-light-gray); | |||
} | |||
} | |||
.for-forgot, | |||
@@ -94,6 +94,8 @@ | |||
max-width: 300px; | |||
border: 1px solid var(--dark-border-color); | |||
box-shadow: none; | |||
border-radius: var(--border-radius); | |||
font-size: $font-size-sm; | |||
} | |||
} | |||
} |
@@ -4,6 +4,7 @@ | |||
@import "../common/mixins"; | |||
@import "../common/global"; | |||
@import "../common/icons"; | |||
@import "../common/alert"; | |||
@import 'base'; | |||
@import "../common/flex"; | |||
@import "../common/buttons"; | |||
@@ -27,6 +28,8 @@ | |||
@import 'navbar'; | |||
@import 'footer'; | |||
@import 'error-state'; | |||
@import 'my_account'; | |||
.ql-editor.read-mode { | |||
padding: 0; | |||
@@ -129,7 +132,7 @@ | |||
a { | |||
color: var(--text-color) | |||
} | |||
li.disabled { | |||
a { | |||
color: var(--text-muted); | |||
@@ -166,6 +169,10 @@ a.card { | |||
font-size: inherit; | |||
} | |||
.indicator-pill { | |||
font-size: var(--font-size-xs) | |||
} | |||
h4.modal-title { | |||
font-size: 1em; | |||
} | |||
@@ -298,3 +305,7 @@ h5.modal-title { | |||
margin: 70px auto; | |||
font-size: $font-size-sm; | |||
} | |||
.empty-list-icon { | |||
height: 70px; | |||
} |
@@ -0,0 +1,117 @@ | |||
//styles for my account and edit-profile page | |||
@include media-breakpoint-up(sm) { | |||
body[data-path="me"], | |||
body[data-path="list"], | |||
body[data-path="update-profile"] { | |||
background-color: var(--bg-color); | |||
} | |||
} | |||
@include media-breakpoint-down(sm) { | |||
#page-me { | |||
.side-list { | |||
.list-group { | |||
display: none; | |||
} | |||
} | |||
} | |||
} | |||
.my-account-header { | |||
color: var(--gray-900); | |||
margin-bottom: var(--margin-lg); | |||
font-weight: bold; | |||
@include media-breakpoint-down(sm) { | |||
margin-left: -1rem; | |||
} | |||
} | |||
.account-info { | |||
background-color: var(--fg-color); | |||
border-radius: var(--border-radius-md); | |||
padding: var(--padding-sm) 25px; | |||
max-width: 850px; | |||
@include media-breakpoint-up(sm) { | |||
margin-left: 0; | |||
} | |||
@include media-breakpoint-down(sm) { | |||
padding: 0; | |||
} | |||
.my-account-name, | |||
.my-account-item { | |||
color: var(--gray-900); | |||
font-weight: var(--text-bold); | |||
} | |||
.my-account-avatar { | |||
.avatar { | |||
height: 60px; | |||
width: 60px; | |||
} | |||
} | |||
.my-account-item-desc { | |||
color: var(--gray-700); | |||
font-size: var(--text-md); | |||
} | |||
.my-account-item-link { | |||
font-size: var(--text-md); | |||
a { | |||
text-decoration: none; | |||
.edit-profile-icon { | |||
stroke: var(--blue-500); | |||
} | |||
} | |||
.right-icon { | |||
@include media-breakpoint-up(sm) { | |||
display: none; | |||
} | |||
} | |||
.item-link-text { | |||
@include media-breakpoint-down(sm) { | |||
display: none; | |||
} | |||
} | |||
} | |||
.col { | |||
padding: var(--padding-md) 0; | |||
border-bottom: 1px solid var(--border-color); | |||
.form-group { | |||
margin-right: var(--margin-lg); | |||
} | |||
} | |||
:last-child { | |||
border: 0; | |||
} | |||
} | |||
//styles for third party apps page | |||
//center wrt to outer most container and not immediate parent | |||
.empty-apps-state { | |||
position: relative; | |||
padding-top: 10rem; | |||
margin-left: -250px; | |||
text-align: center; | |||
@include media-breakpoint-down(sm) { | |||
margin: auto; | |||
padding-top: 5rem; | |||
} | |||
@include media-breakpoint-down(md) { | |||
margin-left: 0; | |||
} | |||
} |
@@ -1,5 +1,31 @@ | |||
@import "../common/form"; | |||
[data-doctype="Web Form"] { | |||
.page-content-wrapper { | |||
.breadcrumb-container.container { | |||
@include media-breakpoint-up(sm) { | |||
padding-left: 0; | |||
} | |||
} | |||
.container { | |||
max-width: 800px; | |||
&.my-4 { | |||
background-color: var(--fg-color); | |||
@include media-breakpoint-up(sm) { | |||
padding: 1.8rem; | |||
border-radius: var(--border-radius-md); | |||
box-shadow: var(--card-shadow); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
.web-form-wrapper { | |||
.form-control { | |||
color: var(--text-color); | |||
@@ -16,6 +42,7 @@ | |||
.form-column { | |||
padding: 0 var(--padding-md); | |||
&:first-child { | |||
padding-left: 0; | |||
} | |||
@@ -24,4 +51,24 @@ | |||
padding-right: 0; | |||
} | |||
} | |||
} | |||
.web-form-wrapper~#datatable { | |||
.table { | |||
thead { | |||
th { | |||
border: 0; | |||
font-weight: normal; | |||
color: var(--text-muted) | |||
} | |||
} | |||
tr { | |||
color: var(--text-color); | |||
td { | |||
border-top: 1px solid var(--border-color); | |||
} | |||
} | |||
} | |||
} |
@@ -68,9 +68,14 @@ def get_sessions_to_clear(user=None, keep_current=False, device=None): | |||
session = DocType("Sessions") | |||
session_id = frappe.qb.from_(session).where((session.user == user) & (session.device.isin(device))) | |||
if keep_current: | |||
session_id = session_id.where(session.sid != frappe.db.escape(frappe.session.sid)) | |||
session_id = session_id.where(session.sid != frappe.session.sid) | |||
query = session_id.select(session.sid).offset(offset).limit(100).orderby(session.lastupdate, order=Order.desc) | |||
query = ( | |||
session_id.select(session.sid) | |||
.offset(offset) | |||
.limit(100) | |||
.orderby(session.lastupdate, order=Order.desc) | |||
) | |||
return query.run(pluck=True) | |||
@@ -107,7 +112,7 @@ def get_expired_sessions(): | |||
frappe.db.get_values( | |||
sessions, | |||
filters=( | |||
PseudoColumn(f"({Now() - sessions.lastupdate})") | |||
PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})") | |||
> get_expiry_period_for_query(device) | |||
) | |||
& (sessions.device == device), | |||
@@ -329,7 +334,7 @@ class Session: | |||
sessions, | |||
filters=(sessions.sid == self.sid) | |||
& ( | |||
PseudoColumn(f"({Now() - sessions.lastupdate})") | |||
PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})") | |||
< get_expiry_period_for_query(self.device) | |||
), | |||
fieldname=["user", "sessiondata"], | |||
@@ -369,7 +374,7 @@ class Session: | |||
# database persistence is secondary, don't update it too often | |||
updated_in_db = False | |||
if force or (time_diff==None) or (time_diff > 600): | |||
if force or (time_diff is None) or (time_diff > 600): | |||
# update sessions table | |||
frappe.db.sql("""update `tabSessions` set sessiondata=%s, | |||
lastupdate=NOW() where sid=%s""" , (str(self.data['data']), | |||
@@ -57,7 +57,7 @@ class EnergyPointRule(Document): | |||
def rule_condition_satisfied(self, doc): | |||
if self.for_doc_event == 'New': | |||
# indicates that this was a new doc | |||
return doc.get_doc_before_save() == None | |||
return doc.get_doc_before_save() is None | |||
if self.for_doc_event == 'Submit': | |||
return doc.docstatus == 1 | |||
if self.for_doc_event == 'Cancel': | |||
@@ -2,8 +2,9 @@ | |||
<h4 class="text-muted">{{ sub_title }}</h4> | |||
{% endif %} | |||
{% if not result -%} | |||
<div class="text-muted" style="min-height: 300px;"> | |||
{{ no_result_message or _("Nothing to show") }} | |||
<div class="empty-apps-state"> | |||
<img class="empty-list-icon" src="/assets/frappe/images/ui-states/list-empty-state.svg"/> | |||
<div class="mt-4">{{ no_result_message or _("Nothing to show") }}</div> | |||
</div> | |||
{% else %} | |||
<div class="website-list" data-doctype="{{ doctype }}" | |||
@@ -2,79 +2,87 @@ | |||
background-color: var(--bg-color); | |||
} | |||
body { | |||
background-color: var(--bg-color); | |||
} | |||
.page-card { | |||
max-width: 360px; | |||
padding: 15px; | |||
margin: 70px auto; | |||
border-radius: 4px; | |||
background-color: var(--fg-color); | |||
box-shadow: var(--shadow-base); | |||
max-width: 360px; | |||
padding: 15px; | |||
margin: 70px auto; | |||
border-radius: 4px; | |||
background-color: var(--fg-color); | |||
/* box-shadow: var(--shadow-base); */ | |||
} | |||
.for-reset-password { | |||
margin: 80px 0; | |||
} | |||
margin: 80px 0; | |||
} | |||
.for-reset-password .page-card { | |||
border: 0; | |||
max-width: 450px; | |||
margin: auto; | |||
padding: 40px 60px; | |||
border-radius: 10px; | |||
box-shadow: var(--shadow-base); | |||
border: 0; | |||
max-width: 450px; | |||
margin: auto; | |||
border-radius: 10px; | |||
} | |||
@media (min-width: 567px) { | |||
.for-reset-password .page-card { | |||
box-shadow: var(--shadow-base); | |||
padding: 40px 60px; | |||
} | |||
} | |||
.page-card .page-card-head { | |||
padding: 10px 15px; | |||
margin: -15px; | |||
margin-bottom: 15px; | |||
border-bottom: 1px solid var(--border-color); | |||
padding: 10px 15px; | |||
margin: -15px; | |||
margin-bottom: 15px; | |||
border-bottom: 1px solid var(--border-color); | |||
} | |||
.for-reset-password .page-card .page-card-head { | |||
border-bottom: 0; | |||
.for-reset-password .page-card .page-card-head { | |||
border-bottom: 0; | |||
} | |||
.page-card-head h4 { | |||
font-size: 18px; | |||
font-weight: 600; | |||
font-size: 18px; | |||
font-weight: 600; | |||
} | |||
#reset-password .form-group { | |||
margin-bottom: 10px; | |||
font-size: var(--font-size-sm); | |||
margin-bottom: 10px; | |||
font-size: var(--font-size-sm); | |||
} | |||
.page-card .page-card-head .indicator { | |||
color: #36414C; | |||
font-size: 14px; | |||
color: #36414C; | |||
font-size: 14px; | |||
} | |||
.sign-up-message { | |||
margin-top: 20px; | |||
font-size: 13px; | |||
color: var(--text-color); | |||
margin-top: 20px; | |||
font-size: 13px; | |||
color: var(--text-color); | |||
} | |||
.page-card .page-card-head .indicator::before { | |||
margin: 0 6px 0.5px 0px; | |||
margin: 0 6px 0.5px 0px; | |||
} | |||
button#update { | |||
font-size: var(--font-size-sm); | |||
font-size: var(--font-size-sm); | |||
} | |||
.page-card .btn { | |||
margin-top: 30px; | |||
margin-top: 30px; | |||
} | |||
.page-card p { | |||
font-size: 14px; | |||
font-size: 14px; | |||
} | |||
.ellipsis { | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
max-width: 100%; | |||
vertical-align: middle; | |||
} | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
max-width: 100%; | |||
vertical-align: middle; | |||
} |
@@ -335,7 +335,10 @@ def make_test_records_for_doctype(doctype, verbose=0, force=False): | |||
frappe.local.test_objects[doctype] += test_module._make_test_records(verbose) | |||
elif hasattr(test_module, "test_records"): | |||
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose, force) | |||
if doctype in frappe.local.test_objects: | |||
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose, force) | |||
else: | |||
frappe.local.test_objects[doctype] = make_test_objects(doctype, test_module.test_records, verbose, force) | |||
else: | |||
test_records = frappe.get_test_records(doctype) | |||
@@ -4,38 +4,44 @@ import time | |||
import unittest | |||
import frappe | |||
from frappe.auth import HTTPRequest, LoginAttemptTracker | |||
import frappe.utils | |||
from frappe.auth import LoginAttemptTracker | |||
from frappe.frappeclient import FrappeClient, AuthError | |||
from frappe.utils import set_request | |||
def add_user(email, password, username=None, mobile_no=None): | |||
first_name = email.split('@', 1)[0] | |||
user = frappe.get_doc( | |||
dict(doctype='User', email=email, first_name=first_name, username=username, mobile_no=mobile_no) | |||
).insert() | |||
user.new_password = password | |||
user.add_roles("System Manager") | |||
frappe.db.commit() | |||
class TestAuth(unittest.TestCase): | |||
def __init__(self, *args, **kwargs): | |||
super(TestAuth, self).__init__(*args, **kwargs) | |||
self.test_user_email = 'test_auth@test.com' | |||
self.test_user_name = 'test_auth_user' | |||
self.test_user_mobile = '+911234567890' | |||
self.test_user_password = 'pwd_012' | |||
def setUp(self): | |||
self.tearDown() | |||
self.add_user(self.test_user_email, self.test_user_password, | |||
username=self.test_user_name, mobile_no=self.test_user_mobile) | |||
def tearDown(self): | |||
frappe.delete_doc('User', self.test_user_email, force=True) | |||
def add_user(self, email, password, username=None, mobile_no=None): | |||
first_name = email.split('@', 1)[0] | |||
user = frappe.get_doc( | |||
dict(doctype='User', email=email, first_name=first_name, username=username, mobile_no=mobile_no) | |||
).insert() | |||
user.new_password = password | |||
user.save() | |||
frappe.db.commit() | |||
@classmethod | |||
def setUpClass(cls): | |||
cls.HOST_NAME = ( | |||
frappe.get_site_config().host_name | |||
or frappe.utils.get_site_url(frappe.local.site) | |||
) | |||
cls.test_user_email = 'test_auth@test.com' | |||
cls.test_user_name = 'test_auth_user' | |||
cls.test_user_mobile = '+911234567890' | |||
cls.test_user_password = 'pwd_012' | |||
cls.tearDownClass() | |||
add_user(email=cls.test_user_email, password=cls.test_user_password, | |||
username=cls.test_user_name, mobile_no=cls.test_user_mobile) | |||
@classmethod | |||
def tearDownClass(cls): | |||
frappe.delete_doc('User', cls.test_user_email, force=True) | |||
def set_system_settings(self, k, v): | |||
frappe.db.set_value("System Settings", "System Settings", k, v) | |||
frappe.clear_cache() | |||
frappe.db.commit() | |||
def test_allow_login_using_mobile(self): | |||
@@ -43,12 +49,12 @@ class TestAuth(unittest.TestCase): | |||
self.set_system_settings('allow_login_using_user_name', 0) | |||
# Login by both email and mobile should work | |||
FrappeClient(frappe.get_site_config().host_name, self.test_user_mobile, self.test_user_password) | |||
FrappeClient(frappe.get_site_config().host_name, self.test_user_email, self.test_user_password) | |||
FrappeClient(self.HOST_NAME, self.test_user_mobile, self.test_user_password) | |||
FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password) | |||
# login by username should fail | |||
with self.assertRaises(AuthError): | |||
FrappeClient(frappe.get_site_config().host_name, self.test_user_name, self.test_user_password) | |||
FrappeClient(self.HOST_NAME, self.test_user_name, self.test_user_password) | |||
def test_allow_login_using_only_email(self): | |||
self.set_system_settings('allow_login_using_mobile_number', 0) | |||
@@ -56,14 +62,14 @@ class TestAuth(unittest.TestCase): | |||
# Login by mobile number should fail | |||
with self.assertRaises(AuthError): | |||
FrappeClient(frappe.get_site_config().host_name, self.test_user_mobile, self.test_user_password) | |||
FrappeClient(self.HOST_NAME, self.test_user_mobile, self.test_user_password) | |||
# login by username should fail | |||
with self.assertRaises(AuthError): | |||
FrappeClient(frappe.get_site_config().host_name, self.test_user_name, self.test_user_password) | |||
FrappeClient(self.HOST_NAME, self.test_user_name, self.test_user_password) | |||
# Login by email should work | |||
FrappeClient(frappe.get_site_config().host_name, self.test_user_email, self.test_user_password) | |||
FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password) | |||
def test_allow_login_using_username(self): | |||
self.set_system_settings('allow_login_using_mobile_number', 0) | |||
@@ -71,20 +77,39 @@ class TestAuth(unittest.TestCase): | |||
# Mobile login should fail | |||
with self.assertRaises(AuthError): | |||
FrappeClient(frappe.get_site_config().host_name, self.test_user_mobile, self.test_user_password) | |||
FrappeClient(self.HOST_NAME, self.test_user_mobile, self.test_user_password) | |||
# Both email and username logins should work | |||
FrappeClient(frappe.get_site_config().host_name, self.test_user_email, self.test_user_password) | |||
FrappeClient(frappe.get_site_config().host_name, self.test_user_name, self.test_user_password) | |||
FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password) | |||
FrappeClient(self.HOST_NAME, self.test_user_name, self.test_user_password) | |||
def test_allow_login_using_username_and_mobile(self): | |||
self.set_system_settings('allow_login_using_mobile_number', 1) | |||
self.set_system_settings('allow_login_using_user_name', 1) | |||
# Both email and username and mobile logins should work | |||
FrappeClient(frappe.get_site_config().host_name, self.test_user_mobile, self.test_user_password) | |||
FrappeClient(frappe.get_site_config().host_name, self.test_user_email, self.test_user_password) | |||
FrappeClient(frappe.get_site_config().host_name, self.test_user_name, self.test_user_password) | |||
FrappeClient(self.HOST_NAME, self.test_user_mobile, self.test_user_password) | |||
FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password) | |||
FrappeClient(self.HOST_NAME, self.test_user_name, self.test_user_password) | |||
def test_deny_multiple_login(self): | |||
self.set_system_settings('deny_multiple_sessions', 1) | |||
first_login = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password) | |||
first_login.get_list("ToDo") | |||
second_login = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password) | |||
second_login.get_list("ToDo") | |||
with self.assertRaises(Exception): | |||
first_login.get_list("ToDo") | |||
third_login = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password) | |||
with self.assertRaises(Exception): | |||
first_login.get_list("ToDo") | |||
with self.assertRaises(Exception): | |||
second_login.get_list("ToDo") | |||
third_login.get_list("ToDo") | |||
class TestLoginAttemptTracker(unittest.TestCase): | |||
def test_account_lock(self): | |||
@@ -1,4 +1,5 @@ | |||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors | |||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
# imports - standard imports | |||
import gzip | |||
@@ -9,13 +10,14 @@ import shutil | |||
import subprocess | |||
from typing import List | |||
import unittest | |||
import glob | |||
from glob import glob | |||
from unittest.case import skipIf | |||
# imports - module imports | |||
import frappe | |||
import frappe.recorder | |||
from frappe.installer import add_to_installed_apps, remove_app | |||
from frappe.utils import add_to_date, get_bench_relative_path, now | |||
from frappe.utils import add_to_date, get_bench_path, get_bench_relative_path, now | |||
from frappe.utils.backups import fetch_latest_backups | |||
# imports - third party imports | |||
@@ -133,134 +135,6 @@ class TestCommands(BaseTestCommands): | |||
self.assertEqual(self.returncode, 0) | |||
self.assertEqual(self.stdout[1:-1], frappe.bold(text="DocType")) | |||
def test_backup(self): | |||
backup = { | |||
"includes": { | |||
"includes": [ | |||
"ToDo", | |||
"Note", | |||
] | |||
}, | |||
"excludes": { | |||
"excludes": [ | |||
"Activity Log", | |||
"Access Log", | |||
"Error Log" | |||
] | |||
} | |||
} | |||
home = os.path.expanduser("~") | |||
site_backup_path = frappe.utils.get_site_path("private", "backups") | |||
# test 1: take a backup | |||
before_backup = fetch_latest_backups() | |||
self.execute("bench --site {site} backup") | |||
after_backup = fetch_latest_backups() | |||
self.assertEqual(self.returncode, 0) | |||
self.assertIn("successfully completed", self.stdout) | |||
self.assertNotEqual(before_backup["database"], after_backup["database"]) | |||
# test 2: take a backup with --with-files | |||
before_backup = after_backup.copy() | |||
self.execute("bench --site {site} backup --with-files") | |||
after_backup = fetch_latest_backups() | |||
self.assertEqual(self.returncode, 0) | |||
self.assertIn("successfully completed", self.stdout) | |||
self.assertIn("with files", self.stdout) | |||
self.assertNotEqual(before_backup, after_backup) | |||
self.assertIsNotNone(after_backup["public"]) | |||
self.assertIsNotNone(after_backup["private"]) | |||
# test 3: take a backup with --backup-path | |||
backup_path = os.path.join(home, "backups") | |||
self.execute("bench --site {site} backup --backup-path {backup_path}", {"backup_path": backup_path}) | |||
self.assertEqual(self.returncode, 0) | |||
self.assertTrue(os.path.exists(backup_path)) | |||
self.assertGreaterEqual(len(os.listdir(backup_path)), 2) | |||
# test 4: take a backup with --backup-path-db, --backup-path-files, --backup-path-private-files, --backup-path-conf | |||
kwargs = { | |||
key: os.path.join(home, key, value) | |||
for key, value in { | |||
"db_path": "database.sql.gz", | |||
"files_path": "public.tar", | |||
"private_path": "private.tar", | |||
"conf_path": "config.json", | |||
}.items() | |||
} | |||
self.execute( | |||
"""bench | |||
--site {site} backup --with-files | |||
--backup-path-db {db_path} | |||
--backup-path-files {files_path} | |||
--backup-path-private-files {private_path} | |||
--backup-path-conf {conf_path}""", | |||
kwargs, | |||
) | |||
self.assertEqual(self.returncode, 0) | |||
for path in kwargs.values(): | |||
self.assertTrue(os.path.exists(path)) | |||
# test 5: take a backup with --compress | |||
self.execute("bench --site {site} backup --with-files --compress") | |||
self.assertEqual(self.returncode, 0) | |||
compressed_files = glob.glob(site_backup_path + "/*.tgz") | |||
self.assertGreater(len(compressed_files), 0) | |||
# test 6: take a backup with --verbose | |||
self.execute("bench --site {site} backup --verbose") | |||
self.assertEqual(self.returncode, 0) | |||
# test 7: take a backup with frappe.conf.backup.includes | |||
self.execute( | |||
"bench --site {site} set-config backup '{includes}' --parse", | |||
{"includes": json.dumps(backup["includes"])}, | |||
) | |||
self.execute("bench --site {site} backup --verbose") | |||
self.assertEqual(self.returncode, 0) | |||
database = fetch_latest_backups(partial=True)["database"] | |||
self.assertEqual([], missing_in_backup(backup["includes"]["includes"], database)) | |||
# test 8: take a backup with frappe.conf.backup.excludes | |||
self.execute( | |||
"bench --site {site} set-config backup '{excludes}' --parse", | |||
{"excludes": json.dumps(backup["excludes"])}, | |||
) | |||
self.execute("bench --site {site} backup --verbose") | |||
self.assertEqual(self.returncode, 0) | |||
database = fetch_latest_backups(partial=True)["database"] | |||
self.assertFalse(exists_in_backup(backup["excludes"]["excludes"], database)) | |||
self.assertEqual([], missing_in_backup(backup["includes"]["includes"], database)) | |||
# test 9: take a backup with --include (with frappe.conf.excludes still set) | |||
self.execute( | |||
"bench --site {site} backup --include '{include}'", | |||
{"include": ",".join(backup["includes"]["includes"])}, | |||
) | |||
self.assertEqual(self.returncode, 0) | |||
database = fetch_latest_backups(partial=True)["database"] | |||
self.assertEqual([], missing_in_backup(backup["includes"]["includes"], database)) | |||
# test 10: take a backup with --exclude | |||
self.execute( | |||
"bench --site {site} backup --exclude '{exclude}'", | |||
{"exclude": ",".join(backup["excludes"]["excludes"])}, | |||
) | |||
self.assertEqual(self.returncode, 0) | |||
database = fetch_latest_backups(partial=True)["database"] | |||
self.assertFalse(exists_in_backup(backup["excludes"]["excludes"], database)) | |||
# test 11: take a backup with --ignore-backup-conf | |||
self.execute("bench --site {site} backup --ignore-backup-conf") | |||
self.assertEqual(self.returncode, 0) | |||
database = fetch_latest_backups()["database"] | |||
self.assertEqual([], missing_in_backup(backup["excludes"]["excludes"], database)) | |||
def test_restore(self): | |||
# step 0: create a site to run the test on | |||
global_config = { | |||
@@ -405,7 +279,7 @@ class TestCommands(BaseTestCommands): | |||
self.assertIsInstance(json.loads(self.stdout), dict) | |||
def test_get_bench_relative_path(self): | |||
bench_path = frappe.utils.get_bench_path() | |||
bench_path = get_bench_path() | |||
test1_path = os.path.join(bench_path, "test1.txt") | |||
test2_path = os.path.join(bench_path, "sites", "test2.txt") | |||
@@ -463,7 +337,7 @@ class TestCommands(BaseTestCommands): | |||
b"MIT" # app_license | |||
] | |||
app_name = "testapp0" | |||
apps_path = os.path.join(frappe.utils.get_bench_path(), "apps") | |||
apps_path = os.path.join(get_bench_path(), "apps") | |||
test_app_path = os.path.join(apps_path, app_name) | |||
self.execute(f"bench make-app {apps_path} {app_name}", {"cmd_input": b'\n'.join(user_input)}) | |||
self.assertEqual(self.returncode, 0) | |||
@@ -474,26 +348,199 @@ class TestCommands(BaseTestCommands): | |||
# cleanup | |||
shutil.rmtree(test_app_path) | |||
def disable_test_bench_drop_site_should_archive_site(self): | |||
@skipIf( | |||
not ( | |||
frappe.conf.root_password | |||
and frappe.conf.admin_password | |||
and frappe.conf.db_type == "mariadb" | |||
), | |||
"DB Root password and Admin password not set in config" | |||
) | |||
def test_bench_drop_site_should_archive_site(self): | |||
# TODO: Make this test postgres compatible | |||
site = 'test_site.localhost' | |||
self.execute( | |||
f"bench new-site {site} --force --verbose --admin-password {frappe.conf.admin_password} " | |||
f"--mariadb-root-password {frappe.conf.root_password}" | |||
f"bench new-site {site} --force --verbose " | |||
f"--admin-password {frappe.conf.admin_password} " | |||
f"--mariadb-root-password {frappe.conf.root_password} " | |||
f"--db-type {frappe.conf.db_type or 'mariadb'} " | |||
) | |||
self.assertEqual(self.returncode, 0) | |||
self.execute(f"bench drop-site {site} --force --root-password {frappe.conf.root_password}") | |||
self.assertEqual(self.returncode, 0) | |||
bench_path = frappe.utils.get_bench_path() | |||
bench_path = get_bench_path() | |||
site_directory = os.path.join(bench_path, f'sites/{site}') | |||
self.assertFalse(os.path.exists(site_directory)) | |||
archive_directory = os.path.join(bench_path, f'archived/sites/{site}') | |||
self.assertTrue(os.path.exists(archive_directory)) | |||
class RemoveAppUnitTests(unittest.TestCase): | |||
class TestBackups(BaseTestCommands): | |||
backup_map = { | |||
"includes": { | |||
"includes": [ | |||
"ToDo", | |||
"Note", | |||
] | |||
}, | |||
"excludes": { | |||
"excludes": [ | |||
"Activity Log", | |||
"Access Log", | |||
"Error Log" | |||
] | |||
} | |||
} | |||
home = os.path.expanduser("~") | |||
site_backup_path = frappe.utils.get_site_path("private", "backups") | |||
def setUp(self): | |||
self.files_to_trash = [] | |||
def tearDown(self): | |||
if self._testMethodName == "test_backup": | |||
for file in self.files_to_trash: | |||
os.remove(file) | |||
try: | |||
os.rmdir(os.path.dirname(file)) | |||
except OSError: | |||
pass | |||
def test_backup_no_options(self): | |||
"""Take a backup without any options | |||
""" | |||
before_backup = fetch_latest_backups(partial=True) | |||
self.execute("bench --site {site} backup") | |||
after_backup = fetch_latest_backups(partial=True) | |||
self.assertEqual(self.returncode, 0) | |||
self.assertIn("successfully completed", self.stdout) | |||
self.assertNotEqual(before_backup["database"], after_backup["database"]) | |||
def test_backup_with_files(self): | |||
"""Take a backup with files (--with-files) | |||
""" | |||
before_backup = fetch_latest_backups() | |||
self.execute("bench --site {site} backup --with-files") | |||
after_backup = fetch_latest_backups() | |||
self.assertEqual(self.returncode, 0) | |||
self.assertIn("successfully completed", self.stdout) | |||
self.assertIn("with files", self.stdout) | |||
self.assertNotEqual(before_backup, after_backup) | |||
self.assertIsNotNone(after_backup["public"]) | |||
self.assertIsNotNone(after_backup["private"]) | |||
def test_backup_with_custom_path(self): | |||
"""Backup to a custom path (--backup-path) | |||
""" | |||
backup_path = os.path.join(self.home, "backups") | |||
self.execute("bench --site {site} backup --backup-path {backup_path}", {"backup_path": backup_path}) | |||
self.assertEqual(self.returncode, 0) | |||
self.assertTrue(os.path.exists(backup_path)) | |||
self.assertGreaterEqual(len(os.listdir(backup_path)), 2) | |||
def test_backup_with_different_file_paths(self): | |||
"""Backup with different file paths (--backup-path-db, --backup-path-files, --backup-path-private-files, --backup-path-conf) | |||
""" | |||
kwargs = { | |||
key: os.path.join(self.home, key, value) | |||
for key, value in { | |||
"db_path": "database.sql.gz", | |||
"files_path": "public.tar", | |||
"private_path": "private.tar", | |||
"conf_path": "config.json", | |||
}.items() | |||
} | |||
self.execute( | |||
"""bench | |||
--site {site} backup --with-files | |||
--backup-path-db {db_path} | |||
--backup-path-files {files_path} | |||
--backup-path-private-files {private_path} | |||
--backup-path-conf {conf_path}""", | |||
kwargs, | |||
) | |||
self.assertEqual(self.returncode, 0) | |||
for path in kwargs.values(): | |||
self.assertTrue(os.path.exists(path)) | |||
def test_backup_compress_files(self): | |||
"""Take a compressed backup (--compress) | |||
""" | |||
self.execute("bench --site {site} backup --with-files --compress") | |||
self.assertEqual(self.returncode, 0) | |||
compressed_files = glob(f"{self.site_backup_path}/*.tgz") | |||
self.assertGreater(len(compressed_files), 0) | |||
def test_backup_verbose(self): | |||
"""Take a verbose backup (--verbose) | |||
""" | |||
self.execute("bench --site {site} backup --verbose") | |||
self.assertEqual(self.returncode, 0) | |||
def test_backup_only_specific_doctypes(self): | |||
"""Take a backup with (include) backup options set in the site config `frappe.conf.backup.includes` | |||
""" | |||
self.execute( | |||
"bench --site {site} set-config backup '{includes}' --parse", | |||
{"includes": json.dumps(self.backup_map["includes"])}, | |||
) | |||
self.execute("bench --site {site} backup --verbose") | |||
self.assertEqual(self.returncode, 0) | |||
database = fetch_latest_backups(partial=True)["database"] | |||
self.assertEqual([], missing_in_backup(self.backup_map["includes"]["includes"], database)) | |||
def test_backup_excluding_specific_doctypes(self): | |||
"""Take a backup with (exclude) backup options set (`frappe.conf.backup.excludes`, `--exclude`) | |||
""" | |||
# test 1: take a backup with frappe.conf.backup.excludes | |||
self.execute( | |||
"bench --site {site} set-config backup '{excludes}' --parse", | |||
{"excludes": json.dumps(self.backup_map["excludes"])}, | |||
) | |||
self.execute("bench --site {site} backup --verbose") | |||
self.assertEqual(self.returncode, 0) | |||
database = fetch_latest_backups(partial=True)["database"] | |||
self.assertFalse(exists_in_backup(self.backup_map["excludes"]["excludes"], database)) | |||
self.assertEqual([], missing_in_backup(self.backup_map["includes"]["includes"], database)) | |||
# test 2: take a backup with --exclude | |||
self.execute( | |||
"bench --site {site} backup --exclude '{exclude}'", | |||
{"exclude": ",".join(self.backup_map["excludes"]["excludes"])}, | |||
) | |||
self.assertEqual(self.returncode, 0) | |||
database = fetch_latest_backups(partial=True)["database"] | |||
self.assertFalse(exists_in_backup(self.backup_map["excludes"]["excludes"], database)) | |||
def test_selective_backup_priority_resolution(self): | |||
"""Take a backup with conflicting backup options set (`frappe.conf.excludes`, `--include`) | |||
""" | |||
self.execute( | |||
"bench --site {site} backup --include '{include}'", | |||
{"include": ",".join(self.backup_map["includes"]["includes"])}, | |||
) | |||
self.assertEqual(self.returncode, 0) | |||
database = fetch_latest_backups(partial=True)["database"] | |||
self.assertEqual([], missing_in_backup(self.backup_map["includes"]["includes"], database)) | |||
def test_dont_backup_conf(self): | |||
"""Take a backup ignoring frappe.conf.backup settings (with --ignore-backup-conf option) | |||
""" | |||
self.execute("bench --site {site} backup --ignore-backup-conf") | |||
self.assertEqual(self.returncode, 0) | |||
database = fetch_latest_backups()["database"] | |||
self.assertEqual([], missing_in_backup(self.backup_map["excludes"]["excludes"], database)) | |||
class TestRemoveApp(unittest.TestCase): | |||
def test_delete_modules(self): | |||
from frappe.installer import ( | |||
_delete_doctypes, | |||
@@ -1,6 +1,8 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe, unittest | |||
import frappe | |||
import datetime | |||
import unittest | |||
from frappe.model.db_query import DatabaseQuery | |||
from frappe.desk.reportview import get_filters_cond | |||
@@ -380,6 +382,22 @@ class TestReportview(unittest.TestCase): | |||
owners = DatabaseQuery("DocType").execute(filters={"name": "DocType"}, pluck="owner") | |||
self.assertEqual(owners, ["Administrator"]) | |||
def test_prepare_select_args(self): | |||
# frappe.get_all inserts modified field into order_by clause | |||
# test to make sure this is inserted into select field when postgres | |||
doctypes = frappe.get_all("DocType", | |||
filters={"docstatus": 0, "document_type": ("!=", "")}, | |||
group_by="document_type", | |||
fields=["document_type", "sum(is_submittable) as is_submittable"], | |||
limit=1, | |||
as_list=True, | |||
) | |||
if frappe.conf.db_type == "mariadb": | |||
self.assertTrue(len(doctypes[0]) == 2) | |||
else: | |||
self.assertTrue(len(doctypes[0]) == 3) | |||
self.assertTrue(isinstance(doctypes[0][2], datetime.datetime)) | |||
def test_column_comparison(self): | |||
"""Test DatabaseQuery.execute to test column comparison | |||
""" | |||
@@ -34,6 +34,52 @@ class TestDBUpdate(unittest.TestCase): | |||
self.assertEqual(fieldtype, table_column.type) | |||
self.assertIn(cstr(table_column.default) or 'NULL', [cstr(default), "'{}'".format(default)]) | |||
def test_index_and_unique_constraints(self): | |||
doctype = "User" | |||
frappe.reload_doctype('User', force=True) | |||
frappe.model.meta.trim_tables('User') | |||
make_property_setter(doctype, 'restrict_ip', 'unique', '1', 'Int') | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertTrue(restrict_ip_in_table.unique) | |||
make_property_setter(doctype, 'restrict_ip', 'unique', '0', 'Int') | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertFalse(restrict_ip_in_table.unique) | |||
make_property_setter(doctype, 'restrict_ip', 'search_index', '1', 'Int') | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertTrue(restrict_ip_in_table.index) | |||
make_property_setter(doctype, 'restrict_ip', 'search_index', '0', 'Int') | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertFalse(restrict_ip_in_table.index) | |||
make_property_setter(doctype, 'restrict_ip', 'search_index', '1', 'Int') | |||
make_property_setter(doctype, 'restrict_ip', 'unique', '1', 'Int') | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertTrue(restrict_ip_in_table.index) | |||
self.assertTrue(restrict_ip_in_table.unique) | |||
make_property_setter(doctype, 'restrict_ip', 'search_index', '1', 'Int') | |||
make_property_setter(doctype, 'restrict_ip', 'unique', '0', 'Int') | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertTrue(restrict_ip_in_table.index) | |||
self.assertFalse(restrict_ip_in_table.unique) | |||
make_property_setter(doctype, 'restrict_ip', 'search_index', '0', 'Int') | |||
make_property_setter(doctype, 'restrict_ip', 'unique', '1', 'Int') | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertFalse(restrict_ip_in_table.index) | |||
self.assertTrue(restrict_ip_in_table.unique) | |||
def get_fieldtype_from_def(field_def): | |||
fieldtuple = frappe.db.type_map.get(field_def.fieldtype, ('', 0)) | |||
fieldtype = fieldtuple[0] | |||
@@ -69,4 +115,8 @@ def get_other_fields_meta(meta): | |||
fields = dict(default_fields_map, **optional_fields_map) | |||
field_map = [frappe._dict({'fieldname': field, 'fieldtype': _type, 'length': _length}) for field, (_type, _length) in fields.items()] | |||
return field_map | |||
return field_map | |||
def get_table_column(doctype, fieldname): | |||
table_columns = frappe.db.get_table_columns_description('tab{}'.format(doctype)) | |||
return find(table_columns, lambda d: d.get('name') == fieldname) |
@@ -252,3 +252,8 @@ class TestDocument(unittest.TestCase): | |||
'currency': 100000 | |||
}) | |||
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') | |||
def test_limit_for_get(self): | |||
doc = frappe.get_doc("DocType", "DocType") | |||
# assuming DocType has more that 3 Data fields | |||
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3) |
@@ -222,9 +222,10 @@ def disable_2fa(): | |||
def toggle_2fa_all_role(state=None): | |||
'''Enable or disable 2fa for 'all' role on the system.''' | |||
all_role = frappe.get_doc('Role','All') | |||
if state == None: | |||
state = False if all_role.two_factor_auth == True else False | |||
if state not in [True, False]: return | |||
state = state if state is not None else False | |||
if type(state) != bool: | |||
return | |||
all_role.two_factor_auth = cint(state) | |||
all_role.save(ignore_permissions=True) | |||
frappe.db.commit() | |||
@@ -197,6 +197,7 @@ class TestWebsite(unittest.TestCase): | |||
frappe.cache().delete_key('app_hooks') | |||
def test_printview_page(self): | |||
frappe.db.value_cache[('DocType', 'Language', 'name')] = (('Language',),) | |||
content = get_response_content('/Language/ru') | |||
self.assertIn('<div class="print-format">', content) | |||
self.assertIn('<div>Language</div>', content) | |||
@@ -248,10 +248,11 @@ def create_topic_and_reply(web_page): | |||
@frappe.whitelist() | |||
def update_webform_to_multistep(): | |||
doc = frappe.get_doc("Web Form", "edit-profile") | |||
_doc = frappe.copy_doc(doc) | |||
_doc.is_multi_step_form = 1 | |||
_doc.title = "update-profile-duplicate" | |||
_doc.route = "update-profile-duplicate" | |||
_doc.is_standard = False | |||
_doc.save() | |||
if not frappe.db.exists("Web Form", "update-profile-duplicate"): | |||
doc = frappe.get_doc("Web Form", "edit-profile") | |||
_doc = frappe.copy_doc(doc) | |||
_doc.is_multi_step_form = 1 | |||
_doc.title = "update-profile-duplicate" | |||
_doc.route = "update-profile-duplicate" | |||
_doc.is_standard = False | |||
_doc.save() |
@@ -206,7 +206,7 @@ def make_dict_from_messages(messages, full_dict=None, load_user_translation=True | |||
:param messages: List of untranslated messages | |||
""" | |||
out = {} | |||
if full_dict==None: | |||
if full_dict is None: | |||
if load_user_translation: | |||
full_dict = get_full_dict(frappe.local.lang) | |||
else: | |||
@@ -1393,6 +1393,7 @@ Is Spam,ist Spam, | |||
Is Standard,Ist Standard, | |||
Is Submittable,Ist übertragbar, | |||
Is Table,ist eine Tabelle, | |||
Is Template, Ist Vorlage, | |||
Is Your Company Address,Ist Ihre Unternehmensadresse, | |||
It is risky to delete this file: {0}. Please contact your System Manager.,"Es ist riskant, diese Datei zu löschen: {0}. Bitte kontaktieren Sie Ihren System-Manager.", | |||
Item cannot be added to its own descendents,Artikel kann nicht zu seinen eigenen Abkömmlingen hinzugefügt werden, | |||
@@ -56,7 +56,7 @@ def get_email_address(user=None): | |||
def get_formatted_email(user, mail=None): | |||
"""get Email Address of user formatted as: `John Doe <johndoe@example.com>`""" | |||
fullname = get_fullname(user) | |||
method = get_hook_method('get_sender_details') | |||
if method: | |||
sender_name, mail = method() | |||
@@ -290,7 +290,7 @@ def remove_blanks(d): | |||
""" | |||
empty_keys = [] | |||
for key in d: | |||
if d[key]=='' or d[key]==None: | |||
if d[key] == "" or d[key] is None: | |||
# del d[key] raises runtime exception, using a workaround | |||
empty_keys.append(key) | |||
for key in empty_keys: | |||
@@ -623,12 +623,11 @@ def get_installed_apps_info(): | |||
return out | |||
def get_site_info(): | |||
from frappe.core.doctype.user.user import STANDARD_USERS | |||
from frappe.email.queue import get_emails_sent_this_month | |||
from frappe.utils.user import get_system_managers | |||
# only get system users | |||
users = frappe.get_all('User', filters={'user_type': 'System User', 'name': ('not in', STANDARD_USERS)}, | |||
users = frappe.get_all('User', filters={'user_type': 'System User', 'name': ('not in', frappe.STANDARD_USERS)}, | |||
fields=['name', 'enabled', 'last_login', 'last_active', 'language', 'time_zone']) | |||
system_managers = get_system_managers(only_name=True) | |||
for u in users: | |||
@@ -898,3 +897,14 @@ def dictify(arg): | |||
arg = frappe._dict(arg) | |||
return arg | |||
def add_user_info(user, user_info): | |||
if user not in user_info: | |||
info = frappe.db.get_value("User", | |||
user, ["full_name", "user_image", "name", 'email'], as_dict=True) or frappe._dict() | |||
user_info[user] = frappe._dict( | |||
fullname = info.full_name or user, | |||
image = info.user_image, | |||
name = user, | |||
email = info.email | |||
) |
@@ -20,11 +20,17 @@ from frappe.utils.redis_queue import RedisQueue | |||
from frappe.utils.commands import log | |||
common_site_config = frappe.get_file_json("common_site_config.json") | |||
custom_workers_config = common_site_config.get("workers", {}) | |||
default_timeout = 300 | |||
queue_timeout = { | |||
'long': 1500, | |||
'default': 300, | |||
'short': 300 | |||
"default": default_timeout, | |||
"short": default_timeout, | |||
"long": 1500, | |||
**{ | |||
worker: config.get("timeout", default_timeout) | |||
for worker, config in custom_workers_config.items() | |||
} | |||
} | |||
redis_connection = None | |||