diff --git a/cypress/integration/form.js b/cypress/integration/form.js index 2eb48214a3..23fc57fc57 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -44,4 +44,21 @@ context('Form', () => { list_view.filter_area.filter_list.clear_filters(); }); }); + it('validates behaviour of Data options validations in child table', () => { + // test email validations for set_invalid controller + let website_input = 'website.in'; + let expectBackgroundColor = 'rgb(255, 220, 220)'; + + cy.visit('/desk#Form/Contact/New Contact 1'); + cy.get('.frappe-control[data-fieldname="email_ids"]').as('table'); + cy.get('@table').find('button.grid-add-row').click(); + cy.get('.grid-body .rows [data-fieldname="email_id"]').click(); + cy.get('@table').find('input.input-with-feedback.form-control').as('email_input'); + cy.get('@email_input').type(website_input, { waitForAnimations: false }); + cy.fill_field('company_name', 'Test Company'); + cy.get('@email_input').should($div => { + const style = window.getComputedStyle($div[0]); + expect(style.backgroundColor).to.equal(expectBackgroundColor); + }); + }); }); diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 78f452db21..2daed59074 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -16,7 +16,7 @@ global_cache_keys = ("app_hooks", "installed_apps", 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', 'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version', 'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts', - 'sitemap_routes') + 'sitemap_routes', 'db_tables') user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", "defaults", "user_permissions", "home_page", "linked_with", diff --git a/frappe/core/doctype/data_import/test_exporter_new.py b/frappe/core/doctype/data_import/test_exporter_new.py index d17cef390b..faa48a35f4 100644 --- a/frappe/core/doctype/data_import/test_exporter_new.py +++ b/frappe/core/doctype/data_import/test_exporter_new.py @@ -20,7 +20,7 @@ class TestExporter(unittest.TestCase): e = Exporter('Web Page', export_fields='All') csv_array = e.get_csv_array() header = csv_array[0] - self.assertEqual(len(header), 35) + self.assertEqual(len(header), 36) def test_exports_selected_fields(self): diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 0509ea9af7..904deb9990 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -712,9 +712,10 @@ def validate_fields(meta): if d.fieldtype == "Currency" and cint(d.width) < 100: frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx)) - def check_in_list_view(d): + def check_in_list_view(is_table, d): if d.in_list_view and (d.fieldtype in not_allowed_in_list_view): - frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx)) + property_label = 'In Grid View' if is_table else 'In List View' + frappe.throw(_("'{0}' not allowed for type {1} in row {2}").format(property_label, d.fieldtype, d.idx)) def check_in_global_search(d): if d.in_global_search and d.fieldtype in no_value_fields: @@ -906,6 +907,16 @@ def validate_fields(meta): frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True) + def check_child_table_option(docfield): + if docfield.fieldtype not in ['Table MultiSelect', 'Table']: return + + doctype = docfield.options + meta = frappe.get_meta(doctype) + + if not meta.istable: + frappe.throw(_('Option {0} for field {1} is not a child table') + .format(frappe.bold(doctype), frappe.bold(docfield.fieldname)), title=_("Invalid Option")) + fields = meta.get("fields") fieldname_list = [d.fieldname for d in fields] @@ -929,11 +940,12 @@ def validate_fields(meta): check_link_table_options(meta.get("name"), d) check_dynamic_link_options(d) check_hidden_and_mandatory(meta.get("name"), d) - check_in_list_view(d) + check_in_list_view(meta.get('istable'), d) check_in_global_search(d) check_illegal_default(d) check_unique_and_text(meta.get("name"), d) check_illegal_depends_on_conditions(d) + check_child_table_option(d) check_table_multiselect_option(d) scrub_options_in_select(d) scrub_fetch_from(d) diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js index ad65b05894..222a31a863 100644 --- a/frappe/core/page/dashboard/dashboard.js +++ b/frappe/core/page/dashboard/dashboard.js @@ -76,7 +76,16 @@ class Dashboard { } refresh() { - this.get_permitted_dashboard_charts().then(charts => { + frappe.run_serially([ + () => this.render_cards(), + () => this.render_charts() + ]); + } + + render_charts() { + return this.get_permitted_items( + 'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts' + ).then(charts => { if (!charts.length) { frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts')) } @@ -92,6 +101,7 @@ class Dashboard { ...chart } }); + this.chart_group = new frappe.widget.WidgetGroup({ title: null, container: this.container, @@ -110,14 +120,46 @@ class Dashboard { }); } - get_permitted_dashboard_charts() { + render_cards() { + return this.get_permitted_items( + 'frappe.desk.doctype.dashboard.dashboard.get_permitted_cards' + ).then(cards => { + if (!cards.length) { + return; + } + + this.number_cards = + cards.map(card => { + return { + name: card.card, + }; + }); + + this.number_card_group = new frappe.widget.WidgetGroup({ + container: this.container, + type: "number_card", + columns: 3, + options: { + allow_sorting: false, + allow_create: false, + allow_delete: false, + allow_hiding: false, + allow_edit: false, + }, + widgets: this.number_cards, + }); + }); + } + + get_permitted_items(method) { return frappe.xcall( - 'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts', + method, { dashboard_name: this.dashboard_name - }).then(charts => { - return charts; - }); + } + ).then(items => { + return items; + }); } set_dropdown() { diff --git a/frappe/database/database.py b/frappe/database/database.py index b083ff1014..101b97c915 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -124,6 +124,8 @@ class Database(object): # in transaction validations self.check_transaction_status(query) + self.clear_db_table_cache(query) + # autocommit if auto_commit: self.commit() @@ -277,6 +279,11 @@ class Database(object): ret.append(frappe._dict(zip(keys, values))) return ret + @staticmethod + def clear_db_table_cache(query): + if query and query.strip().split()[0].lower() in {'drop', 'create'}: + frappe.cache().delete_key('db_tables') + @staticmethod def needs_formatting(result, formatted): """Returns true if the first row in the result has a Date, Datetime, Long Int.""" @@ -769,7 +776,16 @@ class Database(object): return ("tab" + doctype) in self.get_tables() def get_tables(self): - return [d[0] for d in self.sql("select table_name from information_schema.tables where table_schema not in ('pg_catalog', 'information_schema')")] + tables = frappe.cache().get_value('db_tables') + if not tables: + table_rows = self.sql(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema NOT IN ('pg_catalog', 'information_schema') + """) + tables = {d[0] for d in table_rows} + frappe.cache().set_value('db_tables', tables) + return tables def a_row_exists(self, doctype): """Returns True if atleast one row exists.""" diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 28e055f382..52dc2ba917 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -137,16 +137,14 @@ class DBTable: if frappe.db.is_missing_column(e): # Unknown column 'column_name' in 'field list' continue - else: - raise + raise if max_length and max_length[0][0] and max_length[0][0] > new_length: if col.fieldname in self.columns: self.columns[col.fieldname].length = current_length - - frappe.msgprint(_("""Reverting length to {0} for '{1}' in '{2}'; - Setting the length as {3} will cause truncation of data.""") - .format(current_length, col.fieldname, self.doctype, new_length)) + info_message = _("Reverting length to {0} for '{1}' in '{2}'. Setting the length as {3} will cause truncation of data.") \ + .format(current_length, col.fieldname, self.doctype, new_length) + frappe.msgprint(info_message) def is_new(self): return self.table_name not in frappe.db.get_tables() diff --git a/frappe/desk/doctype/dashboard/dashboard.js b/frappe/desk/doctype/dashboard/dashboard.js index 19ce8eb1fd..609e943995 100644 --- a/frappe/desk/doctype/dashboard/dashboard.js +++ b/frappe/desk/doctype/dashboard/dashboard.js @@ -4,5 +4,21 @@ frappe.ui.form.on('Dashboard', { refresh: function(frm) { frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name)); + + frm.set_query("chart", "charts", function() { + return { + filters: { + is_public: 1 + } + }; + }); + + frm.set_query("card", "cards", function() { + return { + filters: { + is_public: 1 + } + }; + }); } }); diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json index c177ee70ac..c17bc3235c 100644 --- a/frappe/desk/doctype/dashboard/dashboard.json +++ b/frappe/desk/doctype/dashboard/dashboard.json @@ -8,7 +8,8 @@ "field_order": [ "dashboard_name", "is_default", - "charts" + "charts", + "cards" ], "fields": [ { @@ -31,10 +32,16 @@ "label": "Charts", "options": "Dashboard Chart Link", "reqd": 1 + }, + { + "fieldname": "cards", + "fieldtype": "Table", + "label": "Cards", + "options": "Number Card Link" } ], "links": [], - "modified": "2020-03-25 21:09:37.080132", + "modified": "2020-04-19 17:44:36.237163", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard", diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index 5c344956bf..b85e135071 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -21,3 +21,12 @@ def get_permitted_charts(dashboard_name): if frappe.has_permission('Dashboard Chart', doc=chart.chart): permitted_charts.append(chart) return permitted_charts + +@frappe.whitelist() +def get_permitted_cards(dashboard_name): + permitted_cards = [] + dashboard = frappe.get_doc('Dashboard', dashboard_name) + for card in dashboard.cards: + if frappe.has_permission('Number Card', doc=card.card): + permitted_cards.append(card) + return permitted_cards diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index 275028fc15..f8d5886b26 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -9,6 +9,7 @@ frappe.ui.form.on('Dashboard Chart', { frm.add_fetch('source', 'timeseries', 'timeseries'); }, + refresh: function(frm) { frm.chart_filters = null; frm.add_custom_button('Add Chart to Dashboard', () => { diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index cd32292783..75941a9019 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -22,6 +22,7 @@ "aggregate_function_based_on", "number_of_groups", "column_break_6", + "is_public", "timespan", "from_date", "to_date", @@ -99,7 +100,7 @@ }, { "default": "0", - "depends_on": "eval:doc.chart_type !== 'Group By'", + "depends_on": "eval: ['Count', 'Sum', 'Average'].includes(doc.chart_type)", "fieldname": "timeseries", "fieldtype": "Check", "label": "Time Series" @@ -220,10 +221,18 @@ "fieldname": "custom_options", "fieldtype": "Code", "label": "Custom Options" + }, + { + "default": "0", + "description": "This chart will be public to all Users if this is set", + "fieldname": "is_public", + "fieldtype": "Check", + "label": "Is Public", + "permlevel": 1 } ], "links": [], - "modified": "2020-04-20 23:49:11.389909", + "modified": "2020-04-23 13:01:07.178866", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", @@ -254,6 +263,7 @@ "write": 1 }, { + "create": 1, "email": 1, "export": 1, "print": 1, diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 7bed8f4504..4da8970dd2 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -92,20 +92,25 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d return chart_config @frappe.whitelist() -def create_report_chart(args): +def create_dashboard_chart(args): args = frappe.parse_json(args) - _doc = frappe.new_doc('Dashboard Chart') + doc = frappe.new_doc('Dashboard Chart') - _doc.update(args) + doc.update(args) - if (args.get("custom_options")): - _doc.custom_options = json.dumps(args.get("custom_options")) + if args.get('custom_options'): + doc.custom_options = json.dumps(args.get('custom_options')) if frappe.db.exists('Dashboard Chart', args.chart_name): args.chart_name = append_number_if_name_exists('Dashboard Chart', args.chart_name) - _doc.chart_name = args.chart_name - _doc.insert(ignore_permissions=True) + doc.chart_name = args.chart_name + doc.insert(ignore_permissions=True) + return doc + +@frappe.whitelist() +def create_report_chart(args): + create_dashboard_chart() if args.dashboard: add_chart_to_dashboard(json.dumps(args)) @@ -356,6 +361,13 @@ def get_year_ending(date): # last day of this month return add_to_date(date, days=-1) +def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters): + or_filters = {'owner': frappe.session.user, 'is_public': 1} + return frappe.db.get_list('Dashboard Chart', + fields=['name'], + filters=filters, + or_filters=or_filters, + as_list = 1) class DashboardChart(Document): diff --git a/frappe/desk/doctype/number_card/__init__.py b/frappe/desk/doctype/number_card/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js new file mode 100644 index 0000000000..812582ae05 --- /dev/null +++ b/frappe/desk/doctype/number_card/number_card.js @@ -0,0 +1,114 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Number Card', { + refresh: function(frm) { + frm.set_df_property("filters_section", "hidden", 1); + frm.trigger('set_options'); + frm.trigger('render_filters_table'); + }, + + document_type: function(frm) { + frm.set_query('document_type', function() { + return { + filters: { + 'issingle': false + } + }; + }); + frm.set_value('filters_json', '[]'); + frm.set_value('aggregate_function_based_on', ''); + if (frm.doc.document_type) { + frm.trigger('set_options'); + } + }, + + set_options: function(frm) { + let aggregate_based_on_fields = []; + const doctype = frm.doc.document_type; + + frappe.model.with_doctype(doctype, () => { + frappe.get_meta(doctype).fields.map(df => { + if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) { + aggregate_based_on_fields.push({label: df.label, value: df.fieldname}); + } + }); + + frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields); + }); + }, + + render_filters_table: function(frm) { + frm.set_df_property("filters_section", "hidden", 0); + + let wrapper = $(frm.get_field('filters_json').wrapper).empty(); + frm.filter_table = $(`
| ${__('Filter')} | +${__('Condition')} | +${__('Value')} | +
|---|
${__("You haven't added any Dashboard Charts or Number Cards yet.")}
+
${__("Click On Customize to add your first widget")}