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')}
`).appendTo(wrapper); + + frm.filters = JSON.parse(frm.doc.filters_json || '[]'); + + frm.trigger('set_filters_in_table'); + + frm.filter_table.on('click', () => { + let dialog = new frappe.ui.Dialog({ + title: __('Set Filters'), + fields: [{ + fieldtype: 'HTML', + fieldname: 'filter_area', + }], + primary_action: function() { + let values = this.get_values(); + if (values) { + this.hide(); + frm.filters = frm.filter_group.get_filters(); + frm.set_value('filters_json', JSON.stringify(frm.filters)); + frm.trigger('set_filters_in_table'); + } + }, + primary_action_label: "Set" + }); + + frappe.dashboards.filters_dialog = dialog; + + frm.filter_group = new frappe.ui.FilterGroup({ + parent: dialog.get_field('filter_area').$wrapper, + doctype: frm.doc.document_type, + on_change: () => {}, + }); + + frm.filter_group.add_filters_to_filter_group(frm.filters); + + dialog.show(); + dialog.set_values(frm.filters); + }); + + }, + + set_filters_in_table: function(frm) { + if (!frm.filters.length) { + const filter_row = $(` + ${__("Click to Set Filters")}`); + frm.filter_table.find('tbody').html(filter_row); + } else { + let filter_rows = ''; + frm.filters.forEach(filter => { + filter_rows += + ` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `; + + }); + frm.filter_table.find('tbody').html(filter_rows); + } + } +}); diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json new file mode 100644 index 0000000000..8d04ce07f5 --- /dev/null +++ b/frappe/desk/doctype/number_card/number_card.json @@ -0,0 +1,148 @@ +{ + "actions": [], + "autoname": "CARD.#####", + "creation": "2020-04-15 18:06:39.444683", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "function", + "aggregate_function_based_on", + "column_break_2", + "document_type", + "is_public", + "stats_section", + "show_percentage_stats", + "stats_time_interval", + "filters_section", + "filters_json", + "color" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "depends_on": "eval: doc.document_type", + "fieldname": "function", + "fieldtype": "Select", + "label": "Function", + "options": "Count\nSum\nAverage\nMinimum\nMaximum", + "reqd": 1 + }, + { + "depends_on": "eval: doc.function !== 'Count'", + "fieldname": "aggregate_function_based_on", + "fieldtype": "Select", + "label": "Aggregate Function Based On", + "mandatory_depends_on": "eval: doc.function !== 'Count'" + }, + { + "fieldname": "filters_json", + "fieldtype": "Code", + "label": "Filters JSON", + "options": "JSON" + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "fieldname": "color", + "fieldtype": "Color", + "label": "Color" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "filters_section", + "fieldtype": "Section Break", + "label": "Filters Section" + }, + { + "default": "0", + "description": "This card will be public to all Users if this is set", + "fieldname": "is_public", + "fieldtype": "Check", + "label": "Is Public", + "permlevel": 1 + }, + { + "default": "1", + "fieldname": "show_percentage_stats", + "fieldtype": "Check", + "label": "Show Percentage Stats" + }, + { + "default": "Daily", + "depends_on": "eval: doc.show_percentage_stats", + "description": "Show percentage difference according to this time interval", + "fieldname": "stats_time_interval", + "fieldtype": "Select", + "label": "Stats Time Interval", + "options": "Daily\nWeekly\nMonthly\nYearly" + }, + { + "fieldname": "stats_section", + "fieldtype": "Section Break", + "label": "Stats" + } + ], + "links": [], + "modified": "2020-04-25 17:31:34.204607", + "modified_by": "Administrator", + "module": "Desk", + "name": "Number Card", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Dashboard Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "search_fields": "label, document_type", + "sort_field": "modified", + "sort_order": "DESC", + "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 new file mode 100644 index 0000000000..2c5655beda --- /dev/null +++ b/frappe/desk/doctype/number_card/number_card.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe.utils import cint + +class NumberCard(Document): + pass + + +def get_permission_query_conditions(user=None): + if not user: + user = frappe.session.user + + if user == 'Administrator': + return + + roles = frappe.get_roles(user) + if "System Manager" in roles: + return None + + allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read()) + + return ''' + `tabNumber Card`.`document_type` in {allowed_doctypes} + '''.format( + allowed_doctypes=allowed_doctypes, + ) + +def has_permission(doc, ptype, user): + roles = frappe.get_roles(user) + if "System Manager" in roles: + return True + + allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read()) + if doc.document_type in allowed_doctypes: + return True + + return False + +@frappe.whitelist() +def get_result(doc, to_date=None): + doc = frappe.parse_json(doc) + fields = [] + sql_function_map = { + 'Count': 'count', + 'Sum': 'sum', + 'Average': 'avg', + 'Minimum': 'min', + 'Maximum': 'max' + } + + function = sql_function_map[doc.function] + + if function == 'count': + fields = ['{function}(*) as result'.format(function=function)] + else: + fields = ['{function}({based_on}) as result'.format(function=function, based_on=doc.aggregate_function_based_on)] + + filters = frappe.parse_json(doc.filters_json) + + if to_date: + filters.append([doc.document_type, 'creation', '<', to_date, False]) + + res = frappe.db.get_all(doc.document_type, fields=fields, filters=filters) + number = res[0]['result'] if res else 0 + + return cint(number) + +@frappe.whitelist() +def get_percentage_difference(doc, result): + doc = frappe.parse_json(doc) + result = frappe.parse_json(result) + + doc = frappe.get_doc('Number Card', doc.name) + + if not doc.get('show_percentage_stats'): + return + + previous_result = calculate_previous_result(doc) + difference = (result - previous_result)/100.0 + + return difference + + +def calculate_previous_result(doc): + from frappe.utils import add_to_date + + current_date = frappe.utils.now() + if doc.stats_time_interval == 'Daily': + previous_date = add_to_date(current_date, days=-1) + elif doc.stats_time_interval == 'Weekly': + previous_date = add_to_date(current_date, weeks=-1) + elif doc.stats_time_interval == 'Monthly': + previous_date = add_to_date(current_date, months=-1) + else: + previous_date = add_to_date(current_date, years=-1) + + number = get_result(doc, previous_date) + return number + +@frappe.whitelist() +def create_number_card(args): + args = frappe.parse_json(args) + doc = frappe.new_doc('Number Card') + + doc.update(args) + doc.insert(ignore_permissions=True) + return doc + +def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters): + meta = frappe.get_meta(doctype) + searchfields = meta.get_search_fields() + search_conditions = [] + + if txt: + for field in searchfields: + search_conditions.append('`tab{doctype}`.`{field}` like %(txt)s'.format(field=field, doctype=doctype, txt=txt)) + + search_conditions = ' or '.join(search_conditions) + + search_conditions = 'and (' + search_conditions +')' if search_conditions else '' + conditions, values = frappe.db.build_conditions(filters) + values['txt'] = '%' + txt + '%' + + return frappe.db.sql( + '''select + `tabNumber Card`.name, `tabNumber Card`.label, `tabNumber Card`.document_type + from + `tabNumber Card` + where + {conditions} and + (`tabNumber Card`.owner = '{user}' or + `tabNumber Card`.is_public = 1) + {search_conditions} + '''.format( + filters=filters, + user=frappe.session.user, + search_conditions=search_conditions, + conditions=conditions + ), values) diff --git a/frappe/desk/doctype/number_card/test_number_card.py b/frappe/desk/doctype/number_card/test_number_card.py new file mode 100644 index 0000000000..4aa1ecf282 --- /dev/null +++ b/frappe/desk/doctype/number_card/test_number_card.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestNumberCard(unittest.TestCase): + pass diff --git a/frappe/desk/doctype/number_card_link/__init__.py b/frappe/desk/doctype/number_card_link/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/number_card_link/number_card_link.json b/frappe/desk/doctype/number_card_link/number_card_link.json new file mode 100644 index 0000000000..ac035b32d8 --- /dev/null +++ b/frappe/desk/doctype/number_card_link/number_card_link.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2020-04-19 17:43:50.858343", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "card" + ], + "fields": [ + { + "fieldname": "card", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Card", + "options": "Number Card" + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-19 17:45:11.878472", + "modified_by": "Administrator", + "module": "Desk", + "name": "Number Card Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/number_card_link/number_card_link.py b/frappe/desk/doctype/number_card_link/number_card_link.py new file mode 100644 index 0000000000..67ad7e70cd --- /dev/null +++ b/frappe/desk/doctype/number_card_link/number_card_link.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class NumberCardLink(Document): + pass diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index c0a198f5e5..082b16c17a 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -10,7 +10,7 @@ import socket import time from frappe import _ from frappe.model.document import Document -from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html +from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html, add_days from frappe.utils.user import is_system_user from frappe.utils.jinja import render_template from frappe.email.smtp import SMTPServer @@ -533,28 +533,37 @@ class EmailAccount(Document): parent = None in_reply_to = (email.mail.get("In-Reply-To") or "").strip(" <>") - if in_reply_to and "@{0}".format(frappe.local.site) in in_reply_to: - # reply to a communication sent from the system - email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name']) - if email_queue: - parent_communication, parent_doctype, parent_name = email_queue - if parent_communication: - communication.in_reply_to = parent_communication + if in_reply_to: + if "@{0}".format(frappe.local.site) in in_reply_to: + # reply to a communication sent from the system + email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name']) + if email_queue: + parent_communication, parent_doctype, parent_name = email_queue + if parent_communication: + communication.in_reply_to = parent_communication + else: + reference, domain = in_reply_to.split("@", 1) + parent_doctype, parent_name = 'Communication', reference + + if frappe.db.exists(parent_doctype, parent_name): + parent = frappe._dict(doctype=parent_doctype, name=parent_name) + + # set in_reply_to of current communication + if parent_doctype=='Communication': + # communication.in_reply_to = email_queue.communication + + if parent.reference_name: + # the true parent is the communication parent + parent = frappe.get_doc(parent.reference_doctype, + parent.reference_name) else: - reference, domain = in_reply_to.split("@", 1) - parent_doctype, parent_name = 'Communication', reference - - if frappe.db.exists(parent_doctype, parent_name): - parent = frappe._dict(doctype=parent_doctype, name=parent_name) - - # set in_reply_to of current communication - if parent_doctype=='Communication': - # communication.in_reply_to = email_queue.communication - - if parent.reference_name: - # the true parent is the communication parent - parent = frappe.get_doc(parent.reference_doctype, - parent.reference_name) + comm = frappe.db.get_value('Communication', + dict( + message_id=in_reply_to, + creation=['>=', add_days(get_datetime(), -30)]), + ['reference_doctype', 'reference_name'], as_dict=1) + if comm: + parent = frappe._dict(doctype=comm.reference_doctype, name=comm.reference_name) return parent diff --git a/frappe/handler.py b/frappe/handler.py index 6e0bf7a6be..e5a7f742ae 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -14,6 +14,12 @@ from frappe.core.doctype.server_script.server_script_utils import run_server_scr from werkzeug.wrappers import Response from six import string_types +ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet') + + def handle(): """handle request""" validate_auth() @@ -148,12 +154,14 @@ def uploadfile(): @frappe.whitelist(allow_guest=True) def upload_file(): + user = None if frappe.session.user == 'Guest': if frappe.get_system_settings('allow_guests_to_upload_files'): ignore_permissions = True else: return else: + user = frappe.get_doc("User", frappe.session.user) ignore_permissions = False files = frappe.request.files @@ -175,11 +183,11 @@ def upload_file(): frappe.local.uploaded_file = content frappe.local.uploaded_filename = filename - if frappe.session.user == 'Guest': + if frappe.session.user == 'Guest' or (user and not user.has_desk_access()): import mimetypes filetype = mimetypes.guess_type(filename)[0] - if filetype not in ['image/png', 'image/jpeg', 'application/pdf']: - frappe.throw("You can only upload JPG, PNG or PDF files.") + if filetype not in ALLOWED_MIMETYPES: + frappe.throw(_("You can only upload JPG, PNG, PDF, or Microsoft documents.")) if method: method = frappe.get_attr(method) diff --git a/frappe/hooks.py b/frappe/hooks.py index 2561399a78..a63fba14f9 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -89,6 +89,7 @@ permission_query_conditions = { "Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions", "Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions", "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions", + "Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions", "Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions", "Note": "frappe.desk.doctype.note.note.get_permission_query_conditions", "Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions", @@ -105,6 +106,7 @@ has_permission = { "User": "frappe.core.doctype.user.user.has_permission", "Note": "frappe.desk.doctype.note.note.has_permission", "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.has_permission", + "Number Card": "frappe.desk.doctype.number_card.number_card.has_permission", "Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.has_permission", "Contact": "frappe.contacts.address_and_contact.has_permission", "Address": "frappe.contacts.address_and_contact.has_permission", diff --git a/frappe/monitor.py b/frappe/monitor.py index 7181bd92ad..cd365fd13f 100644 --- a/frappe/monitor.py +++ b/frappe/monitor.py @@ -23,7 +23,7 @@ def start(transaction_type="request", method=None, kwargs=None): def stop(response=None): - if frappe.conf.monitor and hasattr(frappe.local, "monitor"): + if hasattr(frappe.local, "monitor"): frappe.local.monitor.dump(response) diff --git a/frappe/patches.txt b/frappe/patches.txt index cbda8cf677..97a79df889 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -273,3 +273,4 @@ execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings') frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() frappe.patches.v13_0.website_theme_custom_scss +frappe.patches.v13_0.set_existing_dashboard_charts_as_public diff --git a/frappe/patches/v13_0/set_existing_dashboard_charts_as_public.py b/frappe/patches/v13_0/set_existing_dashboard_charts_as_public.py new file mode 100644 index 0000000000..9b7442167d --- /dev/null +++ b/frappe/patches/v13_0/set_existing_dashboard_charts_as_public.py @@ -0,0 +1,29 @@ +import frappe + +def execute(): + frappe.reload_doc('desk', 'doctype', 'dashboard_chart') + + if not frappe.db.table_exists('Dashboard Chart'): + return + + users_with_permission = frappe.get_all( + "Has Role", + fields=["parent"], + filters={"role": ['in', ['System Manager', 'Dashboard Manager']], "parenttype": "User"}, + distinct=True, + as_list=True + ) + + users = tuple( + [item if type(item) == str else item.encode('utf8') for sublist in users_with_permission for item in sublist] + ) + + frappe.db.sql(""" + UPDATE + `tabDashboard Chart` + SET + `tabDashboard Chart`.`is_public`=1 + WHERE + `tabDashboard Chart`.owner in {users} + """.format(users=users) + ) diff --git a/frappe/public/build.json b/frappe/public/build.json index afea6bf2af..d56907b558 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -107,6 +107,7 @@ "public/less/form.less", "public/less/mobile.less", "public/less/kanban.less", + "public/less/dashboard_view.less", "public/less/controls.less", "public/less/chat.less", "public/less/filters.less", @@ -299,6 +300,7 @@ "public/js/frappe/views/gantt/gantt_view.js", "public/js/frappe/views/calendar/calendar.js", + "public/js/frappe/views/dashboard/dashboard_view.js", "public/js/frappe/views/image/image_view.js", "public/js/frappe/views/kanban/kanban_view.js", "public/js/frappe/views/inbox/inbox_view.js", diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index c1ba41ab16..41e06537e1 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -152,12 +152,14 @@ frappe.ui.form.Control = Class.extend({ () => me.set_model_value(value), () => { me.set_mandatory && me.set_mandatory(value); - me.set_invalid && me.set_invalid(); if(me.df.change || me.df.onchange) { // onchange event specified in df - return (me.df.change || me.df.onchange).apply(me, [e]); + let set = (me.df.change || me.df.onchange).apply(me, [e]); + me.set_invalid && me.set_invalid(); + return set; } + me.set_invalid && me.set_invalid(); } ]); }; diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index 0dbaaeb63c..f3f04ec4d8 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -180,7 +180,14 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false); }, set_invalid: function () { - this.$wrapper.toggleClass("has-error", (this.df.invalid ? true : false)); + let invalid = !!this.df.invalid; + if (this.grid) { + this.$wrapper.parents('.grid-static-col').toggleClass('invalid', invalid); + this.$input.toggleClass('invalid', invalid); + this.grid_row.columns[this.df.fieldname].is_invalid = invalid; + } else { + this.$wrapper.toggleClass('has-error', invalid); + } }, set_bold: function() { if(this.$input) { diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 97d081dbf5..f5a06311e9 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -265,7 +265,9 @@ export default class GridRow { if(df.reqd && !txt) { column.addClass('error'); } - if (df.reqd || df.bold) { + if (column.is_invalid) { + column.addClass('invalid'); + } else if (df.reqd || df.bold) { column.addClass('bold'); } } diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 8a88ee0c0d..5aeb29b1ed 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -498,11 +498,18 @@ frappe.ui.form.Layout = Class.extend({ }, set_dependant_property: function(condition, fieldname, property) { let set_property = this.evaluate_depends_on_value(condition); + let form_obj; + if (this.frm) { + form_obj = this.frm; + } else if (this.is_dialog) { + form_obj = this; + } + if (form_obj) { if (set_property) { - this.frm.set_df_property(fieldname, property, 1); + form_obj.set_df_property(fieldname, property, 1); } else { - this.frm.set_df_property(fieldname, property, 0); + form_obj.set_df_property(fieldname, property, 0); } } }, diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index 8f88ff8db0..8386cb6c7e 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -36,7 +36,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) { freeze_message: freeze_message }); } else { - frappe.show_alert({message: __("No changes in document"), indicator: "blue"}); + !frm.is_dirty() && frappe.show_alert({message: __("No changes in document"), indicator: "blue"}); $(btn).prop("disabled", false); } }; diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 0aae8b361f..76bda11f0a 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -686,5 +686,5 @@ class FilterArea { } // utility function to validate view modes -frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox', 'Report']; +frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox', 'Report', 'Dashboard']; frappe.views.is_valid = view_mode => frappe.views.view_modes.includes(view_mode); diff --git a/frappe/public/js/frappe/list/list_sidebar.html b/frappe/public/js/frappe/list/list_sidebar.html index 880a91cf81..37f0dafb96 100644 --- a/frappe/public/js/frappe/list/list_sidebar.html +++ b/frappe/public/js/frappe/list/list_sidebar.html @@ -26,6 +26,8 @@
  • +