diff --git a/cypress/integration/list_paging.js b/cypress/integration/list_paging.js
new file mode 100644
index 0000000000..b6832f5a53
--- /dev/null
+++ b/cypress/integration/list_paging.js
@@ -0,0 +1,35 @@
+context('List Paging', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app/website');
+ return cy.window().its('frappe').then(frappe => {
+ return frappe.call("frappe.tests.ui_test_helpers.create_multiple_todo_records");
+ });
+ });
+
+ it('test load more with count selection buttons', () => {
+ cy.visit('/app/todo/view/report');
+
+ cy.get('.list-paging-area .list-count').should('contain.text', '20 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '40 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '60 of');
+
+ cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click();
+
+ cy.get('.list-paging-area .list-count').should('contain.text', '100 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '200 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
+
+ // check if refresh works after load more
+ cy.get('.page-head .standard-actions [data-original-title="Refresh"]').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
+
+ cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click();
+
+ cy.get('.list-paging-area .list-count').should('contain.text', '500 of');
+ });
+});
diff --git a/cypress/integration/number_card.js b/cypress/integration/number_card.js
new file mode 100644
index 0000000000..a01ff1152d
--- /dev/null
+++ b/cypress/integration/number_card.js
@@ -0,0 +1,22 @@
+context('Number Card', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app/website');
+ });
+
+ it('Check filter populate for child table doctype', () => {
+ cy.visit('/app/number-card/new-number-card-1');
+ cy.get('[data-fieldname="parent_document_type"]').should('have.css', 'display', 'none');
+
+ cy.get_field('document_type', 'Link');
+ cy.fill_field('document_type', 'Workspace Link', 'Link').focus().blur();
+ cy.get_field('document_type', 'Link').should('have.value', 'Workspace Link');
+
+ cy.fill_field('label', 'Test Number Card', 'Data');
+
+ cy.get('[data-fieldname="filters_json"]').click().wait(200);
+ cy.get('.modal-body .filter-action-buttons .add-filter').click();
+ cy.get('.modal-body .fieldname-select-area').click();
+ cy.get('.modal-actions .btn-modal-close').click();
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js
index 6e3a28bbfc..bacbf9c172 100644
--- a/cypress/integration/report_view.js
+++ b/cypress/integration/report_view.js
@@ -13,9 +13,6 @@ context('Report View', () => {
'enabled': 0,
'docstatus': 1 // submit document
}, true);
- return cy.window().its('frappe').then(frappe => {
- return frappe.call("frappe.tests.ui_test_helpers.create_multiple_contact_records");
- });
});
it('Field with enabled allow_on_submit should be editable.', () => {
@@ -43,32 +40,4 @@ context('Report View', () => {
expect(r.message.enabled).to.equals(1);
});
});
-
- it('test load more with count selection buttons', () => {
- cy.visit('/app/contact/view/report');
-
- cy.get('.list-paging-area .list-count').should('contain.text', '20 of');
- cy.get('.list-paging-area .btn-more').click();
- cy.get('.list-paging-area .list-count').should('contain.text', '40 of');
- cy.get('.list-paging-area .btn-more').click();
- cy.get('.list-paging-area .list-count').should('contain.text', '60 of');
-
- cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click();
-
- cy.get('.list-paging-area .list-count').should('contain.text', '100 of');
- cy.get('.list-paging-area .btn-more').click();
- cy.get('.list-paging-area .list-count').should('contain.text', '200 of');
- cy.get('.list-paging-area .btn-more').click();
- cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
-
- // check if refresh works after load more
- cy.get('.page-head .standard-actions [data-original-title="Refresh"]').click();
- cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
-
- cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click();
-
- cy.get('.list-paging-area .list-count').should('contain.text', '500 of');
- cy.get('.list-paging-area .btn-more').click();
- cy.get('.list-paging-area .list-count').should('contain.text', '1000 of');
- });
});
diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js
index 792cb56198..43c01e88fb 100644
--- a/esbuild/esbuild.js
+++ b/esbuild/esbuild.js
@@ -9,7 +9,7 @@ const cliui = require("cliui")();
const chalk = require("chalk");
const html_plugin = require("./frappe-html");
const rtlcss = require('rtlcss');
-const postCssPlugin = require("esbuild-plugin-postcss2").default;
+const postCssPlugin = require("@frappe/esbuild-plugin-postcss2").default;
const ignore_assets = require("./ignore-assets");
const sass_options = require("./sass_options");
const build_cleanup_plugin = require("./build-cleanup");
diff --git a/esbuild/frappe-html.js b/esbuild/frappe-html.js
index 8c4b7ca3d7..9a7edb144d 100644
--- a/esbuild/frappe-html.js
+++ b/esbuild/frappe-html.js
@@ -20,7 +20,8 @@ module.exports = {
.then(content => {
content = scrub_html_template(content);
return {
- contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n`
+ contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n`,
+ watchFiles: [filepath]
};
})
.catch(() => {
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 8a8b70afe3..93fc3b71b9 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -850,8 +850,7 @@ def set_value(doctype, docname, fieldname, value=None):
return frappe.client.set_value(doctype, docname, fieldname, value)
def get_cached_doc(*args, **kwargs):
- if args and len(args) > 1 and isinstance(args[1], str):
- key = get_document_cache_key(args[0], args[1])
+ if key := can_cache_doc(args):
# local cache
doc = local.document_cache.get(key)
if doc:
@@ -869,8 +868,24 @@ def get_cached_doc(*args, **kwargs):
return doc
+def can_cache_doc(args):
+ """
+ Determine if document should be cached based on get_doc params.
+ Returns cache key if doc can be cached, None otherwise.
+ """
+
+ if not args:
+ return
+
+ doctype = args[0]
+ name = doctype if len(args) == 1 else args[1]
+
+ # Only cache if both doctype and name are strings
+ if isinstance(doctype, str) and isinstance(name, str):
+ return get_document_cache_key(doctype, name)
+
def get_document_cache_key(doctype, name):
- return '{0}::{1}'.format(doctype, name)
+ return f'{doctype}::{name}'
def clear_document_cache(doctype, name):
cache().hdel("last_modified", doctype)
@@ -911,8 +926,7 @@ def get_doc(*args, **kwargs):
doc = frappe.model.document.get_doc(*args, **kwargs)
# set in cache
- if args and len(args) > 1:
- key = get_document_cache_key(args[0], args[1])
+ if key := can_cache_doc(args):
local.document_cache[key] = doc
cache().hset('document_cache', key, doc.as_dict())
diff --git a/frappe/boot.py b/frappe/boot.py
index 524913059c..b5008f778a 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -325,6 +325,7 @@ def get_desk_settings():
def get_notification_settings():
return frappe.get_cached_doc('Notification Settings', frappe.session.user)
+@frappe.whitelist()
def get_link_title_doctypes():
dts = frappe.get_all("DocType", {"show_title_field_in_link": 1})
custom_dts = frappe.get_all(
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
old mode 100755
new mode 100644
index c5d2257d75..b54f369e34
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -1,7 +1,7 @@
# imports - standard imports
import os
-import sys
import shutil
+import sys
# imports - third party imports
import click
@@ -65,11 +65,11 @@ def restore(context, sql_file_path, encryption_key=None, db_root_username=None,
"Restore site database from an sql file"
from frappe.installer import (
_new_site,
- extract_sql_from_archive,
extract_files,
+ extract_sql_from_archive,
is_downgrade,
is_partial,
- validate_database_sql
+ validate_database_sql,
)
from frappe.utils.backups import Backup
if not os.path.exists(sql_file_path):
@@ -207,7 +207,7 @@ def restore(context, sql_file_path, encryption_key=None, db_root_username=None,
@click.option('--encryption-key', help='Backup encryption key')
@pass_context
def partial_restore(context, sql_file_path, verbose, encryption_key=None):
- from frappe.installer import partial_restore, extract_sql_from_archive
+ from frappe.installer import extract_sql_from_archive, partial_restore
from frappe.utils.backups import Backup
if not os.path.exists(sql_file_path):
@@ -545,7 +545,7 @@ def _use(site, sites_path='.'):
def use(site, sites_path='.'):
if os.path.exists(os.path.join(sites_path, site)):
- with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile:
+ with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile:
sitefile.write(site)
print("Current Site set to {}".format(site))
else:
@@ -751,6 +751,7 @@ def set_admin_password(context, admin_password=None, logout_all_sessions=False):
def set_user_password(site, user, password, logout_all_sessions=False):
import getpass
+
from frappe.utils.password import update_password
try:
@@ -881,15 +882,16 @@ def stop_recording(context):
raise SiteNotSpecifiedError
@click.command('ngrok')
+@click.option('--bind-tls', is_flag=True, default=False, help='Returns a reference to the https tunnel.')
@pass_context
-def start_ngrok(context):
+def start_ngrok(context, bind_tls):
from pyngrok import ngrok
site = get_site(context)
frappe.init(site=site)
port = frappe.conf.http_port or frappe.conf.webserver_port
- tunnel = ngrok.connect(addr=str(port), host_header=site)
+ tunnel = ngrok.connect(addr=str(port), host_header=site, bind_tls=bind_tls)
print(f'Public URL: {tunnel.public_url}')
print('Inspect logs at http://localhost:4040')
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index f89f0d8765..475762f39d 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -18,6 +18,7 @@ from urllib.parse import unquote
from frappe.utils.user import is_system_user
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule
+from parse import compile
exclude_from_linked_with = True
@@ -114,6 +115,44 @@ class Communication(Document, CommunicationEmailMixin):
frappe.publish_realtime('new_message', self.as_dict(),
user=self.reference_name, after_commit=True)
+ def set_signature_in_email_content(self):
+ """Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email
+ """
+ if not self.content:
+ return
+
+ quill_parser = compile('
{}
')
+ email_body = quill_parser.parse(self.content)
+
+ if not email_body:
+ return
+
+ email_body = email_body[0]
+
+ user_email_signature = frappe.db.get_value(
+ "User",
+ self.sender,
+ "email_signature",
+ ) if self.sender else None
+
+ signature = user_email_signature or frappe.db.get_value(
+ "Email Account",
+ {"default_outgoing": 1, "add_signature": 1},
+ "signature",
+ )
+
+ if not signature:
+ return
+
+ _signature = quill_parser.parse(signature)[0] if "ql-editor" in signature else None
+
+ if (_signature or signature) not in self.content:
+ self.content = f'{self.content}
{signature}'
+
+ def before_save(self):
+ if not self.flags.skip_add_signature:
+ self.set_signature_in_email_content()
+
def on_update(self):
# add to _comment property of the doctype, so it shows up in
# comments count for the list view
diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py
index 46ef7bf5d2..b51749ccb7 100755
--- a/frappe/core/doctype/communication/email.py
+++ b/frappe/core/doctype/communication/email.py
@@ -22,12 +22,30 @@ OUTGOING_EMAIL_ACCOUNT_MISSING = _("""
@frappe.whitelist()
-def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
- sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False,
- print_html=None, print_format=None, attachments='[]', send_me_a_copy=False, cc=None, bcc=None,
- flags=None, read_receipt=None, print_letterhead=True, email_template=None, communication_type=None,
- ignore_permissions=False) -> Dict[str, str]:
- """Make a new communication.
+def make(
+ doctype=None,
+ name=None,
+ content=None,
+ subject=None,
+ sent_or_received="Sent",
+ sender=None,
+ sender_full_name=None,
+ recipients=None,
+ communication_medium="Email",
+ send_email=False,
+ print_html=None,
+ print_format=None,
+ attachments="[]",
+ send_me_a_copy=False,
+ cc=None,
+ bcc=None,
+ read_receipt=None,
+ print_letterhead=True,
+ email_template=None,
+ communication_type=None,
+ **kwargs,
+) -> Dict[str, str]:
+ """Make a new communication. Checks for email permissions for specified Document.
:param doctype: Reference DocType.
:param name: Reference Document name.
@@ -44,17 +62,71 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
:param send_me_a_copy: Send a copy to the sender (default **False**).
:param email_template: Template which is used to compose mail .
"""
- is_error_report = (doctype=="User" and name==frappe.session.user and subject=="Error Report")
- send_me_a_copy = cint(send_me_a_copy)
+ if kwargs:
+ from frappe.utils.commands import warn
+ warn(
+ f"Options {kwargs} used in frappe.core.doctype.communication.email.make "
+ "are deprecated or unsupported",
+ category=DeprecationWarning
+ )
+
+ if doctype and name and not frappe.has_permission(doctype=doctype, ptype="email", doc=name):
+ raise frappe.PermissionError(
+ f"You are not allowed to send emails related to: {doctype} {name}"
+ )
+
+ return _make(
+ doctype=doctype,
+ name=name,
+ content=content,
+ subject=subject,
+ sent_or_received=sent_or_received,
+ sender=sender,
+ sender_full_name=sender_full_name,
+ recipients=recipients,
+ communication_medium=communication_medium,
+ send_email=send_email,
+ print_html=print_html,
+ print_format=print_format,
+ attachments=attachments,
+ send_me_a_copy=cint(send_me_a_copy),
+ cc=cc,
+ bcc=bcc,
+ read_receipt=read_receipt,
+ print_letterhead=print_letterhead,
+ email_template=email_template,
+ communication_type=communication_type,
+ add_signature=False,
+ )
- if not ignore_permissions:
- if doctype and name and not is_error_report and not frappe.has_permission(doctype, "email", name) and not (flags or {}).get('ignore_doctype_permissions'):
- raise frappe.PermissionError("You are not allowed to send emails related to: {doctype} {name}".format(
- doctype=doctype, name=name))
- if not sender:
- sender = get_formatted_email(frappe.session.user)
+def _make(
+ doctype=None,
+ name=None,
+ content=None,
+ subject=None,
+ sent_or_received="Sent",
+ sender=None,
+ sender_full_name=None,
+ recipients=None,
+ communication_medium="Email",
+ send_email=False,
+ print_html=None,
+ print_format=None,
+ attachments="[]",
+ send_me_a_copy=False,
+ cc=None,
+ bcc=None,
+ read_receipt=None,
+ print_letterhead=True,
+ email_template=None,
+ communication_type=None,
+ add_signature=True,
+) -> Dict[str, str]:
+ """Internal method to make a new communication that ignores Permission checks.
+ """
+ sender = sender or get_formatted_email(frappe.session.user)
recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients
cc = list_to_str(cc) if isinstance(cc, list) else cc
bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc
@@ -77,7 +149,9 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
"read_receipt":read_receipt,
"has_attachment": 1 if attachments else 0,
"communication_type": communication_type,
- }).insert(ignore_permissions=True)
+ })
+ comm.flags.skip_add_signature = not add_signature
+ comm.insert(ignore_permissions=True)
# if not committed, delayed task doesn't find the communication
if attachments:
@@ -87,17 +161,21 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
if cint(send_email):
if not comm.get_outgoing_email_account():
- frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError)
+ frappe.throw(
+ msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError
+ )
- comm.send_email(print_html=print_html, print_format=print_format,
- send_me_a_copy=send_me_a_copy, print_letterhead=print_letterhead)
+ comm.send_email(
+ print_html=print_html,
+ print_format=print_format,
+ send_me_a_copy=send_me_a_copy,
+ print_letterhead=print_letterhead,
+ )
emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy)
- return {
- "name": comm.name,
- "emails_not_sent_to": ", ".join(emails_not_sent_to)
- }
+ return {"name": comm.name, "emails_not_sent_to": ", ".join(emails_not_sent_to)}
+
def validate_email(doc: "Communication") -> None:
"""Validate Email Addresses of Recipients and CC"""
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index dca0a05281..5f82abac1f 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -732,9 +732,12 @@ class DocType(Document):
frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError)
# a DocType's name should not start with a number or underscore
- # and should only contain letters, numbers and underscore
- if not re.match(r"^(?![\W])[^\d_\s][\w ]+$", name, **flags):
- frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError)
+ # and should only contain letters, numbers, underscore, and hyphen
+ if not re.match(r"^(?![\W])[^\d_\s][\w -]+$", name, **flags):
+ frappe.throw(_(
+ "A DocType's name should start with a letter and can only "
+ "consist of letters, numbers, spaces, underscores and hyphens"
+ ), frappe.NameError, title="Invalid Name")
validate_route_conflict(self.doctype, self.name)
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 9b4f733e7d..cb22f581c6 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -24,7 +24,7 @@ class TestDocType(unittest.TestCase):
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert)
self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert)
self.assertRaises(frappe.NameError, new_doctype("Some Doctype with a name whose length is more than 61 characters").insert)
- for name in ("Some DocType", "Some_DocType"):
+ for name in ("Some DocType", "Some_DocType", "Some-DocType"):
if frappe.db.exists("DocType", name):
frappe.delete_doc("DocType", name)
diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py
index 389e18dd4c..f955c29462 100644
--- a/frappe/core/doctype/role/role.py
+++ b/frappe/core/doctype/role/role.py
@@ -61,7 +61,7 @@ class Role(Document):
def get_info_based_on_role(role, field='email'):
''' Get information of all users that have been assigned this role '''
- users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"},
+ users = frappe.get_list("Has Role", filters={"role": role}, parent_doctype="User",
fields=["parent as user_name"])
return get_user_info(users, field)
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index a47f539466..9e9529cd5e 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -668,8 +668,7 @@
"link_fieldname": "user"
}
],
- "max_attachments": 5,
- "modified": "2022-01-03 11:53:25.250822",
+ "modified": "2022-03-09 01:47:56.745069",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
diff --git a/frappe/database/database.py b/frappe/database/database.py
index dc9f20d8c2..a4eca64d8d 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -181,7 +181,7 @@ class Database(object):
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)):
+ if ignore_ddl and (self.is_missing_column(e) or self.is_table_missing(e) or self.cant_drop_field_or_key(e)):
pass
else:
raise
@@ -1028,7 +1028,7 @@ class Database(object):
return []
def is_missing_table_or_column(self, e):
- return self.is_missing_column(e) or self.is_missing_table(e)
+ return self.is_missing_column(e) or self.is_table_missing(e)
def multisql(self, sql_dict, values=(), **kwargs):
current_dialect = frappe.db.db_type or 'mariadb'
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index b5971e236e..a6d5e7b3f2 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -154,6 +154,10 @@ class MariaDBDatabase(Database):
def is_table_missing(e):
return e.args[0] == ER.NO_SUCH_TABLE
+ @staticmethod
+ def is_missing_table(e):
+ return MariaDBDatabase.is_table_missing(e)
+
@staticmethod
def is_missing_column(e):
return e.args[0] == ER.BAD_FIELD_ERROR
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index b0793fcbf0..a20ffe17a5 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -99,16 +99,8 @@ class PostgresDatabase(Database):
return db_size[0].get('database_size')
# pylint: disable=W0221
- def sql(self, *args, **kwargs):
- if args:
- # since tuple is immutable
- args = list(args)
- args[0] = modify_query(args[0])
- args = tuple(args)
- elif kwargs.get('query'):
- kwargs['query'] = modify_query(kwargs.get('query'))
-
- return super(PostgresDatabase, self).sql(*args, **kwargs)
+ def sql(self, query, *args, **kwargs):
+ return super(PostgresDatabase, self).sql(modify_query(query), *args, **kwargs)
def get_tables(self, cached=True):
return [d[0] for d in self.sql("""select table_name
@@ -153,6 +145,10 @@ class PostgresDatabase(Database):
def is_table_missing(e):
return getattr(e, 'pgcode', None) == '42P01'
+ @staticmethod
+ def is_missing_table(e):
+ return PostgresDatabase.is_table_missing(e)
+
@staticmethod
def is_missing_column(e):
return getattr(e, 'pgcode', None) == '42703'
@@ -335,7 +331,7 @@ def modify_query(query):
query = replace_locate_with_strpos(query)
# select from requires ""
if re.search('from tab', query, flags=re.IGNORECASE):
- query = re.sub('from tab([a-zA-Z]*)', r'from "tab\1"', query, flags=re.IGNORECASE)
+ query = re.sub(r'from tab([\w-]*)', r'from "tab\1"', query, flags=re.IGNORECASE)
return query
diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js
index 6d1454a2cb..f548388a99 100644
--- a/frappe/desk/doctype/number_card/number_card.js
+++ b/frappe/desk/doctype/number_card/number_card.js
@@ -28,6 +28,7 @@ frappe.ui.form.on('Number Card', {
frm.trigger('render_filters_table');
}
frm.trigger('create_add_to_dashboard_button');
+ frm.trigger('set_parent_document_type');
},
create_add_to_dashboard_button: function(frm) {
@@ -141,7 +142,9 @@ frappe.ui.form.on('Number Card', {
frm.set_value('filters_json', '[]');
frm.set_value('dynamic_filters_json', '[]');
frm.set_value('aggregate_function_based_on', '');
+ frm.set_value('parent_document_type', '');
frm.trigger('set_options');
+ frm.trigger('set_parent_document_type');
},
set_options: function(frm) {
@@ -317,6 +320,7 @@ frappe.ui.form.on('Number Card', {
frm.filter_group = new frappe.ui.FilterGroup({
parent: dialog.get_field('filter_area').$wrapper,
doctype: frm.doc.document_type,
+ parent_doctype: frm.doc.parent_document_type,
on_change: () => {},
});
filters && frm.filter_group.add_filters_to_filter_group(filters);
@@ -436,6 +440,36 @@ frappe.ui.form.on('Number Card', {
frm.dynamic_filter_table.find('tbody').html(filter_rows);
}
+ },
+
+ set_parent_document_type: async function(frm) {
+ let document_type = frm.doc.document_type;
+ let doc_is_table = document_type &&
+ (await frappe.db.get_value('DocType', document_type, 'istable')).message.istable;
+
+ frm.set_df_property('parent_document_type', 'hidden', !doc_is_table);
+
+ if (document_type && doc_is_table) {
+ let parent = await frappe.db.get_list('DocField', {
+ filters: {
+ 'fieldtype': 'Table',
+ 'options': document_type
+ },
+ fields: ['parent']
+ });
+
+ parent && frm.set_query('parent_document_type', function() {
+ return {
+ filters: {
+ "name": ['in', parent.map(({ parent }) => parent)]
+ }
+ };
+ });
+
+ if (parent.length === 1) {
+ frm.set_value('parent_document_type', parent[0].parent);
+ }
+ }
}
});
diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json
index d3e9598eb7..7975d878ba 100644
--- a/frappe/desk/doctype/number_card/number_card.json
+++ b/frappe/desk/doctype/number_card/number_card.json
@@ -16,6 +16,7 @@
"aggregate_function_based_on",
"column_break_2",
"document_type",
+ "parent_document_type",
"report_field",
"report_function",
"is_public",
@@ -188,10 +189,17 @@
"label": "Function",
"mandatory_depends_on": "eval: doc.type == 'Report'",
"options": "Sum\nAverage\nMinimum\nMaximum"
+ },
+ {
+ "description": "The document type selected is a child table, so the parent document type is required.",
+ "fieldname": "parent_document_type",
+ "fieldtype": "Link",
+ "label": "Parent Document Type",
+ "options": "DocType"
}
],
"links": [],
- "modified": "2020-07-23 11:11:03.391719",
+ "modified": "2022-03-10 15:34:38.210910",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",
@@ -234,6 +242,7 @@
"search_fields": "label, document_type",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "label",
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index 5662523a9d..784f46bb19 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -3,6 +3,7 @@
# License: MIT. See LICENSE
import frappe
+from frappe import _
from frappe.model.document import Document
from frappe.utils import cint
from frappe.model.naming import append_number_if_name_exists
@@ -17,6 +18,13 @@ class NumberCard(Document):
if frappe.db.exists("Number Card", self.name):
self.name = append_number_if_name_exists('Number Card', self.name)
+ def validate(self):
+ if not self.document_type:
+ frappe.throw(_("Document type is required to create a number card"))
+
+ if self.document_type and frappe.get_meta(self.document_type).istable and not self.parent_document_type:
+ frappe.throw(_("Parent document type is required to create a number card"))
+
def on_update(self):
if frappe.conf.developer_mode and self.is_standard:
export_to_files(record_list=[['Number Card', self.name]], record_module=self.module)
diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js
index fc83069fd2..7751ffe860 100644
--- a/frappe/desk/doctype/system_console/system_console.js
+++ b/frappe/desk/doctype/system_console/system_console.js
@@ -88,15 +88,16 @@ frappe.ui.form.on('System Console', {
${row.Progress} |
`
}
+
frm.get_field('processlist').html(`
Requested on: ${timestamp}
- Id
+ | Id
| Time
| State
| Info
- | Progress
+ | Progress / Wait Event
|
${rows}`);
});
diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py
index 107ab2f932..bf0925e2d7 100644
--- a/frappe/desk/doctype/system_console/system_console.py
+++ b/frappe/desk/doctype/system_console/system_console.py
@@ -41,4 +41,14 @@ def execute_code(doc):
@frappe.whitelist()
def show_processlist():
frappe.only_for('System Manager')
- return frappe.db.sql('show full processlist', as_dict=1)
+
+ return frappe.db.multisql({
+ "postgres": """
+ SELECT pid AS "Id",
+ query_start AS "Time",
+ state AS "State",
+ query AS "Info",
+ wait_event AS "Progress"
+ FROM pg_stat_activity""",
+ "mariadb": "show full processlist"
+ }, as_dict=True)
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index f0a3531ae4..ba3319b591 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -277,6 +277,7 @@ def sort_page(workspace_pages, pages):
doc = frappe.get_doc('Workspace', page.name)
doc.sequence_id = seq + 1
doc.parent_page = d.get('parent_page') or ""
+ doc.flags.ignore_links = True
doc.save(ignore_permissions=True)
break
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index 572d3f2a94..010d65c95b 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -1,9 +1,10 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
+
import json
from collections import defaultdict
import itertools
-from typing import List
+from typing import Dict, List, Optional
import frappe
import frappe.desk.form.load
@@ -367,7 +368,7 @@ def get_exempted_doctypes():
@frappe.whitelist()
-def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
+def get_linked_docs(doctype: str, name: str, linkinfo: Optional[Dict] = None) -> Dict[str, List]:
if isinstance(linkinfo, str):
# additional fields are added in linkinfo
linkinfo = json.loads(linkinfo)
@@ -377,23 +378,21 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
if not linkinfo:
return results
- if for_doctype:
- links = frappe.get_doc(doctype, name).get_link_filters(for_doctype)
-
- if links:
- linkinfo = links
-
- if for_doctype in linkinfo:
- # only get linked with for this particular doctype
- linkinfo = { for_doctype: linkinfo.get(for_doctype) }
- else:
- return results
-
for dt, link in linkinfo.items():
filters = []
link["doctype"] = dt
- link_meta_bundle = frappe.desk.form.load.get_meta_bundle(dt)
+ try:
+ link_meta_bundle = frappe.desk.form.load.get_meta_bundle(dt)
+ except Exception as e:
+ if isinstance(e, frappe.DoesNotExistError):
+ if frappe.local.message_log:
+ frappe.local.message_log.pop()
+ continue
linkmeta = link_meta_bundle[0]
+
+ if not linkmeta.has_permission():
+ continue
+
if not linkmeta.get("issingle"):
fields = [d.fieldname for d in linkmeta.get("fields", {
"in_list_view": 1,
@@ -456,6 +455,13 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
return results
+
+@frappe.whitelist()
+def get(doctype, docname):
+ linked_doctypes = get_linked_doctypes(doctype=doctype)
+ return get_linked_docs(doctype=doctype, name=docname, linkinfo=linked_doctypes)
+
+
@frappe.whitelist()
def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):
"""add list of doctypes this doctype is 'linked' with.
@@ -470,6 +476,7 @@ def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):
else:
return frappe.cache().hget("linked_doctypes", doctype, lambda: _get_linked_doctypes(doctype))
+
def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):
ret = {}
# find fields where this doctype is linked
@@ -499,6 +506,7 @@ def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False)
return ret
+
def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False):
filters = [['fieldtype','=', 'Link'], ['options', '=', doctype]]
@@ -529,6 +537,7 @@ def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False):
return ret
+
def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=False):
ret = {}
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index b5dfacb1d6..dba84e5175 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -124,7 +124,6 @@ def get_docinfo(doc=None, doctype=None, name=None):
update_user_info(docinfo)
frappe.response["docinfo"] = docinfo
- return docinfo
def add_comments(doc, docinfo):
# divide comments into separate lists
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index b344763916..f5f50b14fe 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -352,14 +352,10 @@ def export_query():
)
return
- columns = get_columns_dict(data.columns)
-
from frappe.utils.xlsxutils import make_xlsx
- data["result"] = handle_duration_fieldtype_values(
- data.get("result"), data.get("columns")
- )
- xlsx_data, column_widths = build_xlsx_data(columns, data, visible_idx, include_indentation)
+ format_duration_fields(data)
+ xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths)
frappe.response["filename"] = report_name + ".xlsx"
@@ -367,39 +363,18 @@ def export_query():
frappe.response["type"] = "binary"
-def handle_duration_fieldtype_values(result, columns):
- for i, col in enumerate(columns):
- fieldtype = None
- if isinstance(col, str):
- col = col.split(":")
- if len(col) > 1:
- if col[1]:
- fieldtype = col[1]
- if "/" in fieldtype:
- fieldtype, options = fieldtype.split("/")
- else:
- fieldtype = "Data"
- else:
- fieldtype = col.get("fieldtype")
-
- if fieldtype == "Duration":
- for entry in range(0, len(result)):
- row = result[entry]
- if isinstance(row, dict):
- val_in_seconds = row[col.fieldname]
- if val_in_seconds:
- duration_val = format_duration(val_in_seconds)
- row[col.fieldname] = duration_val
- else:
- val_in_seconds = row[i]
- if val_in_seconds:
- duration_val = format_duration(val_in_seconds)
- row[i] = duration_val
+def format_duration_fields(data: frappe._dict) -> None:
+ for i, col in enumerate(data.columns):
+ if col.get("fieldtype") != "Duration":
+ continue
- return result
+ for row in data.result:
+ index = col.fieldname if isinstance(row, dict) else i
+ if row[index]:
+ row[index] = format_duration(row[index])
-def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visible_idx=False):
+def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=False):
result = [[]]
column_widths = []
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py
index 682f0df7cf..5ffde0c37b 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -104,7 +104,7 @@ class AutoEmailReport(Document):
report_data['columns'] = columns
report_data['result'] = data
- xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
+ xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True)
xlsx_file = make_xlsx(xlsx_data, "Auto Email Report", column_widths=column_widths)
return xlsx_file.getvalue()
@@ -113,7 +113,7 @@ class AutoEmailReport(Document):
report_data['columns'] = columns
report_data['result'] = data
- xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
+ xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True)
return to_csv(xlsx_data)
else:
diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json
index baabd4991e..b42f4755cb 100644
--- a/frappe/email/doctype/newsletter/newsletter.json
+++ b/frappe/email/doctype/newsletter/newsletter.json
@@ -236,8 +236,7 @@
"index_web_pages_for_search": 1,
"is_published_field": "published",
"links": [],
- "max_attachments": 3,
- "modified": "2021-12-06 20:09:37.963141",
+ "modified": "2022-03-09 01:48:16.741603",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index 2b62530847..bad32fb68f 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -186,7 +186,7 @@ def get_context(context):
def send_an_email(self, doc, context):
from email.utils import formataddr
- from frappe.core.doctype.communication.email import make as make_communication
+ from frappe.core.doctype.communication.email import _make as make_communication
subject = self.subject
if "{" in subject:
subject = frappe.render_template(self.subject, context)
@@ -216,7 +216,8 @@ def get_context(context):
# Add mail notification to communication list
# No need to add if it is already a communication.
if doc.doctype != 'Communication':
- make_communication(doctype=doc.doctype,
+ make_communication(
+ doctype=doc.doctype,
name=doc.name,
content=message,
subject=subject,
@@ -228,7 +229,7 @@ def get_context(context):
cc=cc,
bcc=bcc,
communication_type='Automated Message',
- ignore_permissions=True)
+ )
def send_a_slack_msg(self, doc, context):
send_slack_message(
diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py
index c25e996bd3..0f45e42aac 100755
--- a/frappe/email/email_body.py
+++ b/frappe/email/email_body.py
@@ -259,17 +259,12 @@ def get_formatted_html(subject, message, footer=None, print_html=None,
email_account = email_account or EmailAccount.find_outgoing(match_by_email=sender)
- signature = None
- if "" not in message:
- signature = get_signature(email_account)
-
rendered_email = frappe.get_template("templates/emails/standard.html").render({
"brand_logo": get_brand_logo(email_account) if with_container or header else None,
"with_container": with_container,
"site_url": get_url(),
"header": get_header(header),
"content": message,
- "signature": signature,
"footer": get_footer(email_account, footer),
"title": subject,
"print_html": print_html,
@@ -281,8 +276,7 @@ def get_formatted_html(subject, message, footer=None, print_html=None,
if unsubscribe_link:
html = html.replace("", unsubscribe_link.html)
- html = inline_style_in_html(html)
- return html
+ return inline_style_in_html(html)
@frappe.whitelist()
def get_email_html(template, args, subject, header=None, with_container=False):
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index faa3859c91..b4a53e3131 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -43,8 +43,8 @@ def update_document_title(
title_field = doc.meta.get_title_field()
- title_updated = (title_field != "name") and (updated_title != doc.get(title_field))
- name_updated = updated_name != doc.name
+ title_updated = updated_title and (title_field != "name") and (updated_title != doc.get(title_field))
+ name_updated = updated_name and (updated_name != doc.name)
if name_updated:
docname = rename_doc(doctype=doctype, old=docname, new=updated_name, merge=merge)
diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py
index 92e7523e6d..f9c7b55a99 100644
--- a/frappe/modules/import_file.py
+++ b/frappe/modules/import_file.py
@@ -11,7 +11,7 @@ from frappe.query_builder import DocType
from frappe.utils import get_datetime, now
-def caclulate_hash(path: str) -> str:
+def calculate_hash(path: str) -> str:
"""Calculate md5 hash of the file in binary mode
Args:
@@ -99,7 +99,7 @@ def import_file_by_path(path: str,force: bool = False,data_import: bool = False,
print(f"{path} missing")
return
- calculated_hash = caclulate_hash(path)
+ calculated_hash = calculate_hash(path)
if docs:
if not isinstance(docs, list):
diff --git a/frappe/public/css/tree.css b/frappe/public/css/tree.css
index 2aa411bc11..8b216bc321 100644
--- a/frappe/public/css/tree.css
+++ b/frappe/public/css/tree.css
@@ -24,7 +24,7 @@ ul.tree-children {
}
.tree-link .node-parent,
.tree-link .node-leaf {
- margin-right: 5px;
+ margin-right: 8px;
}
.tree-link.active i {
color: #5e64ff;
diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js
index 4e66ed6642..a509af4121 100644
--- a/frappe/public/js/frappe/form/controls/autocomplete.js
+++ b/frappe/public/js/frappe/form/controls/autocomplete.js
@@ -166,6 +166,9 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
}
parse_options(options) {
+ if (typeof options === 'string' && options[0] === '[') {
+ options = frappe.utils.parse_json(options);
+ }
if (typeof options === 'string') {
options = options.split('\n');
}
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index ea90387922..e620caa244 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -501,9 +501,9 @@ export default class Grid {
}
set_column_disp(fieldname, show) {
- if ($.isArray(fieldname)) {
+ if (Array.isArray(fieldname)) {
for (let field of fieldname) {
- this.update_docfield_property(field, "hidden", show);
+ this.update_docfield_property(field, "hidden", show ? 0 : 1);
this.set_editable_grid_column_disp(field, show);
}
} else {
diff --git a/frappe/public/js/frappe/form/linked_with.js b/frappe/public/js/frappe/form/linked_with.js
index 20db7bdb7c..c47a6e0c86 100644
--- a/frappe/public/js/frappe/form/linked_with.js
+++ b/frappe/public/js/frappe/form/linked_with.js
@@ -1,9 +1,8 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// MIT License. See license.txt
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+// MIT License. See LICENSE
frappe.ui.form.LinkedWith = class LinkedWith {
-
constructor(opts) {
$.extend(this, opts);
}
@@ -21,29 +20,23 @@ frappe.ui.form.LinkedWith = class LinkedWith {
}
make_dialog() {
-
this.dialog = new frappe.ui.Dialog({
title: __("Linked With")
});
this.dialog.on_page_show = () => {
- // execute ajax calls sequentially
- // 1. get linked doctypes
- // 2. load all doctypes
- // 3. load linked docs
- this.get_linked_doctypes()
- .then(() => this.load_doctypes())
- .then(() => this.links_not_permitted_or_missing())
- .then(() => this.get_linked_docs())
- .then(() => this.make_html());
+ frappe.xcall(
+ "frappe.desk.form.linked_with.get",
+ {"doctype": cur_frm.doctype, "docname": cur_frm.docname},
+ ).then(r => {
+ this.frm.__linked_docs = r;
+ }).then(() => this.make_html());
};
}
make_html() {
- const linked_docs = this.frm.__linked_docs;
-
let html = '';
-
+ const linked_docs = this.frm.__linked_docs;
const linked_doctypes = Object.keys(linked_docs);
if (linked_doctypes.length === 0) {
@@ -63,88 +56,6 @@ frappe.ui.form.LinkedWith = class LinkedWith {
$(this.dialog.body).html(html);
}
- load_doctypes() {
- const already_loaded = Object.keys(locals.DocType);
- let doctypes_to_load = [];
-
- if (this.frm.__linked_doctypes) {
- doctypes_to_load =
- Object.keys(this.frm.__linked_doctypes)
- .filter(doctype => !already_loaded.includes(doctype));
- }
-
- // load all doctypes asynchronously using with_doctype
- const promises = doctypes_to_load.map(dt => {
- return frappe.model.with_doctype(dt, () => {
- if(frappe.listview_settings[dt]) {
- // add additional fields to __linked_doctypes
- this.frm.__linked_doctypes[dt].add_fields =
- frappe.listview_settings[dt].add_fields;
- }
- });
- });
-
- return Promise.all(promises);
- }
-
- links_not_permitted_or_missing() {
- let links = null;
-
- if (this.frm.__linked_doctypes) {
- links =
- Object.keys(this.frm.__linked_doctypes)
- .filter(frappe.model.can_get_report);
- }
-
- let flag;
- if(!links) {
- $(this.dialog.body).html(`${this.frm.__linked_doctypes
- ? __("Not enough permission to see links")
- : __("Not Linked to any record")}`);
- flag = true;
- }
- flag = false;
-
- // reject Promise if not_permitted or missing
- return new Promise(
- (resolve, reject) => flag ? reject() : resolve()
- );
- }
-
- get_linked_doctypes() {
- return new Promise((resolve) => {
- if (this.frm.__linked_doctypes) {
- resolve();
- }
-
- frappe.call({
- method: "frappe.desk.form.linked_with.get_linked_doctypes",
- args: {
- doctype: this.frm.doctype
- },
- callback: (r) => {
- this.frm.__linked_doctypes = r.message;
- resolve();
- }
- });
- });
- }
-
- get_linked_docs() {
- return frappe.call({
- method: "frappe.desk.form.linked_with.get_linked_docs",
- args: {
- doctype: this.frm.doctype,
- name: this.frm.docname,
- linkinfo: this.frm.__linked_doctypes,
- for_doctype: this.for_doctype
- },
- callback: (r) => {
- this.frm.__linked_docs = r.message || {};
- }
- });
- }
-
make_doc_head(heading) {
return `
diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js
index 538534e5cf..0713d5dc43 100644
--- a/frappe/public/js/frappe/form/sidebar/attachments.js
+++ b/frappe/public/js/frappe/form/sidebar/attachments.js
@@ -44,8 +44,17 @@ frappe.ui.form.Attachments = class Attachments {
// add attachment objects
var attachments = this.get_attachments();
if(attachments.length) {
- attachments.forEach(function(attachment) {
- me.add_attachment(attachment)
+ let exists = {};
+ let unique_attachments = attachments.filter(attachment => {
+ return Object.prototype.hasOwnProperty.call(
+ exists,
+ attachment.file_name
+ )
+ ? false
+ : (exists[attachment.file_name] = true);
+ });
+ unique_attachments.forEach(attachment => {
+ me.add_attachment(attachment);
});
} else {
this.attachments_label.removeClass("has-attachments");
@@ -75,7 +84,19 @@ frappe.ui.form.Attachments = class Attachments {
remove_action = function(target_id) {
frappe.confirm(__("Are you sure you want to delete the attachment?"),
function() {
- me.remove_attachment(target_id);
+ let target_attachment = me
+ .get_attachments()
+ .find(attachment => attachment.name === target_id);
+ let to_be_removed = me
+ .get_attachments()
+ .filter(
+ attachment =>
+ attachment.file_name ===
+ target_attachment.file_name
+ );
+ to_be_removed.forEach(attachment =>
+ me.remove_attachment(attachment.name)
+ );
}
);
return false;
diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js
index 75063cc53f..d5ee82acce 100644
--- a/frappe/public/js/frappe/list/base_list.js
+++ b/frappe/public/js/frappe/list/base_list.js
@@ -760,6 +760,10 @@ class FilterArea {
const doctype_fields = this.list_view.meta.fields;
const title_field = this.list_view.meta.title_field;
+ const has_existing_filters = (
+ this.list_view.filters
+ && this.list_view.filters.length > 0
+ );
fields = fields.concat(
doctype_fields
@@ -794,13 +798,17 @@ class FilterArea {
options = options.join("\n");
}
}
- let default_value =
- fieldtype === "Link"
- ? frappe.defaults.get_user_default(options)
- : null;
+
+ let default_value;
+
+ if (fieldtype === "Link" && !has_existing_filters) {
+ default_value = frappe.defaults.get_user_default(options);
+ }
+
if (["__default", "__global"].includes(default_value)) {
default_value = null;
}
+
return {
fieldtype: fieldtype,
label: __(df.label),
diff --git a/frappe/public/js/frappe/list/list_factory.js b/frappe/public/js/frappe/list/list_factory.js
index acad85fdcb..ef48af4937 100644
--- a/frappe/public/js/frappe/list/list_factory.js
+++ b/frappe/public/js/frappe/list/list_factory.js
@@ -6,8 +6,8 @@ frappe.provide('frappe.views.list_view');
window.cur_list = null;
frappe.views.ListFactory = class ListFactory extends frappe.views.Factory {
make (route) {
- var me = this;
- var doctype = route[1];
+ const me = this;
+ const doctype = route[1];
// List / Gantt / Kanban / etc
// File is a special view
@@ -21,60 +21,58 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory {
}
frappe.provide('frappe.views.list_view.' + doctype);
- const page_name = frappe.get_route_str();
-
- if (!frappe.views.list_view[page_name]) {
- frappe.views.list_view[page_name] = new view_class({
- doctype: doctype,
- parent: me.make_page(true, page_name)
- });
- } else {
- frappe.container.change_to(page_name);
- }
- me.set_cur_list();
+ frappe.views.list_view[me.page_name] = new view_class({
+ doctype: doctype,
+ parent: me.make_page(true, me.page_name)
+ });
+ me.set_cur_list();
}
- show() {
+ before_show() {
if (this.re_route_to_view()) {
- return;
+ return false;
}
+
this.set_module_breadcrumb();
- super.show();
+ }
+
+ on_show() {
this.set_cur_list();
- cur_list && cur_list.show();
+ if (cur_list) cur_list.show();
}
re_route_to_view() {
- var route = frappe.get_route();
- var doctype = route[1];
- var last_route = frappe.route_history.slice(-2)[0];
- if (route[0] === 'List' && route.length === 2 && frappe.views.list_view[doctype]) {
- if(last_route && last_route[0]==='List' && last_route[1]===doctype) {
- // last route same as this route, so going back.
- // this happens because /app/List/Item will redirect to /app/List/Item/List
- // while coming from back button, the last 2 routes will be same, so
- // we know user is coming in the reverse direction (via back button)
+ const doctype = this.route[1];
+ const last_route = frappe.route_history.slice(-2)[0];
+ if (
+ this.route[0] === 'List' &&
+ this.route.length === 2 &&
+ frappe.views.list_view[doctype] &&
+ last_route &&
+ last_route[0] === 'List' &&
+ last_route[1] === doctype
+ ) {
+ // last route same as this route, so going back.
+ // this happens because /app/List/Item will redirect to /app/List/Item/List
+ // while coming from back button, the last 2 routes will be same, so
+ // we know user is coming in the reverse direction (via back button)
- // example:
- // Step 1: /app/List/Item redirects to /app/List/Item/List
- // Step 2: User hits "back" comes back to /app/List/Item
- // Step 3: Now we cannot send the user back to /app/List/Item/List so go back one more step
- window.history.go(-1);
- return true;
- } else {
- return false;
- }
+ // example:
+ // Step 1: /app/List/Item redirects to /app/List/Item/List
+ // Step 2: User hits "back" comes back to /app/List/Item
+ // Step 3: Now we cannot send the user back to /app/List/Item/List so go back one more step
+ window.history.go(-1);
+ return true;
}
}
set_module_breadcrumb() {
if (frappe.route_history.length > 1) {
- var prev_route = frappe.route_history[frappe.route_history.length - 2];
+ const prev_route = frappe.route_history[frappe.route_history.length - 2];
if (prev_route[0] === 'modules') {
- var doctype = frappe.get_route()[1],
- module = prev_route[1];
+ const doctype = this.route[1], module = prev_route[1];
if (frappe.module_links[module] && frappe.module_links[module].includes(doctype)) {
// save the last page from the breadcrumb was accessed
frappe.breadcrumbs.set_doctype_module(doctype, module);
@@ -84,10 +82,8 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory {
}
set_cur_list() {
- var route = frappe.get_route();
- var page_name = frappe.get_route_str();
- cur_list = frappe.views.list_view[page_name];
- if (cur_list && cur_list.doctype !== route[1]) {
+ cur_list = frappe.views.list_view[this.page_name];
+ if (cur_list && cur_list.doctype !== this.route[1]) {
// changing...
window.cur_list = null;
}
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 5cfc7c75d4..beee935040 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -83,32 +83,15 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
this.sort_by = this.view_user_settings.sort_by || "modified";
this.sort_order = this.view_user_settings.sort_order || "desc";
- // set filters from user_settings or list_settings
- if (
- this.view_user_settings.filters &&
- this.view_user_settings.filters.length
- ) {
- // Priority 1: user_settings
- const saved_filters = this.view_user_settings.filters;
- this.filters = this.validate_filters(saved_filters);
- } else {
- // Priority 2: filters in listview_settings
- this.filters = (this.settings.filters || []).map((f) => {
- if (f.length === 3) {
- f = [this.doctype, f[0], f[1], f[2]];
- }
- return f;
- });
- }
-
// build menu items
this.menu_items = this.menu_items.concat(this.get_menu_items());
+ // set filters from view_user_settings or list_settings
if (
this.view_user_settings.filters &&
this.view_user_settings.filters.length
) {
- // Priority 1: saved filters
+ // Priority 1: view_user_settings
const saved_filters = this.view_user_settings.filters;
this.filters = this.validate_filters(saved_filters);
} else {
@@ -1757,8 +1740,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
const docnames = this.get_checked_items(true).map(
(docname) => docname.toString()
);
+ let message = __("Delete {0} item permanently?", [docnames.length], "Title of confirmation dialog");
+ if (docnames.length > 1) {
+ message = __("Delete {0} items permanently?", [docnames.length], "Title of confirmation dialog");
+ }
frappe.confirm(
- __("Delete {0} items permanently?", [docnames.length], "Title of confirmation dialog"),
+ message,
() => {
this.disable_list_update = true;
bulk_operations.delete(docnames, () => {
diff --git a/frappe/public/js/frappe/microtemplate.js b/frappe/public/js/frappe/microtemplate.js
index 151d008d3e..176862e233 100644
--- a/frappe/public/js/frappe/microtemplate.js
+++ b/frappe/public/js/frappe/microtemplate.js
@@ -138,6 +138,7 @@ frappe.render_tree = function(opts) {
opts.base_url = frappe.urllib.get_base_url();
opts.landscape = false;
opts.print_css = frappe.boot.print_css;
+ opts.print_format_css_path = frappe.assets.bundled_asset('print_format.bundle.css');
var tree = frappe.render_template("print_tree", opts);
var w = window.open();
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index 62f8ec2f1e..fe959b259d 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -577,13 +577,15 @@ $.extend(frappe.model, {
},
delete_doc: function(doctype, docname, callback) {
- var title = docname;
- var title_field = frappe.get_meta(doctype).title_field;
+ let title = docname;
+ const title_field = frappe.get_meta(doctype).title_field;
if (frappe.get_meta(doctype).autoname == "hash" && title_field) {
- var title = frappe.model.get_value(doctype, docname, title_field);
- title += " (" + docname + ")";
+ const value = frappe.model.get_value(doctype, docname, title_field);
+ if (value) {
+ title = `${value} (${docname})`;
+ }
}
- frappe.confirm(__("Permanently delete {0}?", [title]), function() {
+ frappe.confirm(__("Permanently delete {0}?", [title.bold()]), function() {
return frappe.call({
method: 'frappe.client.delete',
args: {
diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js
index f0d03f0743..504c534665 100644
--- a/frappe/public/js/frappe/ui/messages.js
+++ b/frappe/public/js/frappe/ui/messages.js
@@ -134,7 +134,17 @@ frappe.msgprint = function(msg, title, is_minimizable) {
}
if(data.message instanceof Array) {
- data.message.forEach(function(m) {
+ let messages = data.message;
+ const exceptions = messages
+ .map(m => JSON.parse(m))
+ .filter(m => m.raise_exception);
+
+ // only show exceptions if any exceptions exist
+ if (exceptions.length) {
+ messages = exceptions;
+ }
+
+ messages.forEach(function(m) {
frappe.msgprint(m);
});
return;
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index ff55f5578f..6971d3bc20 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -196,6 +196,15 @@ Object.assign(frappe.utils, {
}
return true;
},
+ parse_json: function(str) {
+ let parsed_json = '';
+ try {
+ parsed_json = JSON.parse(str);
+ } catch (e) {
+ return str;
+ }
+ return parsed_json;
+ },
strip_whitespace: function(html) {
return (html || "").replace(/\s*<\/p>/g, "").replace(/
(\s*
\s*)+/g, "
");
},
diff --git a/frappe/public/js/frappe/views/factory.js b/frappe/public/js/frappe/views/factory.js
index ef7f403541..e6a4212af0 100644
--- a/frappe/public/js/frappe/views/factory.js
+++ b/frappe/public/js/frappe/views/factory.js
@@ -10,20 +10,21 @@ frappe.views.Factory = class Factory {
}
show() {
- var page_name = frappe.get_route_str(),
- me = this;
+ this.route = frappe.get_route();
+ this.page_name = frappe.get_route_str();
- if (frappe.pages[page_name]) {
- frappe.container.change_to(page_name);
- if(me.on_show) {
- me.on_show();
+ if (this.before_show && this.before_show() === false) return;
+
+ if (frappe.pages[this.page_name]) {
+ frappe.container.change_to(this.page_name);
+ if (this.on_show) {
+ this.on_show();
}
} else {
- var route = frappe.get_route();
- if(route[1]) {
- me.make(route);
+ if (this.route[1]) {
+ this.make(this.route);
} else {
- frappe.show_not_found(route);
+ frappe.show_not_found(this.route);
}
}
}
@@ -34,15 +35,17 @@ frappe.views.Factory = class Factory {
}
frappe.make_page = function(double_column, page_name) {
- if(!page_name) {
- var page_name = frappe.get_route_str();
+ if (!page_name) {
+ page_name = frappe.get_route_str();
}
- var page = frappe.container.add_page(page_name);
+
+ const page = frappe.container.add_page(page_name);
frappe.ui.make_app_page({
parent: page,
single_column: !double_column
});
+
frappe.container.change_to(page_name);
return page;
}
diff --git a/frappe/public/js/frappe/views/reports/print_tree.html b/frappe/public/js/frappe/views/reports/print_tree.html
index 817c0c1e9f..973c1c0e21 100644
--- a/frappe/public/js/frappe/views/reports/print_tree.html
+++ b/frappe/public/js/frappe/views/reports/print_tree.html
@@ -1,91 +1,106 @@
-
-
-
-
-
-
- {{ title }}
-
-
-
-
-
+
-
-
-