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 }}
-
\ 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 @@
-
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) => ``;
-
- 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) { %}
-
- {% } %}
- {% } %}
-
-
-{% 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);
}
}
- $('