diff --git a/.eslintrc b/.eslintrc index 12ee09c163..13c6dce9fb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,10 +19,6 @@ "error", "1tbs" ], - "space-before-function-paren": [ - "error", - "never" - ], "space-unary-ops": [ "error", { "words": true } @@ -121,6 +117,7 @@ "md5": true, "$": true, "jQuery": true, + "Vue": true, "moment": true, "hljs": true, "Awesomplete": true, diff --git a/.snyk b/.snyk new file mode 100644 index 0000000000..e58c14f21b --- /dev/null +++ b/.snyk @@ -0,0 +1,9 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.13.3 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + 'npm:mem:20180117': + - showdown > yargs > os-locale > mem: + reason: None given + expires: '2019-04-01T10:08:52.588Z' +patch: {} diff --git a/bandit.yml b/bandit.yml index fce28629e8..b8560e97c8 100644 --- a/bandit.yml +++ b/bandit.yml @@ -1 +1 @@ -skips: ['B605', 'B404', 'B603', 'B607'] \ No newline at end of file +skips: ['E0203', 'B605', 'B404', 'B603', 'B607'] \ No newline at end of file diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js index 44dbde8fcf..4b2ae0da93 100644 --- a/cypress/integration/awesome_bar.js +++ b/cypress/integration/awesome_bar.js @@ -8,13 +8,6 @@ context('Awesome Bar', () => { cy.get('.navbar-home').click(); }); - it('navigates to modules', () => { - cy.get('#navbar-search') - .type('modules{downarrow}{enter}', { delay: 100 }); - - cy.location('hash').should('eq', '#modules'); - }); - it('navigates to doctype list', () => { cy.get('#navbar-search') .type('todo{downarrow}{enter}', { delay: 100 }); diff --git a/cypress/integration/relative_filters.js b/cypress/integration/relative_filters.js index efc6b930b2..c071ce0355 100644 --- a/cypress/integration/relative_filters.js +++ b/cypress/integration/relative_filters.js @@ -19,15 +19,15 @@ context('Relative Timeframe', () => { cy.get('select.condition.form-control').select("Previous"); cy.get('.filter-field select.input-with-feedback.form-control').select("1 week"); cy.server(); - cy.route({ - method: 'POST', - url: '/' - }).as('applyFilter'); + cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); cy.get('.filter-box .btn:contains("Apply")').click(); - cy.wait('@applyFilter'); + cy.wait('@list_refresh'); cy.get('.list-row-container').its('length').should('eq', 1); cy.get('.list-row-container').should('contain', 'this is second todo'); + cy.route('POST', '/api/method/frappe.model.utils.user_settings.save') + .as('save_user_settings'); cy.get('.remove-filter.btn').click(); + cy.wait('@save_user_settings'); }); it('set relative filter for Next and check list', () => { cy.visit('/desk#List/ToDo/List'); @@ -37,14 +37,14 @@ context('Relative Timeframe', () => { cy.get('select.condition.form-control').select("Next"); cy.get('.filter-field select.input-with-feedback.form-control').select("1 week"); cy.server(); - cy.route({ - method: 'POST', - url: '/' - }).as('applyFilter'); + cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); cy.get('.filter-box .btn:contains("Apply")').click(); - cy.wait('@applyFilter'); + cy.wait('@list_refresh'); cy.get('.list-row-container').its('length').should('eq', 1); cy.get('.list-row').should('contain', 'this is first todo'); + cy.route('POST', '/api/method/frappe.model.utils.user_settings.save') + .as('save_user_settings'); cy.get('.remove-filter.btn').click(); + cy.wait('@save_user_settings'); }); }); diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js index 67b522c364..a083e514ba 100644 --- a/cypress/integration/table_multiselect.js +++ b/cypress/integration/table_multiselect.js @@ -3,9 +3,11 @@ context('Table MultiSelect', () => { cy.login('Administrator', 'qwe'); }); + let todo_description = 'table multiselect' + Math.random().toString().slice(2, 8); + it('select value from multiselect dropdown', () => { cy.visit('/desk#Form/ToDo/New ToDo 1'); - cy.fill_field('description', 'asdf', 'Text Editor').blur(); + cy.fill_field('description', todo_description, 'Text Editor').blur(); cy.get('input[data-fieldname="assign_to"]').focus().as('input'); cy.get('input[data-fieldname="assign_to"] + ul').should('be.visible'); cy.get('@input').type('faris{enter}', { delay: 100 }); @@ -14,7 +16,7 @@ context('Table MultiSelect', () => { cy.get('@selected-value').should('contain', 'faris@erpnext.com'); cy.server(); - cy.route('POST', '/').as('save_form'); + cy.route('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form'); // trigger save cy.get('.primary-action').click(); cy.wait('@save_form').its('status').should('eq', 200); @@ -23,8 +25,7 @@ context('Table MultiSelect', () => { it('delete value using backspace', () => { cy.visit('/desk#List/ToDo/List'); - cy.get('.list-row a').should('exist'); - cy.get('.list-subject').last().find('a').click(); + cy.get(`.list-subject:contains("table multiselect")`).last().find('a').click(); cy.get('input[data-fieldname="assign_to"]').focus().type('{backspace}'); cy.get('.frappe-control[data-fieldname="assign_to"] .form-control .tb-selected-value') .should('not.exist'); @@ -32,8 +33,7 @@ context('Table MultiSelect', () => { it('delete value using x', () => { cy.visit('/desk#List/ToDo/List'); - cy.get('.list-row a').should('exist'); - cy.get('.list-subject').last().find('a').click(); + cy.get(`.list-subject:contains("table multiselect")`).last().find('a').click(); cy.get('.frappe-control[data-fieldname="assign_to"] .form-control .tb-selected-value').as('existing_value'); cy.get('@existing_value').find('.btn-remove').click(); cy.get('@existing_value').should('not.exist'); @@ -41,8 +41,7 @@ context('Table MultiSelect', () => { it('navigate to selected value', () => { cy.visit('/desk#List/ToDo/List'); - cy.get('.list-row a').should('exist'); - cy.get('.list-subject').last().find('a').click(); + cy.get(`.list-subject:contains("table multiselect")`).last().find('a').click(); cy.get('.frappe-control[data-fieldname="assign_to"] .form-control .tb-selected-value').as('existing_value'); cy.get('@existing_value').find('.btn-link-to-form').click(); cy.location('hash').should('contain', 'Form/User/faris@erpnext.com'); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index f25cc12ea6..36df14d1b0 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -49,4 +49,8 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => { } else { return cy.get('@input').type(value); } -}); \ No newline at end of file +}); + +Cypress.Commands.add('awesomebar', (text) => { + cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 }); +}); diff --git a/frappe/__init__.py b/frappe/__init__.py index 6521ce06f8..68611e8d53 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -16,6 +16,7 @@ from faker import Faker # public from .exceptions import * from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader) +from .utils.error import get_frame_locals # Hamless for Python 3 # For Python 2 set default encoding to utf-8 @@ -23,7 +24,7 @@ if sys.version[0] == '2': reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '11.1.4' +__version__ = '11.1.13' __title__ = "Frappe Framework" local = Local() @@ -273,7 +274,8 @@ def errprint(msg): if not request or (not "cmd" in local.form_dict) or conf.developer_mode: print(msg) - error_log.append(msg) + from .utils import escape_html + error_log.append({"exc": escape_html(msg), "locals": get_frame_locals()}) def log(msg): """Add to `debug_log`. @@ -501,6 +503,7 @@ def read_only(): retval = fn(*args, **get_newargs(fn, kwargs)) if local and hasattr(local, 'master_db'): + local.db.close() local.db = local.master_db return retval @@ -916,11 +919,15 @@ def get_hooks(hook=None, default=None, app_name=None): append_hook(hooks, key, getattr(app_hooks, key)) return hooks + no_cache = conf.developer_mode or False if app_name: hooks = _dict(load_app_hooks(app_name)) else: - hooks = _dict(cache().get_value("app_hooks", load_app_hooks)) + if no_cache: + hooks = _dict(load_app_hooks()) + else: + hooks = _dict(cache().get_value("app_hooks", load_app_hooks)) if hook: return hooks.get(hook) or (default if default is not None else []) diff --git a/frappe/app.py b/frappe/app.py index bc4ecc9b2d..9da7ea71a0 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -22,8 +22,9 @@ import frappe.website.render from frappe.utils import get_site_name from frappe.middlewares import StaticDataMiddleware from frappe.utils.error import make_error_snapshot -from frappe.core.doctype.communication.comment import update_comments_in_parent_after_request +from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request from frappe import _ +import frappe.recorder local_manager = LocalManager([frappe.local]) @@ -41,7 +42,6 @@ class RequestContext(object): def __exit__(self, type, value, traceback): frappe.destroy() - @Request.application def application(request): response = None @@ -51,6 +51,8 @@ def application(request): init_request(request) + frappe.recorder.record() + if frappe.local.form_dict.cmd: response = frappe.handler.handle() @@ -91,6 +93,8 @@ def application(request): if response and hasattr(frappe.local, 'cookie_manager'): frappe.local.cookie_manager.flush_cookies(response=response) + frappe.recorder.dump() + frappe.destroy() return response diff --git a/frappe/core/doctype/user_permission_for_page_and_report/__init__.py b/frappe/automation/__init__.py similarity index 100% rename from frappe/core/doctype/user_permission_for_page_and_report/__init__.py rename to frappe/automation/__init__.py diff --git a/frappe/desk/page/modules/__init__.py b/frappe/automation/doctype/__init__.py similarity index 100% rename from frappe/desk/page/modules/__init__.py rename to frappe/automation/doctype/__init__.py diff --git a/frappe/automation/doctype/assignment_rule/__init__.py b/frappe/automation/doctype/assignment_rule/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.js b/frappe/automation/doctype/assignment_rule/assignment_rule.js new file mode 100644 index 0000000000..3e86f6cefa --- /dev/null +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.js @@ -0,0 +1,16 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Assignment Rule', { + refresh: function(frm) { + // refresh description + frm.events.rule(frm); + }, + rule: function(frm) { + if (frm.doc.rule === 'Round Robin') { + frm.get_field('rule').set_description(__('Assign one by one, in sequence')); + } else { + frm.get_field('rule').set_description(__('Assign to the one who has the least assignments')); + } + } +}); diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.json b/frappe/automation/doctype/assignment_rule/assignment_rule.json new file mode 100644 index 0000000000..f64a965028 --- /dev/null +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.json @@ -0,0 +1,488 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 1, + "autoname": "Prompt", + "beta": 0, + "creation": "2019-02-28 17:12:18.815830", + "custom": 0, + "description": "Automatically Assign Documents to Users", + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "document_type", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Higher priority rule will be applied first", + "fieldname": "priority", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Priority", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "disabled", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Disabled", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_4", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Automatic Assignment", + "description": "Example: {{ subject }}", + "fieldname": "description", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Description", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "assignment_rules_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Assignment Rules", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Simple Python Expression, Example: status == 'Open' and type == 'Bug'", + "fieldname": "assign_condition", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Assign Condition", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_6", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Simple Python Expression, Example: Status in (\"Closed\", \"Cancelled\")", + "fieldname": "unassign_condition", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Unassign Condition", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "assign_to_users_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Assign To Users", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "rule", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Rule", + "length": 0, + "no_copy": 0, + "options": "Round Robin\nLoad Balancing", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "users", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Users", + "length": 0, + "no_copy": 0, + "options": "Assignment Rule User", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "last_user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Last User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-02-28 17:12:44.413782", + "modified_by": "Administrator", + "module": "Automation", + "name": "Assignment Rule", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py new file mode 100644 index 0000000000..3b3a96690c --- /dev/null +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, 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.desk.form import assign_to + +class AssignmentRule(Document): + def on_update(self): # pylint: disable=no-self-use + frappe.cache().delete_value('assignment_rule') + + def after_rename(self): # pylint: disable=no-self-use + frappe.cache().delete_value('assignment_rule') + + def apply(self, doc): + assignments = assign_to.get(doc) + if not assignments and self.safe_eval('assign_condition', doc): + self.do_assignment(doc) + return True + + # try clearing + if assignments and self.unassign_condition: + return self.clear_assignment(doc) + + return False + + def do_assignment(self, doc): + # clear existing assignment, to reassign + assign_to.clear(doc.get('doctype'), doc.get('name')) + + user = self.get_user() + + assign_to.add(dict( + assign_to = user, + doctype = doc.get('doctype'), + name = doc.get('name'), + description = frappe.render_template(self.description, doc) + )) + + # set for reference in round robin + self.db_set('last_user', user) + + def clear_assignment(self, doc): + '''Clear assignments''' + if self.safe_eval('unassign_condition', doc): + return assign_to.clear(doc.get('doctype'), doc.get('name')) + + def get_user(self): + ''' + Get the next user for assignment + ''' + if self.rule == 'Round Robin': + return self.get_user_round_robin() + elif self.rule == 'Load Balancing': + return self.get_user_load_balancing() + + def get_user_round_robin(self): + ''' + Get next user based on round robin + ''' + + # first time, or last in list, pick the first + if not self.last_user or self.last_user == self.users[-1].user: + return self.users[0].user + + # find out the next user in the list + for i, d in enumerate(self.users): + if self.last_user == d.user: + return self.users[i+1].user + + # bad last user, assign to the first one + return self.users[0].user + + def get_user_load_balancing(self): + '''Assign to the user with least number of open assignments''' + counts = [] + for d in self.users: + counts.append(dict( + user = d.user, + count = frappe.db.count('ToDo', dict( + reference_type = self.document_type, + owner = d.user, + status = "Open")) + )) + + # sort by dict value + sorted_counts = sorted(counts, key = lambda k: k['count']) + + # pick the first user + return sorted_counts[0].get('user') + + def safe_eval(self, fieldname, doc): + try: + return frappe.safe_eval(self.get(fieldname), None, doc) + except Exception: + # when assignment fails, don't block the document as it may be + # a part of the email pulling + frappe.msgprint(frappe._('Auto assignment failed'), indicator = 'orange') + +def apply(doc, method): + if frappe.flags.in_patch or frappe.flags.in_install: + return + + assignment_rules = frappe.cache().get_value('assignment_rule', get_assignment_rules) + if doc.doctype in assignment_rules: + # multiple auto assigns + for d in frappe.db.get_all('Assignment Rule', dict(document_type=doc.doctype, disabled = 0), order_by = 'priority desc'): + if frappe.get_doc('Assignment Rule', d.name).apply(doc.as_dict()): + break + +def get_assignment_rules(): + return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))] \ No newline at end of file diff --git a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py new file mode 100644 index 0000000000..ed7fab43ab --- /dev/null +++ b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from frappe.utils import random_string + +class TestAutoAssign(unittest.TestCase): + def setUp(self): + self.assignment_rule = get_assignment_rule() + clear_assignments() + + def test_round_robin(self): + note = make_note(dict(public=1)) + + # check if auto assigned to first user + self.assertEqual(frappe.db.get_value('ToDo', dict( + reference_type = 'Note', + reference_name = note.name, + status = 'Open' + ), 'owner'), 'test@example.com') + + note = make_note(dict(public=1)) + + # check if auto assigned to second user + self.assertEqual(frappe.db.get_value('ToDo', dict( + reference_type = 'Note', + reference_name = note.name, + status = 'Open' + ), 'owner'), 'test1@example.com') + + clear_assignments() + + note = make_note(dict(public=1)) + + # check if auto assigned to third user, even if + # previous assignments where closed + self.assertEqual(frappe.db.get_value('ToDo', dict( + reference_type = 'Note', + reference_name = note.name, + status = 'Open' + ), 'owner'), 'test2@example.com') + + # check loop back to first user + note = make_note(dict(public=1)) + + self.assertEqual(frappe.db.get_value('ToDo', dict( + reference_type = 'Note', + reference_name = note.name, + status = 'Open' + ), 'owner'), 'test@example.com') + + def test_load_balancing(self): + self.assignment_rule.rule = 'Load Balancing' + self.assignment_rule.save() + + for _ in range(30): + note = make_note(dict(public=1)) + + # check if each user has 10 assignments (?) + for user in ('test@example.com', 'test1@example.com', 'test2@example.com'): + self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10) + + # clear 5 assignments for first user + # can't do a limit in "delete" since postgres does not support it + for d in frappe.get_all('ToDo', dict(reference_type = 'Note', owner = 'test@example.com'), limit=5): + frappe.db.sql("delete from tabToDo where name = %s", d.name) + + # add 5 more assignments + for i in range(5): + make_note(dict(public=1)) + + # check if each user still has 10 assignments + for user in ('test@example.com', 'test1@example.com', 'test2@example.com'): + self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10) + + + def test_assign_condition(self): + # check condition + note = make_note(dict(public=0)) + + self.assertEqual(frappe.db.get_value('ToDo', dict( + reference_type = 'Note', + reference_name = note.name, + status = 'Open' + ), 'owner'), None) + + def test_clear_assignment(self): + note = make_note(dict(public=1)) + + # check if auto assigned to first user + self.assertEqual(frappe.db.get_value('ToDo', dict( + reference_type = 'Note', + reference_name = note.name, + status = 'Open' + ), 'owner'), 'test@example.com') + + # test auto unassign + note.public = 0 + note.save() + + # check if cleared + self.assertEqual(frappe.db.get_value('ToDo', dict( + reference_type = 'Note', + reference_name = note.name, + status = 'Open' + ), 'owner'), None) + + def check_multiple_rules(self): + note = make_note(dict(public=1, notify_on_login=1)) + + # check if auto assigned to test3 (2nd rule is applied, as it has higher priority) + self.assertEqual(frappe.db.get_value('ToDo', dict( + reference_type = 'Note', + reference_name = note.name, + status = 'Open' + ), 'owner'), 'test@example.com') + +def clear_assignments(): + frappe.db.sql("delete from tabToDo where reference_type = 'Note'") + +def get_assignment_rule(): + frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1') + + assignment_rule = frappe.get_doc(dict( + name = 'For Note 1', + doctype = 'Assignment Rule', + priority = 0, + document_type = 'Note', + assign_condition = 'public == 1', + unassign_condition = 'pubic == 0 or notify_on_login == 1', + rule = 'Round Robin', + users = [ + dict(user = 'test@example.com'), + dict(user = 'test1@example.com'), + dict(user = 'test2@example.com'), + ] + )).insert() + + frappe.delete_doc_if_exists('Assignment Rule', 'For Note 2') + + # 2nd rule + frappe.get_doc(dict( + name = 'For Note 2', + doctype = 'Assignment Rule', + priority = 1, + document_type = 'Note', + assign_condition = 'notify_on_login == 1', + unassign_condition = 'notify_on_login == 0', + rule = 'Round Robin', + users = [ + dict(user = 'test3@example.com') + ] + )).insert() + + + return assignment_rule + +def make_note(values=None): + note = frappe.get_doc(dict( + doctype = 'Note', + title = random_string(10), + content = random_string(20) + )) + + if values: + note.update(values) + + note.insert() + + return note \ No newline at end of file diff --git a/frappe/automation/doctype/assignment_rule_user/__init__.py b/frappe/automation/doctype/assignment_rule_user/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json b/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json new file mode 100644 index 0000000000..f529772c8e --- /dev/null +++ b/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json @@ -0,0 +1,76 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2019-02-27 11:41:46.602400", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2019-02-27 17:16:41.399261", + "modified_by": "Administrator", + "module": "Automation", + "name": "Assignment Rule User", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.py b/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.py new file mode 100644 index 0000000000..ee8081c6d8 --- /dev/null +++ b/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, 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 AssignmentRuleUser(Document): + pass diff --git a/frappe/boot.py b/frappe/boot.py index 18fb080d4f..043c1b0361 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -96,8 +96,8 @@ def load_conf_settings(bootinfo): if key in conf: bootinfo[key] = conf.get(key) def load_desktop_icons(bootinfo): - from frappe.desk.doctype.desktop_icon.desktop_icon import get_desktop_icons - bootinfo.desktop_icons = get_desktop_icons() + from frappe.config import get_modules_from_all_apps_for_user + bootinfo.allowed_modules = get_modules_from_all_apps_for_user() def get_allowed_pages(): return get_user_pages_or_reports('Page') diff --git a/frappe/build.py b/frappe/build.py index 3c0576732a..c3f4bfb47b 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -2,19 +2,16 @@ # MIT License. See license.txt from __future__ import unicode_literals, print_function -from frappe.utils.minify import JavascriptMinify -import warnings - -from six import iteritems, text_type -import subprocess +import os, frappe, json, shutil, re, warnings +from os.path import exists as path_exists, join as join_path, abspath, isdir from distutils.spawn import find_executable +from six import iteritems, text_type +from frappe.utils.minify import JavascriptMinify """ Build the `public` folders and setup languages """ -import os, frappe, json, shutil, re - app_paths = None def setup(): global app_paths @@ -45,7 +42,7 @@ def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False) if app: command += ' --app {app}'.format(app=app) - frappe_app_path = os.path.abspath(os.path.join(app_paths[0], '..')) + frappe_app_path = abspath(join_path(app_paths[0], '..')) check_yarn() frappe.commands.popen(command, cwd=frappe_app_path) @@ -55,7 +52,7 @@ def watch(no_compress): pacman = get_node_pacman() - frappe_app_path = os.path.abspath(os.path.join(app_paths[0], '..')) + frappe_app_path = abspath(join_path(app_paths[0], '..')) check_yarn() frappe_app_path = frappe.get_app_path('frappe', '..') frappe.commands.popen('{pacman} run watch'.format(pacman=pacman), cwd = frappe_app_path) @@ -69,51 +66,53 @@ def check_yarn(): def make_asset_dirs(make_copy=False, restore=False): # don't even think of making assets_path absolute - rm -rf ahead. - assets_path = os.path.join(frappe.local.sites_path, "assets") + assets_path = join_path(frappe.local.sites_path, "assets") for dir_path in [ - os.path.join(assets_path, 'js'), - os.path.join(assets_path, 'css')]: + join_path(assets_path, 'js'), + join_path(assets_path, 'css')]: - if not os.path.exists(dir_path): + if not path_exists(dir_path): os.makedirs(dir_path) for app_name in frappe.get_all_apps(True): pymodule = frappe.get_module(app_name) - app_base_path = os.path.abspath(os.path.dirname(pymodule.__file__)) + app_base_path = abspath(os.path.dirname(pymodule.__file__)) symlinks = [] + app_public_path = join_path(app_base_path, 'public') # app/public > assets/app - symlinks.append([os.path.join(app_base_path, 'public'), os.path.join(assets_path, app_name)]) + symlinks.append([app_public_path, join_path(assets_path, app_name)]) # app/node_modules > assets/app/node_modules - symlinks.append([os.path.join(app_base_path, '..', 'node_modules'), os.path.join(assets_path, app_name, 'node_modules')]) + if path_exists(abspath(app_public_path)): + symlinks.append([join_path(app_base_path, '..', 'node_modules'), join_path(assets_path, app_name, 'node_modules')]) app_doc_path = None - if os.path.isdir(os.path.join(app_base_path, 'docs')): - app_doc_path = os.path.join(app_base_path, 'docs') + if isdir(join_path(app_base_path, 'docs')): + app_doc_path = join_path(app_base_path, 'docs') - elif os.path.isdir(os.path.join(app_base_path, 'www', 'docs')): - app_doc_path = os.path.join(app_base_path, 'www', 'docs') + elif isdir(join_path(app_base_path, 'www', 'docs')): + app_doc_path = join_path(app_base_path, 'www', 'docs') if app_doc_path: - symlinks.append([app_doc_path, os.path.join(assets_path, app_name + '_docs')]) + symlinks.append([app_doc_path, join_path(assets_path, app_name + '_docs')]) for source, target in symlinks: - source = os.path.abspath(source) - if os.path.exists(source): + source = abspath(source) + if path_exists(source): if restore: - if os.path.exists(target): + if path_exists(target): if os.path.islink(target): os.unlink(target) else: shutil.rmtree(target) shutil.copytree(source, target) elif make_copy: - if os.path.exists(target): + if path_exists(target): warnings.warn('Target {target} already exists.'.format(target = target)) else: shutil.copytree(source, target) else: - if os.path.exists(target): + if path_exists(target): if os.path.islink(target): os.unlink(target) else: @@ -124,10 +123,10 @@ def make_asset_dirs(make_copy=False, restore=False): pass def build(no_compress=False, verbose=False): - assets_path = os.path.join(frappe.local.sites_path, "assets") + assets_path = join_path(frappe.local.sites_path, "assets") for target, sources in iteritems(get_build_maps()): - pack(os.path.join(assets_path, target), sources, no_compress, verbose) + pack(join_path(assets_path, target), sources, no_compress, verbose) def get_build_maps(): """get all build.jsons with absolute paths""" @@ -135,8 +134,8 @@ def get_build_maps(): build_maps = {} for app_path in app_paths: - path = os.path.join(app_path, 'public', 'build.json') - if os.path.exists(path): + path = join_path(app_path, 'public', 'build.json') + if path_exists(path): with open(path) as f: try: for target, sources in iteritems(json.loads(f.read())): @@ -146,7 +145,7 @@ def get_build_maps(): if isinstance(source, list): s = frappe.get_pymodule_path(source[0], *source[1].split("/")) else: - s = os.path.join(app_path, source) + s = join_path(app_path, source) source_paths.append(s) build_maps[target] = source_paths @@ -166,7 +165,7 @@ def pack(target, sources, no_compress, verbose): for f in sources: suffix = None if ':' in f: f, suffix = f.split(':') - if not os.path.exists(f) or os.path.isdir(f): + if not path_exists(f) or isdir(f): print("did not find " + f) continue timestamps[f] = os.path.getmtime(f) @@ -220,7 +219,7 @@ def files_dirty(): for target, sources in iteritems(get_build_maps()): for f in sources: if ':' in f: f, suffix = f.split(':') - if not os.path.exists(f) or os.path.isdir(f): continue + if not path_exists(f) or isdir(f): continue if os.path.getmtime(f) != timestamps.get(f): print(f + ' dirty') return True @@ -233,11 +232,11 @@ def compile_less(): return for path in app_paths: - less_path = os.path.join(path, "public", "less") - if os.path.exists(less_path): + less_path = join_path(path, "public", "less") + if path_exists(less_path): for fname in os.listdir(less_path): if fname.endswith(".less") and fname != "variables.less": - fpath = os.path.join(less_path, fname) + fpath = join_path(less_path, fname) mtime = os.path.getmtime(fpath) if fpath in timestamps and mtime == timestamps[fpath]: continue @@ -246,5 +245,5 @@ def compile_less(): print("compiling {0}".format(fpath)) - css_path = os.path.join(path, "public", "css", fname.rsplit(".", 1)[0] + ".css") + css_path = join_path(path, "public", "css", fname.rsplit(".", 1)[0] + ".css") os.system("lessc {0} > {1}".format(fpath, css_path)) diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 616b0ad03b..a913f5fba9 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -39,7 +39,8 @@ def clear_global_cache(): clear_website_cache() frappe.cache().delete_value(["app_hooks", "installed_apps", "app_modules", "module_app", "notification_config", 'system_settings', - 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', 'active_modules']) + 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', + 'active_modules', 'assignment_rule']) frappe.setup_module_map() def clear_defaults_cache(user=None): diff --git a/frappe/chat/doctype/chat_message/chat_message.py b/frappe/chat/doctype/chat_message/chat_message.py index 657009b2c1..677b850e8e 100644 --- a/frappe/chat/doctype/chat_message/chat_message.py +++ b/frappe/chat/doctype/chat_message/chat_message.py @@ -141,13 +141,16 @@ def send(user, room, content, type = "Content"): def seen(message, user = None): authenticate(user) - mess = frappe.get_doc('Chat Message', message) - mess.add_seen(user) + has_message = frappe.db.exists('Chat Message', message) - room = mess.room - resp = dict(message = message, data = dict(seen = json.loads(mess._seen))) + if has_message: + mess = frappe.get_doc('Chat Message', message) + mess.add_seen(user) - frappe.publish_realtime('frappe.chat.message:update', resp, room = room, after_commit = True) + room = mess.room + resp = dict(message = message, data = dict(seen = json.loads(mess._seen))) + + frappe.publish_realtime('frappe.chat.message:update', resp, room = room, after_commit = True) def history(room, fields = None, limit = 10, start = None, end = None): room = frappe.get_doc('Chat Room', room) @@ -194,18 +197,21 @@ def mark_messages_as_seen(message_names, user): def get(name, rooms = None, fields = None): rooms, fields = safe_json_loads(rooms, fields) - dmess = frappe.get_doc('Chat Message', name) - data = dict( - name = dmess.name, - user = dmess.user, - room = dmess.room, - room_type = dmess.room_type, - content = json.loads(dmess.content) if dmess.type in ["File"] else dmess.content, - type = dmess.type, - urls = dmess.urls, - mentions = dmess.mentions, - creation = dmess.creation, - seen = get_if_empty(dmess._seen, [ ]) - ) + has_message = frappe.db.exists('Chat Message', name) - return data \ No newline at end of file + if has_message: + dmess = frappe.get_doc('Chat Message', name) + data = dict( + name = dmess.name, + user = dmess.user, + room = dmess.room, + room_type = dmess.room_type, + content = json.loads(dmess.content) if dmess.type in ["File"] else dmess.content, + type = dmess.type, + urls = dmess.urls, + mentions = dmess.mentions, + creation = dmess.creation, + seen = get_if_empty(dmess._seen, [ ]) + ) + + return data \ No newline at end of file diff --git a/frappe/chat/doctype/chat_profile/chat_profile.py b/frappe/chat/doctype/chat_profile/chat_profile.py index 22ce94858d..283494de85 100644 --- a/frappe/chat/doctype/chat_profile/chat_profile.py +++ b/frappe/chat/doctype/chat_profile/chat_profile.py @@ -9,99 +9,94 @@ import frappe from frappe.core.doctype.version.version import get_diff from frappe.chat.doctype.chat_room import chat_room from frappe.chat.util import ( - safe_json_loads, - filter_dict, - dictify + safe_json_loads, + filter_dict, + dictify ) session = frappe.session class ChatProfile(Document): - def before_save(self): - if not self.is_new(): - self.get_doc_before_save() + def before_save(self): + if not self.is_new(): + self.get_doc_before_save() - def on_update(self): - if not self.is_new(): - b, a = self.get_doc_before_save(), self - diff = dictify(get_diff(a, b)) - if diff: - user = session.user + def on_update(self): + if not self.is_new(): + b, a = self.get_doc_before_save(), self + diff = dictify(get_diff(a, b)) + if diff: + user = session.user - fields = [changed[0] for changed in diff.changed] + fields = [changed[0] for changed in diff.changed] - if 'status' in fields: - rooms = chat_room.get(user, filters = ['Chat Room', 'type', '=', 'Direct']) - update = dict(user = user, data = dict(status = self.status)) + if 'status' in fields: + rooms = chat_room.get(user, filters = ['Chat Room', 'type', '=', 'Direct']) + update = dict(user = user, data = dict(status = self.status)) - for room in rooms: - frappe.publish_realtime('frappe.chat.profile:update', update, room = room.name, after_commit = True) + for room in rooms: + frappe.publish_realtime('frappe.chat.profile:update', update, room = room.name, after_commit = True) - if 'enable_chat' in fields: - update = dict(user = user, data = dict(enable_chat = bool(self.enable_chat))) - frappe.publish_realtime('frappe.chat.profile:update', update, user = user, after_commit = True) + if 'enable_chat' in fields: + update = dict(user = user, data = dict(enable_chat = bool(self.enable_chat))) + frappe.publish_realtime('frappe.chat.profile:update', update, user = user, after_commit = True) def authenticate(user): - if user != session.user: - frappe.throw(_("Sorry, you're not authorized.")) + if user != session.user: + frappe.throw(_("Sorry, you're not authorized.")) @frappe.whitelist() def get(user, fields = None): - duser = frappe.get_doc('User', user) - dprof = frappe.get_doc('Chat Profile', user) + duser = frappe.get_doc('User', user) + dprof = frappe.get_doc('Chat Profile', user) - # If you're adding something here, make sure the client recieves it. - profile = dict( - # User - name = duser.name, - email = duser.email, - first_name = duser.first_name, - last_name = duser.last_name, - username = duser.username, - avatar = duser.user_image, - bio = duser.bio, - # Chat Profile - status = dprof.status, - chat_background = dprof.chat_background, - message_preview = bool(dprof.message_preview), - notification_tones = bool(dprof.notification_tones), - conversation_tones = bool(dprof.conversation_tones), - enable_chat = bool(dprof.enable_chat) - ) - profile = filter_dict(profile, fields) + # If you're adding something here, make sure the client recieves it. + profile = dict( + # User + name = duser.name, + email = duser.email, + first_name = duser.first_name, + last_name = duser.last_name, + username = duser.username, + avatar = duser.user_image, + bio = duser.bio, + # Chat Profile + status = dprof.status, + chat_background = dprof.chat_background, + message_preview = bool(dprof.message_preview), + notification_tones = bool(dprof.notification_tones), + conversation_tones = bool(dprof.conversation_tones), + enable_chat = bool(dprof.enable_chat) + ) + profile = filter_dict(profile, fields) - return dictify(profile) + return dictify(profile) @frappe.whitelist() def create(user, exists_ok = False, fields = None): - authenticate(user) + authenticate(user) - exists_ok, fields = safe_json_loads(exists_ok, fields) + exists_ok, fields = safe_json_loads(exists_ok, fields) - result = frappe.db.sql(""" - SELECT * - FROM `tabChat Profile` - WHERE `user` = '{user}' - """.format(user = user)) + try: + dprof = frappe.new_doc('Chat Profile') + dprof.user = user + dprof.save(ignore_permissions = True) + except frappe.DuplicateEntryError: + frappe.clear_messages() + if not exists_ok: + frappe.throw(_('Chat Profile for User {0} exists.').format(user)) - if result: - if not exists_ok: - frappe.throw(_('Chat Profile for User {0} exists.').format(user)) - else: - dprof = frappe.new_doc('Chat Profile') - dprof.user = user - dprof.save(ignore_permissions = True) + profile = get(user, fields = fields) - profile = get(user, fields = fields) - - return profile + return profile @frappe.whitelist() def update(user, data): - authenticate(user) + authenticate(user) - data = safe_json_loads(data) + data = safe_json_loads(data) - dprof = frappe.get_doc('Chat Profile', user) - dprof.update(data) - dprof.save(ignore_permissions = True) \ No newline at end of file + dprof = frappe.get_doc('Chat Profile', user) + dprof.update(data) + dprof.save(ignore_permissions = True) \ No newline at end of file diff --git a/frappe/client.py b/frappe/client.py index 60a2a9a00f..39f0fd8516 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -358,7 +358,7 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder doc.set(docfield, _file.file_url) doc.save() - return f.as_dict() + return _file.as_dict() def check_parent_permission(parent, child_doctype): if parent: diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 5634e6108d..3798c07e91 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -569,6 +569,23 @@ def browse(context, site): else: click.echo("\nSite named \033[1m{}\033[0m doesn't exist\n".format(site)) + +@click.command('start-recording') +@pass_context +def start_recording(context): + for site in context.sites: + frappe.init(site=site) + frappe.recorder.start() + + +@click.command('stop-recording') +@pass_context +def stop_recording(context): + for site in context.sites: + frappe.init(site=site) + frappe.recorder.stop() + + commands = [ add_system_manager, backup, @@ -592,5 +609,7 @@ commands = [ _use, set_last_active_for_user, publish_realtime, - browse + browse, + start_recording, + stop_recording, ] diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index bb900590e3..ab8597832d 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -223,15 +223,16 @@ def export_csv(context, doctype, path): frappe.destroy() @click.command('export-fixtures') +@click.option('--app', default=None, help='Export fixtures of a specific app') @pass_context -def export_fixtures(context): +def export_fixtures(context, app=None): "Export fixtures" from frappe.utils.fixtures import export_fixtures for site in context.sites: try: frappe.init(site=site) frappe.connect() - export_fixtures() + export_fixtures(app=app) finally: frappe.destroy() diff --git a/frappe/config/__init__.py b/frappe/config/__init__.py index e69de29bb2..cbe0b267f4 100644 --- a/frappe/config/__init__.py +++ b/frappe/config/__init__.py @@ -0,0 +1,113 @@ +from __future__ import unicode_literals +import json +from six import iteritems +import frappe +from frappe import _ +from frappe.desk.moduleview import (get_data, get_onboard_items, config_exists, get_module_link_items_from_list) + +def get_modules_from_all_apps_for_user(user=None): + if not user: + user = frappe.session.user + + all_modules = get_modules_from_all_apps() + user_blocked_modules = frappe.get_doc('User', user).get_blocked_modules() + + allowed_modules_list = [m for m in all_modules if m.get("module_name") not in user_blocked_modules] + + empty_tables_by_module = get_all_empty_tables_by_module() + + home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings') + if home_settings: + home_settings = json.loads(home_settings) + + for module in allowed_modules_list: + module_name = module["module_name"] + if module_name in empty_tables_by_module: + module["onboard_present"] = 1 + + if home_settings: + category_settings = home_settings[module.get("category")] if module.get("category") else {} + if module_name not in category_settings: + module["hidden"] = 1 + else: + links = category_settings[module_name]["links"] + if links: + module["links"] = get_module_link_items_from_list(module["app"], module_name, links.split(",")) + + return allowed_modules_list + +def get_modules_from_all_apps(): + modules_list = [] + for app in frappe.get_installed_apps(): + modules_list += get_modules_from_app(app) + return modules_list + +def get_modules_from_app(app): + try: + modules = frappe.get_attr(app + '.config.desktop.get_data')() or {} + except ImportError: + return [] + + active_domains = frappe.get_active_domains() + + if isinstance(modules, dict): + active_modules_list = [] + for m, module in iteritems(modules): + module['module_name'] = m + active_modules_list.append(module) + else: + for m in modules: + if m.get("type") == "module" and "category" not in m: + m["category"] = "Modules" + + # Only newly formatted modules that have a category to be shown on desk + modules = [m for m in modules if m.get("category")] + active_modules_list = [] + + for m in modules: + to_add = True + module_name = m.get("module_name") + + # Check Domain + if is_domain(m) and module_name not in active_domains: + to_add = False + + # Check if config + if is_module(m) and not config_exists(app, frappe.scrub(module_name)): + to_add = False + + if "condition" in m and not m["condition"]: + to_add = False + + if to_add: + m["app"] = app + active_modules_list.append(m) + + return active_modules_list + +def get_all_empty_tables_by_module(): + results = frappe.db.sql(""" + SELECT + name, module + FROM information_schema.tables as i + JOIN tabDocType as d + ON i.table_name = CONCAT('tab', d.name) + WHERE table_rows = 0; + + """) + + empty_tables_by_module = {} + + for doctype, module in results: + if module in empty_tables_by_module: + empty_tables_by_module[module].append(doctype) + else: + empty_tables_by_module[module] = [doctype] + + return empty_tables_by_module + +def is_domain(module): + return module.get("category") == "Domains" + +def is_module(module): + return module.get("type") == "module" diff --git a/frappe/config/customization.py b/frappe/config/customization.py new file mode 100644 index 0000000000..1ecf039f88 --- /dev/null +++ b/frappe/config/customization.py @@ -0,0 +1,44 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return [ + { + "label": _("Customize"), + "icon": "fa fa-glass", + "items": [ + { + "type": "doctype", + "name": "Customize Form", + "description": _("Change field properties (hide, readonly, permission etc.)") + }, + { + "type": "doctype", + "name": "Custom Field", + "description": _("Add fields to forms.") + }, + { + "type": "doctype", + "label": _("Custom Translations"), + "name": "Translation", + "description": _("Add your own translations") + }, + { + "type": "doctype", + "name": "Custom Script", + "description": _("Add custom javascript to forms.") + }, + { + "type": "doctype", + "name": "DocType", + "description": _("Add custom forms.") + }, + { + "type": "doctype", + "label": _("Custom Tags"), + "name": "Tag Category", + "description": _("Add your own Tag Categories") + } + ] + }, + ] diff --git a/frappe/config/desk.py b/frappe/config/desk.py index 8efe293411..40db97ef8c 100644 --- a/frappe/config/desk.py +++ b/frappe/config/desk.py @@ -12,11 +12,7 @@ def get_data(): "name": "ToDo", "label": _("To Do"), "description": _("Documents assigned to you and by you."), - }, - { - "type": "doctype", - "name": "File", - "label": _("Files"), + "onboard": 1, }, { "type": "doctype", @@ -24,6 +20,18 @@ def get_data(): "label": _("Calendar"), "link": "List/Event/Calendar", "description": _("Event and other calendars."), + "onboard": 1, + }, + { + "type": "doctype", + "name": "Note", + "description": _("Private and public Notes."), + "onboard": 1, + }, + { + "type": "doctype", + "name": "File", + "label": _("Files"), }, { "type": "page", @@ -32,11 +40,6 @@ def get_data(): "description": _("Chat messages and other notifications."), "data_doctype": "Communication" }, - { - "type": "doctype", - "name": "Note", - "description": _("Private and public Notes."), - }, { "type": "page", "label": _("Activity"), @@ -52,6 +55,7 @@ def get_data(): "type": "doctype", "name": "Newsletter", "description": _("Newsletters to contacts, leads."), + "onboard": 1, }, { "type": "doctype", diff --git a/frappe/config/desktop.py b/frappe/config/desktop.py index b620508c06..628c531e70 100644 --- a/frappe/config/desktop.py +++ b/frappe/config/desktop.py @@ -1,91 +1,102 @@ from __future__ import unicode_literals +import frappe from frappe import _ def get_data(): return [ + # Administration { "module_name": "Desk", + "category": "Administration", "label": _("Tools"), "color": "#FFF5A7", "reverse": 1, "icon": "octicon octicon-calendar", - "type": "module" - }, - { - "module_name": "File Manager", - "color": "#AA784D", - "doctype": "File", - "icon": "octicon octicon-file-directory", - "label": _("File Manager"), - "link": "List/File", - "type": "list", - "hidden": 1 - }, - { - "module_name": "Website", - "color": "#16a085", - "icon": "octicon octicon-globe", "type": "module", - "hidden": 1 + "description": "Todos, notes, calendar and newsletter." }, { - "module_name": "Integrations", - "color": "#16a085", - "icon": "octicon octicon-globe", - "type": "module", - "hidden": 1 - }, - { - "module_name": "Setup", + "module_name": "Settings", + "category": "Administration", + "label": _("Settings"), "color": "#bdc3c7", "reverse": 1, "icon": "octicon octicon-settings", "type": "module", - "hidden": 1 + "description": "Data import, printing, email and workflows." }, { - "module_name": 'Email Inbox', - "type": 'list', - "label": 'Email Inbox', - "_label": _('Email Inbox'), - "_id": 'Email Inbox', - "_doctype": 'Communication', - "icon": 'fa fa-envelope-o', - "color": '#589494', - "link": 'List/Communication/Inbox' + "module_name": "Users and Permissions", + "category": "Administration", + "label": _("Users and Permissions"), + "color": "#bdc3c7", + "reverse": 1, + "icon": "octicon octicon-settings", + "type": "module", + "description": "Setup roles and permissions for users on documents." + }, + { + "module_name": "Customization", + "category": "Administration", + "label": _("Customization"), + "color": "#bdc3c7", + "reverse": 1, + "icon": "octicon octicon-settings", + "type": "module", + "description": "Customize forms, custom fields, scripts and translations." + }, + { + "module_name": "Integrations", + "category": "Administration", + "label": _("Integrations"), + "color": "#16a085", + "icon": "octicon octicon-globe", + "type": "module", + "description": "DropBox, Woocomerce, AWS, Shopify and GoCardless." + }, + { + "module_name": 'Contacts', + "category": "Administration", + "label": _("Contacts"), + "type": 'module', + "icon": "octicon octicon-book", + "color": '#ffaedb', + "description": "People Contacts and Address Book." }, { "module_name": "Core", - "label": "Developer", + "category": "Administration", "_label": _("Developer"), + "label": "Developer", "color": "#589494", "icon": "octicon octicon-circuit-board", "type": "module", "system_manager": 1, - "hidden": 1 + "condition": getattr(frappe.local.conf, 'developer_mode', 0), + "description": "Doctypes, dev tools and logs." }, + + # Places { - "module_name": 'Contacts', - "type": 'module', - "icon": "octicon octicon-book", - "color": '#FFAEDB', - "hidden": 1, + "module_name": "Website", + "category": "Places", + "label": _("Website"), + "_label": _("Website"), + "color": "#16a085", + "icon": "octicon octicon-globe", + "type": "module", + "description": "Webpages, webforms, blogs and website theme." }, { "module_name": 'Social', + "category": "Places", "label": _('Social'), "icon": "octicon octicon-heart", "type": 'link', - "link": 'social/home', + "link": '#social/home', "color": '#FF4136', 'standard': 1, - 'idx': 15 + 'idx': 15, + "description": "Build your profile and share posts with other users." }, - { - "module_name": 'Settings', - "color": "#bdc3c7", - "reverse": 1, - "icon": "octicon octicon-settings", - "type": "module" - } ] diff --git a/frappe/config/settings.py b/frappe/config/settings.py index d1c00c2e71..9577879fc0 100644 --- a/frappe/config/settings.py +++ b/frappe/config/settings.py @@ -1,51 +1,181 @@ +from __future__ import unicode_literals from frappe import _ +from frappe.desk.moduleview import add_setup_section def get_data(): - return [{ - "label": _("Settings"), - "icon": "fa fa-wrench", - "items": [ - { - "type": "doctype", - "name": "System Settings", - "label": _("System Settings"), - "description": _("Language, Date and Time settings"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Domain Settings", - "label": _("Domain Settings"), - "description": _("Enable / Disable Domains"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Print Settings", - "label": _("Print Settings"), - "description": _("Print Style, PDF Size"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Website Settings", - "label": _("Website Settings"), - "description": _("Landing Page, Website Theme, Brand Setup and more"), - "hide_count": True - }, - { - "type": "doctype", - "name": "S3 Backup Settings", - "label": _("S3 Backup Settings"), - "description": _("Enable / Disable Backup, Backup Frequency"), - "hide_count": True - }, - { - "type": "doctype", - "name": "SMS Settings", - "label": _("SMS Settings"), - "description": _("SMS Gateway URL, Message & Receiver Parameter"), - "hide_count": True - } - ] - }] + data = [ + { + "label": _("Core"), + "icon": "fa fa-wrench", + "items": [ + { + "type": "doctype", + "name": "System Settings", + "label": _("System Settings"), + "description": _("Language, Date and Time settings"), + "hide_count": True + }, + { + "type": "doctype", + "name": "Error Log", + "description": _("Log of error on automated events (scheduler).") + }, + { + "type": "doctype", + "name": "Error Snapshot", + "description": _("Log of error during requests.") + }, + { + "type": "doctype", + "name": "Domain Settings", + "label": _("Domain Settings"), + "description": _("Enable / Disable Domains"), + "hide_count": True + }, + ] + }, + { + "label": _("Data"), + "icon": "fa fa-th", + "items": [ + { + "type": "doctype", + "name": "Data Import", + "label": _("Import Data"), + "icon": "octicon octicon-cloud-upload", + "description": _("Import Data from CSV / Excel files.") + }, + { + "type": "doctype", + "name": "Data Export", + "label": _("Export Data"), + "icon": "octicon octicon-cloud-upload", + "description": _("Export Data in CSV / Excel format.") + }, + { + "type": "doctype", + "name": "Naming Series", + "description": _("Set numbering series for transactions."), + "hide_count": True + }, + { + "type": "doctype", + "name": "Rename Tool", + "label": _("Bulk Rename"), + "description": _("Rename many items by uploading a .csv file."), + "hide_count": True + }, + { + "type": "doctype", + "name": "Bulk Update", + "label": _("Bulk Update"), + "description": _("Update many values at one time."), + "hide_count": True + }, + { + "type": "page", + "name": "backups", + "label": _("Download Backups"), + "description": _("List of backups available for download"), + "icon": "fa fa-download" + }, + { + "type": "doctype", + "name": "Deleted Document", + "label": _("Deleted Documents"), + "description": _("Restore or permanently delete a document.") + }, + ] + }, + { + "label": _("Email"), + "icon": "fa fa-envelope", + "items": [ + { + "type": "doctype", + "name": "Email Account", + "description": _("Add / Manage Email Accounts.") + }, + { + "type": "doctype", + "name": "Email Domain", + "description": _("Add / Manage Email Domains.") + }, + { + "type": "doctype", + "name": "Notification", + "description": _("Setup Notifications based on various criteria.") + }, + { + "type": "doctype", + "name": "Email Template", + "description": _("Email Templates for common queries.") + }, + { + "type": "doctype", + "name": "Auto Email Report", + "description": _("Setup Reports to be emailed at regular intervals"), + }, + { + "type": "doctype", + "name": "Newsletter", + "description": _("Create and manage newsletter") + } + ] + }, + { + "label": _("Printing"), + "icon": "fa fa-print", + "items": [ + { + "type": "page", + "label": _("Print Format Builder"), + "name": "print-format-builder", + "description": _("Drag and Drop tool to build and customize Print Formats.") + }, + { + "type": "doctype", + "name": "Print Settings", + "description": _("Set default format, page size, print style etc.") + }, + { + "type": "doctype", + "name": "Print Format", + "description": _("Customized HTML Templates for printing transactions.") + }, + { + "type": "doctype", + "name": "Print Style", + "description": _("Stylesheets for Print Formats") + }, + ] + }, + { + "label": _("Workflow"), + "icon": "fa fa-random", + "items": [ + { + "type": "doctype", + "name": "Workflow", + "description": _("Define workflows for forms.") + }, + { + "type": "doctype", + "name": "Workflow State", + "description": _("States for workflow (e.g. Draft, Approved, Cancelled).") + }, + { + "type": "doctype", + "name": "Workflow Action", + "description": _("Actions for workflow (e.g. Approve, Cancel).") + }, + { + "type": "doctype", + "name": "Assignment Rule", + "description": _("Set up rules for user assignments.") + } + ] + }, + ] + add_setup_section(data, "frappe", "website", _("Website"), "fa fa-globe") + return data diff --git a/frappe/config/setup.py b/frappe/config/setup.py deleted file mode 100644 index 7ed9d837ce..0000000000 --- a/frappe/config/setup.py +++ /dev/null @@ -1,289 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ -from frappe.desk.moduleview import add_setup_section - -def get_data(): - data = [ - { - "label": _("Users"), - "icon": "fa fa-group", - "items": [ - { - "type": "doctype", - "name": "User", - "description": _("System and Website Users") - }, - { - "type": "doctype", - "name": "Role", - "description": _("User Roles") - }, - { - "type": "doctype", - "name": "Role Profile", - "description": _("Role Profile") - } - ] - }, - { - "label": _("Permissions"), - "icon": "fa fa-lock", - "items": [ - { - "type": "page", - "name": "permission-manager", - "label": _("Role Permissions Manager"), - "icon": "fa fa-lock", - "description": _("Set Permissions on Document Types and Roles") - }, - { - "type": "doctype", - "name": "User Permission", - "label": _("User Permissions"), - "icon": "fa fa-lock", - "description": _("Restrict user for specific document") - }, - { - "type": "doctype", - "name": "Role Permission for Page and Report", - "description": _("Set custom roles for page and report") - }, - { - "type": "report", - "is_query_report": True, - "doctype": "User", - "icon": "fa fa-eye-open", - "name": "Permitted Documents For User", - "description": _("Check which Documents are readable by a User") - }, - { - "type": "report", - "doctype": "DocShare", - "icon": "fa fa-share", - "name": "Document Share Report", - "description": _("Report of all document shares") - } - ] - }, - { - "label": _("Settings"), - "icon": "fa fa-wrench", - "items": [ - { - "type": "doctype", - "name": "System Settings", - "label": _("System Settings"), - "description": _("Language, Date and Time settings"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Error Log", - "description": _("Log of error on automated events (scheduler).") - }, - { - "type": "doctype", - "name": "Error Snapshot", - "description": _("Log of error during requests.") - }, - { - "type": "doctype", - "name": "Domain Settings", - "label": _("Domain Settings"), - "description": _("Enable / Disable Domains"), - "hide_count": True - }, - ] - }, - { - "label": _("Data"), - "icon": "fa fa-th", - "items": [ - { - "type": "doctype", - "name": "Data Import", - "label": _("Import Data"), - "icon": "octicon octicon-cloud-upload", - "description": _("Import Data from CSV / Excel files.") - }, - { - "type": "doctype", - "name": "Data Export", - "label": _("Export Data"), - "icon": "octicon octicon-cloud-upload", - "description": _("Export Data in CSV / Excel format.") - }, - { - "type": "doctype", - "name": "Naming Series", - "description": _("Set numbering series for transactions."), - "hide_count": True - }, - { - "type": "doctype", - "name": "Rename Tool", - "label": _("Bulk Rename"), - "description": _("Rename many items by uploading a .csv file."), - "hide_count": True - }, - { - "type": "doctype", - "name": "Bulk Update", - "label": _("Bulk Update"), - "description": _("Update many values at one time."), - "hide_count": True - }, - { - "type": "page", - "name": "backups", - "label": _("Download Backups"), - "description": _("List of backups available for download"), - "icon": "fa fa-download" - }, - { - "type": "doctype", - "name": "Deleted Document", - "label": _("Deleted Documents"), - "description": _("Restore or permanently delete a document.") - }, - ] - }, - { - "label": _("Email"), - "icon": "fa fa-envelope", - "items": [ - { - "type": "doctype", - "name": "Email Account", - "description": _("Add / Manage Email Accounts.") - }, - { - "type": "doctype", - "name": "Email Domain", - "description": _("Add / Manage Email Domains.") - }, - { - "type": "doctype", - "name": "Notification", - "description": _("Setup Notifications based on various criteria.") - }, - { - "type": "doctype", - "name": "Email Template", - "description": _("Email Templates for common queries.") - }, - { - "type": "doctype", - "name": "Auto Email Report", - "description": _("Setup Reports to be emailed at regular intervals"), - }, - { - "type": "doctype", - "name": "Newsletter", - "description": _("Create and manage newsletter") - } - ] - }, - { - "label": _("Printing"), - "icon": "fa fa-print", - "items": [ - { - "type": "page", - "label": _("Print Format Builder"), - "name": "print-format-builder", - "description": _("Drag and Drop tool to build and customize Print Formats.") - }, - { - "type": "doctype", - "name": "Print Settings", - "description": _("Set default format, page size, print style etc.") - }, - { - "type": "doctype", - "name": "Print Format", - "description": _("Customized HTML Templates for printing transactions.") - }, - { - "type": "doctype", - "name": "Print Style", - "description": _("Stylesheets for Print Formats") - }, - ] - }, - { - "label": _("Workflow"), - "icon": "fa fa-random", - "items": [ - { - "type": "doctype", - "name": "Workflow", - "description": _("Define workflows for forms.") - }, - { - "type": "doctype", - "name": "Workflow State", - "description": _("States for workflow (e.g. Draft, Approved, Cancelled).") - }, - { - "type": "doctype", - "name": "Workflow Action", - "description": _("Actions for workflow (e.g. Approve, Cancel).") - }, - ] - }, - { - "label": _("Customize"), - "icon": "fa fa-glass", - "items": [ - { - "type": "doctype", - "name": "Customize Form", - "description": _("Change field properties (hide, readonly, permission etc.)"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Custom Field", - "description": _("Add fields to forms.") - }, - { - "type": "doctype", - "label": _("Custom Translations"), - "name": "Translation", - "description": _("Add your own translations") - }, - { - "type": "doctype", - "name": "Custom Script", - "description": _("Add custom javascript to forms.") - }, - { - "type": "doctype", - "name": "DocType", - "description": _("Add custom forms.") - }, - { - "type": "doctype", - "label": _("Custom Tags"), - "name": "Tag Category", - "description": _("Add your own Tag Categories") - } - - ] - }, - { - "label": _("Applications"), - "items":[ - { - "type": "page", - "name": "applications", - "label": _("Application Installer"), - "description": _("Install Applications."), - "icon": "fa fa-download" - }, - ] - } - ] - add_setup_section(data, "frappe", "website", _("Website"), "fa fa-globe") - return data diff --git a/frappe/config/users_and_permissions.py b/frappe/config/users_and_permissions.py new file mode 100644 index 0000000000..6f739d43ce --- /dev/null +++ b/frappe/config/users_and_permissions.py @@ -0,0 +1,67 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return [ + { + "label": _("Users"), + "icon": "fa fa-group", + "items": [ + { + "type": "doctype", + "name": "User", + "description": _("System and Website Users") + }, + { + "type": "doctype", + "name": "Role", + "description": _("User Roles") + }, + { + "type": "doctype", + "name": "Role Profile", + "description": _("Role Profile") + } + ] + }, + { + "label": _("Permissions"), + "icon": "fa fa-lock", + "items": [ + { + "type": "page", + "name": "permission-manager", + "label": _("Role Permissions Manager"), + "icon": "fa fa-lock", + "description": _("Set Permissions on Document Types and Roles") + }, + { + "type": "doctype", + "name": "User Permission", + "label": _("User Permissions"), + "icon": "fa fa-lock", + "description": _("Restrict user for specific document") + }, + { + "type": "doctype", + "name": "Role Permission for Page and Report", + "description": _("Set custom roles for page and report") + }, + { + "type": "report", + "is_query_report": True, + "doctype": "User", + "icon": "fa fa-eye-open", + "name": "Permitted Documents For User", + "description": _("Check which Documents are readable by a User") + }, + { + "type": "report", + "doctype": "DocShare", + "icon": "fa fa-share", + "name": "Document Share Report", + "description": _("Report of all document shares") + } + ] + }, + ] \ No newline at end of file diff --git a/frappe/config/website.py b/frappe/config/website.py index de66ca0959..331efc0d6a 100644 --- a/frappe/config/website.py +++ b/frappe/config/website.py @@ -11,11 +11,13 @@ def get_data(): "type": "doctype", "name": "Web Page", "description": _("Content web page."), + "onboard": 1, }, { "type": "doctype", "name": "Web Form", "description": _("User editable form on Website."), + "onboard": 1, }, { "type": "doctype", @@ -26,6 +28,11 @@ def get_data(): "name": "Website Slideshow", "description": _("Embed image slideshows in website pages."), }, + { + "type": "doctype", + "name": "Website Route Meta", + "description": _("Add meta tags to your web pages"), + }, ] }, { @@ -35,6 +42,7 @@ def get_data(): "type": "doctype", "name": "Blog Post", "description": _("Single Post (article)."), + "onboard": 1, }, { "type": "doctype", @@ -56,11 +64,13 @@ def get_data(): "type": "doctype", "name": "Website Settings", "description": _("Setup of top navigation bar, footer and logo."), + "onboard": 1, }, { "type": "doctype", "name": "Website Theme", "description": _("List of themes for Website."), + "onboard": 1, }, { "type": "doctype", @@ -86,6 +96,7 @@ def get_data(): "type": "doctype", "name": "Portal Settings", "label": _("Portal Settings"), + "onboard": 1, } ] }, diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index 2ab680c765..38d292e9b4 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -101,12 +101,13 @@ def get_permitted_and_not_permitted_links(doctype): not_permitted_links = [] meta = frappe.get_meta(doctype) + allowed_doctypes = frappe.permissions.get_doctypes_with_read() for df in meta.get_link_fields(): if df.options not in ("Customer", "Supplier", "Company", "Sales Partner"): continue - if frappe.has_permission(df.options): + if df.options in allowed_doctypes: permitted_links.append(df) else: not_permitted_links.append(df) @@ -145,10 +146,9 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil _doctypes = tuple([d for d in _doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)]) all_doctypes = [d[0] for d in doctypes + _doctypes] - valid_doctypes = [] + allowed_doctypes = frappe.permissions.get_doctypes_with_read() - for doctype in all_doctypes: - if frappe.has_permission(doctype): - valid_doctypes.append([doctype]) + valid_doctypes = sorted(set(all_doctypes).intersection(set(allowed_doctypes))) + valid_doctypes = [[doctype] for doctype in valid_doctypes] - return sorted(valid_doctypes) + return valid_doctypes diff --git a/frappe/core/doctype/activity_log/feed.py b/frappe/core/doctype/activity_log/feed.py index b4d46eb5c1..e09436196e 100644 --- a/frappe/core/doctype/activity_log/feed.py +++ b/frappe/core/doctype/activity_log/feed.py @@ -56,10 +56,13 @@ def logout_feed(user, reason): subject = _("{0} logged out: {1}").format(get_fullname(user), frappe.bold(reason)) add_authentication_log(subject, user, operation="Logout") -def get_feed_match_conditions(user=None, force=True): +def get_feed_match_conditions(user=None, doctype='Comment'): if not user: user = frappe.session.user - conditions = ['`tabCommunication`.owner={user} or `tabCommunication`.reference_owner={user}'.format(user=frappe.db.escape(user))] + conditions = ['`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}'.format( + user = frappe.db.escape(user), + doctype = doctype + )] user_permissions = frappe.permissions.get_user_permissions(user) can_read = frappe.get_user().get_can_read() @@ -68,9 +71,13 @@ def get_feed_match_conditions(user=None, force=True): list(set(can_read) - set(list(user_permissions)))] if can_read_doctypes: - conditions += ["""(`tabCommunication`.reference_doctype is null - or `tabCommunication`.reference_doctype = '' - or `tabCommunication`.reference_doctype in ({}))""".format(", ".join(can_read_doctypes))] + conditions += ["""(`tab{doctype}`.reference_doctype is null + or `tab{doctype}`.reference_doctype = '' + or `tab{doctype}`.reference_doctype + in ({values}))""".format( + doctype = doctype, + values =", ".join(can_read_doctypes) + )] if user_permissions: can_read_docs = [] @@ -79,7 +86,8 @@ def get_feed_match_conditions(user=None, force=True): can_read_docs.append('{}|{}'.format(doctype, frappe.db.escape(n.get('doc', '')))) if can_read_docs: - conditions.append("concat_ws('|', `tabCommunication`.reference_doctype, `tabCommunication`.reference_name) in ({})".format( - ", ".join(can_read_docs))) + conditions.append("concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format( + doctype = doctype, + values = ", ".join(can_read_docs))) - return "(" + " or ".join(conditions) + ")" + return "(" + " or ".join(conditions) + ")" diff --git a/frappe/core/doctype/comment/__init__.py b/frappe/core/doctype/comment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/comment/comment.js b/frappe/core/doctype/comment/comment.js new file mode 100644 index 0000000000..a793f766cb --- /dev/null +++ b/frappe/core/doctype/comment/comment.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Comment', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/comment/comment.json b/frappe/core/doctype/comment/comment.json new file mode 100644 index 0000000000..344d6399b9 --- /dev/null +++ b/frappe/core/doctype/comment/comment.json @@ -0,0 +1,535 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2019-02-07 10:10:46.845678", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Comment", + "fieldname": "comment_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Comment Type", + "length": 0, + "no_copy": 0, + "options": "Comment\nLike\nInfo\nLabel\nWorkflow\nCreated\nSubmitted\nCancelled\nUpdated\nDeleted\nAssigned\nAssignment Completed\nAttachment\nAttachment Removed\nShared\nUnshared\nBot\nRelinked\nEdit", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "comment_email", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Comment Email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subject", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subject", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "comment_by", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Comment By", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "published", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Published", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "seen", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Seen", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_5", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Reference Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reference Name", + "length": 0, + "no_copy": 0, + "options": "reference_doctype", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "link_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Link DocType", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "link_name", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Link Name", + "length": 0, + "no_copy": 0, + "options": "link_doctype", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_owner", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reference Owner", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "content", + "fieldtype": "HTML Editor", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Content", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-02-08 09:18:33.843171", + "modified_by": "Administrator", + "module": "Core", + "name": "Comment", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + }, + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Website Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "comment_type", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py new file mode 100644 index 0000000000..a2d5a41cf8 --- /dev/null +++ b/frappe/core/doctype/comment/comment.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals, absolute_import +import frappe +from frappe import _ +import json +from frappe.model.document import Document +from frappe.core.doctype.user.user import extract_mentions +from frappe.utils import get_fullname, get_link_to_form +from frappe.website.render import clear_cache +from frappe.database.schema import add_column +from frappe.exceptions import ImplicitCommitError + +class Comment(Document): + def after_insert(self): + self.notify_mentions() + + frappe.publish_realtime('new_communication', self.as_dict(), + doctype=self.reference_doctype, docname=self.reference_name, + after_commit=True) + + def validate(self): + if not self.comment_email: + self.comment_email = frappe.session.user + + def on_update(self): + update_comment_in_doc(self) + + def on_trash(self): + self.remove_comment_from_cache() + frappe.publish_realtime('delete_communication', self.as_dict(), + doctype= self.reference_doctype, docname = self.reference_name, + after_commit=True) + + def remove_comment_from_cache(self): + _comments = get_comments_from_parent(self) + for c in _comments: + if c.get("name")==self.name: + _comments.remove(c) + + update_comments_in_parent(self.reference_doctype, self.reference_name, _comments) + + def notify_mentions(self): + if self.reference_doctype and self.reference_name and self.content: + mentions = extract_mentions(self.content) + + if not mentions: + return + + sender_fullname = get_fullname(frappe.session.user) + title_field = frappe.get_meta(self.reference_doctype).get_title_field() + title = self.reference_name if title_field == "name" else \ + frappe.db.get_value(self.reference_doctype, self.reference_name, title_field) + + if title != self.reference_name: + parent_doc_label = "{0}: {1} (#{2})".format(_(self.reference_doctype), + title, self.reference_name) + else: + parent_doc_label = "{0}: {1}".format(_(self.reference_doctype), + self.reference_name) + + subject = _("{0} mentioned you in a comment in {1}").format(sender_fullname, parent_doc_label) + + recipients = [frappe.db.get_value("User", {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, "email") + for name in mentions] + link = get_link_to_form(self.reference_doctype, self.reference_name, label=parent_doc_label) + + frappe.sendmail( + recipients = recipients, + sender = frappe.session.user, + subject = subject, + template = "mentioned_in_comment", + args = { + "body_content": _("{0} mentioned you in a comment in {1}").format(sender_fullname, link), + "comment": self, + "link": link + }, + header = [_('New Mention'), 'orange'] + ) + + +def on_doctype_update(): + frappe.db.add_index("Comment", ["reference_doctype", "reference_name"]) + frappe.db.add_index("Comment", ["link_doctype", "link_name"]) + + +def update_comment_in_doc(doc): + """Updates `_comments` (JSON) property in parent Document. + Creates a column `_comments` if property does not exist. + + Only user created Communication or Comment of type Comment are saved. + + `_comments` format + + { + "comment": [String], + "by": [user], + "name": [Comment Document name] + }""" + + # only comments get updates, not likes, assignments etc. + if doc.doctype == 'Comment' and doc.comment_type != 'Comment': + return + + def get_truncated(content): + return (content[:97] + '...') if len(content) > 100 else content + + if doc.reference_doctype and doc.reference_name and doc.content: + _comments = get_comments_from_parent(doc) + + updated = False + for c in _comments: + if c.get("name")==doc.name: + c["comment"] = get_truncated(doc.content) + updated = True + + if not updated: + _comments.append({ + "comment": get_truncated(doc.content), + + # "comment_email" for Comment and "sender" for Communication + "by": getattr(doc, 'comment_email', None) or getattr(doc, 'sender', None) or doc.owner, + "name": doc.name + }) + + update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments) + + +def get_comments_from_parent(doc): + ''' + get the list of comments cached in the document record in the column + `_comments` + ''' + try: + _comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]" + + except Exception as e: + if frappe.db.is_missing_table_or_column(e): + _comments = "[]" + + else: + raise + + try: + return json.loads(_comments) + except ValueError: + return [] + +def update_comments_in_parent(reference_doctype, reference_name, _comments): + """Updates `_comments` property in parent Document with given dict. + + :param _comments: Dict of comments.""" + if not reference_doctype or frappe.db.get_value("DocType", reference_doctype, "issingle"): + return + + try: + # use sql, so that we do not mess with the timestamp + frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec + (json.dumps(_comments[-50:]), reference_name)) + + except Exception as e: + if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None): + # missing column and in request, add column and update after commit + frappe.local._comments = (getattr(frappe.local, "_comments", []) + + [(reference_doctype, reference_name, _comments)]) + else: + raise ImplicitCommitError + + else: + if not frappe.flags.in_patch: + reference_doc = frappe.get_doc(reference_doctype, reference_name) + if getattr(reference_doc, "route", None): + clear_cache(reference_doc.route) + +def update_comments_in_parent_after_request(): + """update _comments in parent if _comments column is missing""" + if hasattr(frappe.local, "_comments"): + for (reference_doctype, reference_name, _comments) in frappe.local._comments: + add_column(reference_doctype, "_comments", "Text") + update_comments_in_parent(reference_doctype, reference_name, _comments) + + frappe.db.commit() diff --git a/frappe/core/doctype/comment/test_comment.py b/frappe/core/doctype/comment/test_comment.py new file mode 100644 index 0000000000..0f46f0b3b5 --- /dev/null +++ b/frappe/core/doctype/comment/test_comment.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe, json +import unittest + +class TestComment(unittest.TestCase): + def test_comment_creation(self): + test_doc = frappe.get_doc(dict(doctype = 'ToDo', description = 'test')) + test_doc.insert() + comment = test_doc.add_comment('Comment', 'test comment') + + test_doc.reload() + + # check if updated in _comments cache + comments = json.loads(test_doc.get('_comments')) + self.assertEqual(comments[0].get('name'), comment.name) + self.assertEqual(comments[0].get('comment'), comment.content) + + # check document creation + comment_1 = frappe.get_all('Comment', fields = ['*'], filters = dict( + reference_doctype = test_doc.doctype, + reference_name = test_doc.name + ))[0] + + self.assertEqual(comment_1.content, 'test comment') + + # test via blog + def test_public_comment(self): + from frappe.website.doctype.blog_post.test_blog_post import make_test_blog + test_blog = make_test_blog() + + frappe.db.sql("delete from `tabComment` where reference_doctype = 'Blog Post'") + + from frappe.templates.includes.comments.comments import add_comment + add_comment('hello', 'test@test.com', 'Good Tester', + 'Blog Post', test_blog.name, test_blog.route) + + self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict( + reference_doctype = test_blog.doctype, + reference_name = test_blog.name + ))[0].published, 1) + + frappe.db.sql("delete from `tabComment` where reference_doctype = 'Blog Post'") + + add_comment('pleez vizits my site http://mysite.com', 'test@test.com', 'bad commentor', + 'Blog Post', test_blog.name, test_blog.route) + + self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict( + reference_doctype = test_blog.doctype, + reference_name = test_blog.name + ))[0].published, 0) + + + diff --git a/frappe/core/doctype/communication/comment.py b/frappe/core/doctype/communication/comment.py deleted file mode 100644 index b7a008cd38..0000000000 --- a/frappe/core/doctype/communication/comment.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals, absolute_import -import frappe -from frappe import _ -import json -from frappe.core.doctype.user.user import extract_mentions -from frappe.utils import get_fullname, get_link_to_form -from frappe.website.render import clear_cache -from frappe.database.schema import add_column -from frappe.exceptions import ImplicitCommitError - -def on_trash(doc): - if doc.communication_type != "Comment": - return - - if doc.reference_doctype == "Message": - return - - if (doc.comment_type or "Comment") != "Comment": - frappe.only_for("System Manager") - - _comments = get_comments_from_parent(doc) - for c in _comments: - if c.get("name")==doc.name: - _comments.remove(c) - - update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments) - -def update_comment_in_doc(doc): - """Updates `_comments` (JSON) property in parent Document. - Creates a column `_comments` if property does not exist. - - Only user created comments Communication or Comment of type Comment are saved. - - `_comments` format - - { - "comment": [String], - "by": [user], - "name": [Comment Document name] - }""" - - if doc.communication_type not in ("Comment", "Communication"): - return - - if doc.communication_type == 'Comment' and doc.comment_type != 'Comment': - # other updates - return - - def get_content(doc): - return (doc.content[:97] + '...') if len(doc.content) > 100 else doc.content - - if doc.reference_doctype and doc.reference_name and doc.content: - _comments = get_comments_from_parent(doc) - - updated = False - for c in _comments: - if c.get("name")==doc.name: - c["comment"] = get_content(doc) - updated = True - - if not updated: - _comments.append({ - "comment": get_content(doc), - "by": doc.sender or doc.owner, - "name": doc.name - }) - - update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments) - -def notify_mentions(doc): - if doc.communication_type != "Comment": - return - - if doc.reference_doctype and doc.reference_name and doc.content and doc.comment_type=="Comment": - mentions = extract_mentions(doc.content) - - if not mentions: - return - - sender_fullname = get_fullname(frappe.session.user) - title_field = frappe.get_meta(doc.reference_doctype).get_title_field() - title = doc.reference_name if title_field == "name" else \ - frappe.db.get_value(doc.reference_doctype, doc.reference_name, title_field) - - if title != doc.reference_name: - parent_doc_label = "{0}: {1} (#{2})".format(_(doc.reference_doctype), - title, doc.reference_name) - else: - parent_doc_label = "{0}: {1}".format(_(doc.reference_doctype), - doc.reference_name) - - subject = _("{0} mentioned you in a comment in {1}").format(sender_fullname, parent_doc_label) - - recipients = [frappe.db.get_value("User", {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, "email") - for name in mentions] - link = get_link_to_form(doc.reference_doctype, doc.reference_name, label=parent_doc_label) - - frappe.sendmail( - recipients=recipients, - sender=frappe.session.user, - subject=subject, - template="mentioned_in_comment", - args={ - "body_content": _("{0} mentioned you in a comment in {1}").format(sender_fullname, link), - "comment": doc, - "link": link - }, - header=[_('New Mention'), 'orange'] - ) - -def get_comments_from_parent(doc): - try: - _comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]" - - except Exception as e: - if frappe.db.is_missing_table_or_column(e): - _comments = "[]" - - else: - raise - - try: - return json.loads(_comments) - except ValueError: - return [] - -def update_comments_in_parent(reference_doctype, reference_name, _comments): - """Updates `_comments` property in parent Document with given dict. - - :param _comments: Dict of comments.""" - if not reference_doctype or frappe.db.get_value("DocType", reference_doctype, "issingle"): - return - - try: - # use sql, so that we do not mess with the timestamp - frappe.db.sql("""update `tab%s` set `_comments`=%s where name=%s""" % (reference_doctype, - "%s", "%s"), (json.dumps(_comments), reference_name)) - - except Exception as e: - if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None): - # missing column and in request, add column and update after commit - frappe.local._comments = (getattr(frappe.local, "_comments", []) - + [(reference_doctype, reference_name, _comments)]) - else: - raise ImplicitCommitError - - else: - if not frappe.flags.in_patch: - reference_doc = frappe.get_doc(reference_doctype, reference_name) - if getattr(reference_doc, "route", None): - clear_cache(reference_doc.route) - -def add_info_comment(**kwargs): - kwargs.update({ - "doctype": "Communication", - "communication_type": "Comment", - "comment_type": "Info", - "status": "Closed" - }) - return frappe.get_doc(kwargs).insert(ignore_permissions=True) - -def update_comments_in_parent_after_request(): - """update _comments in parent if _comments column is missing""" - if hasattr(frappe.local, "_comments"): - for (reference_doctype, reference_name, _comments) in frappe.local._comments: - add_column(reference_doctype, "_comments", "Text") - update_comments_in_parent(reference_doctype, reference_name, _comments) - - frappe.db.commit() diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index d0a7433e72..71d3537bd7 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -6,13 +6,12 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import validate_email_add, get_fullname, strip_html, cstr -from frappe.core.doctype.communication.comment import (notify_mentions, - update_comment_in_doc, on_trash) from frappe.core.doctype.communication.email import (validate_email, notify, _notify, update_parent_mins_to_first_response) from frappe.core.utils import get_parent_doc, set_timeline_doc from frappe.utils.bot import BotReply from frappe.utils import parse_addr +from frappe.core.doctype.comment.comment import update_comment_in_doc from collections import Counter @@ -87,19 +86,15 @@ class Communication(Document): if not (self.reference_doctype and self.reference_name): return - if self.reference_doctype == "Communication" and self.sent_or_received == "Sent" and \ - self.communication_type != 'Comment': + if self.reference_doctype == "Communication" and self.sent_or_received == "Sent": frappe.db.set_value("Communication", self.reference_name, "status", "Replied") - if self.communication_type in ("Communication", "Comment"): + if self.communication_type == "Communication": # send new comment to listening clients frappe.publish_realtime('new_communication', self.as_dict(), doctype=self.reference_doctype, docname=self.reference_name, after_commit=True) - if self.communication_type == "Comment": - notify_mentions(self) - elif self.communication_type in ("Chat", "Notification", "Bot"): if self.reference_name == frappe.session.user: message = self.as_dict() @@ -111,26 +106,20 @@ class Communication(Document): user=self.reference_name, after_commit=True) def on_update(self): - """Update parent status as `Open` or `Replied`.""" + # add to _comment property of the doctype, so it shows up in + # comments count for the list view + update_comment_in_doc(self) + if self.comment_type != 'Updated': update_parent_mins_to_first_response(self) - update_comment_in_doc(self) self.bot_reply() def on_trash(self): - if (not self.flags.ignore_permissions - and self.communication_type=="Comment" and self.comment_type != "Comment"): - - # prevent deletion of auto-created comments if not ignore_permissions - frappe.throw(_("Sorry! You cannot delete auto-generated comments")) - - if self.communication_type in ("Communication", "Comment"): + if self.communication_type == "Communication": # send delete comment to listening clients frappe.publish_realtime('delete_communication', self.as_dict(), doctype= self.reference_doctype, docname = self.reference_name, after_commit=True) - # delete the comments from _comment - on_trash(self) def set_status(self): if not self.is_new(): diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index fbcd30a3f2..f29f2ad346 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -33,7 +33,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = :param sender: Communcation sender (default current user). :param recipients: Communication recipients as list. :param communication_medium: Medium of communication (default **Email**). - :param send_mail: Send via email (default **False**). + :param send_email: Send via email (default **False**). :param print_html: HTML Print format to be sent as attachment. :param print_format: Print Format name of parent document to be sent as attachment. :param attachments: List of attachments as list of files or JSON string. @@ -50,6 +50,9 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = if not sender: sender = get_formatted_email(frappe.session.user) + if isinstance(recipients, list): + recipients = ', '.join(recipients) + comm = frappe.get_doc({ "doctype":"Communication", "subject": subject, @@ -307,7 +310,8 @@ def set_incoming_outgoing_accounts(doc): doc.outgoing_email_account = frappe.db.get_value("Email Account", {"append_to": doc.reference_doctype, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender", "name"], as_dict=True) + ["email_id", "always_use_account_email_id_as_sender", "name", + "always_use_account_name_as_sender_name"], as_dict=True) if not doc.incoming_email_account: doc.incoming_email_account = frappe.db.get_value("Email Account", @@ -317,12 +321,14 @@ def set_incoming_outgoing_accounts(doc): # if from address is not the default email account doc.outgoing_email_account = frappe.db.get_value("Email Account", {"email_id": doc.sender, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender", "name", "send_unsubscribe_message"], as_dict=True) or frappe._dict() + ["email_id", "always_use_account_email_id_as_sender", "name", + "send_unsubscribe_message", "always_use_account_name_as_sender_name"], as_dict=True) or frappe._dict() if not doc.outgoing_email_account: doc.outgoing_email_account = frappe.db.get_value("Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender", "name", "send_unsubscribe_message"],as_dict=True) or frappe._dict() + ["email_id", "always_use_account_email_id_as_sender", "name", + "send_unsubscribe_message", "always_use_account_name_as_sender_name"],as_dict=True) or frappe._dict() if doc.sent_or_received == "Sent": doc.db_set("email_account", doc.outgoing_email_account.name) @@ -494,9 +500,9 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments communication._notify(print_html=print_html, print_format=print_format, attachments=attachments, recipients=recipients, cc=cc, bcc=bcc) - except frappe.db.InternalError: + except frappe.db.InternalError as e: # deadlock, try again - if frappe.db.is_deadlocked(): + if frappe.db.is_deadlocked(e): frappe.db.rollback() time.sleep(1) continue diff --git a/frappe/core/doctype/docfield/docfield.py b/frappe/core/doctype/docfield/docfield.py index 6b53be3288..bce92557ba 100644 --- a/frappe/core/doctype/docfield/docfield.py +++ b/frappe/core/doctype/docfield/docfield.py @@ -3,7 +3,26 @@ from __future__ import unicode_literals +import frappe from frappe.model.document import Document class DocField(Document): - pass + def get_link_doctype(self): + '''Returns the Link doctype for the docfield (if applicable) + if fieldtype is Link: Returns "options" + if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table + ''' + if self.fieldtype == 'Link': + return self.options + + if self.fieldtype == 'Table MultiSelect': + table_doctype = self.options + + link_doctype = frappe.db.get_value('DocField', { + 'fieldtype': 'Link', + 'parenttype': 'DocType', + 'parent': table_doctype, + 'in_list_view': 1 + }, 'options') + + return link_doctype diff --git a/frappe/core/doctype/doctype/boilerplate/controller._py b/frappe/core/doctype/doctype/boilerplate/controller._py index d6af4f15aa..65f9b2bc35 100644 --- a/frappe/core/doctype/doctype/boilerplate/controller._py +++ b/frappe/core/doctype/doctype/boilerplate/controller._py @@ -3,7 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document class {classname}(Document): diff --git a/frappe/core/doctype/doctype/boilerplate/controller.js b/frappe/core/doctype/doctype/boilerplate/controller.js index 87c69d29ad..6d9fb2a514 100644 --- a/frappe/core/doctype/doctype/boilerplate/controller.js +++ b/frappe/core/doctype/doctype/boilerplate/controller.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('{doctype}', {{ - refresh: function(frm) {{ + // refresh: function(frm) {{ - }} + // }} }}); diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index ce252c2d70..4edf29d937 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -52,7 +52,6 @@ class DocType(Document): self.permissions = [] self.scrub_field_names() - self.scrub_options_in_select() self.set_default_in_list_view() self.set_default_translatable() self.validate_series() @@ -202,17 +201,6 @@ class DocType(Document): # unique is automatically an index if d.unique: d.search_index = 0 - def scrub_options_in_select(self): - """Strip options for whitespaces""" - for field in self.fields: - if field.fieldtype == "Select" and field.options is not None: - options_list = [] - for i, option in enumerate(field.options.split("\n")): - _option = option.strip() - if i==0 or _option: - options_list.append(_option) - field.options = '\n'.join(options_list) - def validate_series(self, autoname=None, name=None): """Validate if `autoname` property is correctly set.""" if not autoname: autoname = self.autoname @@ -705,6 +693,20 @@ def validate_fields(meta): frappe.throw(_('DocType {0} provided for the field {1} must have atleast one Link field') .format(doctype, docfield.fieldname), frappe.ValidationError) + def scrub_options_in_select(field): + """Strip options for whitespaces""" + + if field.fieldtype == "Select" and field.options is not None: + options_list = [] + for i, option in enumerate(field.options.split("\n")): + _option = option.strip() + if i==0 or _option: + options_list.append(_option) + field.options = '\n'.join(options_list) + + def scrub_fetch_from(field): + if hasattr(field, 'fetch_from') and getattr(field, 'fetch_from'): + field.fetch_from = field.fetch_from.strip('\n').strip() fields = meta.get("fields") fieldname_list = [d.fieldname for d in fields] @@ -734,6 +736,8 @@ def validate_fields(meta): check_unique_and_text(d) check_illegal_depends_on_conditions(d) check_table_multiselect_option(d) + scrub_options_in_select(d) + scrub_fetch_from(d) check_fold(fields) check_search_fields(meta, fields) diff --git a/frappe/core/doctype/domain/domain.py b/frappe/core/doctype/domain/domain.py index ea21e73b95..a4e9f503ab 100644 --- a/frappe/core/doctype/domain/domain.py +++ b/frappe/core/doctype/domain/domain.py @@ -18,8 +18,7 @@ class Domain(Document): self.setup_roles() self.setup_properties() self.set_values() - # always set the desktop icons while changing the domain settings - self.setup_desktop_icons() + if not int(frappe.defaults.get_defaults().setup_complete or 0): # if setup not complete, setup desktop etc. self.setup_sidebar_items() @@ -89,12 +88,6 @@ class Domain(Document): frappe.db.set_value('Portal Settings', None, 'default_role', self.data.get('default_portal_role')) - def setup_desktop_icons(self): - '''set desktop icons form `data.desktop_icons`''' - from frappe.desk.doctype.desktop_icon.desktop_icon import set_desktop_icons - if self.data.desktop_icons: - set_desktop_icons(self.data.desktop_icons) - def setup_properties(self): if self.data.properties: for args in self.data.properties: diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 4936870e88..6942b72f6a 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -139,7 +139,7 @@ class File(NestedSet): def set_folder_size(self): """Set folder size if folder""" if self.is_folder and not self.is_new(): - self.file_size = frappe.utils.cint(self.get_folder_size()) + self.file_size = cint(self.get_folder_size()) self.db_set('file_size', self.file_size) for folder in self.get_ancestors(): @@ -176,6 +176,9 @@ class File(NestedSet): """ full_path = self.get_full_path() + if full_path.startswith('http'): + return True + if not os.path.exists(full_path): frappe.throw(_("File {0} does not exist").format(self.file_url), IOError) @@ -231,7 +234,7 @@ class File(NestedSet): else: try: image, filename, extn = get_web_image(self.file_url) - except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError): + except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError, TypeError): return size = width, height @@ -384,6 +387,9 @@ class File(NestedSet): elif file_path.startswith("/files/"): file_path = get_files_path(*file_path.split("/files/", 1)[1].split("/")) + elif file_path.startswith("http"): + pass + elif not self.file_url: frappe.throw(_("There is some problem with the file url: {0}").format(file_path)) diff --git a/frappe/core/doctype/language/language.py b/frappe/core/doctype/language/language.py index 8c7e01cb62..fb18abdf5e 100644 --- a/frappe/core/doctype/language/language.py +++ b/frappe/core/doctype/language/language.py @@ -3,11 +3,22 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe, json +import frappe, json, re +from frappe import _ from frappe.model.document import Document class Language(Document): - pass + def validate(self): + validate_with_regex(self.language_code, "Language Code") + + def before_rename(self, old, new, merge=False): + validate_with_regex(new, "Name") + +def validate_with_regex(name, label): + pattern = re.compile("^[a-zA-Z]+[-_]*[a-zA-Z]+$") + if not pattern.match(name): + frappe.throw(_("""{0} must begin and end with a letter and can only contain letters, + hyphen or underscore.""").format(label)) def export_languages_json(): '''Export list of all languages''' diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 71d7cfc0e8..d334731015 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -71,10 +71,6 @@ frappe.ui.form.on('User', { frm.toggle_display(['sb1', 'sb3', 'modules_access'], false); if(!frm.is_new()) { - frm.add_custom_button(__("Set Desktop Icons"), function() { - frappe.frappe_toolbar.modules_select.show(doc.name); - }, null, "btn-default") - if(has_access_to_edit_user()) { frm.add_custom_button(__("Set User Permissions"), function() { diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 4fcee60dc5..e9bb6b74c9 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1511,7 +1511,7 @@ "columns": 0, "default": "", "description": "", - "fieldname": "desktop_icon_access", + "fieldname": "sb_allow_modules", "fieldtype": "Section Break", "hidden": 0, "ignore_user_permissions": 0, @@ -1520,7 +1520,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Allow Desktop Icon", + "label": "Allow Modules", "length": 0, "no_copy": 0, "permlevel": 1, @@ -1601,6 +1601,39 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "", + "fieldname": "home_settings", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Home Settings", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -2303,7 +2336,7 @@ "issingle": 0, "istable": 0, "max_attachments": 5, - "modified": "2018-11-21 12:34:57.652854", + "modified": "2019-03-03 11:10:06.162540", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index dbc2ccd873..878d87f36a 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -36,9 +36,9 @@ class User(Document): self.name = self.email def onload(self): + from frappe.config import get_modules_from_all_apps self.set_onload('all_modules', - [m.module_name for m in frappe.db.get_all('Desktop Icon', - fields=['module_name'], filters={'standard': 1}, order_by="module_name")]) + [m.get("module_name") for m in get_modules_from_all_apps()]) def before_insert(self): self.flags.in_insert = True @@ -328,6 +328,12 @@ class User(Document): and reference_doctype='User' and (reference_name=%s or owner=%s)""", (self.name, self.name)) + # unlink contact + frappe.db.sql("""update `tabContact` + set `user`=null + where `user`=%s""", (self.name)) + + def before_rename(self, old_name, new_name, merge=False): self.check_demo() frappe.clear_cache(user=old_name) diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py index 157fa44ae2..b83d103013 100644 --- a/frappe/core/doctype/user_permission/test_user_permission.py +++ b/frappe/core/doctype/user_permission/test_user_permission.py @@ -2,9 +2,84 @@ # Copyright (c) 2017, Frappe Technologies and Contributors # See license.txt from __future__ import unicode_literals +from frappe.core.doctype.user_permission.user_permission import add_user_permissions -#import frappe +import frappe import unittest class TestUserPermission(unittest.TestCase): - pass + def test_apply_to_all(self): + ''' Create User permission for User having access to all applicable Doctypes''' + user = get_user() + param = get_params(user, apply = 1) + created = add_user_permissions(param) + self.assertEquals(created, 1) + + def test_for_applicable_on_update_from_apply_to_all(self): + ''' Update User Permission from all to some applicable Doctypes''' + user = get_user() + param = get_params(user, applicable = ["Chat Room", "Chat Message"]) + create = add_user_permissions(param) + frappe.db.commit() + + removed_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user)) + created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room")) + created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message")) + + self.assertIsNone(removed_apply_to_all) + self.assertIsNotNone(created_applicable_first) + self.assertIsNotNone(created_applicable_second) + self.assertEquals(create, 1) + + def test_for_apply_to_all_on_update_from_applicable(self): + ''' Update User Permission from some to all applicable Doctypes''' + user = get_user() + param = get_params(user, apply = 1) + created = add_user_permissions(param) + created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user)) + removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room")) + removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message")) + + + self.assertIsNotNone(created_apply_to_all) + self.assertIsNone(removed_applicable_first) + self.assertIsNone(removed_applicable_second) + self.assertEquals(created, 1) + +def get_user(): + if frappe.db.exists('User', 'test_bulk_creation_update@example.com'): + return frappe.get_doc('User', 'test_bulk_creation_update@example.com') + else: + user = frappe.new_doc('User') + user.email = 'test_bulk_creation_update@example.com' + user.first_name = 'Test_Bulk_Creation' + user.add_roles("System Manager") + return user + +def get_params(user, apply = None , applicable = None): + ''' Return param to insert ''' + param = { + "user": user.name, + "doctype":"User", + "docname":user.name + } + if apply: + param.update({"apply_to_all_doctypes": 1}) + param.update({"applicable_doctypes": []}) + if applicable: + param.update({"apply_to_all_doctypes": 0}) + param.update({"applicable_doctypes": applicable}) + return param + +def get_exists_param(user, applicable = None): + ''' param to check existing Document ''' + param = { + "user": user.name, + "allow": "User", + "for_value": user.name, + } + if applicable: + param.update({"applicable_for": applicable}) + else: + param.update({"apply_to_all_doctypes": 1}) + return param diff --git a/frappe/core/doctype/user_permission/user_permission.json b/frappe/core/doctype/user_permission/user_permission.json index 9a38f8d953..c2ea05e731 100644 --- a/frappe/core/doctype/user_permission/user_permission.json +++ b/frappe/core/doctype/user_permission/user_permission.json @@ -41,7 +41,7 @@ "remember_last_selected_value": 0, "report_hide": 0, "reqd": 1, - "search_index": 0, + "search_index": 1, "set_only_once": 0, "translatable": 0, "unique": 0 @@ -222,7 +222,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-11-12 16:26:12.362352", + "modified": "2019-02-13 22:58:27.428741", "modified_by": "Administrator", "module": "Core", "name": "User Permission", diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index f8099cac38..88144f8078 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -42,6 +42,9 @@ def get_user_permissions(user=None): if not user: user = frappe.session.user + if user == "Administrator": + return {} + cached_user_permissions = frappe.cache().hget("user_permissions", user) if cached_user_permissions is not None: @@ -76,8 +79,8 @@ def get_user_permissions(user=None): out = frappe._dict(out) frappe.cache().hset("user_permissions", user, out) - except frappe.db.SQLError: - if frappe.db.is_table_missing(): + except frappe.db.SQLError as e: + if frappe.db.is_table_missing(e): # called from patch pass @@ -111,12 +114,100 @@ def get_permitted_documents(doctype): return [d.get('doc') for d in get_user_permissions().get(doctype, []) \ if d.get('doc')] +@frappe.whitelist() +def check_applicable_doc_perm(user, doctype, docname): + frappe.only_for('System Manager') + applicable = [] + doc_exists = frappe.get_all('User Permission', + fields=['name'], + filters={"user": user, + "allow": doctype, + "for_value": docname, + "apply_to_all_doctypes":1, + }, limit=1) + if doc_exists: + applicable = get_linked_doctypes(doctype).keys() + else: + data = frappe.get_all('User Permission', + fields=['applicable_for'], + filters={"user": user, + "allow": doctype, + "for_value":docname, + }) + for d in data: + applicable.append(d.applicable_for) + return applicable + + @frappe.whitelist() def clear_user_permissions(user, for_doctype): frappe.only_for('System Manager') - total = frappe.db.count('User Permission', filters = dict(user=user, allow=for_doctype)) if total: frappe.db.sql('DELETE FROM `tabUser Permission` WHERE `user`=%s AND `allow`=%s', (user, for_doctype)) frappe.clear_cache() return total + +@frappe.whitelist() +def add_user_permissions(data): + ''' Add and update the user permissions ''' + frappe.only_for('System Manager') + if isinstance(data, frappe.string_types): + data = json.loads(data) + data = frappe._dict(data) + + d = check_applicable_doc_perm(data.user, data.doctype, data.docname) + exists = frappe.db.exists("User Permission", {"user": data.user, "allow": data.doctype, "for_value": data.docname, "apply_to_all_doctypes": 1}) + if data.apply_to_all_doctypes == 1 and not exists: + remove_applicable(d, data.user, data.doctype, data.docname) + insert_user_perm(data.user, data.doctype, data.docname, apply_to_all = 1) + return 1 + else: + remove_apply_to_all(data.user, data.doctype, data.docname) + update_applicable(d, data.applicable_doctypes, data.user, data.doctype, data.docname) + for applicable in data.applicable_doctypes : + if applicable not in d: + insert_user_perm(data.user, data.doctype, data.docname, applicable = applicable) + elif exists: + insert_user_perm(data.user, data.doctype, data.docname, applicable = applicable) + return 1 + return 0 + +def insert_user_perm(user, doctype, docname, apply_to_all=None, applicable=None): + user_perm = frappe.new_doc("User Permission") + user_perm.user = user + user_perm.allow = doctype + user_perm.for_value = docname + if applicable: + user_perm.applicable_for = applicable + user_perm.apply_to_all_doctypes = 0 + else: + user_perm.apply_to_all_doctypes = 1 + user_perm.insert() + +def remove_applicable(d, user, doctype, docname): + for applicable_for in d: + frappe.db.sql("""DELETE FROM `tabUser Permission` + WHERE `user`=%s + AND `applicable_for`=%s + AND `allow`=%s + AND `for_value`=%s + """, (user, applicable_for, doctype, docname)) + +def remove_apply_to_all(user, doctype, docname): + frappe.db.sql("""DELETE from `tabUser Permission` + WHERE `user`=%s + AND `apply_to_all_doctypes`=1 + AND `allow`=%s + AND `for_value`=%s + """,(user, doctype, docname)) + +def update_applicable(already_applied, to_apply, user, doctype, docname): + for applied in already_applied: + if applied not in to_apply: + frappe.db.sql("""DELETE FROM `tabUser Permission` + WHERE `user`=%s + AND `applicable_for`=%s + AND `allow`=%s + AND `for_value`=%s + """,(user, applied, doctype, docname)) \ No newline at end of file diff --git a/frappe/core/doctype/user_permission/user_permission_list.js b/frappe/core/doctype/user_permission/user_permission_list.js index 39a4648334..00d829b2a0 100644 --- a/frappe/core/doctype/user_permission/user_permission_list.js +++ b/frappe/core/doctype/user_permission/user_permission_list.js @@ -1,22 +1,123 @@ frappe.listview_settings['User Permission'] = { + onload: function(list_view) { - list_view.page.add_menu_item(__("Clear User Permissions"), () => { + var me = this; + list_view.page.add_inner_button( __("Add / Update"), function() { + let dialog =new frappe.ui.Dialog({ + title : __('Add User Permissions'), + fields: [ + { + fieldname: 'user', + label: __('For User'), + fieldtype: 'Link', + options: 'User', + reqd: 1, + onchange: function() { + dialog.fields_dict.doctype.set_input(undefined); + dialog.fields_dict.docname.set_input(undefined); + dialog.set_df_property("docname", "hidden", 1); + dialog.set_df_property("apply_to_all_doctypes", "hidden", 1); + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } + }, + { + fieldname: 'doctype', + label: __('Document Type'), + fieldtype: 'Link', + options: 'DocType', + reqd: 1, + onchange: function() { + me.on_doctype_change(dialog); + } + }, + { + fieldname: 'docname', + label: __('Document Name'), + fieldtype: 'Dynamic Link', + options: 'doctype', + hidden: 1, + onchange: function() { + let field = dialog.fields_dict["docname"]; + if(field.value != field.last_value) { + if(dialog.fields_dict.doctype.value && dialog.fields_dict.docname.value && dialog.fields_dict.user.value){ + me.get_applicable_doctype(dialog).then(applicable => { + me.get_multi_select_options(dialog, applicable).then(options => { + me.applicable_options = options; + me.on_docname_change(dialog, options, applicable); + if(options.length > 5){ + dialog.fields_dict.applicable_doctypes.setup_select_all(); + } + }); + }); + } + } + } + }, + { + fieldname: 'apply_to_all_doctypes', + label: __('Apply to all Documents Types'), + fieldtype: 'Check', + checked: 1, + hidden: 1, + onchange: function() { + if(dialog.fields_dict.doctype.value && dialog.fields_dict.docname.value && dialog.fields_dict.user.value){ + me.on_apply_to_all_doctypes_change(dialog, me.applicable_options); + if(me.applicable_options.length > 5){ + dialog.fields_dict.applicable_doctypes.setup_select_all(); + } + } + } + }, + { + label: __("Applicable Document Types"), + fieldname: "applicable_doctypes", + fieldtype: "MultiCheck", + options: [], + columns: 2, + hidden: 1 + }, + ], + primary_action: (data) => { + data = me.validate(dialog, data); + frappe.call({ + async: false, + method: "frappe.core.doctype.user_permission.user_permission.add_user_permissions", + args: { + data : data + }, + callback: function(r) { + if(r.message === 1) { + frappe.show_alert({message:__("User Permissions created sucessfully"), indicator:'blue'}); + } else { + frappe.show_alert({message:__("Nothing to update"), indicator:'red'}); + + } + } + }); + dialog.hide(); + list_view.refresh(); + }, + primary_action_label: __('Submit') + }); + dialog.show(); + }); + list_view.page.add_inner_button( __("Bulk Delete"), function() { const dialog = new frappe.ui.Dialog({ title: __('Clear User Permissions'), fields: [ { - 'fieldname': 'user', - 'label': __('For User'), - 'fieldtype': 'Link', - 'options': 'User', - 'reqd': 1 + fieldname: 'user', + label: __('For User'), + fieldtype: 'Link', + options: 'User', + reqd: 1 }, { - 'fieldname': 'for_doctype', - 'label': __('For Document Type'), - 'fieldtype': 'Link', - 'options': 'DocType', - 'reqd': 1 + fieldname: 'for_doctype', + label: __('For Document Type'), + fieldtype: 'Link', + options: 'DocType', + reqd: 1 }, ], primary_action: (data) => { @@ -31,6 +132,8 @@ frappe.listview_settings['User Permission'] = { let message = ''; if (data === 0) { message = __('No records deleted'); + } else if(data === 1) { + message = __('{0} record deleted', [data]); } else { message = __('{0} records deleted', [data]); } @@ -43,10 +146,95 @@ frappe.listview_settings['User Permission'] = { }); }, - primary_action_label: __('Clear') + primary_action_label: __('Delete') }); dialog.show(); }); + }, + + validate: function(dialog, data) { + if(dialog.fields_dict.applicable_doctypes.get_unchecked_options().length == 0) { + data.apply_to_all_doctypes = 1; + data.applicable_doctypes = []; + return data; + } + if(data.apply_to_all_doctypes == 0 && !("applicable_doctypes" in data)) { + frappe.throw("Please select applicable Doctypes"); + } + return data; + }, + + get_applicable_doctype: function(dialog) { + return new Promise(resolve => { + frappe.call({ + method: 'frappe.core.doctype.user_permission.user_permission.check_applicable_doc_perm', + async: false, + args:{ + user: dialog.fields_dict.user.value, + doctype: dialog.fields_dict.doctype.value, + docname: dialog.fields_dict.docname.value + } + }).then(r => { + resolve(r.message); + }); + }); + }, + + get_multi_select_options: function(dialog, applicable){ + return new Promise(resolve => { + frappe.call({ + method: 'frappe.desk.form.linked_with.get_linked_doctypes', + async: false, + args:{ + user: dialog.fields_dict.user.value, + doctype: dialog.fields_dict.doctype.value, + docname: dialog.fields_dict.docname.value + } + }).then(r => { + var options = []; + for(var d in r.message){ + var checked = ($.inArray(d, applicable) != -1) ? 1 : 0; + options.push({ "label":d, "value": d , "checked": checked}); + } + resolve(options); + }); + }); + }, + + on_doctype_change: function(dialog) { + dialog.set_df_property("docname", "hidden", 0); + dialog.set_df_property("docname", "reqd", 1); + dialog.set_df_property("apply_to_all_doctypes", "hidden", 0); + dialog.set_value("apply_to_all_doctypes","checked",1); + }, + + on_docname_change: function(dialog, options, applicable) { + if(applicable.length != 0 ) { + dialog.set_primary_action("Update"); + dialog.set_title("Update User Permissions"); + dialog.set_df_property("applicable_doctypes", "options", options); + if(dialog.fields_dict.applicable_doctypes.get_checked_options().length == options.length) { + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } else { + dialog.set_df_property("applicable_doctypes", "hidden", 0); + dialog.set_df_property("apply_to_all_doctypes", "checked", 0); + } + } else { + dialog.set_primary_action("Submit"); + dialog.set_title("Add User Permissions"); + dialog.set_df_property("applicable_doctypes", "options", options); + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } + }, + + on_apply_to_all_doctypes_change: function(dialog, options) { + if(dialog.fields_dict.apply_to_all_doctypes.get_value() == 0) { + dialog.set_df_property("applicable_doctypes", "hidden", 0); + dialog.set_df_property("applicable_doctypes", "options", options); + } else { + dialog.set_df_property("applicable_doctypes", "options", options); + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } } -}; +}; \ No newline at end of file diff --git a/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.js b/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.js deleted file mode 100644 index d5293ddfe1..0000000000 --- a/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.js +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('User Permission for Page and Report', { - refresh: function(frm) { - frm.disable_save(); - frm.role_area.hide(); - }, - - onload: function(frm) { - if(!frm.roles_editor) { - frm.role_area = $('
') - .appendTo(frm.fields_dict.roles_html.wrapper); - frm.roles_editor = new frappe.RoleEditor(frm.role_area, frm); - } - }, - - page: function(frm) { - frm.trigger("get_roles"); - }, - - report: function(frm){ - frm.trigger("get_roles"); - }, - - get_roles: function(frm) { - frm.role_area.show(); - - return frappe.call({ - method:"get_custom_roles", - doc: frm.doc, - callback: function(r) { - refresh_field('roles'); - frm.roles_editor.show(); - } - }); - }, - - update: function(frm) { - if(frm.roles_editor) { - frm.roles_editor.set_roles_in_table(); - } - - return frappe.call({ - method:"set_custom_roles", - doc: frm.doc, - callback: function(r) { - refresh_field('roles'); - frm.roles_editor.show(); - frappe.msgprint(__("Successfully Updated")); - frm.reload_doc(); - } - }); - } -}); diff --git a/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.json b/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.json deleted file mode 100644 index 040a136347..0000000000 --- a/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.json +++ /dev/null @@ -1,309 +0,0 @@ -{ - "allow_copy": 1, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-02-13 17:33:25.157332", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "set_role_for", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Set Role For", - "length": 0, - "no_copy": 0, - "options": "\nPage\nReport", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.set_role_for == 'Page'", - "fieldname": "page", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Page", - "length": 0, - "no_copy": 0, - "options": "Page", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.set_role_for == 'Report'", - "fieldname": "report", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Report", - "length": 0, - "no_copy": 0, - "options": "Report", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "roles_permission", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Roles Permission", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "roles_html", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Roles Html", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "roles", - "fieldtype": "Table", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Roles", - "length": 0, - "no_copy": 0, - "options": "Has Role", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_8", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "update", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Update", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 1, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-21 04:24:24.963988", - "modified_by": "Administrator", - "module": "Core", - "name": "User Permission for Page and Report", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 -} \ No newline at end of file diff --git a/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.py b/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.py deleted file mode 100644 index 7efe3397f6..0000000000 --- a/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, 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 UserPermissionforPageandReport(Document): - def get_custom_roles(self): - args = self.get_args() - self.set('roles', []) - - name = frappe.db.get_value('Custom Role', args, "name") - if name: - doc = frappe.get_doc('Custom Role', name) - else: - doctype = self.set_role_for - docname = self.page if self.set_role_for == 'Page' else self.report - doc = frappe.get_doc(doctype, docname) - - self.set('roles', doc.roles) - - def set_custom_roles(self): - args = self.get_args() - name = frappe.db.get_value('Custom Role', args, "name") - - args.update({ - 'doctype': 'Custom Role', - 'roles': self.roles - }) - - if name: - doc = frappe.get_doc("Custom Role", name) - doc.set('roles', self.roles) - doc.save() - else: - frappe.get_doc(args).insert() - - def get_args(self, row=None): - name = self.page if self.set_role_for == 'Page' else self.report - check_for_field = self.set_role_for.replace(" ","_").lower() - - return { - check_for_field: name - } - - def update_status(self): - return frappe.render_template diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py index 619231e5cd..0f932d9f67 100644 --- a/frappe/core/notifications.py +++ b/frappe/core/notifications.py @@ -44,14 +44,14 @@ def get_todays_events(as_list=False): def get_unseen_likes(): """Returns count of unseen likes""" - return frappe.db.sql("""select count(*) from `tabCommunication` + return frappe.db.sql("""select count(*) from `tabComment` where - communication_type='Comment' + comment_type='Like' and modified >= (NOW() - INTERVAL '1' YEAR) - and comment_type='Like' and owner is not null and owner!=%(user)s and reference_owner=%(user)s - and seen=0""", {"user": frappe.session.user})[0][0] + and seen=0 + """, {"user": frappe.session.user})[0][0] def get_unread_emails(): "returns unread emails for a user" diff --git a/frappe/core/page/desktop/README.md b/frappe/core/page/desktop/README.md deleted file mode 100644 index 4ac65f50b5..0000000000 --- a/frappe/core/page/desktop/README.md +++ /dev/null @@ -1 +0,0 @@ -First screen after login. Array of module icons based on permission. \ No newline at end of file diff --git a/frappe/core/page/desktop/__init__.py b/frappe/core/page/desktop/__init__.py index 0e57cb68c3..e69de29bb2 100644 --- a/frappe/core/page/desktop/__init__.py +++ b/frappe/core/page/desktop/__init__.py @@ -1,3 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - diff --git a/frappe/core/page/desktop/all_applications_dialog.html b/frappe/core/page/desktop/all_applications_dialog.html deleted file mode 100644 index d26152c84b..0000000000 --- a/frappe/core/page/desktop/all_applications_dialog.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
-{% if (frappe.user.has_role("System Manager")) { %} -

Install new applications -

-{% } %} -
-

{%= __("Checked items will be shown on desktop") %}

-
- {% for(var i=0, l=all_modules.length; i < l; i++) { - var module_name = all_modules[i]; - var module = frappe.get_module(module_name); - if (desktop_items.indexOf(module_name)===-1 - || frappe.user.is_module_blocked(module_name)) { continue; } - %} -
-
- -
-
- {% } %} -
diff --git a/frappe/core/page/desktop/desktop.js b/frappe/core/page/desktop/desktop.js index 24dd0c3b4e..13b64c9246 100644 --- a/frappe/core/page/desktop/desktop.js +++ b/frappe/core/page/desktop/desktop.js @@ -1,344 +1 @@ -frappe.provide('frappe.desktop'); - -frappe.pages['desktop'].on_page_load = function(wrapper) { - - // load desktop - if(!frappe.list_desktop) { - frappe.desktop.set_background(); - } - frappe.desktop.refresh(wrapper); -}; - -frappe.pages['desktop'].on_page_show = function(wrapper) { - if(frappe.list_desktop) { - $("body").attr("data-route", "list-desktop"); - } -}; - -$.extend(frappe.desktop, { - refresh: function(wrapper) { - if (wrapper) { - this.wrapper = $(wrapper); - } - - this.render(); - this.make_sortable(); - }, - - render: function() { - var me = this; - frappe.utils.set_title(__("Desktop")); - - var template = frappe.list_desktop ? "desktop_list_view" : "desktop_icon_grid"; - - var all_icons = frappe.get_desktop_icons(); - var explore_icon = { - module_name: 'Explore', - label: 'Explore', - _label: __('Explore'), - _id: 'Explore', - _doctype: '', - icon: 'octicon octicon-telescope', - color: '#7578f6', - link: 'modules' - }; - explore_icon.app_icon = frappe.ui.app_icon.get_html(explore_icon); - all_icons.push(explore_icon); - - frappe.desktop.wrapper.html(frappe.render_template(template, { - // all visible icons - desktop_items: all_icons, - })); - - frappe.desktop.setup_module_click(); - - // notifications - frappe.desktop.show_pending_notifications(); - $(document).on("notification-update", function() { - me.show_pending_notifications(); - }); - - $(document).trigger("desktop-render"); - - }, - - render_help_messages: function(help_messages) { - var wrapper = frappe.desktop.wrapper.find('.help-message-wrapper'); - var $help_messages = wrapper.find('.help-messages'); - - var set_current_message = function(idx) { - idx = cint(idx); - wrapper.current_message_idx = idx; - wrapper.find('.left-arrow, .right-arrow').addClass('disabled'); - wrapper.find('.help-message-item').addClass('hidden'); - wrapper.find('[data-message-idx="'+idx+'"]').removeClass('hidden'); - if(idx > 0) { - wrapper.find('.left-arrow').removeClass('disabled'); - } - if(idx < help_messages.length - 1) { - wrapper.find('.right-arrow').removeClass('disabled'); - } - } - - if(help_messages) { - wrapper.removeClass('hidden'); - help_messages.forEach(function(message, i) { - var $message = $('') - .attr('data-message-idx', i) - .html(frappe.render_template('desktop_help_message', message)) - .appendTo($help_messages); - - }); - - set_current_message(0); - - wrapper.find('.close').on('click', function() { - wrapper.addClass('hidden'); - }); - } - - wrapper.find('.left-arrow').on('click', function() { - if(wrapper.current_message_idx) { - set_current_message(wrapper.current_message_idx - 1); - } - }) - - wrapper.find('.right-arrow').on('click', function() { - if(help_messages.length > wrapper.current_message_idx + 1) { - set_current_message(wrapper.current_message_idx + 1); - } - }); - - }, - - setup_module_click: function() { - frappe.desktop.wiggling = false; - - if(frappe.list_desktop) { - frappe.desktop.wrapper.on("click", ".desktop-list-item", function() { - frappe.desktop.open_module($(this)); - }); - } else { - frappe.desktop.wrapper.on("click", ".app-icon, .app-icon-svg", function() { - if ( !frappe.desktop.wiggling ) { - frappe.desktop.open_module($(this).parent()); - } - }); - } - frappe.desktop.wrapper.on("click", ".circle", function() { - var doctype = $(this).attr('data-doctype'); - if(doctype) { - frappe.ui.notifications.show_open_count_list(doctype); - } - }); - - frappe.desktop.setup_wiggle(); - }, - - setup_wiggle: () => { - // Wiggle, Wiggle, Wiggle. - const DURATION_LONG_PRESS = 1000; - - var timer_id = 0; - const $cases = frappe.desktop.wrapper.find('.case-wrapper'); - const $icons = frappe.desktop.wrapper.find('.app-icon'); - const $notis = $(frappe.desktop.wrapper.find('.circle').toArray().filter((object) => { - // This hack is so bad, I should punch myself. - // Seriously, punch yourself. - const text = $(object).find('.circle-text').html(); - - return text; - })); - - const clearWiggle = () => { - const $closes = $cases.find('.module-remove'); - $closes.hide(); - $notis.show(); - - $icons.removeClass('wiggle'); - - frappe.desktop.wiggling = false; - }; - - frappe.desktop.wrapper.on('mousedown', '.app-icon', () => { - timer_id = setTimeout(() => { - frappe.desktop.wiggling = true; - // hide all notifications. - $notis.hide(); - - $cases.each((i) => { - const $case = $($cases[i]); - const template = - ` -
-
- - × - -
-
- `; - - $case.append(template); - const $close = $case.find('.module-remove'); - const name = $case.attr('title'); - $close.click(() => { - // good enough to create dynamic dialogs? - const dialog = new frappe.ui.Dialog({ - title: __(`Hide ${name}?`) - }); - dialog.set_primary_action(__('Hide'), () => { - frappe.call({ - method: 'frappe.desk.doctype.desktop_icon.desktop_icon.hide', - args: { name: name }, - freeze: true, - callback: (response) => - { - if ( response.message ) { - location.reload(); - } - } - }) - - dialog.hide(); - - clearWiggle(); - }); - // Hacks, Hacks and Hacks. - var $cancel = dialog.get_close_btn(); - $cancel.click(() => { - clearWiggle(); - }); - $cancel.html(__(`Cancel`)); - - dialog.show(); - }); - }); - - $icons.addClass('wiggle'); - - }, DURATION_LONG_PRESS); - }); - frappe.desktop.wrapper.on('mouseup mouseleave', '.app-icon', () => { - clearTimeout(timer_id); - }); - - // also stop wiggling if clicked elsewhere. - $('body').click((event) => { - if ( frappe.desktop.wiggling ) { - const $target = $(event.target); - // our target shouldn't be .app-icons or .close - const $parent = $target.parents('.case-wrapper'); - if ( $parent.length == 0 ) - clearWiggle(); - } - }); - // end wiggle - }, - - open_module: function(parent) { - var link = parent.attr("data-link"); - if(link) { - if(link.indexOf('javascript:')===0) { - eval(link.substr(11)); - } else if(link.substr(0, 1)==="/" || link.substr(0, 4)==="http") { - window.open(link, "_blank"); - } else { - frappe.set_route(link); - } - return false; - } else { - var module = frappe.get_module(parent.attr("data-name")); - if (module && module.onclick) { - module.onclick(); - return false; - } - } - }, - - make_sortable: function() { - if (frappe.dom.is_touchscreen() || frappe.list_desktop) { - return; - } - - new Sortable($("#icon-grid").get(0), { - animation: 150, - onUpdate: function(event) { - var new_order = []; - $("#icon-grid .case-wrapper").each(function(i, e) { - new_order.push($(this).attr("data-name")); - }); - - frappe.call({ - method: 'frappe.desk.doctype.desktop_icon.desktop_icon.set_order', - args: { - 'new_order': new_order, - 'user': frappe.session.user - }, - quiet: true - }); - } - }); - }, - - set_background: function() { - frappe.ui.set_user_background(frappe.boot.user.background_image, null, - frappe.boot.user.background_style); - }, - - show_pending_notifications: function() { - var modules_list = frappe.get_desktop_icons(); - for (var i=0, l=modules_list.length; i < l; i++) { - var module = modules_list[i]; - - var module_doctypes = frappe.boot.notification_info.module_doctypes[module.module_name]; - - var sum = 0; - - if(module_doctypes && frappe.boot.notification_info.open_count_doctype) { - // sum all doctypes for a module - for (var j=0, k=module_doctypes.length; j < k; j++) { - var doctype = module_doctypes[j]; - let count = (frappe.boot.notification_info.open_count_doctype[doctype] || 0); - count = typeof count == "string" ? parseInt(count) : count; - sum += count; - } - } - - if(frappe.boot.notification_info.open_count_doctype - && frappe.boot.notification_info.open_count_doctype[module.module_name]!=null) { - // notification count explicitly for doctype - let count = frappe.boot.notification_info.open_count_doctype[module.module_name] || 0; - count = typeof count == "string" ? parseInt(count) : count; - sum += count; - } - - if(frappe.boot.notification_info.open_count_module - && frappe.boot.notification_info.open_count_module[module.module_name]!=null) { - // notification count explicitly for module - let count = frappe.boot.notification_info.open_count_module[module.module_name] || 0; - count = typeof count == "string" ? parseInt(count) : count; - sum += count; - } - - // if module found - if(module._id.indexOf('/')===-1 && !module._report) { - var notifier = $(".module-count-" + frappe.scrub(module._id)); - if(notifier.length) { - notifier.toggle(sum ? true : false); - var circle = notifier.find(".circle-text"); - var text = sum || ''; - if(text > 99) { - text = '99+'; - } - - if(circle.length) { - circle.html(text); - } else { - notifier.html(text); - } - } - } - } - } -}); +frappe.pages['desktop'].on_page_load = function() {}; \ No newline at end of file diff --git a/frappe/core/page/desktop/desktop.json b/frappe/core/page/desktop/desktop.json index 6255e3e6fe..66bbfbfd40 100644 --- a/frappe/core/page/desktop/desktop.json +++ b/frappe/core/page/desktop/desktop.json @@ -1,10 +1,11 @@ { - "creation": "2013-02-14 17:37:37.000000", + "content": null, + "creation": "2019-01-29 13:11:48.872579", "docstatus": 0, "doctype": "Page", - "icon": "fa fa-th", - "idx": 1, - "modified": "2013-07-11 14:41:56.000000", + "icon": "icon-th", + "idx": 0, + "modified": "2019-01-29 13:11:48.872579", "modified_by": "Administrator", "module": "Core", "name": "desktop", @@ -15,6 +16,9 @@ "role": "All" } ], + "script": null, "standard": "Yes", + "style": null, + "system_page": 0, "title": "Desktop" } \ No newline at end of file diff --git a/frappe/core/page/desktop/desktop.py b/frappe/core/page/desktop/desktop.py deleted file mode 100644 index f426a67979..0000000000 --- a/frappe/core/page/desktop/desktop.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import unicode_literals - -import functools -import frappe -from past.builtins import cmp - -@frappe.whitelist() -def get_help_messages(): - '''Return help messages for the desktop (called via `get_help_messages` hook) - - Format for message: - - { - title: _('Add Employees to Manage Them'), - description: _('Add your Employees so you can manage leaves, expenses and payroll'), - action: 'Add Employee', - route: 'List/Employee' - } - - ''' - messages = [] - for fn in frappe.get_hooks('get_help_messages'): - messages += frappe.get_attr(fn)() - - return sorted(messages, key = functools.cmp_to_key(lambda a, b: cmp(a.get('count'), b.get('count')))) diff --git a/frappe/core/page/desktop/desktop_help_message.html b/frappe/core/page/desktop/desktop_help_message.html deleted file mode 100644 index 7de47abf03..0000000000 --- a/frappe/core/page/desktop/desktop_help_message.html +++ /dev/null @@ -1,8 +0,0 @@ -
{{ title }}
-

{{ description }}

-
- {{ action }} - - - -
\ No newline at end of file diff --git a/frappe/core/page/desktop/desktop_icon_grid.html b/frappe/core/page/desktop/desktop_icon_grid.html deleted file mode 100644 index 6b85c8a56e..0000000000 --- a/frappe/core/page/desktop/desktop_icon_grid.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
- {% for (var i=0, l=desktop_items.length; i < l; i++) { %} - {{ frappe.render_template("desktop_module_icon", desktop_items[i]) }} - {% } %} -
- -
-
diff --git a/frappe/core/page/desktop/desktop_list_view.html b/frappe/core/page/desktop/desktop_list_view.html deleted file mode 100644 index d3dd514076..0000000000 --- a/frappe/core/page/desktop/desktop_list_view.html +++ /dev/null @@ -1,23 +0,0 @@ -
-
-
-
- {% for (var i=0, l=desktop_items.length; i < l; i++) { - var module = desktop_items[i]; - %} -
-

- - {{ module._label }} -

- -
- {% } %} -
-
-
-
diff --git a/frappe/core/page/desktop/desktop_module_icon.html b/frappe/core/page/desktop/desktop_module_icon.html deleted file mode 100644 index 3e9a451eec..0000000000 --- a/frappe/core/page/desktop/desktop_module_icon.html +++ /dev/null @@ -1,11 +0,0 @@ -
- {{ app_icon }} -
- - - {{ _label }} -
-
diff --git a/frappe/core/page/recorder/__init__.py b/frappe/core/page/recorder/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/page/recorder/recorder.js b/frappe/core/page/recorder/recorder.js new file mode 100644 index 0000000000..f38af41af0 --- /dev/null +++ b/frappe/core/page/recorder/recorder.js @@ -0,0 +1,26 @@ +frappe.pages['recorder'].on_page_load = function(wrapper) { + frappe.ui.make_app_page({ + parent: wrapper, + title: 'Recorder', + single_column: true + }); + + frappe.recorder = new Recorder(wrapper); + $(wrapper).bind('show', function() { + frappe.recorder.show(); + }); + + frappe.require('/assets/js/frappe-recorder.min.js'); +}; + +class Recorder { + constructor(wrapper) { + this.wrapper = $(wrapper); + this.container = this.wrapper.find('.layout-main-section'); + this.container.append($('
')); + } + + show() { + + } +} diff --git a/frappe/core/page/recorder/recorder.json b/frappe/core/page/recorder/recorder.json new file mode 100644 index 0000000000..43dfbc0e09 --- /dev/null +++ b/frappe/core/page/recorder/recorder.json @@ -0,0 +1,23 @@ +{ + "content": null, + "creation": "2019-02-08 08:17:45.392739", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2019-02-08 08:23:04.416426", + "modified_by": "Administrator", + "module": "Core", + "name": "recorder", + "owner": "Administrator", + "page_name": "Recorder", + "roles": [ + { + "role": "Administrator" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Recorder" +} \ No newline at end of file diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 125db507ea..0a6f6d4d54 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -119,7 +119,7 @@ frappe.ui.form.on("Customize Form", { frm.set_df_property("sort_field", "options", fields); } - if(frappe.route_options) { + if(frappe.route_options && frappe.route_options.doc_type) { setTimeout(function() { frm.set_value("doc_type", frappe.route_options.doc_type); frappe.route_options = null; diff --git a/frappe/database/database.py b/frappe/database/database.py index 136e25cd93..c9b184cbd1 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -881,9 +881,14 @@ class Database(object): def get_descendants(self, doctype, name): '''Return descendants of the current record''' - lft, rgt = self.get_value(doctype, name, ('lft', 'rgt')) - return self.sql_list('''select name from `tab{doctype}` - where lft > {lft} and rgt < {rgt}'''.format(doctype=doctype, lft=lft, rgt=rgt)) + node_location_indexes = self.get_value(doctype, name, ('lft', 'rgt')) + if node_location_indexes: + lft, rgt = node_location_indexes + return self.sql_list('''select name from `tab{doctype}` + where lft > {lft} and rgt < {rgt}'''.format(doctype=doctype, lft=lft, rgt=rgt)) + else: + # when document does not exist + return [] def is_missing_table_or_column(self, e): return self.is_missing_column(e) or self.is_missing_table(e) diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 30ef20467f..8a95bba228 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -27,7 +27,7 @@ class MariaDBDatabase(Database): self.type_map = { 'Currency': ('decimal', '18,6'), 'Int': ('int', '11'), - 'Long Int': ('bigint', '20'), # convert int to bigint if length is more than 11 + 'Long Int': ('bigint', '20'), 'Float': ('decimal', '18,6'), 'Percent': ('decimal', '18,6'), 'Check': ('int', '1'), diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index be7cf060c9..cfbbb80b4c 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -32,7 +32,7 @@ class PostgresDatabase(Database): self.type_map = { 'Currency': ('decimal', '18,6'), 'Int': ('bigint', None), - 'Long Int': ('bigint', None), # convert int to bigint if length is more than 11 + 'Long Int': ('bigint', None), 'Float': ('decimal', '18,6'), 'Percent': ('decimal', '18,6'), 'Check': ('smallint', None), @@ -40,8 +40,8 @@ class PostgresDatabase(Database): 'Long Text': ('text', ''), 'Code': ('text', ''), 'Text Editor': ('text', ''), - 'Markdown Editor': ('longtext', ''), - 'HTML Editor': ('longtext', ''), + 'Markdown Editor': ('text', ''), + 'HTML Editor': ('text', ''), 'Date': ('date', ''), 'Datetime': ('timestamp', None), 'Time': ('time', '6'), diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.json b/frappe/desk/doctype/desktop_icon/desktop_icon.json index ee9765e944..59c95953ad 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.json +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, @@ -14,6 +15,7 @@ "fields": [ { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -40,10 +42,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -70,10 +74,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -100,10 +106,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -130,10 +138,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -159,10 +169,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -189,10 +201,76 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "description", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Description", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "category", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Category", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -219,10 +297,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -249,10 +329,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -279,10 +361,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -308,10 +392,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -339,10 +425,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -370,10 +458,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -401,10 +491,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -431,10 +523,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -460,10 +554,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -490,10 +586,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -520,10 +618,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -550,10 +650,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -580,6 +682,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 } ], @@ -593,7 +696,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-05-08 15:41:31.121652", + "modified": "2019-01-24 04:58:58.720618", "modified_by": "Administrator", "module": "Desk", "name": "Desktop Icon", @@ -602,7 +705,6 @@ "permissions": [ { "amend": 0, - "apply_user_permissions": 0, "cancel": 0, "create": 1, "delete": 1, @@ -629,5 +731,6 @@ "sort_order": "DESC", "title_field": "module_name", "track_changes": 1, - "track_seen": 0 + "track_seen": 0, + "track_views": 0 } \ No newline at end of file diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index 175d1ece7d..fcf10ef61d 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -31,7 +31,7 @@ def get_desktop_icons(user=None): user_icons = frappe.cache().hget('desktop_icons', user) if not user_icons: - fields = ['module_name', 'hidden', 'label', 'link', 'type', 'icon', 'color', + fields = ['module_name', 'hidden', 'label', 'link', 'type', 'icon', 'color', 'description', 'category', '_doctype', '_report', 'idx', 'force_show', 'reverse', 'custom', 'standard', 'blocked'] active_domains = frappe.get_active_domains() diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py index ac8775730d..a53599db7f 100644 --- a/frappe/desk/form/assign_to.py +++ b/frappe/desk/form/assign_to.py @@ -6,7 +6,6 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.desk.form.load import get_docinfo import frappe.share class DuplicateToDoError(frappe.ValidationError): pass @@ -16,15 +15,11 @@ def get(args=None): if not args: args = frappe.local.form_dict - get_docinfo(frappe.get_doc(args.get("doctype"), args.get("name"))) - - return frappe.db.sql("""SELECT `owner`, `description` - FROM `tabToDo` - WHERE reference_type=%(doctype)s - AND reference_name=%(name)s - AND status='Open' - ORDER BY modified DESC - LIMIT 5""", args, as_dict=True) + return frappe.get_all('ToDo', fields = ['owner', 'description'], filters = dict( + reference_type = args.get('doctype'), + reference_name = args.get('name'), + status = 'Open' + ), limit = 5) @frappe.whitelist() def add(args=None): @@ -100,7 +95,8 @@ def add_multiple(args=None): add(args) def remove_from_todo_if_already_assigned(doctype, docname): - owner = frappe.db.get_value("ToDo", {"reference_type": doctype, "reference_name": docname, "status":"Open"}, "owner") + owner = frappe.db.get_value("ToDo", {"reference_type": doctype, "reference_name": docname, + "status":"Open"}, "owner") if owner: remove(doctype, docname, owner) @@ -125,9 +121,18 @@ def remove(doctype, name, assign_to): return get({"doctype": doctype, "name": name}) def clear(doctype, name): - for assign_to in frappe.db.sql_list("""select owner from `tabToDo` - where reference_type=%(doctype)s and reference_name=%(name)s""", locals()): - remove(doctype, name, assign_to) + ''' + Clears assignments, return False if not assigned. + ''' + assignments = frappe.db.get_all('ToDo', fields=['owner'], filters = + dict(reference_type = doctype, reference_name = name)) + if not assignments: + return False + + for assign_to in assignments: + remove(doctype, name, assign_to.owner) + + return True def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE', description=None, notify=0): diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index c64cc6cfab..7a8b059baa 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -94,6 +94,7 @@ def get_docinfo(doc=None, doctype=None, name=None): frappe.response["docinfo"] = { "attachments": get_attachments(doc.doctype, doc.name), "communications": _get_communications(doc.doctype, doc.name), + 'comments': get_comments(doc.doctype, doc.name), 'total_comments': len(json.loads(doc.get('_comments') or '[]')), 'versions': get_versions(doc), "assignments": get_assignments(doc.doctype, doc.name), @@ -120,6 +121,19 @@ def get_communications(doctype, name, start=0, limit=20): return _get_communications(doctype, name, start, limit) +def get_comments(doctype, name): + comments = frappe.get_all('Comment', fields = ['*'], filters = dict( + reference_doctype = doctype, + reference_name = name + )) + + # convert to markdown (legacy ?) + for c in comments: + if c.comment_type == 'Comment': + c.content = frappe.utils.markdown(c.content) + + return comments + def _get_communications(doctype, name, start=0, limit=20): communications = get_communication_data(doctype, name, start, limit) for c in communications: @@ -130,8 +144,6 @@ def _get_communications(doctype, name, start=0, limit=20): "attached_to_name": c.name} )) - elif c.communication_type=="Comment" and c.comment_type=="Comment": - c.content = frappe.utils.markdown(c.content) return communications def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=None, @@ -144,17 +156,13 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= `timeline_doctype`, `timeline_name`, `reference_doctype`, `reference_name`, `link_doctype`, `link_name`, `read_by_recipient`, `rating`, 'Communication' AS `doctype`''' - conditions = '''communication_type in ('Communication', 'Comment', 'Feedback') + conditions = '''communication_type in ('Communication', 'Feedback') and ( (reference_doctype=%(doctype)s and reference_name=%(name)s) or ( - (timeline_doctype=%(doctype)s and timeline_name=%(name)s) - and ( - communication_type='Communication' - or ( - communication_type='Comment' - and comment_type in ('Created', 'Updated', 'Submitted', 'Cancelled', 'Deleted') - ))) + (timeline_doctype=%(doctype)s and timeline_name=%(name)s) + and (communication_type='Communication') + ) )''' diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 403da16361..a0a33f5d42 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -57,23 +57,23 @@ def validate_link(): frappe.response['message'] = 'Ok' @frappe.whitelist() -def add_comment(doc): +def add_comment(reference_doctype, reference_name, content, comment_email): """allow any logged user to post a comment""" - doc = frappe.get_doc(json.loads(doc)) - - doc.content = clean_email_html(doc.content) - - if not (doc.doctype=="Communication" and doc.communication_type=='Comment'): - frappe.throw(_("This method can only be used to create a Comment"), frappe.PermissionError) - - doc.insert(ignore_permissions=True) + doc = frappe.get_doc(dict( + doctype = 'Comment', + reference_doctype = reference_doctype, + reference_name = reference_name, + content = clean_email_html(content), + comment_email = comment_email, + comment_type = 'Comment' + )).insert(ignore_permissions = True) return doc.as_dict() @frappe.whitelist() def update_comment(name, content): """allow only owner to update comment""" - doc = frappe.get_doc('Communication', name) + doc = frappe.get_doc('Comment', name) if frappe.session.user not in ['Administrator', doc.owner]: frappe.throw(_('Comment can only be edited by the owner'), frappe.PermissionError) diff --git a/frappe/desk/like.py b/frappe/desk/like.py index ec01eace83..e7d1ff298c 100644 --- a/frappe/desk/like.py +++ b/frappe/desk/like.py @@ -64,13 +64,12 @@ def _toggle_like(doctype, name, add, user=None): def remove_like(doctype, name): """Remove previous Like""" # remove Comment - frappe.delete_doc("Communication", [c.name for c in frappe.get_all("Communication", + frappe.delete_doc("Comment", [c.name for c in frappe.get_all("Comment", filters={ - "communication_type": "Comment", + "comment_type": "Like", "reference_doctype": doctype, "reference_name": name, "owner": frappe.session.user, - "comment_type": "Like" } )], ignore_permissions=True) diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py index f5a1e47bf4..d5b62d1406 100644 --- a/frappe/desk/moduleview.py +++ b/frappe/desk/moduleview.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +import json from frappe import _ from frappe.boot import get_allowed_pages, get_allowed_reports from frappe.desk.doctype.desktop_icon.desktop_icon import set_hidden, clear_desktop_icons_cache @@ -24,7 +25,7 @@ def hide_module(module): set_hidden(module, frappe.session.user, 1) clear_desktop_icons_cache() -def get_data(module): +def get_data(module, build=True): """Get module data for the module view `desk/#Module/[name]`""" doctype_info = get_doctype_info(module) data = build_config_from_file(module) @@ -40,7 +41,43 @@ def get_data(module): data = combine_common_sections(data) data = apply_permissions(data) - #set_last_modified(data) + # set_last_modified(data) + + if build: + exists_cache = {} + def doctype_contains_a_record(name): + exists = exists_cache.get(name) + if not exists: + if not frappe.db.get_value('DocType', name, 'issingle'): + exists = frappe.db.count(name) + else: + exists = True + exists_cache[name] = exists + return exists + + for section in data: + for item in section["items"]: + # Onboarding + + # First disable based on exists of depends_on list + doctype = item.get("doctype") + dependencies = item.get("dependencies") or None + if not dependencies and doctype: + item["dependencies"] = [doctype] + + dependencies = item.get("dependencies") + if dependencies: + incomplete_dependencies = [d for d in dependencies if not doctype_contains_a_record(d)] + if len(incomplete_dependencies): + item["incomplete_dependencies"] = incomplete_dependencies + + if item.get("onboard"): + # Mark Spotlights for initial + if item.get("type") == "doctype": + name = item.get("name") + count = doctype_contains_a_record(name) + + item["count"] = count return data @@ -184,14 +221,24 @@ def get_config(app, module): config = frappe.get_module("{app}.config.{module}".format(app=app, module=module)) config = config.get_data() - for section in config: + sections = [s for s in config if s.get("condition", True)] + + for section in sections: for item in section["items"]: if item["type"]=="report" and frappe.db.get_value("Report", item["name"], "disabled")==1: section["items"].remove(item) continue if not "label" in item: item["label"] = _(item["name"]) - return config + + return sections + +def config_exists(app, module): + try: + frappe.get_module("{app}.config.{module}".format(app=app, module=module)) + return True + except ImportError: + return False def add_setup_section(config, app, module, label, icon): """Add common sections to `/desk#Module/Setup`""" @@ -213,6 +260,75 @@ def get_setup_section(app, module, label, icon): "items": section["items"] } + +def get_onboard_items(app, module): + try: + sections = get_config(app, module) + except ImportError: + return [] + + onboard_items = [] + fallback_items = [] + + if not sections: + doctype_info = get_doctype_info(module) + sections = build_standard_config(module, doctype_info) + + for section in sections: + for item in section["items"]: + if item.get("onboard", 0) == 1: + onboard_items.append(item) + + # in case onboard is not set + fallback_items.append(item) + + if len(onboard_items) > 5: + return onboard_items + + return onboard_items or fallback_items + + +@frappe.whitelist() +def get_links(app, module): + try: + sections = get_config(app, frappe.scrub(module)) + except ImportError: + return [] + + link_names = [] + + for section in sections: + for item in section["items"]: + link_names.append(item.get("label")) + print(link_names) + return link_names + + +@frappe.whitelist() +def get_module_link_items_from_dict(module_link_list_map): + module_link_list_map = json.loads(module_link_list_map) + module_links = {} + for module, data in module_link_list_map.items(): + print(data) + module_links[module] = get_module_link_items_from_list(data["app"], module, data["links"]) + return module_links + + +def get_module_link_items_from_list(app, module, list_of_link_names): + try: + sections = get_config(app, frappe.scrub(module)) + except ImportError: + return [] + + links = [] + for section in sections: + for item in section["items"]: + if item.get("label", "") in list_of_link_names: + links.append(item) + + return links + + def set_last_modified(data): for section in data: for item in section["items"]: diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 43b67f2976..c700e8f046 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -251,6 +251,11 @@ def get_open_count(doctype, name, items=[]): :param transactions: List of transactions (json/dict) :param filters: optional filters (json/list)''' + if frappe.flags.in_migrate or frappe.flags.in_install: + return { + 'count': [] + } + frappe.has_permission(doc=frappe.get_doc(doctype, name), throw=True) meta = frappe.get_meta(doctype) diff --git a/frappe/desk/page/activity/activity.js b/frappe/desk/page/activity/activity.js index 4f012512e4..fe81fdf0fa 100644 --- a/frappe/desk/page/activity/activity.js +++ b/frappe/desk/page/activity/activity.js @@ -53,14 +53,7 @@ frappe.pages['activity'].on_page_load = function(wrapper) { } frappe.set_route("List", "Activity Log", "Report"); - }, 'fa fa-th') - - this.page.add_menu_item(__('Show Likes'), function() { - frappe.route_options = { - show_likes: true - }; - me.page.list.refresh(); - }, 'octicon octicon-heart'); + }, 'fa fa-th'); }; frappe.pages['activity'].on_page_show = function() { @@ -158,9 +151,7 @@ frappe.activity.render_heatmap = function(page) { data: {} }); - heatmap.update({ - dataPoints: r.message - }); + heatmap.update(r.message); } } }) @@ -196,8 +187,7 @@ frappe.views.Activity = class Activity extends frappe.views.BaseList { get_args() { return { start: this.start, - page_length: this.page_length, - show_likes: (frappe.route_options || {}).show_likes || 0 + page_length: this.page_length }; } diff --git a/frappe/desk/page/activity/activity.py b/frappe/desk/page/activity/activity.py index 54d643ade2..e25c94d0e5 100644 --- a/frappe/desk/page/activity/activity.py +++ b/frappe/desk/page/activity/activity.py @@ -7,43 +7,45 @@ from frappe.utils import cint from frappe.core.doctype.activity_log.feed import get_feed_match_conditions @frappe.whitelist() -def get_feed(start, page_length, show_likes=False): +def get_feed(start, page_length): """get feed""" - match_conditions = get_feed_match_conditions(frappe.session.user) + match_conditions_communication = get_feed_match_conditions(frappe.session.user, 'Communication') + match_conditions_comment = get_feed_match_conditions(frappe.session.user, 'Comment') result = frappe.db.sql("""select X.* from (select name, owner, modified, creation, seen, comment_type, - reference_doctype, reference_name, link_doctype, link_name, subject, - communication_type, communication_medium, content - from `tabCommunication` + reference_doctype, reference_name, link_doctype, link_name, subject, + communication_type, communication_medium, content + from + `tabCommunication` where - communication_type in ("Communication", "Comment") - and communication_medium != "Email" - and (comment_type is null or comment_type != "Like" - or (comment_type="Like" and (owner=%(user)s or reference_owner=%(user)s))) - {match_conditions} - {show_likes} - union + communication_type = "Communication" + and communication_medium != "Email" + and {match_conditions_communication} + UNION select name, owner, modified, creation, '0', 'Updated', - reference_doctype, reference_name, link_doctype, link_name, subject, - 'Comment', '', content - from `tabActivity Log`) X + reference_doctype, reference_name, link_doctype, link_name, subject, + 'Comment', '', content + from + `tabActivity Log` + UNION + select name, owner, modified, creation, '0', comment_type, + reference_doctype, reference_name, link_doctype, link_name, '', + 'Comment', '', content + from + `tabComment` + where + {match_conditions_comment} + ) X order by X.creation DESC limit %(start)s, %(page_length)s""" - .format(match_conditions="and {0}".format(match_conditions) if match_conditions else "", - show_likes="and comment_type='Like'" if show_likes else ""), - { + .format(match_conditions_comment = match_conditions_comment, + match_conditions_communication = match_conditions_communication), { "user": frappe.session.user, "start": cint(start), "page_length": cint(page_length) }, as_dict=True) - if show_likes: - # mark likes as seen! - frappe.db.sql("""update `tabCommunication` set seen=1 - where comment_type='Like' and reference_owner=%s""", frappe.session.user) - frappe.local.flags.commit = True - return result @frappe.whitelist() diff --git a/frappe/desk/page/modules/modules.js b/frappe/desk/page/modules/modules.js deleted file mode 100644 index 80048fbf59..0000000000 --- a/frappe/desk/page/modules/modules.js +++ /dev/null @@ -1,185 +0,0 @@ -frappe.pages['modules'].on_page_load = function(wrapper) { - var page = frappe.ui.make_app_page({ - parent: wrapper, - title: 'Modules', - single_column: false - }); - - frappe.modules_page = page; - frappe.module_links = {}; - page.section_data = {}; - - page.wrapper.find('.page-head h1').css({'padding-left': '15px'}); - // page.wrapper.find('.page-content').css({'margin-top': '0px'}); - - // menu - page.add_menu_item(__('Set Desktop Icons'), function() { - frappe.frappe_toolbar.modules_select - .show(frappe.session.user); - }); - - if(frappe.user.has_role('System Manager')) { - page.add_menu_item(__('Install Apps'), function() { - frappe.set_route("applications"); - }); - } - - page.get_page_modules = () => { - return frappe.get_desktop_icons(true) - .filter(d => d.type==='module' && !d.blocked) - .sort((a, b) => { return (a._label > b._label) ? 1 : -1; }); - }; - - let get_module_sidebar_item = (item) => `
  • - - - ${item._label} - -
  • `; - - let get_sidebar_html = () => { - let sidebar_items_html = page.get_page_modules() - .map(get_module_sidebar_item.bind(this)).join(""); - - return ``; - }; - - // render sidebar - page.sidebar.html(get_sidebar_html()); - - // help click - page.main.on("click", '.module-section-link[data-type="help"]', function() { - frappe.help.show_video($(this).attr("data-youtube-id")); - return false; - }); - - // notifications click - page.main.on("click", '.open-notification', function() { - var doctype = $(this).attr('data-doctype'); - if(doctype) { - frappe.ui.notifications.show_open_count_list(doctype); - } - }); - - page.activate_link = function(link) { - page.last_link = link; - page.wrapper.find('.module-sidebar-item.active, .module-link.active').removeClass('active'); - $(link).addClass('active').parent().addClass("active"); - show_section($(link).attr('data-name')); - }; - - var show_section = function(module_name) { - if (!module_name) return; - if(module_name in page.section_data) { - render_section(page.section_data[module_name]); - } else { - page.main.empty(); - return frappe.call({ - method: "frappe.desk.moduleview.get", - args: { - module: module_name - }, - callback: function(r) { - var m = frappe.get_module(module_name); - m.data = r.message.data; - process_data(module_name, m.data); - page.section_data[module_name] = m; - render_section(m); - }, - freeze: true, - }); - } - - }; - - var render_section = function(m) { - page.set_title(__(m.label)); - page.main.html(frappe.render_template('modules_section', m)); - - // if(frappe.utils.is_xs() || frappe.utils.is_sm()) { - // // call this after a timeout, becuase a refresh will set the page to the top - // setTimeout(function() { - // $(document).scrollTop($('.module-body').offset().top - 150); - // }, 100); - // } - - //setup_section_toggle(); - frappe.app.update_notification_count_in_modules(); - }; - - var process_data = function(module_name, data) { - frappe.module_links[module_name] = []; - data.forEach(function(section) { - section.items.forEach(function(item) { - item.style = ''; - if(item.type==="doctype") { - item.doctype = item.name; - - // map of doctypes that belong to a module - frappe.module_links[module_name].push(item.name); - } - if(!item.route) { - if(item.link) { - item.route=strip(item.link, "#"); - } - else if(item.type==="doctype") { - if(frappe.model.is_single(item.doctype)) { - item.route = 'Form/' + item.doctype; - } else { - if (item.filters) { - frappe.route_options=item.filters; - } - item.route="List/" + item.doctype; - //item.style = 'font-weight: 500;'; - } - // item.style = 'font-weight: bold;'; - } - else if(item.type==="report" && item.is_query_report) { - item.route="query-report/" + item.name; - } - else if(item.type==="report") { - item.route="List/" + item.doctype + "/Report/" + item.name; - } - else if(item.type==="page") { - item.route=item.name; - } - } - - if(item.route_options) { - item.route += "?" + $.map(item.route_options, function(value, key) { - return encodeURIComponent(key) + "=" + encodeURIComponent(value); }).join('&'); - } - - if(item.type==="page" || item.type==="help" || item.type==="report" || - (item.doctype && frappe.model.can_read(item.doctype))) { - item.shown = true; - } - }); - }); - }; -}; - -frappe.pages['modules'].on_page_show = function(wrapper) { - let route = frappe.get_route(); - let modules = frappe.modules_page.get_page_modules().map(d => d.module_name); - $("body").attr("data-sidebar", 1); - if(route.length > 1) { - // activate section based on route - let module_name = route[1]; - if(modules.includes(module_name)) { - frappe.modules_page.activate_link( - frappe.modules_page.sidebar.find('.module-link[data-name="'+ module_name +'"]')); - } else { - frappe.throw(__(`Module ${module_name} not found.`)); - } - } else if(frappe.modules_page.last_link) { - // open last link - frappe.set_route('modules', frappe.modules_page.last_link.attr('data-name')); - } else { - // first time, open the first page - frappe.modules_page.activate_link(frappe.modules_page.sidebar.find('.module-link:first')); - } -}; diff --git a/frappe/desk/page/modules/modules.json b/frappe/desk/page/modules/modules.json deleted file mode 100644 index 1858a921b2..0000000000 --- a/frappe/desk/page/modules/modules.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "content": null, - "creation": "2016-03-07 04:46:00.420330", - "docstatus": 0, - "doctype": "Page", - "idx": 0, - "modified": "2016-03-07 04:46:00.420330", - "modified_by": "Administrator", - "module": "Desk", - "name": "modules", - "owner": "Administrator", - "page_name": "modules", - "roles": [], - "script": null, - "standard": "Yes", - "style": null, - "title": "Modules" -} \ No newline at end of file diff --git a/frappe/desk/page/modules/modules_section.html b/frappe/desk/page/modules/modules_section.html deleted file mode 100644 index 5d23db795a..0000000000 --- a/frappe/desk/page/modules/modules_section.html +++ /dev/null @@ -1,30 +0,0 @@ -
    -{% for (var i=0; i < data.length; i++) { var section = data[i]; %} -{% if ((i % 2)===0) { %}
    {% } %} -
    -
    - {{ section.label }} -
    -
    - {% for (var j=0; j < section.items.length; j++) { - var item = section.items[j]; - if(item.shown) { %} -
    - - {{ item.label || __(item.name) }} - - {% if(item.type==="doctype") { %} - - {% } %} -
    - {% } %} - {% } %} -
    -
    -{% if ((i % 2)===1 || i===data.length-1) { %}
    {% } %} -{% } %} -
    diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 8954a9d36c..21eaa8909b 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -190,30 +190,34 @@ def run(report_name, filters=None, user=None): def get_prepared_report_result(report, filters, dn="", user=None): latest_report_data = {} - # Only look for completed prepared reports with given filters. - doc_list = frappe.get_all("Prepared Report", - filters={"status": "Completed", "report_name": report.name, "filters": filters, "owner": user}) - doc = None - if len(doc_list): - if dn: - # Get specified dn - doc = frappe.get_doc("Prepared Report", dn) - else: + if dn: + # Get specified dn + doc = frappe.get_doc("Prepared Report", dn) + else: + # Only look for completed prepared reports with given filters. + doc_list = frappe.get_all("Prepared Report", filters={"status": "Completed", "filters": json.dumps(filters), "owner": user}) + if doc_list: # Get latest doc = frappe.get_doc("Prepared Report", doc_list[0]) - # Prepared Report data is stored in a GZip compressed JSON file - attached_file_name = frappe.db.get_value("File", {"attached_to_doctype": doc.doctype, "attached_to_name":doc.name}, "name") - attached_file = frappe.get_doc('File', attached_file_name) - compressed_content = attached_file.get_content() - uncompressed_content = gzip_decompress(compressed_content) - data = json.loads(uncompressed_content) - if data: - latest_report_data = { - "columns": json.loads(doc.columns) if doc.columns else data[0], - "result": data - } + if doc: + try: + # Prepared Report data is stored in a GZip compressed JSON file + attached_file_name = frappe.db.get_value("File", {"attached_to_doctype": doc.doctype, "attached_to_name":doc.name}, "name") + attached_file = frappe.get_doc('File', attached_file_name) + compressed_content = attached_file.get_content() + uncompressed_content = gzip_decompress(compressed_content) + data = json.loads(uncompressed_content) + if data: + latest_report_data = { + "columns": json.loads(doc.columns) if doc.columns else data[0], + "result": data + } + except Exception: + frappe.delete_doc("Prepared Report", doc.name) + frappe.db.commit() + doc = None latest_report_data.update({ "prepared_report": True, @@ -238,45 +242,53 @@ def export_query(): report_name = data["report_name"] if isinstance(data.get("file_format_type"), string_types): file_format_type = data["file_format_type"] + if isinstance(data.get("visible_idx"), string_types): visible_idx = json.loads(data.get("visible_idx")) else: visible_idx = None if file_format_type == "Excel": - data = run(report_name, filters) data = frappe._dict(data) columns = get_columns_dict(data.columns) - result = [[]] - - # add column headings - for idx in range(len(data.columns)): - result[0].append(columns[idx]["label"]) - - # build table from dict - if isinstance(data.result[0], dict): - for i,row in enumerate(data.result): - # only rows which are visible in the report - if row and (i in visible_idx): - row_list = [] - for idx in range(len(data.columns)): - row_list.append(row.get(columns[idx]["fieldname"], row.get(columns[idx]["label"], ""))) - result.append(row_list) - elif not row: - result.append([]) - else: - result = result + [d for i,d in enumerate(data.result) if (i in visible_idx)] - from frappe.utils.xlsxutils import make_xlsx - xlsx_file = make_xlsx(result, "Query Report") + xlsx_data = build_xlsx_data(columns, data, visible_idx) + xlsx_file = make_xlsx(xlsx_data, "Query Report") frappe.response['filename'] = report_name + '.xlsx' frappe.response['filecontent'] = xlsx_file.getvalue() frappe.response['type'] = 'binary' +def build_xlsx_data(columns, data, visible_idx): + result = [[]] + + # add column headings + for idx in range(len(data.columns)): + result[0].append(columns[idx]["label"]) + + # build table from result + for i, row in enumerate(data.result): + # only pick up rows that are visible in the report + if i in visible_idx: + row_data = [] + + if isinstance(row, list): + row_data = row + elif isinstance(row, dict) and row: + for idx in range(len(data.columns)): + label = columns[idx]["label"] + fieldname = columns[idx]["fieldname"] + + row_data.append(row.get(fieldname, row.get(label, ""))) + + result.append(row_data) + + return result + + def get_report_module_dotted_path(module, report_name): return frappe.local.module_app[scrub(module)] + "." + scrub(module) \ + ".report." + scrub(report_name) + "." + scrub(report_name) diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index cc7688b7f0..42409867fb 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -27,7 +27,9 @@ def get_form_params(): """Stringify GET request parameters.""" data = frappe._dict(frappe.local.form_dict) - del data["cmd"] + data.pop('cmd', None) + data.pop('data', None) + if "csrf_token" in data: del data["csrf_token"] @@ -212,23 +214,27 @@ def delete_items(): """delete selected items""" import json - il = sorted(json.loads(frappe.form_dict.get('items')), reverse=True) + items = sorted(json.loads(frappe.form_dict.get('items')), reverse=True) doctype = frappe.form_dict.get('doctype') - failed = [] + if len(items) > 10: + frappe.enqueue('frappe.desk.reportview.delete_bulk', + doctype=doctype, items=items) + else: + delete_bulk(doctype, items) - for i, d in enumerate(il): +def delete_bulk(doctype, items): + failed = [] + for i, d in enumerate(items): try: frappe.delete_doc(doctype, d) - if len(il) >= 5: + if len(items) >= 5: frappe.publish_realtime("progress", - dict(progress=[i+1, len(il)], title=_('Deleting {0}').format(doctype), description=d), + dict(progress=[i+1, len(items)], title=_('Deleting {0}').format(doctype), description=d), user=frappe.session.user) except Exception: failed.append(d) - return failed - @frappe.whitelist() @frappe.read_only() def get_sidebar_stats(stats, doctype, filters=[]): diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json index 5ab3cd1630..ce9b0f30b0 100644 --- a/frappe/email/doctype/email_account/email_account.json +++ b/frappe/email/doctype/email_account/email_account.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 1, @@ -1063,6 +1064,40 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "enable_outgoing", + "description": "Uses the Email Address Name mentioned in this Account as the Sender's Name for all emails sent using this Account.", + "fieldname": "always_use_account_name_as_sender_name", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Always use Account's Name as Sender's Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1563,7 +1598,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-01-30 11:02:41.011412", + "modified": "2019-02-12 17:09:50.653403", "modified_by": "Administrator", "module": "Email", "name": "Email Account", diff --git a/frappe/email/doctype/email_group_member/email_group_member.py b/frappe/email/doctype/email_group_member/email_group_member.py index d0968425d0..23b279e755 100644 --- a/frappe/email/doctype/email_group_member/email_group_member.py +++ b/frappe/email/doctype/email_group_member/email_group_member.py @@ -7,7 +7,13 @@ import frappe from frappe.model.document import Document class EmailGroupMember(Document): - pass + def after_delete(self): + email_group = frappe.get_doc('Email Group', self.email_group) + email_group.update_total_subscribers() + + def after_insert(self): + email_group = frappe.get_doc('Email Group', self.email_group) + email_group.update_total_subscribers() def after_doctype_insert(): - frappe.db.add_unique("Email Group Member", ("email_group", "email")) \ No newline at end of file + frappe.db.add_unique("Email Group Member", ("email_group", "email")) diff --git a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py index 2f879f98ee..e532e2b7eb 100644 --- a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py +++ b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py @@ -35,5 +35,5 @@ class EmailUnsubscribe(Document): def on_update(self): if self.reference_doctype and self.reference_name: doc = frappe.get_doc(self.reference_doctype, self.reference_name) - doc.add_comment("Label", _("Left this conversation"), comment_by=self.email) + doc.add_comment("Label", _("Left this conversation"), comment_email=self.email) diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 486b44bea5..ba211bdf23 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -216,8 +216,15 @@ def get_context(context): please enable Allow Print For {0} in Print Settings""".format(status)), title=_("Error in Notification")) else: - return [{"print_format_attachment":1, "doctype":doc.doctype, "name": doc.name, - "print_format":self.print_format, "print_letterhead": print_settings.with_letterhead}] + return [{ + "print_format_attachment": 1, + "doctype": doc.doctype, + "name": doc.name, + "print_format": self.print_format, + "print_letterhead": print_settings.with_letterhead, + "lang": frappe.db.get_value('Print Format', self.print_format, 'default_print_language') + if self.print_format else 'en' + }] def get_template(self): diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index b4af93e61d..4904f60831 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -174,6 +174,7 @@ class EMail: self.reply_to = validate_email_add(strip(self.reply_to) or self.sender, True) self.replace_sender() + self.replace_sender_name() self.recipients = [strip(r) for r in self.recipients] self.cc = [strip(r) for r in self.cc] @@ -188,6 +189,12 @@ class EMail: sender_name, sender_email = parse_addr(self.sender) self.sender = email.utils.formataddr((str(Header(sender_name or self.email_account.name, 'utf-8')), self.email_account.email_id)) + def replace_sender_name(self): + if cint(self.email_account.always_use_account_name_as_sender_name): + self.set_header('X-Original-From', self.sender) + sender_name, sender_email = parse_addr(self.sender) + self.sender = email.utils.formataddr((str(Header(self.email_account.name, 'utf-8')), sender_email)) + def set_message_id(self, message_id, is_notification=False): if message_id: self.msg_root["Message-Id"] = '<' + message_id + '>' diff --git a/frappe/email/queue.py b/frappe/email/queue.py index ba66e670dd..a7988bc46e 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -174,7 +174,8 @@ def get_email_queue(recipients, sender, subject, **kwargs): if att.get('fid'): _attachments.append(att) elif att.get("print_format_attachment") == 1: - att['lang'] = frappe.local.lang + if not att.get('lang', None): + att['lang'] = frappe.local.lang att['print_letterhead'] = kwargs.get('print_letterhead') _attachments.append(att) e.attachments = json.dumps(_attachments) diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 9bdf3a9ba3..09aa4ff57a 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -190,12 +190,10 @@ class EmailServer: # compare the UIDVALIDITY of email account and imap server uid_validity = self.settings.uid_validity - responce, message = self.imap.status("Inbox", "(UIDVALIDITY UIDNEXT)") - current_uid_validity = self.parse_imap_responce("UIDVALIDITY", message[0]) - if not current_uid_validity: - frappe.throw(_("Can not find UIDVALIDITY in imap status response")) + response, message = self.imap.status("Inbox", "(UIDVALIDITY UIDNEXT)") + current_uid_validity = self.parse_imap_response("UIDVALIDITY", message[0]) or 0 - uidnext = int(self.parse_imap_responce("UIDNEXT", message[0]) or "1") + uidnext = int(self.parse_imap_response("UIDNEXT", message[0]) or "1") frappe.db.set_value("Email Account", self.settings.email_account, "uidnext", uidnext) if not uid_validity or uid_validity != current_uid_validity: @@ -223,9 +221,9 @@ class EmailServer: elif uid_validity == current_uid_validity: return - def parse_imap_responce(self, cmd, responce): + def parse_imap_response(self, cmd, response): pattern = r"(?<={cmd} )[0-9]*".format(cmd=cmd) - match = re.search(pattern, responce.decode('utf-8'), re.U | re.I) + match = re.search(pattern, response.decode('utf-8'), re.U | re.I) if match: return match.group(0) else: diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index 549b3f1d1e..99b5f94bf0 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -109,7 +109,8 @@ def get_default_outgoing_email_account(raise_exception_not_set=True): "mail_password": "Super.Secret.Password", "auto_email_id": "emails@example.com", "email_sender_name": "Example Notifications", - "always_use_account_email_id_as_sender": 0 + "always_use_account_email_id_as_sender": 0, + "always_use_account_name_as_sender_name": 0 } ''' email_account = _get_email_account({"enable_outgoing": 1, "default_outgoing": 1}) @@ -128,7 +129,8 @@ def get_default_outgoing_email_account(raise_exception_not_set=True): "login_id": frappe.conf.get("mail_login"), "email_id": frappe.conf.get("auto_email_id") or frappe.conf.get("mail_login") or 'notifications@example.com', "password": frappe.conf.get("mail_password"), - "always_use_account_email_id_as_sender": frappe.conf.get("always_use_account_email_id_as_sender", 0) + "always_use_account_email_id_as_sender": frappe.conf.get("always_use_account_email_id_as_sender", 0), + "always_use_account_name_as_sender_name": frappe.conf.get("always_use_account_name_as_sender_name", 0) }) email_account.from_site_config = True email_account.name = frappe.conf.get("email_sender_name") or "Frappe" @@ -182,6 +184,7 @@ class SMTPServer: self.use_tls = self.email_account.use_tls self.sender = self.email_account.email_id self.always_use_account_email_id_as_sender = cint(self.email_account.get("always_use_account_email_id_as_sender")) + self.always_use_account_name_as_sender_name = cint(self.email_account.get("always_use_account_name_as_sender_name")) @property def sess(self): diff --git a/frappe/hooks.py b/frappe/hooks.py index 81129fbfdf..a531b27ddd 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -46,15 +46,13 @@ web_include_js = [ "website_script.js" ] -bootstrap = "assets/frappe/css/bootstrap.css" -web_include_css = [ - "assets/css/frappe-web.css" -] +web_include_css = [] website_route_rules = [ {"from_route": "/blog/", "to_route": "Blog Post"}, {"from_route": "/kb/", "to_route": "Help Article"}, - {"from_route": "/newsletters", "to_route": "Newsletter"} + {"from_route": "/newsletters", "to_route": "Newsletter"}, + {"from_route": "/profile", "to_route": "me"}, ] write_file_keys = ["file_url", "file_name"] @@ -118,7 +116,8 @@ doc_events = { "on_update": [ "frappe.desk.notifications.clear_doctype_notifications", "frappe.core.doctype.activity_log.feed.update_feed", - "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions" + "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", + "frappe.automation.doctype.assignment_rule.assignment_rule.apply" ], "after_rename": "frappe.desk.notifications.clear_doctype_notifications", "on_cancel": [ diff --git a/frappe/installer.py b/frappe/installer.py index c4af23976f..4c97ffd8fc 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -16,14 +16,13 @@ from frappe import _ from frappe.model.sync import sync_for from frappe.utils.fixtures import sync_fixtures from frappe.website import render -from frappe.desk.doctype.desktop_icon.desktop_icon import sync_from_app from frappe.modules.utils import sync_customizations from frappe.database import setup_database def install_db(root_login="root", root_password=None, db_name=None, source_sql=None, admin_password=None, verbose=True, force=0, site_config=None, reinstall=False, db_type=None): - + if not db_type: db_type = frappe.conf.db_type or 'mariadb' @@ -84,8 +83,6 @@ def install_app(name, verbose=False, set_as_patched=True): sync_for(name, force=True, sync_everything=True, verbose=verbose, reset_permissions=True) - sync_from_app(name) - add_to_installed_apps(name) frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu() @@ -158,9 +155,6 @@ def remove_app(app_name, dry_run=False, yes=False): if not dry_run: frappe.delete_doc("Module Def", module_name) - # delete desktop icons - frappe.db.sql('delete from `tabDesktop Icon` where app=%s', app_name) - remove_from_installed_apps(app_name) if not dry_run: diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json index b609d7e025..39a3f66552 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json @@ -296,7 +296,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2018-08-07 04:12:43.691760", + "modified": "2019-02-25 04:12:43.691760", "modified_by": "Administrator", "module": "Integrations", "name": "S3 Backup Settings", diff --git a/frappe/migrate.py b/frappe/migrate.py index 956b4a3c93..4057c99b63 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -11,7 +11,6 @@ from frappe.utils.fixtures import sync_fixtures from frappe.cache_manager import clear_global_cache from frappe.desk.notifications import clear_notifications from frappe.website import render, router -from frappe.desk.doctype.desktop_icon.desktop_icon import sync_desktop_icons from frappe.core.doctype.language.language import sync_languages from frappe.modules.utils import sync_customizations @@ -41,7 +40,6 @@ def migrate(verbose=True, rebuild_website=False): frappe.translate.clear_cache() sync_fixtures() sync_customizations() - sync_desktop_icons() sync_languages() frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu() diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 3b9b3765aa..8527828404 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -16,6 +16,12 @@ from frappe.utils.password import get_decrypted_password, set_encrypted_password from frappe.utils import (cint, flt, now, cstr, strip_html, getdate, get_datetime, to_timedelta, sanitize_html, sanitize_email, cast_fieldtype) +max_positive_value = { + 'smallint': 2 ** 15, + 'int': 2 ** 31, + 'bigint': 2 ** 63 +} + _classes = {} def get_controller(doctype): @@ -549,7 +555,6 @@ class BaseDocument(object): # single doctype value type is mediumtext return - column_types_to_check_length = ('varchar', 'int', 'bigint') type_map = frappe.db.type_map for fieldname, value in iteritems(self.get_valid_dict()): @@ -560,20 +565,29 @@ class BaseDocument(object): continue column_type = type_map[df.fieldtype][0] or None - default_column_max_length = type_map[df.fieldtype][1] or None - if df and df.fieldtype in type_map and column_type in column_types_to_check_length: + if column_type == 'varchar': + default_column_max_length = type_map[df.fieldtype][1] or None max_length = cint(df.get("length")) or cint(default_column_max_length) if len(cstr(value)) > max_length: - if self.parentfield and self.idx: - reference = _("{0}, Row {1}").format(_(self.doctype), self.idx) + self.throw_length_exceeded_error(df, max_length, value) - else: - reference = "{0} {1}".format(_(self.doctype), self.name) + elif column_type in ('int', 'bigint', 'smallint'): + max_length = max_positive_value[column_type] - frappe.throw(_("{0}: '{1}' ({3}) will get truncated, as max characters allowed is {2}")\ - .format(reference, _(df.label), max_length, value), frappe.CharacterLengthExceededError, title=_('Value too big')) + if abs(value) > max_length: + self.throw_length_exceeded_error(df, max_length, value) + + def throw_length_exceeded_error(self, df, max_length, value): + if self.parentfield and self.idx: + reference = _("{0}, Row {1}").format(_(self.doctype), self.idx) + + else: + reference = "{0} {1}".format(_(self.doctype), self.name) + + frappe.throw(_("{0}: '{1}' ({3}) will get truncated, as max characters allowed is {2}")\ + .format(reference, _(df.label), max_length, value), frappe.CharacterLengthExceededError, title=_('Value too big')) def _validate_update_after_submit(self): # get the full doc with children diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index 76176e3939..f6744c9310 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -12,6 +12,7 @@ import frappe.defaults from frappe.model import data_fieldtypes from frappe.utils import nowdate, nowtime, now_datetime from frappe.core.doctype.user_permission.user_permission import get_user_permissions +from frappe.permissions import get_allowed_docs_for_doctype def get_new_doc(doctype, parent_doc = None, parentfield = None, as_dict=False): if doctype not in frappe.local.new_doc_templates: @@ -53,36 +54,39 @@ def set_user_and_static_default_values(doc): for df in doc.meta.get("fields"): if df.fieldtype in data_fieldtypes: - user_default_value = get_user_default_value(df, defaults, user_permissions) + # user permissions for link options + doctype_user_permissions = user_permissions.get(df.options, []) + # Allowed records for the reference doctype (link field) + allowed_records = get_allowed_docs_for_doctype(doctype_user_permissions, df.parent) + + user_default_value = get_user_default_value(df, defaults, doctype_user_permissions, allowed_records) + if user_default_value is not None: doc.set(df.fieldname, user_default_value) - else: if df.fieldname != doc.meta.title_field: - static_default_value = get_static_default_value(df, user_permissions) + static_default_value = get_static_default_value(df, doctype_user_permissions, allowed_records) if static_default_value is not None: doc.set(df.fieldname, static_default_value) -def get_user_default_value(df, defaults, user_permissions): +def get_user_default_value(df, defaults, doctype_user_permissions, allowed_records): # don't set defaults for "User" link field using User Permissions! if df.fieldtype == "Link" and df.options != "User": # 1 - look in user permissions only for document_type==Setup # We don't want to include permissions of transactions to be used for defaults. - if (frappe.get_meta(df.options).document_type=="Setup" - and user_permissions_exist(df, user_permissions) - and len(user_permissions.get(df.options))==1): - return user_permissions.get(df.options)[0].get("doc") + if frappe.get_meta(df.options).document_type=="Setup" and len(allowed_records)==1: + return allowed_records[0] # 2 - Look in user defaults user_default = defaults.get(df.fieldname) - is_allowed_user_default = user_default and (not user_permissions_exist(df, user_permissions) - or (user_default in user_permissions.get(df.options, []))) + is_allowed_user_default = user_default and (not user_permissions_exist(df, doctype_user_permissions) + or user_default in allowed_records) # is this user default also allowed as per user permissions? if is_allowed_user_default: return user_default -def get_static_default_value(df, user_permissions): +def get_static_default_value(df, doctype_user_permissions, allowed_records): # 3 - look in default of docfield if df.get("default"): if df.default == "__user": @@ -93,8 +97,8 @@ def get_static_default_value(df, user_permissions): elif not df.default.startswith(":"): # a simple default value - is_allowed_default_value = (not user_permissions_exist(df, user_permissions) - or (df.default in user_permissions.get(df.options, []))) + is_allowed_default_value = (not user_permissions_exist(df, doctype_user_permissions) + or (df.default in allowed_records)) if df.fieldtype!="Link" or df.options=="User" or is_allowed_default_value: return df.default @@ -126,10 +130,10 @@ def set_dynamic_default_values(doc, parent_doc, parentfield): if parentfield: doc["parentfield"] = parentfield -def user_permissions_exist(df, user_permissions): +def user_permissions_exist(df, doctype_user_permissions): return (df.fieldtype=="Link" and not getattr(df, "ignore_user_permissions", False) - and df.options in (user_permissions or [])) + and doctype_user_permissions) def get_default_based_on_another_field(df, user_permissions, parent_doc): # default value based on another document @@ -139,7 +143,7 @@ def get_default_based_on_another_field(df, user_permissions, parent_doc): ref_fieldname = ref_doctype.lower().replace(" ", "_") reference_name = parent_doc.get(ref_fieldname) if parent_doc else frappe.db.get_default(ref_fieldname) default_value = frappe.db.get_value(ref_doctype, reference_name, df.fieldname) - is_allowed_default_value = (not user_permissions_exist(df, user_permissions) or + is_allowed_default_value = (not user_permissions_exist(df, user_permissions.get(df.options)) or (default_value in get_allowed_docs_for_doctype(user_permissions[df.options], df.parent))) # is this allowed as per user permissions diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 66238dd4b0..f4cdd29321 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -386,7 +386,7 @@ class DatabaseQuery(object): elif f.operator.lower() in ('in', 'not in'): values = f.value or '' - if not isinstance(values, (list, tuple)): + if isinstance(values, frappe.string_types): values = values.split(",") fallback = "''" @@ -747,6 +747,7 @@ def get_list(doctype, *args, **kwargs): '''wrapper for DatabaseQuery''' kwargs.pop('cmd', None) kwargs.pop('ignore_permissions', None) + kwargs.pop('data', None) # If doctype is child table if frappe.is_table(doctype): diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 20e0d00199..153065c5ce 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -195,7 +195,7 @@ def check_if_doc_is_linked(doc, method="Delete"): for item in frappe.db.get_values(link_dt, {link_field:doc.name}, ["name", "parent", "parenttype", "docstatus"], as_dict=True): linked_doctype = item.parenttype if item.parent else link_dt - if linked_doctype in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", 'File', 'Version', "Activity Log"): + if linked_doctype in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", 'File', 'Version', "Activity Log", 'Comment'): # don't check for communication and todo! continue @@ -220,7 +220,7 @@ def check_if_doc_is_linked(doc, method="Delete"): def check_if_doc_is_dynamically_linked(doc, method="Delete"): '''Raise `frappe.LinkExistsError` if the document is dynamically linked''' for df in get_dynamic_link_map().get(doc.doctype, []): - if df.parent in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", 'File', 'Version', 'View Log'): + if df.parent in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", 'File', 'Version', 'View Log', 'Comment'): # don't check for communication and todo! continue @@ -266,59 +266,37 @@ def raise_link_exists_exception(doc, reference_doctype, reference_docname, row=' .format(doc.doctype, doc_link, reference_doctype, reference_link, row), frappe.LinkExistsError) def delete_dynamic_links(doctype, name): - delete_doc("ToDo", frappe.db.sql_list("""select name from `tabToDo` - where reference_type=%s and reference_name=%s""", (doctype, name)), - ignore_permissions=True, force=True) - - frappe.db.sql('''delete from `tabEmail Unsubscribe` - where reference_doctype=%s and reference_name=%s''', (doctype, name)) - - # delete shares - frappe.db.sql("""delete from `tabDocShare` - where share_doctype=%s and share_name=%s""", (doctype, name)) - - # delete versions - frappe.db.sql('delete from tabVersion where ref_doctype=%s and docname=%s', (doctype, name)) - - # delete comments - frappe.db.sql("""delete from `tabCommunication` - where - communication_type = 'Comment' - and reference_doctype=%s and reference_name=%s""", (doctype, name)) - - # delete view logs - frappe.db.sql("""delete from `tabView Log` - where reference_doctype=%s and reference_name=%s""", (doctype, name)) + delete_references('ToDo', doctype, name, 'reference_type') + delete_references('Email Unsubscribe', doctype, name) + delete_references('DocShare', doctype, name, 'share_doctype', 'share_name') + delete_references('Version', doctype, name, 'ref_doctype', 'docname') + delete_references('Comment', doctype, name) + delete_references('View Log', doctype, name) # unlink communications - frappe.db.sql("""update `tabCommunication` - set reference_doctype=null, reference_name=null + clear_references('Communication', doctype, name) + clear_references('Communication', doctype, name, 'link_doctype', 'link_name') + clear_references('Communication', doctype, name, 'timeline_doctype', 'timeline_name') + + clear_references('Activity Log', doctype, name) + clear_references('Activity Log', doctype, name, 'timeline_doctype', 'timeline_name') + +def delete_references(doctype, reference_doctype, reference_name, + reference_doctype_field = 'reference_doctype', reference_name_field = 'reference_name'): + frappe.db.sql('''delete from `tab{0}` + where {1}=%s and {2}=%s'''.format(doctype, reference_doctype_field, reference_name_field), # nosec + (reference_doctype, reference_name)) + +def clear_references(doctype, reference_doctype, reference_name, + reference_doctype_field = 'reference_doctype', reference_name_field = 'reference_name'): + frappe.db.sql('''update + `tab{0}` + set + {1}=NULL, {2}=NULL where - communication_type = 'Communication' - and reference_doctype=%s - and reference_name=%s""", (doctype, name)) + {1}=%s and {2}=%s'''.format(doctype, reference_doctype_field, reference_name_field), # nosec + (reference_doctype, reference_name)) - # unlink secondary references - frappe.db.sql("""update `tabCommunication` - set link_doctype=null, link_name=null - where link_doctype=%s and link_name=%s""", (doctype, name)) - - # unlink feed - frappe.db.sql("""update `tabCommunication` - set timeline_doctype=null, timeline_name=null - where timeline_doctype=%s and timeline_name=%s""", (doctype, name)) - - # unlink activity_log reference_doctype - frappe.db.sql("""update `tabActivity Log` - set reference_doctype=null, reference_name=null - where - reference_doctype=%s - and reference_name=%s""", (doctype, name)) - - # unlink activity_log timeline_doctype - frappe.db.sql("""update `tabActivity Log` - set timeline_doctype=null, timeline_name=null - where timeline_doctype=%s and timeline_name=%s""", (doctype, name)) def insert_feed(doc): from frappe.utils import get_fullname @@ -327,8 +305,7 @@ def insert_feed(doc): return frappe.get_doc({ - "doctype": "Communication", - "communication_type": "Comment", + "doctype": "Comment", "comment_type": "Deleted", "reference_doctype": doc.doctype, "subject": "{0} {1}".format(_(doc.doctype), doc.name), diff --git a/frappe/model/document.py b/frappe/model/document.py index 6bd82ba25a..d6876c7347 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -165,10 +165,10 @@ class Document(BaseDocument): self.latest = frappe.get_doc(self.doctype, self.name) return self.latest - def check_permission(self, permtype='read', permlabel=None): + def check_permission(self, permtype='read', permlevel=None): """Raise `frappe.PermissionError` if not permitted""" if not self.has_permission(permtype): - self.raise_no_permission_to(permlabel or permtype) + self.raise_no_permission_to(permlevel or permtype) def has_permission(self, permtype="read", verbose=False): """Call `frappe.has_permission` if `self.flags.ignore_permissions` @@ -999,7 +999,7 @@ class Document(BaseDocument): frappe.db.commit() def db_get(self, fieldname): - '''get database vale for this fieldname''' + '''get database value for this fieldname''' return frappe.db.get_value(self.doctype, self.name, fieldname) def check_no_back_links_exist(self): @@ -1116,33 +1116,22 @@ class Document(BaseDocument): """Returns Desk URL for this document. `/desk#Form/{doctype}/{name}`""" return "/desk#Form/{doctype}/{name}".format(doctype=self.doctype, name=self.name) - def add_comment(self, comment_type, text=None, comment_by=None, link_doctype=None, link_name=None): + def add_comment(self, comment_type='Comment', text=None, comment_email=None, link_doctype=None, link_name=None, comment_by=None): """Add a comment to this document. :param comment_type: e.g. `Comment`. See Communication for more info.""" - if comment_type=='Comment': - out = frappe.get_doc({ - "doctype":"Communication", - "communication_type": "Comment", - "sender": comment_by or frappe.session.user, - "comment_type": comment_type, - "reference_doctype": self.doctype, - "reference_name": self.name, - "content": text or comment_type, - "link_doctype": link_doctype, - "link_name": link_name - }).insert(ignore_permissions=True) - else: - out = frappe.get_doc(dict( - doctype='Version', - ref_doctype= self.doctype, - docname= self.name, - data = frappe.as_json(dict(comment_type=comment_type, comment=text)) - )) - if comment_by: - out.owner = comment_by - out.insert(ignore_permissions=True) + out = frappe.get_doc({ + "doctype":"Comment", + 'comment_type': comment_type, + "comment_email": comment_email or frappe.session.user, + "comment_by": comment_by, + "reference_doctype": self.doctype, + "reference_name": self.name, + "content": text or comment_type, + "link_doctype": link_doctype, + "link_name": link_name + }).insert(ignore_permissions=True) return out def add_seen(self, user=None): diff --git a/frappe/modules.txt b/frappe/modules.txt index 3f7a01b79d..3a9ea8f05e 100644 --- a/frappe/modules.txt +++ b/frappe/modules.txt @@ -10,4 +10,5 @@ Printing Contacts Data Migration Chat -Social \ No newline at end of file +Social +Automation \ No newline at end of file diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 0703a064d3..94a0d0dbfa 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -83,12 +83,10 @@ def sync_customizations(app=None): for app_name in apps: for module_name in frappe.local.app_modules.get(app_name) or []: folder = frappe.get_app_path(app_name, module_name, 'custom') - if os.path.exists(folder): for fname in os.listdir(folder): with open(os.path.join(folder, fname), 'r') as f: data = json.loads(f.read()) - if data.get('sync_on_migrate'): sync_customizations_for_doctype(data, folder) @@ -105,14 +103,31 @@ def sync_customizations_for_doctype(data, folder): # sync single doctype exculding the child doctype def sync_single_doctype(doc_type): - frappe.db.sql('delete from `tab{0}` where `{1}` =%s'.format( - custom_doctype, doctype_fieldname), doc_type) - for d in data[key]: - if d.get(doctype_fieldname) == doc_type: - d['doctype'] = custom_doctype - doc = frappe.get_doc(d) + def _insert(data): + if data.get(doctype_fieldname) == doc_type: + data['doctype'] = custom_doctype + doc = frappe.get_doc(data) doc.db_insert() + if custom_doctype != 'Custom Field': + frappe.db.sql('delete from `tab{0}` where `{1}` =%s'.format( + custom_doctype, doctype_fieldname), doc_type) + + for d in data[key]: + _insert(data) + + else: + for d in data[key]: + field = frappe.db.get_value("Custom Field", {"dt": doc_type, "fieldname": d["fieldname"]}) + if not field: + d["owner"] = "Administrator" + _insert(d) + else: + custom_field = frappe.get_doc("Custom Field", field) + custom_field.flags.ignore_validate = True + custom_field.update(d) + custom_field.db_update() + for doc_type in doctypes: # only sync the parent doctype and child doctype if there isn't any other child table json file if doc_type == doctype or not os.path.exists(os.path.join(folder, frappe.scrub(doc_type)+".json")): diff --git a/frappe/patches.txt b/frappe/patches.txt index c8623b78de..c50221fece 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -5,15 +5,16 @@ frappe.patches.v8_0.update_global_search_table frappe.patches.v7_0.update_auth frappe.patches.v8_0.drop_in_dialog #2017-09-22 frappe.patches.v7_2.remove_in_filter +frappe.patches.v11_0.drop_column_apply_user_permissions execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') execute:frappe.reload_doc('core', 'doctype', 'docperm') #2018-05-29 +execute:frappe.reload_doc('core', 'doctype', 'comment') frappe.patches.v8_0.drop_is_custom_from_docperm execute:frappe.reload_doc('core', 'doctype', 'module_def') #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01 frappe.patches.v11_0.replicate_old_user_permissions -frappe.patches.v11_0.drop_column_apply_user_permissions frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03 frappe.patches.v7_1.rename_scheduler_log_to_error_log frappe.patches.v6_1.rename_file_data @@ -103,8 +104,6 @@ frappe.patches.v4_3.remove_allow_on_submit_customization frappe.patches.v5_0.rename_table_fieldnames frappe.patches.v5_0.communication_parent frappe.patches.v5_0.clear_website_group_and_notifications -execute:frappe.db.sql("""update tabComment set comment = substr(comment, 6, locate(":", comment)-6) where comment_type in ("Assigned", "Assignment Completed")""") -execute:frappe.db.sql("update `tabComment` set comment_type='Comment' where comment_doctype='Blog Post' and ifnull(comment_type, '')=''") frappe.patches.v5_0.update_shared execute:frappe.reload_doc("core", "doctype", "docshare") #2015-07-21 frappe.patches.v6_19.comment_feed_communication @@ -145,11 +144,9 @@ frappe.patches.v6_16.feed_doc_owner frappe.patches.v6_21.print_settings_repeat_header_footer frappe.patches.v6_24.set_language_as_code frappe.patches.v6_20x.update_insert_after -finally:frappe.patches.v6_24.sync_desktop_icons frappe.patches.v6_20x.set_allow_draft_for_print frappe.patches.v6_20x.remove_roles_from_website_user frappe.patches.v7_0.set_user_fullname -frappe.patches.v7_0.desktop_icons_hidden_by_admin_as_blocked frappe.patches.v7_0.add_communication_in_doc frappe.patches.v7_0.update_send_after_in_bulk_email execute:frappe.db.sql('''delete from `tabSingles` where doctype="Email Settings"''') # 2016-06-13 @@ -183,15 +180,11 @@ frappe.patches.v8_0.deprecate_integration_broker frappe.patches.v8_0.update_gender_and_salutation frappe.patches.v8_0.setup_email_inbox #2017-03-29 frappe.patches.v8_0.newsletter_childtable_migrate -execute:frappe.db.sql("delete from `tabDesktop Icon` where module_name='Communication'") -execute:frappe.db.sql("update `tabDesktop Icon` set type='list' where _doctype='Communication'") -frappe.patches.v8_0.fix_non_english_desktop_icons # 2017-04-12 frappe.patches.v8_0.set_doctype_values_in_custom_role frappe.patches.v8_0.install_new_build_system_requirements frappe.patches.v8_0.set_currency_field_precision # 2017-05-09 frappe.patches.v8_0.rename_print_to_printing frappe.patches.v7_1.disabled_print_settings_for_custom_print_format -frappe.patches.v8_0.update_desktop_icons execute:frappe.db.sql('update tabReport set module="Desk" where name="ToDo"') frappe.patches.v8_1.enable_allow_error_traceback_in_system_settings frappe.patches.v8_1.update_format_options_in_auto_email_report @@ -235,4 +228,10 @@ frappe.patches.v11_0.delete_all_prepared_reports frappe.patches.v11_0.fix_order_by_in_reports_json execute:frappe.delete_doc('Page', 'applications', ignore_missing=True) frappe.patches.v11_0.set_missing_creation_and_modified_value_for_user_permissions +frappe.patches.v11_0.remove_doctype_user_permissions_for_page_and_report +frappe.patches.v11_0.set_default_letter_head_source frappe.patches.v12_0.set_primary_key_in_series +execute:frappe.delete_doc("Page", "modules", ignore_missing=True) +frappe.patches.v11_0.set_default_letter_head_source +frappe.patches.v12_0.setup_comments_from_communications +frappe.patches.v12_0.init_desk_settings diff --git a/frappe/patches/v11_0/drop_column_apply_user_permissions.py b/frappe/patches/v11_0/drop_column_apply_user_permissions.py index ed0a6881af..4f46bc0907 100644 --- a/frappe/patches/v11_0/drop_column_apply_user_permissions.py +++ b/frappe/patches/v11_0/drop_column_apply_user_permissions.py @@ -6,8 +6,9 @@ def execute(): to_remove = ['DocPerm', 'Custom DocPerm'] for doctype in to_remove: - if column in frappe.db.get_table_columns(doctype): - frappe.db.sql("alter table `tab{0}` drop column {1}".format(doctype, column)) + if frappe.db.table_exists(doctype): + if column in frappe.db.get_table_columns(doctype): + frappe.db.sql("alter table `tab{0}` drop column {1}".format(doctype, column)) frappe.reload_doc('core', 'doctype', 'docperm', force=True) frappe.reload_doc('core', 'doctype', 'custom_docperm', force=True) diff --git a/frappe/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py b/frappe/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py new file mode 100644 index 0000000000..c1dc1b79be --- /dev/null +++ b/frappe/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.table_exists('User Permission for Page and Report'): + frappe.delete_doc("DocType", "User Permission for Page and Report") \ No newline at end of file diff --git a/frappe/patches/v11_0/set_default_letter_head_source.py b/frappe/patches/v11_0/set_default_letter_head_source.py new file mode 100644 index 0000000000..069f4e3d2e --- /dev/null +++ b/frappe/patches/v11_0/set_default_letter_head_source.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals + +import frappe + +def execute(): + frappe.reload_doctype('Letter Head') + + # source of all existing letter heads must be HTML + frappe.db.sql('update `tabLetter Head` set source = "HTML"') \ No newline at end of file diff --git a/frappe/patches/v12_0/init_desk_settings.py b/frappe/patches/v12_0/init_desk_settings.py new file mode 100644 index 0000000000..aa1060b906 --- /dev/null +++ b/frappe/patches/v12_0/init_desk_settings.py @@ -0,0 +1,34 @@ +from __future__ import unicode_literals + +import json +import frappe +from frappe.config import get_modules_from_all_apps_for_user +from frappe.desk.moduleview import get_onboard_items + +def execute(): + """Set the initial customizations for desk, with modules, indices and links.""" + frappe.reload_doc("core", "doctype", "user") + all_modules = get_modules_from_all_apps_for_user() + + settings = {} + + for module in all_modules: + if not module.get("app"): continue + + links = get_onboard_items(module["app"], frappe.scrub(module["module_name"]))[:5] + module_settings = { + "links": ",".join([d["label"] for d in links]) + } + category_dict = settings.get(module.get("category", ""), None) + if category_dict: + module_settings["index"] = len(category_dict) + category_dict[module.get("module_name")] = module_settings + else: + module_settings["index"] = 0 + settings[module.get("category", "")] = { + module.get("module_name"): module_settings + } + + settings_json_str = json.dumps(settings) + + frappe.db.sql("""update tabUser set home_settings = %s""", (settings_json_str), debug=True) diff --git a/frappe/patches/v12_0/setup_comments_from_communications.py b/frappe/patches/v12_0/setup_comments_from_communications.py new file mode 100644 index 0000000000..92256e130e --- /dev/null +++ b/frappe/patches/v12_0/setup_comments_from_communications.py @@ -0,0 +1,26 @@ +from __future__ import unicode_literals + +import frappe + +def execute(): + for comment in frappe.get_all('Communication', fields = ['*'], + filters = dict(communication_type = 'Comment')): + + new_comment = frappe.new_doc('Comment') + new_comment.comment_type = comment.comment_type + new_comment.comment_email = comment.sender + new_comment.comment_by = comment.sender_full_name + new_comment.subject = comment.subject + new_comment.content = comment.content or comment.subject + new_comment.reference_doctype = comment.reference_doctype + new_comment.reference_name = comment.reference_name + new_comment.link_doctype = comment.link_doctype + new_comment.link_name = comment.link_name + new_comment.creation = comment.creation + new_comment.modified = comment.modified + new_comment.owner = comment.owner + new_comment.modified_by = comment.modified_by + new_comment.db_insert() + + # clean up + frappe.db.sql('delete from tabCommunication where communication_type = "Comment"') \ No newline at end of file diff --git a/frappe/patches/v12_0/webpage_migrate_description_to_meta_tag.py b/frappe/patches/v12_0/webpage_migrate_description_to_meta_tag.py new file mode 100644 index 0000000000..cb960e84bb --- /dev/null +++ b/frappe/patches/v12_0/webpage_migrate_description_to_meta_tag.py @@ -0,0 +1,14 @@ +import frappe + +def execute(): + web_pages = frappe.get_all('Web Page', ['name', 'description']) + + for web_page in web_pages: + if web_page.description and web_page.route: + doc = frappe.new_doc('Website Route Meta') + doc.name = web_page.route + doc.append('meta_tags', { + 'key': 'description', + 'value': web_page.description + }) + doc.save() diff --git a/frappe/patches/v6_19/comment_feed_communication.py b/frappe/patches/v6_19/comment_feed_communication.py index 6f91ba04f9..a7503c08ab 100644 --- a/frappe/patches/v6_19/comment_feed_communication.py +++ b/frappe/patches/v6_19/comment_feed_communication.py @@ -6,6 +6,9 @@ from frappe.model.dynamic_links import dynamic_link_queries from frappe.permissions import reset_perms def execute(): + # comments stay comments in v12 + return + frappe.reload_doctype("DocType") frappe.reload_doctype("Communication") reset_perms("Communication") diff --git a/frappe/patches/v6_24/sync_desktop_icons.py b/frappe/patches/v6_24/sync_desktop_icons.py deleted file mode 100644 index 74f52e6056..0000000000 --- a/frappe/patches/v6_24/sync_desktop_icons.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import unicode_literals -import frappe, json - -from frappe.desk.doctype.desktop_icon.desktop_icon import sync_from_app, get_user_copy -import frappe.defaults - -def execute(): - frappe.reload_doc('desk', 'doctype', 'desktop_icon') - - frappe.db.sql('delete from `tabDesktop Icon`') - - modules_list = [] - for app in frappe.get_installed_apps(): - modules_list += sync_from_app(app) - - # sync hidden modules - hidden_modules = frappe.db.get_global('hidden_modules') - if hidden_modules: - for m in json.loads(hidden_modules): - try: - desktop_icon = frappe.get_doc('Desktop Icon', {'module_name': m, 'standard': 1, 'app': app}) - desktop_icon.db_set('hidden', 1) - except frappe.DoesNotExistError: - pass - - # sync user sort - for user in frappe.get_all('User', filters={'user_type': 'System User'}): - user_list = frappe.defaults.get_user_default('_user_desktop_items', user=user.name) - if user_list: - user_list = json.loads(user_list) - for i, module_name in enumerate(user_list): - try: - desktop_icon = get_user_copy(module_name, user=user.name) - desktop_icon.db_set('idx', i) - except frappe.DoesNotExistError: - pass - - # set remaining icons as hidden - for module_name in list(set([m['module_name'] for m in modules_list]) - set(user_list)): - try: - desktop_icon = get_user_copy(module_name, user=user.name) - desktop_icon.db_set('hidden', 1) - except frappe.DoesNotExistError: - pass diff --git a/frappe/patches/v7_0/add_communication_in_doc.py b/frappe/patches/v7_0/add_communication_in_doc.py index 92120634ef..4db02c5bab 100644 --- a/frappe/patches/v7_0/add_communication_in_doc.py +++ b/frappe/patches/v7_0/add_communication_in_doc.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import frappe -from frappe.core.doctype.communication.comment import update_comment_in_doc +from frappe.core.doctype.comment.comment import update_comment_in_doc def execute(): for d in frappe.db.get_all("Communication", diff --git a/frappe/patches/v7_0/desktop_icons_hidden_by_admin_as_blocked.py b/frappe/patches/v7_0/desktop_icons_hidden_by_admin_as_blocked.py deleted file mode 100644 index 496af17cd2..0000000000 --- a/frappe/patches/v7_0/desktop_icons_hidden_by_admin_as_blocked.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import unicode_literals -import frappe - -def execute(): - # all icons hidden in standard are "blocked" - # this is for the use case where the admin wants to remove icon for everyone - - # in 7.0, icons may be hidden by default, but still can be shown to the user - # e.g. Accounts, Stock etc, so we need a new property for blocked - - if frappe.db.table_exists('Desktop Icon'): - frappe.db.sql('update `tabDesktop Icon` set blocked = 1 where standard=1 and hidden=1') \ No newline at end of file diff --git a/frappe/patches/v7_2/merge_knowledge_base.py b/frappe/patches/v7_2/merge_knowledge_base.py index 81ff98230b..301d15e1dd 100644 --- a/frappe/patches/v7_2/merge_knowledge_base.py +++ b/frappe/patches/v7_2/merge_knowledge_base.py @@ -11,12 +11,6 @@ def execute(): update_routes(['Help Category', 'Help Article']) remove_from_installed_apps('knowledge_base') - # remove desktop icon - desktop_icon_name = frappe.db.get_value('Desktop Icon', - dict(module_name='Knowledge Base', type='module')) - if desktop_icon_name: - frappe.delete_doc('Desktop Icon', desktop_icon_name) - # remove module def if frappe.db.exists('Module Def', 'Knowledge Base'): frappe.delete_doc('Module Def', 'Knowledge Base') diff --git a/frappe/patches/v8_0/fix_non_english_desktop_icons.py b/frappe/patches/v8_0/fix_non_english_desktop_icons.py deleted file mode 100644 index b4389578ab..0000000000 --- a/frappe/patches/v8_0/fix_non_english_desktop_icons.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2017, Frappe and Contributors -# License: GNU General Public License v3. See license.txt -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals -import frappe -from frappe.desk.doctype.desktop_icon.desktop_icon import clear_desktop_icons_cache - -def execute(): - frappe.db.sql(""" - update `tabDesktop Icon` - set module_name=_doctype, label=_doctype - where type = 'link' and _doctype != label and link like 'List/%' - """) \ No newline at end of file diff --git a/frappe/patches/v8_0/setup_email_inbox.py b/frappe/patches/v8_0/setup_email_inbox.py index 8cd8b28116..1bfe3b0b74 100644 --- a/frappe/patches/v8_0/setup_email_inbox.py +++ b/frappe/patches/v8_0/setup_email_inbox.py @@ -3,31 +3,18 @@ import frappe, json from frappe.core.doctype.user.user import ask_pass_update, setup_user_email_inbox def execute(): - """ + """ depricate email inbox page if exists remove desktop icon for email inbox page if exists patch to remove Custom DocPerm for communication + add user inbox child table entry for existing email account in not exists """ if frappe.db.exists("Page", "email_inbox"): frappe.delete_doc("Page", "email_inbox") - desktop_icon = frappe.db.get_value("Desktop Icon", { - "module_name": "Email", - "type": "Page", - "link": "email_inbox" - }) - - if desktop_icon: - frappe.delete_doc("Desktop Icon", desktop_icon) - frappe.db.sql("""update `tabCustom DocPerm` set `write`=0, email=1 where parent='Communication'""") - setup_inbox_from_email_account() - -def setup_inbox_from_email_account(): - """ add user inbox child table entry for existing email account in not exists """ - frappe.reload_doc("core", "doctype", "user_email") frappe.reload_doc("email", "doctype", "email_account") @@ -36,4 +23,4 @@ def setup_inbox_from_email_account(): for email_account in email_accounts: setup_user_email_inbox(email_account.get("name"), email_account.get("awaiting_password"), - email_account.get("email_id"), email_account.get("enabled_outgoing")) \ No newline at end of file + email_account.get("email_id"), email_account.get("enabled_outgoing")) diff --git a/frappe/patches/v8_0/update_desktop_icons.py b/frappe/patches/v8_0/update_desktop_icons.py deleted file mode 100644 index ea8527718b..0000000000 --- a/frappe/patches/v8_0/update_desktop_icons.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals -import frappe -from frappe.utils import cstr - -def execute(): - """ update the desktop icons """ - - frappe.reload_doc('desk', 'doctype', 'desktop_icon') - - icons = frappe.get_all("Desktop Icon", filters={ "type": "link" }, fields=["link", "name"]) - - for icon in icons: - # check if report exists - icon_link = icon.get("link", "") or "" - parts = icon_link.split("/") - if not parts: - continue - - report_name = parts[-1] - if "report" in parts[0] and frappe.db.get_value("Report", report_name): - frappe.db.sql(""" update `tabDesktop Icon` set _report='{report_name}' - where name='{name}'""".format(report_name=report_name, name=icon.get("name"))) \ No newline at end of file diff --git a/frappe/permissions.py b/frappe/permissions.py index abe5eca84d..261049c226 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -24,8 +24,10 @@ def print_has_permission_check_logs(func): def inner(*args, **kwargs): frappe.flags['has_permission_check_logs'] = [] result = func(*args, **kwargs) + self_perm_check = True if not kwargs.get('user') else kwargs.get('user') == frappe.session.user # print only if access denied - if not result: + # and if user is checking his own permission + if not result and self_perm_check: msgprint(('
    ').join(frappe.flags['has_permission_check_logs'])) frappe.flags.pop('has_permission_check_logs', None) return result diff --git a/frappe/printing/doctype/letter_head/letter_head.js b/frappe/printing/doctype/letter_head/letter_head.js index f70224565d..ca4dad2d07 100644 --- a/frappe/printing/doctype/letter_head/letter_head.js +++ b/frappe/printing/doctype/letter_head/letter_head.js @@ -3,6 +3,6 @@ frappe.ui.form.on('Letter Head', { refresh: function(frm) { - + frm.flag_public_attachments = true; } }); diff --git a/frappe/printing/doctype/letter_head/letter_head.json b/frappe/printing/doctype/letter_head/letter_head.json index fb10be3f84..1bee220213 100644 --- a/frappe/printing/doctype/letter_head/letter_head.json +++ b/frappe/printing/doctype/letter_head/letter_head.json @@ -1,241 +1,450 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "field:letter_head_name", - "beta": 0, - "creation": "2012-11-22 17:45:46", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 1, + "autoname": "field:letter_head_name", + "beta": 0, + "creation": "2012-11-22 17:45:46", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 0, "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "letter_head_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Letter Head Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "letter_head_name", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "letter_head_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Letter Head Name", + "length": 0, + "no_copy": 0, + "oldfieldname": "letter_head_name", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "letter_head_name", - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Disabled", - "length": 0, - "no_copy": 0, - "oldfieldname": "disabled", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "letter_head_name", + "fieldname": "source", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Letter Head Based On", + "length": 0, + "no_copy": 0, + "options": "Image\nHTML", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "letter_head_name", - "description": "Check this to make this the default letter head in all prints", - "fieldname": "is_default", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Is Default", - "length": 0, - "no_copy": 0, - "oldfieldname": "is_default", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.__islocal", - "description": "Letter Head in HTML", - "fieldname": "content", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Content", - "length": 0, - "no_copy": 0, - "oldfieldname": "content", - "oldfieldtype": "Text Editor", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "letter_head_name", + "fieldname": "disabled", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Disabled", + "length": 0, + "no_copy": 0, + "oldfieldname": "disabled", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.__islocal", - "fieldname": "footer", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Footer", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "letter_head_name", + "description": "", + "fieldname": "is_default", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Default Letter Head", + "length": 0, + "no_copy": 0, + "oldfieldname": "is_default", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": "", + "columns": 0, + "depends_on": "eval:doc.letter_head_name && doc.source === 'Image'", + "fieldname": "letter_head_image_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Letter Head Image", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.letter_head_name && doc.source === 'Image'", + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Image", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": "", + "columns": 0, + "depends_on": "eval:doc.source==='HTML' && doc.letter_head_name", + "fieldname": "header_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Header", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:!doc.__islocal && doc.source==='HTML'", + "description": "Letter Head in HTML", + "fieldname": "content", + "fieldtype": "HTML Editor", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Header HTML", + "length": 0, + "no_copy": 0, + "oldfieldname": "content", + "oldfieldtype": "Text Editor", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "footer_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Footer", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:!doc.__islocal", + "description": "Footer will display correctly only in PDF", + "fieldname": "footer", + "fieldtype": "HTML Editor", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Footer HTML", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-font", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 3, - "modified": "2018-04-21 17:23:55.709575", - "modified_by": "Administrator", - "module": "Printing", - "name": "Letter Head", - "owner": "Administrator", + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "fa fa-font", + "idx": 1, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 3, + "modified": "2019-02-12 09:48:26.017783", + "modified_by": "Administrator", + "module": "Printing", + "name": "Letter Head", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "All", - "set_user_permissions": 0, - "share": 0, - "submit": 0, + "amend": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "All", + "set_user_permissions": 0, + "share": 0, + "submit": 0, "write": 0 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0 + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_order": "ASC", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 } \ No newline at end of file diff --git a/frappe/printing/doctype/letter_head/letter_head.py b/frappe/printing/doctype/letter_head/letter_head.py index 3e9ce09c83..1a70ebcb08 100644 --- a/frappe/printing/doctype/letter_head/letter_head.py +++ b/frappe/printing/doctype/letter_head/letter_head.py @@ -3,16 +3,30 @@ from __future__ import unicode_literals import frappe +from frappe.utils import is_image from frappe.model.document import Document class LetterHead(Document): + def before_insert(self): + # for better UX, let user set from attachment + self.source = 'Image' + def validate(self): + self.set_image() if not self.is_default: if not frappe.db.sql("""select count(*) from `tabLetter Head` where ifnull(is_default,0)=1"""): self.is_default = 1 + def set_image(self): + if self.source=='Image': + if self.image and is_image(self.image): + self.content = ''.format(self.image) + frappe.msgprint(frappe._('Header HTML set from attachment {0}').format(self.image), alert = True) + else: + frappe.msgprint(frappe._('Please attach an image file to set HTML'), alert = True, indicator = 'orange') + def on_update(self): self.set_as_default() diff --git a/frappe/printing/doctype/letter_head/test_letter_head.py b/frappe/printing/doctype/letter_head/test_letter_head.py index 9535841817..b69e9924ea 100644 --- a/frappe/printing/doctype/letter_head/test_letter_head.py +++ b/frappe/printing/doctype/letter_head/test_letter_head.py @@ -7,4 +7,14 @@ import frappe import unittest class TestLetterHead(unittest.TestCase): - pass + def test_auto_image(self): + letter_head = frappe.get_doc(dict( + doctype = 'Letter Head', + letter_head_name = 'Test', + source = 'Image', + image = '/public/test.png' + )).insert() + + # test if image is automatically set + self.assertTrue(letter_head.image in letter_head.content) + diff --git a/frappe/public/build.json b/frappe/public/build.json index 6e8c8a97fc..9c395c9bca 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -11,6 +11,10 @@ "public/less/form_grid.less", "node_modules/frappe-datatable/dist/frappe-datatable.css" ], + "css/frappe-web-b4.css": [ + "public/less/indicator.less", + "public/scss/website.scss" + ], "concat:js/moment-bundle.min.js": [ "node_modules/moment/min/moment-with-locales.min.js", "node_modules/moment-timezone/builds/moment-timezone-with-data.min.js" @@ -21,6 +25,9 @@ "js/frappe-vue.min.js": [ "public/js/frappe_vue.js" ], + "js/frappe-recorder.min.js": [ + "public/js/frappe/recorder/recorder.js" + ], "js/frappe-web.min.js": [ "public/js/frappe/class.js", "public/js/frappe/polyfill.js", @@ -48,6 +55,9 @@ "public/js/frappe/misc/rating_icons.html", "public/js/frappe/socketio_client.js" ], + "js/bootstrap-4-web.min.js": [ + "website/js/bootstrap-4.js" + ], "js/control.min.js": [ "public/js/frappe/ui/capture.js", "public/js/frappe/form/controls/base_control.js", @@ -133,6 +143,7 @@ "public/js/lib/Sortable.min.js", "public/js/lib/jquery/jquery.hotkeys.js", "public/js/lib/bootstrap.min.js", + "node_modules/vue/dist/vue.js", "node_modules/moment/min/moment-with-locales.min.js", "node_modules/moment-timezone/builds/moment-timezone-with-data.min.js", "public/js/lib/socket.io.min.js", @@ -229,7 +240,6 @@ "public/js/frappe/ui/toolbar/about.js", "public/js/frappe/ui/toolbar/navbar.html", "public/js/frappe/ui/toolbar/toolbar.js", - "public/js/frappe/ui/toolbar/modules_select.js", "public/js/frappe/ui/toolbar/notifications.js", "public/js/frappe/views/communication.js", "public/js/frappe/views/translation_manager.js", @@ -288,7 +298,8 @@ "public/js/frappe/form/footer/timeline.js", "public/js/frappe/form/footer/assign_to.js", "public/js/frappe/form/quick_entry.js", - "public/js/frappe/form/success_action.js" + "public/js/frappe/form/success_action.js", + "public/js/frappe/meta_tag.js" ], "css/list.min.css": [ "public/less/list.less", @@ -369,7 +380,8 @@ "css/web_form.css": [ "public/less/list.less", "website/css/web_form.css", - "public/less/quill.less" + "public/less/quill.less", + "public/less/datepicker.less" ], "js/print_format_v3.min.js": [ "public/js/legacy/layout.js", @@ -381,5 +393,8 @@ ], "js/social.min.js": [ "public/js/frappe/social/social_home.js" + ], + "js/modules.min.js": [ + "public/js/frappe/views/modules_home.js" ] } diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 3787d5d575..c5f2fa0f36 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -196,7 +196,7 @@ frappe.Application = Class.extend({ load_bootinfo: function() { if(frappe.boot) { frappe.modules = {}; - frappe.boot.desktop_icons.forEach(function(m) { + frappe.boot.allowed_modules.forEach(function(m) { frappe.modules[m.module_name]=m; }); frappe.model.sync(frappe.boot.docs); @@ -266,9 +266,6 @@ frappe.Application = Class.extend({ $.extend(frappe.boot.notification_info, r.message); $(document).trigger("notification-update"); - // update in module views - me.update_notification_count_in_modules(); - if(frappe.get_route()[0] != "messages") { if(r.message.new_messages.length) { frappe.utils.set_title_prefix("(" + r.message.new_messages.length + ")"); @@ -281,18 +278,6 @@ frappe.Application = Class.extend({ } }, - update_notification_count_in_modules: function() { - $.each(frappe.boot.notification_info.open_count_doctype, function(doctype, count) { - if(count) { - $('.open-notification.global[data-doctype="'+ doctype +'"]') - .removeClass("hide").html(count > 99 ? "99+" : count); - } else { - $('.open-notification.global[data-doctype="'+ doctype +'"]') - .addClass("hide"); - } - }); - }, - set_globals: function() { frappe.session.user = frappe.boot.user.name; frappe.session.user_email = frappe.boot.user.email; @@ -414,6 +399,7 @@ frappe.Application = Class.extend({ } }); dialog.set_primary_action(__('Login'), () => { + dialog.set_message(__('Authenticating...')); frappe.call({ method: 'login', args: { @@ -599,86 +585,3 @@ frappe.get_module = function(m, default_module) { return module; }; - -frappe.get_desktop_icons = function(show_hidden, show_global) { - // filter valid icons - - // hidden == hidden from desktop - // blocked == no view from modules either - - var out = []; - - var add_to_out = function(module) { - module = frappe.get_module(module.module_name, module); - module.app_icon = frappe.ui.app_icon.get_html(module); - out.push(module); - }; - - var show_module = function(m) { - var out = true; - if(m.type==="page") { - out = m.link in frappe.boot.page_info; - } else if(m.force_show) { - out = true; - } else if(m._report) { - out = m._report in frappe.boot.user.all_reports; - } else if(m._doctype) { - //out = frappe.model.can_read(m._doctype); - out = frappe.boot.user.can_read.includes(m._doctype); - } else { - if(['Help', 'Settings'].includes(m.module_name)) { - // no permissions necessary for learn - out = true; - } else if(m.module_name==='Setup' && frappe.user.has_role('System Manager')) { - out = true; - } else { - out = frappe.boot.user.allow_modules.indexOf(m.module_name) !== -1; - } - } - if(m.hidden && !show_hidden) { - out = false; - } - if(m.blocked && !show_global) { - out = false; - } - return out; - }; - - let m; - for (var i=0, l=frappe.boot.desktop_icons.length; i < l; i++) { - m = frappe.boot.desktop_icons[i]; - if ((['Setup', 'Core'].indexOf(m.module_name) === -1) && show_module(m)) { - add_to_out(m); - } - } - - if(frappe.user_roles.includes('System Manager')) { - m = frappe.get_module('Setup'); - if(show_module(m)) add_to_out(m); - } - - if(frappe.user_roles.includes('Administrator')) { - m = frappe.get_module('Core'); - if(show_module(m)) add_to_out(m); - } - - return out; -}; - -frappe.add_to_desktop = function(label, doctype, report) { - frappe.call({ - method: 'frappe.desk.doctype.desktop_icon.desktop_icon.add_user_icon', - args: { - 'link': frappe.get_route_str(), - 'label': label, - 'type': 'link', - '_doctype': doctype, - '_report': report - }, - callback: function(r) { - if(r.message) { - frappe.show_alert(__("Added")); - } - } - }); -}; diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index 29b333f83c..39e576ebf8 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -232,6 +232,16 @@ frappe.dom = { frappe.ui.scroll(section.parent().parent()); } }, 200); + }, + pixel_to_inches(pixels) { + const div = $('
    '); + div.appendTo(document.body); + + const dpi_x = document.getElementById('dpi').offsetWidth; + const inches = pixels / dpi_x; + div.remove(); + + return inches; } }; @@ -271,8 +281,8 @@ frappe.timeout = seconds => { }); }; -frappe.scrub = function(text) { - return text.replace(/ /g, "_").toLowerCase(); +frappe.scrub = function(text, spacer='_') { + return text.replace(/ /g, spacer).toLowerCase(); }; frappe.get_modal = function(title, content) { @@ -343,6 +353,7 @@ $(window).on('offline', function() { } else { var is_value_null = is_null(v.value); var is_label_null = is_null(v.label); + var is_disabled = Boolean(v.disabled); if (is_value_null && is_label_null) { var value = v; @@ -352,7 +363,10 @@ $(window).on('offline', function() { var label = is_label_null ? __(value) : __(v.label); } } - $('