Browse Source

Merge branch 'develop' into fix-ui-test

version-14
Shariq Ansari 3 years ago
committed by GitHub
parent
commit
50db829b3e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 1442 additions and 752 deletions
  1. +3
    -3
      cypress/integration/timeline_email.js
  2. +7
    -4
      frappe/__init__.py
  3. +3
    -3
      frappe/auth.py
  4. +1
    -1
      frappe/automation/doctype/assignment_rule/assignment_rule.py
  5. +7
    -10
      frappe/boot.py
  6. +1
    -1
      frappe/cache_manager.py
  7. +2
    -2
      frappe/commands/site.py
  8. +1
    -1
      frappe/core/doctype/data_export/exporter.py
  9. +105
    -0
      frappe/core/doctype/data_export/test_data_exporter.py
  10. +4
    -1
      frappe/core/doctype/document_naming_rule/document_naming_rule.py
  11. +2
    -1
      frappe/core/doctype/language/language.py
  12. +47
    -144
      frappe/core/doctype/payment_gateway/payment_gateway.json
  13. +4
    -3
      frappe/core/doctype/role/role.json
  14. +2
    -2
      frappe/core/doctype/user/user.py
  15. +4
    -6
      frappe/core/doctype/user_type/user_type.py
  16. +1
    -1
      frappe/custom/doctype/customize_form/customize_form.py
  17. +5
    -3
      frappe/custom/doctype/property_setter/property_setter.py
  18. +9
    -2
      frappe/database/mariadb/database.py
  19. +25
    -9
      frappe/database/mariadb/schema.py
  20. +14
    -6
      frappe/database/postgres/database.py
  21. +58
    -26
      frappe/database/postgres/schema.py
  22. +1
    -1
      frappe/database/query.py
  23. +5
    -4
      frappe/database/schema.py
  24. +2
    -2
      frappe/defaults.py
  25. +2
    -1
      frappe/desk/doctype/bulk_update/bulk_update.py
  26. +19
    -9
      frappe/desk/doctype/dashboard/dashboard.py
  27. +80
    -13
      frappe/desk/form/load.py
  28. +1
    -1
      frappe/desk/moduleview.py
  29. +2
    -1
      frappe/desk/page/setup_wizard/setup_wizard.py
  30. +1
    -1
      frappe/desk/page/user_profile/user_profile.py
  31. +10
    -2
      frappe/desk/reportview.py
  32. +1
    -1
      frappe/desk/search.py
  33. +3
    -2
      frappe/desk/treeview.py
  34. +14
    -22
      frappe/email/doctype/email_queue/email_queue.py
  35. +0
    -4
      frappe/email/receive.py
  36. +1
    -1
      frappe/installer.py
  37. +2
    -2
      frappe/integrations/doctype/connected_app/connected_app.json
  38. +8
    -11
      frappe/model/base_document.py
  39. +19
    -1
      frappe/model/db_query.py
  40. +11
    -9
      frappe/model/document.py
  41. +1
    -1
      frappe/model/dynamic_links.py
  42. +3
    -2
      frappe/patches.txt
  43. +1
    -1
      frappe/patches/v11_0/remove_skip_for_doctype.py
  44. +17
    -0
      frappe/patches/v13_0/remove_invalid_options_for_data_fields.py
  45. +4
    -2
      frappe/permissions.py
  46. +6
    -0
      frappe/public/images/ui-states/empty-app-state.svg
  47. +3
    -2
      frappe/public/js/frappe/desk.js
  48. +6
    -4
      frappe/public/js/frappe/form/controls/date.js
  49. +2
    -1
      frappe/public/js/frappe/form/controls/date_range.js
  50. +3
    -1
      frappe/public/js/frappe/form/controls/markdown_editor.js
  51. +6
    -3
      frappe/public/js/frappe/form/footer/base_timeline.js
  52. +5
    -3
      frappe/public/js/frappe/form/footer/form_timeline.js
  53. +4
    -1
      frappe/public/js/frappe/form/form.js
  54. +35
    -14
      frappe/public/js/frappe/form/form_viewers.js
  55. +8
    -9
      frappe/public/js/frappe/form/save.js
  56. +5
    -0
      frappe/public/js/frappe/list/base_list.js
  57. +42
    -35
      frappe/public/js/frappe/list/list_view.js
  58. +21
    -10
      frappe/public/js/frappe/model/sync.js
  59. +12
    -5
      frappe/public/js/frappe/request.js
  60. +4
    -3
      frappe/public/js/frappe/router.js
  61. +4
    -2
      frappe/public/js/frappe/ui/dialog.js
  62. +7
    -4
      frappe/public/js/frappe/ui/messages.js
  63. +1
    -1
      frappe/public/js/frappe/utils/diffview.js
  64. +0
    -31
      frappe/public/js/frappe/utils/user.js
  65. +1
    -0
      frappe/public/js/frappe/views/formview.js
  66. +40
    -0
      frappe/public/js/frappe/views/kanban/kanban_board.js
  67. +9
    -3
      frappe/public/js/frappe/views/reports/report_view.js
  68. +8
    -6
      frappe/public/js/frappe/web_form/web_form.js
  69. +0
    -2
      frappe/public/js/frappe/web_form/web_form_list.js
  70. +12
    -0
      frappe/public/scss/common/alert.scss
  71. +20
    -0
      frappe/public/scss/common/css_variables.scss
  72. +0
    -9
      frappe/public/scss/desk/css_variables.scss
  73. +10
    -0
      frappe/public/scss/desk/dark.scss
  74. +5
    -0
      frappe/public/scss/desk/frappe_datatable.scss
  75. +1
    -0
      frappe/public/scss/desk/index.scss
  76. +1
    -1
      frappe/public/scss/desk/page.scss
  77. +17
    -29
      frappe/public/scss/desk/timeline.scss
  78. +22
    -2
      frappe/public/scss/desk/toast.scss
  79. +3
    -1
      frappe/public/scss/login.bundle.scss
  80. +2
    -0
      frappe/public/scss/website/footer.scss
  81. +12
    -1
      frappe/public/scss/website/index.scss
  82. +117
    -0
      frappe/public/scss/website/my_account.scss
  83. +47
    -0
      frappe/public/scss/website/web_form.scss
  84. +10
    -5
      frappe/sessions.py
  85. +1
    -1
      frappe/social/doctype/energy_point_rule/energy_point_rule.py
  86. +3
    -2
      frappe/templates/includes/list/list.html
  87. +49
    -41
      frappe/templates/styles/card_style.css
  88. +4
    -1
      frappe/test_runner.py
  89. +63
    -38
      frappe/tests/test_auth.py
  90. +185
    -138
      frappe/tests/test_commands.py
  91. +19
    -1
      frappe/tests/test_db_query.py
  92. +51
    -1
      frappe/tests/test_db_update.py
  93. +5
    -0
      frappe/tests/test_document.py
  94. +4
    -3
      frappe/tests/test_twofactor.py
  95. +1
    -0
      frappe/tests/test_website.py
  96. +8
    -7
      frappe/tests/ui_test_helpers.py
  97. +1
    -1
      frappe/translate.py
  98. +1
    -0
      frappe/translations/de.csv
  99. +14
    -4
      frappe/utils/__init__.py
  100. +9
    -3
      frappe/utils/background_jobs.py

+ 3
- 3
cypress/integration/timeline_email.js View File

@@ -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();


+ 7
- 4
frappe/__init__.py View File

@@ -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()



+ 3
- 3
frappe/auth.py View File

@@ -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",


+ 1
- 1
frappe/automation/doctype/assignment_rule/assignment_rule.py View File

@@ -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:


+ 7
- 10
frappe/boot.py View File

@@ -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"""


+ 1
- 1
frappe/cache_manager.py View File

@@ -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


+ 2
- 2
frappe/commands/site.py View File

@@ -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,
)


+ 1
- 1
frappe/core/doctype/data_export/exporter.py View File

@@ -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:


+ 105
- 0
frappe/core/doctype/data_export/test_data_exporter.py View File

@@ -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


+ 4
- 1
frappe/core/doctype/document_naming_rule/document_naming_rule.py View File

@@ -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()


+ 2
- 1
frappe/core/doctype/language/language.py View File

@@ -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():


+ 47
- 144
frappe/core/doctype/payment_gateway/payment_gateway.json View File

@@ -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": []
}

+ 4
- 3
frappe/core/doctype/role/role.json View File

@@ -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
}

+ 2
- 2
frappe/core/doctype/user/user.py View File

@@ -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():


+ 4
- 6
frappe/core/doctype/user_type/user_type.py View File

@@ -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))


+ 1
- 1
frappe/custom/doctype/customize_form/customize_form.py View File

@@ -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)



+ 5
- 3
frappe/custom/doctype/property_setter/property_setter.py View File

@@ -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)


+ 9
- 2
frappe/database/mariadb/database.py View File

@@ -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):


+ 25
- 9
frappe/database/mariadb/schema.py View File

@@ -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]:


+ 14
- 6
frappe/database/postgres/database.py View File

@@ -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;")]


+ 58
- 26
frappe/database/postgres/schema.py View File

@@ -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

+ 1
- 1
frappe/database/query.py View File

@@ -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",


+ 5
- 4
frappe/database/schema.py View File

@@ -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')):


+ 2
- 2
frappe/defaults.py View File

@@ -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(


+ 2
- 1
frappe/desk/doctype/bulk_update/bulk_update.py View File

@@ -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


+ 19
- 9
frappe/desk/doctype/dashboard/dashboard.py View File

@@ -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:


+ 80
- 13
frappe/desk/form/load.py View File

@@ -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

+ 1
- 1
frappe/desk/moduleview.py View File

@@ -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


+ 2
- 1
frappe/desk/page/setup_wizard/setup_wizard.py View File

@@ -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)


+ 1
- 1
frappe/desk/page/user_profile/user_profile.py View File

@@ -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]
}]


+ 10
- 2
frappe/desk/reportview.py View File

@@ -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()


+ 1
- 1
frappe/desk/search.py View File

@@ -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 = []



+ 3
- 2
frappe/desk/treeview.py View File

@@ -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)



+ 14
- 22
frappe/email/doctype/email_queue/email_queue.py View File

@@ -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



+ 0
- 4
frappe/email/receive.py View File

@@ -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


+ 1
- 1
frappe/installer.py View File

@@ -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":


+ 2
- 2
frappe/integrations/doctype/connected_app/connected_app.json View File

@@ -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",


+ 8
- 11
frappe/model/base_document.py View File

@@ -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

+ 19
- 1
frappe/model/db_query.py View File

@@ -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):


+ 11
- 9
frappe/model/document.py View File

@@ -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)


+ 1
- 1
frappe/model/dynamic_links.py View File

@@ -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():


+ 3
- 2
frappe/patches.txt View File

@@ -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


+ 1
- 1
frappe/patches/v11_0/remove_skip_for_doctype.py View File

@@ -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


+ 17
- 0
frappe/patches/v13_0/remove_invalid_options_for_data_fields.py View File

@@ -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()

+ 4
- 2
frappe/permissions.py View File

@@ -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,


+ 6
- 0
frappe/public/images/ui-states/empty-app-state.svg View File

@@ -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>

+ 3
- 2
frappe/public/js/frappe/desk.js View File

@@ -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")
}
]
});


+ 6
- 4
frappe/public/js/frappe/form/controls/date.js View File

@@ -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;


+ 2
- 1
frappe/public/js/frappe/form/controls/date_range.js View File

@@ -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');


+ 3
- 1
frappe/public/js/frappe/form/controls/markdown_editor.js View File

@@ -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();
}



+ 6
- 3
frappe/public/js/frappe/form/footer/base_timeline.js View File

@@ -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;
}



+ 5
- 3
frappe/public/js/frappe/form/footer/form_timeline.js View File

@@ -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')


+ 4
- 1
frappe/public/js/frappe/form/form.js View File

@@ -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")
);
}
}



+ 35
- 14
frappe/public/js/frappe/form/form_viewers.js View File

@@ -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();
});
}
};

+ 8
- 9
frappe/public/js/frappe/form/save.js View File

@@ -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;
}
}


+ 5
- 0
frappe/public/js/frappe/list/base_list.js View File

@@ -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;


+ 42
- 35
frappe/public/js/frappe/list/list_view.js View File

@@ -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);



+ 21
- 10
frappe/public/js/frappe/model/sync.js View File

@@ -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];


+ 12
- 5
frappe/public/js/frappe/request.js View File

@@ -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);


+ 4
- 3
frappe/public/js/frappe/router.js View File

@@ -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();


+ 4
- 2
frappe/public/js/frappe/ui/dialog.js View File

@@ -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) {


+ 7
- 4
frappe/public/js/frappe/ui/messages.js View File

@@ -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();



+ 1
- 1
frappe/public/js/frappe/utils/diffview.js View File

@@ -54,7 +54,7 @@ frappe.ui.DiffView = class DiffView {
fieldname: "diff",
},
],
size: "large",
size: "extra-large",
});
return dialog;
}


+ 0
- 31
frappe/public/js/frappe/utils/user.js View File

@@ -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, {


+ 1
- 0
frappe/public/js/frappe/views/formview.js View File

@@ -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);
}
}


+ 40
- 0
frappe/public/js/frappe/views/kanban/kanban_board.js View File

@@ -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;


+ 9
- 3
frappe/public/js/frappe/views/reports/report_view.js View File

@@ -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()])
);
}
}



+ 8
- 6
frappe/public/js/frappe/web_form/web_form.js View File

@@ -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'
});


+ 0
- 2
frappe/public/js/frappe/web_form/web_form_list.js View File

@@ -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");


+ 12
- 0
frappe/public/scss/common/alert.scss View File

@@ -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;
}
}
}

+ 20
- 0
frappe/public/scss/common/css_variables.scss View File

@@ -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;


+ 0
- 9
frappe/public/scss/desk/css_variables.scss View File

@@ -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');


+ 10
- 0
frappe/public/scss/desk/dark.scss View File

@@ -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);


+ 5
- 0
frappe/public/scss/desk/frappe_datatable.scss View File

@@ -87,6 +87,11 @@
}
}

.dt-cell__resize-handle {
right: -3px !important;
left: unset !important;
}

.dt-row.dt-row-totalRow {
font-weight: bold;
}


+ 1
- 0
frappe/public/scss/desk/index.scss View File

@@ -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";


+ 1
- 1
frappe/public/scss/desk/page.scss View File

@@ -164,7 +164,7 @@

// To compensate for perceived centering
.null-state {
height: 85px;
height: 60px;
width: auto;
margin-bottom: var(--margin-md);
img {


+ 17
- 29
frappe/public/scss/desk/timeline.scss View File

@@ -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);


+ 22
- 2
frappe/public/scss/desk/toast.scss View File

@@ -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);
}
}



+ 3
- 1
frappe/public/scss/login.bundle.scss View File

@@ -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,


+ 2
- 0
frappe/public/scss/website/footer.scss View File

@@ -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;
}
}
}

+ 12
- 1
frappe/public/scss/website/index.scss View File

@@ -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;
}

+ 117
- 0
frappe/public/scss/website/my_account.scss View File

@@ -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;
}
}

+ 47
- 0
frappe/public/scss/website/web_form.scss View File

@@ -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);
}
}
}
}

+ 10
- 5
frappe/sessions.py View File

@@ -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']),


+ 1
- 1
frappe/social/doctype/energy_point_rule/energy_point_rule.py View File

@@ -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':


+ 3
- 2
frappe/templates/includes/list/list.html View File

@@ -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 }}"


+ 49
- 41
frappe/templates/styles/card_style.css View File

@@ -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;
}

+ 4
- 1
frappe/test_runner.py View File

@@ -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)


+ 63
- 38
frappe/tests/test_auth.py View File

@@ -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):


+ 185
- 138
frappe/tests/test_commands.py View File

@@ -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,


+ 19
- 1
frappe/tests/test_db_query.py View File

@@ -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
"""


+ 51
- 1
frappe/tests/test_db_update.py View File

@@ -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)

+ 5
- 0
frappe/tests/test_document.py View File

@@ -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)

+ 4
- 3
frappe/tests/test_twofactor.py View File

@@ -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()


+ 1
- 0
frappe/tests/test_website.py View File

@@ -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)


+ 8
- 7
frappe/tests/ui_test_helpers.py View File

@@ -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()

+ 1
- 1
frappe/translate.py View File

@@ -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:


+ 1
- 0
frappe/translations/de.csv View File

@@ -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,


+ 14
- 4
frappe/utils/__init__.py View File

@@ -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
)

+ 9
- 3
frappe/utils/background_jobs.py View File

@@ -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


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save