Bläddra i källkod

Merge branch 'develop' of https://github.com/frappe/frappe into link_title_refactor

version-14
Saqib Ansari 3 år sedan
förälder
incheckning
027fa61963
33 ändrade filer med 899 tillägg och 610 borttagningar
  1. +3
    -3
      cypress/integration/workspace.js
  2. +2
    -3
      frappe/__init__.py
  3. +0
    -21
      frappe/app.py
  4. +13
    -0
      frappe/core/doctype/comment/test_comment.py
  5. +20
    -11
      frappe/core/doctype/communication/communication.py
  6. +14
    -0
      frappe/core/doctype/communication/test_communication.py
  7. +17
    -15
      frappe/core/doctype/doctype/doctype.py
  8. +1
    -1
      frappe/core/doctype/file/test_file.py
  9. +56
    -0
      frappe/core/doctype/report/test_report.py
  10. +1
    -0
      frappe/core/doctype/user/test_user.py
  11. +49
    -53
      frappe/desk/doctype/form_tour/form_tour.js
  12. +16
    -51
      frappe/desk/doctype/form_tour/form_tour.py
  13. +12
    -35
      frappe/desk/doctype/form_tour_step/form_tour_step.json
  14. +13
    -13
      frappe/desk/doctype/workspace/workspace.json
  15. +5
    -16
      frappe/desk/page/setup_wizard/setup_wizard.py
  16. +59
    -15
      frappe/desk/reportview.py
  17. +3
    -3
      frappe/model/document.py
  18. +2
    -2
      frappe/public/js/frappe/form/dashboard.js
  19. +1
    -1
      frappe/public/js/frappe/form/form_tour.js
  20. +1
    -1
      frappe/public/js/frappe/ui/messages.js
  21. +5
    -4
      frappe/public/js/frappe/ui/notifications/notifications.js
  22. +53
    -8
      frappe/public/js/frappe/views/reports/report_view.js
  23. +7
    -1
      frappe/public/js/frappe/views/workspace/blocks/paragraph.js
  24. +2
    -2
      frappe/public/js/frappe/views/workspace/workspace.js
  25. +1
    -1
      frappe/public/js/frappe/widgets/base_widget.js
  26. +29
    -3
      frappe/public/js/frappe/widgets/widget_dialog.js
  27. +22
    -5
      frappe/public/scss/desk/desktop.scss
  28. +2
    -1
      frappe/templates/includes/comments/comments.py
  29. +52
    -5
      frappe/tests/test_naming.py
  30. +1
    -8
      frappe/tests/ui_test_helpers.py
  31. +1
    -1
      package.json
  32. +3
    -3
      requirements.txt
  33. +433
    -324
      yarn.lock

+ 3
- 3
cypress/integration/workspace.js Visa fil

@@ -23,7 +23,7 @@ context('Workspace 2.0', () => {
// check if sidebar item is added in pubic section
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');

cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
cy.wait(300);
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');

@@ -67,7 +67,7 @@ context('Workspace 2.0', () => {
cy.get('.ce-block:last .dropdown-item').contains('Expand').click();
cy.get(".ce-block:last").should('have.class', 'col-xs-12');

cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
});

it('Delete Private Page', () => {
@@ -80,7 +80,7 @@ context('Workspace 2.0', () => {
.find('.dropdown-item[title="Delete Workspace"]').click({force: true});
cy.wait(300);
cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click();
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
cy.get('.codex-editor__redactor .ce-block');
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist');
});


+ 2
- 3
frappe/__init__.py Visa fil

@@ -358,7 +358,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False,
response JSON and shown in a pop-up / modal.

:param msg: Message.
:param title: [optional] Message title.
:param title: [optional] Message title. Default: "Message".
:param raise_exception: [optional] Raise given exception and show message.
:param as_table: [optional] If `msg` is a list of lists, render as HTML table.
:param as_list: [optional] If `msg` is a list, render as un-ordered list.
@@ -395,8 +395,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False,
if flags.print_messages and out.message:
print(f"Message: {strip_html_tags(out.message)}")

if title:
out.title = title
out.title = title or _("Message", context="Default title of the message dialog")

if not indicator and raise_exception:
indicator = 'red'


+ 0
- 21
frappe/app.py Visa fil

@@ -294,7 +294,6 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
_sites_path = sites_path

from werkzeug.serving import run_simple
patch_werkzeug_reloader()

if profile or os.environ.get('USE_PROFILER'):
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls'))
@@ -325,23 +324,3 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
use_debugger=not in_test_env,
use_evalex=not in_test_env,
threaded=not no_threading)

def patch_werkzeug_reloader():
"""
This function monkey patches Werkzeug reloader to ignore reloading files in
the __pycache__ directory.

To be deprecated when upgrading to Werkzeug 2.
"""

from werkzeug._reloader import WatchdogReloaderLoop

trigger_reload = WatchdogReloaderLoop.trigger_reload

def custom_trigger_reload(self, filename):
if os.path.basename(os.path.dirname(filename)) == "__pycache__":
return

return trigger_reload(self, filename)

WatchdogReloaderLoop.trigger_reload = custom_trigger_reload

+ 13
- 0
frappe/core/doctype/comment/test_comment.py Visa fil

@@ -70,6 +70,19 @@ class TestComment(unittest.TestCase):
reference_name = test_blog.name
))), 0)

# test for filtering html and css injection elements
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})

frappe.form_dict.comment = '<script>alert(1)</script>Comment'
frappe.form_dict.comment_by = 'hacker'

add_comment()

self.assertEqual(frappe.get_all('Comment', fields = ['content'], filters = dict(
reference_doctype = test_blog.doctype,
reference_name = test_blog.name
))[0]['content'], 'Comment')

test_blog.delete()




+ 20
- 11
frappe/core/doctype/communication/communication.py Visa fil

@@ -2,6 +2,7 @@
# License: MIT. See LICENSE

from collections import Counter
from typing import List
import frappe
from frappe import _
from frappe.model.document import Document
@@ -367,15 +368,8 @@ def get_permission_query_conditions_for_communication(user):
return """`tabCommunication`.email_account in ({email_accounts})"""\
.format(email_accounts=','.join(email_accounts))

def get_contacts(email_strings, auto_create_contact=False):
email_addrs = []

for email_string in email_strings:
if email_string:
result = getaddresses([email_string])
for email in result:
email_addrs.append(email[1])

def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]:
email_addrs = get_emails(email_strings)
contacts = []
for email in email_addrs:
email = get_email_without_link(email)
@@ -404,6 +398,17 @@ def get_contacts(email_strings, auto_create_contact=False):

return contacts

def get_emails(email_strings: List[str]) -> List[str]:
email_addrs = []

for email_string in email_strings:
if email_string:
result = getaddresses([email_string])
for email in result:
email_addrs.append(email[1])

return email_addrs

def add_contact_links_to_communication(communication, contact_name):
contact_links = frappe.get_all("Dynamic Link", filters={
"parenttype": "Contact",
@@ -449,8 +454,12 @@ def get_email_without_link(email):
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}):
return email

email_id = email.split("@")[0].split("+")[0]
email_host = email.split("@")[1]
try:
_email = email.split("@")
email_id = _email[0].split("+")[0]
email_host = _email[1]
except IndexError:
return email

return "{0}@{1}".format(email_id, email_host)



+ 14
- 0
frappe/core/doctype/communication/test_communication.py Visa fil

@@ -5,6 +5,7 @@ from urllib.parse import quote

import frappe
from frappe.email.doctype.email_queue.email_queue import EmailQueue
from frappe.core.doctype.communication.communication import get_emails

test_records = frappe.get_test_records('Communication')

@@ -201,6 +202,19 @@ class TestCommunication(unittest.TestCase):

self.assertIn(("Note", note.name), doc_links)

def parse_emails(self):
emails = get_emails(
[
'comm_recipient+DocType+DocName@example.com',
'"First, LastName" <first.lastname@email.com>',
'test@user.com'
]
)

self.assertEqual(emails[0], "comm_recipient+DocType+DocName@example.com")
self.assertEqual(emails[1], "first.lastname@email.com")
self.assertEqual(emails[2], "test@user.com")

class TestCommunicationEmailMixin(unittest.TestCase):
def new_communication(self, recipients=None, cc=None, bcc=None):
recipients = ', '.join(recipients or [])


+ 17
- 15
frappe/core/doctype/doctype/doctype.py Visa fil

@@ -781,28 +781,30 @@ def validate_series(dt, autoname=None, name=None):

def validate_links_table_fieldnames(meta):
"""Validate fieldnames in Links table"""
if frappe.flags.in_patch: return
if frappe.flags.in_fixtures: return
if not meta.links: return
if not meta.links or frappe.flags.in_patch or frappe.flags.in_fixtures:
return

for index, link in enumerate(meta.links):
fieldnames = tuple(field.fieldname for field in meta.fields)
for index, link in enumerate(meta.links, 1):
link_meta = frappe.get_meta(link.link_doctype)
if not link_meta.get_field(link.link_fieldname):
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))

if link.is_child_table and not meta.get_field(link.table_fieldname):
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname"))
if not link.is_child_table:
continue

if link.is_child_table:
if not link.parent_doctype:
message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format(index+1)
frappe.throw(message, frappe.ValidationError, _("Parent Missing"))
if not link.parent_doctype:
message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format(index)
frappe.throw(message, frappe.ValidationError, _("Parent Missing"))

if not link.table_fieldname:
message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index+1)
frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))
if not link.table_fieldname:
message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index)
frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))

if link.table_fieldname not in fieldnames:
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname"))

def validate_fields_for_doctype(doctype):
meta = frappe.get_meta(doctype, cached=False)


+ 1
- 1
frappe/core/doctype/file/test_file.py Visa fil

@@ -406,7 +406,7 @@ class TestFile(unittest.TestCase):
test_file.reload()
test_file.file_url = frappe.utils.get_url('unknown.jpg')
test_file.make_thumbnail(suffix="xs")
self.assertEqual(json.loads(frappe.message_log[0]), {"message": f"File '{frappe.utils.get_url('unknown.jpg')}' not found"})
self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found")
self.assertEquals(test_file.thumbnail_url, None)

def test_file_unzip(self):


+ 56
- 0
frappe/core/doctype/report/test_report.py Visa fil

@@ -4,7 +4,9 @@
import frappe, json, os
import unittest
from frappe.desk.query_report import run, save_report
from frappe.desk.reportview import delete_report, save_report as _save_report
from frappe.custom.doctype.customize_form.customize_form import reset_customization
from frappe.core.doctype.user_permission.test_user_permission import create_user

test_records = frappe.get_test_records('Report')
test_dependencies = ['User']
@@ -30,6 +32,60 @@ class TestReport(unittest.TestCase):
self.assertEqual(columns[1].get('label'), 'Module')
self.assertTrue('User' in [d.get('name') for d in data])

def test_save_or_delete_report(self):
'''Test for validations when editing / deleting report of type Report Builder'''

try:
report = frappe.get_doc({
'doctype': 'Report',
'ref_doctype': 'User',
'report_name': 'Test Delete Report',
'report_type': 'Report Builder',
'is_standard': 'No',
}).insert()

# Check for PermissionError
create_user("test_report_owner@example.com", "Website Manager")
frappe.set_user("test_report_owner@example.com")
self.assertRaises(frappe.PermissionError, delete_report, report.name)

# Check for Report Type
frappe.set_user("Administrator")
report.db_set("report_type", "Custom Report")
self.assertRaisesRegex(
frappe.ValidationError,
"Only reports of type Report Builder can be deleted",
delete_report,
report.name
)

# Check if creating and deleting works with proper validations
frappe.set_user("test@example.com")
report_name = _save_report(
'Dummy Report',
'User',
json.dumps([{
'fieldname': 'email',
'fieldtype': 'Data',
'label': 'Email',
'insert_after_index': 0,
'link_field': 'name',
'doctype': 'User',
'options': 'Email',
'width': 100,
'id':'email',
'name': 'Email'
}])
)

doc = frappe.get_doc("Report", report_name)
delete_report(doc.name)

finally:
frappe.set_user("Administrator")
frappe.db.rollback()


def test_custom_report(self):
reset_customization('User')
custom_report_name = save_report(


+ 1
- 0
frappe/core/doctype/user/test_user.py Visa fil

@@ -359,6 +359,7 @@ class TestUser(unittest.TestCase):
json.loads(frappe.message_log[0]).get("message"),
"Password reset instructions have been sent to your email"
)

sendmail.assert_called_once()
self.assertEqual(sendmail.call_args[1]["recipients"], "test2@example.com")



+ 49
- 53
frappe/desk/doctype/form_tour/form_tour.js Visa fil

@@ -15,12 +15,12 @@ frappe.ui.form.on('Form Tour', {

frm.add_custom_button(__('Show Tour'), async () => {
const issingle = await check_if_single(frm.doc.reference_doctype);
const name = await get_first_document(frm.doc.reference_doctype);
let route_changed = null;
if (issingle) {
route_changed = frappe.set_route('Form', frm.doc.reference_doctype);
} else if (frm.doc.first_document) {
const name = await get_first_document(frm.doc.reference_doctype);
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, name);
} else {
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new');
@@ -53,73 +53,69 @@ frappe.ui.form.on('Form Tour', {
};
});

frm.set_query("field", "steps", function() {
return {
query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
filters: {
doctype: frm.doc.reference_doctype,
hidden: 0
}
};
});

frm.set_query("parent_field", "steps", function() {
return {
query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
filters: {
doctype: frm.doc.reference_doctype,
fieldtype: "Table",
hidden: 0,
}
};
});

frm.trigger('reference_doctype');
},

reference_doctype(frm) {
if (!frm.doc.reference_doctype) return;

frappe.db.get_list('DocField', {
filters: {
parent: frm.doc.reference_doctype,
parenttype: 'DocType',
fieldtype: 'Table'
},
fields: ['options']
}).then(res => {
if (Array.isArray(res)) {
frm.child_doctypes = res.map(r => r.options);
}
frm.set_fields_as_options(
"fieldname",
frm.doc.reference_doctype,
df => !df.hidden
).then(options => {
frm.fields_dict.steps.grid.update_docfield_property(
"fieldname",
"options",
[""].concat(options)
);
});

frm.set_fields_as_options(
'parent_fieldname',
frm.doc.reference_doctype,
(df) => df.fieldtype == "Table" && !df.hidden,
).then(options => {
frm.fields_dict.steps.grid.update_docfield_property(
"parent_fieldname",
"options",
[""].concat(options)
);
});

}
});

frappe.ui.form.on('Form Tour Step', {
parent_field(frm, cdt, cdn) {
form_render(frm, cdt, cdn) {
if (locals[cdt][cdn].is_table_field) {
frm.trigger('parent_fieldname', cdt, cdn);
}
},
parent_fieldname(frm, cdt, cdn) {
const child_row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, 'field', '');
const field_control = get_child_field("steps", cdn, "field");
field_control.get_query = function() {
return {
query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
filters: {
doctype: child_row.child_doctype,
hidden: 0
}
};
};

const parent_fieldname_df = frappe
.get_meta(frm.doc.reference_doctype)
.fields.find(df => df.fieldname == child_row.parent_fieldname);

frm.set_fields_as_options(
'fieldname',
parent_fieldname_df.options,
(df) => !df.hidden,
).then(options => {
frm.fields_dict.steps.grid.update_docfield_property(
"fieldname",
"options",
[""].concat(options)
);
if (child_row.fieldname) {
frappe.model.set_value(cdt, cdn, 'fieldname', child_row.fieldname);
}
});
}
});

function get_child_field(child_table, child_name, fieldname) {
// gets the field from grid row form
const grid = cur_frm.fields_dict[child_table].grid;
const grid_row = grid.grid_rows_by_docname[child_name];
return grid_row.grid_form.fields_dict[fieldname];
}

async function check_if_single(doctype) {
const { message } = await frappe.db.get_value('DocType', doctype, 'issingle');
return message.issingle || 0;


+ 16
- 51
frappe/desk/doctype/form_tour/form_tour.py Visa fil

@@ -5,58 +5,23 @@ import frappe
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files

class FormTour(Document):
def before_insert(self):
if not self.is_standard:
return

# while syncing, set proper docfield reference
for d in self.steps:
if not frappe.db.exists('DocField', d.field):
d.field = frappe.db.get_value('DocField', {
'fieldname': d.fieldname, 'parent': self.reference_doctype, 'fieldtype': d.fieldtype
}, "name")

if d.is_table_field and not frappe.db.exists('DocField', d.parent_field):
d.parent_field = frappe.db.get_value('DocField', {
'fieldname': d.parent_fieldname, 'parent': self.reference_doctype, 'fieldtype': 'Table'
}, "name")
class FormTour(Document):
def before_save(self):
meta = frappe.get_meta(self.reference_doctype)
for step in self.steps:
if step.is_table_field and step.parent_fieldname:
parent_field_df = meta.get_field(step.parent_fieldname)
step.child_doctype = parent_field_df.options

field_df = frappe.get_meta(step.child_doctype).get_field(step.fieldname)
step.label = field_df.label
step.fieldtype = field_df.fieldtype
else:
field_df = meta.get_field(step.fieldname)
step.label = field_df.label
step.fieldtype = field_df.fieldtype

def on_update(self):
if frappe.conf.developer_mode and self.is_standard:
export_to_files([['Form Tour', self.name]], self.module)

def before_export(self, doc):
for d in doc.steps:
d.field = ""
d.parent_field = ""

@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_docfield_list(doctype, txt, searchfield, start, page_len, filters):
or_filters = [
['fieldname', 'like', '%' + txt + '%'],
['label', 'like', '%' + txt + '%'],
['fieldtype', 'like', '%' + txt + '%']
]

parent_doctype = filters.get('doctype')
fieldtype = filters.get('fieldtype')
if not fieldtype:
excluded_fieldtypes = ['Column Break']
excluded_fieldtypes += filters.get('excluded_fieldtypes', [])
fieldtype_filter = ['not in', excluded_fieldtypes]
else:
fieldtype_filter = fieldtype

docfields = frappe.get_all(
doctype,
fields=["name as value", "label", "fieldtype"],
filters={'parent': parent_doctype, 'fieldtype': fieldtype_filter},
or_filters=or_filters,
limit_start=start,
limit_page_length=page_len,
order_by="idx",
as_list=1,
)
return docfields
export_to_files([["Form Tour", self.name]], self.module)

+ 12
- 35
frappe/desk/doctype/form_tour_step/form_tour_step.json Visa fil

@@ -6,19 +6,17 @@
"field_order": [
"is_table_field",
"section_break_2",
"parent_field",
"field",
"parent_fieldname",
"fieldname",
"title",
"description",
"column_break_2",
"position",
"label",
"fieldtype",
"has_next_condition",
"next_step_condition",
"section_break_13",
"fieldname",
"parent_fieldname",
"fieldtype",
"child_doctype"
],
"fields": [
@@ -38,23 +36,13 @@
"reqd": 1
},
{
"depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_field))",
"fieldname": "field",
"fieldtype": "Link",
"label": "Field",
"options": "DocField",
"reqd": 1
},
{
"fetch_from": "field.fieldname",
"depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_fieldname))",
"fieldname": "fieldname",
"fieldtype": "Data",
"hidden": 1,
"fieldtype": "Select",
"label": "Fieldname",
"read_only": 1
"reqd": 1
},
{
"fetch_from": "field.label",
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
@@ -88,10 +76,8 @@
},
{
"default": "0",
"fetch_from": "field.fieldtype",
"fieldname": "fieldtype",
"fieldtype": "Data",
"hidden": 1,
"label": "Fieldtype",
"read_only": 1
},
@@ -105,14 +91,6 @@
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"depends_on": "is_table_field",
"fieldname": "parent_field",
"fieldtype": "Link",
"label": "Parent Field",
"mandatory_depends_on": "is_table_field",
"options": "DocField"
},
{
"fieldname": "section_break_13",
"fieldtype": "Section Break",
@@ -120,7 +98,6 @@
"label": "Hidden Fields"
},
{
"fetch_from": "parent_field.options",
"fieldname": "child_doctype",
"fieldtype": "Data",
"hidden": 1,
@@ -128,18 +105,17 @@
"read_only": 1
},
{
"fetch_from": "parent_field.fieldname",
"depends_on": "is_table_field",
"fieldname": "parent_fieldname",
"fieldtype": "Data",
"hidden": 1,
"label": "Parent Fieldname",
"read_only": 1
"fieldtype": "Select",
"label": "Parent Field",
"mandatory_depends_on": "is_table_field"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-06 20:52:21.076972",
"modified": "2022-01-27 15:18:36.481801",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour Step",
@@ -147,5 +123,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

+ 13
- 13
frappe/desk/doctype/workspace/workspace.json Visa fil

@@ -20,13 +20,13 @@
"hide_custom",
"public",
"content",
"section_break_2",
"tab_break_2",
"charts",
"section_break_15",
"tab_break_15",
"shortcuts",
"section_break_18",
"tab_break_18",
"links",
"roles_section",
"roles_tab",
"roles"
],
"fields": [
@@ -40,8 +40,8 @@
{
"collapsible": 1,
"collapsible_depends_on": "charts",
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"fieldname": "tab_break_2",
"fieldtype": "Tab Break",
"label": "Dashboards"
},
{
@@ -78,15 +78,15 @@
{
"collapsible": 1,
"collapsible_depends_on": "shortcuts",
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"fieldname": "tab_break_15",
"fieldtype": "Tab Break",
"label": "Shortcuts"
},
{
"collapsible": 1,
"collapsible_depends_on": "links",
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"fieldname": "tab_break_18",
"fieldtype": "Tab Break",
"label": "Link Cards"
},
{
@@ -152,14 +152,14 @@
"options": "Has Role"
},
{
"fieldname": "roles_section",
"fieldtype": "Section Break",
"fieldname": "roles_tab",
"fieldtype": "Tab Break",
"label": "Roles"
}
],
"in_create": 1,
"links": [],
"modified": "2021-12-15 19:33:00.805265",
"modified": "2022-01-27 12:06:13.111743",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",


+ 5
- 16
frappe/desk/page/setup_wizard/setup_wizard.py Visa fil

@@ -6,7 +6,6 @@ from frappe.utils import strip, cint
from frappe.translate import (set_default_language, get_dict, send_translations)
from frappe.geo.country_info import get_country_info
from frappe.utils.password import update_password
from werkzeug.useragents import UserAgent
from . import install_fixtures

def get_setup_stages(args):
@@ -315,17 +314,10 @@ def prettify_args(args):
return pretty_args

def email_setup_wizard_exception(traceback, args):
if not frappe.local.conf.setup_wizard_exception_email:
if not frappe.conf.setup_wizard_exception_email:
return

pretty_args = prettify_args(args)

if frappe.local.request:
user_agent = UserAgent(frappe.local.request.headers.get('User-Agent', ''))

else:
user_agent = frappe._dict()

message = """

#### Traceback
@@ -349,18 +341,15 @@ def email_setup_wizard_exception(traceback, args):
#### Basic Information

- **Site:** {site}
- **User:** {user}
- **Browser:** {user_agent.platform} {user_agent.browser} version: {user_agent.version} language: {user_agent.language}
- **Browser Languages**: `{accept_languages}`""".format(
- **User:** {user}""".format(
site=frappe.local.site,
traceback=traceback,
args="\n".join(pretty_args),
user=frappe.session.user,
user_agent=user_agent,
headers=frappe.local.request.headers,
accept_languages=", ".join(frappe.local.request.accept_languages.values()))
headers=frappe.request.headers,
)

frappe.sendmail(recipients=frappe.local.conf.setup_wizard_exception_email,
frappe.sendmail(recipients=frappe.conf.setup_wizard_exception_email,
sender=frappe.session.user,
subject="Setup failed: {}".format(frappe.local.site),
message=message,


+ 59
- 15
frappe/desk/reportview.py Visa fil

@@ -262,22 +262,66 @@ def compress(data, args=None):
}

@frappe.whitelist()
def save_report():
"""save report"""

data = frappe.local.form_dict
if frappe.db.exists('Report', data['name']):
d = frappe.get_doc('Report', data['name'])
def save_report(name, doctype, report_settings):
"""Save reports of type Report Builder from Report View"""

if frappe.db.exists('Report', name):
report = frappe.get_doc('Report', name)
if report.is_standard == "Yes":
frappe.throw(_("Standard Reports cannot be edited"))

if report.report_type != "Report Builder":
frappe.throw(_("Only reports of type Report Builder can be edited"))

if (
report.owner != frappe.session.user
and not frappe.has_permission("Report", "write")
):
frappe.throw(
_("Insufficient Permissions for editing Report"),
frappe.PermissionError
)
else:
d = frappe.new_doc('Report')
d.report_name = data['name']
d.ref_doctype = data['doctype']

d.report_type = "Report Builder"
d.json = data['json']
frappe.get_doc(d).save()
frappe.msgprint(_("{0} is saved").format(d.name), alert=True)
return d.name
report = frappe.new_doc('Report')
report.report_name = name
report.ref_doctype = doctype

report.report_type = "Report Builder"
report.json = report_settings
report.save(ignore_permissions=True)
frappe.msgprint(
_("Report {0} saved").format(frappe.bold(report.name)),
indicator="green",
alert=True,
)
return report.name

@frappe.whitelist()
def delete_report(name):
"""Delete reports of type Report Builder from Report View"""

report = frappe.get_doc("Report", name)
if report.is_standard == "Yes":
frappe.throw(_("Standard Reports cannot be deleted"))

if report.report_type != "Report Builder":
frappe.throw(_("Only reports of type Report Builder can be deleted"))

if (
report.owner != frappe.session.user
and not frappe.has_permission("Report", "delete")
):
frappe.throw(
_("Insufficient Permissions for deleting Report"),
frappe.PermissionError
)

report.delete(ignore_permissions=True)
frappe.msgprint(
_("Report {0} deleted").format(frappe.bold(report.name)),
indicator="green",
alert=True,
)

@frappe.whitelist()
@frappe.read_only()


+ 3
- 3
frappe/model/document.py Visa fil

@@ -9,7 +9,7 @@ import frappe
from frappe import _, msgprint, is_whitelisted
from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff
from frappe.model.base_document import BaseDocument, get_controller
from frappe.model.naming import set_new_name
from frappe.model.naming import set_new_name, validate_name
from frappe.model.docstatus import DocStatus
from frappe.model import optional_fields, table_fields
from frappe.model.workflow import validate_workflow
@@ -416,12 +416,12 @@ class Document(BaseDocument):

# If autoname has set as Prompt (name)
if self.get("__newname"):
self.name = self.get("__newname")
self.name = validate_name(self.doctype, self.get("__newname"))
self.flags.name_set = True
return

if set_name:
self.name = set_name
self.name = validate_name(self.doctype, set_name)
else:
set_new_name(self)



+ 2
- 2
frappe/public/js/frappe/form/dashboard.js Visa fil

@@ -549,14 +549,14 @@ frappe.ui.form.Dashboard = class FormDashboard {
render_graph(args) {
this.chart_area.show();
this.chart_area.body.empty();
$.extend({
$.extend(args, {
type: 'line',
colors: ['green'],
truncateLegends: 1,
axisOptions: {
shortenYAxisNumbers: 1
}
}, args);
});
this.show();

this.chart = new frappe.Chart('.form-graph', args);


+ 1
- 1
frappe/public/js/frappe/form/form_tour.js Visa fil

@@ -151,7 +151,7 @@ frappe.ui.form.FormTour = class FormTour {

const curr_step = step_info;
const next_step = this.tour.steps[curr_step.idx];
const is_next_field_in_curr_table = next_step.parent_field == curr_step.field;
const is_next_field_in_curr_table = next_step.parent_fieldname == curr_step.fieldname;

if (!is_next_field_in_curr_table) return;



+ 1
- 1
frappe/public/js/frappe/ui/messages.js Visa fil

@@ -233,7 +233,7 @@ frappe.msgprint = function(msg, title, is_minimizable) {
if(data.title || !msg_exists) {
// set title only if it is explicitly given
// and no existing title exists
frappe.msg_dialog.set_title(data.title || __('Message'));
frappe.msg_dialog.set_title(data.title || __('Message', null, 'Default title of the message dialog'));
}

// show / hide indicator


+ 5
- 4
frappe/public/js/frappe/ui/notifications/notifications.js Visa fil

@@ -283,12 +283,13 @@ class NotificationsView extends BaseNotificationsView {
e.stopImmediatePropagation();
this.mark_as_read(field.name, item_html);
});

item_html.on('click', () => {
this.mark_as_read(field.name, item_html);
});
}

item_html.on('click', () => {
!field.read && this.mark_as_read(field.name, item_html);
this.notifications_icon.trigger('click');
});

return item_html;
}



+ 53
- 8
frappe/public/js/frappe/views/reports/report_view.js Visa fil

@@ -18,7 +18,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
setup_defaults() {
super.setup_defaults();
this.page_title = __('Report:') + ' ' + this.page_title;
this.menu_items = this.report_menu_items();
this.view = 'Report';

const route = frappe.get_route();
@@ -52,6 +51,11 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
this.page.main.addClass('report-view');
}

setup_page() {
this.menu_items = this.report_menu_items();
super.setup_page();
}

toggle_side_bar() {
super.toggle_side_bar();
// refresh datatable when sidebar is toggled to accomodate extra space
@@ -1207,7 +1211,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
args: {
name: name,
doctype: this.doctype,
json: JSON.stringify(report_settings)
report_settings: JSON.stringify(report_settings)
},
callback:(r) => {
if(r.exc) {
@@ -1244,6 +1248,17 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
}
}

delete_report() {
return frappe.call({
method: 'frappe.desk.reportview.delete_report',
args: { name: this.report_name },
callback(response) {
if (response.exc) return;
window.history.back();
}
});
}

get_column_widths() {
if (this.datatable) {
return this.datatable
@@ -1465,12 +1480,42 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
}
});

// save buttons
if(frappe.user.is_report_manager()) {
items = items.concat([
{ label: __('Save'), action: () => this.save_report('save') },
{ label: __('Save As'), action: () => this.save_report('save_as') }
]);
const can_edit_or_delete = (action) => {
const method = action == "delete" ? "can_delete" : "can_write";
return (
this.report_doc
&& this.report_doc.is_standard !== "Yes"
&& (
frappe.model[method]("Report")
|| this.report_doc.owner === frappe.session.user
)
);
};

// A user with role Report Manager or Report Owner can save
if (can_edit_or_delete()) {
items.push({
label: __("Save"),
action: () => this.save_report('save')
});
}

// anyone can save as
items.push({
label: __('Save As'),
action: () => this.save_report('save_as')
});

// A user with role Report Manager or Report Owner can delete
if (can_edit_or_delete("delete")) {
items.push({
label: __("Delete"),
action: () => frappe.confirm(
"Are you sure you want to delete this report?",
() => this.delete_report(),
),
shortcut: "Shift+Ctrl+D"
});
}

// user permissions


+ 7
- 1
frappe/public/js/frappe/views/workspace/blocks/paragraph.js Visa fil

@@ -62,7 +62,7 @@ export default class Paragraph extends Block {
this.show_hide_block_list();
});
div.addEventListener('blur', () => {
setTimeout(() => this.show_hide_block_list(true), 10);
!this.over_block_list_item && this.show_hide_block_list(true);
});
div.dataset.placeholder = this.api.i18n.t(this._placeholder);
div.addEventListener('keyup', this.onKeyUp);
@@ -95,6 +95,12 @@ export default class Paragraph extends Block {
this.api.caret.setToBlock(index);
});

$block_list_item.mouseenter(() => {
this.over_block_list_item = true;
}).mouseleave(() => {
this.over_block_list_item = false;
});

$block_list_container.append($block_list_item);
});



+ 2
- 2
frappe/public/js/frappe/views/workspace/workspace.js Visa fil

@@ -376,7 +376,7 @@ frappe.views.Workspace = class Workspace {
this.clear_page_actions();

page.is_editable && this.page.set_primary_action(
__("Save Customizations"),
__("Save"),
() => {
this.clear_page_actions();
this.save_page(page).then((saved) => {
@@ -1158,7 +1158,7 @@ frappe.views.Workspace = class Workspace {
item.data.card_name !== 'Custom Reports')
);

if (page.content == JSON.stringify(blocks)) {
if (page.content == JSON.stringify(blocks) && Object.keys(new_widgets).length === 0) {
this.setup_customization_buttons(page);
frappe.show_alert({ message: __("No changes made on the page"), indicator: "warning" });
return false;


+ 1
- 1
frappe/public/js/frappe/widgets/base_widget.js Visa fil

@@ -100,7 +100,7 @@ export default class Widget {
let title = max_chars ? frappe.ellipsis(base, max_chars) : base;

if (this.icon) {
let icon = frappe.utils.icon(this.icon);
let icon = frappe.utils.icon(this.icon, "lg");
this.title_field[0].innerHTML = `${icon} <span class="ellipsis" title="${title}">${title}</span>`;
} else {
this.title_field[0].innerHTML = `<span class="ellipsis" title="${title}">${title}</span>`;


+ 29
- 3
frappe/public/js/frappe/widgets/widget_dialog.js Visa fil

@@ -154,7 +154,7 @@ class CardDialog extends WidgetDialog {
{
fieldtype: "Data",
fieldname: "label",
label: "Label",
label: "Label"
},
{
fieldname: 'links',
@@ -174,7 +174,7 @@ class CardDialog extends WidgetDialog {
},
{
fieldname: "icon",
fieldtype: "Data",
fieldtype: "Icon",
label: "Icon"
},
{
@@ -182,6 +182,7 @@ class CardDialog extends WidgetDialog {
fieldtype: "Select",
in_list_view: 1,
label: "Link Type",
reqd: 1,
options: ["DocType", "Page", "Report"]
},
{
@@ -189,9 +190,9 @@ class CardDialog extends WidgetDialog {
fieldtype: "Dynamic Link",
in_list_view: 1,
label: "Link To",
reqd: 1,
get_options: (df) => {
return df.doc.link_type;

}
},
{
@@ -227,6 +228,31 @@ class CardDialog extends WidgetDialog {
}

process_data(data) {
data.links.map((item, idx) => {
let message = '';
let row = idx+1;

if (!item.link_type) {
message = "Following fields have missing values: <br><br><ul>";
message += `<li>Link Type in Row ${row}</li>`;
}

if (!item.link_to) {
message += `<li>Link To in Row ${row}</li>`;
}

if (message) {
message += "</ul>";
frappe.throw({
message: __(message),
title: __("Missing Values Required"),
indicator: 'orange'
});
}

item.label = item.label ? item.label : item.link_to;
});

data.label = data.label ? data.label : data.chart_name;
return data;
}


+ 22
- 5
frappe/public/scss/desk/desktop.scss Visa fil

@@ -155,6 +155,7 @@ body {
svg {
flex: none;
margin-right: 6px;
margin-left: -2px;
box-shadow: none;
}
}
@@ -560,21 +561,29 @@ body {
}

&.links-widget-box {
padding: 18px 12px;

.link-item {
display: flex;
text-decoration: none;
font-size: var(--text-md);
color: var(--text-color);
padding: var(--padding-xs);
margin-left: -5px;
padding: 4px;
margin-left: -4px;
margin-bottom: 4px;
border-radius: var(--border-radius-md);
cursor: pointer;

&:hover {
background-color: var(--bg-color);
background-color: var(--fg-hover-color);

.indicator-pill {
background-color: var(--fg-color);
}
}

&:first-child {
margin-top: 15px;
margin-top: 18px;
}

&:last-child {
@@ -601,6 +610,8 @@ body {

.indicator-pill {
margin-right: var(--margin-sm);
height: 20px;
padding: 3px 8px;
}
}
}
@@ -850,10 +861,16 @@ body {
}
}

.layout-main-section-wrapper {
margin-top: -5px;
padding-top: 5px;
}

.layout-main-section {
background-color: var(--fg-color);
padding: var(--padding-sm);
box-shadow: var(--card-shadow);
border-radius: var(--border-radius-lg);
padding: var(--padding-sm);
}

.block-menu-item-icon svg{


+ 2
- 1
frappe/templates/includes/comments/comments.py Visa fil

@@ -6,6 +6,7 @@ from frappe.website.utils import clear_cache
from frappe.rate_limiter import rate_limit
from frappe.utils import add_to_date, now
from frappe.website.doctype.blog_settings.blog_settings import get_comment_limit
from frappe.utils.html_utils import clean_html

from frappe import _

@@ -29,7 +30,7 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference
return False

comment = doc.add_comment(
text=comment,
text=clean_html(comment),
comment_email=comment_email,
comment_by=comment_by)



+ 52
- 5
frappe/tests/test_naming.py Visa fil

@@ -10,11 +10,11 @@ from frappe.model.naming import append_number_if_name_exists, revert_series_if_l
from frappe.model.naming import determine_consecutive_week_number, parse_naming_series

class TestNaming(unittest.TestCase):
def setUp(self):
frappe.db.delete('Note')

def tearDown(self):
# Reset ToDo autoname to hash
todo_doctype = frappe.get_doc('DocType', 'ToDo')
todo_doctype.autoname = 'hash'
todo_doctype.save()
frappe.db.rollback()

def test_append_number_if_name_exists(self):
'''
@@ -203,4 +203,51 @@ class TestNaming(unittest.TestCase):

dt = datetime.fromisoformat("2021-12-31")
w = determine_consecutive_week_number(dt)
self.assertEqual(w, "52")
self.assertEqual(w, "52")

def test_naming_validations(self):
# case 1: check same name as doctype
# set name via prompt
tag = frappe.get_doc({
'doctype': 'Tag',
'__newname': 'Tag'
})
self.assertRaises(frappe.NameError, tag.insert)

# set by passing set_name as ToDo
self.assertRaises(frappe.NameError, make_invalid_todo)

# set new name - Note
note = frappe.get_doc({
'doctype': 'Note',
'title': 'Note'
})
self.assertRaises(frappe.NameError, note.insert)

# case 2: set name with "New ---"
tag = frappe.get_doc({
'doctype': 'Tag',
'__newname': 'New Tag'
})
self.assertRaises(frappe.NameError, tag.insert)

# case 3: set name with special characters
tag = frappe.get_doc({
'doctype': 'Tag',
'__newname': 'Tag<>'
})
self.assertRaises(frappe.NameError, tag.insert)

# case 4: no name specified
tag = frappe.get_doc({
'doctype': 'Tag',
'__newname': ''
})
self.assertRaises(frappe.ValidationError, tag.insert)


def make_invalid_todo():
frappe.get_doc({
'doctype': 'ToDo',
'description': 'Test'
}).insert(set_name='ToDo')

+ 1
- 8
frappe/tests/ui_test_helpers.py Visa fil

@@ -148,9 +148,6 @@ def create_form_tour():
if frappe.db.exists('Form Tour', {'name': 'Test Form Tour'}):
return

def get_docfield_name(filters):
return frappe.db.get_value('DocField', filters, "name")

tour = frappe.get_doc({
'doctype': 'Form Tour',
'title': 'Test Form Tour',
@@ -161,7 +158,6 @@ def create_form_tour():
"description": "Test Description 1",
"has_next_condition": 1,
"next_step_condition": "eval: doc.first_name",
"field": get_docfield_name({'parent': 'Contact', 'fieldname': 'first_name'}),
"fieldname": "first_name",
"fieldtype": "Data"
},{
@@ -169,21 +165,18 @@ def create_form_tour():
"description": "Test Description 2",
"has_next_condition": 1,
"next_step_condition": "eval: doc.last_name",
"field": get_docfield_name({'parent': 'Contact', 'fieldname': 'last_name'}),
"fieldname": "last_name",
"fieldtype": "Data"
},{
"title": "Test Title 3",
"description": "Test Description 3",
"field": get_docfield_name({'parent': 'Contact', 'fieldname': 'phone_nos'}),
"fieldname": "phone_nos",
"fieldtype": "Table"
},{
"title": "Test Title 4",
"description": "Test Description 4",
"is_table_field": 1,
"parent_field": get_docfield_name({'parent': 'Contact', 'fieldname': 'phone_nos'}),
"field": get_docfield_name({'parent': 'Contact Phone', 'fieldname': 'phone'}),
"parent_fieldname": "phone_nos",
"next_step_condition": "eval: doc.phone",
"has_next_condition": 1,
"fieldname": "phone",


+ 1
- 1
package.json Visa fil

@@ -47,7 +47,7 @@
"localforage": "^1.9.0",
"moment": "^2.20.1",
"moment-timezone": "^0.5.28",
"node-sass": "^4.14.1",
"node-sass": "^7.0.0",
"plyr": "^3.6.2",
"popper.js": "^1.16.0",
"quagga": "^0.12.1",


+ 3
- 3
requirements.txt Visa fil

@@ -21,7 +21,7 @@ googlemaps~=4.4.5
gunicorn~=20.1.0
html2text==2020.1.16
html5lib~=1.1
ipython~=7.27.0
ipython~=7.31.1
Jinja2~=3.0.1
ldap3~=2.9
markdown2~=2.4.0
@@ -32,7 +32,7 @@ openpyxl~=3.0.7
passlib~=1.7.4
paytmchecksum~=1.7.0
pdfkit~=0.6.1
Pillow~=8.2.0
Pillow~=9.0.0
premailer~=3.8.0
psutil~=5.8.0
psycopg2-binary~=2.9.1
@@ -63,7 +63,7 @@ sqlparse~=0.4.1
stripe~=2.56.0
terminaltables~=3.1.0
urllib3~=1.26.4
Werkzeug~=0.16.1
Werkzeug~=2.0.3
Whoosh~=2.7.4
wrapt~=1.12.1
xlrd~=2.0.1


+ 433
- 324
yarn.lock
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


Laddar…
Avbryt
Spara