diff --git a/cypress/integration/depends_on.js b/cypress/integration/depends_on.js
index 1a6e1082aa..6cf39f3dd8 100644
--- a/cypress/integration/depends_on.js
+++ b/cypress/integration/depends_on.js
@@ -3,7 +3,31 @@ context('Depends On', () => {
cy.login();
cy.visit('/app/website');
return cy.window().its('frappe').then(frappe => {
- return frappe.call('frappe.tests.ui_test_helpers.create_doctype', {
+ return frappe.xcall('frappe.tests.ui_test_helpers.create_child_doctype', {
+ name: 'Child Test Depends On',
+ fields: [
+ {
+ "label": "Child Test Field",
+ "fieldname": "child_test_field",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ },
+ {
+ "label": "Child Dependant Field",
+ "fieldname": "child_dependant_field",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ },
+ {
+ "label": "Child Display Dependant Field",
+ "fieldname": "child_display_dependant_field",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ },
+ ]
+ });
+ }).then(frappe => {
+ return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', {
name: 'Test Depends On',
fields: [
{
@@ -24,6 +48,13 @@ context('Depends On', () => {
"fieldtype": "Data",
'depends_on': "eval:doc.test_field=='Value'"
},
+ {
+ "label": "Child Test Depends On Field",
+ "fieldname": "child_test_depends_on_field",
+ "fieldtype": "Table",
+ 'read_only_depends_on': "eval:doc.test_field=='Some Other Value'",
+ 'options': "Child Test Depends On"
+ },
]
});
});
@@ -48,6 +79,30 @@ context('Depends On', () => {
cy.get('body').click();
cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled');
});
+ it('should set the table and its fields as read only depending on other fields value', () => {
+ cy.new_form('Test Depends On');
+ cy.fill_field('dependant_field', 'Some Value');
+ //cy.fill_field('test_field', 'Some Other Value');
+ cy.get('.frappe-control[data-fieldname="child_test_depends_on_field"]').as('table');
+ cy.get('@table').find('button.grid-add-row').click();
+ cy.get('@table').find('[data-idx="1"]').as('row1');
+ cy.get('@row1').find('.btn-open-row').click();
+ cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid');
+ //cy.get('@row1-form_in_grid').find('')
+ cy.fill_table_field('child_test_depends_on_field', '1', 'child_test_field', 'Some Value');
+ cy.fill_table_field('child_test_depends_on_field', '1', 'child_dependant_field', 'Some Other Value');
+
+ cy.get('@row1-form_in_grid').find('.octicon-triangle-up').click();
+
+ // set the table to read-only
+ cy.fill_field('test_field', 'Some Other Value');
+
+ // grid row form fields should be read-only
+ cy.get('@row1').find('.btn-open-row').click();
+
+ cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_test_field"]').should('be.disabled');
+ cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_dependant_field"]').should('be.disabled');
+ });
it('should display the field depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible');
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index f95bbeeeb5..b907076248 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -160,7 +160,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => {
Cypress.Commands.add('create_records', doc => {
return cy
- .call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
+ .call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc})
.then(r => r.message);
});
@@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
- cy.get('@input').type(value, { waitForAnimations: false, force: true });
+ cy.get('@input').type(value, {waitForAnimations: false, force: true});
}
return cy.get('@input');
});
@@ -204,8 +204,43 @@ Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
return cy.get(selector);
});
+Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
+ cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input');
+
+ if (['Date', 'Time', 'Datetime'].includes(fieldtype)) {
+ cy.get('@input').click().wait(200);
+ cy.get('.datepickers-container .datepicker.active').should('exist');
+ }
+ if (fieldtype === 'Time') {
+ cy.get('@input').clear().wait(200);
+ }
+
+ if (fieldtype === 'Select') {
+ cy.get('@input').select(value);
+ } else {
+ cy.get('@input').type(value, {waitForAnimations: false, force: true});
+ }
+ return cy.get('@input');
+});
+
+Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => {
+ let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`;
+ selector += ` [data-idx="${row_idx}"]`;
+ selector += ` .form-in-grid`;
+
+ if (fieldtype === 'Text Editor') {
+ selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
+ } else if (fieldtype === 'Code') {
+ selector += ` [data-fieldname="${fieldname}"] .ace_text-input`;
+ } else {
+ selector += ` .form-control[data-fieldname="${fieldname}"]`;
+ }
+
+ return cy.get(selector);
+});
+
Cypress.Commands.add('awesomebar', text => {
- cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 });
+ cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100});
});
Cypress.Commands.add('new_form', doctype => {
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 7a6061576d..59edafa891 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -27,6 +27,7 @@ __version__ = '13.0.0-dev'
__title__ = "Frappe Framework"
local = Local()
+controllers = {}
class _dict(dict):
"""dict like object that exposes keys as attributes"""
@@ -628,6 +629,21 @@ def clear_cache(user=None, doctype=None):
local.role_permissions = {}
+def only_has_select_perm(doctype, user=None, ignore_permissions=False):
+ if ignore_permissions:
+ return False
+
+ if not user:
+ user = local.session.user
+
+ import frappe.permissions
+ permissions = frappe.permissions.get_role_permissions(doctype, user=user)
+
+ if permissions.get('select') and not permissions.get('read'):
+ return True
+ else:
+ return False
+
def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False):
"""Raises `frappe.PermissionError` if not permitted.
diff --git a/frappe/app.py b/frappe/app.py
index 82471c4e32..adf2bfa8c9 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -7,8 +7,8 @@ import os
from six import iteritems
import logging
-from werkzeug.wrappers import Request
from werkzeug.local import LocalManager
+from werkzeug.wrappers import Request, Response
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.middleware.profiler import ProfilerMiddleware
from werkzeug.middleware.shared_data import SharedDataMiddleware
@@ -57,19 +57,22 @@ def application(request):
frappe.monitor.start()
frappe.rate_limiter.apply()
- if frappe.local.form_dict.cmd:
+ if request.method == "OPTIONS":
+ response = Response()
+
+ elif frappe.form_dict.cmd:
response = frappe.handler.handle()
- elif frappe.request.path.startswith("/api/"):
+ elif request.path.startswith("/api/"):
response = frappe.api.handle()
- elif frappe.request.path.startswith('/backups'):
+ elif request.path.startswith('/backups'):
response = frappe.utils.response.download_backup(request.path)
- elif frappe.request.path.startswith('/private/files/'):
+ elif request.path.startswith('/private/files/'):
response = frappe.utils.response.download_private_file(request.path)
- elif frappe.local.request.method in ('GET', 'HEAD', 'POST'):
+ elif request.method in ('GET', 'HEAD', 'POST'):
response = frappe.website.render.render()
else:
@@ -88,13 +91,9 @@ def application(request):
rollback = after_request(rollback)
finally:
- if frappe.local.request.method in ("POST", "PUT") and frappe.db and rollback:
+ if request.method in ("POST", "PUT") and frappe.db and rollback:
frappe.db.rollback()
- # set cookies
- if response and hasattr(frappe.local, 'cookie_manager'):
- frappe.local.cookie_manager.flush_cookies(response=response)
-
frappe.rate_limiter.update()
frappe.monitor.stop(response)
frappe.recorder.dump()
@@ -110,9 +109,7 @@ def application(request):
"http_status_code": getattr(response, "status_code", "NOTFOUND")
})
- if response and hasattr(frappe.local, 'rate_limiter'):
- response.headers.extend(frappe.local.rate_limiter.headers())
-
+ process_response(response)
frappe.destroy()
return response
@@ -134,7 +131,46 @@ def init_request(request):
make_form_dict(request)
- frappe.local.http_request = frappe.auth.HTTPRequest()
+ if request.method != "OPTIONS":
+ frappe.local.http_request = frappe.auth.HTTPRequest()
+
+def process_response(response):
+ if not response:
+ return
+
+ # set cookies
+ if hasattr(frappe.local, 'cookie_manager'):
+ frappe.local.cookie_manager.flush_cookies(response=response)
+
+ # rate limiter headers
+ if hasattr(frappe.local, 'rate_limiter'):
+ response.headers.extend(frappe.local.rate_limiter.headers())
+
+ # CORS headers
+ if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors:
+ set_cors_headers(response)
+
+def set_cors_headers(response):
+ origin = frappe.request.headers.get('Origin')
+ if not origin:
+ return
+
+ allow_cors = frappe.conf.allow_cors
+ if allow_cors != "*":
+ if not isinstance(allow_cors, list):
+ allow_cors = [allow_cors]
+
+ if origin not in allow_cors:
+ return
+
+ response.headers.extend({
+ 'Access-Control-Allow-Origin': origin,
+ 'Access-Control-Allow-Credentials': 'true',
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+ 'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,'
+ 'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,'
+ 'Cache-Control,Content-Type')
+ })
def make_form_dict(request):
import json
diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js
index 06d15d6d2c..7028ac486d 100644
--- a/frappe/automation/doctype/auto_repeat/auto_repeat.js
+++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js
@@ -54,10 +54,12 @@ frappe.ui.form.on('Auto Repeat', {
toggle_submit_on_creation: function(frm) {
// submit on creation checkbox
- frappe.model.with_doctype(frm.doc.reference_doctype, () => {
- let meta = frappe.get_meta(frm.doc.reference_doctype);
- frm.toggle_display('submit_on_creation', meta.is_submittable);
- });
+ if (frm.doc.reference_doctype) {
+ frappe.model.with_doctype(frm.doc.reference_doctype, () => {
+ let meta = frappe.get_meta(frm.doc.reference_doctype);
+ frm.toggle_display('submit_on_creation', meta.is_submittable);
+ });
+ }
},
template: function(frm) {
diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json
index 5ff4cbeead..74965346fd 100644
--- a/frappe/automation/doctype/auto_repeat/auto_repeat.json
+++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json
@@ -23,7 +23,7 @@
"repeat_on_last_day",
"column_break_12",
"next_schedule_date",
- "section_break_12",
+ "section_break_16",
"repeat_on_days",
"notification",
"notify_by_email",
@@ -198,20 +198,20 @@
"label": "Repeat on Days",
"options": "Auto Repeat Day"
},
- {
- "depends_on": "eval:doc.frequency==='Weekly';",
- "fieldname": "section_break_12",
- "fieldtype": "Section Break"
- },
{
"default": "0",
"fieldname": "submit_on_creation",
"fieldtype": "Check",
"label": "Submit on Creation"
+ },
+ {
+ "depends_on": "eval:doc.frequency==='Weekly';",
+ "fieldname": "section_break_16",
+ "fieldtype": "Section Break"
}
],
"links": [],
- "modified": "2020-12-10 10:43:13.449172",
+ "modified": "2021-01-12 09:24:49.719611",
"modified_by": "Administrator",
"module": "Automation",
"name": "Auto Repeat",
diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py
index 63785d03c6..4470e83932 100644
--- a/frappe/cache_manager.py
+++ b/frappe/cache_manager.py
@@ -68,6 +68,7 @@ def clear_defaults_cache(user=None):
frappe.cache().delete_key("defaults")
def clear_doctype_cache(doctype=None):
+ clear_controller_cache(doctype)
cache = frappe.cache()
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache):
@@ -99,6 +100,18 @@ def clear_doctype_cache(doctype=None):
for name in doctype_cache_keys:
cache.delete_value(name)
+ # Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured
+ clear_document_cache()
+
+def clear_controller_cache(doctype=None):
+ if not doctype:
+ del frappe.controllers
+ frappe.controllers = {}
+ return
+
+ for site_controllers in frappe.controllers.values():
+ site_controllers.pop(doctype, None)
+
def get_doctype_map(doctype, name, filters=None, order_by=None):
cache = frappe.cache()
cache_key = frappe.scrub(doctype) + '_map'
diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py
index 36d4a39c1a..9f5156424f 100644
--- a/frappe/core/doctype/comment/comment.py
+++ b/frappe/core/doctype/comment/comment.py
@@ -164,7 +164,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
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))
+ (json.dumps(_comments[-100:]), reference_name))
except Exception as e:
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None):
diff --git a/frappe/core/doctype/custom_docperm/custom_docperm.json b/frappe/core/doctype/custom_docperm/custom_docperm.json
index f8f7f58be1..93f5431903 100644
--- a/frappe/core/doctype/custom_docperm/custom_docperm.json
+++ b/frappe/core/doctype/custom_docperm/custom_docperm.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "hash",
"creation": "2017-01-11 04:21:35.217943",
@@ -13,6 +14,7 @@
"column_break_2",
"permlevel",
"section_break_4",
+ "select",
"read",
"write",
"create",
@@ -211,9 +213,16 @@
"fieldtype": "Data",
"label": "Reference Document Type",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "select",
+ "fieldtype": "Check",
+ "label": "Select"
}
],
- "modified": "2019-10-31 16:58:16.157079",
+ "links": [],
+ "modified": "2020-12-03 15:20:48.296730",
"modified_by": "Administrator",
"module": "Core",
"name": "Custom DocPerm",
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index 7880648b6f..dde3dfaee9 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -751,7 +751,7 @@ class Row:
self.warnings.append(
{
"row": self.row_number,
- "message": _("{0} is a mandatory field asdadsf").format(id_field.label),
+ "message": _("{0} is a mandatory field").format(id_field.label),
}
)
return
diff --git a/frappe/core/doctype/docperm/docperm.json b/frappe/core/doctype/docperm/docperm.json
index 1a23118a29..4411a67435 100644
--- a/frappe/core/doctype/docperm/docperm.json
+++ b/frappe/core/doctype/docperm/docperm.json
@@ -1,775 +1,229 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
+ "actions": [],
"autoname": "hash",
- "beta": 0,
"creation": "2013-02-22 01:27:33",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "role_and_level",
+ "role",
+ "if_owner",
+ "column_break_2",
+ "permlevel",
+ "section_break_4",
+ "select",
+ "read",
+ "write",
+ "create",
+ "delete",
+ "column_break_8",
+ "submit",
+ "cancel",
+ "amend",
+ "additional_permissions",
+ "report",
+ "export",
+ "import",
+ "set_user_permissions",
+ "column_break_19",
+ "share",
+ "print",
+ "email"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "role_and_level",
"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": "Role and Level",
- "length": 0,
- "no_copy": 0,
- "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
+ "label": "Role and Level"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "role",
"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": "Role",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "role",
"oldfieldtype": "Link",
"options": "Role",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "150px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "150px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"description": "Apply this rule if the User is the Owner",
"fieldname": "if_owner",
"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": "If user is the owner",
- "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
+ "label": "If user is the owner"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_2",
- "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,
- "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
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
- "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": "Level",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "permlevel",
"oldfieldtype": "Int",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "40px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "40px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "section_break_4",
"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": "Permissions",
- "length": 0,
- "no_copy": 0,
- "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
+ "label": "Permissions"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "read",
"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": "Read",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "read",
"oldfieldtype": "Check",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "write",
"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": "Write",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "write",
"oldfieldtype": "Check",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "create",
"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": "Create",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "create",
"oldfieldtype": "Check",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "delete",
"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": "Delete",
- "length": 0,
- "no_copy": 0,
- "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
+ "label": "Delete"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_8",
- "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,
- "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
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"fieldname": "submit",
"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": "Submit",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "submit",
"oldfieldtype": "Check",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"fieldname": "cancel",
"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": "Cancel",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "cancel",
"oldfieldtype": "Check",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"fieldname": "amend",
"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": "Amend",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "amend",
"oldfieldtype": "Check",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "additional_permissions",
"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": "Additional Permissions",
- "length": 0,
- "no_copy": 0,
- "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
+ "label": "Additional Permissions"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "report",
"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": "Report",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "export",
"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": "Export",
- "length": 0,
- "no_copy": 0,
- "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
+ "label": "Export"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"fieldname": "import",
"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": "Import",
- "length": 0,
- "no_copy": 0,
- "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
+ "label": "Import"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"description": "This role update User Permissions for a user",
"fieldname": "set_user_permissions",
"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": "Set User Permissions",
- "length": 0,
- "no_copy": 0,
- "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
+ "label": "Set User Permissions"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_19",
- "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,
- "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
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "share",
"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": "Share",
- "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
+ "label": "Share"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "print",
"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": "Print",
- "length": 0,
- "no_copy": 0,
- "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
+ "label": "Print"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "email",
"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": "Email",
- "length": 0,
- "no_copy": 0,
- "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
+ "label": "Email"
+ },
+ {
+ "default": "0",
+ "fieldname": "select",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Select"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
"idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
"istable": 1,
- "max_attachments": 0,
- "modified": "2018-05-29 11:54:38.613936",
+ "links": [],
+ "modified": "2020-12-03 15:15:30.488212",
"modified_by": "Administrator",
"module": "Core",
"name": "DocPerm",
"owner": "Administrator",
"permissions": [],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_order": "ASC",
- "track_changes": 0,
- "track_seen": 0
+ "sort_field": "modified",
+ "sort_order": "ASC"
}
\ No newline at end of file
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 6cca646fc9..7faeaac290 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import re, copy, os, shutil
import json
-from frappe.cache_manager import clear_user_cache
+from frappe.cache_manager import clear_user_cache, clear_controller_cache
# imports - third party imports
import six
@@ -395,13 +395,11 @@ class DocType(Document):
if not frappe.flags.in_patch:
self.rename_files_and_folders(old, new)
- for site in frappe.utils.get_sites():
- frappe.cache().delete(f"{site}:doctype_classes", old)
+ clear_controller_cache(old)
def after_delete(self):
if not self.custom:
- for site in frappe.utils.get_sites():
- frappe.cache().delete(f"{site}:doctype_classes", self.name)
+ clear_controller_cache(self.name)
def rename_files_and_folders(self, old, new):
# move files
@@ -1004,10 +1002,10 @@ def validate_fields(meta):
check_sort_field(meta)
check_image_field(meta)
-def validate_permissions_for_doctype(doctype, for_remove=False):
+def validate_permissions_for_doctype(doctype, for_remove=False, alert=False):
"""Validates if permissions are set correctly."""
doctype = frappe.get_doc("DocType", doctype)
- validate_permissions(doctype, for_remove)
+ validate_permissions(doctype, for_remove, alert=alert)
# save permissions
for perm in doctype.get("permissions"):
@@ -1030,9 +1028,10 @@ def clear_permissions_cache(doctype):
""", doctype):
frappe.clear_cache(user=user)
-def validate_permissions(doctype, for_remove=False):
+def validate_permissions(doctype, for_remove=False, alert=False):
permissions = doctype.get("permissions")
- if not permissions:
+ # Some DocTypes may not have permissions by default, don't show alert for them
+ if not permissions and alert:
frappe.msgprint(_('No Permissions Specified'), alert=True, indicator='orange')
issingle = issubmittable = isimportable = False
if doctype:
@@ -1044,7 +1043,7 @@ def validate_permissions(doctype, for_remove=False):
return _("For {0} at level {1} in {2} in row {3}").format(d.role, d.permlevel, d.parent, d.idx)
def check_atleast_one_set(d):
- if not d.read and not d.write and not d.submit and not d.cancel and not d.create:
+ if not d.select and not d.read and not d.write and not d.submit and not d.cancel and not d.create:
frappe.throw(_("{0}: No basic permissions set").format(get_txt(d)))
def check_double(d):
diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py
index 3ff47facc3..4b34293af6 100644
--- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py
+++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py
@@ -6,8 +6,19 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils.data import evaluate_filters
+from frappe import _
class DocumentNamingRule(Document):
+ def validate(self):
+ self.validate_fields_in_conditions()
+
+ def validate_fields_in_conditions(self):
+ if self.has_value_changed("document_type"):
+ docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields]
+ for condition in self.conditions:
+ if condition.field not in docfields:
+ frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type)))
+
def apply(self, doc):
'''
Apply naming rules for the given document. Will set `name` if the rule is matched.
diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json
index 94a48f196c..9aa7b5afe5 100644
--- a/frappe/core/doctype/server_script/server_script.json
+++ b/frappe/core/doctype/server_script/server_script.json
@@ -47,7 +47,7 @@
"fieldname": "doctype_event",
"fieldtype": "Select",
"label": "DocType Event",
- "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
+ "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
},
{
"depends_on": "eval:doc.script_type==='API'",
@@ -88,7 +88,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-12-03 22:42:02.708148",
+ "modified": "2021-01-03 18:50:14.767595",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",
diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py
index 4dc4f12b34..12a8fa47fa 100644
--- a/frappe/core/doctype/server_script/server_script_utils.py
+++ b/frappe/core/doctype/server_script/server_script_utils.py
@@ -6,6 +6,7 @@ import frappe
EVENT_MAP = {
'before_insert': 'Before Insert',
'after_insert': 'After Insert',
+ 'before_validate': 'Before Validate',
'validate': 'Before Save',
'on_update': 'After Save',
'before_submit': 'Before Submit',
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 467ccf7e13..13dbc32620 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -358,7 +358,7 @@
"collapsible": 1,
"fieldname": "email",
"fieldtype": "Section Break",
- "label": "EMail"
+ "label": "Email"
},
{
"description": "Your organization name and address for the email footer.",
@@ -504,4 +504,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 0e684a3dd4..61e99cf196 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -654,6 +654,11 @@
"group": "Activity",
"link_doctype": "ToDo",
"link_fieldname": "owner"
+ },
+ {
+ "group": "Integrations",
+ "link_doctype": "Token Cache",
+ "link_fieldname": "user"
}
],
"max_attachments": 5,
diff --git a/frappe/core/doctype/version/version_view.html b/frappe/core/doctype/version/version_view.html
index 5383be82a1..67f005ed4c 100644
--- a/frappe/core/doctype/version/version_view.html
+++ b/frappe/core/doctype/version/version_view.html
@@ -21,7 +21,7 @@
{{ item[1] }} |
{{ item[2] }} |
- {% endif %}
+ {% endfor %}
{% endif %}
@@ -58,7 +58,7 @@
- {% endif %}
+ {% endfor %}
@@ -93,4 +93,4 @@
{% endfor %}
{% endif %}
-
\ No newline at end of file
+
diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js
index 90747b8aae..3379b77739 100644
--- a/frappe/core/page/permission_manager/permission_manager.js
+++ b/frappe/core/page/permission_manager/permission_manager.js
@@ -292,7 +292,7 @@ frappe.PermissionEngine = class PermissionEngine {
}
get rights() {
- return ["read", "write", "create", "delete", "submit", "cancel", "amend",
+ return ["select", "read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share"]
}
diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py
index 637b526d5c..be8921e2ff 100644
--- a/frappe/core/page/permission_manager/permission_manager.py
+++ b/frappe/core/page/permission_manager/permission_manager.py
@@ -77,6 +77,18 @@ def add(parent, role, permlevel):
@frappe.whitelist()
def update(doctype, role, permlevel, ptype, value=None):
+ """Update role permission params
+
+ Args:
+ doctype (str): Name of the DocType to update params for
+ role (str): Role to be updated for, eg "Website Manager".
+ permlevel (int): perm level the provided rule applies to
+ ptype (str): permission type, example "read", "delete", etc.
+ value (None, optional): value for ptype, None indicates False
+
+ Returns:
+ str: Refresh flag is permission is updated successfully
+ """
frappe.only_for("System Manager")
out = update_permission_property(doctype, role, permlevel, ptype, value)
return 'refresh' if out else None
@@ -92,7 +104,7 @@ def remove(doctype, role, permlevel):
if not frappe.get_all('Custom DocPerm', dict(parent=doctype)):
frappe.throw(_('There must be atleast one permission rule.'), title=_('Cannot Remove'))
- validate_permissions_for_doctype(doctype, for_remove=True)
+ validate_permissions_for_doctype(doctype, for_remove=True, alert=True)
@frappe.whitelist()
def reset(doctype):
diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py
index 5219a98cbd..da43b14fce 100644
--- a/frappe/desk/form/save.py
+++ b/frappe/desk/form/save.py
@@ -42,7 +42,6 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat
except Exception:
frappe.errprint(frappe.utils.get_traceback())
- frappe.msgprint(frappe._("Did not cancel"))
raise
def send_updated_docs(doc):
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index f249c36746..f4e6543844 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -150,7 +150,8 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
# 2 is the index of _relevance column
order_by = "_relevance, {0}, `tab{1}`.idx desc".format(order_by_based_on_meta, doctype)
- ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype))
+ ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
+ ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype))
if doctype in UNTRANSLATED_DOCTYPES:
page_length = None
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 343141c66d..ca4dbb83e2 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -210,7 +210,7 @@ class EmailAccount(Document):
elif not in_receive and any(map(lambda t: t in message, auth_error_codes)):
self.throw_invalid_credentials_exception()
else:
- frappe.throw(e)
+ frappe.throw(cstr(e))
except socket.error:
if in_receive:
diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py
index d8a6a55510..e43b4d131c 100644
--- a/frappe/event_streaming/doctype/event_producer/event_producer.py
+++ b/frappe/event_streaming/doctype/event_producer/event_producer.py
@@ -295,7 +295,7 @@ def set_update(update, producer_site):
if data.changed:
local_doc.update(data.changed)
if data.removed:
- update_row_removed(local_doc, data.removed)
+ local_doc = update_row_removed(local_doc, data.removed)
if data.row_changed:
update_row_changed(local_doc, data.row_changed)
if data.added:
@@ -318,7 +318,17 @@ def update_row_removed(local_doc, removed):
for tablename, rownames in iteritems(removed):
table = local_doc.get_table_field_doctype(tablename)
for row in rownames:
- frappe.db.delete(table, row)
+ table_rows = local_doc.get(tablename)
+ child_table_row = get_child_table_row(table_rows, row)
+ table_rows.remove(child_table_row)
+ local_doc.set(tablename, table_rows)
+ return local_doc
+
+
+def get_child_table_row(table_rows, row):
+ for entry in table_rows:
+ if entry.get('name') == row:
+ return entry
def update_row_changed(local_doc, changed):
diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py
new file mode 100644
index 0000000000..d94a13ea41
--- /dev/null
+++ b/frappe/geo/utils.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+
+from pymysql import InternalError
+
+
+@frappe.whitelist()
+def get_coords(doctype, filters, type):
+ '''Get a geojson dict representing a doctype.'''
+ filters_sql = get_coords_conditions(doctype, filters)[4:]
+
+ coords = None
+ if type == 'location_field':
+ coords = return_location(doctype, filters_sql)
+ elif type == 'coordinates':
+ coords = return_coordinates(doctype, filters_sql)
+
+ out = convert_to_geojson(type, coords)
+ return out
+
+def convert_to_geojson(type, coords):
+ '''Converts GPS coordinates to geoJSON string.'''
+ geojson = {"type": "FeatureCollection", "features": None}
+
+ if type == 'location_field':
+ geojson['features'] = merge_location_features_in_one(coords)
+ elif type == 'coordinates':
+ geojson['features'] = create_gps_markers(coords)
+
+ return geojson
+
+
+def merge_location_features_in_one(coords):
+ '''Merging all features from location field.'''
+ geojson_dict = []
+ for element in coords:
+ geojson_loc = frappe.parse_json(element['location'])
+ if not geojson_loc:
+ continue
+ for coord in geojson_loc['features']:
+ coord['properties']['name'] = element['name']
+ geojson_dict.append(coord.copy())
+
+ return geojson_dict
+
+
+def create_gps_markers(coords):
+ '''Build Marker based on latitude and longitude.'''
+ geojson_dict = []
+ for i in coords:
+ node = {"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": None}}
+ node['properties']['name'] = i.name
+ node['geometry']['coordinates'] = [i.latitude, i.longitude]
+ geojson_dict.append(node.copy())
+
+ return geojson_dict
+
+
+def return_location(doctype, filters_sql):
+ '''Get name and location fields for Doctype.'''
+ if filters_sql:
+ try:
+ coords = frappe.db.sql('''SELECT name, location FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True)
+ except InternalError:
+ frappe.msgprint(frappe._('This Doctype does not contain location fields'), raise_exception=True)
+ return
+ else:
+ coords = frappe.get_all(doctype, fields=['name', 'location'])
+ return coords
+
+
+def return_coordinates(doctype, filters_sql):
+ '''Get name, latitude and longitude fields for Doctype.'''
+ if filters_sql:
+ try:
+ coords = frappe.db.sql('''SELECT name, latitude, longitude FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True)
+ except InternalError:
+ frappe.msgprint(frappe._('This Doctype does not contain latitude and longitude fields'), raise_exception=True)
+ return
+ else:
+ coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude'])
+ return coords
+
+
+def get_coords_conditions(doctype, filters=None):
+ '''Returns SQL conditions with user permissions and filters for event queries.'''
+ from frappe.desk.reportview import get_filters_cond
+ if not frappe.has_permission(doctype):
+ frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
+
+ return get_filters_cond(doctype, filters, [], with_match_conditions=True)
diff --git a/frappe/hooks.py b/frappe/hooks.py
index d024cb7929..97a8b70953 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -18,7 +18,7 @@ app_email = "info@frappe.io"
docs_app = "frappe_io"
-translator_url = "https://translatev2.erpnext.com"
+translator_url = "https://translate.erpnext.com"
before_install = "frappe.utils.install.before_install"
after_install = "frappe.utils.install.after_install"
diff --git a/frappe/integrations/doctype/connected_app/__init__.py b/frappe/integrations/doctype/connected_app/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js
new file mode 100644
index 0000000000..4d20f65559
--- /dev/null
+++ b/frappe/integrations/doctype/connected_app/connected_app.js
@@ -0,0 +1,38 @@
+// Copyright (c) 2019, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Connected App', {
+ refresh: frm => {
+ frm.add_custom_button(__('Get OpenID Configuration'), async () => {
+ if (!frm.doc.openid_configuration) {
+ frappe.msgprint(__('Please enter OpenID Configuration URL'));
+ } else {
+ try {
+ const response = await fetch(frm.doc.openid_configuration);
+ const oidc = await response.json();
+ frm.set_value('authorization_uri', oidc.authorization_endpoint);
+ frm.set_value('token_uri', oidc.token_endpoint);
+ frm.set_value('userinfo_uri', oidc.userinfo_endpoint);
+ frm.set_value('introspection_uri', oidc.introspection_endpoint);
+ frm.set_value('revocation_uri', oidc.revocation_endpoint);
+ } catch (error) {
+ frappe.msgprint(__('Please check OpenID Configuration URL'));
+ }
+ }
+ });
+
+ if (!frm.is_new()) {
+ frm.add_custom_button(__('Connect to {}', [frm.doc.provider_name]), async () => {
+ frappe.call({
+ method: 'initiate_web_application_flow',
+ doc: frm.doc,
+ callback: function(r) {
+ window.open(r.message, '_blank');
+ }
+ });
+ });
+ }
+
+ frm.toggle_display('sb_client_credentials_section', !frm.is_new());
+ }
+});
diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json
new file mode 100644
index 0000000000..e5dbb0472a
--- /dev/null
+++ b/frappe/integrations/doctype/connected_app/connected_app.json
@@ -0,0 +1,166 @@
+{
+ "actions": [],
+ "beta": 1,
+ "creation": "2019-01-24 15:51:06.362222",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "provider_name",
+ "cb_00",
+ "openid_configuration",
+ "sb_client_credentials_section",
+ "client_id",
+ "redirect_uri",
+ "cb_01",
+ "client_secret",
+ "sb_scope_section",
+ "scopes",
+ "sb_endpoints_section",
+ "authorization_uri",
+ "token_uri",
+ "revocation_uri",
+ "cb_02",
+ "userinfo_uri",
+ "introspection_uri",
+ "section_break_18",
+ "query_parameters"
+ ],
+ "fields": [
+ {
+ "fieldname": "provider_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Provider Name",
+ "reqd": 1
+ },
+ {
+ "fieldname": "cb_00",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "openid_configuration",
+ "fieldtype": "Data",
+ "label": "OpenID Configuration"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "sb_client_credentials_section",
+ "fieldtype": "Section Break",
+ "label": "Client Credentials"
+ },
+ {
+ "fieldname": "client_id",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Client Id"
+ },
+ {
+ "fieldname": "redirect_uri",
+ "fieldtype": "Data",
+ "label": "Redirect URI",
+ "read_only": 1
+ },
+ {
+ "fieldname": "cb_01",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "client_secret",
+ "fieldtype": "Password",
+ "label": "Client Secret"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "sb_scope_section",
+ "fieldtype": "Section Break",
+ "label": "Scopes"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "sb_endpoints_section",
+ "fieldtype": "Section Break",
+ "label": "Endpoints"
+ },
+ {
+ "fieldname": "cb_02",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "scopes",
+ "fieldtype": "Table",
+ "label": "Scopes",
+ "options": "OAuth Scope"
+ },
+ {
+ "fieldname": "authorization_uri",
+ "fieldtype": "Data",
+ "label": "Authorization URI"
+ },
+ {
+ "fieldname": "token_uri",
+ "fieldtype": "Data",
+ "label": "Token URI"
+ },
+ {
+ "fieldname": "revocation_uri",
+ "fieldtype": "Data",
+ "label": "Revocation URI"
+ },
+ {
+ "fieldname": "userinfo_uri",
+ "fieldtype": "Data",
+ "label": "Userinfo URI"
+ },
+ {
+ "fieldname": "introspection_uri",
+ "fieldtype": "Data",
+ "label": "Introspection URI"
+ },
+ {
+ "fieldname": "section_break_18",
+ "fieldtype": "Section Break",
+ "label": "Extra Parameters"
+ },
+ {
+ "fieldname": "query_parameters",
+ "fieldtype": "Table",
+ "label": "Query Parameters",
+ "options": "Query Parameters"
+ }
+ ],
+ "links": [
+ {
+ "link_doctype": "Token Cache",
+ "link_fieldname": "connected_app"
+ }
+ ],
+ "modified": "2020-11-16 16:29:50.277405",
+ "modified_by": "Administrator",
+ "module": "Integrations",
+ "name": "Connected App",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "read": 1,
+ "role": "All"
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "provider_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py
new file mode 100644
index 0000000000..ec08f8e4be
--- /dev/null
+++ b/frappe/integrations/doctype/connected_app/connected_app.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import os
+from urllib.parse import urljoin
+from urllib.parse import urlencode
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from requests_oauthlib import OAuth2Session
+
+if any((os.getenv('CI'), frappe.conf.developer_mode, frappe.conf.allow_tests)):
+ # Disable mandatory TLS in developer mode and tests
+ os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
+
+class ConnectedApp(Document):
+ """Connect to a remote oAuth Server. Retrieve and store user's access token
+ in a Token Cache.
+ """
+
+ def validate(self):
+ base_url = frappe.utils.get_url()
+ callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name
+ self.redirect_uri = urljoin(base_url, callback_path)
+
+ def get_oauth2_session(self, user=None, init=False):
+ token = None
+ token_updater = None
+
+ if not init:
+ user = user or frappe.session.user
+ token_cache = self.get_user_token(user)
+ token = token_cache.get_json()
+ token_updater = token_cache.update_data
+
+ return OAuth2Session(
+ client_id=self.client_id,
+ token=token,
+ token_updater=token_updater,
+ auto_refresh_url=self.token_uri,
+ redirect_uri=self.redirect_uri,
+ scope=self.get_scopes()
+ )
+
+ def initiate_web_application_flow(self, user=None, success_uri=None):
+ """Return an authorization URL for the user. Save state in Token Cache."""
+ user = user or frappe.session.user
+ oauth = self.get_oauth2_session(init=True)
+ query_params = self.get_query_params()
+ authorization_url, state = oauth.authorization_url(self.authorization_uri, **query_params)
+ token_cache = self.get_token_cache(user)
+
+ if not token_cache:
+ token_cache = frappe.new_doc('Token Cache')
+ token_cache.user = user
+ token_cache.connected_app = self.name
+
+ token_cache.success_uri = success_uri
+ token_cache.state = state
+ token_cache.save(ignore_permissions=True)
+ frappe.db.commit()
+
+ return authorization_url
+
+ def get_user_token(self, user=None, success_uri=None):
+ """Return an existing user token or initiate a Web Application Flow."""
+ user = user or frappe.session.user
+ token_cache = self.get_token_cache(user)
+
+ if token_cache:
+ return token_cache
+
+ redirect = self.initiate_web_application_flow(user, success_uri)
+ frappe.local.response['type'] = 'redirect'
+ frappe.local.response['location'] = redirect
+ return redirect
+
+ def get_token_cache(self, user):
+ token_cache = None
+ token_cache_name = self.name + '-' + user
+
+ if frappe.db.exists('Token Cache', token_cache_name):
+ token_cache = frappe.get_doc('Token Cache', token_cache_name)
+
+ return token_cache
+
+ def get_scopes(self):
+ return [row.scope for row in self.scopes]
+
+ def get_query_params(self):
+ return {param.key: param.value for param in self.query_parameters}
+
+
+@frappe.whitelist(allow_guest=True)
+def callback(code=None, state=None):
+ """Handle client's code.
+
+ Called during the oauthorization flow by the remote oAuth2 server to
+ transmit a code that can be used by the local server to obtain an access
+ token.
+ """
+ if frappe.request.method != 'GET':
+ frappe.throw(_('Invalid request method: {}').format(frappe.request.method))
+
+ if frappe.session.user == 'Guest':
+ frappe.local.response['type'] = 'redirect'
+ frappe.local.response['location'] = '/login?' + urlencode({'redirect-to': frappe.request.url})
+ return
+
+ path = frappe.request.path[1:].split('/')
+ if len(path) != 4 or not path[3]:
+ frappe.throw(_('Invalid Parameters.'))
+
+ connected_app = frappe.get_doc('Connected App', path[3])
+ token_cache = frappe.get_doc('Token Cache', connected_app.name + '-' + frappe.session.user)
+
+ if state != token_cache.state:
+ frappe.throw(_('Invalid state.'))
+
+ oauth_session = connected_app.get_oauth2_session(init=True)
+ query_params = connected_app.get_query_params()
+ token = oauth_session.fetch_token(connected_app.token_uri,
+ code=code,
+ client_secret=connected_app.get_password('client_secret'),
+ include_client_id=True,
+ **query_params
+ )
+ token_cache.update_data(token)
+
+ frappe.local.response['type'] = 'redirect'
+ frappe.local.response['location'] = token_cache.get('success_uri') or connected_app.get_url()
diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py
new file mode 100644
index 0000000000..6faa542a60
--- /dev/null
+++ b/frappe/integrations/doctype/connected_app/test_connected_app.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies and contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+import requests
+from urllib.parse import urljoin
+
+import frappe
+from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key
+
+
+def get_user(usr, pwd):
+ user = frappe.new_doc('User')
+ user.email = usr
+ user.enabled = 1
+ user.first_name = "_Test"
+ user.new_password = pwd
+ user.roles = []
+ user.append('roles', {
+ 'doctype': 'Has Role',
+ 'parentfield': 'roles',
+ 'role': 'System Manager'
+ })
+ user.insert()
+
+ return user
+
+
+def get_connected_app():
+ doctype = 'Connected App'
+ connected_app = frappe.new_doc(doctype)
+ connected_app.provider_name = 'frappe'
+ connected_app.scopes = []
+ connected_app.append('scopes', {'scope': 'all'})
+ connected_app.insert()
+
+ return connected_app
+
+
+def get_oauth_client():
+ oauth_client = frappe.new_doc('OAuth Client')
+ oauth_client.app_name = '_Test Connected App'
+ oauth_client.redirect_uris = 'to be replaced'
+ oauth_client.default_redirect_uri = 'to be replaced'
+ oauth_client.grant_type = 'Authorization Code'
+ oauth_client.response_type = 'Code'
+ oauth_client.skip_authorization = 1
+ oauth_client.insert()
+
+ return oauth_client
+
+
+class TestConnectedApp(unittest.TestCase):
+
+ def setUp(self):
+ """Set up a Connected App that connects to our own oAuth provider.
+
+ Frappe comes with it's own oAuth2 provider that we can test against. The
+ client credentials can be obtained from an "OAuth Client". All depends
+ on "Social Login Key" so we create one as well.
+
+ The redirect URIs from "Connected App" and "OAuth Client" have to match.
+ Frappe's "Authorization URL" and "Access Token URL" (actually they're
+ just endpoints) are stored in "Social Login Key" so we get them from
+ there.
+ """
+ self.user_name = 'test-connected-app@example.com'
+ self.user_password = 'Eastern_43A1W'
+
+ self.user = get_user(self.user_name, self.user_password)
+ self.connected_app = get_connected_app()
+ self.oauth_client = get_oauth_client()
+ social_login_key = create_or_update_social_login_key()
+ self.base_url = social_login_key.get('base_url')
+
+ frappe.db.commit()
+ self.connected_app.reload()
+ self.oauth_client.reload()
+
+ redirect_uri = self.connected_app.get('redirect_uri')
+ self.oauth_client.update({
+ 'redirect_uris': redirect_uri,
+ 'default_redirect_uri': redirect_uri
+ })
+ self.oauth_client.save()
+
+ self.connected_app.update({
+ 'authorization_uri': urljoin(self.base_url, social_login_key.get('authorize_url')),
+ 'client_id': self.oauth_client.get('client_id'),
+ 'client_secret': self.oauth_client.get('client_secret'),
+ 'token_uri': urljoin(self.base_url, social_login_key.get('access_token_url'))
+ })
+ self.connected_app.save()
+
+ frappe.db.commit()
+ self.connected_app.reload()
+ self.oauth_client.reload()
+
+ def test_web_application_flow(self):
+ """Simulate a logged in user who opens the authorization URL."""
+ def login():
+ return session.get(urljoin(self.base_url, '/api/method/login'), params={
+ 'usr': self.user_name,
+ 'pwd': self.user_password
+ })
+
+ session = requests.Session()
+
+ # first login of a new user on a new site fails with "401 UNAUTHORIZED"
+ # when anybody fixes that, the two lines below can be removed
+ first_login = login()
+ self.assertEqual(first_login.status_code, 401)
+
+ second_login = login()
+ self.assertEqual(second_login.status_code, 200)
+
+ authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name)
+
+ auth_response = session.get(authorization_url)
+ self.assertEqual(auth_response.status_code, 200)
+
+ callback_response = session.get(auth_response.url)
+ self.assertEqual(callback_response.status_code, 200)
+
+ self.token_cache = self.connected_app.get_token_cache(self.user_name)
+ token = self.token_cache.get_password('access_token')
+ self.assertNotEqual(token, None)
+
+ oauth2_session = self.connected_app.get_oauth2_session(self.user_name)
+ resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user'))
+ self.assertEqual(resp.json().get('message'), self.user_name)
+
+ def tearDown(self):
+ def delete_if_exists(attribute):
+ doc = getattr(self, attribute, None)
+ if doc:
+ doc.delete()
+
+ delete_if_exists('token_cache')
+ delete_if_exists('connected_app')
+
+ if getattr(self, 'oauth_client', None):
+ tokens = frappe.get_all('OAuth Bearer Token', filters={
+ 'client': self.oauth_client.name
+ })
+ for token in tokens:
+ doc = frappe.get_doc('OAuth Bearer Token', token.name)
+ doc.delete()
+
+ codes = frappe.get_all('OAuth Authorization Code', filters={
+ 'client': self.oauth_client.name
+ })
+ for code in codes:
+ doc = frappe.get_doc('OAuth Authorization Code', code.name)
+ doc.delete()
+
+ delete_if_exists('user')
+ delete_if_exists('oauth_client')
+
+ frappe.db.commit()
diff --git a/frappe/integrations/doctype/connected_app/test_records.json b/frappe/integrations/doctype/connected_app/test_records.json
new file mode 100644
index 0000000000..4d19369248
--- /dev/null
+++ b/frappe/integrations/doctype/connected_app/test_records.json
@@ -0,0 +1,13 @@
+[
+ {
+ "doctype": "Connected App",
+ "provider_name": "frappe",
+ "client_id": "test_client_id",
+ "client_secret": "test_client_secret",
+ "scopes": [
+ {
+ "scope": "all"
+ }
+ ]
+ }
+]
diff --git a/frappe/integrations/doctype/oauth_client/test_records.json b/frappe/integrations/doctype/oauth_client/test_records.json
index cff06457c5..11e6338a87 100644
--- a/frappe/integrations/doctype/oauth_client/test_records.json
+++ b/frappe/integrations/doctype/oauth_client/test_records.json
@@ -1,7 +1,6 @@
[
{
- "app_name": "_Test OAuth Client",
- "client_id": "test_client_id",
+ "app_name": "_Test OAuth Client",
"client_secret": "test_client_secret",
"default_redirect_uri": "http://localhost",
"docstatus": 0,
diff --git a/frappe/integrations/doctype/oauth_scope/__init__.py b/frappe/integrations/doctype/oauth_scope/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.json b/frappe/integrations/doctype/oauth_scope/oauth_scope.json
new file mode 100644
index 0000000000..3a6e528999
--- /dev/null
+++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.json
@@ -0,0 +1,30 @@
+{
+ "actions": [],
+ "creation": "2020-07-15 22:08:14.616585",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "scope"
+ ],
+ "fields": [
+ {
+ "fieldname": "scope",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Scope"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-07-15 22:15:18.930632",
+ "modified_by": "Administrator",
+ "module": "Integrations",
+ "name": "OAuth Scope",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.py b/frappe/integrations/doctype/oauth_scope/oauth_scope.py
new file mode 100644
index 0000000000..a5dfe7e1ce
--- /dev/null
+++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class OAuthScope(Document):
+ pass
diff --git a/frappe/integrations/doctype/query_parameters/__init__.py b/frappe/integrations/doctype/query_parameters/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/integrations/doctype/query_parameters/query_parameters.json b/frappe/integrations/doctype/query_parameters/query_parameters.json
new file mode 100644
index 0000000000..de31c28df7
--- /dev/null
+++ b/frappe/integrations/doctype/query_parameters/query_parameters.json
@@ -0,0 +1,37 @@
+{
+ "actions": [],
+ "creation": "2020-11-16 14:54:37.226914",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "key",
+ "value"
+ ],
+ "fields": [
+ {
+ "fieldname": "key",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Key",
+ "reqd": 1
+ },
+ {
+ "fieldname": "value",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Value",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-11-16 15:18:35.887149",
+ "modified_by": "Administrator",
+ "module": "Integrations",
+ "name": "Query Parameters",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/query_parameters/query_parameters.py b/frappe/integrations/doctype/query_parameters/query_parameters.py
new file mode 100644
index 0000000000..bfb8eae0b6
--- /dev/null
+++ b/frappe/integrations/doctype/query_parameters/query_parameters.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class QueryParameters(Document):
+ pass
diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py
index 58bd48d64a..e0b99ad391 100644
--- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py
+++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py
@@ -22,3 +22,17 @@ def make_social_login_key(**kwargs):
kwargs["provider_name"] = "Test OAuth2 Provider"
doc = frappe.get_doc(kwargs)
return doc
+
+def create_or_update_social_login_key():
+ # used in other tests (connected app, oauth20)
+ try:
+ social_login_key = frappe.get_doc("Social Login Key", "frappe")
+ except frappe.DoesNotExistError:
+ social_login_key = frappe.new_doc("Social Login Key")
+ social_login_key.get_social_login_provider("Frappe", initialize=True)
+ social_login_key.base_url = frappe.utils.get_url()
+ social_login_key.enable_social_login = 0
+ social_login_key.save()
+ frappe.db.commit()
+
+ return social_login_key
diff --git a/frappe/integrations/doctype/token_cache/__init__.py b/frappe/integrations/doctype/token_cache/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/integrations/doctype/token_cache/test_records.json b/frappe/integrations/doctype/token_cache/test_records.json
new file mode 100644
index 0000000000..05840221a6
--- /dev/null
+++ b/frappe/integrations/doctype/token_cache/test_records.json
@@ -0,0 +1,18 @@
+[
+ {
+ "doctype": "Token Cache",
+ "user": "test@example.com",
+ "access_token": "test-access-token",
+ "refresh_token": "test-refresh-token",
+ "token_type": "Bearer",
+ "expires_in": 1000,
+ "scopes": [
+ {
+ "scope": "all"
+ },
+ {
+ "scope": "openid"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py
new file mode 100644
index 0000000000..73c9f38fce
--- /dev/null
+++ b/frappe/integrations/doctype/token_cache/test_token_cache.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies and contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+import frappe
+
+test_dependencies = ['User', 'Connected App', 'Token Cache']
+
+class TestTokenCache(unittest.TestCase):
+
+ def setUp(self):
+ self.token_cache = frappe.get_last_doc('Token Cache')
+ self.token_cache.update({'connected_app': frappe.get_last_doc('Connected App').name})
+ self.token_cache.save()
+
+ def test_get_auth_header(self):
+ self.token_cache.get_auth_header()
+
+ def test_update_data(self):
+ self.token_cache.update_data({
+ 'access_token': 'new-access-token',
+ 'refresh_token': 'new-refresh-token',
+ 'token_type': 'bearer',
+ 'expires_in': 2000,
+ 'scope': 'new scope'
+ })
+
+ def test_get_expires_in(self):
+ self.token_cache.get_expires_in()
+
+ def test_is_expired(self):
+ self.token_cache.is_expired()
+
+ def get_json(self):
+ self.token_cache.get_json()
diff --git a/frappe/integrations/doctype/token_cache/token_cache.js b/frappe/integrations/doctype/token_cache/token_cache.js
new file mode 100644
index 0000000000..b7cac9b804
--- /dev/null
+++ b/frappe/integrations/doctype/token_cache/token_cache.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Token Cache', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json
new file mode 100644
index 0000000000..c016405031
--- /dev/null
+++ b/frappe/integrations/doctype/token_cache/token_cache.json
@@ -0,0 +1,110 @@
+{
+ "actions": [],
+ "autoname": "format:{connected_app}-{user}",
+ "beta": 1,
+ "creation": "2019-01-24 16:56:55.631096",
+ "doctype": "DocType",
+ "document_type": "System",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "connected_app",
+ "provider_name",
+ "access_token",
+ "refresh_token",
+ "expires_in",
+ "state",
+ "scopes",
+ "success_uri",
+ "token_type"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "User",
+ "options": "User",
+ "read_only": 1
+ },
+ {
+ "fieldname": "connected_app",
+ "fieldtype": "Link",
+ "label": "Connected App",
+ "options": "Connected App",
+ "read_only": 1
+ },
+ {
+ "fieldname": "access_token",
+ "fieldtype": "Password",
+ "label": "Access Token",
+ "read_only": 1
+ },
+ {
+ "fieldname": "refresh_token",
+ "fieldtype": "Password",
+ "label": "Refresh Token",
+ "read_only": 1
+ },
+ {
+ "fieldname": "expires_in",
+ "fieldtype": "Int",
+ "label": "Expires In",
+ "read_only": 1
+ },
+ {
+ "fieldname": "state",
+ "fieldtype": "Data",
+ "label": "State",
+ "read_only": 1
+ },
+ {
+ "fieldname": "scopes",
+ "fieldtype": "Table",
+ "label": "Scopes",
+ "options": "OAuth Scope",
+ "read_only": 1
+ },
+ {
+ "fieldname": "success_uri",
+ "fieldtype": "Data",
+ "label": "Success URI",
+ "read_only": 1
+ },
+ {
+ "fieldname": "token_type",
+ "fieldtype": "Data",
+ "label": "Token Type",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "connected_app.provider_name",
+ "fieldname": "provider_name",
+ "fieldtype": "Data",
+ "label": "Provider Name",
+ "read_only": 1
+ }
+ ],
+ "links": [],
+ "modified": "2020-11-13 13:35:53.714352",
+ "modified_by": "Administrator",
+ "module": "Integrations",
+ "name": "Token Cache",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "delete": 1,
+ "read": 1,
+ "role": "System Manager"
+ },
+ {
+ "delete": 1,
+ "if_owner": 1,
+ "read": 1,
+ "role": "All"
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py
new file mode 100644
index 0000000000..7cac58fae0
--- /dev/null
+++ b/frappe/integrations/doctype/token_cache/token_cache.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+from datetime import datetime, timedelta
+
+import frappe
+from frappe import _
+from frappe.utils import cstr, cint
+from frappe.model.document import Document
+
+class TokenCache(Document):
+
+ def get_auth_header(self):
+ if self.access_token:
+ headers = {'Authorization': 'Bearer ' + self.get_password('access_token')}
+ return headers
+
+ raise frappe.exceptions.DoesNotExistError
+
+ def update_data(self, data):
+ """
+ Store data returned by authorization flow.
+
+ Params:
+ data - Dict with access_token, refresh_token, expires_in and scope.
+ """
+ token_type = cstr(data.get('token_type', '')).lower()
+ if token_type not in ['bearer', 'mac']:
+ frappe.throw(_('Received an invalid token type.'))
+ # 'Bearer' or 'MAC'
+ token_type = token_type.title() if token_type == 'bearer' else token_type.upper()
+
+ self.token_type = token_type
+ self.access_token = cstr(data.get('access_token', ''))
+ self.refresh_token = cstr(data.get('refresh_token', ''))
+ self.expires_in = cint(data.get('expires_in', 0))
+
+ new_scopes = data.get('scope')
+ if new_scopes:
+ if isinstance(new_scopes, str):
+ new_scopes = new_scopes.split(' ')
+ if isinstance(new_scopes, list):
+ self.scopes = None
+ for scope in new_scopes:
+ self.append('scopes', {'scope': scope})
+
+ self.state = None
+ self.save(ignore_permissions=True)
+ frappe.db.commit()
+ return self
+
+ def get_expires_in(self):
+ expiry_time = frappe.utils.get_datetime(self.modified) + timedelta(self.expires_in)
+ return (datetime.now() - expiry_time).total_seconds()
+
+ def is_expired(self):
+ return self.get_expires_in() < 0
+
+ def get_json(self):
+ return {
+ 'access_token': self.get_password('access_token', ''),
+ 'refresh_token': self.get_password('refresh_token', ''),
+ 'expires_in': self.get_expires_in(),
+ 'token_type': self.token_type
+ }
diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py
index f1556aa661..ad64d9f714 100644
--- a/frappe/integrations/doctype/webhook/webhook.py
+++ b/frappe/integrations/doctype/webhook/webhook.py
@@ -85,7 +85,7 @@ def enqueue_webhook(doc, webhook):
for i in range(3):
try:
- r = requests.post(webhook.request_url, data=json.dumps(data), headers=headers, timeout=5)
+ r = requests.post(webhook.request_url, data=json.dumps(data, default=str), headers=headers, timeout=5)
r.raise_for_status()
frappe.logger().debug({"webhook_success": r.text})
break
diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py
index a750c8328c..07db778a2d 100644
--- a/frappe/integrations/oauth2.py
+++ b/frappe/integrations/oauth2.py
@@ -20,6 +20,7 @@ def get_oauth_server():
return frappe.local.oauth_server
def sanitize_kwargs(param_kwargs):
+ """Remove 'data' and 'cmd' keys, if present."""
arguments = param_kwargs
arguments.pop('data', None)
arguments.pop('cmd', None)
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 5d86b3bac8..7a90ecaca5 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -48,7 +48,7 @@ def get_controller(doctype):
else:
class_overrides = frappe.get_hooks('override_doctype_class')
if class_overrides and class_overrides.get(doctype):
- import_path = frappe.get_hooks('override_doctype_class').get(doctype)[-1]
+ import_path = class_overrides[doctype][-1]
module_path, classname = import_path.rsplit('.', 1)
module = frappe.get_module(module_path)
if not hasattr(module, classname):
@@ -69,10 +69,13 @@ def get_controller(doctype):
if frappe.local.dev_server:
return _get_controller()
-
- key = '{}:doctype_classes'.format(frappe.local.site)
- return frappe.cache().hget(key, doctype, generator=_get_controller, shared=True)
-
+
+ site_controllers = frappe.controllers.setdefault(frappe.local.site, {})
+ if doctype not in site_controllers:
+ site_controllers[doctype] = _get_controller()
+
+ return site_controllers[doctype]
+
class BaseDocument(object):
ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index ee4b1dde2a..d429ba31ea 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -40,7 +40,10 @@ class DatabaseQuery(object):
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
update=None, add_total_row=None, user_settings=None, reference_doctype=None,
return_query=False, strict=True, pluck=None, ignore_ddl=False):
- if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user):
+ if not ignore_permissions and \
+ not frappe.has_permission(self.doctype, "select", user=user) and \
+ not frappe.has_permission(self.doctype, "read", user=user):
+
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype))
raise frappe.PermissionError(self.doctype)
@@ -315,7 +318,10 @@ class DatabaseQuery(object):
def append_table(self, table_name):
self.tables.append(table_name)
doctype = table_name[4:-1]
- if (not self.flags.ignore_permissions) and (not frappe.has_permission(doctype)):
+ ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
+
+ if (not self.flags.ignore_permissions) and\
+ (not frappe.has_permission(doctype, ptype=ptype)):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype))
raise frappe.PermissionError(doctype)
@@ -576,7 +582,7 @@ class DatabaseQuery(object):
self.shared = frappe.share.get_shared(self.doctype, self.user)
if (not meta.istable and
- not role_permissions.get("read") and
+ not (role_permissions.get("select") or role_permissions.get("read")) and
not self.flags.ignore_permissions and
not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)):
only_if_shared = True
diff --git a/frappe/model/document.py b/frappe/model/document.py
index d17025538b..1cd981ead8 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -939,15 +939,17 @@ class Document(BaseDocument):
self.load_doc_before_save()
self.reset_seen()
+ # before_validate method should be executed before ignoring validations
+ if self._action in ("save", "submit"):
+ self.run_method("before_validate")
+
if self.flags.ignore_validate:
return
if self._action=="save":
- self.run_method("before_validate")
self.run_method("validate")
self.run_method("before_save")
elif self._action=="submit":
- self.run_method("before_validate")
self.run_method("validate")
self.run_method("before_submit")
elif self._action=="cancel":
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index c740d495c1..88ed1a7e78 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -68,7 +68,7 @@ def load_doctype_from_file(doctype):
class Meta(Document):
_metaclass = True
default_fields = list(default_fields)[1:]
- special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def", 'DocType Action', 'DocType Link')
+ special_doctypes = ("DocField", "DocPerm", "DocType", "Module Def", 'DocType Action', 'DocType Link')
def __init__(self, doctype):
self._fields = {}
@@ -484,6 +484,8 @@ class Meta(Document):
if not data.transactions:
# init groups
data.transactions = []
+
+ if not data.non_standard_fieldnames:
data.non_standard_fieldnames = {}
for link in dashboard_links:
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index 2baf0c562c..2c9dc5d823 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -21,8 +21,16 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne
docname = rename_doc(doctype=doctype, old=docname, new=new_name, merge=merge)
if old_title and new_title and not old_title == new_title:
- frappe.db.set_value(doctype, docname, title_field, new_title)
- frappe.msgprint(_('Saved'), alert=True, indicator='green')
+ try:
+ frappe.db.set_value(doctype, docname, title_field, new_title)
+ frappe.msgprint(_('Saved'), alert=True, indicator='green')
+ except Exception as e:
+ if frappe.db.is_duplicate_entry(e):
+ frappe.throw(
+ _("{0} {1} already exists").format(doctype, frappe.bold(docname)),
+ title=_("Duplicate Name"),
+ exc=frappe.DuplicateEntryError
+ )
return docname
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index 43e26cc5d0..3e8125f9b1 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -120,9 +120,8 @@ def apply_workflow(doc, action):
return doc
@frappe.whitelist()
-def can_cancel_document(doc):
- doc = frappe.get_doc(frappe.parse_json(doc))
- workflow = get_workflow(doc.doctype)
+def can_cancel_document(doctype):
+ workflow = get_workflow(doctype)
for state_doc in workflow.states:
if state_doc.doc_status == '2':
for transition in workflow.transitions:
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 8299089444..0865911c4e 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -326,3 +326,4 @@ execute:frappe.delete_doc_if_exists('Page', 'dashboard', force=1)
frappe.core.doctype.page.patches.drop_unused_pages
execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
frappe.patches.v13_0.rename_desk_page_to_workspace
+frappe.patches.v13_0.delete_package_publish_tool
diff --git a/frappe/patches/v13_0/delete_package_publish_tool.py b/frappe/patches/v13_0/delete_package_publish_tool.py
new file mode 100644
index 0000000000..25024f58dd
--- /dev/null
+++ b/frappe/patches/v13_0/delete_package_publish_tool.py
@@ -0,0 +1,11 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+
+def execute():
+ frappe.delete_doc("DocType", "Package Publish Tool", ignore_missing=True)
+ frappe.delete_doc("DocType", "Package Document Type", ignore_missing=True)
+ frappe.delete_doc("DocType", "Package Publish Target", ignore_missing=True)
diff --git a/frappe/patches/v13_0/website_theme_custom_scss.py b/frappe/patches/v13_0/website_theme_custom_scss.py
index 0035283428..a5f08324e8 100644
--- a/frappe/patches/v13_0/website_theme_custom_scss.py
+++ b/frappe/patches/v13_0/website_theme_custom_scss.py
@@ -2,9 +2,23 @@ import frappe
def execute():
frappe.reload_doctype('Website Theme')
+ frappe.reload_doc('website', 'doctype', 'website_theme_ignore_app')
+ frappe.reload_doc('website', 'doctype', 'color')
+
for theme in frappe.get_all('Website Theme'):
doc = frappe.get_doc('Website Theme', theme.name)
if not doc.get('custom_scss') and doc.theme_scss:
# move old theme to new theme
doc.custom_scss = doc.theme_scss
+
+ if doc.background_color:
+ setup_color_record(doc.background_color)
+
doc.save()
+
+def setup_color_record(color):
+ frappe.get_doc({
+ "doctype": "Color",
+ "__newname": color,
+ "color": color,
+ }).save()
diff --git a/frappe/permissions.py b/frappe/permissions.py
index 0d766aec8d..a45fbdcd06 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -7,7 +7,7 @@ import frappe, copy, json
from frappe import _, msgprint
from frappe.utils import cint
import frappe.share
-rights = ("read", "write", "create", "delete", "submit", "cancel", "amend",
+rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share")
# TODO:
@@ -73,6 +73,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra
role_permissions = get_role_permissions(meta, user=user)
perm = role_permissions.get(ptype)
+
if not perm:
push_perm_check_log(_('User {0} does not have doctype access via role permission for document {1}').format(frappe.bold(user), frappe.bold(doctype)))
@@ -192,9 +193,9 @@ def get_role_permissions(doctype_meta, user=None):
and ptype != 'create'):
perms['if_owner'][ptype] = 1
# has no access if not owner
- # only provide read access so that user is able to at-least access list
+ # only provide select or read access so that user is able to at-least access list
# (and the documents will be filtered based on owner sin further checks)
- perms[ptype] = 1 if ptype == 'read' else 0
+ perms[ptype] = 1 if ptype in ['select', 'read'] else 0
frappe.local.role_permissions[cache_key] = perms
diff --git a/frappe/public/build.json b/frappe/public/build.json
index 54ed37b307..18b84e08ad 100755
--- a/frappe/public/build.json
+++ b/frappe/public/build.json
@@ -261,6 +261,7 @@
"public/js/frappe/views/calendar/calendar.js",
"public/js/frappe/views/dashboard/dashboard_view.js",
"public/js/frappe/views/image/image_view.js",
+ "public/js/frappe/views/map/map_view.js",
"public/js/frappe/views/kanban/kanban_view.js",
"public/js/frappe/views/inbox/inbox_view.js",
"public/js/frappe/views/file/file_view.js",
diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css
index 5ae77c73ca..88ad147d33 100644
--- a/frappe/public/css/list.css
+++ b/frappe/public/css/list.css
@@ -401,6 +401,13 @@ input.list-row-checkbox {
.pswp__more-item img {
max-height: 100%;
}
+.map-view-container {
+ display: flex;
+ flex-wrap: wrap;
+ width: 100%;
+ height: calc(100vh - 284px);
+ z-index: 0;
+}
.list-paging-area .gantt-view-mode {
margin-left: 15px;
margin-right: 15px;
diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js
index 319aa067cc..d7f873bee0 100644
--- a/frappe/public/js/frappe/form/controls/base_control.js
+++ b/frappe/public/js/frappe/form/controls/base_control.js
@@ -40,23 +40,31 @@ frappe.ui.form.Control = Class.extend({
return this.df.get_status(this);
}
- if((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form') {
+ if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) {
// like in case of a dialog box
if (cint(this.df.hidden)) {
// eslint-disable-next-line
- if(explain) console.log("By Hidden: None");
+ if (explain) console.log("By Hidden: None"); // eslint-disable-line no-console
return "None";
} else if (cint(this.df.hidden_due_to_dependency)) {
// eslint-disable-next-line
- if(explain) console.log("By Hidden Dependency: None");
+ if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console
return "None";
} else if (cint(this.df.read_only)) {
// eslint-disable-next-line
- if(explain) console.log("By Read Only: Read");
+ if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console
return "Read";
+ } else if ((this.grid &&
+ this.grid.display_status == 'Read') ||
+ (this.layout &&
+ this.layout.grid &&
+ this.layout.grid.display_status == 'Read')) {
+ // parent grid is read
+ if (explain) console.log("By Parent Grid Read-only: Read"); // eslint-disable-line no-console
+ return "Read";
}
return "Write";
@@ -65,13 +73,22 @@ frappe.ui.form.Control = Class.extend({
var status = frappe.perm.get_field_display_status(this.df,
frappe.model.get_doc(this.doctype, this.docname), this.perm || (this.frm && this.frm.perm), explain);
+ // Match parent grid controls read only status
+ if (status === 'Write' && (this.grid || (this.layout && this.layout.grid))) {
+ var grid = this.grid || this.layout.grid;
+ if (grid.display_status == 'Read') {
+ status = 'Read';
+ if (explain) console.log("By Parent Grid Read-only: Read"); // eslint-disable-line no-console
+ }
+ }
+
// hide if no value
if (this.doctype && status==="Read" && !this.only_input
&& is_null(frappe.model.get_value(this.doctype, this.docname, this.df.fieldname))
&& !in_list(["HTML", "Image", "Button"], this.df.fieldtype)) {
// eslint-disable-next-line
- if(explain) console.log("By Hide Read-only, null fields: None");
+ if (explain) console.log("By Hide Read-only, null fields: None"); // eslint-disable-line no-console
status = "None";
}
diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js
index 471825f193..3c5faf4a9a 100644
--- a/frappe/public/js/frappe/form/controls/base_input.js
+++ b/frappe/public/js/frappe/form/controls/base_input.js
@@ -20,7 +20,7 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
\
\
').appendTo(this.parent);
diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js
index 9dfad09299..9e4d1d82ec 100644
--- a/frappe/public/js/frappe/form/controls/geolocation.js
+++ b/frappe/public/js/frappe/form/controls/geolocation.js
@@ -1,3 +1,5 @@
+frappe.provide('frappe.utils.utils');
+
frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
horizontal: false,
@@ -90,11 +92,11 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
});
L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/';
- this.map = L.map(this.map_id).setView([19.0800, 72.8961], 13);
+ this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center,
+ frappe.utils.map_defaults.zoom);
- L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
- attribution: '© OpenStreetMap contributors'
- }).addTo(this.map);
+ L.tileLayer(frappe.utils.map_defaults.tiles,
+ frappe.utils.map_defaults.options).addTo(this.map);
},
bind_leaflet_locate_control() {
diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js
index 713ffe5f92..97a7d867b9 100644
--- a/frappe/public/js/frappe/form/controls/link.js
+++ b/frappe/public/js/frappe/form/controls/link.js
@@ -51,6 +51,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
this.translate_values = true;
this.setup_buttons();
this.setup_awesomeplete();
+ this.bind_change_event();
},
get_options: function() {
return this.df.options;
@@ -217,6 +218,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
}
me.$input.cache[doctype][term] = r.results;
me.awesomplete.list = me.$input.cache[doctype][term];
+ me.toggle_href(doctype);
}
});
}, 500));
@@ -303,6 +305,15 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
// returns [{value: 'Manufacturer 1', 'description': 'mobile part 1, mobile part 2'}]
},
+ toggle_href(doctype) {
+ if (frappe.model.can_select(doctype) && !frappe.model.can_read(doctype)) {
+ // remove href from link field as user has only select perm
+ this.$input_area.find(".link-btn").addClass('hide');
+ } else {
+ this.$input_area.find(".link-btn").removeClass('hide');
+ }
+ },
+
get_filter_description(filters) {
let doctype = this.get_options();
let filter_array = [];
diff --git a/frappe/public/js/frappe/form/controls/rating.js b/frappe/public/js/frappe/form/controls/rating.js
index 34e890d45c..191db35538 100644
--- a/frappe/public/js/frappe/form/controls/rating.js
+++ b/frappe/public/js/frappe/form/controls/rating.js
@@ -47,7 +47,7 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({
});
},
get_value() {
- return cint(this.value);
+ return cint(this.value, null);
},
set_formatted_input(value) {
let el = $(this.input_area).find('i');
diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js
index 5c4aed250b..bde08e4cee 100644
--- a/frappe/public/js/frappe/form/controls/table.js
+++ b/frappe/public/js/frappe/form/controls/table.js
@@ -9,7 +9,8 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({
frm: this.frm,
df: this.df,
perm: this.perm || (this.frm && this.frm.perm) || this.df.perm,
- parent: this.wrapper
+ parent: this.wrapper,
+ control: this
});
if(this.frm) {
this.frm.grids[this.frm.grids.length] = this;
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 20999deea7..916470f023 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -1264,7 +1264,10 @@ frappe.ui.form.Form = class FrappeForm {
}
if (df && df[property] != value) {
df[property] = value;
- this.refresh_field(fieldname);
+ if (!docname || !table_field) {
+ // do not refresh childtable fields since `this.fields_dict` doesn't have child table fields
+ this.refresh_field(fieldname);
+ }
}
}
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index 27ddaa0712..51e10b76b6 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -128,11 +128,15 @@ frappe.form.formatters = {
return repl('%(value)s',
{onclick: docfield.link_onclick.replace(/"/g, '"'), value:value});
} else if(docfield && doctype) {
- return `
- ${__(options && options.label || value)}`
+ if (!frappe.model.can_select(doctype) && frappe.model.can_read(doctype)) {
+ return `
+ ${__(options && options.label || value)}`
+ } else {
+ return value;
+ }
} else {
return value;
}
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index ad35a96013..921ad1cdb5 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -280,6 +280,8 @@ export default class Grid {
if (this.frm) {
this.display_status = frappe.perm.get_field_display_status(this.df, this.frm.doc,
this.perm);
+ } else if (this.df.is_web_form && this.control) {
+ this.display_status = this.control.get_status();
} else {
// not in form
this.display_status = 'Write';
diff --git a/frappe/public/js/frappe/form/grid_row_form.js b/frappe/public/js/frappe/form/grid_row_form.js
index ee6735d3ac..399f233c54 100644
--- a/frappe/public/js/frappe/form/grid_row_form.js
+++ b/frappe/public/js/frappe/form/grid_row_form.js
@@ -16,6 +16,9 @@ export default class GridRowForm {
body: this.form_area,
no_submit_on_enter: true,
frm: this.row.frm,
+ grid: this.row.grid,
+ grid_row: this.row,
+ grid_row_form: this,
});
this.layout.make();
diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js
index 7bf9dd7e39..09df97db43 100644
--- a/frappe/public/js/frappe/form/layout.js
+++ b/frappe/public/js/frappe/form/layout.js
@@ -1,7 +1,7 @@
import '../class';
frappe.ui.form.Layout = Class.extend({
- init: function(opts) {
+ init: function (opts) {
this.views = {};
this.pages = [];
this.sections = [];
@@ -87,7 +87,7 @@ frappe.ui.form.Layout = Class.extend({
this.message.empty().addClass('hidden');
}
},
- render: function(new_fields) {
+ render: function (new_fields) {
var me = this;
var fields = new_fields || this.fields;
@@ -101,8 +101,8 @@ frappe.ui.form.Layout = Class.extend({
if (this.no_opening_section()) {
this.make_section();
}
- $.each(fields, function(i, df) {
- switch(df.fieldtype) {
+ $.each(fields, function (i, df) {
+ switch (df.fieldtype) {
case "Fold":
me.make_page(df);
break;
@@ -119,17 +119,17 @@ frappe.ui.form.Layout = Class.extend({
},
- no_opening_section: function() {
- return (this.fields[0] && this.fields[0].fieldtype!="Section Break") || !this.fields.length;
+ no_opening_section: function () {
+ return (this.fields[0] && this.fields[0].fieldtype != "Section Break") || !this.fields.length;
},
- setup_dashboard_section: function() {
+ setup_dashboard_section: function () {
if (this.no_opening_section()) {
this.fields.unshift({fieldtype: 'Section Break'});
}
},
- replace_field: function(fieldname, df, render) {
+ replace_field: function (fieldname, df, render) {
df.fieldname = fieldname; // change of fieldname is avoided
if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) {
const fieldobj = this.init_field(df, render);
@@ -145,7 +145,7 @@ frappe.ui.form.Layout = Class.extend({
}
},
- make_field: function(df, colspan, render) {
+ make_field: function (df, colspan, render) {
!this.section && this.make_section();
!this.column && this.make_column();
@@ -161,29 +161,30 @@ frappe.ui.form.Layout = Class.extend({
fieldobj.section = this.section;
},
- init_field: function(df, render = false) {
+ init_field: function (df, render = false) {
const fieldobj = frappe.ui.form.make_control({
df: df,
doctype: this.doctype,
parent: this.column.wrapper.get(0),
frm: this.frm,
render_input: render,
- doc: this.doc
+ doc: this.doc,
+ layout: this
});
fieldobj.layout = this;
return fieldobj;
},
- make_page: function(df) {
+ make_page: function (df) { // eslint-disable-line no-unused-vars
var me = this,
head = $('').appendTo(this.wrapper);
this.page = $('').appendTo(this.wrapper);
- this.fold_btn = head.find(".btn-fold").on("click", function() {
+ this.fold_btn = head.find(".btn-fold").on("click", function () {
var page = $(this).parent().next();
if (page.hasClass("hide")) {
$(this).removeClass("btn-fold").html(__("Hide details"));
@@ -201,11 +202,11 @@ frappe.ui.form.Layout = Class.extend({
this.folded = true;
},
- unfold: function() {
+ unfold: function () {
this.fold_btn.trigger('click');
},
- make_section: function(df) {
+ make_section: function (df) {
this.section = new frappe.ui.form.Section(this, df);
// append to layout fields
@@ -217,14 +218,14 @@ frappe.ui.form.Layout = Class.extend({
this.column = null;
},
- make_column: function(df) {
+ make_column: function (df) {
this.column = new frappe.ui.form.Column(this.section, df);
if (df && df.fieldname) {
this.fields_list.push(this.column);
}
},
- refresh: function(doc) {
+ refresh: function (doc) {
var me = this;
if (doc) this.doc = doc;
@@ -267,7 +268,7 @@ frappe.ui.form.Layout = Class.extend({
},
- refresh_fields: function(fields) {
+ refresh_fields: function (fields) {
let fieldnames = fields.map((field) => {
if (field.fieldname) return field.fieldname;
});
@@ -282,15 +283,15 @@ frappe.ui.form.Layout = Class.extend({
});
},
- add_fields: function(fields) {
+ add_fields: function (fields) {
this.render(fields);
this.refresh_fields(fields);
},
- refresh_section_collapse: function() {
+ refresh_section_collapse: function () {
if (!this.doc) return;
- for (var i=0; i=0;i--) {
+ for (var i = me.fields_list.length - 1; i >= 0; i--) {
var f = me.fields_list[i];
f.guardian_has_value = true;
if (f.df.depends_on) {
@@ -498,14 +499,14 @@ frappe.ui.form.Layout = Class.extend({
this.refresh_section_count();
},
- set_dependant_property: function(condition, fieldname, property) {
+ set_dependant_property: function (condition, fieldname, property) {
let set_property = this.evaluate_depends_on_value(condition);
let value = set_property ? 1 : 0;
let form_obj;
if (this.frm) {
form_obj = this.frm;
- } else if (this.is_dialog) {
+ } else if (this.is_dialog || this.doctype === 'Web Form') {
form_obj = this;
}
if (form_obj) {
@@ -513,12 +514,14 @@ frappe.ui.form.Layout = Class.extend({
form_obj.setting_dependency = true;
form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname);
form_obj.setting_dependency = false;
+ // refresh child fields
+ this.fields_dict[fieldname] && this.fields_dict[fieldname].refresh();
} else {
form_obj.set_df_property(fieldname, property, value);
}
}
},
- evaluate_depends_on_value: function(expression) {
+ evaluate_depends_on_value: function (expression) {
var out = null;
var doc = this.doc;
@@ -544,7 +547,7 @@ frappe.ui.form.Layout = Class.extend({
if (parent && parent.istable && expression.includes('is_submittable')) {
out = true;
}
- } catch(e) {
+ } catch (e) {
frappe.throw(__('Invalid "depends_on" expression'));
}
@@ -640,7 +643,7 @@ frappe.ui.form.Section = Class.extend({
this.wrapper.toggleClass("hide-control", !!hide);
},
- collapse: function(hide) {
+ collapse: function (hide) {
// unknown edge case
if (!(this.head && this.body)) {
return;
@@ -659,7 +662,7 @@ frappe.ui.form.Section = Class.extend({
// refresh signature fields
this.fields_list.forEach((f) => {
- if (f.df.fieldtype=='Signature') {
+ if (f.df.fieldtype == 'Signature') {
f.refresh();
}
});
@@ -669,11 +672,11 @@ frappe.ui.form.Section = Class.extend({
return this.body.hasClass('hide');
},
- has_missing_mandatory: function() {
+ has_missing_mandatory: function () {
var missing_mandatory = false;
- for (var j=0, l=this.fields_list.length; j < l; j++) {
+ for (var j = 0, l = this.fields_list.length; j < l; j++) {
var section_df = this.fields_list[j].df;
- if (section_df.reqd && this.layout.doc[section_df.fieldname]==null) {
+ if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) {
missing_mandatory = true;
break;
}
@@ -691,13 +694,13 @@ frappe.ui.form.Column = Class.extend({
this.make();
this.resize_all_columns();
},
- make: function() {
+ make: function () {
this.wrapper = $('\
\
').appendTo(this.section.body)
.find("form")
- .on("submit", function() {
+ .on("submit", function () {
return false;
});
@@ -706,7 +709,7 @@ frappe.ui.form.Column = Class.extend({
+ '').appendTo(this.wrapper);
}
},
- resize_all_columns: function() {
+ resize_all_columns: function () {
// distribute all columns equally
var colspan = cint(12 / this.section.wrapper.find(".form-column").length);
@@ -715,7 +718,7 @@ frappe.ui.form.Column = Class.extend({
.addClass("col-sm-" + colspan);
},
- refresh: function() {
+ refresh: function () {
this.section.refresh();
}
});
diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js
index af3223cc9d..a8dd503f43 100644
--- a/frappe/public/js/frappe/form/toolbar.js
+++ b/frappe/public/js/frappe/form/toolbar.js
@@ -226,7 +226,7 @@ frappe.ui.form.Toolbar = class Toolbar {
this.page.add_action_icon("right", ()=> {
this.frm.navigate_records(0);
}, 'next-doc', __("Next"));
- }
+ }
}
make_menu_items() {
@@ -470,9 +470,22 @@ frappe.ui.form.Toolbar = class Toolbar {
me.frm.page.set_view('main');
}, 'edit');
} else if(status === "Cancel") {
- this.page.set_secondary_action(__(status), function() {
- me.frm.savecancel(this);
- });
+ let add_cancel_button = () => {
+ this.page.set_secondary_action(__(status), function() {
+ me.frm.savecancel(this);
+ });
+ };
+ if (this.has_workflow()) {
+ frappe.xcall('frappe.model.workflow.can_cancel_document', {
+ 'doctype': this.frm.doc.doctype,
+ }).then((can_cancel) => {
+ if (can_cancel) {
+ add_cancel_button();
+ }
+ });
+ } else {
+ add_cancel_button();
+ }
} else {
var click = {
"Save": function() {
diff --git a/frappe/public/js/frappe/form/workflow.js b/frappe/public/js/frappe/form/workflow.js
index 4c59e8219b..16d9f8676b 100644
--- a/frappe/public/js/frappe/form/workflow.js
+++ b/frappe/public/js/frappe/form/workflow.js
@@ -85,7 +85,7 @@ frappe.ui.form.States = Class.extend({
frappe.workflow.get_transitions(this.frm.doc).then(transitions => {
this.frm.page.clear_actions_menu();
transitions.forEach(d => {
- if(frappe.user_roles.includes(d.allowed) && has_approval_access(d)) {
+ if (frappe.user_roles.includes(d.allowed) && has_approval_access(d)) {
added = true;
me.frm.page.add_action_item(__(d.action), function() {
// set the workflow_action for use in form scripts
@@ -103,17 +103,8 @@ frappe.ui.form.States = Class.extend({
});
}
});
- if (!added) {
- //call function and clear cancel button if Cancel doc state is defined in the workfloe
- frappe.xcall('frappe.model.workflow.can_cancel_document', {doc: this.frm.doc}).then((can_cancel) => {
- if (!can_cancel) {
- this.frm.page.clear_secondary_action();
- }
- });
- } else {
- this.setup_btn(added);
- }
+ this.setup_btn(added);
});
},
diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js
index 68a462e53e..5b92119807 100644
--- a/frappe/public/js/frappe/list/list_sidebar.js
+++ b/frappe/public/js/frappe/list/list_sidebar.js
@@ -39,13 +39,105 @@ frappe.views.ListSidebar = class ListSidebar {
}
- setup_list_group_by() {
- this.list_group_by = new frappe.views.ListGroupBy({
- doctype: this.doctype,
- sidebar: this,
- list_view: this.list_view,
- page: this.page
- });
+ setup_views() {
+ var show_list_link = false;
+
+ if (frappe.views.calendar[this.doctype]) {
+ this.sidebar.find('.list-link[data-view="Calendar"]').removeClass("hide");
+ this.sidebar.find('.list-link[data-view="Gantt"]').removeClass('hide');
+ show_list_link = true;
+ }
+ //show link for kanban view
+ this.sidebar.find('.list-link[data-view="Kanban"]').removeClass('hide');
+ if (this.doctype === "Communication" && frappe.boot.email_accounts.length) {
+ this.sidebar.find('.list-link[data-view="Inbox"]').removeClass('hide');
+ show_list_link = true;
+ }
+
+ if (frappe.treeview_settings[this.doctype] || frappe.get_meta(this.doctype).is_tree) {
+ this.sidebar.find(".tree-link").removeClass("hide");
+ }
+
+ this.current_view = 'List';
+ var route = frappe.get_route();
+ if (route.length > 2 && frappe.views.view_modes.includes(route[2])) {
+ this.current_view = route[2];
+
+ if (this.current_view === 'Kanban') {
+ this.kanban_board = route[3];
+ } else if (this.current_view === 'Inbox') {
+ this.email_account = route[3];
+ }
+ }
+
+ // disable link for current view
+ this.sidebar.find('.list-link[data-view="' + this.current_view + '"] a')
+ .attr('disabled', 'disabled').addClass('disabled');
+
+ //enable link for Kanban view
+ this.sidebar.find('.list-link[data-view="Kanban"] a, .list-link[data-view="Inbox"] a')
+ .attr('disabled', null).removeClass('disabled');
+
+ // show image link if image_view
+ if (this.list_view.meta.image_field) {
+ this.sidebar.find('.list-link[data-view="Image"]').removeClass('hide');
+ show_list_link = true;
+ }
+
+ if (this.list_view.settings.get_coords_method ||
+ (this.list_view.meta.fields.find(i => i.fieldname === "latitude") &&
+ this.list_view.meta.fields.find(i => i.fieldname === "longitude")) ||
+ (this.list_view.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype == 'Geolocation'))) {
+ this.sidebar.find('.list-link[data-view="Map"]').removeClass('hide');
+ show_list_link = true;
+ }
+
+ if (show_list_link) {
+ this.sidebar.find('.list-link[data-view="List"]').removeClass('hide');
+ }
+ }
+
+ setup_reports() {
+ // add reports linked to this doctype to the dropdown
+ var me = this;
+ var added = [];
+ var dropdown = this.page.sidebar.find('.reports-dropdown');
+ var divider = false;
+
+ var add_reports = function(reports) {
+ $.each(reports, function(name, r) {
+ if (!r.ref_doctype || r.ref_doctype == me.doctype) {
+ var report_type = r.report_type === 'Report Builder' ?
+ `List/${r.ref_doctype}/Report` : 'query-report';
+
+ var route = r.route || report_type + '/' + (r.title || r.name);
+
+ if (added.indexOf(route) === -1) {
+ // don't repeat
+ added.push(route);
+
+ if (!divider) {
+ me.get_divider().appendTo(dropdown);
+ divider = true;
+ }
+
+ $('' +
+ __(r.title || r.name) + '').appendTo(dropdown);
+ }
+ }
+ });
+ };
+
+ // from reference doctype
+ if (this.list_view.settings.reports) {
+ add_reports(this.list_view.settings.reports);
+ }
+
+ // Sort reports alphabetically
+ var reports = Object.values(frappe.boot.user.all_reports).sort((a,b) => a.title.localeCompare(b.title)) || [];
+
+ // from specially tagged reports
+ add_reports(reports);
}
setup_list_filter() {
@@ -56,6 +148,29 @@ frappe.views.ListSidebar = class ListSidebar {
});
}
+ setup_kanban_boards() {
+ const $dropdown = this.page.sidebar.find('.kanban-dropdown');
+ frappe.views.KanbanView.setup_dropdown_in_sidebar(this.doctype, $dropdown);
+ }
+
+
+ setup_keyboard_shortcuts() {
+ this.sidebar.find('.list-link > a, .list-link > .btn-group > a').each((i, el) => {
+ frappe.ui.keys
+ .get_shortcut_group(this.page)
+ .add($(el));
+ });
+ }
+
+ setup_list_group_by() {
+ this.list_group_by = new frappe.views.ListGroupBy({
+ doctype: this.doctype,
+ sidebar: this,
+ list_view: this.list_view,
+ page: this.page
+ });
+ }
+
get_stats() {
var me = this;
frappe.call({
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index 630081fd80..a2e872085e 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -135,8 +135,8 @@ $.extend(frappe.model, {
let cached_timestamp = null;
let cached_doc = null;
- let cached_docs = frappe.model.get_from_localstorage(doctype)
-
+ let cached_docs = frappe.model.get_from_localstorage(doctype);
+
if (cached_docs) {
cached_doc = cached_docs.filter(doc => doc.name === doctype)[0];
if(cached_doc) {
@@ -252,6 +252,10 @@ $.extend(frappe.model, {
return frappe.boot.user.can_create.indexOf(doctype)!==-1;
},
+ can_select: function(doctype) {
+ return frappe.boot.user.can_select.indexOf(doctype)!==-1;
+ },
+
can_read: function(doctype) {
return frappe.boot.user.can_read.indexOf(doctype)!==-1;
},
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js
index ae12bbe16c..ef70a6195f 100644
--- a/frappe/public/js/frappe/ui/filters/filter.js
+++ b/frappe/public/js/frappe/ui/filters/filter.js
@@ -527,7 +527,7 @@ frappe.ui.filter_utils = {
['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)
) {
df.fieldtype = 'Select';
- df.options = this.get_timespan_options(['Last', 'Today', 'This', 'Next']);
+ df.options = this.get_timespan_options(['Last', 'Yesterday', 'Today', 'Tomorrow', 'This', 'Next']);
}
if (condition === 'is') {
df.fieldtype = 'Select';
@@ -542,7 +542,6 @@ frappe.ui.filter_utils = {
get_timespan_options(periods) {
const period_map = {
Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
- Today: null,
This: ['Week', 'Month', 'Quarter', 'Year'],
Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
};
diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js
index f0a174ef37..0f9adcad97 100644
--- a/frappe/public/js/frappe/utils/common.js
+++ b/frappe/public/js/frappe/utils/common.js
@@ -176,7 +176,7 @@ window.replace_all = function (s, t1, t2) {
return s.split(t1).join(t2);
}
-window.strip_html = function (txt) {
+window.strip_html = function(txt) {
return cstr(txt).replace(/<[^>]*>/g, "");
}
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index 074832da4f..b678d3e140 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -1220,6 +1220,7 @@ Object.assign(frappe.utils, {
if (Math.floor(number) === number) return 0;
return number.toString().split(".")[1].length || 0;
},
+
build_summary_item(summary) {
if (summary.type == "separator") {
return $(`
@@ -1242,6 +1243,7 @@ Object.assign(frappe.utils, {
${value}
`);
},
+
get_names_for_mentions() {
let names_for_mentions = Object.keys(frappe.boot.user_info || [])
.filter(user => {
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index 541fb3209b..07e657b83c 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -112,7 +112,6 @@ frappe.views.CommunicationComposer = Class.extend({
{
label: __("Message"),
fieldtype: "Text Editor",
- reqd: 1,
fieldname: "content",
onchange: frappe.utils.debounce(
this.save_as_draft.bind(this),
@@ -124,7 +123,7 @@ frappe.views.CommunicationComposer = Class.extend({
label: __("Send me a copy"),
fieldtype: "Check",
fieldname: "send_me_a_copy",
- default: 1
+ default: 1 // frappe.boot.user.send_me_a_copy
},
{
label: __("Send Read Receipt"),
diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js
new file mode 100644
index 0000000000..878311b9bd
--- /dev/null
+++ b/frappe/public/js/frappe/views/map/map_view.js
@@ -0,0 +1,85 @@
+/**
+ * frappe.views.MapView
+ */
+frappe.provide('frappe.utils.utils');
+frappe.provide("frappe.views");
+
+frappe.views.MapView = class MapView extends frappe.views.ListView {
+ get view_name() {
+ return 'Map';
+ }
+
+ setup_defaults() {
+ super.setup_defaults();
+ this.page_title = __('{0} Map', [this.page_title]);
+ }
+
+ setup_view() {
+ }
+
+ on_filter_change() {
+ this.get_coords();
+ }
+
+ render() {
+ this.get_coords()
+ .then(() => {
+ this.render_map_view();
+ });
+ this.$paging_area.find('.level-left').append('');
+ }
+
+ render_map_view() {
+ this.map_id = frappe.dom.get_unique_id();
+
+ this.$result.html(``);
+
+ L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/';
+ this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center,
+ frappe.utils.map_defaults.zoom);
+
+ L.tileLayer(frappe.utils.map_defaults.tiles,
+ frappe.utils.map_defaults.options).addTo(this.map);
+
+ L.control.scale().addTo(this.map);
+ if (this.coords.features && this.coords.features.length) {
+ this.coords.features.forEach(
+ coords => L.geoJSON(coords).bindPopup(coords.properties.name).addTo(this.map)
+ );
+ let lastCoords = this.coords.features[0].geometry.coordinates.reverse();
+ this.map.panTo(lastCoords, 8);
+ }
+ }
+
+ get_coords() {
+ let get_coords_method = this.settings && this.settings.get_coords_method || 'frappe.geo.utils.get_coords';
+
+ if (cur_list.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype === 'Geolocation')) {
+ this.type = 'location_field';
+ } else if (cur_list.meta.fields.find(i => i.fieldname === "latitude") &&
+ cur_list.meta.fields.find(i => i.fieldname === "longitude")) {
+ this.type = 'coordinates';
+ }
+ return frappe.call({
+ method: get_coords_method,
+ args: {
+ doctype: this.doctype,
+ filters: cur_list.filter_area.get(),
+ type: this.type
+ }
+ }).then(r => {
+ this.coords = r.message;
+
+ });
+ }
+
+
+ get required_libs() {
+ return [
+ "assets/frappe/js/lib/leaflet/leaflet.css",
+ "assets/frappe/js/lib/leaflet/leaflet.js"
+ ];
+ }
+
+
+};
diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js
index cdf5f5602b..6da03ca7c8 100644
--- a/frappe/public/js/frappe/views/reports/report_view.js
+++ b/frappe/public/js/frappe/views/reports/report_view.js
@@ -708,6 +708,32 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
super.build_fields();
}
+ reorder_fields() {
+ // generate table fields in the required format ["name", "DocType"]
+ // these are fields in the column before adding new fields
+ let table_fields = this.columns.map(df => [df.field, df.docfield.parent]);
+
+ // filter fields that are already in table
+ // iterate over table_fields to preserve the existing order of fields
+ // The filter will ensure the unchecked fields are removed
+ let fields_already_in_table = table_fields.filter(df => {
+ return this.fields.find((field) => {
+ return df[0] == field[0] && df[1] == field[1]
+ })
+ })
+
+ // find new fields that didn't already exists
+ // This will be appended to the end of the table
+ let fields_to_add = this.fields.filter(df => {
+ return !table_fields.find((field) => {
+ return df[0] == field[0] && df[1] == field[1]
+ })
+ })
+
+ // rebuild fields
+ this.fields = [...fields_already_in_table, ...fields_to_add];
+ }
+
get_fields() {
let fields = this.fields.map(f => {
let column_name = frappe.model.get_full_column_name(f[0], f[1]);
@@ -1329,6 +1355,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
this.fields.map(f => this.add_currency_column(f[0], f[1]));
+ this.reorder_fields();
this.build_fields();
this.setup_columns();
diff --git a/frappe/public/js/frappe/views/treeview.js b/frappe/public/js/frappe/views/treeview.js
index 38561730b7..ff9c2a05c2 100644
--- a/frappe/public/js/frappe/views/treeview.js
+++ b/frappe/public/js/frappe/views/treeview.js
@@ -94,17 +94,17 @@ frappe.views.TreeView = Class.extend({
var me = this;
this.opts.onload && this.opts.onload(me);
},
- make_filters: function(){
+ make_filters: function() {
var me = this;
frappe.treeview_settings.filters = []
$.each(this.opts.filters || [], function(i, filter) {
- if(frappe.route_options && frappe.route_options[filter.fieldname]) {
- filter.default = frappe.route_options[filter.fieldname]
+ if (frappe.route_options && frappe.route_options[filter.fieldname]) {
+ filter.default = frappe.route_options[filter.fieldname];
}
- if(!filter.disable_onchange) {
+ if (!filter.disable_onchange) {
filter.change = function() {
- filter.on_change && filter.on_change();
+ filter.onchange && filter.onchange();
var val = this.get_value();
me.args[filter.fieldname] = val;
if (val) {
@@ -114,7 +114,7 @@ frappe.views.TreeView = Class.extend({
}
me.set_title();
me.make_tree();
- }
+ };
}
me.page.add_field(filter);
@@ -122,7 +122,7 @@ frappe.views.TreeView = Class.extend({
if (filter.default) {
$("[data-fieldname='"+filter.fieldname+"']").trigger("change");
}
- })
+ });
},
get_root: function() {
var me = this;
diff --git a/frappe/public/js/frappe/web_form/webform_script.js b/frappe/public/js/frappe/web_form/webform_script.js
index c3211de99f..6df526e7ac 100644
--- a/frappe/public/js/frappe/web_form/webform_script.js
+++ b/frappe/public/js/frappe/web_form/webform_script.js
@@ -85,6 +85,7 @@ frappe.ready(function() {
function setup_fields(form_data) {
form_data.web_form.web_form_fields.map(df => {
+ df.is_web_form = true;
if (df.fieldtype === "Table") {
df.get_data = () => {
let data = [];
@@ -99,14 +100,13 @@ frappe.ready(function() {
if (field.fieldtype === "Link") {
field.only_select = true;
}
+ field.is_web_form = true;
});
if (df.fieldtype === "Attach") {
df.is_private = true;
}
- df.is_web_form = true;
-
delete df.parent;
delete df.parentfield;
delete df.parenttype;
diff --git a/frappe/public/scss/website/page_builder.scss b/frappe/public/scss/website/page_builder.scss
index 24dbca3e21..1803e52cf7 100644
--- a/frappe/public/scss/website/page_builder.scss
+++ b/frappe/public/scss/website/page_builder.scss
@@ -29,11 +29,11 @@
}
.hero.align-center {
- h1, .hero-subtitle, .hero-buttons {
+ h1, .hero-title, .hero-subtitle, .hero-buttons {
text-align: center;
}
- .hero-subtitle {
+ .hero-title, .hero-subtitle {
margin-left: auto;
margin-right: auto;
}
diff --git a/frappe/templates/includes/breadcrumbs.html b/frappe/templates/includes/breadcrumbs.html
index e281c4b111..ccc77de253 100644
--- a/frappe/templates/includes/breadcrumbs.html
+++ b/frappe/templates/includes/breadcrumbs.html
@@ -3,6 +3,7 @@
diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html
index 11f514396f..fff3024490 100644
--- a/frappe/templates/print_formats/standard_macros.html
+++ b/frappe/templates/print_formats/standard_macros.html
@@ -5,8 +5,11 @@
{{ frappe.render_template(df.options, {"doc": doc}) or "" }}
{%- elif df.fieldtype in ("Text", "Text Editor", "Code", "Long Text") -%}
{{ render_text_field(df, doc) }}
- {%- elif df.fieldtype in ("Image", "Attach Image", "Attach")
- and (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") -%}
+ {%- elif df.fieldtype in ("Image", "Attach Image")
+ and (
+ (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/")
+ or doc[df.fieldname].startswith("http")
+ ) -%}
{{ render_image(df, doc) }}
{%- elif df.fieldtype=="Geolocation" -%}
{{ render_geolocation(df, doc) }}
@@ -137,15 +140,14 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
style="width: 12px; height: 12px; margin-top: 5px;">
- {% elif df.fieldtype=="Image" %}
+ {% elif df.fieldtype in ("Image", "Attach Image") %}
{% elif df.fieldtype=="Signature" %}
- {% elif df.fieldtype in ("Attach", "Attach Image") and doc[df.fieldname]
- and frappe.utils.is_image(doc[df.fieldname]) %}
+ {% elif df.fieldtype == "Attach" and doc[df.fieldname] and frappe.utils.is_image(doc[df.fieldname]) %}
{% elif df.fieldtype=="HTML" %}
diff --git a/frappe/tests/test_cors.py b/frappe/tests/test_cors.py
new file mode 100644
index 0000000000..d4ed260f61
--- /dev/null
+++ b/frappe/tests/test_cors.py
@@ -0,0 +1,57 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+from __future__ import unicode_literals
+
+import frappe, unittest
+from werkzeug.wrappers import Response
+from frappe.app import process_response
+
+HEADERS = ('Access-Control-Allow-Origin', 'Access-Control-Allow-Credentials',
+ 'Access-Control-Allow-Methods', 'Access-Control-Allow-Headers')
+
+class TestCORS(unittest.TestCase):
+ def make_request_and_test(self, origin='http://example.com', absent=False):
+ self.origin = origin
+
+ headers = {}
+ if origin:
+ headers = {'Origin': origin}
+
+ frappe.utils.set_request(headers=headers)
+
+ self.response = Response()
+ process_response(self.response)
+
+ for header in HEADERS:
+ if absent:
+ self.assertNotIn(header, self.response.headers)
+ else:
+ if header == 'Access-Control-Allow-Origin':
+ self.assertEqual(self.response.headers.get(header), self.origin)
+ else:
+ self.assertIn(header, self.response.headers)
+
+ def test_cors_disabled(self):
+ frappe.conf.allow_cors = None
+ self.make_request_and_test('http://example.com', True)
+
+ def test_request_without_origin(self):
+ frappe.conf.allow_cors = 'http://example.com'
+ self.make_request_and_test(None, True)
+
+ def test_valid_origin(self):
+ frappe.conf.allow_cors = 'http://example.com'
+ self.make_request_and_test()
+
+ frappe.conf.allow_cors = "*"
+ self.make_request_and_test()
+
+ frappe.conf.allow_cors = ['http://example.com', 'https://example.com']
+ self.make_request_and_test()
+
+ def test_invalid_origin(self):
+ frappe.conf.allow_cors = 'http://example1.com'
+ self.make_request_and_test(absent=True)
+
+ frappe.conf.allow_cors = ['http://example1.com', 'https://example.com']
+ self.make_request_and_test(absent=True)
diff --git a/frappe/tests/test_hooks.py b/frappe/tests/test_hooks.py
index f19904c8fc..ff71e2414c 100644
--- a/frappe/tests/test_hooks.py
+++ b/frappe/tests/test_hooks.py
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.desk.doctype.todo.todo import ToDo
+from frappe.cache_manager import clear_controller_cache
class TestHooks(unittest.TestCase):
def test_hooks(self):
@@ -17,21 +18,20 @@ class TestHooks(unittest.TestCase):
hooks.get("doc_events").get("*").get("on_update"))
def test_override_doctype_class(self):
- # mock get_hooks
- original = frappe.get_hooks
- def get_hooks(hook=None, default=None, app_name=None):
- if hook == 'override_doctype_class':
- return {
- 'ToDo': ['frappe.tests.test_hooks.CustomToDo']
- }
- return original(hook, default, app_name)
- frappe.get_hooks = get_hooks
+ from frappe import hooks
+
+ # Set hook
+ hooks.override_doctype_class = {
+ 'ToDo': ['frappe.tests.test_hooks.CustomToDo']
+ }
+
+ # Clear cache
+ frappe.cache().delete_value('app_hooks')
+ clear_controller_cache('ToDo')
todo = frappe.get_doc(doctype='ToDo', description='asdf')
self.assertTrue(isinstance(todo, CustomToDo))
- # restore
- frappe.get_hooks = original
class CustomToDo(ToDo):
pass
diff --git a/frappe/tests/test_permissions.py b/frappe/tests/test_permissions.py
index dddc790c94..6897d500c9 100644
--- a/frappe/tests/test_permissions.py
+++ b/frappe/tests/test_permissions.py
@@ -9,7 +9,7 @@ import frappe.defaults
import unittest
import frappe.model.meta
from frappe.permissions import (add_user_permission, remove_user_permission,
- clear_user_permissions_for_doctype, get_doc_permissions, add_permission)
+ clear_user_permissions_for_doctype, get_doc_permissions, add_permission, update_permission_property)
from frappe.core.page.permission_manager.permission_manager import update, reset
from frappe.test_runner import make_test_records_for_doctype
from frappe.core.doctype.user_permission.user_permission import clear_user_permissions
@@ -58,6 +58,24 @@ class TestPermissions(unittest.TestCase):
post = frappe.get_doc("Blog Post", "-test-blog-post")
self.assertTrue(post.has_permission("read"))
+ def test_select_permission(self):
+ # grant only select perm to blog post
+ add_permission('Blog Post', 'Sales User', 0)
+ update_permission_property('Blog Post', 'Sales User', 0, 'select', 1)
+ update_permission_property('Blog Post', 'Sales User', 0, 'read', 0)
+ update_permission_property('Blog Post', 'Sales User', 0, 'write', 0)
+
+ frappe.clear_cache(doctype="Blog Post")
+ frappe.set_user("test3@example.com")
+
+ # validate select perm
+ post = frappe.get_doc("Blog Post", "-test-blog-post")
+ self.assertTrue(post.has_permission("select"))
+
+ # validate does not have read and write perm
+ self.assertFalse(post.has_permission("read"))
+ self.assertRaises(frappe.PermissionError, post.save)
+
def test_user_permissions_in_doc(self):
add_user_permission("Blog Category", "-test-blog-category-1",
"test2@example.com")
diff --git a/frappe/tests/tests_geo_utils.py b/frappe/tests/tests_geo_utils.py
new file mode 100644
index 0000000000..2067a6aa97
--- /dev/null
+++ b/frappe/tests/tests_geo_utils.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import unittest
+
+import frappe
+from frappe.geo.utils import get_coords
+
+
+class TestGeoUtils(unittest.TestCase):
+ def setUp(self):
+ self.todo = frappe.get_doc(
+ dict(doctype='ToDo', description='Test description', assigned_by='Administrator')).insert()
+
+ self.test_location_dict = {'type': 'FeatureCollection', 'features': [
+ {'type': 'Feature', 'properties': {}, "geometry": {'type': 'Point', 'coordinates': [49.20433, 55.753395]}}]}
+ self.test_location = frappe.get_doc({'name': 'Test Location', 'doctype': 'Location',
+ 'location': str(self.test_location_dict)})
+
+ self.test_filter_exists = [['Location', 'name', 'like', '%Test Location%']]
+ self.test_filter_not_exists = [['Location', 'name', 'like', '%Test Location Not exists%']]
+ self.test_filter_todo = [['ToDo', 'description', 'like', '%Test description%']]
+
+ def test_get_coords_location_with_filter_exists(self):
+ coords = get_coords('Location', self.test_filter_exists, 'location_field')
+ self.assertEqual(self.test_location_dict['features'][0]['geometry'], coords['features'][0]['geometry'])
+
+ def test_get_coords_location_with_filter_not_exists(self):
+ coords = get_coords('Location', self.test_filter_not_exists, 'location_field')
+ self.assertEqual(coords, {'type': 'FeatureCollection', 'features': []})
+
+ def test_get_coords_from_not_existable_location(self):
+ self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'location_field')
+
+ def test_get_coords_from_not_existable_coords(self):
+ self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'coordinates')
+
+ def tearDown(self):
+ self.todo.delete()
diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py
index ef572c6971..54a5a24acf 100644
--- a/frappe/tests/ui_test_helpers.py
+++ b/frappe/tests/ui_test_helpers.py
@@ -95,6 +95,24 @@ def create_doctype(name, fields):
"name": name
}).insert()
+@frappe.whitelist()
+def create_child_doctype(name, fields):
+ fields = frappe.parse_json(fields)
+ if frappe.db.exists('DocType', name):
+ return
+ frappe.get_doc({
+ "doctype": "DocType",
+ "module": "Core",
+ "istable": 1,
+ "custom": 1,
+ "fields": fields,
+ "permissions": [{
+ "role": "System Manager",
+ "read": 1
+ }],
+ "name": name
+ }).insert()
+
@frappe.whitelist()
def create_contact_records():
if frappe.db.get_all('Contact', {'first_name': 'Test Form Contact 1'}):
diff --git a/frappe/translate.py b/frappe/translate.py
index 3685daf986..2cee8c34b5 100644
--- a/frappe/translate.py
+++ b/frappe/translate.py
@@ -190,7 +190,7 @@ def get_full_dict(lang):
frappe.local.lang_full_dict = load_lang(lang)
try:
- # get user specific transaltion data
+ # get user specific translation data
user_translations = get_user_translations(lang)
frappe.local.lang_full_dict.update(user_translations)
except Exception:
diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv
index f1d72c1443..5b45d8c217 100644
--- a/frappe/translations/de.csv
+++ b/frappe/translations/de.csv
@@ -1577,7 +1577,7 @@ Monospace,Monospace,
More articles on {0},Weitere Artikel zum {0},
More content for the bottom of the page.,Zusätzlicher Inhalt für den unteren Teil der Seite.,
Most Used,Am Meisten verwendet,
-Move To,Ziehen nach,
+Move To,Bewegen nach,
Move To Trash,In den Papierkorb verschieben,
Move to Row Number,Gehe zu Zeilennummer,
Mr,Hr.,
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index 9d2d98674d..a52b8ca5dd 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -445,25 +445,29 @@ def get_weekday(datetime=None):
return weekdays[datetime.weekday()]
def get_timespan_date_range(timespan):
+ today = nowdate()
date_range_map = {
- "last week": [add_to_date(nowdate(), days=-7), nowdate()],
- "last month": [add_to_date(nowdate(), months=-1), nowdate()],
- "last quarter": [add_to_date(nowdate(), months=-3), nowdate()],
- "last 6 months": [add_to_date(nowdate(), months=-6), nowdate()],
- "last year": [add_to_date(nowdate(), years=-1), nowdate()],
- "today": [nowdate(), nowdate()],
- "this week": [get_first_day_of_week(nowdate(), as_str=True), nowdate()],
- "this month": [get_first_day(nowdate(), as_str=True), nowdate()],
- "this quarter": [get_quarter_start(nowdate(), as_str=True), nowdate()],
- "this year": [get_year_start(nowdate(), as_str=True), nowdate()],
- "next week": [nowdate(), add_to_date(nowdate(), days=7)],
- "next month": [nowdate(), add_to_date(nowdate(), months=1)],
- "next quarter": [nowdate(), add_to_date(nowdate(), months=3)],
- "next 6 months": [nowdate(), add_to_date(nowdate(), months=6)],
- "next year": [nowdate(), add_to_date(nowdate(), years=1)],
+ "last week": lambda: (add_to_date(today, days=-7), today),
+ "last month": lambda: (add_to_date(today, months=-1), today),
+ "last quarter": lambda: (add_to_date(today, months=-3), today),
+ "last 6 months": lambda: (add_to_date(today, months=-6), today),
+ "last year": lambda: (add_to_date(today, years=-1), today),
+ "yesterday": lambda: (add_to_date(today, days=-1),) * 2,
+ "today": lambda: (today, today),
+ "tomorrow": lambda: (add_to_date(today, days=1),) * 2,
+ "this week": lambda: (get_first_day_of_week(today, as_str=True), today),
+ "this month": lambda: (get_first_day(today, as_str=True), today),
+ "this quarter": lambda: (get_quarter_start(today, as_str=True), today),
+ "this year": lambda: (get_year_start(today, as_str=True), today),
+ "next week": lambda: (today, add_to_date(today, days=7)),
+ "next month": lambda: (today, add_to_date(today, months=1)),
+ "next quarter": lambda: (today, add_to_date(today, months=3)),
+ "next 6 months": lambda: (today, add_to_date(today, months=6)),
+ "next year": lambda: (today, add_to_date(today, years=1)),
}
- return date_range_map.get(timespan)
+ if timespan in date_range_map:
+ return date_range_map[timespan]()
def global_date_format(date, format="long"):
"""returns localized date in the form of January 1, 2012"""
diff --git a/frappe/utils/user.py b/frappe/utils/user.py
index e032c6004b..61b698db9f 100755
--- a/frappe/utils/user.py
+++ b/frappe/utils/user.py
@@ -22,6 +22,7 @@ class UserPermissions:
self.all_read = []
self.can_create = []
+ self.can_select = []
self.can_read = []
self.can_write = []
self.can_cancel = []
@@ -104,6 +105,9 @@ class UserPermissions:
if not p.get("read") and (dt in user_shared):
p["read"] = 1
+ if p.get('select'):
+ self.can_select.append(dt)
+
if not dtp.get('istable'):
if p.get('create') and not dtp.get('issingle'):
if dtp.get('in_create'):
@@ -193,9 +197,8 @@ class UserPermissions:
d.name = self.name
d.roles = self.get_roles()
d.defaults = self.get_defaults()
-
- for key in ("can_create", "can_write", "can_read", "can_cancel", "can_delete",
- "can_get_report", "allow_modules", "all_read", "can_search",
+ for key in ("can_select", "can_create", "can_write", "can_read", "can_cancel",
+ "can_delete", "can_get_report", "allow_modules", "all_read", "can_search",
"in_create", "can_export", "can_import", "can_print", "can_email",
"can_set_user_permissions"):
d[key] = list(set(getattr(self, key)))
diff --git a/frappe/website/doctype/blog_post/templates/blog_post_row.html b/frappe/website/doctype/blog_post/templates/blog_post_row.html
index 7daf27adc8..53539c33e0 100644
--- a/frappe/website/doctype/blog_post/templates/blog_post_row.html
+++ b/frappe/website/doctype/blog_post/templates/blog_post_row.html
@@ -21,7 +21,7 @@
{%- if post.featured -%}
{{ post.title }}
{%- else -%}
- {{ post.title }}
+ {{ post.title }}
{%- endif -%}
{{ post.intro }}
@@ -38,4 +38,4 @@
-
\ No newline at end of file
+
diff --git a/frappe/website/js/bootstrap-4.js b/frappe/website/js/bootstrap-4.js
index dbe837b101..da720eedaf 100644
--- a/frappe/website/js/bootstrap-4.js
+++ b/frappe/website/js/bootstrap-4.js
@@ -18,7 +18,7 @@ $('.dropdown-menu a.dropdown-toggle').on('click', function (e) {
return false;
});
-frappe.get_modal = function(title, content) {
+frappe.get_modal = function (title, content) {
return $(
`
@@ -33,6 +33,10 @@ frappe.get_modal = function(title, content) {
${content}
diff --git a/frappe/website/web_template/testimonial/testimonial.html b/frappe/website/web_template/testimonial/testimonial.html
index b656d3b03d..f860abbae6 100644
--- a/frappe/website/web_template/testimonial/testimonial.html
+++ b/frappe/website/web_template/testimonial/testimonial.html
@@ -5,9 +5,7 @@
{% endif %}
- “
- {{ content }}
- ”
+ “{{ content }}”
{{ name }}
diff --git a/package.json b/package.json
index 3a8291b5a9..cd6ad6847a 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,7 @@
"redis": "^2.8.0",
"showdown": "^1.9.1",
"snyk": "^1.425.4",
- "socket.io": "^2.3.0",
+ "socket.io": "^2.4.0",
"superagent": "^3.8.2",
"touch": "^3.1.0",
"vue": "^2.6.11",
diff --git a/yarn.lock b/yarn.lock
index e79622b7d0..78b0c686fc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -693,13 +693,6 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2:
dependencies:
tweetnacl "^0.14.3"
-better-assert@~1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
- integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=
- dependencies:
- callsite "1.0.0"
-
big.js@^3.1.3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
@@ -940,11 +933,6 @@ caller-path@^2.0.0:
dependencies:
caller-callsite "^2.0.0"
-callsite@1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
- integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
-
callsites@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
@@ -1252,6 +1240,11 @@ component-emitter@1.2.1, component-emitter@^1.2.0, component-emitter@^1.2.1:
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
+component-emitter@~1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+ integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+
component-inherit@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
@@ -1320,16 +1313,16 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
-cookie@0.3.1:
- version "0.3.1"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
- integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
-
cookie@0.4.0, cookie@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
+cookie@~0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
+ integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
+
cookiejar@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c"
@@ -1982,20 +1975,20 @@ endian-reader@^0.3.0:
resolved "https://registry.yarnpkg.com/endian-reader/-/endian-reader-0.3.0.tgz#84eca436b80aed0d0639c47291338b932efe50a0"
integrity sha1-hOykNrgK7Q0GOcRykTOLky7+UKA=
-engine.io-client@~3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700"
- integrity sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA==
+engine.io-client@~3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.5.0.tgz#fc1b4d9616288ce4f2daf06dcf612413dec941c7"
+ integrity sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==
dependencies:
- component-emitter "1.2.1"
+ component-emitter "~1.3.0"
component-inherit "0.0.3"
- debug "~4.1.0"
+ debug "~3.1.0"
engine.io-parser "~2.2.0"
has-cors "1.1.0"
indexof "0.0.1"
- parseqs "0.0.5"
- parseuri "0.0.5"
- ws "~6.1.0"
+ parseqs "0.0.6"
+ parseuri "0.0.6"
+ ws "~7.4.2"
xmlhttprequest-ssl "~1.5.4"
yeast "0.1.2"
@@ -2010,17 +2003,17 @@ engine.io-parser@~2.2.0:
blob "0.0.5"
has-binary2 "~1.0.2"
-engine.io@~3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.0.tgz#3a962cc4535928c252759a00f98519cb46c53ff3"
- integrity sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w==
+engine.io@~3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.5.0.tgz#9d6b985c8a39b1fe87cd91eb014de0552259821b"
+ integrity sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==
dependencies:
accepts "~1.3.4"
base64id "2.0.0"
- cookie "0.3.1"
+ cookie "~0.4.1"
debug "~4.1.0"
engine.io-parser "~2.2.0"
- ws "^7.1.2"
+ ws "~7.4.2"
entities@^1.1.1:
version "1.1.2"
@@ -4623,11 +4616,6 @@ object-assign@^4.0.1, object-assign@^4.1.0:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
-object-component@0.0.3:
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
- integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=
-
object-copy@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
@@ -4938,19 +4926,15 @@ parse-passwd@^1.0.0:
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
-parseqs@0.0.5:
- version "0.0.5"
- resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
- integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=
- dependencies:
- better-assert "~1.0.0"
+parseqs@0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
+ integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
-parseuri@0.0.5:
- version "0.0.5"
- resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a"
- integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=
- dependencies:
- better-assert "~1.0.0"
+parseuri@0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
+ integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
parseurl@~1.3.3:
version "1.3.3"
@@ -6808,23 +6792,20 @@ socket.io-adapter@~1.1.0:
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b"
integrity sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=
-socket.io-client@2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4"
- integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==
+socket.io-client@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.4.0.tgz#aafb5d594a3c55a34355562fc8aea22ed9119a35"
+ integrity sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==
dependencies:
backo2 "1.0.2"
- base64-arraybuffer "0.1.5"
component-bind "1.0.0"
- component-emitter "1.2.1"
- debug "~4.1.0"
- engine.io-client "~3.4.0"
+ component-emitter "~1.3.0"
+ debug "~3.1.0"
+ engine.io-client "~3.5.0"
has-binary2 "~1.0.2"
- has-cors "1.1.0"
indexof "0.0.1"
- object-component "0.0.3"
- parseqs "0.0.5"
- parseuri "0.0.5"
+ parseqs "0.0.6"
+ parseuri "0.0.6"
socket.io-parser "~3.3.0"
to-array "0.1.4"
@@ -6846,16 +6827,16 @@ socket.io-parser@~3.4.0:
debug "~4.1.0"
isarray "2.0.1"
-socket.io@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb"
- integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==
+socket.io@^2.4.0:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.4.1.tgz#95ad861c9a52369d7f1a68acf0d4a1b16da451d2"
+ integrity sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==
dependencies:
debug "~4.1.0"
- engine.io "~3.4.0"
+ engine.io "~3.5.0"
has-binary2 "~1.0.2"
socket.io-adapter "~1.1.0"
- socket.io-client "2.3.0"
+ socket.io-client "2.4.0"
socket.io-parser "~3.4.0"
socks-proxy-agent@^4.0.1:
@@ -7970,17 +7951,10 @@ write-file-atomic@^3.0.0:
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"
-ws@^7.1.2:
- version "7.2.1"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
- integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==
-
-ws@~6.1.0:
- version "6.1.4"
- resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
- integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==
- dependencies:
- async-limiter "~1.0.0"
+ws@~7.4.2:
+ version "7.4.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd"
+ integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==
xdg-basedir@^4.0.0:
version "4.0.0"