diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml index 02a01bf4e4..5e91063698 100644 --- a/.github/workflows/docs-checker.yml +++ b/.github/workflows/docs-checker.yml @@ -12,7 +12,7 @@ jobs: - name: 'Setup Environment' uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: 3.8 - name: 'Clone repo' uses: actions/checkout@v2 diff --git a/frappe/__init__.py b/frappe/__init__.py index 08c0f794b3..a8bf114b9b 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -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: @@ -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") diff --git a/frappe/auth.py b/frappe/auth.py index 078a6bb165..a87edb6460 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -250,8 +250,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", diff --git a/frappe/boot.py b/frappe/boot.py index ca48f8b42e..6d1c2c6959 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -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""" @@ -223,17 +223,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""" diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 96c8f271d9..1ab07d92e4 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -12,7 +12,7 @@ from frappe.core.utils import get_parent_doc from frappe.utils.bot import BotReply from frappe.utils import parse_addr, split_emails from frappe.core.doctype.comment.comment import update_comment_in_doc -from email.utils import parseaddr +from email.utils import getaddresses from urllib.parse import unquote from frappe.utils.user import is_system_user from frappe.contacts.doctype.contact.contact import get_contact_name @@ -372,10 +372,9 @@ def get_contacts(email_strings, auto_create_contact=False): for email_string in email_strings: if email_string: - for email in email_string.split(","): - parsed_email = parseaddr(email)[1] - if parsed_email: - email_addrs.append(parsed_email) + result = getaddresses([email_string]) + for email in result: + email_addrs.append(email[1]) contacts = [] for email in email_addrs: diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index c5cf67ba57..79570d5048 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -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: diff --git a/frappe/core/doctype/data_export/test_data_exporter.py b/frappe/core/doctype/data_export/test_data_exporter.py new file mode 100644 index 0000000000..8d05707cf1 --- /dev/null +++ b/frappe/core/doctype/data_export/test_data_exporter.py @@ -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 + diff --git a/frappe/core/doctype/language/language.py b/frappe/core/doctype/language/language.py index 511c8ddeb6..69942ffd6d 100644 --- a/frappe/core/doctype/language/language.py +++ b/frappe/core/doctype/language/language.py @@ -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(): diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json index ba82e023a9..e370082fb5 100644 --- a/frappe/core/doctype/role/role.json +++ b/frappe/core/doctype/role/role.json @@ -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 } \ No newline at end of file diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index ef7845d3b0..b674ea6891 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -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 diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py index c1fd678141..661ac932e7 100644 --- a/frappe/core/doctype/user_type/user_type.py +++ b/frappe/core/doctype/user_type/user_type.py @@ -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)) diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 0ce6fbb265..33f07990af 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -74,9 +74,15 @@ 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("%", "%%") diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py index b512ca175c..a0523d90cd 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.py +++ b/frappe/desk/doctype/bulk_update/bulk_update.py @@ -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 diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index 0dfd458a37..ac62796dc2 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -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: diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 649491e9bc..c903d15d28 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -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) @@ -371,3 +417,25 @@ def send_link_titles(link_titles): frappe.local.response["_link_titles"] = {} frappe.local.response["_link_titles"].update(link_titles) + +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 + diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index b5f0c5043c..b42d8c58b7 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -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) diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index fb150e4bea..e81ed0767b 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -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() diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index f40c135653..7e3efb5d48 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -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) diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index d89a3d83be..9730004065 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -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)) + all_ids = list(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, - }) + 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 diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 4f4ed6d48e..dd64d0df80 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -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 diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index b5330f4d4f..b66cd9014b 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -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", diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index eeef552a8a..631174b4db 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -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 diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index cb2c2af898..51d53c69a5 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -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): diff --git a/frappe/public/images/ui-states/empty-app-state.svg b/frappe/public/images/ui-states/empty-app-state.svg new file mode 100644 index 0000000000..b7e346f310 --- /dev/null +++ b/frappe/public/images/ui-states/empty-app-state.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index 7af0705e78..ce871c50cb 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -148,8 +148,9 @@ frappe.ui.form.Control = class BaseControl { return this.doc[this.df.fieldname]; } } - set_value(value) { - return this.validate_and_set_in_model(value); + + set_value(value, force_set_value=false) { + return this.validate_and_set_in_model(value, null, force_set_value); } parse_validate_and_set_in_model(value, e) { if(this.parse) { @@ -157,12 +158,11 @@ frappe.ui.form.Control = class BaseControl { } return this.validate_and_set_in_model(value, e); } - validate_and_set_in_model(value, e) { - var me = this; - let force_value_set = (this.doc && this.doc.__run_link_triggers); - let is_value_same = (this.get_model_value() === value); + validate_and_set_in_model(value, e, force_set_value=false) { + const me = this; + const is_value_same = (this.get_model_value() === value); - if (this.inside_change_event || (!force_value_set && is_value_same)) { + if (this.inside_change_event || (is_value_same && !force_set_value)) { return Promise.resolve(); } diff --git a/frappe/public/js/frappe/form/controls/date.js b/frappe/public/js/frappe/form/controls/date.js index 78eb3832cc..7ad1887d62 100644 --- a/frappe/public/js/frappe/form/controls/date.js +++ b/frappe/public/js/frappe/form/controls/date.js @@ -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; diff --git a/frappe/public/js/frappe/form/controls/markdown_editor.js b/frappe/public/js/frappe/form/controls/markdown_editor.js index d9ba2df261..5acf4bd467 100644 --- a/frappe/public/js/frappe/form/controls/markdown_editor.js +++ b/frappe/public/js/frappe/form/controls/markdown_editor.js @@ -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(); } diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 57e3f576a1..1459b38df6 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -983,7 +983,7 @@ frappe.ui.form.Form = class FrappeForm { $.each(this.fields_dict, function(fieldname, field) { if (field.df.fieldtype=="Link" && this.doc[fieldname]) { // triggers add fetch, sets value in model and runs triggers - field.set_value(this.doc[fieldname]); + field.set_value(this.doc[fieldname], true); } }); diff --git a/frappe/public/js/frappe/form/form_viewers.js b/frappe/public/js/frappe/form/form_viewers.js index 964576ef8a..ecf5eea504 100644 --- a/frappe/public/js/frappe/form/form_viewers.js +++ b/frappe/public/js/frappe/form/form_viewers.js @@ -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 (new_users.length===0) return; - 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); + 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(); + }); } }; diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index aa1101c64e..8fa512bfc0 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -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; diff --git a/frappe/public/js/frappe/model/sync.js b/frappe/public/js/frappe/model/sync.js index 753046fa13..2cb7ca1a32 100644 --- a/frappe/public/js/frappe/model/sync.js +++ b/frappe/public/js/frappe/model/sync.js @@ -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]; diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index 03fd11c8f3..801435866a 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -296,11 +296,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); diff --git a/frappe/public/js/frappe/utils/user.js b/frappe/public/js/frappe/utils/user.js index f22611b515..a5a7801cc1 100644 --- a/frappe/public/js/frappe/utils/user.js +++ b/frappe/public/js/frappe/utils/user.js @@ -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, { diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index de45b3ac11..f4d41c2a0b 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -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"); diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss index a14c19af2a..e5a0052f04 100644 --- a/frappe/public/scss/common/css_variables.scss +++ b/frappe/public/scss/common/css_variables.scss @@ -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); diff --git a/frappe/public/scss/desk/css_variables.scss b/frappe/public/scss/desk/css_variables.scss index 4a2a27f8d1..0912cb278b 100644 --- a/frappe/public/scss/desk/css_variables.scss +++ b/frappe/public/scss/desk/css_variables.scss @@ -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'); diff --git a/frappe/public/scss/login.bundle.scss b/frappe/public/scss/login.bundle.scss index 3963fbecc6..0c8c2d58e2 100644 --- a/frappe/public/scss/login.bundle.scss +++ b/frappe/public/scss/login.bundle.scss @@ -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, diff --git a/frappe/public/scss/website/footer.scss b/frappe/public/scss/website/footer.scss index dc73fd180e..f3bdfed07f 100644 --- a/frappe/public/scss/website/footer.scss +++ b/frappe/public/scss/website/footer.scss @@ -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; } } } \ No newline at end of file diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss index c4f66b803b..69a7b205c4 100644 --- a/frappe/public/scss/website/index.scss +++ b/frappe/public/scss/website/index.scss @@ -27,6 +27,14 @@ @import 'navbar'; @import 'footer'; @import 'error-state'; +@import 'my_account'; + + +body { + @include media-breakpoint-up(sm) { + background-color: var(--bg-color); + } +} .ql-editor.read-mode { padding: 0; @@ -166,6 +174,10 @@ a.card { font-size: inherit; } +.indicator-pill { + font-size: var(--font-size-xs) +} + h4.modal-title { font-size: 1em; } @@ -298,3 +310,7 @@ h5.modal-title { margin: 70px auto; font-size: $font-size-sm; } + +.empty-list-icon { + height: 70px; +} \ No newline at end of file diff --git a/frappe/public/scss/website/my_account.scss b/frappe/public/scss/website/my_account.scss new file mode 100644 index 0000000000..bdc52588aa --- /dev/null +++ b/frappe/public/scss/website/my_account.scss @@ -0,0 +1,116 @@ +//styles for my account and edit-profile page +@include media-breakpoint-up(sm) { + body[data-path="me"], + body[data-path="list"] { + 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; + } +} \ No newline at end of file diff --git a/frappe/public/scss/website/web_form.scss b/frappe/public/scss/website/web_form.scss index 6a6547d79e..cb79f88266 100644 --- a/frappe/public/scss/website/web_form.scss +++ b/frappe/public/scss/website/web_form.scss @@ -1,5 +1,31 @@ @import "../common/form"; + +[data-doctype="Web Form"] { + .page-content-wrapper { + + .breadcrumb-container.container { + @include media-breakpoint-up(sm) { + padding-left: var(--padding-sm); + } + } + + .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); + } + } + } } \ No newline at end of file diff --git a/frappe/sessions.py b/frappe/sessions.py index f0609cd74e..6c9acdba13 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -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) diff --git a/frappe/templates/includes/list/list.html b/frappe/templates/includes/list/list.html index fba5f20ed5..14769ccf93 100644 --- a/frappe/templates/includes/list/list.html +++ b/frappe/templates/includes/list/list.html @@ -2,8 +2,9 @@

{{ sub_title }}

{% endif %} {% if not result -%} -
- {{ no_result_message or _("Nothing to show") }} +
+ +
{{ no_result_message or _("Nothing to show") }}
{% else %}
', content) self.assertIn('
Language
', content) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index ffff7032aa..54a6f0b7e8 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -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 `""" fullname = get_fullname(user) - + method = get_hook_method('get_sender_details') if method: sender_name, mail = method() @@ -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 + ) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 545d49054a..b4ac5e7fef 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -113,6 +113,9 @@ def get_timedelta(time: Optional[str] = None) -> Optional[datetime.timedelta]: def to_timedelta(time_str): from dateutil import parser + if isinstance(time_str, datetime.time): + time_str = str(time_str) + if isinstance(time_str, str): t = parser.parse(time_str) return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond) diff --git a/frappe/utils/install.py b/frappe/utils/install.py index 5ca8c4878a..cf76c9fffc 100644 --- a/frappe/utils/install.py +++ b/frappe/utils/install.py @@ -219,6 +219,12 @@ def add_standard_navbar_items(): 'action': 'frappe.ui.toolbar.toggle_full_width()', 'is_standard': 1 }, + { + 'item_label': 'Toggle Theme', + 'item_type': 'Action', + 'action': 'new frappe.ui.ThemeSwitcher().show()', + 'is_standard': 1 + }, { 'item_label': 'Background Jobs', 'item_type': 'Route', diff --git a/frappe/utils/make_random.py b/frappe/utils/make_random.py index 2ebabb78f9..bb4395b626 100644 --- a/frappe/utils/make_random.py +++ b/frappe/utils/make_random.py @@ -35,9 +35,13 @@ def get_random(doctype, filters=None, doc=False): condition = " where " + " and ".join(condition) else: condition = "" - - out = frappe.db.sql("""select name from `tab%s` %s - order by RAND() limit 0,1""" % (doctype, condition)) + + out = frappe.db.multisql({ + 'mariadb': """select name from `tab%s` %s + order by RAND() limit 1 offset 0""" % (doctype, condition), + 'postgres': """select name from `tab%s` %s + order by RANDOM() limit 1 offset 0""" % (doctype, condition) + }) out = out and out[0][0] or None diff --git a/frappe/utils/nestedset.py b/frappe/utils/nestedset.py index bdab81bcaa..98ad337043 100644 --- a/frappe/utils/nestedset.py +++ b/frappe/utils/nestedset.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE # Tree (Hierarchical) Nested Set Model (nsm) @@ -109,7 +109,6 @@ def update_move_node(doc, parent_field): new_parent = frappe.db.sql("""select lft, rgt from `tab%s` where name = %s for update""" % (doc.doctype, '%s'), parent, as_dict=1)[0] - # set parent lft, rgt frappe.db.sql("""update `tab{0}` set rgt = rgt + %s where name = %s""".format(doc.doctype), (diff, parent)) @@ -134,6 +133,7 @@ def update_move_node(doc, parent_field): frappe.db.sql("""update `tab{0}` set lft = -lft + %s, rgt = -rgt + %s where lft < 0""".format(doc.doctype), (new_diff, new_diff)) + @frappe.whitelist() def rebuild_tree(doctype, parent_field): """ @@ -153,7 +153,6 @@ def rebuild_tree(doctype, parent_field): right = 1 table = DocType(doctype) column = getattr(table, parent_field) - result = ( frappe.qb.from_(table) .where( diff --git a/frappe/utils/response.py b/frappe/utils/response.py index 104c48527c..f6ad91dbd2 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -125,7 +125,7 @@ def json_handler(obj): # serialize date import collections.abc - if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)): + if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime, datetime.time)): return str(obj) elif isinstance(obj, decimal.Decimal): diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index 43580c1287..8ebb4b2937 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -17,7 +17,6 @@ import schedule # imports - module imports import frappe -from frappe.core.doctype.user.user import STANDARD_USERS from frappe.installer import update_site_config from frappe.utils import get_sites, now_datetime from frappe.utils.background_jobs import get_jobs diff --git a/frappe/utils/user.py b/frappe/utils/user.py index e5acb53daf..cbf38f6acb 100755 --- a/frappe/utils/user.py +++ b/frappe/utils/user.py @@ -230,7 +230,6 @@ def get_fullname_and_avatar(user): def get_system_managers(only_name=False): """returns all system manager's user details""" import email.utils - from frappe.core.doctype.user.user import STANDARD_USERS system_managers = frappe.db.sql("""SELECT DISTINCT `name`, `creation`, CONCAT_WS(' ', CASE WHEN `first_name`= '' THEN NULL ELSE `first_name` END, @@ -245,8 +244,8 @@ def get_system_managers(only_name=False): FROM `tabHas Role` AS ur WHERE ur.parent = p.name AND ur.role='System Manager') - ORDER BY `creation` DESC""".format(", ".join(["%s"]*len(STANDARD_USERS))), - STANDARD_USERS, as_dict=True) + ORDER BY `creation` DESC""".format(", ".join(["%s"]*len(frappe.STANDARD_USERS))), + frappe.STANDARD_USERS, as_dict=True) if only_name: return [p.name for p in system_managers] diff --git a/frappe/website/doctype/blog_post/test_blog_post.py b/frappe/website/doctype/blog_post/test_blog_post.py index 7a09d0a3ca..d649d25f7e 100644 --- a/frappe/website/doctype/blog_post/test_blog_post.py +++ b/frappe/website/doctype/blog_post/test_blog_post.py @@ -58,15 +58,18 @@ class TestBlogPost(unittest.TestCase): category_page_link = list(soup.find_all('a', href=re.compile(blog.blog_category)))[0] category_page_url = category_page_link["href"] + cached_value = frappe.db.value_cache[('DocType', 'Blog Post', 'name')] + frappe.db.value_cache[('DocType', 'Blog Post', 'name')] = (('Blog Post',),) + # Visit the category page (by following the link found in above stage) set_request(path=category_page_url) category_page_response = get_response() category_page_html = frappe.safe_decode(category_page_response.get_data()) - # Category page should contain the blog post title self.assertIn(blog.title, category_page_html) # Cleanup + frappe.db.value_cache[('DocType', 'Blog Post', 'name')] = cached_value frappe.delete_doc("Blog Post", blog.name) frappe.delete_doc("Blog Category", blog.blog_category) diff --git a/frappe/website/doctype/web_form/templates/web_form.html b/frappe/website/doctype/web_form/templates/web_form.html index 743c094314..a8666b55e9 100644 --- a/frappe/website/doctype/web_form/templates/web_form.html +++ b/frappe/website/doctype/web_form/templates/web_form.html @@ -3,7 +3,7 @@ {% block title %}{{ _(title) }}{% endblock %} {% block header %} -

{{ _(title) }}

+

{{ _(title) }}

{% endblock %} {% block breadcrumbs %} @@ -29,8 +29,8 @@ data-web-form="{{ name }}" data-web-form-doctype="{{ doc_type }}" data-login-req {% if is_list %} {# web form list #}
-
-
+
+
{% else %} {# web form #} @@ -38,7 +38,7 @@ data-web-form="{{ name }}" data-web-form-doctype="{{ doc_type }}" data-login-req

- +
{% if show_attachments and not frappe.form_dict.new and attachments %} diff --git a/frappe/website/doctype/web_form/test_web_form.py b/frappe/website/doctype/web_form/test_web_form.py index 91ee4195df..3e05b221d8 100644 --- a/frappe/website/doctype/web_form/test_web_form.py +++ b/frappe/website/doctype/web_form/test_web_form.py @@ -66,7 +66,7 @@ class TestWebForm(unittest.TestCase): def test_webform_render(self): content = get_response_content('request-data') - self.assertIn('

Request Data

', content) + self.assertIn('

Request Data

', content) self.assertIn('data-doctype="Web Form"', content) self.assertIn('data-path="request-data"', content) self.assertIn('source-type="Generator"', content) diff --git a/frappe/www/list.html b/frappe/www/list.html index 842d55ff92..06c494b804 100644 --- a/frappe/www/list.html +++ b/frappe/www/list.html @@ -5,7 +5,7 @@ {% endblock %} {% block header %} -

{{ title or (_("{0} List").format(_(doctype))) }}

+

{{ title or (_("{0} List").format(_(doctype))) }}

{% endblock %} {% block breadcrumbs %} @@ -23,11 +23,9 @@ {% endblock %} {% block page_content %} - -{% if introduction %}

{{ introduction }}

{% endif %} -{% include list_template or "templates/includes/list/list.html" %} -{% if list_footer %}{{ list_footer }}{% endif %} - + {% if introduction %}

{{ introduction }}

{% endif %} + {% include list_template or "templates/includes/list/list.html" %} + {% if list_footer %}{{ list_footer }}{% endif %} {% endblock %} {% block script %} diff --git a/frappe/www/me.html b/frappe/www/me.html index 4f9a59cac5..196457ae0b 100644 --- a/frappe/www/me.html +++ b/frappe/www/me.html @@ -1,31 +1,94 @@ +{% from "frappe/templates/includes/avatar_macro.html" import avatar %} {% extends "templates/web.html" %} - -{% block title %}{{ _("My Account") }}{% endblock %} -{% block header %}

{{ _("My Account") }}

{% endblock %} - +{% block title %} +{{ _("My Account") }} +{% endblock %} +{% block header %} +

{{_("My Account") }}

+{% endblock %} {% block page_content %} -
-
- +
-
+
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/frappe/www/me.py b/frappe/www/me.py index fbb6d53ac6..0ec73117ac 100644 --- a/frappe/www/me.py +++ b/frappe/www/me.py @@ -10,5 +10,6 @@ no_cache = 1 def get_context(context): if frappe.session.user=='Guest': frappe.throw(_("You need to be logged in to access this page"), frappe.PermissionError) - + + context.current_user = frappe.get_doc("User", frappe.session.user) context.show_sidebar=True \ No newline at end of file diff --git a/frappe/www/third_party_apps.html b/frappe/www/third_party_apps.html index db31a4d1c8..0763382f70 100644 --- a/frappe/www/third_party_apps.html +++ b/frappe/www/third_party_apps.html @@ -2,7 +2,7 @@ {% block title %} {{ _("Third Party Apps") }} {% endblock %} {% block header %} -

{{ _("Third Party Apps") }}

+ {% endblock %} {% block page_sidebar %} @@ -52,9 +52,15 @@
{% endfor %} {% else %} -
+
+ +
{{ _("No Active Sessions")}}
+
+ {{ _("Looks like you haven’t added any third party apps.")}} +
+
{% endif %}