Merge branch 'develop' into image-processing
This commit is contained in:
commit
4f86cdb8f8
20 changed files with 402 additions and 284 deletions
57
cypress/integration/sidebar.js
Normal file
57
cypress/integration/sidebar.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
context('Sidebar', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/doctype');
|
||||
});
|
||||
|
||||
it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => {
|
||||
cy.click_sidebar_button(0);
|
||||
|
||||
//To check if no filter is available in "Assigned To" dropdown
|
||||
cy.get('.empty-state').should('contain', 'No filters found');
|
||||
|
||||
cy.click_sidebar_button(1);
|
||||
|
||||
//To check if "Created By" dropdown contains filter
|
||||
cy.get('.group-by-item > .dropdown-item').should('contain', 'Me');
|
||||
|
||||
//Assigning a doctype to a user
|
||||
cy.click_listview_row_item(0);
|
||||
cy.get('.form-assignments > .flex > .text-muted').click();
|
||||
cy.get_field('assign_to_me', 'Check').click();
|
||||
cy.get('.modal-footer > .standard-actions > .btn-primary').click();
|
||||
cy.visit('/app/doctype');
|
||||
cy.click_sidebar_button(0);
|
||||
|
||||
//To check if filter is added in "Assigned To" dropdown after assignment
|
||||
cy.get('.group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item').should('contain', '1');
|
||||
|
||||
//To check if there is no filter added to the listview
|
||||
cy.get('.filter-selector > .btn').should('contain', 'Filter');
|
||||
|
||||
//To add a filter to display data into the listview
|
||||
cy.get('.group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item').click();
|
||||
|
||||
//To check if filter is applied
|
||||
cy.click_filter_button().should('contain', '1 filter');
|
||||
cy.get('.fieldname-select-area > .awesomplete > .form-control').should('have.value', 'Assigned To');
|
||||
cy.get('.condition').should('have.value', 'like');
|
||||
cy.get('.filter-field > .form-group > .input-with-feedback').should('have.value', '%Administrator%');
|
||||
|
||||
//To remove the applied filter
|
||||
cy.get('.filter-action-buttons > div > .btn-secondary').contains('Clear Filters').click();
|
||||
cy.click_filter_button();
|
||||
cy.get('.filter-selector > .btn').should('contain', 'Filter');
|
||||
|
||||
//To remove the assignment
|
||||
cy.visit('/app/doctype');
|
||||
cy.click_listview_row_item(0);
|
||||
cy.get('.assignments > .avatar-group > .avatar > .avatar-frame').click();
|
||||
cy.get('.remove-btn').click({force: true});
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-header > .modal-actions > .btn-modal-close').click();
|
||||
cy.visit('/app/doctype');
|
||||
cy.click_sidebar_button(0);
|
||||
cy.get('.empty-state').should('contain', 'No filters found');
|
||||
});
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@ context('Table MultiSelect', () => {
|
|||
cy.new_form('Assignment Rule');
|
||||
cy.fill_field('__newname', name);
|
||||
cy.fill_field('document_type', 'Blog Post');
|
||||
cy.get('.section-head').contains('Assignment Rules').scrollIntoView();
|
||||
cy.fill_field('assign_condition', 'status=="Open"', 'Code');
|
||||
cy.get('input[data-fieldname="users"]').focus().as('input');
|
||||
cy.get('input[data-fieldname="users"] + ul').should('be.visible');
|
||||
|
|
|
|||
53
cypress/integration/timeline.js
Normal file
53
cypress/integration/timeline.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
context('Timeline', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/todo');
|
||||
});
|
||||
|
||||
it('Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo', () => {
|
||||
//Adding new ToDo
|
||||
cy.click_listview_primary_button('Add ToDo');
|
||||
cy.get('.modal-footer > .custom-actions > .btn').contains('Edit in full page').click();
|
||||
cy.get('.row > .section-body > .form-column > form > .frappe-control > .form-group > .control-input-wrapper > .control-input > .ql-container > .ql-editor').eq(0).type('Test ToDo', {force: true});
|
||||
cy.wait(200);
|
||||
cy.get('#page-ToDo > .page-head > .container > .row > .col > .standard-actions > .primary-action').contains('Save').click();
|
||||
cy.wait(700);
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();
|
||||
|
||||
//To check if the comment box is initially empty and tying some text into it
|
||||
cy.get('.comment-input-container > .frappe-control > .ql-container > .ql-editor').should('contain', '').type('Testing Timeline');
|
||||
|
||||
//Adding new comment
|
||||
cy.get('.comment-input-wrapper > .btn').contains('Comment').click();
|
||||
|
||||
//To check if the commented text is visible in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Testing Timeline');
|
||||
|
||||
//Editing comment
|
||||
cy.click_timeline_action_btn(0);
|
||||
cy.get('.timeline-content > .timeline-message-box > .comment-edit-box > .frappe-control > .ql-container > .ql-editor').first().type(' 123');
|
||||
cy.click_timeline_action_btn(0);
|
||||
|
||||
//To check if the edited comment text is visible in timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Testing Timeline 123');
|
||||
|
||||
//Discarding comment
|
||||
cy.click_timeline_action_btn(0);
|
||||
cy.get('.actions > .btn').eq(1).first().click();
|
||||
|
||||
//To check if after discarding the timeline content is same as previous
|
||||
cy.get('.timeline-content').should('contain', 'Testing Timeline 123');
|
||||
|
||||
//Deleting the added comment
|
||||
cy.get('.actions > .btn > .icon').first().click();
|
||||
cy.get('.modal-footer > .standard-actions > .btn-primary').contains('Yes').click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
|
||||
//Deleting the added ToDo
|
||||
cy.get('#page-ToDo > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click({force: true});
|
||||
cy.get('.menu-btn-group > .dropdown-menu > li > .grey-link').eq(17).click({force: true});
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').contains('Yes').click({force: true});
|
||||
});
|
||||
});
|
||||
71
cypress/integration/timeline_email.js
Normal file
71
cypress/integration/timeline_email.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
context('Timeline Email', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/todo');
|
||||
});
|
||||
|
||||
it('Adding new ToDo, adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => {
|
||||
//Adding new ToDo
|
||||
cy.click_listview_primary_button('Add ToDo');
|
||||
cy.get('.custom-actions > .btn').trigger('click', {delay: 500});
|
||||
cy.get('.row > .section-body > .form-column > form > .frappe-control > .form-group > .control-input-wrapper > .control-input > .ql-container > .ql-editor').eq(0).type('Test ToDo', {force: true});
|
||||
cy.wait(500);
|
||||
//cy.click_listview_primary_button('Save');
|
||||
cy.get('.primary-action').contains('Save').click({force: true});
|
||||
cy.wait(700);
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();
|
||||
|
||||
//Creating a new email
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-body > :nth-child(1) > .form-layout > .form-page > :nth-child(3) > .section-body > .form-column > form > [data-fieldtype="Text Editor"] > .form-group > .control-input-wrapper > .control-input > .ql-container > .ql-editor').type('Test Mail');
|
||||
|
||||
//Adding attachment to the email
|
||||
cy.get('.add-more-attachments > .btn').click();
|
||||
cy.get('.mt-2 > .btn > .mt-1').eq(2).click();
|
||||
cy.get('.input-group > .form-control').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
|
||||
cy.get('.btn-primary').contains('Upload').click();
|
||||
|
||||
//Sending the email
|
||||
cy.click_modal_primary_button('Send', {delay: 500});
|
||||
|
||||
//To check if the sent mail content is shown in the timeline content
|
||||
cy.get('[data-doctype="Communication"] > .timeline-content').should('contain', 'Test Mail');
|
||||
|
||||
//To check if the attachment of email is shown in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Added 72402.jpg');
|
||||
|
||||
//Deleting the sent email
|
||||
cy.get('[title="Open Communication"] > .icon').first().click({force: true});
|
||||
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click();
|
||||
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .dropdown-menu > li > .grey-link').eq(9).click();
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click();
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();
|
||||
|
||||
//Removing the added attachment
|
||||
cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click();
|
||||
cy.get('.modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').contains('Yes').click();
|
||||
|
||||
//To check if the removed attachment is shown in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Removed 72402.jpg');
|
||||
cy.wait(500);
|
||||
|
||||
//To check if the discard button functionality in email is working correctly
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
|
||||
cy.get('.modal-footer > .standard-actions > .btn-secondary').contains('Discard').click();
|
||||
cy.wait(500);
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.wait(500);
|
||||
cy.get_field('recipients', 'MultiSelect').should('have.text', '');
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-header > .modal-actions > .btn-modal-close > .icon').click();
|
||||
|
||||
//Deleting the added ToDo
|
||||
cy.get('#page-ToDo > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click();
|
||||
cy.get('.menu-btn-group > .dropdown-menu > li > .grey-link').eq(17).click();
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click();
|
||||
});
|
||||
});
|
||||
|
|
@ -192,16 +192,16 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
|
||||
let selector = `.form-control[data-fieldname="${fieldname}"]`;
|
||||
let selector = `[data-fieldname="${fieldname}"] input:visible`;
|
||||
|
||||
if (fieldtype === 'Text Editor') {
|
||||
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
|
||||
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]:visible`;
|
||||
}
|
||||
if (fieldtype === 'Code') {
|
||||
selector = `[data-fieldname="${fieldname}"] .ace_text-input`;
|
||||
}
|
||||
|
||||
return cy.get(selector);
|
||||
return cy.get(selector).first();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
|
||||
|
|
@ -323,4 +323,30 @@ Cypress.Commands.add('clear_filters', () => {
|
|||
cy.window().its('cur_list').then(cur_list => {
|
||||
cur_list && cur_list.filter_area && cur_list.filter_area.clear();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_modal_primary_button', (btn_name) => {
|
||||
cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).trigger('click', {force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_sidebar_button', (btn_no) => {
|
||||
cy.get('.list-group-by-fields > .group-by-field > .btn').eq(btn_no).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_listview_row_item', (row_no) => {
|
||||
cy.get('.list-row > .level-left > .list-subject > .bold > .ellipsis').eq(row_no).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_filter_button', () => {
|
||||
cy.get('.filter-selector > .btn').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_listview_primary_button', (btn_name) => {
|
||||
cy.get('.primary-action').contains(btn_name).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_timeline_action_btn', (btn_no) => {
|
||||
cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').eq(btn_no).first().click();
|
||||
});
|
||||
|
|
@ -72,6 +72,7 @@
|
|||
"fieldtype": "Code",
|
||||
"in_list_view": 1,
|
||||
"label": "Assign Condition",
|
||||
"options": "PythonExpression",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -82,7 +83,8 @@
|
|||
"description": "Simple Python Expression, Example: Status in (\"Closed\", \"Cancelled\")",
|
||||
"fieldname": "unassign_condition",
|
||||
"fieldtype": "Code",
|
||||
"label": "Unassign Condition"
|
||||
"label": "Unassign Condition",
|
||||
"options": "PythonExpression"
|
||||
},
|
||||
{
|
||||
"fieldname": "assign_to_users_section",
|
||||
|
|
@ -120,7 +122,8 @@
|
|||
"description": "Simple Python Expression, Example: Status in (\"Invalid\")",
|
||||
"fieldname": "close_condition",
|
||||
"fieldtype": "Code",
|
||||
"label": "Close Condition"
|
||||
"label": "Close Condition",
|
||||
"options": "PythonExpression"
|
||||
},
|
||||
{
|
||||
"fieldname": "sb",
|
||||
|
|
@ -151,7 +154,7 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-20 14:47:20.662954",
|
||||
"modified": "2021-07-16 22:51:35.505575",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Assignment Rule",
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ def install_app(context, apps):
|
|||
print("App {} is Incompatible with Site {}{}".format(app, site, err_msg))
|
||||
exit_code = 1
|
||||
except Exception as err:
|
||||
err_msg = ":\n{}".format(err if str(err) else frappe.get_traceback())
|
||||
err_msg = ": {}\n{}".format(str(err), frappe.get_traceback())
|
||||
print("An error occurred while installing {}{}".format(app, err_msg))
|
||||
exit_code = 1
|
||||
|
||||
|
|
|
|||
|
|
@ -722,6 +722,19 @@ def validate_links_table_fieldnames(meta):
|
|||
message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, 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 = _("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 link.is_child_table:
|
||||
if not link.parent_doctype:
|
||||
message = _("Row #{0}: Parent DocType is mandatory for internal links").format(index+1)
|
||||
frappe.throw(message, frappe.ValidationError, _("Parent Missing"))
|
||||
|
||||
if not link.table_fieldname:
|
||||
message = _("Row #{0}: Table Fieldname is mandatory for internal links").format(index+1)
|
||||
frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))
|
||||
|
||||
def validate_fields_for_doctype(doctype):
|
||||
meta = frappe.get_meta(doctype, cached=False)
|
||||
validate_links_table_fieldnames(meta)
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@
|
|||
"field_order": [
|
||||
"link_doctype",
|
||||
"link_fieldname",
|
||||
"parent_doctype",
|
||||
"table_fieldname",
|
||||
"group",
|
||||
"hidden",
|
||||
"is_child_table",
|
||||
"custom"
|
||||
],
|
||||
"fields": [
|
||||
|
|
@ -45,12 +48,33 @@
|
|||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Custom"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_child_table",
|
||||
"fieldname": "parent_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "Parent DocType",
|
||||
"mandatory_depends_on": "is_child_table",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "link_doctype.istable",
|
||||
"fieldname": "is_child_table",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Child Table",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "table_fieldname",
|
||||
"fieldtype": "Data",
|
||||
"label": "Table Fieldname"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-24 14:19:25.189511",
|
||||
"modified": "2021-07-31 15:23:12.237491",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType Link",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ from frappe import _
|
|||
class ServerScript(Document):
|
||||
def validate(self):
|
||||
frappe.only_for("Script Manager", True)
|
||||
self.validate_script()
|
||||
self.sync_scheduled_jobs()
|
||||
self.clear_scheduled_events()
|
||||
|
||||
|
|
@ -36,10 +35,6 @@ class ServerScript(Document):
|
|||
fields=["name", "stopped"],
|
||||
)
|
||||
|
||||
def validate_script(self):
|
||||
"""Utilizes the ast module to check for syntax errors
|
||||
"""
|
||||
ast.parse(self.script)
|
||||
|
||||
def sync_scheduled_jobs(self):
|
||||
"""Sync Scheduled Job Type statuses if Server Script's disabled status is changed
|
||||
|
|
|
|||
|
|
@ -232,6 +232,32 @@ class TestCustomizeForm(unittest.TestCase):
|
|||
testdt.delete()
|
||||
testdt1.delete()
|
||||
|
||||
def test_custom_internal_links(self):
|
||||
# add a custom internal link
|
||||
frappe.clear_cache()
|
||||
d = self.get_customize_form("User Group")
|
||||
|
||||
d.append('links', dict(link_doctype='User Group Member', parent_doctype='User',
|
||||
link_fieldname='user', table_fieldname='user_group_members', group='Tests', custom=1))
|
||||
|
||||
d.run_method("save_customization")
|
||||
|
||||
frappe.clear_cache()
|
||||
user_group = frappe.get_meta('User Group')
|
||||
|
||||
# check links exist
|
||||
self.assertTrue([d.name for d in user_group.links if d.link_doctype == 'User Group Member'])
|
||||
self.assertTrue([d.name for d in user_group.links if d.parent_doctype == 'User'])
|
||||
|
||||
# remove the link
|
||||
d = self.get_customize_form("User Group")
|
||||
d.links = []
|
||||
d.run_method("save_customization")
|
||||
|
||||
frappe.clear_cache()
|
||||
user_group = frappe.get_meta('Event')
|
||||
self.assertFalse([d.name for d in (user_group.links or []) if d.link_doctype == 'User Group Member'])
|
||||
|
||||
def test_custom_action(self):
|
||||
test_route = '/app/List/DocType'
|
||||
|
||||
|
|
|
|||
|
|
@ -543,7 +543,7 @@ class Database(object):
|
|||
"""
|
||||
|
||||
if not doctype in self.value_cache:
|
||||
self.value_cache = self.value_cache[doctype] = {}
|
||||
self.value_cache[doctype] = {}
|
||||
|
||||
if fieldname in self.value_cache[doctype]:
|
||||
return self.value_cache[doctype][fieldname]
|
||||
|
|
|
|||
|
|
@ -727,6 +727,18 @@ class BaseDocument(object):
|
|||
if abs(cint(value)) > max_length:
|
||||
self.throw_length_exceeded_error(df, max_length, value)
|
||||
|
||||
def _validate_code_fields(self):
|
||||
for field in self.meta.get_code_fields():
|
||||
code_string = self.get(field.fieldname)
|
||||
language = field.get("options")
|
||||
|
||||
if language == "Python":
|
||||
frappe.utils.validate_python_code(code_string, fieldname=field.label, is_expression=False)
|
||||
|
||||
elif language == "PythonExpression":
|
||||
frappe.utils.validate_python_code(code_string, fieldname=field.label)
|
||||
|
||||
|
||||
def throw_length_exceeded_error(self, df, max_length, value):
|
||||
if self.parentfield and self.idx:
|
||||
reference = _("{0}, Row {1}").format(_(self.doctype), self.idx)
|
||||
|
|
|
|||
|
|
@ -495,6 +495,7 @@ class Document(BaseDocument):
|
|||
self._validate_selects()
|
||||
self._validate_non_negative()
|
||||
self._validate_length()
|
||||
self._validate_code_fields()
|
||||
self._extract_images_from_text_editor()
|
||||
self._sanitize_content()
|
||||
self._save_passwords()
|
||||
|
|
|
|||
|
|
@ -141,6 +141,9 @@ class Meta(Document):
|
|||
def get_image_fields(self):
|
||||
return self.get("fields", {"fieldtype": "Attach Image"})
|
||||
|
||||
def get_code_fields(self):
|
||||
return self.get("fields", {"fieldtype": "Code"})
|
||||
|
||||
def get_set_only_once_fields(self):
|
||||
'''Return fields with `set_only_once` set'''
|
||||
if not hasattr(self, "_set_only_once_fields"):
|
||||
|
|
@ -504,6 +507,9 @@ class Meta(Document):
|
|||
if not data.non_standard_fieldnames:
|
||||
data.non_standard_fieldnames = {}
|
||||
|
||||
if not data.internal_links:
|
||||
data.internal_links = {}
|
||||
|
||||
for link in dashboard_links:
|
||||
link.added = False
|
||||
if link.hidden:
|
||||
|
|
@ -511,24 +517,32 @@ class Meta(Document):
|
|||
|
||||
for group in data.transactions:
|
||||
group = frappe._dict(group)
|
||||
|
||||
# For internal links parent doctype will be the key
|
||||
doctype = link.parent_doctype or link.link_doctype
|
||||
# group found
|
||||
if link.group and group.label == link.group:
|
||||
if link.link_doctype not in group.get('items'):
|
||||
group.get('items').append(link.link_doctype)
|
||||
if doctype not in group.get('items'):
|
||||
group.get('items').append(doctype)
|
||||
link.added = True
|
||||
|
||||
if not link.added:
|
||||
# group not found, make a new group
|
||||
data.transactions.append(dict(
|
||||
label = link.group,
|
||||
items = [link.link_doctype]
|
||||
items = [link.parent_doctype or link.link_doctype]
|
||||
))
|
||||
|
||||
if link.link_fieldname != data.fieldname:
|
||||
if data.fieldname:
|
||||
data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
|
||||
else:
|
||||
|
||||
if not link.is_child_table:
|
||||
if link.link_fieldname != data.fieldname:
|
||||
if data.fieldname:
|
||||
data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
|
||||
else:
|
||||
data.fieldname = link.link_fieldname
|
||||
elif link.is_child_table:
|
||||
if not data.fieldname:
|
||||
data.fieldname = link.link_fieldname
|
||||
data.internal_links[link.parent_doctype] = [link.table_fieldname, link.link_fieldname]
|
||||
|
||||
|
||||
def get_row_template(self):
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import frappe
|
|||
from frappe.utils import evaluate_filters, money_in_words, scrub_urls, get_url
|
||||
from frappe.utils import validate_url, validate_email_address
|
||||
from frappe.utils import ceil, floor
|
||||
from frappe.utils.data import validate_python_code
|
||||
|
||||
from PIL import Image
|
||||
from frappe.utils.image import strip_exif_data, optimize_image
|
||||
|
|
@ -201,4 +202,31 @@ class TestImage(unittest.TestCase):
|
|||
|
||||
self.assertLessEqual(width, 500)
|
||||
self.assertLessEqual(height, 500)
|
||||
self.assertLess(len(optimized_content), len(original_content))
|
||||
self.assertLess(len(optimized_content), len(original_content))
|
||||
|
||||
class TestPythonExpressions(unittest.TestCase):
|
||||
|
||||
def test_validation_for_good_python_expression(self):
|
||||
valid_expressions = [
|
||||
"foo == bar",
|
||||
"foo == 42",
|
||||
"password != 'hunter2'",
|
||||
"complex != comparison and more_complex == condition",
|
||||
"escaped_values == 'str with newline\\n'",
|
||||
"check_box_field",
|
||||
]
|
||||
for expr in valid_expressions:
|
||||
try:
|
||||
validate_python_code(expr)
|
||||
except Exception as e:
|
||||
self.fail(f"Invalid error thrown for valid expression: {expr}: {str(e)}")
|
||||
|
||||
def test_validation_for_bad_python_expression(self):
|
||||
invalid_expressions = [
|
||||
"these_are && js_conditions",
|
||||
"more || js_conditions",
|
||||
"curly_quotes_bad == “const”",
|
||||
"oops = forgot_equals",
|
||||
]
|
||||
for expr in invalid_expressions:
|
||||
self.assertRaises(frappe.ValidationError, validate_python_code, expr)
|
||||
|
|
@ -5,6 +5,7 @@ import frappe
|
|||
import operator
|
||||
import json
|
||||
import re, datetime, math, time
|
||||
from code import compile_command
|
||||
from urllib.parse import quote, urljoin
|
||||
from frappe.desk.utils import slug
|
||||
|
||||
|
|
@ -1511,6 +1512,34 @@ def get_user_info_for_avatar(user_id):
|
|||
return user_info
|
||||
|
||||
|
||||
def validate_python_code(string: str, fieldname=None, is_expression: bool = True) -> None:
|
||||
""" Validate python code fields by using compile_command to ensure that expression is valid python.
|
||||
|
||||
args:
|
||||
fieldname: name of field being validated.
|
||||
is_expression: true for validating simple single line python expression, else validated as script.
|
||||
"""
|
||||
|
||||
if not string:
|
||||
return
|
||||
|
||||
try:
|
||||
compile_command(string, symbol="eval" if is_expression else "exec")
|
||||
except SyntaxError as se:
|
||||
line_no = se.lineno - 1 or 0
|
||||
offset = se.offset - 1 or 0
|
||||
error_line = string if is_expression else string.split("\n")[line_no]
|
||||
msg = (frappe._("{} Invalid python code on line {}")
|
||||
.format(fieldname + ":" if fieldname else "", line_no+1))
|
||||
msg += f"<br><pre>{error_line}</pre>"
|
||||
msg += f"<pre>{' ' * offset}^</pre>"
|
||||
|
||||
frappe.throw(msg, title=frappe._("Syntax Error"))
|
||||
except Exception as e:
|
||||
frappe.msgprint(frappe._("{} Possibly invalid python code. <br>{}")
|
||||
.format(fieldname + ": " or "", str(e)), indicator="orange")
|
||||
|
||||
|
||||
class UnicodeWithAttrs(str):
|
||||
def __init__(self, text):
|
||||
self.toc_html = text.toc_html
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@
|
|||
"label": "Landing Page"
|
||||
},
|
||||
{
|
||||
"description": "Link that is the website home page. Standard Links (index, login, products, blog, about, contact)",
|
||||
"description": "Link that is the website home page. Standard Links (home, login, products, blog, about, contact)",
|
||||
"fieldname": "home_page",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
|
|
@ -433,7 +433,7 @@
|
|||
"issingle": 1,
|
||||
"links": [],
|
||||
"max_attachments": 10,
|
||||
"modified": "2021-04-14 17:39:56.609771",
|
||||
"modified": "2021-07-15 17:39:56.609771",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Website Settings",
|
||||
|
|
@ -457,4 +457,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class WebsiteSettings(Document):
|
|||
return
|
||||
from frappe.website.path_resolver import PathResolver
|
||||
if self.home_page and not PathResolver(self.home_page).is_valid_path():
|
||||
frappe.msgprint(_("Invalid Home Page") + " (Standard pages - index, login, products, blog, about, contact)")
|
||||
frappe.msgprint(_("Invalid Home Page") + " (Standard pages - home, login, products, blog, about, contact)")
|
||||
self.home_page = ''
|
||||
|
||||
def validate_top_bar_items(self):
|
||||
|
|
|
|||
|
|
@ -1,335 +1,100 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"actions": [],
|
||||
"creation": "2013-02-22 01:27:36",
|
||||
"custom": 0,
|
||||
"description": "Defines actions on states and the next step and allowed roles.",
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"state",
|
||||
"action",
|
||||
"next_state",
|
||||
"allowed",
|
||||
"allow_self_approval",
|
||||
"conditions",
|
||||
"condition",
|
||||
"column_break_7",
|
||||
"example"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "State",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Workflow State",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "200px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "200px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "action",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Action",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Workflow Action Master",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "200px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "200px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "next_state",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Next State",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Workflow State",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "200px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "200px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "allowed",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Allowed",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Role",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "200px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "200px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"description": "Allow approval for creator of the document",
|
||||
"fieldname": "allow_self_approval",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Allow Self Approval",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Allow Self Approval"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "conditions",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Conditions",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Conditions"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "condition",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Condition",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "PythonExpression"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "example",
|
||||
"fieldtype": "HTML",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Example",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "<pre><code>doc.grand_total > 0</code></pre>\n\n<p>Conditions should be written in simple Python. Please use properties available in the form only.</p>\n<p>Allowed functions: \n</p><ul>\n<li>frappe.db.get_value</li>\n<li>frappe.db.get_list</li>\n<li>frappe.session</li>\n<li>frappe.utils.now_datetime</li>\n<li>frappe.utils.get_datetime</li>\n<li>frappe.utils.add_to_date</li>\n<li>frappe.utils.now</li>\n</ul>\n<p>Example: </p><pre><code>doc.creation > frappe.utils.add_to_date(frappe.utils.now_datetime(), days=-5, as_string=True, as_datetime=True) </code></pre><p></p>",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "<pre><code>doc.grand_total > 0</code></pre>\n\n<p>Conditions should be written in simple Python. Please use properties available in the form only.</p>\n<p>Allowed functions: \n</p><ul>\n<li>frappe.db.get_value</li>\n<li>frappe.db.get_list</li>\n<li>frappe.session</li>\n<li>frappe.utils.now_datetime</li>\n<li>frappe.utils.get_datetime</li>\n<li>frappe.utils.add_to_date</li>\n<li>frappe.utils.now</li>\n</ul>\n<p>Example: </p><pre><code>doc.creation > frappe.utils.add_to_date(frappe.utils.now_datetime(), days=-5, as_string=True, as_datetime=True) </code></pre><p></p>"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2020-11-08 12:11:00.294908",
|
||||
"links": [],
|
||||
"modified": "2021-07-21 13:24:59.084836",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Workflow",
|
||||
"name": "Workflow Transition",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue