浏览代码

Merge branch 'develop' of github.com:frappe/frappe into owner-unchange

version-14
Gavin D'souza 3 年前
父节点
当前提交
6f2125fca6
共有 47 个文件被更改,包括 940 次插入713 次删除
  1. +3
    -1
      cypress.json
  2. +2
    -2
      cypress/support/commands.js
  3. +1
    -1
      frappe/automation/doctype/assignment_rule/assignment_rule.py
  4. +14
    -14
      frappe/automation/doctype/assignment_rule/test_assignment_rule.py
  5. +551
    -541
      frappe/core/doctype/docfield/docfield.json
  6. +1
    -0
      frappe/core/doctype/file/test_file.py
  7. +10
    -2
      frappe/core/doctype/module_def/module_def.json
  8. +2
    -3
      frappe/core/doctype/translation/translation.json
  9. +1
    -1
      frappe/core/doctype/user/test_user.py
  10. +9
    -8
      frappe/core/doctype/user/user.json
  11. +1
    -1
      frappe/core/doctype/user/user.py
  12. +1
    -1
      frappe/core/doctype/user_permission/user_permission.js
  13. +7
    -6
      frappe/core/doctype/user_permission/user_permission.json
  14. +1
    -1
      frappe/core/notifications.py
  15. +1
    -0
      frappe/custom/doctype/customize_form/customize_form.py
  16. +13
    -3
      frappe/custom/doctype/customize_form_field/customize_form_field.json
  17. +2
    -0
      frappe/database/__init__.py
  18. +35
    -5
      frappe/database/database.py
  19. +1
    -0
      frappe/database/mariadb/framework_mariadb.sql
  20. +1
    -0
      frappe/database/postgres/framework_postgres.sql
  21. +1
    -1
      frappe/desk/doctype/event/test_event.py
  22. +11
    -11
      frappe/desk/doctype/todo/todo.json
  23. +9
    -9
      frappe/desk/doctype/todo/todo.py
  24. +15
    -14
      frappe/desk/form/assign_to.py
  25. +3
    -3
      frappe/desk/listview.py
  26. +1
    -10
      frappe/email/doctype/email_template/email_template.json
  27. +2
    -2
      frappe/email/doctype/notification/notification.py
  28. +1
    -4
      frappe/model/base_document.py
  29. +10
    -4
      frappe/model/document.py
  30. +1
    -1
      frappe/model/rename_doc.py
  31. +1
    -0
      frappe/patches.txt
  32. +12
    -0
      frappe/patches/v14_0/transform_todo_schema.py
  33. +1
    -0
      frappe/public/js/frappe/form/controls/multiselect_list.js
  34. +1
    -1
      frappe/public/js/frappe/form/form.js
  35. +5
    -0
      frappe/public/js/frappe/list/list_view.js
  36. +15
    -10
      frappe/public/js/frappe/views/reports/query_report.js
  37. +3
    -1
      frappe/public/js/frappe/views/treeview.js
  38. +1
    -1
      frappe/tests/test_assign.py
  39. +27
    -0
      frappe/tests/test_db.py
  40. +19
    -0
      frappe/tests/test_document.py
  41. +5
    -0
      frappe/utils/nestedset.py
  42. +28
    -12
      frappe/website/doctype/blog_post/blog_post.py
  43. +28
    -10
      frappe/website/doctype/blog_post/templates/blog_post_list.html
  44. +36
    -0
      frappe/website/doctype/blog_post/ui_test_blog_post.js
  45. +10
    -2
      frappe/website/doctype/blog_settings/blog_settings.json
  46. +2
    -10
      frappe/website/doctype/help_article/help_article.json
  47. +35
    -17
      frappe/website/doctype/web_page/web_page.json

+ 3
- 1
cypress.json 查看文件

@@ -9,5 +9,7 @@
"retries": { "retries": {
"runMode": 2, "runMode": 2,
"openMode": 2 "openMode": 2
}
},
"integrationFolder": ".",
"testFiles": ["cypress/integration/*.js", "**/ui_test_*.js"]
} }

+ 2
- 2
cypress/support/commands.js 查看文件

@@ -30,7 +30,7 @@ Cypress.Commands.add('login', (email, password) => {
email = 'Administrator'; email = 'Administrator';
} }
if (!password) { if (!password) {
password = Cypress.config('adminPassword');
password = Cypress.env('adminPassword');
} }
cy.request({ cy.request({
url: '/api/method/login', url: '/api/method/login',
@@ -161,7 +161,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => {


Cypress.Commands.add('create_records', doc => { Cypress.Commands.add('create_records', doc => {
return cy return cy
.call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc})
.call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc: JSON.stringify(doc)})
.then(r => r.message); .then(r => r.message);
}); });




+ 1
- 1
frappe/automation/doctype/assignment_rule/assignment_rule.py 查看文件

@@ -123,7 +123,7 @@ class AssignmentRule(Document):
user = d.user, user = d.user,
count = frappe.db.count('ToDo', dict( count = frappe.db.count('ToDo', dict(
reference_type = self.document_type, reference_type = self.document_type,
owner = d.user,
allocated_to = d.user,
status = "Open")) status = "Open"))
)) ))




+ 14
- 14
frappe/automation/doctype/assignment_rule/test_assignment_rule.py 查看文件

@@ -40,7 +40,7 @@ class TestAutoAssign(unittest.TestCase):
reference_type = 'Note', reference_type = 'Note',
reference_name = note.name, reference_name = note.name,
status = 'Open' status = 'Open'
), 'owner'), 'test@example.com')
), 'allocated_to'), 'test@example.com')


note = make_note(dict(public=1)) note = make_note(dict(public=1))


@@ -49,7 +49,7 @@ class TestAutoAssign(unittest.TestCase):
reference_type = 'Note', reference_type = 'Note',
reference_name = note.name, reference_name = note.name,
status = 'Open' status = 'Open'
), 'owner'), 'test1@example.com')
), 'allocated_to'), 'test1@example.com')


clear_assignments() clear_assignments()


@@ -61,7 +61,7 @@ class TestAutoAssign(unittest.TestCase):
reference_type = 'Note', reference_type = 'Note',
reference_name = note.name, reference_name = note.name,
status = 'Open' status = 'Open'
), 'owner'), 'test2@example.com')
), 'allocated_to'), 'test2@example.com')


# check loop back to first user # check loop back to first user
note = make_note(dict(public=1)) note = make_note(dict(public=1))
@@ -70,7 +70,7 @@ class TestAutoAssign(unittest.TestCase):
reference_type = 'Note', reference_type = 'Note',
reference_name = note.name, reference_name = note.name,
status = 'Open' status = 'Open'
), 'owner'), 'test@example.com')
), 'allocated_to'), 'test@example.com')


def test_load_balancing(self): def test_load_balancing(self):
self.assignment_rule.rule = 'Load Balancing' self.assignment_rule.rule = 'Load Balancing'
@@ -81,11 +81,11 @@ class TestAutoAssign(unittest.TestCase):


# check if each user has 10 assignments (?) # check if each user has 10 assignments (?)
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'): for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10)
self.assertEqual(len(frappe.get_all('ToDo', dict(allocated_to = user, reference_type = 'Note'))), 10)


# clear 5 assignments for first user # clear 5 assignments for first user
# can't do a limit in "delete" since postgres does not support it # can't do a limit in "delete" since postgres does not support it
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', owner = 'test@example.com'), limit=5):
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', allocated_to = 'test@example.com'), limit=5):
frappe.db.delete("ToDo", {"name": d.name}) frappe.db.delete("ToDo", {"name": d.name})


# add 5 more assignments # add 5 more assignments
@@ -94,7 +94,7 @@ class TestAutoAssign(unittest.TestCase):


# check if each user still has 10 assignments # check if each user still has 10 assignments
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'): for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10)
self.assertEqual(len(frappe.get_all('ToDo', dict(allocated_to = user, reference_type = 'Note'))), 10)


def test_based_on_field(self): def test_based_on_field(self):
self.assignment_rule.rule = 'Based on Field' self.assignment_rule.rule = 'Based on Field'
@@ -129,7 +129,7 @@ class TestAutoAssign(unittest.TestCase):
reference_type = 'Note', reference_type = 'Note',
reference_name = note.name, reference_name = note.name,
status = 'Open' status = 'Open'
), 'owner'), None)
), 'allocated_to'), None)


def test_clear_assignment(self): def test_clear_assignment(self):
note = make_note(dict(public=1)) note = make_note(dict(public=1))
@@ -142,7 +142,7 @@ class TestAutoAssign(unittest.TestCase):
), limit=1)[0] ), limit=1)[0]


todo = frappe.get_doc('ToDo', todo['name']) todo = frappe.get_doc('ToDo', todo['name'])
self.assertEqual(todo.owner, 'test@example.com')
self.assertEqual(todo.allocated_to, 'test@example.com')


# test auto unassign # test auto unassign
note.public = 0 note.public = 0
@@ -164,7 +164,7 @@ class TestAutoAssign(unittest.TestCase):
), limit=1)[0] ), limit=1)[0]


todo = frappe.get_doc('ToDo', todo['name']) todo = frappe.get_doc('ToDo', todo['name'])
self.assertEqual(todo.owner, 'test@example.com')
self.assertEqual(todo.allocated_to, 'test@example.com')


note.content="Closed" note.content="Closed"
note.save() note.save()
@@ -174,7 +174,7 @@ class TestAutoAssign(unittest.TestCase):
# check if todo is closed # check if todo is closed
self.assertEqual(todo.status, 'Closed') self.assertEqual(todo.status, 'Closed')
# check if closed todo retained assignment # check if closed todo retained assignment
self.assertEqual(todo.owner, 'test@example.com')
self.assertEqual(todo.allocated_to, 'test@example.com')


def check_multiple_rules(self): def check_multiple_rules(self):
note = make_note(dict(public=1, notify_on_login=1)) note = make_note(dict(public=1, notify_on_login=1))
@@ -184,7 +184,7 @@ class TestAutoAssign(unittest.TestCase):
reference_type = 'Note', reference_type = 'Note',
reference_name = note.name, reference_name = note.name,
status = 'Open' status = 'Open'
), 'owner'), 'test@example.com')
), 'allocated_to'), 'test@example.com')


def check_assignment_rule_scheduling(self): def check_assignment_rule_scheduling(self):
frappe.db.delete("Assignment Rule") frappe.db.delete("Assignment Rule")
@@ -202,7 +202,7 @@ class TestAutoAssign(unittest.TestCase):
reference_type = 'Note', reference_type = 'Note',
reference_name = note.name, reference_name = note.name,
status = 'Open' status = 'Open'
), 'owner'), ['test@example.com', 'test1@example.com', 'test2@example.com'])
), 'allocated_to'), ['test@example.com', 'test1@example.com', 'test2@example.com'])


frappe.flags.assignment_day = "Friday" frappe.flags.assignment_day = "Friday"
note = make_note(dict(public=1)) note = make_note(dict(public=1))
@@ -211,7 +211,7 @@ class TestAutoAssign(unittest.TestCase):
reference_type = 'Note', reference_type = 'Note',
reference_name = note.name, reference_name = note.name,
status = 'Open' status = 'Open'
), 'owner'), ['test3@example.com'])
), 'allocated_to'), ['test3@example.com'])


def test_assignment_rule_condition(self): def test_assignment_rule_condition(self):
frappe.db.delete("Assignment Rule") frappe.db.delete("Assignment Rule")


+ 551
- 541
frappe/core/doctype/docfield/docfield.json
文件差异内容过多而无法显示
查看文件


+ 1
- 0
frappe/core/doctype/file/test_file.py 查看文件

@@ -18,6 +18,7 @@ test_content2 = 'Hello World'
def make_test_doc(): def make_test_doc():
d = frappe.new_doc('ToDo') d = frappe.new_doc('ToDo')
d.description = 'Test' d.description = 'Test'
d.assigned_by = frappe.session.user
d.save() d.save()
return d.doctype, d.name return d.doctype, d.name




+ 10
- 2
frappe/core/doctype/module_def/module_def.json 查看文件

@@ -10,7 +10,8 @@
"custom", "custom",
"package", "package",
"app_name", "app_name",
"restrict_to_domain"
"restrict_to_domain",
"connections_tab"
], ],
"fields": [ "fields": [
{ {
@@ -50,6 +51,12 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Package", "label": "Package",
"options": "Package" "options": "Package"
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
} }
], ],
"icon": "fa fa-sitemap", "icon": "fa fa-sitemap",
@@ -116,7 +123,7 @@
"link_fieldname": "module" "link_fieldname": "module"
} }
], ],
"modified": "2021-09-05 21:58:40.253909",
"modified": "2022-01-03 13:56:52.817954",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "Module Def", "name": "Module Def",
@@ -154,5 +161,6 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

+ 2
- 3
frappe/core/doctype/translation/translation.json 查看文件

@@ -43,8 +43,7 @@
{ {
"fieldname": "context", "fieldname": "context",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Context",
"read_only": 1
"label": "Context"
}, },
{ {
"default": "0", "default": "0",
@@ -83,7 +82,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-03-12 13:28:48.223409",
"modified": "2021-12-31 10:19:52.541055",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "Translation", "name": "Translation",


+ 1
- 1
frappe/core/doctype/user/test_user.py 查看文件

@@ -70,7 +70,7 @@ class TestUser(unittest.TestCase):
delete_contact("_test@example.com") delete_contact("_test@example.com")
delete_doc("User", "_test@example.com") delete_doc("User", "_test@example.com")


self.assertTrue(not frappe.db.sql("""select * from `tabToDo` where owner=%s""",
self.assertTrue(not frappe.db.sql("""select * from `tabToDo` where allocated_to=%s""",
("_test@example.com",))) ("_test@example.com",)))


from frappe.core.doctype.role.test_role import test_records as role_records from frappe.core.doctype.role.test_role import test_records as role_records


+ 9
- 8
frappe/core/doctype/user/user.json 查看文件

@@ -10,15 +10,15 @@
"enabled", "enabled",
"section_break_3", "section_break_3",
"email", "email",
"first_name",
"middle_name",
"last_name", "last_name",
"language",
"column_break0", "column_break0",
"first_name",
"full_name", "full_name",
"time_zone",
"column_break_11",
"middle_name",
"username", "username",
"column_break_11",
"language",
"time_zone",
"send_welcome_email", "send_welcome_email",
"unsubscribed", "unsubscribed",
"user_image", "user_image",
@@ -660,7 +660,7 @@
{ {
"group": "Activity", "group": "Activity",
"link_doctype": "ToDo", "link_doctype": "ToDo",
"link_fieldname": "owner"
"link_fieldname": "allocated_to"
}, },
{ {
"group": "Integrations", "group": "Integrations",
@@ -669,7 +669,7 @@
} }
], ],
"max_attachments": 5, "max_attachments": 5,
"modified": "2021-11-17 17:17:16.098457",
"modified": "2022-01-03 11:53:25.250822",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "User", "name": "User",
@@ -702,6 +702,7 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "full_name", "title_field": "full_name",
"track_changes": 1 "track_changes": 1
}
}

+ 1
- 1
frappe/core/doctype/user/user.py 查看文件

@@ -363,7 +363,7 @@ class User(Document):
frappe.local.login_manager.logout(user=self.name) frappe.local.login_manager.logout(user=self.name)


# delete todos # delete todos
frappe.db.delete("ToDo", {"owner": self.name})
frappe.db.delete("ToDo", {"allocated_to": self.name})
todo_table = DocType("ToDo") todo_table = DocType("ToDo")
( (
frappe.qb.update(todo_table) frappe.qb.update(todo_table)


+ 1
- 1
frappe/core/doctype/user_permission/user_permission.js 查看文件

@@ -44,7 +44,7 @@ frappe.ui.form.on('User Permission', {


set_applicable_for_constraint: frm => { set_applicable_for_constraint: frm => {
frm.toggle_reqd('applicable_for', !frm.doc.apply_to_all_doctypes); frm.toggle_reqd('applicable_for', !frm.doc.apply_to_all_doctypes);
if (frm.doc.apply_to_all_doctypes) {
if (frm.doc.apply_to_all_doctypes && frm.doc.applicable_for) {
frm.set_value('applicable_for', null); frm.set_value('applicable_for', null);
} }
}, },


+ 7
- 6
frappe/core/doctype/user_permission/user_permission.json 查看文件

@@ -8,8 +8,8 @@
"field_order": [ "field_order": [
"user", "user",
"allow", "allow",
"column_break_3",
"for_value", "for_value",
"column_break_3",
"is_default", "is_default",
"advanced_control_section", "advanced_control_section",
"apply_to_all_doctypes", "apply_to_all_doctypes",
@@ -37,10 +37,6 @@
"options": "DocType", "options": "DocType",
"reqd": 1 "reqd": 1
}, },
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{ {
"fieldname": "for_value", "fieldname": "for_value",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
@@ -87,10 +83,14 @@
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1, "hidden": 1,
"label": "Hide Descendants" "label": "Hide Descendants"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
} }
], ],
"links": [], "links": [],
"modified": "2021-01-21 18:14:10.839381",
"modified": "2022-01-03 11:25:03.726150",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "User Permission", "name": "User Permission",
@@ -111,6 +111,7 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "user", "title_field": "user",
"track_changes": 1 "track_changes": 1
} }

+ 1
- 1
frappe/core/notifications.py 查看文件

@@ -23,7 +23,7 @@ def get_things_todo(as_list=False):
data = frappe.get_list("ToDo", data = frappe.get_list("ToDo",
fields=["name", "description"] if as_list else "count(*)", fields=["name", "description"] if as_list else "count(*)",
filters=[["ToDo", "status", "=", "Open"]], filters=[["ToDo", "status", "=", "Open"]],
or_filters=[["ToDo", "owner", "=", frappe.session.user],
or_filters=[["ToDo", "allocated_to", "=", frappe.session.user],
["ToDo", "assigned_by", "=", frappe.session.user]], ["ToDo", "assigned_by", "=", frappe.session.user]],
as_list=True) as_list=True)




+ 1
- 0
frappe/custom/doctype/customize_form/customize_form.py 查看文件

@@ -516,6 +516,7 @@ docfield_properties = {
'options': 'Text', 'options': 'Text',
'fetch_from': 'Small Text', 'fetch_from': 'Small Text',
'fetch_if_empty': 'Check', 'fetch_if_empty': 'Check',
'show_dashboard': 'Check',
'permlevel': 'Int', 'permlevel': 'Int',
'width': 'Data', 'width': 'Data',
'print_width': 'Data', 'print_width': 'Data',


+ 13
- 3
frappe/custom/doctype/customize_form_field/customize_form_field.json 查看文件

@@ -28,6 +28,7 @@
"options", "options",
"fetch_from", "fetch_from",
"fetch_if_empty", "fetch_if_empty",
"show_dashboard",
"permissions", "permissions",
"depends_on", "depends_on",
"permlevel", "permlevel",
@@ -82,7 +83,7 @@
"label": "Type", "label": "Type",
"oldfieldname": "fieldtype", "oldfieldname": "fieldtype",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nTab Break",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
@@ -422,18 +423,27 @@
"fieldname": "non_negative", "fieldname": "non_negative",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Non Negative" "label": "Non Negative"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Tab Break'",
"fieldname": "show_dashboard",
"fieldtype": "Check",
"label": "Show Dashboard"
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-07-11 21:57:24.479749",
"modified": "2022-01-03 14:50:32.035768",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Custom", "module": "Custom",
"name": "Customize Form Field", "name": "Customize Form Field",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
} }

+ 2
- 0
frappe/database/__init__.py 查看文件

@@ -4,6 +4,8 @@
# Database Module # Database Module
# -------------------- # --------------------


from frappe.database.database import savepoint

def setup_database(force, source_sql=None, verbose=None, no_mariadb_socket=False): def setup_database(force, source_sql=None, verbose=None, no_mariadb_socket=False):
import frappe import frappe
if frappe.conf.db_type == 'postgres': if frappe.conf.db_type == 'postgres':


+ 35
- 5
frappe/database/database.py 查看文件

@@ -4,16 +4,18 @@
# Database Module # Database Module
# -------------------- # --------------------


import datetime
import random
import re import re
import time
from typing import Dict, List, Union
import string
from contextlib import contextmanager
from time import time
from typing import Dict, List, Union, Tuple

import frappe import frappe
import datetime
import frappe.defaults import frappe.defaults
import frappe.model.meta import frappe.model.meta

from frappe import _ from frappe import _
from time import time
from frappe.utils import now, getdate, cast, get_datetime from frappe.utils import now, getdate, cast, get_datetime
from frappe.model.utils.link_count import flush_local_link_count from frappe.model.utils.link_count import flush_local_link_count
from frappe.query_builder.functions import Count from frappe.query_builder.functions import Count
@@ -811,6 +813,9 @@ class Database(object):
Avoid using savepoints when writing to filesystem.""" Avoid using savepoints when writing to filesystem."""
self.sql(f"savepoint {save_point}") self.sql(f"savepoint {save_point}")


def release_savepoint(self, save_point):
self.sql(f"release savepoint {save_point}")

def rollback(self, *, save_point=None): def rollback(self, *, save_point=None):
"""`ROLLBACK` current transaction. Optionally rollback to a known save_point.""" """`ROLLBACK` current transaction. Optionally rollback to a known save_point."""
if save_point: if save_point:
@@ -1097,3 +1102,28 @@ def enqueue_jobs_after_commit():
q.enqueue_call(execute_job, timeout=job.get("timeout"), q.enqueue_call(execute_job, timeout=job.get("timeout"),
kwargs=job.get("queue_args")) kwargs=job.get("queue_args"))
frappe.flags.enqueue_after_commit = [] frappe.flags.enqueue_after_commit = []

@contextmanager
def savepoint(catch: Union[type, Tuple[type, ...]] = Exception):
""" Wrapper for wrapping blocks of DB operations in a savepoint.

as contextmanager:

for doc in docs:
with savepoint(catch=DuplicateError):
doc.insert()

as decorator (wraps FULL function call):

@savepoint(catch=DuplicateError)
def process_doc(doc):
doc.insert()
"""
try:
savepoint = ''.join(random.sample(string.ascii_lowercase, 10))
frappe.db.savepoint(savepoint)
yield # control back to calling function
except catch:
frappe.db.rollback(save_point=savepoint)
else:
frappe.db.release_savepoint(savepoint)

+ 1
- 0
frappe/database/mariadb/framework_mariadb.sql 查看文件

@@ -25,6 +25,7 @@ CREATE TABLE `tabDocField` (
`oldfieldtype` varchar(255) DEFAULT NULL, `oldfieldtype` varchar(255) DEFAULT NULL,
`options` text, `options` text,
`search_index` int(1) NOT NULL DEFAULT 0, `search_index` int(1) NOT NULL DEFAULT 0,
`show_dashboard` int(1) NOT NULL DEFAULT 0,
`hidden` int(1) NOT NULL DEFAULT 0, `hidden` int(1) NOT NULL DEFAULT 0,
`set_only_once` int(1) NOT NULL DEFAULT 0, `set_only_once` int(1) NOT NULL DEFAULT 0,
`allow_in_quick_entry` int(1) NOT NULL DEFAULT 0, `allow_in_quick_entry` int(1) NOT NULL DEFAULT 0,


+ 1
- 0
frappe/database/postgres/framework_postgres.sql 查看文件

@@ -27,6 +27,7 @@ CREATE TABLE "tabDocField" (
"search_index" smallint NOT NULL DEFAULT 0, "search_index" smallint NOT NULL DEFAULT 0,
"hidden" smallint NOT NULL DEFAULT 0, "hidden" smallint NOT NULL DEFAULT 0,
"set_only_once" smallint NOT NULL DEFAULT 0, "set_only_once" smallint NOT NULL DEFAULT 0,
"show_dashboard" smallint NOT NULL DEFAULT 0,
"allow_in_quick_entry" smallint NOT NULL DEFAULT 0, "allow_in_quick_entry" smallint NOT NULL DEFAULT 0,
"print_hide" smallint NOT NULL DEFAULT 0, "print_hide" smallint NOT NULL DEFAULT 0,
"report_hide" smallint NOT NULL DEFAULT 0, "report_hide" smallint NOT NULL DEFAULT 0,


+ 1
- 1
frappe/desk/doctype/event/test_event.py 查看文件

@@ -93,7 +93,7 @@ class TestEvent(unittest.TestCase):


# Remove an assignment # Remove an assignment
todo = frappe.get_doc("ToDo", {"reference_type": ev.doctype, "reference_name": ev.name, todo = frappe.get_doc("ToDo", {"reference_type": ev.doctype, "reference_name": ev.name,
"owner": self.test_user})
"allocated_to": self.test_user})
todo.status = "Cancelled" todo.status = "Cancelled"
todo.save() todo.save()




+ 11
- 11
frappe/desk/doctype/todo/todo.json 查看文件

@@ -13,7 +13,7 @@
"column_break_2", "column_break_2",
"color", "color",
"date", "date",
"owner",
"allocated_to",
"description_section", "description_section",
"description", "description",
"section_break_6", "section_break_6",
@@ -69,15 +69,6 @@
"oldfieldname": "date", "oldfieldname": "date",
"oldfieldtype": "Date" "oldfieldtype": "Date"
}, },
{
"fieldname": "owner",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_global_search": 1,
"in_standard_filter": 1,
"label": "Allocated To",
"options": "User"
},
{ {
"fieldname": "description_section", "fieldname": "description_section",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@@ -153,12 +144,21 @@
"label": "Assignment Rule", "label": "Assignment Rule",
"options": "Assignment Rule", "options": "Assignment Rule",
"read_only": 1 "read_only": 1
},
{
"fieldname": "allocated_to",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_global_search": 1,
"in_standard_filter": 1,
"label": "Allocated To",
"options": "User"
} }
], ],
"icon": "fa fa-check", "icon": "fa fa-check",
"idx": 2, "idx": 2,
"links": [], "links": [],
"modified": "2020-01-14 17:04:36.971002",
"modified": "2021-09-16 11:36:34.586898",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Desk", "module": "Desk",
"name": "ToDo", "name": "ToDo",


+ 9
- 9
frappe/desk/doctype/todo/todo.py 查看文件

@@ -16,10 +16,10 @@ class ToDo(Document):
self._assignment = None self._assignment = None
if self.is_new(): if self.is_new():


if self.assigned_by == self.owner:
if self.assigned_by == self.allocated_to:
assignment_message = frappe._("{0} self assigned this task: {1}").format(get_fullname(self.assigned_by), self.description) assignment_message = frappe._("{0} self assigned this task: {1}").format(get_fullname(self.assigned_by), self.description)
else: else:
assignment_message = frappe._("{0} assigned {1}: {2}").format(get_fullname(self.assigned_by), get_fullname(self.owner), self.description)
assignment_message = frappe._("{0} assigned {1}: {2}").format(get_fullname(self.assigned_by), get_fullname(self.allocated_to), self.description)


self._assignment = { self._assignment = {
"text": assignment_message, "text": assignment_message,
@@ -29,12 +29,12 @@ class ToDo(Document):
else: else:
# NOTE the previous value is only available in validate method # NOTE the previous value is only available in validate method
if self.get_db_value("status") != self.status: if self.get_db_value("status") != self.status:
if self.owner == frappe.session.user:
if self.allocated_to == frappe.session.user:
removal_message = frappe._("{0} removed their assignment.").format( removal_message = frappe._("{0} removed their assignment.").format(
get_fullname(frappe.session.user)) get_fullname(frappe.session.user))
else: else:
removal_message = frappe._("Assignment of {0} removed by {1}").format( removal_message = frappe._("Assignment of {0} removed by {1}").format(
get_fullname(self.owner), get_fullname(frappe.session.user))
get_fullname(self.allocated_to), get_fullname(frappe.session.user))


self._assignment = { self._assignment = {
"text": removal_message, "text": removal_message,
@@ -75,7 +75,7 @@ class ToDo(Document):
"reference_name": self.reference_name, "reference_name": self.reference_name,
"status": ("!=", "Cancelled") "status": ("!=", "Cancelled")
}, },
fields=["owner"], as_list=True)]
fields=["allocated_to"], as_list=True)]


assignments.reverse() assignments.reverse()
frappe.db.set_value(self.reference_type, self.reference_name, frappe.db.set_value(self.reference_type, self.reference_name,
@@ -98,8 +98,8 @@ class ToDo(Document):
def get_owners(cls, filters=None): def get_owners(cls, filters=None):
"""Returns list of owners after applying filters on todo's. """Returns list of owners after applying filters on todo's.
""" """
rows = frappe.get_all(cls.DocType, filters=filters or {}, fields=['owner'])
return [parse_addr(row.owner)[1] for row in rows if row.owner]
rows = frappe.get_all(cls.DocType, filters=filters or {}, fields=['allocated_to'])
return [parse_addr(row.allocated_to)[1] for row in rows if row.allocated_to]


# NOTE: todo is viewable if a user is an owner, or set as assigned_to value, or has any role that is allowed to access ToDo doctype. # NOTE: todo is viewable if a user is an owner, or set as assigned_to value, or has any role that is allowed to access ToDo doctype.
def on_doctype_update(): def on_doctype_update():
@@ -115,7 +115,7 @@ def get_permission_query_conditions(user):
if any(check in todo_roles for check in frappe.get_roles(user)): if any(check in todo_roles for check in frappe.get_roles(user)):
return None return None
else: else:
return """(`tabToDo`.owner = {user} or `tabToDo`.assigned_by = {user})"""\
return """(`tabToDo`.allocated_to = {user} or `tabToDo`.assigned_by = {user})"""\
.format(user=frappe.db.escape(user)) .format(user=frappe.db.escape(user))


def has_permission(doc, ptype="read", user=None): def has_permission(doc, ptype="read", user=None):
@@ -127,7 +127,7 @@ def has_permission(doc, ptype="read", user=None):
if any(check in todo_roles for check in frappe.get_roles(user)): if any(check in todo_roles for check in frappe.get_roles(user)):
return True return True
else: else:
return doc.owner==user or doc.assigned_by==user
return doc.allocated_to==user or doc.assigned_by==user


@frappe.whitelist() @frappe.whitelist()
def new_todo(description): def new_todo(description):


+ 15
- 14
frappe/desk/form/assign_to.py 查看文件

@@ -19,7 +19,7 @@ def get(args=None):
if not args: if not args:
args = frappe.local.form_dict args = frappe.local.form_dict


return frappe.get_all('ToDo', fields=['owner', 'name'], filters=dict(
return frappe.get_all('ToDo', fields=['allocated_to', 'name'], filters=dict(
reference_type = args.get('doctype'), reference_type = args.get('doctype'),
reference_name = args.get('name'), reference_name = args.get('name'),
status = ('!=', 'Cancelled') status = ('!=', 'Cancelled')
@@ -48,7 +48,7 @@ def add(args=None):
"reference_type": args['doctype'], "reference_type": args['doctype'],
"reference_name": args['name'], "reference_name": args['name'],
"status": "Open", "status": "Open",
"owner": assign_to
"allocated_to": assign_to
} }


if frappe.get_all("ToDo", filters=filters): if frappe.get_all("ToDo", filters=filters):
@@ -61,7 +61,7 @@ def add(args=None):


d = frappe.get_doc({ d = frappe.get_doc({
"doctype": "ToDo", "doctype": "ToDo",
"owner": assign_to,
"allocated_to": assign_to,
"reference_type": args['doctype'], "reference_type": args['doctype'],
"reference_name": args['name'], "reference_name": args['name'],
"description": args.get('description'), "description": args.get('description'),
@@ -87,7 +87,7 @@ def add(args=None):
follow_document(args['doctype'], args['name'], assign_to) follow_document(args['doctype'], args['name'], assign_to)


# notify # notify
notify_assignment(d.assigned_by, d.owner, d.reference_type, d.reference_name, action='ASSIGN',
notify_assignment(d.assigned_by, d.allocated_to, d.reference_type, d.reference_name, action='ASSIGN',
description=args.get("description")) description=args.get("description"))


if shared_with_users: if shared_with_users:
@@ -112,13 +112,13 @@ def add_multiple(args=None):
add(args) add(args)


def close_all_assignments(doctype, name): def close_all_assignments(doctype, name):
assignments = frappe.db.get_all('ToDo', fields=['owner'], filters =
assignments = frappe.db.get_all('ToDo', fields=['allocated_to'], filters =
dict(reference_type = doctype, reference_name = name, status=('!=', 'Cancelled'))) dict(reference_type = doctype, reference_name = name, status=('!=', 'Cancelled')))
if not assignments: if not assignments:
return False return False


for assign_to in assignments: for assign_to in assignments:
set_status(doctype, name, assign_to.owner, status="Closed")
set_status(doctype, name, assign_to.allocated_to, status="Closed")


return True return True


@@ -130,13 +130,13 @@ def set_status(doctype, name, assign_to, status="Cancelled"):
"""remove from todo""" """remove from todo"""
try: try:
todo = frappe.db.get_value("ToDo", {"reference_type":doctype, todo = frappe.db.get_value("ToDo", {"reference_type":doctype,
"reference_name":name, "owner":assign_to, "status": ('!=', status)})
"reference_name":name, "allocated_to":assign_to, "status": ('!=', status)})
if todo: if todo:
todo = frappe.get_doc("ToDo", todo) todo = frappe.get_doc("ToDo", todo)
todo.status = status todo.status = status
todo.save(ignore_permissions=True) todo.save(ignore_permissions=True)


notify_assignment(todo.assigned_by, todo.owner, todo.reference_type, todo.reference_name)
notify_assignment(todo.assigned_by, todo.allocated_to, todo.reference_type, todo.reference_name)
except frappe.DoesNotExistError: except frappe.DoesNotExistError:
pass pass


@@ -150,25 +150,26 @@ def clear(doctype, name):
''' '''
Clears assignments, return False if not assigned. Clears assignments, return False if not assigned.
''' '''
assignments = frappe.db.get_all('ToDo', fields=['owner'], filters =
assignments = frappe.db.get_all('ToDo', fields=['allocated_to'], filters =
dict(reference_type = doctype, reference_name = name)) dict(reference_type = doctype, reference_name = name))
if not assignments: if not assignments:
return False return False


for assign_to in assignments: for assign_to in assignments:
set_status(doctype, name, assign_to.owner, "Cancelled")
set_status(doctype, name, assign_to.allocated_to, "Cancelled")


return True return True


def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE',
def notify_assignment(assigned_by, allocated_to, doc_type, doc_name, action='CLOSE',
description=None): description=None):
""" """
Notify assignee that there is a change in assignment Notify assignee that there is a change in assignment
""" """
if not (assigned_by and owner and doc_type and doc_name): return
if not (assigned_by and allocated_to and doc_type and doc_name):
return


# return if self assigned or user disabled # return if self assigned or user disabled
if assigned_by == owner or not frappe.db.get_value('User', owner, 'enabled'):
if assigned_by == allocated_to or not frappe.db.get_value('User', allocated_to, 'enabled'):
return return


# Search for email address in description -- i.e. assignee # Search for email address in description -- i.e. assignee
@@ -194,7 +195,7 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE',
'email_content': description_html 'email_content': description_html
} }


enqueue_create_notification(owner, notification_doc)
enqueue_create_notification(allocated_to, notification_doc)


def format_message_for_assign_to(users): def format_message_for_assign_to(users):
return "<br><br>" + "<br>".join(users) return "<br><br>" + "<br>".join(users)

+ 3
- 3
frappe/desk/listview.py 查看文件

@@ -29,16 +29,16 @@ def get_group_by_count(doctype, current_filters, field):
subquery = frappe.get_all(doctype, filters=current_filters, run=False) subquery = frappe.get_all(doctype, filters=current_filters, run=False)
if field == 'assigned_to': if field == 'assigned_to':
subquery_condition = ' and `tabToDo`.reference_name in ({subquery})'.format(subquery = subquery) subquery_condition = ' and `tabToDo`.reference_name in ({subquery})'.format(subquery = subquery)
return frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count
return frappe.db.sql("""select `tabToDo`.allocated_to as name, count(*) as count
from from
`tabToDo`, `tabUser` `tabToDo`, `tabUser`
where where
`tabToDo`.status!='Cancelled' and `tabToDo`.status!='Cancelled' and
`tabToDo`.owner = `tabUser`.name and
`tabToDo`.allocated_to = `tabUser`.name and
`tabUser`.user_type = 'System User' `tabUser`.user_type = 'System User'
{subquery_condition} {subquery_condition}
group by group by
`tabToDo`.owner
`tabToDo`.allocated_to
order by order by
count desc count desc
limit 50""".format(subquery_condition = subquery_condition), as_dict=True) limit 50""".format(subquery_condition = subquery_condition), as_dict=True)


+ 1
- 10
frappe/email/doctype/email_template/email_template.json 查看文件

@@ -12,7 +12,6 @@
"use_html", "use_html",
"response_html", "response_html",
"response", "response",
"owner",
"section_break_4", "section_break_4",
"email_reply_help" "email_reply_help"
], ],
@@ -32,14 +31,6 @@
"label": "Response", "label": "Response",
"mandatory_depends_on": "eval:!doc.use_html" "mandatory_depends_on": "eval:!doc.use_html"
}, },
{
"default": "user",
"fieldname": "owner",
"fieldtype": "Link",
"hidden": 1,
"label": "Owner",
"options": "User"
},
{ {
"fieldname": "section_break_4", "fieldname": "section_break_4",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@@ -66,7 +57,7 @@
], ],
"icon": "fa fa-comment", "icon": "fa fa-comment",
"links": [], "links": [],
"modified": "2020-11-30 14:12:50.321633",
"modified": "2022-01-04 14:12:50.321633",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Email", "module": "Email",
"name": "Email Template", "name": "Email Template",


+ 2
- 2
frappe/email/doctype/notification/notification.py 查看文件

@@ -435,8 +435,8 @@ def get_context(doc):
def get_assignees(doc): def get_assignees(doc):
assignees = [] assignees = []
assignees = frappe.get_all('ToDo', filters={'status': 'Open', 'reference_name': doc.name, assignees = frappe.get_all('ToDo', filters={'status': 'Open', 'reference_name': doc.name,
'reference_type': doc.doctype}, fields=['owner'])
'reference_type': doc.doctype}, fields=['allocated_to'])


recipients = [d.owner for d in assignees]
recipients = [d.allocated_to for d in assignees]


return recipients return recipients

+ 1
- 4
frappe/model/base_document.py 查看文件

@@ -101,13 +101,10 @@ class BaseDocument(object):
"balance": 42000 "balance": 42000
}) })
""" """
if "doctype" in d:
self.set("doctype", d.get("doctype"))

# first set default field values of base document # first set default field values of base document
for key in default_fields: for key in default_fields:
if key in d: if key in d:
self.set(key, d.get(key))
self.set(key, d[key])


for key, value in d.items(): for key, value in d.items():
self.set(key, value) self.set(key, value)


+ 10
- 4
frappe/model/document.py 查看文件

@@ -504,6 +504,7 @@ class Document(BaseDocument):
self._sanitize_content() self._sanitize_content()
self._save_passwords() self._save_passwords()
self.validate_workflow() self.validate_workflow()
self.validate_owner()


children = self.get_all_children() children = self.get_all_children()
for d in children: for d in children:
@@ -546,6 +547,11 @@ class Document(BaseDocument):
if not self._action == 'save': if not self._action == 'save':
set_workflow_state_on_action(self, workflow, self._action) set_workflow_state_on_action(self, workflow, self._action)


def validate_owner(self):
"""Validate if the owner of the Document has changed"""
if not self.is_new() and self.has_value_changed('owner'):
frappe.throw(_('Document owner cannot be changed'))

def validate_set_only_once(self): def validate_set_only_once(self):
"""Validate that fields are not changed if not in insert""" """Validate that fields are not changed if not in insert"""
set_only_once_fields = self.meta.get_set_only_once_fields() set_only_once_fields = self.meta.get_set_only_once_fields()
@@ -1348,15 +1354,15 @@ class Document(BaseDocument):
), frappe.exceptions.InvalidDates) ), frappe.exceptions.InvalidDates)


def get_assigned_users(self): def get_assigned_users(self):
assignments = frappe.get_all('ToDo',
fields=['owner'],
assigned_users = frappe.get_all('ToDo',
fields=['allocated_to'],
filters={ filters={
'reference_type': self.doctype, 'reference_type': self.doctype,
'reference_name': self.name, 'reference_name': self.name,
'status': ('!=', 'Cancelled'), 'status': ('!=', 'Cancelled'),
})
}, pluck='allocated_to')


users = set([assignment.owner for assignment in assignments])
users = set(assigned_users)
return users return users


def add_tag(self, tag): def add_tag(self, tag):


+ 1
- 1
frappe/model/rename_doc.py 查看文件

@@ -293,7 +293,7 @@ def update_link_field_values(link_fields, old, new, doctype):
if parent == new and doctype == "DocType": if parent == new and doctype == "DocType":
parent = old parent = old


frappe.db.set_value(parent, {docfield: old}, docfield, new)
frappe.db.set_value(parent, {docfield: old}, docfield, new, update_modified=False)


# update cached link_fields as per new # update cached link_fields as per new
if doctype=='DocType' and field['parent'] == old: if doctype=='DocType' and field['parent'] == old:


+ 1
- 0
frappe/patches.txt 查看文件

@@ -190,3 +190,4 @@ frappe.patches.v14_0.update_github_endpoints #08-11-2021
frappe.patches.v14_0.remove_db_aggregation frappe.patches.v14_0.remove_db_aggregation
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
frappe.patches.v14_0.update_color_names_in_kanban_board_column frappe.patches.v14_0.update_color_names_in_kanban_board_column
frappe.patches.v14_0.transform_todo_schema

+ 12
- 0
frappe/patches/v14_0/transform_todo_schema.py 查看文件

@@ -0,0 +1,12 @@
import frappe
from frappe.query_builder.utils import DocType


def execute():
# Email Template & Help Article have owner field that doesn't have any additional functionality
# Only ToDo has to be updated.

ToDo = DocType("ToDo")
frappe.reload_doctype("ToDo", force=True)

frappe.qb.update(ToDo).set(ToDo.allocated_to, ToDo.owner).run()

+ 1
- 0
frappe/public/js/frappe/form/controls/multiselect_list.js 查看文件

@@ -109,6 +109,7 @@ frappe.ui.form.ControlMultiSelectList = class ControlMultiSelectList extends fra
let value = decodeURIComponent($selectable_item.data().value); let value = decodeURIComponent($selectable_item.data().value);


if ($selectable_item.hasClass('selected')) { if ($selectable_item.hasClass('selected')) {
this.values = this.values.slice();
this.values.push(value); this.values.push(value);
} else { } else {
this.values = this.values.filter(val => val !== value); this.values = this.values.filter(val => val !== value);


+ 1
- 1
frappe/public/js/frappe/form/form.js 查看文件

@@ -215,7 +215,7 @@ frappe.ui.form.Form = class FrappeForm {


if (this.layout.tabs.length) { if (this.layout.tabs.length) {
this.layout.tabs.every(tab => { this.layout.tabs.every(tab => {
if (tab.df.options === 'Dashboard') {
if (tab.df.show_dashboard) {
tab.wrapper.prepend(dashboard_parent); tab.wrapper.prepend(dashboard_parent);
dashboard_added = true; dashboard_added = true;
return false; return false;


+ 5
- 0
frappe/public/js/frappe/list/list_view.js 查看文件

@@ -1500,6 +1500,11 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
read_only: 1, read_only: 1,
}, },
], ],
primary_action_label: __("Copy to clipboard"),
primary_action: () => {
frappe.utils.copy_to_clipboard(this.get_share_url());
d.hide();
},
}); });
d.show(); d.show();
} }


+ 15
- 10
frappe/public/js/frappe/views/reports/query_report.js 查看文件

@@ -105,15 +105,18 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.toggle_nothing_to_show(true); this.toggle_nothing_to_show(true);
return; return;
} }
let route_options = {};
route_options = Object.assign(route_options, frappe.route_options);


if (this.report_name !== frappe.get_route()[1]) { if (this.report_name !== frappe.get_route()[1]) {
// different report // different report
this.load_report();
this.load_report(route_options);
} }
else if (frappe.has_route_options()) { else if (frappe.has_route_options()) {
// filters passed through routes // filters passed through routes
// so refresh report again // so refresh report again
this.refresh_report();
this.refresh_report(route_options);
} else { } else {
// same report // same report
// don't do anything to preserve state // don't do anything to preserve state
@@ -121,7 +124,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
} }
} }


load_report() {
load_report(route_options) {
this.page.clear_inner_toolbar(); this.page.clear_inner_toolbar();
this.route = frappe.get_route(); this.route = frappe.get_route();
this.page_name = frappe.get_route_str(); this.page_name = frappe.get_route_str();
@@ -137,7 +140,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
() => this.get_report_settings(), () => this.get_report_settings(),
() => this.setup_progress_bar(), () => this.setup_progress_bar(),
() => this.setup_page_head(), () => this.setup_page_head(),
() => this.refresh_report(),
() => this.refresh_report(route_options),
() => this.add_chart_buttons_to_toolbar(true), () => this.add_chart_buttons_to_toolbar(true),
() => this.add_card_button_to_toolbar(true), () => this.add_card_button_to_toolbar(true),
]); ]);
@@ -343,13 +346,13 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}); });
} }


refresh_report() {
refresh_report(route_options) {
this.toggle_message(true); this.toggle_message(true);
this.toggle_report(false); this.toggle_report(false);


return frappe.run_serially([ return frappe.run_serially([
() => this.setup_filters(), () => this.setup_filters(),
() => this.set_route_filters(),
() => this.set_route_filters(route_options),
() => this.page.clear_custom_actions(), () => this.page.clear_custom_actions(),
() => this.report_settings.onload && this.report_settings.onload(this), () => this.report_settings.onload && this.report_settings.onload(this),
() => this.refresh() () => this.refresh()
@@ -525,15 +528,17 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}); });
} }


set_route_filters() {
if(frappe.route_options) {
const fields = Object.keys(frappe.route_options);
set_route_filters(route_options) {
if (!route_options) route_options = frappe.route_options;

if (route_options) {
const fields = Object.keys(route_options);


const filters_to_set = this.filters.filter(f => fields.includes(f.df.fieldname)); const filters_to_set = this.filters.filter(f => fields.includes(f.df.fieldname));


const promises = filters_to_set.map(f => { const promises = filters_to_set.map(f => {
return () => { return () => {
const value = frappe.route_options[f.df.fieldname];
const value = route_options[f.df.fieldname];
f.set_value(value); f.set_value(value);
}; };
}); });


+ 3
- 1
frappe/public/js/frappe/views/treeview.js 查看文件

@@ -409,7 +409,9 @@ frappe.views.TreeView = class TreeView {
}, },
]; ];


if (frappe.user.has_role('System Manager')) {
if (frappe.user.has_role('System Manager') &&
frappe.meta.has_field(me.doctype, "lft") &&
frappe.meta.has_field(me.doctype, "rgt")) {
this.menu_items.push( this.menu_items.push(
{ {
label: __('Rebuild Tree'), label: __('Rebuild Tree'),


+ 1
- 1
frappe/tests/test_assign.py 查看文件

@@ -13,7 +13,7 @@ class TestAssign(unittest.TestCase):


added = assign(todo, "test@example.com") added = assign(todo, "test@example.com")


self.assertTrue("test@example.com" in [d.owner for d in added])
self.assertTrue("test@example.com" in [d.allocated_to for d in added])


removed = frappe.desk.form.assign_to.remove(todo.doctype, todo.name, "test@example.com") removed = frappe.desk.form.assign_to.remove(todo.doctype, todo.name, "test@example.com")




+ 27
- 0
frappe/tests/test_db.py 查看文件

@@ -12,6 +12,7 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.utils import random_string from frappe.utils import random_string
from frappe.utils.testutils import clear_custom_fields from frappe.utils.testutils import clear_custom_fields
from frappe.query_builder import Field from frappe.query_builder import Field
from frappe.database import savepoint


from .test_query_builder import run_only_if, db_type_is from .test_query_builder import run_only_if, db_type_is
from frappe.query_builder.functions import Concat_ws from frappe.query_builder.functions import Concat_ws
@@ -267,6 +268,32 @@ class TestDB(unittest.TestCase):
for d in created_docs: for d in created_docs:
self.assertTrue(frappe.db.exists("ToDo", d)) self.assertTrue(frappe.db.exists("ToDo", d))


def test_savepoints_wrapper(self):
frappe.db.rollback()

class SpecificExc(Exception):
pass

created_docs = []
failed_docs = []

for _ in range(5):
with savepoint(catch=SpecificExc):
doc_kept = frappe.get_doc(doctype="ToDo", description="nope").save()
created_docs.append(doc_kept.name)

with savepoint(catch=SpecificExc):
doc_gone = frappe.get_doc(doctype="ToDo", description="nope").save()
failed_docs.append(doc_gone.name)
raise SpecificExc

frappe.db.commit()

for d in failed_docs:
self.assertFalse(frappe.db.exists("ToDo", d))
for d in created_docs:
self.assertTrue(frappe.db.exists("ToDo", d))



@run_only_if(db_type_is.MARIADB) @run_only_if(db_type_is.MARIADB)
class TestDDLCommandsMaria(unittest.TestCase): class TestDDLCommandsMaria(unittest.TestCase):


+ 19
- 0
frappe/tests/test_document.py 查看文件

@@ -252,3 +252,22 @@ class TestDocument(unittest.TestCase):
'currency': 100000 'currency': 100000
}) })
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')

def test_owner_changed(self):
frappe.delete_doc_if_exists("User", "hello@example.com")
frappe.set_user("Administrator")

d = frappe.get_doc({
"doctype": "User",
"email": "hello@example.com",
"first_name": "John"
})
d.insert()
self.assertEqual(frappe.db.get_value("User", d.owner), d.owner)

d.set("owner", "johndoe@gmail.com")
self.assertRaises(frappe.ValidationError, d.save)

d.reload()
d.save()
self.assertEqual(frappe.db.get_value("User", d.owner), d.owner)

+ 5
- 0
frappe/utils/nestedset.py 查看文件

@@ -144,6 +144,11 @@ def rebuild_tree(doctype, parent_field):
if frappe.request and frappe.local.form_dict.cmd == 'rebuild_tree': if frappe.request and frappe.local.form_dict.cmd == 'rebuild_tree':
frappe.only_for('System Manager') frappe.only_for('System Manager')


meta = frappe.get_meta(doctype)
if not meta.has_field("lft") or not meta.has_field("rgt"):
frappe.throw(_("Rebuilding of tree is not supported for {}").format(frappe.bold(doctype)),
title=_("Invalid Action"))

# get all roots # get all roots
right = 1 right = 1
table = DocType(doctype) table = DocType(doctype)


+ 28
- 12
frappe/website/doctype/blog_post/blog_post.py 查看文件

@@ -159,10 +159,10 @@ class BlogPost(WebsiteGenerator):
like_count = 0 like_count = 0


if frappe.db.count('Feedback'): if frappe.db.count('Feedback'):
like_count = frappe.db.count('Feedback',
like_count = frappe.db.count('Feedback',
filters = dict( filters = dict(
reference_doctype = self.doctype,
reference_name = self.name,
reference_doctype = self.doctype,
reference_name = self.name,
like = True like = True
) )
) )
@@ -183,7 +183,6 @@ def get_list_context(context=None):
get_list = get_blog_list, get_list = get_blog_list,
no_breadcrumbs = True, no_breadcrumbs = True,
hide_filters = True, hide_filters = True,
children = get_children(),
# show_search = True, # show_search = True,
title = _('Blog') title = _('Blog')
) )
@@ -208,17 +207,34 @@ def get_list_context(context=None):
else: else:
list_context.parents = [{"name": _("Home"), "route": "/"}] list_context.parents = [{"name": _("Home"), "route": "/"}]


list_context.update(frappe.get_doc("Blog Settings").as_dict(no_default_fields=True))
blog_settings = frappe.get_doc("Blog Settings").as_dict(no_default_fields=True)
list_context.update(blog_settings)

if blog_settings.browse_by_category:
list_context.blog_categories = get_blog_categories()


return list_context return list_context


def get_children():
return frappe.db.sql("""select route as name,
title from `tabBlog Category`
where published = 1
and exists (select name from `tabBlog Post`
where `tabBlog Post`.blog_category=`tabBlog Category`.name and published=1)
order by title asc""", as_dict=1)

def get_blog_categories():
from pypika import Order
from pypika.terms import ExistsCriterion

post, category = frappe.qb.DocType("Blog Post"), frappe.qb.DocType("Blog Category")
return (
frappe.qb.from_(category)
.select(category.name, category.route, category.title)
.where(
(category.published == 1)
& ExistsCriterion(
frappe.qb.from_(post)
.select("name")
.where((post.published == 1) & (post.blog_category == category.name))
)
)
.orderby(category.title, order=Order.asc)
.run(as_dict=1)
)


def clear_blog_cache(): def clear_blog_cache():
for blog in frappe.db.sql_list("""select route from for blog in frappe.db.sql_list("""select route from


+ 28
- 10
frappe/website/doctype/blog_post/templates/blog_post_list.html 查看文件

@@ -4,16 +4,34 @@


{% block page_content %} {% block page_content %}


{{ web_block("Hero",
values={
'title': blog_title or _("Blog"),
'subtitle': blog_introduction or '',
},
add_container=0,
add_top_padding=0,
add_bottom_padding=0,
css_class="py-5"
) }}
<div class="row py-8">
<div class="col-md-8">
<div class="hero">
<div class="hero-content">
<h1 class="hero-title">{{ blog_title or _('Blog') }}</h1>
<p class="hero-subtitle mb-0">{{ blog_introduction or '' }}</p>
</div>
</div>
</div>
<div class="col-md-4 align-self-end">
{%- if browse_by_category -%}
<label for="category-select" class="sr-only">{{ _("Browse by category") }}</label>
<select id="category-select" class="custom-select" onchange="window.location.pathname = this.value">
<option value="" {{ not frappe.form_dict.category and "selected" or "" }} disabled>
{{ _("Browse by category") }}
</option>
{%- if frappe.form_dict.category -%}
<option value="blog">{{ _("Show all blogs") }}</option>
{%- endif -%}
{%- for category in blog_categories -%}
<option value="{{ category.route }}" {{ frappe.form_dict.category == category.name and "selected" or "" }}>
{{ _(category.title) }}
</option>
{%- endfor -%}
</select>
{%- endif -%}
</div>
</div>


<div class="blog-list-content"> <div class="blog-list-content">
<div class="website-list" data-doctype="{{ doctype }}" data-txt="{{ txt or '[notxt]' | e }}"> <div class="website-list" data-doctype="{{ doctype }}" data-txt="{{ txt or '[notxt]' | e }}">


+ 36
- 0
frappe/website/doctype/blog_post/ui_test_blog_post.js 查看文件

@@ -0,0 +1,36 @@
context('Blog Post', () => {
before(() => {
cy.login();
cy.visit('/app');
});

it('Blog Category dropdown works as expected', () => {
cy.create_records([
{
doctype: 'Blog Category',
title: 'Category 1',
published: 1
},
{
doctype: 'Blogger',
short_name: 'John',
full_name: 'John Doe'
},
{
doctype: 'Blog Post',
title: 'Test Blog Post',
content: 'Test Blog Post Content',
blog_category: 'category-1',
blogger: 'John',
published: 1
}
]);
cy.set_value('Blog Settings', 'Blog Settings', {browse_by_category: 1});
cy.visit('/blog');
cy.findByLabelText('Browse by category').select('Category 1');
cy.location('pathname').should('eq', '/blog/category-1');
cy.set_value('Blog Settings', 'Blog Settings', {browse_by_category: 0});
cy.visit('/blog');
cy.findByLabelText('Browse by category').should('not.exist');
});
});

+ 10
- 2
frappe/website/doctype/blog_settings/blog_settings.json 查看文件

@@ -11,6 +11,7 @@
"enable_social_sharing", "enable_social_sharing",
"show_cta_in_blog", "show_cta_in_blog",
"allow_guest_to_comment", "allow_guest_to_comment",
"browse_by_category",
"cta_section", "cta_section",
"title", "title",
"subtitle", "subtitle",
@@ -110,14 +111,20 @@
"default": "1", "default": "1",
"fieldname": "allow_guest_to_comment", "fieldname": "allow_guest_to_comment",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow guest to comment"
"label": "Allow Guest to comment"
},
{
"default": "0",
"fieldname": "browse_by_category",
"fieldtype": "Check",
"label": "Browse by category"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-10-28 20:44:44.143193",
"modified": "2021-12-20 13:40:32.312459",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "Blog Settings", "name": "Blog Settings",
@@ -142,5 +149,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

+ 2
- 10
frappe/website/doctype/help_article/help_article.json 查看文件

@@ -15,8 +15,7 @@
"section_break_7", "section_break_7",
"content", "content",
"likes", "likes",
"route",
"owner"
"route"
], ],
"fields": [ "fields": [
{ {
@@ -79,13 +78,6 @@
"fieldtype": "Data", "fieldtype": "Data",
"in_global_search": 1, "in_global_search": 1,
"label": "Route" "label": "Route"
},
{
"default": "user",
"fieldname": "owner",
"fieldtype": "Link",
"label": "Owner",
"options": "User"
} }
], ],
"has_web_view": 1, "has_web_view": 1,
@@ -93,7 +85,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_published_field": "published", "is_published_field": "published",
"links": [], "links": [],
"modified": "2020-07-21 16:25:18.577325",
"modified": "2022-01-04 16:25:18.577325",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "Help Article", "name": "Help Article",


+ 35
- 17
frappe/website/doctype/web_page/web_page.json 查看文件

@@ -11,12 +11,10 @@
"section_title", "section_title",
"title", "title",
"route", "route",
"published",
"dynamic_route", "dynamic_route",
"cb1", "cb1",
"published",
"module", "module",
"start_date",
"end_date",
"sb1", "sb1",
"content_type", "content_type",
"slideshow", "slideshow",
@@ -25,6 +23,7 @@
"main_section_md", "main_section_md",
"main_section_html", "main_section_html",
"page_blocks", "page_blocks",
"scripting_tab",
"context_section", "context_section",
"context_script", "context_script",
"custom_javascript", "custom_javascript",
@@ -34,28 +33,32 @@
"text_align", "text_align",
"css", "css",
"full_width", "full_width",
"settings",
"show_title", "show_title",
"settings",
"publishing_dates_section",
"start_date",
"column_break_30",
"end_date",
"metatags_section",
"meta_title",
"meta_description",
"meta_image",
"set_meta_tags",
"section_break_17", "section_break_17",
"show_sidebar", "show_sidebar",
"idx",
"website_sidebar", "website_sidebar",
"column_break_20", "column_break_20",
"enable_comments", "enable_comments",
"idx",
"sb2", "sb2",
"header", "header",
"breadcrumbs",
"metatags_section",
"meta_title",
"meta_description",
"meta_image",
"set_meta_tags"
"breadcrumbs"
], ],
"fields": [ "fields": [
{ {
"fieldname": "section_title", "fieldname": "section_title",
"fieldtype": "Section Break",
"label": "Title"
"fieldtype": "Tab Break",
"label": "Content"
}, },
{ {
"fieldname": "title", "fieldname": "title",
@@ -161,7 +164,7 @@
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "insert_style", "collapsible_depends_on": "insert_style",
"fieldname": "custom_css", "fieldname": "custom_css",
"fieldtype": "Section Break",
"fieldtype": "Tab Break",
"label": "Style" "label": "Style"
}, },
{ {
@@ -185,7 +188,7 @@
}, },
{ {
"fieldname": "settings", "fieldname": "settings",
"fieldtype": "Section Break",
"fieldtype": "Tab Break",
"label": "Settings" "label": "Settings"
}, },
{ {
@@ -267,7 +270,6 @@
"label": "Full Width" "label": "Full Width"
}, },
{ {
"collapsible": 1,
"fieldname": "metatags_section", "fieldname": "metatags_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Meta Tags" "label": "Meta Tags"
@@ -313,6 +315,21 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Module (for export)", "label": "Module (for export)",
"options": "Module Def" "options": "Module Def"
},
{
"fieldname": "scripting_tab",
"fieldtype": "Tab Break",
"label": "Scripting",
"show_dashboard": 1
},
{
"fieldname": "publishing_dates_section",
"fieldtype": "Section Break",
"label": "Publishing Dates"
},
{
"fieldname": "column_break_30",
"fieldtype": "Column Break"
} }
], ],
"has_web_view": 1, "has_web_view": 1,
@@ -322,7 +339,7 @@
"is_published_field": "published", "is_published_field": "published",
"links": [], "links": [],
"max_attachments": 20, "max_attachments": 20,
"modified": "2021-09-04 12:11:56.070994",
"modified": "2022-01-03 13:01:48.182645",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "Web Page", "name": "Web Page",
@@ -342,6 +359,7 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

正在加载...
取消
保存