diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml index 52fa987994..c8294886a0 100644 --- a/.github/workflows/patch-mariadb-tests.yml +++ b/.github/workflows/patch-mariadb-tests.yml @@ -32,6 +32,12 @@ jobs: with: python-version: '3.9' + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 14 + check-latest: true + - name: Check if build should be run id: check-build run: | diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index 262a6efd90..1c52070063 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -1,16 +1,6 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // MIT License. See license.txt -// ------------- -// Menu Display -// ------------- - -// $(cur_frm.wrapper).on("grid-row-render", function(e, grid_row) { -// if(grid_row.doc && grid_row.doc.fieldtype=="Section Break") { -// $(grid_row.row).css({"font-weight": "bold"}); -// } -// }) - frappe.ui.form.on('DocType', { refresh: function(frm) { frm.set_query('role', 'permissions', function(doc) { @@ -129,7 +119,7 @@ frappe.ui.form.on('DocType', { } frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt'); - } + }, }); frappe.ui.form.on("DocField", { @@ -217,5 +207,11 @@ frappe.ui.form.on("DocField", { $doctype_select.val(curr_value.doctype); update_fieldname_options(); } + }, + + fieldtype: function(frm) { + frm.trigger("max_attachments"); } }); + +extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm})); diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 1c8c1f9217..a6a81cb195 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -75,6 +75,7 @@ class DocType(Document): self.make_repeatable() self.validate_nestedset() self.validate_website() + self.ensure_minimum_max_attachment_limit() validate_links_table_fieldnames(self) if not self.is_new(): @@ -246,6 +247,22 @@ class DocType(Document): # clear website cache clear_cache() + def ensure_minimum_max_attachment_limit(self): + """Ensure that max_attachments is *at least* bigger than number of attach fields.""" + from frappe.model import attachment_fieldtypes + + + if not self.max_attachments: + return + + total_attach_fields = len([d for d in self.fields if d.fieldtype in attachment_fieldtypes]) + if total_attach_fields > self.max_attachments: + self.max_attachments = total_attach_fields + field_label = frappe.bold(self.meta.get_field("max_attachments").label) + frappe.msgprint(_("Number of attachment fields are more than {}, limit updated to {}.") + .format(field_label, total_attach_fields), + title=_("Insufficient attachment limit"), alert=True) + def change_modified_of_parent(self): """Change the timestamp of parent DocType if the current one is a child to clear caches.""" if frappe.flags.in_import: diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 4e00456f0d..8ca6e0e54e 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -332,3 +332,4 @@ frappe.customize_form.clear_locals_and_refresh = function(frm) { frm.refresh(); } +extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm})); diff --git a/frappe/database/database.py b/frappe/database/database.py index f489cea7de..6a4e781b44 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -171,10 +171,10 @@ class Database(object): frappe.errprint(query) elif self.is_deadlocked(e): - raise frappe.QueryDeadlockError + raise frappe.QueryDeadlockError(e) elif self.is_timedout(e): - raise frappe.QueryTimeoutError + raise frappe.QueryTimeoutError(e) if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)): pass @@ -583,7 +583,7 @@ class Database(object): if not isinstance(fields, Criterion): for field in fields: - if "(" in field or " as " in field: + if "(" in str(field) or " as " in str(field): field_objects.append(PseudoColumn(field)) else: field_objects.append(field) @@ -842,7 +842,7 @@ class Database(object): cache_count = frappe.cache().get_value('doctype:count:{}'.format(dt)) if cache_count is not None: return cache_count - query = self.query.build_conditions(table=dt, filters=filters).select(Count("*")) + query = self.query.get_sql(table=dt, filters=filters, fields=Count("*")) if filters: count = self.sql(query, debug=debug)[0][0] return count diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.js b/frappe/desk/doctype/onboarding_step/onboarding_step.js index 793e044d98..3c9bbab9ac 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.js +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.js @@ -2,6 +2,17 @@ // For license information, please see license.txt frappe.ui.form.on("Onboarding Step", { + + setup: function(frm) { + frm.set_query("form_tour", function() { + return { + filters: { + reference_doctype: frm.doc.reference_document + } + }; + }); + }, + refresh: function(frm) { frappe.boot.developer_mode && frm.set_intro( diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.json b/frappe/desk/doctype/onboarding_step/onboarding_step.json index f71e821f65..b5d7851eca 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.json +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.json @@ -20,6 +20,7 @@ "reference_document", "show_full_form", "show_form_tour", + "form_tour", "is_single", "reference_report", "report_reference_doctype", @@ -206,13 +207,21 @@ "fieldname": "show_form_tour", "fieldtype": "Check", "label": "Show Form Tour" + }, + { + "depends_on": "show_form_tour", + "fieldname": "form_tour", + "fieldtype": "Link", + "label": "Form Tour", + "options": "Form Tour" } ], "links": [], - "modified": "2020-10-30 14:54:06.646513", + "modified": "2021-12-02 10:56:04.448580", "modified_by": "Administrator", "module": "Desk", "name": "Onboarding Step", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 7c0e2dfe87..12fe160c9d 100644 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -186,12 +186,13 @@ class Newsletter(WebsiteGenerator): frappe.db.auto_commit_on_many_writes = is_auto_commit_set def get_message(self) -> str: - if self.content_type == "HTML": - return frappe.render_template(self.message_html, {"doc": self.as_dict()}) + message = self.message if self.content_type == "Markdown": - return frappe.utils.markdown(self.message_md) - # fallback to Rich Text - return self.message + message = frappe.utils.md_to_html(self.message_md) + if self.content_type == "HTML": + message = self.message_html + + return frappe.render_template(message, {"doc": self.as_dict()}) def get_recipients(self) -> List[str]: """Get recipients from Email Group""" diff --git a/frappe/installer.py b/frappe/installer.py index 9eed44ea15..cd6526c788 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -324,7 +324,7 @@ def _delete_doctypes(doctypes: List[str], dry_run: bool) -> None: print(f"* dropping Table for '{doctype}'...") if not dry_run: frappe.delete_doc("DocType", doctype, ignore_on_trash=True) - frappe.db.sql_ddl(f"drop table `tab{doctype}`") + frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{doctype}`") def post_install(rebuild_website=False): diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index b460db29a7..b50a0304a5 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -38,6 +38,11 @@ data_fieldtypes = ( 'Icon' ) +attachment_fieldtypes = ( + 'Attach', + 'Attach Image', +) + no_value_fields = ( 'Section Break', 'Column Break', diff --git a/frappe/public/js/form.bundle.js b/frappe/public/js/form.bundle.js index 5bed5c2cb8..2719535599 100644 --- a/frappe/public/js/form.bundle.js +++ b/frappe/public/js/form.bundle.js @@ -14,4 +14,4 @@ import "./frappe/form/controls/control.js"; import "./frappe/views/formview.js"; import "./frappe/form/form.js"; import "./frappe/meta_tag.js"; - +import "./frappe/doctype/" diff --git a/frappe/public/js/frappe/doctype/index.js b/frappe/public/js/frappe/doctype/index.js new file mode 100644 index 0000000000..9fe8957c60 --- /dev/null +++ b/frappe/public/js/frappe/doctype/index.js @@ -0,0 +1,23 @@ +frappe.provide("frappe.model"); + +/* + Common class for handling client side interactions that + apply to both DocType form and customize form. +*/ +frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form.Controller { + + max_attachments() { + if (!this.frm.doc.max_attachments) { + return; + } + const is_attach_field = (f) => ["Attach", "Attach Image"].includes(f.fieldtype); + const no_of_attach_fields = this.frm.doc.fields.filter(is_attach_field).length; + + if (no_of_attach_fields > this.frm.doc.max_attachments) { + this.frm.set_value("max_attachments", no_of_attach_fields); + const label = this.frm.get_docfield("max_attachments").label; + frappe.show_alert( + __("Number of attachment fields are more than {}, limit updated to {}.", [label, no_of_attach_fields])); + } + } +} diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index 110d617f73..7d379d4531 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -234,8 +234,9 @@ export default class OnboardingWidget extends Widget { }, }); }; + const tour_name = step.form_tour; frm.tour - .init({ on_finish }) + .init({ tour_name, on_finish }) .then(() => frm.tour.start()); }; @@ -328,8 +329,9 @@ export default class OnboardingWidget extends Widget { this.mark_complete(step); }; }; + const tour_name = step.form_tour; frm.tour - .init({ on_finish }) + .init({ tour_name, on_finish }) .then(() => frm.tour.start()); }; diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss index 9c84e99a5a..58f5ca79c6 100644 --- a/frappe/public/scss/website/index.scss +++ b/frappe/public/scss/website/index.scss @@ -266,7 +266,8 @@ h5.modal-title { .login-content.container { background-color: var(--fg-color); - padding: 45px 0px; + padding-bottom: 45px; + padding-top: 45px; box-shadow: var(--shadow-base); border-radius: var(--border-radius-md); max-width: 400px; diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 60c8db6ab6..6501d753ff 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -24,10 +24,30 @@ class TestDB(unittest.TestCase): self.assertNotEqual(frappe.db.get_value("User", {"name": ["!=", "Guest"]}), "Guest") self.assertEqual(frappe.db.get_value("User", {"name": ["<", "Adn"]}), "Administrator") self.assertEqual(frappe.db.get_value("User", {"name": ["<=", "Administrator"]}), "Administrator") - self.assertEqual(frappe.db.get_value("User", {}, ["Max(name)"], order_by=None), frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0]) - self.assertEqual(frappe.db.get_value("User", {}, "Min(name)", order_by=None), frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0]) - self.assertIn("for update", frappe.db.get_value("User", Field("name") == "Administrator", for_update=True, run=False).lower()) - + self.assertEqual( + frappe.db.get_value("User", {}, ["Max(name)"], order_by=None), + frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0], + ) + self.assertEqual( + frappe.db.get_value("User", {}, "Min(name)", order_by=None), + frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0], + ) + self.assertIn( + "for update", + frappe.db.get_value( + "User", Field("name") == "Administrator", for_update=True, run=False + ).lower(), + ) + doctype = frappe.qb.DocType("User") + self.assertEqual( + frappe.qb.from_(doctype).select(doctype.name, doctype.email).run(), + frappe.db.get_values( + doctype, + filters={}, + fieldname=[doctype.name, doctype.email], + order_by=None, + ), + ) self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name > 's' ORDER BY MODIFIED DESC""")[0][0], frappe.db.get_value("User", {"name": [">", "s"]}))