Merge branch 'develop' into fix-load-more-btn

This commit is contained in:
Shariq Ansari 2022-02-15 12:32:12 +05:30 committed by GitHub
commit b764e68e7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1521 additions and 1154 deletions

View file

@ -48,3 +48,7 @@ pull_request_rules:
actions:
merge:
method: squash
commit_message_template: |
{{ title }} (#{{ number }})
{{ body }}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,7 @@
"hide_days",
"hide_seconds",
"reqd",
"is_virtual",
"search_index",
"column_break_18",
"options",
@ -534,13 +535,19 @@
"fieldname": "show_dashboard",
"fieldtype": "Check",
"label": "Show Dashboard"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Virtual"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-01-03 11:56:19.812863",
"modified": "2022-01-27 21:22:20.529072",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -781,29 +781,31 @@ 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))
if not link.is_child_table:
continue
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)
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"))
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.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"))
def validate_fields_for_doctype(doctype):
meta = frappe.get_meta(doctype, cached=False)
validate_links_table_fieldnames(meta)
@ -1076,6 +1078,9 @@ def validate_fields(meta):
field.fetch_from = field.fetch_from.strip('\n').strip()
def validate_data_field_type(docfield):
if docfield.get("is_virtual"):
return
if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"):
if docfield.options and (docfield.options not in data_field_options):
df_str = frappe.bold(_(docfield.label))
@ -1321,10 +1326,9 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
else:
raise
def check_fieldname_conflicts(doctype, fieldname):
def check_fieldname_conflicts(docfield):
"""Checks if fieldname conflicts with methods or properties"""
doc = frappe.get_doc({"doctype": doctype})
doc = frappe.get_doc({"doctype": docfield.dt})
available_objects = [x for x in dir(doc) if isinstance(x, str)]
property_list = [
x for x in available_objects if isinstance(getattr(type(doc), x, None), property)
@ -1332,9 +1336,10 @@ def check_fieldname_conflicts(doctype, fieldname):
method_list = [
x for x in available_objects if x not in property_list and callable(getattr(doc, x))
]
msg = _("Fieldname {0} conflicting with meta object").format(docfield.fieldname)
if fieldname in method_list + property_list:
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname))
if docfield.fieldname in method_list + property_list:
frappe.msgprint(msg, raise_exception=not docfield.is_virtual)
def clear_linked_doctype_cache():
frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled')

View file

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

View file

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

View file

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

View file

@ -1,458 +1,468 @@
{
"actions": [],
"allow_import": 1,
"creation": "2013-01-10 16:34:01",
"description": "Adds a custom field to a DocType",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"dt",
"module",
"label",
"label_help",
"fieldname",
"insert_after",
"length",
"column_break_6",
"fieldtype",
"precision",
"hide_seconds",
"hide_days",
"options",
"fetch_from",
"fetch_if_empty",
"options_help",
"section_break_11",
"collapsible",
"collapsible_depends_on",
"default",
"depends_on",
"mandatory_depends_on",
"read_only_depends_on",
"properties",
"non_negative",
"reqd",
"unique",
"read_only",
"ignore_user_permissions",
"hidden",
"print_hide",
"print_hide_if_no_value",
"print_width",
"no_copy",
"allow_on_submit",
"in_list_view",
"in_standard_filter",
"in_global_search",
"in_preview",
"bold",
"report_hide",
"search_index",
"allow_in_quick_entry",
"ignore_xss_filter",
"translatable",
"hide_border",
"description",
"permlevel",
"width",
"columns"
],
"fields": [{
"bold": 1,
"fieldname": "dt",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Document",
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
"reqd": 1,
"search_index": 1
},
{
"bold": 1,
"fieldname": "label",
"fieldtype": "Data",
"in_filter": 1,
"label": "Label",
"no_copy": 1,
"oldfieldname": "label",
"oldfieldtype": "Data"
},
{
"fieldname": "label_help",
"fieldtype": "HTML",
"label": "Label Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"no_copy": 1,
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1
},
{
"description": "Select the label after which you want to insert new field.",
"fieldname": "insert_after",
"fieldtype": "Select",
"label": "Insert After",
"no_copy": 1,
"oldfieldname": "insert_after",
"oldfieldtype": "Select"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"bold": 1,
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"label": "Field Type",
"oldfieldname": "fieldtype",
"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\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
"reqd": 1
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
},
{
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
},
{
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
},
{
"fieldname": "options_help",
"fieldtype": "HTML",
"label": "Options Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
},
{
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On"
},
{
"fieldname": "default",
"fieldtype": "Text",
"label": "Default Value",
"oldfieldname": "default",
"oldfieldtype": "Text"
},
{
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"length": 255
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Field Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
},
{
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"label": "Permission Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
},
{
"fieldname": "width",
"fieldtype": "Data",
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data"
},
{
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
},
{
"fieldname": "properties",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
},
{
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Mandatory Field",
"oldfieldname": "reqd",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype===\"Link\"",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
},
{
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
},
{
"fieldname": "print_width",
"fieldtype": "Data",
"hidden": 1,
"label": "Print Width",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"fieldname": "no_copy",
"fieldtype": "Check",
"label": "No Copy",
"oldfieldname": "no_copy",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
},
{
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
},
{
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
},
{
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
},
{
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "search_index",
"fieldtype": "Check",
"hidden": 1,
"label": "Index",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"description": "Don't HTML Encode HTML tags like &lt;script&gt; or just characters like &lt; or &gt;, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
},
{
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
},
{
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"length": 255
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"length": 255
},
{
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"default": "0",
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_seconds",
"fieldtype": "Check",
"label": "Hide Seconds"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_days",
"fieldtype": "Check",
"label": "Hide Days"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
},
{
"default": "0",
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
"fieldname": "non_negative",
"fieldtype": "Check",
"label": "Non Negative"
},
{
"fieldname": "module",
"fieldtype": "Link",
"label": "Module (for export)",
"options": "Module Def"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-04 12:45:23.810120",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
"owner": "Administrator",
"permissions": [{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "dt,label,fieldtype,options",
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
"actions": [],
"allow_import": 1,
"creation": "2013-01-10 16:34:01",
"description": "Adds a custom field to a DocType",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"dt",
"module",
"label",
"label_help",
"fieldname",
"insert_after",
"length",
"column_break_6",
"fieldtype",
"precision",
"hide_seconds",
"hide_days",
"options",
"fetch_from",
"fetch_if_empty",
"options_help",
"section_break_11",
"collapsible",
"collapsible_depends_on",
"default",
"depends_on",
"mandatory_depends_on",
"read_only_depends_on",
"properties",
"non_negative",
"reqd",
"unique",
"is_virtual",
"read_only",
"ignore_user_permissions",
"hidden",
"print_hide",
"print_hide_if_no_value",
"print_width",
"no_copy",
"allow_on_submit",
"in_list_view",
"in_standard_filter",
"in_global_search",
"in_preview",
"bold",
"report_hide",
"search_index",
"allow_in_quick_entry",
"ignore_xss_filter",
"translatable",
"hide_border",
"description",
"permlevel",
"width",
"columns"
],
"fields": [
{
"bold": 1,
"fieldname": "dt",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Document",
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
"reqd": 1,
"search_index": 1
},
{
"bold": 1,
"fieldname": "label",
"fieldtype": "Data",
"in_filter": 1,
"label": "Label",
"no_copy": 1,
"oldfieldname": "label",
"oldfieldtype": "Data"
},
{
"fieldname": "label_help",
"fieldtype": "HTML",
"label": "Label Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"no_copy": 1,
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1
},
{
"description": "Select the label after which you want to insert new field.",
"fieldname": "insert_after",
"fieldtype": "Select",
"label": "Insert After",
"no_copy": 1,
"oldfieldname": "insert_after",
"oldfieldtype": "Select"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"bold": 1,
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"label": "Field Type",
"oldfieldname": "fieldtype",
"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\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
"reqd": 1
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
},
{
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
},
{
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
},
{
"fieldname": "options_help",
"fieldtype": "HTML",
"label": "Options Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
},
{
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On"
},
{
"fieldname": "default",
"fieldtype": "Text",
"label": "Default Value",
"oldfieldname": "default",
"oldfieldtype": "Text"
},
{
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"length": 255
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Field Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
},
{
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"label": "Permission Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
},
{
"fieldname": "width",
"fieldtype": "Data",
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data"
},
{
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
},
{
"fieldname": "properties",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
},
{
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Mandatory Field",
"oldfieldname": "reqd",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype===\"Link\"",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
},
{
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
},
{
"fieldname": "print_width",
"fieldtype": "Data",
"hidden": 1,
"label": "Print Width",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"fieldname": "no_copy",
"fieldtype": "Check",
"label": "No Copy",
"oldfieldname": "no_copy",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
},
{
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
},
{
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
},
{
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
},
{
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "search_index",
"fieldtype": "Check",
"hidden": 1,
"label": "Index",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"description": "Don't HTML Encode HTML tags like &lt;script&gt; or just characters like &lt; or &gt;, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
},
{
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
},
{
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"length": 255
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"length": 255
},
{
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"default": "0",
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_seconds",
"fieldtype": "Check",
"label": "Hide Seconds"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_days",
"fieldtype": "Check",
"label": "Hide Days"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
},
{
"default": "0",
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
"fieldname": "non_negative",
"fieldtype": "Check",
"label": "Non Negative"
},
{
"fieldname": "module",
"fieldtype": "Link",
"label": "Module (for export)",
"options": "Module Def"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-01-27 21:47:01.065556",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "dt,label,fieldtype,options",
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View file

@ -54,7 +54,7 @@ class CustomField(Document):
old_fieldtype = self.db_get('fieldtype')
is_fieldtype_changed = (not self.is_new()) and (old_fieldtype != self.fieldtype)
if is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype):
if not self.is_virtual and is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype):
frappe.throw(_("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype))
if not self.fieldname:
@ -65,7 +65,7 @@ class CustomField(Document):
if not self.flags.ignore_validate:
from frappe.core.doctype.doctype.doctype import check_fieldname_conflicts
check_fieldname_conflicts(self.dt, self.fieldname)
check_fieldname_conflicts(self)
def on_update(self):
if not frappe.flags.in_setup_wizard:

View file

@ -418,6 +418,9 @@ class CustomizeForm(Document):
return property_value
def validate_fieldtype_change(self, df, old_value, new_value):
if df.is_virtual:
return
allowed = self.allow_fieldtype_change(old_value, new_value)
if allowed:
old_value_length = cint(frappe.db.type_map.get(old_value)[1])
@ -430,7 +433,8 @@ class CustomizeForm(Document):
self.validate_fieldtype_length()
else:
self.flags.update_db = True
if not allowed:
else:
frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx))
def validate_fieldtype_length(self):
@ -558,7 +562,8 @@ docfield_properties = {
'allow_in_quick_entry': 'Check',
'hide_border': 'Check',
'hide_days': 'Check',
'hide_seconds': 'Check'
'hide_seconds': 'Check',
'is_virtual': 'Check',
}
doctype_link_properties = {

View file

@ -14,6 +14,7 @@
"non_negative",
"reqd",
"unique",
"is_virtual",
"in_list_view",
"in_standard_filter",
"in_global_search",
@ -115,6 +116,12 @@
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
},
{
"default": "0",
"fieldname": "in_list_view",
@ -436,7 +443,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-01-03 14:50:32.035768",
"modified": "2022-01-27 21:45:22.349776",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -1,4 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
@ -18,53 +18,19 @@ class PropertySetter(Document):
def validate(self):
self.validate_fieldtype_change()
if self.is_new():
delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name)
# clear cache
frappe.clear_cache(doctype = self.doc_type)
def validate_fieldtype_change(self):
if self.field_name in not_allowed_fieldtype_change and \
self.property == 'fieldtype':
frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name))
def get_property_list(self, dt):
return frappe.db.get_all('DocField',
fields=['fieldname', 'label', 'fieldtype'],
filters={
'parent': dt,
'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
'fieldname': ['!=', '']
},
order_by='label asc',
as_dict=1
)
def get_setup_data(self):
return {
'doctypes': frappe.get_all("DocType", pluck="name"),
'dt_properties': self.get_property_list('DocType'),
'df_properties': self.get_property_list('DocField')
}
def get_field_ids(self):
return frappe.db.get_values(
"DocField",
filters={"parent": self.doc_type},
fieldname=["name", "fieldtype", "label", "fieldname"],
as_dict=True,
)
def get_defaults(self):
if not self.field_name:
return frappe.get_all("DocType", filters={"name": self.doc_type}, fields="*")[0]
else:
return frappe.db.get_values(
"DocField",
filters={"fieldname": self.field_name, "parent": self.doc_type},
fieldname="*",
)[0]
if (
self.property == 'fieldtype'
and self.field_name in not_allowed_fieldtype_change
):
frappe.throw(
_("Field type cannot be changed for {0}").format(self.field_name)
)
def on_update(self):
if frappe.flags.in_patch:
@ -74,6 +40,7 @@ class PropertySetter(Document):
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
validate_fields_for_doctype(self.doc_type)
def make_property_setter(doctype, fieldname, property, value, property_type, for_doctype = False,
validate_fields_for_doctype=True):
# WARNING: Ignores Permissions
@ -91,6 +58,7 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for
property_setter.insert()
return property_setter
def delete_property_setter(doc_type, property, field_name=None, row_name=None):
"""delete other property setters on this, if this is new"""
filters = dict(doc_type=doc_type, property=property)
@ -100,4 +68,3 @@ def delete_property_setter(doc_type, property, field_name=None, row_name=None):
filters["row_name"] = row_name
frappe.db.delete('Property Setter', filters)

View file

@ -177,6 +177,8 @@ class Database(object):
raise frappe.QueryTimeoutError(e)
elif frappe.conf.db_type == 'postgres':
# TODO: added temporarily
print(e)
raise
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):

View file

@ -67,7 +67,7 @@ class DBTable:
"""
get columns from docfields and custom fields
"""
fields = self.meta.get_fieldnames_with_value(True)
fields = self.meta.get_fieldnames_with_value(with_field_meta=True)
# optional fields like _comments
if not self.meta.get('istable'):
@ -85,6 +85,9 @@ class DBTable:
})
for field in fields:
if field.get("is_virtual"):
continue
self.columns[field.get('fieldname')] = DbColumn(
self,
field.get('fieldname'),

View file

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

View file

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

View file

@ -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",
"depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_fieldname))",
"fieldname": "fieldname",
"fieldtype": "Select",
"label": "Fieldname",
"reqd": 1
},
{
"fetch_from": "field.fieldname",
"fieldname": "fieldname",
"fieldtype": "Data",
"hidden": 1,
"label": "Fieldname",
"read_only": 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
}

View file

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

View file

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

View file

@ -262,22 +262,66 @@ def compress(data, args=None):
}
@frappe.whitelist()
def save_report():
"""save report"""
def save_report(name, doctype, report_settings):
"""Save reports of type Report Builder from Report View"""
data = frappe.local.form_dict
if frappe.db.exists('Report', data['name']):
d = frappe.get_doc('Report', data['name'])
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']
report = frappe.new_doc('Report')
report.report_name = name
report.ref_doctype = 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.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()

View file

@ -1,16 +1,14 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import datetime
import frappe
import datetime
from frappe import _
from frappe.model import default_fields, table_fields, child_table_fields
from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields
from frappe.model.naming import set_new_name
from frappe.model.utils.link_count import notify_link_count
from frappe.modules import load_doctype_module
from frappe.model import display_fieldtypes
from frappe.utils import (cint, flt, now, cstr, strip_html,
sanitize_html, sanitize_email, cast_fieldtype)
from frappe.utils import cast_fieldtype, cint, cstr, flt, now, sanitize_html, strip_html
from frappe.utils.html_utils import unescape_html
from frappe.model.docstatus import DocStatus
@ -254,7 +252,22 @@ class BaseDocument(object):
continue
df = self.meta.get_field(fieldname)
if df:
if df and df.get("is_virtual"):
from frappe.utils.safe_exec import get_safe_globals
if d[fieldname] is None:
if df.get("options"):
d[fieldname] = frappe.safe_eval(
code=df.get("options"),
eval_globals=get_safe_globals(),
eval_locals={"doc": self},
)
else:
_val = getattr(self, fieldname, None)
if _val and not callable(_val):
d[fieldname] = _val
elif df:
if df.fieldtype=="Check":
d[fieldname] = 1 if cint(d[fieldname]) else 0
@ -328,6 +341,7 @@ class BaseDocument(object):
def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=False, no_child_table_fields=False):
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str)
doc["doctype"] = self.doctype
for df in self.meta.get_table_fields():
children = self.get(df.fieldname) or []
doc[df.fieldname] = [
@ -375,12 +389,24 @@ class BaseDocument(object):
fieldname = [df.fieldname for df in self.meta.get_table_fields() if df.options==doctype]
return fieldname[0] if fieldname else None
def db_insert(self):
"""INSERT the document (with valid columns) in the database."""
def db_insert(self, ignore_if_duplicate=False):
"""INSERT the document (with valid columns) in the database.
args:
ignore_if_duplicate: ignore primary key collision
at database level (postgres)
in python (mariadb)
"""
if not self.name:
# name will be set by document class in most cases
set_new_name(self)
conflict_handler = ""
# On postgres we can't implcitly ignore PK collision
# So instruct pg to ignore `name` field conflicts
if ignore_if_duplicate and frappe.db.db_type == "postgres":
conflict_handler = "on conflict (name) do nothing"
if not self.creation:
self.creation = self.modified = now()
self.created_by = self.modified_by = frappe.session.user
@ -391,10 +417,11 @@ class BaseDocument(object):
columns = list(d)
try:
frappe.db.sql("""INSERT INTO `tab{doctype}` ({columns})
VALUES ({values})""".format(
doctype = self.doctype,
columns = ", ".join("`"+c+"`" for c in columns),
values = ", ".join(["%s"] * len(columns))
VALUES ({values}) {conflict_handler}""".format(
doctype=self.doctype,
columns=", ".join("`"+c+"`" for c in columns),
values=", ".join(["%s"] * len(columns)),
conflict_handler=conflict_handler
), list(d.values()))
except Exception as e:
if frappe.db.is_primary_key_violation(e):
@ -407,8 +434,11 @@ class BaseDocument(object):
self.db_insert()
return
frappe.msgprint(_("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)), title=_("Duplicate Name"), indicator="red")
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
if not ignore_if_duplicate:
frappe.msgprint(_("{0} {1} already exists")
.format(self.doctype, frappe.bold(self.name)),
title=_("Duplicate Name"), indicator="red")
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
elif frappe.db.is_unique_key_violation(e):
# unique constraint

View file

@ -249,11 +249,7 @@ class Document(BaseDocument):
if getattr(self.meta, "issingle", 0):
self.update_single(self.get_valid_dict())
else:
try:
self.db_insert()
except frappe.DuplicateEntryError as e:
if not ignore_if_duplicate:
raise e
self.db_insert(ignore_if_duplicate=ignore_if_duplicate)
# children
for d in self.get_all_children():

View file

@ -444,9 +444,16 @@ class Meta(Document):
self.permissions = [Document(d) for d in custom_perms]
def get_fieldnames_with_value(self, with_field_meta=False):
return [df if with_field_meta else df.fieldname \
for df in self.fields if df.fieldtype not in no_value_fields]
def is_value_field(docfield):
return not (
docfield.get("is_virtual")
or docfield.fieldtype in no_value_fields
)
if with_field_meta:
return [df for df in self.fields if is_value_field(df)]
return [df.fieldname for df in self.fields if is_value_field(df)]
def get_fields_to_check_permissions(self, user_permission_doctypes):
fields = self.get("fields", {

View file

@ -39,6 +39,9 @@ frappe.ui.form.Control = class BaseControl {
if (this.df.get_status) {
return this.df.get_status(this);
}
if (this.df.is_virtual) {
return "Read";
}
if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) {
// like in case of a dialog box
@ -52,7 +55,7 @@ frappe.ui.form.Control = class BaseControl {
if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console
return "None";
} else if (cint(this.df.read_only)) {
} else if (cint(this.df.read_only || this.df.is_virtual)) {
// eslint-disable-next-line
if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console
return "Read";

View file

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

View file

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

View file

@ -55,7 +55,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
// prepare a list of mandatory, bold and allow in quick entry fields
this.mandatory = fields.filter(df => {
return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only);
return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only && !df.is_virtual);
});
}

View file

@ -192,9 +192,18 @@ frappe.ui.form.ScriptManager = class ScriptManager {
}
function setup_add_fetch(df) {
if ((['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Attach Image',
'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype) || df.read_only==1)
&& df.fetch_from && df.fetch_from.indexOf(".")!=-1) {
let is_read_only_field = (
['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Text Editor', 'Attach Image',
'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype)
|| df.read_only == 1
|| df.is_virtual == 1
)
if (
is_read_only_field
&& df.fetch_from
&& df.fetch_from.indexOf(".") != -1
) {
var parts = df.fetch_from.split(".");
me.frm.add_fetch(parts[0], parts[1], df.fieldname, df.parent);
}

View file

@ -1672,7 +1672,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
frappe.model.is_value_type(field_doc) &&
field_doc.fieldtype !== "Read Only" &&
!field_doc.hidden &&
!field_doc.read_only
!field_doc.read_only &&
!field_doc.is_virtual
);
};

View file

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

View file

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

View file

@ -259,8 +259,16 @@ frappe.utils.xss_sanitise = function (string, options) {
'/': '&#x2F;'
};
const REGEX_SCRIPT = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi; // used in jQuery 1.7.2 src/ajax.js Line 14
const REGEX_ALERT = /confirm\(.*\)|alert\(.*\)|prompt\(.*\)/gi; // captures alert, confirm, prompt
options = Object.assign({}, DEFAULT_OPTIONS, options); // don't deep copy, immutable beauty.
// Rule 3 - TODO: Check event handlers?
// script and alert should be checked first or else it will be escaped
if (options.strategies.includes('js')) {
sanitised = sanitised.replace(REGEX_SCRIPT, "");
sanitised = sanitised.replace(REGEX_ALERT, "");
}
// Rule 1
if (options.strategies.includes('html')) {
for (let char in HTML_ESCAPE_MAP) {
@ -270,11 +278,6 @@ frappe.utils.xss_sanitise = function (string, options) {
}
}
// Rule 3 - TODO: Check event handlers?
if (options.strategies.includes('js')) {
sanitised = sanitised.replace(REGEX_SCRIPT, "");
}
return sanitised;
}

View file

@ -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
@ -644,6 +648,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
// not a cancelled doc
&& data.docstatus !== 2
&& !df.read_only
&& !df.is_virtual
&& !df.hidden
// not a standard field i.e., owner, modified_by, etc.
&& !frappe.model.std_fields_list.includes(df.fieldname))
@ -1025,7 +1030,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
title += ` (${__(doctype)})`;
}
const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only;
const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only && !docfield.is_virtual;
const align = (() => {
const is_numeric = frappe.model.is_numeric_field(docfield);
@ -1207,7 +1212,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 +1249,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 +1481,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

View file

@ -343,7 +343,7 @@ frappe.views.TreeView = class TreeView {
this.ignore_fields = this.opts.ignore_fields || [];
var mandatory_fields = $.map(me.opts.meta.fields, function(d) {
return (d.reqd || d.bold && !d.read_only) ? d : null });
return (d.reqd || d.bold && !d.read_only && !!d.is_virtual) ? d : null });
var opts_field_names = this.fields.map(function(d) {
return d.fieldname

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -104,10 +104,10 @@ body[data-route^="Module"] .main-menu {
}
.sidebar-image-section {
width: min(100%, 170px);
cursor: pointer;
.sidebar-image {
width: min(100%, 170px);
height: auto;
max-height: 170px;
object-fit: cover;

View file

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

View file

@ -291,6 +291,16 @@ class TestDB(unittest.TestCase):
frappe.db.MAX_WRITES_PER_TRANSACTION = Database.MAX_WRITES_PER_TRANSACTION
def test_pk_collision_ignoring(self):
# note has `name` generated from title
for _ in range(3):
frappe.get_doc(doctype="Note", title="duplicate name").insert(ignore_if_duplicate=True)
with savepoint():
self.assertRaises(frappe.DuplicateEntryError, frappe.get_doc(doctype="Note", title="duplicate name").insert)
# recover transaction to continue other tests
raise Exception
@run_only_if(db_type_is.MARIADB)
class TestDDLCommandsMaria(unittest.TestCase):

View file

@ -1,11 +1,20 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import os
import unittest
from contextlib import contextmanager
from datetime import timedelta
from unittest.mock import patch
import frappe
from frappe.utils import cint
from frappe.model.naming import revert_series_if_last, make_autoname, parse_naming_series
from frappe.desk.doctype.note.note import Note
from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last
from frappe.utils import cint, now_datetime
class CustomTestNote(Note):
@property
def age(self):
return now_datetime() - self.creation
class TestDocument(unittest.TestCase):
@ -256,4 +265,50 @@ class TestDocument(unittest.TestCase):
def test_limit_for_get(self):
doc = frappe.get_doc("DocType", "DocType")
# assuming DocType has more that 3 Data fields
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)
def test_virtual_fields(self):
"""Virtual fields are accessible via API and Form views, whenever .as_dict is invoked
"""
frappe.db.delete("Custom Field", {"dt": "Note", "fieldname":"age"})
def patch_note():
return patch("frappe.controllers", new={frappe.local.site: {'Note': CustomTestNote}})
@contextmanager
def customize_note(with_options=False):
options = "frappe.utils.now_datetime() - doc.creation" if with_options else ""
custom_field = frappe.get_doc({
"doctype": "Custom Field",
"dt": "Note",
"fieldname": "age",
"fieldtype": "Data",
"read_only": True,
"is_virtual": True,
"options": options,
})
try:
yield custom_field.insert(ignore_if_duplicate=True)
finally:
custom_field.delete()
with patch_note():
doc = frappe.get_last_doc("Note")
self.assertIsInstance(doc, CustomTestNote)
self.assertIsInstance(doc.age, timedelta)
self.assertIsNone(doc.as_dict().get("age"))
self.assertIsNone(doc.get_valid_dict().get("age"))
with customize_note(), patch_note():
doc = frappe.get_last_doc("Note")
self.assertIsInstance(doc, CustomTestNote)
self.assertIsInstance(doc.age, timedelta)
self.assertIsInstance(doc.as_dict().get("age"), timedelta)
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)
with customize_note(with_options=True):
doc = frappe.get_last_doc("Note")
self.assertIsInstance(doc, Note)
self.assertIsInstance(doc.as_dict().get("age"), timedelta)
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)

View file

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

View file

@ -148,6 +148,8 @@ More Information,Mehr Informationen,
More...,Mehr...,
Move,Bewegen,
My Account,Mein Konto,
My Profile,Mein Profil,
My Settings,Meine Einstellungen,
New Address,Neue Adresse,
New Contact,Neuer Kontakt,
Next,Weiter,
@ -406,7 +408,7 @@ Allow Self Approval,Erlaube Selbstgenehmigung,
Allow approval for creator of the document,Genehmigung für den Ersteller des Dokuments zulassen,
Allow events in timeline,Ereignisse in der Zeitleiste zulassen,
Allow in Quick Entry,In Schnelleingabe zulassen,
Allow on Submit,Beim Übertragen zulassen,
Allow on Submit,Änderungen zulassen wenn gebucht,
Allow only one session per user,Nur eine Sitzung pro Benutzer zulassen,
Allow page break inside tables,Seitenumbruch innerhalb von Tabellen erlauben,
Allow saving if mandatory fields are not filled,Speichern trotz leerer Pflichtfelder zulassen,

1 A4 A4
148 More... Mehr...
149 Move Bewegen
150 My Account Mein Konto
151 My Profile Mein Profil
152 My Settings Meine Einstellungen
153 New Address Neue Adresse
154 New Contact Neuer Kontakt
155 Next Weiter
408 Allow approval for creator of the document Genehmigung für den Ersteller des Dokuments zulassen
409 Allow events in timeline Ereignisse in der Zeitleiste zulassen
410 Allow in Quick Entry In Schnelleingabe zulassen
411 Allow on Submit Beim Übertragen zulassen Änderungen zulassen wenn gebucht
412 Allow only one session per user Nur eine Sitzung pro Benutzer zulassen
413 Allow page break inside tables Seitenumbruch innerhalb von Tabellen erlauben
414 Allow saving if mandatory fields are not filled Speichern trotz leerer Pflichtfelder zulassen

View file

@ -60,7 +60,7 @@ frappe.ui.form.on("Web Form", {
options: field.options,
reqd: field.reqd,
default: field.default,
read_only: field.read_only,
read_only: field.read_only || field.is_virtual,
depends_on: field.depends_on,
mandatory_depends_on: field.mandatory_depends_on,
read_only_depends_on: field.read_only_depends_on,

View file

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

View file

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

753
yarn.lock

File diff suppressed because it is too large Load diff