@@ -3,7 +3,31 @@ context('Depends On', () => { | |||||
cy.login(); | cy.login(); | ||||
cy.visit('/app/website'); | cy.visit('/app/website'); | ||||
return cy.window().its('frappe').then(frappe => { | 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', | name: 'Test Depends On', | ||||
fields: [ | fields: [ | ||||
{ | { | ||||
@@ -24,6 +48,13 @@ context('Depends On', () => { | |||||
"fieldtype": "Data", | "fieldtype": "Data", | ||||
'depends_on': "eval:doc.test_field=='Value'" | '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('body').click(); | ||||
cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled'); | 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', () => { | it('should display the field depending on other fields value', () => { | ||||
cy.new_form('Test Depends On'); | cy.new_form('Test Depends On'); | ||||
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible'); | cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible'); | ||||
@@ -160,7 +160,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => { | |||||
Cypress.Commands.add('create_records', doc => { | Cypress.Commands.add('create_records', doc => { | ||||
return cy | 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); | .then(r => r.message); | ||||
}); | }); | ||||
@@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { | |||||
if (fieldtype === 'Select') { | if (fieldtype === 'Select') { | ||||
cy.get('@input').select(value); | cy.get('@input').select(value); | ||||
} else { | } else { | ||||
cy.get('@input').type(value, { waitForAnimations: false, force: true }); | |||||
cy.get('@input').type(value, {waitForAnimations: false, force: true}); | |||||
} | } | ||||
return cy.get('@input'); | return cy.get('@input'); | ||||
}); | }); | ||||
@@ -204,8 +204,43 @@ Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => { | |||||
return cy.get(selector); | 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 => { | 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 => { | Cypress.Commands.add('new_form', doctype => { | ||||
@@ -27,6 +27,7 @@ __version__ = '13.0.0-dev' | |||||
__title__ = "Frappe Framework" | __title__ = "Frappe Framework" | ||||
local = Local() | local = Local() | ||||
controllers = {} | |||||
class _dict(dict): | class _dict(dict): | ||||
"""dict like object that exposes keys as attributes""" | """dict like object that exposes keys as attributes""" | ||||
@@ -628,6 +629,21 @@ def clear_cache(user=None, doctype=None): | |||||
local.role_permissions = {} | 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): | def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False): | ||||
"""Raises `frappe.PermissionError` if not permitted. | """Raises `frappe.PermissionError` if not permitted. | ||||
@@ -7,8 +7,8 @@ import os | |||||
from six import iteritems | from six import iteritems | ||||
import logging | import logging | ||||
from werkzeug.wrappers import Request | |||||
from werkzeug.local import LocalManager | from werkzeug.local import LocalManager | ||||
from werkzeug.wrappers import Request, Response | |||||
from werkzeug.exceptions import HTTPException, NotFound | from werkzeug.exceptions import HTTPException, NotFound | ||||
from werkzeug.middleware.profiler import ProfilerMiddleware | from werkzeug.middleware.profiler import ProfilerMiddleware | ||||
from werkzeug.middleware.shared_data import SharedDataMiddleware | from werkzeug.middleware.shared_data import SharedDataMiddleware | ||||
@@ -57,19 +57,22 @@ def application(request): | |||||
frappe.monitor.start() | frappe.monitor.start() | ||||
frappe.rate_limiter.apply() | 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() | response = frappe.handler.handle() | ||||
elif frappe.request.path.startswith("/api/"): | |||||
elif request.path.startswith("/api/"): | |||||
response = frappe.api.handle() | response = frappe.api.handle() | ||||
elif frappe.request.path.startswith('/backups'): | |||||
elif request.path.startswith('/backups'): | |||||
response = frappe.utils.response.download_backup(request.path) | 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) | 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() | response = frappe.website.render.render() | ||||
else: | else: | ||||
@@ -88,13 +91,9 @@ def application(request): | |||||
rollback = after_request(rollback) | rollback = after_request(rollback) | ||||
finally: | 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() | 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.rate_limiter.update() | ||||
frappe.monitor.stop(response) | frappe.monitor.stop(response) | ||||
frappe.recorder.dump() | frappe.recorder.dump() | ||||
@@ -110,9 +109,7 @@ def application(request): | |||||
"http_status_code": getattr(response, "status_code", "NOTFOUND") | "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() | frappe.destroy() | ||||
return response | return response | ||||
@@ -134,7 +131,46 @@ def init_request(request): | |||||
make_form_dict(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): | def make_form_dict(request): | ||||
import json | import json | ||||
@@ -54,10 +54,12 @@ frappe.ui.form.on('Auto Repeat', { | |||||
toggle_submit_on_creation: function(frm) { | toggle_submit_on_creation: function(frm) { | ||||
// submit on creation checkbox | // 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) { | template: function(frm) { | ||||
@@ -23,7 +23,7 @@ | |||||
"repeat_on_last_day", | "repeat_on_last_day", | ||||
"column_break_12", | "column_break_12", | ||||
"next_schedule_date", | "next_schedule_date", | ||||
"section_break_12", | |||||
"section_break_16", | |||||
"repeat_on_days", | "repeat_on_days", | ||||
"notification", | "notification", | ||||
"notify_by_email", | "notify_by_email", | ||||
@@ -198,20 +198,20 @@ | |||||
"label": "Repeat on Days", | "label": "Repeat on Days", | ||||
"options": "Auto Repeat Day" | "options": "Auto Repeat Day" | ||||
}, | }, | ||||
{ | |||||
"depends_on": "eval:doc.frequency==='Weekly';", | |||||
"fieldname": "section_break_12", | |||||
"fieldtype": "Section Break" | |||||
}, | |||||
{ | { | ||||
"default": "0", | "default": "0", | ||||
"fieldname": "submit_on_creation", | "fieldname": "submit_on_creation", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"label": "Submit on Creation" | "label": "Submit on Creation" | ||||
}, | |||||
{ | |||||
"depends_on": "eval:doc.frequency==='Weekly';", | |||||
"fieldname": "section_break_16", | |||||
"fieldtype": "Section Break" | |||||
} | } | ||||
], | ], | ||||
"links": [], | "links": [], | ||||
"modified": "2020-12-10 10:43:13.449172", | |||||
"modified": "2021-01-12 09:24:49.719611", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Automation", | "module": "Automation", | ||||
"name": "Auto Repeat", | "name": "Auto Repeat", | ||||
@@ -68,6 +68,7 @@ def clear_defaults_cache(user=None): | |||||
frappe.cache().delete_key("defaults") | frappe.cache().delete_key("defaults") | ||||
def clear_doctype_cache(doctype=None): | def clear_doctype_cache(doctype=None): | ||||
clear_controller_cache(doctype) | |||||
cache = frappe.cache() | cache = frappe.cache() | ||||
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_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: | for name in doctype_cache_keys: | ||||
cache.delete_value(name) | 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): | def get_doctype_map(doctype, name, filters=None, order_by=None): | ||||
cache = frappe.cache() | cache = frappe.cache() | ||||
cache_key = frappe.scrub(doctype) + '_map' | cache_key = frappe.scrub(doctype) + '_map' | ||||
@@ -164,7 +164,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments): | |||||
try: | try: | ||||
# use sql, so that we do not mess with the timestamp | # 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 | 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: | except Exception as e: | ||||
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None): | if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None): | ||||
@@ -1,4 +1,5 @@ | |||||
{ | { | ||||
"actions": [], | |||||
"allow_import": 1, | "allow_import": 1, | ||||
"autoname": "hash", | "autoname": "hash", | ||||
"creation": "2017-01-11 04:21:35.217943", | "creation": "2017-01-11 04:21:35.217943", | ||||
@@ -13,6 +14,7 @@ | |||||
"column_break_2", | "column_break_2", | ||||
"permlevel", | "permlevel", | ||||
"section_break_4", | "section_break_4", | ||||
"select", | |||||
"read", | "read", | ||||
"write", | "write", | ||||
"create", | "create", | ||||
@@ -211,9 +213,16 @@ | |||||
"fieldtype": "Data", | "fieldtype": "Data", | ||||
"label": "Reference Document Type", | "label": "Reference Document Type", | ||||
"read_only": 1 | "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", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "Custom DocPerm", | "name": "Custom DocPerm", | ||||
@@ -751,7 +751,7 @@ class Row: | |||||
self.warnings.append( | self.warnings.append( | ||||
{ | { | ||||
"row": self.row_number, | "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 | return | ||||
@@ -1,775 +1,229 @@ | |||||
{ | { | ||||
"allow_copy": 0, | |||||
"allow_guest_to_view": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"actions": [], | |||||
"autoname": "hash", | "autoname": "hash", | ||||
"beta": 0, | |||||
"creation": "2013-02-22 01:27:33", | "creation": "2013-02-22 01:27:33", | ||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | "doctype": "DocType", | ||||
"document_type": "Setup", | "document_type": "Setup", | ||||
"editable_grid": 1, | "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": [ | "fields": [ | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "role_and_level", | "fieldname": "role_and_level", | ||||
"fieldtype": "Section Break", | "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", | "fieldname": "role", | ||||
"fieldtype": "Link", | "fieldtype": "Link", | ||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | |||||
"label": "Role", | "label": "Role", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"oldfieldname": "role", | "oldfieldname": "role", | ||||
"oldfieldtype": "Link", | "oldfieldtype": "Link", | ||||
"options": "Role", | "options": "Role", | ||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "150px", | "print_width": "150px", | ||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | "reqd": 1, | ||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0, | |||||
"width": "150px" | "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", | "description": "Apply this rule if the User is the Owner", | ||||
"fieldname": "if_owner", | "fieldname": "if_owner", | ||||
"fieldtype": "Check", | "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", | "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", | "default": "0", | ||||
"fieldname": "permlevel", | "fieldname": "permlevel", | ||||
"fieldtype": "Int", | "fieldtype": "Int", | ||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | |||||
"label": "Level", | "label": "Level", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"oldfieldname": "permlevel", | "oldfieldname": "permlevel", | ||||
"oldfieldtype": "Int", | "oldfieldtype": "Int", | ||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "40px", | "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" | "width": "40px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "section_break_4", | "fieldname": "section_break_4", | ||||
"fieldtype": "Section Break", | "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", | "default": "1", | ||||
"fieldname": "read", | "fieldname": "read", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | |||||
"label": "Read", | "label": "Read", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"oldfieldname": "read", | "oldfieldname": "read", | ||||
"oldfieldtype": "Check", | "oldfieldtype": "Check", | ||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "32px", | "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" | "width": "32px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "1", | "default": "1", | ||||
"fieldname": "write", | "fieldname": "write", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | |||||
"label": "Write", | "label": "Write", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"oldfieldname": "write", | "oldfieldname": "write", | ||||
"oldfieldtype": "Check", | "oldfieldtype": "Check", | ||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "32px", | "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" | "width": "32px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "1", | "default": "1", | ||||
"fieldname": "create", | "fieldname": "create", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | |||||
"label": "Create", | "label": "Create", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"oldfieldname": "create", | "oldfieldname": "create", | ||||
"oldfieldtype": "Check", | "oldfieldtype": "Check", | ||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "32px", | "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" | "width": "32px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "1", | "default": "1", | ||||
"fieldname": "delete", | "fieldname": "delete", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "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", | "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", | "fieldname": "submit", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | |||||
"label": "Submit", | "label": "Submit", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"oldfieldname": "submit", | "oldfieldname": "submit", | ||||
"oldfieldtype": "Check", | "oldfieldtype": "Check", | ||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "32px", | "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" | "width": "32px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "0", | |||||
"fieldname": "cancel", | "fieldname": "cancel", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | |||||
"label": "Cancel", | "label": "Cancel", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"oldfieldname": "cancel", | "oldfieldname": "cancel", | ||||
"oldfieldtype": "Check", | "oldfieldtype": "Check", | ||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "32px", | "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" | "width": "32px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "0", | |||||
"fieldname": "amend", | "fieldname": "amend", | ||||
"fieldtype": "Check", | "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", | "label": "Amend", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"oldfieldname": "amend", | "oldfieldname": "amend", | ||||
"oldfieldtype": "Check", | "oldfieldtype": "Check", | ||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "32px", | "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" | "width": "32px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "additional_permissions", | "fieldname": "additional_permissions", | ||||
"fieldtype": "Section Break", | "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", | "default": "1", | ||||
"fieldname": "report", | "fieldname": "report", | ||||
"fieldtype": "Check", | "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", | "label": "Report", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "32px", | "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" | "width": "32px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "1", | "default": "1", | ||||
"fieldname": "export", | "fieldname": "export", | ||||
"fieldtype": "Check", | "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", | "fieldname": "import", | ||||
"fieldtype": "Check", | "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", | "description": "This role update User Permissions for a user", | ||||
"fieldname": "set_user_permissions", | "fieldname": "set_user_permissions", | ||||
"fieldtype": "Check", | "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", | "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", | "default": "1", | ||||
"fieldname": "share", | "fieldname": "share", | ||||
"fieldtype": "Check", | "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", | "default": "1", | ||||
"fieldname": "print", | "fieldname": "print", | ||||
"fieldtype": "Check", | "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", | "default": "1", | ||||
"fieldname": "email", | "fieldname": "email", | ||||
"fieldtype": "Check", | "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, | "idx": 1, | ||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 0, | |||||
"istable": 1, | "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", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "DocPerm", | "name": "DocPerm", | ||||
"owner": "Administrator", | "owner": "Administrator", | ||||
"permissions": [], | "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" | |||||
} | } |
@@ -5,7 +5,7 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import re, copy, os, shutil | import re, copy, os, shutil | ||||
import json | 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 | # imports - third party imports | ||||
import six | import six | ||||
@@ -395,13 +395,11 @@ class DocType(Document): | |||||
if not frappe.flags.in_patch: | if not frappe.flags.in_patch: | ||||
self.rename_files_and_folders(old, new) | 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): | def after_delete(self): | ||||
if not self.custom: | 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): | def rename_files_and_folders(self, old, new): | ||||
# move files | # move files | ||||
@@ -1004,10 +1002,10 @@ def validate_fields(meta): | |||||
check_sort_field(meta) | check_sort_field(meta) | ||||
check_image_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.""" | """Validates if permissions are set correctly.""" | ||||
doctype = frappe.get_doc("DocType", doctype) | doctype = frappe.get_doc("DocType", doctype) | ||||
validate_permissions(doctype, for_remove) | |||||
validate_permissions(doctype, for_remove, alert=alert) | |||||
# save permissions | # save permissions | ||||
for perm in doctype.get("permissions"): | for perm in doctype.get("permissions"): | ||||
@@ -1030,9 +1028,10 @@ def clear_permissions_cache(doctype): | |||||
""", doctype): | """, doctype): | ||||
frappe.clear_cache(user=user) | 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") | 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') | frappe.msgprint(_('No Permissions Specified'), alert=True, indicator='orange') | ||||
issingle = issubmittable = isimportable = False | issingle = issubmittable = isimportable = False | ||||
if doctype: | 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) | 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): | 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))) | frappe.throw(_("{0}: No basic permissions set").format(get_txt(d))) | ||||
def check_double(d): | def check_double(d): | ||||
@@ -6,8 +6,19 @@ from __future__ import unicode_literals | |||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.utils.data import evaluate_filters | from frappe.utils.data import evaluate_filters | ||||
from frappe import _ | |||||
class DocumentNamingRule(Document): | 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): | def apply(self, doc): | ||||
''' | ''' | ||||
Apply naming rules for the given document. Will set `name` if the rule is matched. | Apply naming rules for the given document. Will set `name` if the rule is matched. | ||||
@@ -47,7 +47,7 @@ | |||||
"fieldname": "doctype_event", | "fieldname": "doctype_event", | ||||
"fieldtype": "Select", | "fieldtype": "Select", | ||||
"label": "DocType Event", | "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'", | "depends_on": "eval:doc.script_type==='API'", | ||||
@@ -88,7 +88,7 @@ | |||||
], | ], | ||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2020-12-03 22:42:02.708148", | |||||
"modified": "2021-01-03 18:50:14.767595", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "Server Script", | "name": "Server Script", | ||||
@@ -6,6 +6,7 @@ import frappe | |||||
EVENT_MAP = { | EVENT_MAP = { | ||||
'before_insert': 'Before Insert', | 'before_insert': 'Before Insert', | ||||
'after_insert': 'After Insert', | 'after_insert': 'After Insert', | ||||
'before_validate': 'Before Validate', | |||||
'validate': 'Before Save', | 'validate': 'Before Save', | ||||
'on_update': 'After Save', | 'on_update': 'After Save', | ||||
'before_submit': 'Before Submit', | 'before_submit': 'Before Submit', | ||||
@@ -358,7 +358,7 @@ | |||||
"collapsible": 1, | "collapsible": 1, | ||||
"fieldname": "email", | "fieldname": "email", | ||||
"fieldtype": "Section Break", | "fieldtype": "Section Break", | ||||
"label": "EMail" | |||||
"label": "Email" | |||||
}, | }, | ||||
{ | { | ||||
"description": "Your organization name and address for the email footer.", | "description": "Your organization name and address for the email footer.", | ||||
@@ -504,4 +504,4 @@ | |||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "ASC", | "sort_order": "ASC", | ||||
"track_changes": 1 | "track_changes": 1 | ||||
} | |||||
} |
@@ -654,6 +654,11 @@ | |||||
"group": "Activity", | "group": "Activity", | ||||
"link_doctype": "ToDo", | "link_doctype": "ToDo", | ||||
"link_fieldname": "owner" | "link_fieldname": "owner" | ||||
}, | |||||
{ | |||||
"group": "Integrations", | |||||
"link_doctype": "Token Cache", | |||||
"link_fieldname": "user" | |||||
} | } | ||||
], | ], | ||||
"max_attachments": 5, | "max_attachments": 5, | ||||
@@ -21,7 +21,7 @@ | |||||
<td class="danger">{{ item[1] }}</td> | <td class="danger">{{ item[1] }}</td> | ||||
<td class="success">{{ item[2] }}</td> | <td class="success">{{ item[2] }}</td> | ||||
</tr> | </tr> | ||||
{% endif %} | |||||
{% endfor %} | |||||
</tbody> | </tbody> | ||||
</table> | </table> | ||||
{% endif %} | {% endif %} | ||||
@@ -58,7 +58,7 @@ | |||||
</table> | </table> | ||||
</td> | </td> | ||||
</tr> | </tr> | ||||
{% endif %} | |||||
{% endfor %} | |||||
</tbody> | </tbody> | ||||
</table> | </table> | ||||
@@ -93,4 +93,4 @@ | |||||
{% endfor %} | {% endfor %} | ||||
</tbody> | </tbody> | ||||
{% endif %} | {% endif %} | ||||
</div> | |||||
</div> |
@@ -292,7 +292,7 @@ frappe.PermissionEngine = class PermissionEngine { | |||||
} | } | ||||
get rights() { | 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"] | "print", "email", "report", "import", "export", "set_user_permissions", "share"] | ||||
} | } | ||||
@@ -77,6 +77,18 @@ def add(parent, role, permlevel): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def update(doctype, role, permlevel, ptype, value=None): | 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") | frappe.only_for("System Manager") | ||||
out = update_permission_property(doctype, role, permlevel, ptype, value) | out = update_permission_property(doctype, role, permlevel, ptype, value) | ||||
return 'refresh' if out else None | 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)): | if not frappe.get_all('Custom DocPerm', dict(parent=doctype)): | ||||
frappe.throw(_('There must be atleast one permission rule.'), title=_('Cannot Remove')) | 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() | @frappe.whitelist() | ||||
def reset(doctype): | def reset(doctype): | ||||
@@ -42,7 +42,6 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat | |||||
except Exception: | except Exception: | ||||
frappe.errprint(frappe.utils.get_traceback()) | frappe.errprint(frappe.utils.get_traceback()) | ||||
frappe.msgprint(frappe._("Did not cancel")) | |||||
raise | raise | ||||
def send_updated_docs(doc): | def send_updated_docs(doc): | ||||
@@ -150,7 +150,8 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||||
# 2 is the index of _relevance column | # 2 is the index of _relevance column | ||||
order_by = "_relevance, {0}, `tab{1}`.idx desc".format(order_by_based_on_meta, doctype) | 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: | if doctype in UNTRANSLATED_DOCTYPES: | ||||
page_length = None | page_length = None | ||||
@@ -210,7 +210,7 @@ class EmailAccount(Document): | |||||
elif not in_receive and any(map(lambda t: t in message, auth_error_codes)): | elif not in_receive and any(map(lambda t: t in message, auth_error_codes)): | ||||
self.throw_invalid_credentials_exception() | self.throw_invalid_credentials_exception() | ||||
else: | else: | ||||
frappe.throw(e) | |||||
frappe.throw(cstr(e)) | |||||
except socket.error: | except socket.error: | ||||
if in_receive: | if in_receive: | ||||
@@ -295,7 +295,7 @@ def set_update(update, producer_site): | |||||
if data.changed: | if data.changed: | ||||
local_doc.update(data.changed) | local_doc.update(data.changed) | ||||
if data.removed: | if data.removed: | ||||
update_row_removed(local_doc, data.removed) | |||||
local_doc = update_row_removed(local_doc, data.removed) | |||||
if data.row_changed: | if data.row_changed: | ||||
update_row_changed(local_doc, data.row_changed) | update_row_changed(local_doc, data.row_changed) | ||||
if data.added: | if data.added: | ||||
@@ -318,7 +318,17 @@ def update_row_removed(local_doc, removed): | |||||
for tablename, rownames in iteritems(removed): | for tablename, rownames in iteritems(removed): | ||||
table = local_doc.get_table_field_doctype(tablename) | table = local_doc.get_table_field_doctype(tablename) | ||||
for row in rownames: | 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): | def update_row_changed(local_doc, changed): | ||||
@@ -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) |
@@ -18,7 +18,7 @@ app_email = "info@frappe.io" | |||||
docs_app = "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" | before_install = "frappe.utils.install.before_install" | ||||
after_install = "frappe.utils.install.after_install" | after_install = "frappe.utils.install.after_install" | ||||
@@ -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()); | |||||
} | |||||
}); |
@@ -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 | |||||
} |
@@ -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() |
@@ -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() |
@@ -0,0 +1,13 @@ | |||||
[ | |||||
{ | |||||
"doctype": "Connected App", | |||||
"provider_name": "frappe", | |||||
"client_id": "test_client_id", | |||||
"client_secret": "test_client_secret", | |||||
"scopes": [ | |||||
{ | |||||
"scope": "all" | |||||
} | |||||
] | |||||
} | |||||
] |
@@ -1,7 +1,6 @@ | |||||
[ | [ | ||||
{ | { | ||||
"app_name": "_Test OAuth Client", | |||||
"client_id": "test_client_id", | |||||
"app_name": "_Test OAuth Client", | |||||
"client_secret": "test_client_secret", | "client_secret": "test_client_secret", | ||||
"default_redirect_uri": "http://localhost", | "default_redirect_uri": "http://localhost", | ||||
"docstatus": 0, | "docstatus": 0, | ||||
@@ -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 | |||||
} |
@@ -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 |
@@ -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" | |||||
} |
@@ -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 |
@@ -22,3 +22,17 @@ def make_social_login_key(**kwargs): | |||||
kwargs["provider_name"] = "Test OAuth2 Provider" | kwargs["provider_name"] = "Test OAuth2 Provider" | ||||
doc = frappe.get_doc(kwargs) | doc = frappe.get_doc(kwargs) | ||||
return doc | 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 |
@@ -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" | |||||
} | |||||
] | |||||
} | |||||
] |
@@ -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() |
@@ -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) { | |||||
// } | |||||
}); |
@@ -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 | |||||
} |
@@ -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 | |||||
} |
@@ -85,7 +85,7 @@ def enqueue_webhook(doc, webhook): | |||||
for i in range(3): | for i in range(3): | ||||
try: | 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() | r.raise_for_status() | ||||
frappe.logger().debug({"webhook_success": r.text}) | frappe.logger().debug({"webhook_success": r.text}) | ||||
break | break | ||||
@@ -20,6 +20,7 @@ def get_oauth_server(): | |||||
return frappe.local.oauth_server | return frappe.local.oauth_server | ||||
def sanitize_kwargs(param_kwargs): | def sanitize_kwargs(param_kwargs): | ||||
"""Remove 'data' and 'cmd' keys, if present.""" | |||||
arguments = param_kwargs | arguments = param_kwargs | ||||
arguments.pop('data', None) | arguments.pop('data', None) | ||||
arguments.pop('cmd', None) | arguments.pop('cmd', None) | ||||
@@ -48,7 +48,7 @@ def get_controller(doctype): | |||||
else: | else: | ||||
class_overrides = frappe.get_hooks('override_doctype_class') | class_overrides = frappe.get_hooks('override_doctype_class') | ||||
if class_overrides and class_overrides.get(doctype): | 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_path, classname = import_path.rsplit('.', 1) | ||||
module = frappe.get_module(module_path) | module = frappe.get_module(module_path) | ||||
if not hasattr(module, classname): | if not hasattr(module, classname): | ||||
@@ -69,10 +69,13 @@ def get_controller(doctype): | |||||
if frappe.local.dev_server: | if frappe.local.dev_server: | ||||
return _get_controller() | 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): | class BaseDocument(object): | ||||
ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns") | ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns") | ||||
@@ -40,7 +40,10 @@ class DatabaseQuery(object): | |||||
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, | ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, | ||||
update=None, add_total_row=None, user_settings=None, reference_doctype=None, | update=None, add_total_row=None, user_settings=None, reference_doctype=None, | ||||
return_query=False, strict=True, pluck=None, ignore_ddl=False): | 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)) | frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype)) | ||||
raise frappe.PermissionError(self.doctype) | raise frappe.PermissionError(self.doctype) | ||||
@@ -315,7 +318,10 @@ class DatabaseQuery(object): | |||||
def append_table(self, table_name): | def append_table(self, table_name): | ||||
self.tables.append(table_name) | self.tables.append(table_name) | ||||
doctype = table_name[4:-1] | 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)) | frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype)) | ||||
raise frappe.PermissionError(doctype) | raise frappe.PermissionError(doctype) | ||||
@@ -576,7 +582,7 @@ class DatabaseQuery(object): | |||||
self.shared = frappe.share.get_shared(self.doctype, self.user) | self.shared = frappe.share.get_shared(self.doctype, self.user) | ||||
if (not meta.istable and | 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 self.flags.ignore_permissions and | ||||
not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)): | not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)): | ||||
only_if_shared = True | only_if_shared = True | ||||
@@ -939,15 +939,17 @@ class Document(BaseDocument): | |||||
self.load_doc_before_save() | self.load_doc_before_save() | ||||
self.reset_seen() | 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: | if self.flags.ignore_validate: | ||||
return | return | ||||
if self._action=="save": | if self._action=="save": | ||||
self.run_method("before_validate") | |||||
self.run_method("validate") | self.run_method("validate") | ||||
self.run_method("before_save") | self.run_method("before_save") | ||||
elif self._action=="submit": | elif self._action=="submit": | ||||
self.run_method("before_validate") | |||||
self.run_method("validate") | self.run_method("validate") | ||||
self.run_method("before_submit") | self.run_method("before_submit") | ||||
elif self._action=="cancel": | elif self._action=="cancel": | ||||
@@ -68,7 +68,7 @@ def load_doctype_from_file(doctype): | |||||
class Meta(Document): | class Meta(Document): | ||||
_metaclass = True | _metaclass = True | ||||
default_fields = list(default_fields)[1:] | 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): | def __init__(self, doctype): | ||||
self._fields = {} | self._fields = {} | ||||
@@ -484,6 +484,8 @@ class Meta(Document): | |||||
if not data.transactions: | if not data.transactions: | ||||
# init groups | # init groups | ||||
data.transactions = [] | data.transactions = [] | ||||
if not data.non_standard_fieldnames: | |||||
data.non_standard_fieldnames = {} | data.non_standard_fieldnames = {} | ||||
for link in dashboard_links: | for link in dashboard_links: | ||||
@@ -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) | docname = rename_doc(doctype=doctype, old=docname, new=new_name, merge=merge) | ||||
if old_title and new_title and not old_title == new_title: | 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 | return docname | ||||
@@ -120,9 +120,8 @@ def apply_workflow(doc, action): | |||||
return doc | return doc | ||||
@frappe.whitelist() | @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: | for state_doc in workflow.states: | ||||
if state_doc.doc_status == '2': | if state_doc.doc_status == '2': | ||||
for transition in workflow.transitions: | for transition in workflow.transitions: | ||||
@@ -326,3 +326,4 @@ execute:frappe.delete_doc_if_exists('Page', 'dashboard', force=1) | |||||
frappe.core.doctype.page.patches.drop_unused_pages | frappe.core.doctype.page.patches.drop_unused_pages | ||||
execute:frappe.get_doc('Role', 'Guest').save() # remove desk access | execute:frappe.get_doc('Role', 'Guest').save() # remove desk access | ||||
frappe.patches.v13_0.rename_desk_page_to_workspace | frappe.patches.v13_0.rename_desk_page_to_workspace | ||||
frappe.patches.v13_0.delete_package_publish_tool |
@@ -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) |
@@ -2,9 +2,23 @@ import frappe | |||||
def execute(): | def execute(): | ||||
frappe.reload_doctype('Website Theme') | 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'): | for theme in frappe.get_all('Website Theme'): | ||||
doc = frappe.get_doc('Website Theme', theme.name) | doc = frappe.get_doc('Website Theme', theme.name) | ||||
if not doc.get('custom_scss') and doc.theme_scss: | if not doc.get('custom_scss') and doc.theme_scss: | ||||
# move old theme to new theme | # move old theme to new theme | ||||
doc.custom_scss = doc.theme_scss | doc.custom_scss = doc.theme_scss | ||||
if doc.background_color: | |||||
setup_color_record(doc.background_color) | |||||
doc.save() | doc.save() | ||||
def setup_color_record(color): | |||||
frappe.get_doc({ | |||||
"doctype": "Color", | |||||
"__newname": color, | |||||
"color": color, | |||||
}).save() |
@@ -7,7 +7,7 @@ import frappe, copy, json | |||||
from frappe import _, msgprint | from frappe import _, msgprint | ||||
from frappe.utils import cint | from frappe.utils import cint | ||||
import frappe.share | 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") | "print", "email", "report", "import", "export", "set_user_permissions", "share") | ||||
# TODO: | # 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) | role_permissions = get_role_permissions(meta, user=user) | ||||
perm = role_permissions.get(ptype) | perm = role_permissions.get(ptype) | ||||
if not perm: | 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))) | 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'): | and ptype != 'create'): | ||||
perms['if_owner'][ptype] = 1 | perms['if_owner'][ptype] = 1 | ||||
# has no access if not owner | # 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) | # (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 | frappe.local.role_permissions[cache_key] = perms | ||||
@@ -261,6 +261,7 @@ | |||||
"public/js/frappe/views/calendar/calendar.js", | "public/js/frappe/views/calendar/calendar.js", | ||||
"public/js/frappe/views/dashboard/dashboard_view.js", | "public/js/frappe/views/dashboard/dashboard_view.js", | ||||
"public/js/frappe/views/image/image_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/kanban/kanban_view.js", | ||||
"public/js/frappe/views/inbox/inbox_view.js", | "public/js/frappe/views/inbox/inbox_view.js", | ||||
"public/js/frappe/views/file/file_view.js", | "public/js/frappe/views/file/file_view.js", | ||||
@@ -401,6 +401,13 @@ input.list-row-checkbox { | |||||
.pswp__more-item img { | .pswp__more-item img { | ||||
max-height: 100%; | 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 { | .list-paging-area .gantt-view-mode { | ||||
margin-left: 15px; | margin-left: 15px; | ||||
margin-right: 15px; | margin-right: 15px; | ||||
@@ -40,23 +40,31 @@ frappe.ui.form.Control = Class.extend({ | |||||
return this.df.get_status(this); | 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 | // like in case of a dialog box | ||||
if (cint(this.df.hidden)) { | if (cint(this.df.hidden)) { | ||||
// eslint-disable-next-line | // 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"; | return "None"; | ||||
} else if (cint(this.df.hidden_due_to_dependency)) { | } else if (cint(this.df.hidden_due_to_dependency)) { | ||||
// eslint-disable-next-line | // 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"; | return "None"; | ||||
} else if (cint(this.df.read_only)) { | } else if (cint(this.df.read_only)) { | ||||
// eslint-disable-next-line | // 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"; | 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"; | return "Write"; | ||||
@@ -65,13 +73,22 @@ frappe.ui.form.Control = Class.extend({ | |||||
var status = frappe.perm.get_field_display_status(this.df, | 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); | 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 | // hide if no value | ||||
if (this.doctype && status==="Read" && !this.only_input | if (this.doctype && status==="Read" && !this.only_input | ||||
&& is_null(frappe.model.get_value(this.doctype, this.docname, this.df.fieldname)) | && is_null(frappe.model.get_value(this.doctype, this.docname, this.df.fieldname)) | ||||
&& !in_list(["HTML", "Image", "Button"], this.df.fieldtype)) { | && !in_list(["HTML", "Image", "Button"], this.df.fieldtype)) { | ||||
// eslint-disable-next-line | // 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"; | status = "None"; | ||||
} | } | ||||
@@ -20,7 +20,7 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ | |||||
<div class="control-input-wrapper">\ | <div class="control-input-wrapper">\ | ||||
<div class="control-input"></div>\ | <div class="control-input"></div>\ | ||||
<div class="control-value like-disabled-input" style="display: none;"></div>\ | <div class="control-value like-disabled-input" style="display: none;"></div>\ | ||||
<p class="help-box small text-muted hidden-xs"></p>\ | |||||
<p class="help-box small text-muted"></p>\ | |||||
</div>\ | </div>\ | ||||
</div>\ | </div>\ | ||||
</div>').appendTo(this.parent); | </div>').appendTo(this.parent); | ||||
@@ -1,3 +1,5 @@ | |||||
frappe.provide('frappe.utils.utils'); | |||||
frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ | frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ | ||||
horizontal: false, | horizontal: false, | ||||
@@ -90,11 +92,11 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ | |||||
}); | }); | ||||
L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/'; | 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: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' | |||||
}).addTo(this.map); | |||||
L.tileLayer(frappe.utils.map_defaults.tiles, | |||||
frappe.utils.map_defaults.options).addTo(this.map); | |||||
}, | }, | ||||
bind_leaflet_locate_control() { | bind_leaflet_locate_control() { | ||||
@@ -51,6 +51,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ | |||||
this.translate_values = true; | this.translate_values = true; | ||||
this.setup_buttons(); | this.setup_buttons(); | ||||
this.setup_awesomeplete(); | this.setup_awesomeplete(); | ||||
this.bind_change_event(); | |||||
}, | }, | ||||
get_options: function() { | get_options: function() { | ||||
return this.df.options; | 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.$input.cache[doctype][term] = r.results; | ||||
me.awesomplete.list = me.$input.cache[doctype][term]; | me.awesomplete.list = me.$input.cache[doctype][term]; | ||||
me.toggle_href(doctype); | |||||
} | } | ||||
}); | }); | ||||
}, 500)); | }, 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'}] | // 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) { | get_filter_description(filters) { | ||||
let doctype = this.get_options(); | let doctype = this.get_options(); | ||||
let filter_array = []; | let filter_array = []; | ||||
@@ -47,7 +47,7 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({ | |||||
}); | }); | ||||
}, | }, | ||||
get_value() { | get_value() { | ||||
return cint(this.value); | |||||
return cint(this.value, null); | |||||
}, | }, | ||||
set_formatted_input(value) { | set_formatted_input(value) { | ||||
let el = $(this.input_area).find('i'); | let el = $(this.input_area).find('i'); | ||||
@@ -9,7 +9,8 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({ | |||||
frm: this.frm, | frm: this.frm, | ||||
df: this.df, | df: this.df, | ||||
perm: this.perm || (this.frm && this.frm.perm) || this.df.perm, | perm: this.perm || (this.frm && this.frm.perm) || this.df.perm, | ||||
parent: this.wrapper | |||||
parent: this.wrapper, | |||||
control: this | |||||
}); | }); | ||||
if(this.frm) { | if(this.frm) { | ||||
this.frm.grids[this.frm.grids.length] = this; | this.frm.grids[this.frm.grids.length] = this; | ||||
@@ -1264,7 +1264,10 @@ frappe.ui.form.Form = class FrappeForm { | |||||
} | } | ||||
if (df && df[property] != value) { | if (df && df[property] != value) { | ||||
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); | |||||
} | |||||
} | } | ||||
} | } | ||||
@@ -128,11 +128,15 @@ frappe.form.formatters = { | |||||
return repl('<a onclick="%(onclick)s">%(value)s</a>', | return repl('<a onclick="%(onclick)s">%(value)s</a>', | ||||
{onclick: docfield.link_onclick.replace(/"/g, '"'), value:value}); | {onclick: docfield.link_onclick.replace(/"/g, '"'), value:value}); | ||||
} else if(docfield && doctype) { | } else if(docfield && doctype) { | ||||
return `<a | |||||
href="/app/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}" | |||||
data-doctype="${doctype}" | |||||
data-name="${original_value}"> | |||||
${__(options && options.label || value)}</a>` | |||||
if (!frappe.model.can_select(doctype) && frappe.model.can_read(doctype)) { | |||||
return `<a | |||||
href="/app/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}" | |||||
data-doctype="${doctype}" | |||||
data-name="${original_value}"> | |||||
${__(options && options.label || value)}</a>` | |||||
} else { | |||||
return value; | |||||
} | |||||
} else { | } else { | ||||
return value; | return value; | ||||
} | } | ||||
@@ -280,6 +280,8 @@ export default class Grid { | |||||
if (this.frm) { | if (this.frm) { | ||||
this.display_status = frappe.perm.get_field_display_status(this.df, this.frm.doc, | this.display_status = frappe.perm.get_field_display_status(this.df, this.frm.doc, | ||||
this.perm); | this.perm); | ||||
} else if (this.df.is_web_form && this.control) { | |||||
this.display_status = this.control.get_status(); | |||||
} else { | } else { | ||||
// not in form | // not in form | ||||
this.display_status = 'Write'; | this.display_status = 'Write'; | ||||
@@ -16,6 +16,9 @@ export default class GridRowForm { | |||||
body: this.form_area, | body: this.form_area, | ||||
no_submit_on_enter: true, | no_submit_on_enter: true, | ||||
frm: this.row.frm, | frm: this.row.frm, | ||||
grid: this.row.grid, | |||||
grid_row: this.row, | |||||
grid_row_form: this, | |||||
}); | }); | ||||
this.layout.make(); | this.layout.make(); | ||||
@@ -1,7 +1,7 @@ | |||||
import '../class'; | import '../class'; | ||||
frappe.ui.form.Layout = Class.extend({ | frappe.ui.form.Layout = Class.extend({ | ||||
init: function(opts) { | |||||
init: function (opts) { | |||||
this.views = {}; | this.views = {}; | ||||
this.pages = []; | this.pages = []; | ||||
this.sections = []; | this.sections = []; | ||||
@@ -87,7 +87,7 @@ frappe.ui.form.Layout = Class.extend({ | |||||
this.message.empty().addClass('hidden'); | this.message.empty().addClass('hidden'); | ||||
} | } | ||||
}, | }, | ||||
render: function(new_fields) { | |||||
render: function (new_fields) { | |||||
var me = this; | var me = this; | ||||
var fields = new_fields || this.fields; | var fields = new_fields || this.fields; | ||||
@@ -101,8 +101,8 @@ frappe.ui.form.Layout = Class.extend({ | |||||
if (this.no_opening_section()) { | if (this.no_opening_section()) { | ||||
this.make_section(); | this.make_section(); | ||||
} | } | ||||
$.each(fields, function(i, df) { | |||||
switch(df.fieldtype) { | |||||
$.each(fields, function (i, df) { | |||||
switch (df.fieldtype) { | |||||
case "Fold": | case "Fold": | ||||
me.make_page(df); | me.make_page(df); | ||||
break; | 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()) { | if (this.no_opening_section()) { | ||||
this.fields.unshift({fieldtype: 'Section Break'}); | 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 | df.fieldname = fieldname; // change of fieldname is avoided | ||||
if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) { | if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) { | ||||
const fieldobj = this.init_field(df, render); | 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.section && this.make_section(); | ||||
!this.column && this.make_column(); | !this.column && this.make_column(); | ||||
@@ -161,29 +161,30 @@ frappe.ui.form.Layout = Class.extend({ | |||||
fieldobj.section = this.section; | fieldobj.section = this.section; | ||||
}, | }, | ||||
init_field: function(df, render = false) { | |||||
init_field: function (df, render = false) { | |||||
const fieldobj = frappe.ui.form.make_control({ | const fieldobj = frappe.ui.form.make_control({ | ||||
df: df, | df: df, | ||||
doctype: this.doctype, | doctype: this.doctype, | ||||
parent: this.column.wrapper.get(0), | parent: this.column.wrapper.get(0), | ||||
frm: this.frm, | frm: this.frm, | ||||
render_input: render, | render_input: render, | ||||
doc: this.doc | |||||
doc: this.doc, | |||||
layout: this | |||||
}); | }); | ||||
fieldobj.layout = this; | fieldobj.layout = this; | ||||
return fieldobj; | return fieldobj; | ||||
}, | }, | ||||
make_page: function(df) { | |||||
make_page: function (df) { // eslint-disable-line no-unused-vars | |||||
var me = this, | var me = this, | ||||
head = $('<div class="form-clickable-section text-center">\ | head = $('<div class="form-clickable-section text-center">\ | ||||
<a class="btn-fold h6 text-muted">'+__("Show more details")+'</a>\ | |||||
<a class="btn-fold h6 text-muted">' + __("Show more details") + '</a>\ | |||||
</div>').appendTo(this.wrapper); | </div>').appendTo(this.wrapper); | ||||
this.page = $('<div class="form-page second-page hide"></div>').appendTo(this.wrapper); | this.page = $('<div class="form-page second-page hide"></div>').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(); | var page = $(this).parent().next(); | ||||
if (page.hasClass("hide")) { | if (page.hasClass("hide")) { | ||||
$(this).removeClass("btn-fold").html(__("Hide details")); | $(this).removeClass("btn-fold").html(__("Hide details")); | ||||
@@ -201,11 +202,11 @@ frappe.ui.form.Layout = Class.extend({ | |||||
this.folded = true; | this.folded = true; | ||||
}, | }, | ||||
unfold: function() { | |||||
unfold: function () { | |||||
this.fold_btn.trigger('click'); | this.fold_btn.trigger('click'); | ||||
}, | }, | ||||
make_section: function(df) { | |||||
make_section: function (df) { | |||||
this.section = new frappe.ui.form.Section(this, df); | this.section = new frappe.ui.form.Section(this, df); | ||||
// append to layout fields | // append to layout fields | ||||
@@ -217,14 +218,14 @@ frappe.ui.form.Layout = Class.extend({ | |||||
this.column = null; | this.column = null; | ||||
}, | }, | ||||
make_column: function(df) { | |||||
make_column: function (df) { | |||||
this.column = new frappe.ui.form.Column(this.section, df); | this.column = new frappe.ui.form.Column(this.section, df); | ||||
if (df && df.fieldname) { | if (df && df.fieldname) { | ||||
this.fields_list.push(this.column); | this.fields_list.push(this.column); | ||||
} | } | ||||
}, | }, | ||||
refresh: function(doc) { | |||||
refresh: function (doc) { | |||||
var me = this; | var me = this; | ||||
if (doc) this.doc = doc; | 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) => { | let fieldnames = fields.map((field) => { | ||||
if (field.fieldname) return field.fieldname; | 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.render(fields); | ||||
this.refresh_fields(fields); | this.refresh_fields(fields); | ||||
}, | }, | ||||
refresh_section_collapse: function() { | |||||
refresh_section_collapse: function () { | |||||
if (!this.doc) return; | if (!this.doc) return; | ||||
for (var i=0; i<this.sections.length; i++) { | |||||
for (var i = 0; i < this.sections.length; i++) { | |||||
var section = this.sections[i]; | var section = this.sections[i]; | ||||
var df = section.df; | var df = section.df; | ||||
if (df && df.collapsible) { | if (df && df.collapsible) { | ||||
@@ -309,9 +310,9 @@ frappe.ui.form.Layout = Class.extend({ | |||||
} | } | ||||
}, | }, | ||||
attach_doc_and_docfields: function(refresh) { | |||||
attach_doc_and_docfields: function (refresh) { | |||||
var me = this; | var me = this; | ||||
for (var i=0, l=this.fields_list.length; i<l; i++) { | |||||
for (var i = 0, l = this.fields_list.length; i < l; i++) { | |||||
var fieldobj = this.fields_list[i]; | var fieldobj = this.fields_list[i]; | ||||
if (me.doc) { | if (me.doc) { | ||||
fieldobj.doc = me.doc; | fieldobj.doc = me.doc; | ||||
@@ -329,15 +330,15 @@ frappe.ui.form.Layout = Class.extend({ | |||||
} | } | ||||
}, | }, | ||||
refresh_section_count: function() { | |||||
this.wrapper.find(".section-count-label:visible").each(function(i) { | |||||
$(this).html(i+1); | |||||
refresh_section_count: function () { | |||||
this.wrapper.find(".section-count-label:visible").each(function (i) { | |||||
$(this).html(i + 1); | |||||
}); | }); | ||||
}, | }, | ||||
setup_tabbing: function() { | |||||
setup_tabbing: function () { | |||||
var me = this; | var me = this; | ||||
this.wrapper.on("keydown", function(ev) { | |||||
if (ev.which==9) { | |||||
this.wrapper.on("keydown", function (ev) { | |||||
if (ev.which == 9) { | |||||
var current = $(ev.target), | var current = $(ev.target), | ||||
doctype = current.attr("data-doctype"), | doctype = current.attr("data-doctype"), | ||||
fieldname = current.attr("data-fieldname"); | fieldname = current.attr("data-fieldname"); | ||||
@@ -346,7 +347,7 @@ frappe.ui.form.Layout = Class.extend({ | |||||
} | } | ||||
}); | }); | ||||
}, | }, | ||||
handle_tab: function(doctype, fieldname, shift) { | |||||
handle_tab: function (doctype, fieldname, shift) { | |||||
var me = this, | var me = this, | ||||
grid_row = null, | grid_row = null, | ||||
prev = null, | prev = null, | ||||
@@ -363,8 +364,8 @@ frappe.ui.form.Layout = Class.extend({ | |||||
fields = grid_row.layout.fields_list; | fields = grid_row.layout.fields_list; | ||||
} | } | ||||
for (var i=0, len=fields.length; i < len; i++) { | |||||
if (fields[i].df.fieldname==fieldname) { | |||||
for (var i = 0, len = fields.length; i < len; i++) { | |||||
if (fields[i].df.fieldname == fieldname) { | |||||
if (shift) { | if (shift) { | ||||
if (prev) { | if (prev) { | ||||
this.set_focus(prev); | this.set_focus(prev); | ||||
@@ -373,7 +374,7 @@ frappe.ui.form.Layout = Class.extend({ | |||||
} | } | ||||
break; | break; | ||||
} | } | ||||
if (i < len-1) { | |||||
if (i < len - 1) { | |||||
focused = me.focus_on_next_field(i, fields); | focused = me.focus_on_next_field(i, fields); | ||||
} | } | ||||
@@ -389,9 +390,9 @@ frappe.ui.form.Layout = Class.extend({ | |||||
// last field in this group | // last field in this group | ||||
if (grid_row) { | if (grid_row) { | ||||
// in grid | // in grid | ||||
if (grid_row.doc.idx==grid_row.grid.grid_rows.length) { | |||||
if (grid_row.doc.idx == grid_row.grid.grid_rows.length) { | |||||
// last row, close it and find next field | // last row, close it and find next field | ||||
grid_row.toggle_view(false, function() { | |||||
grid_row.toggle_view(false, function () { | |||||
grid_row.grid.frm.layout.handle_tab(grid_row.grid.df.parent, grid_row.grid.df.fieldname); | grid_row.grid.frm.layout.handle_tab(grid_row.grid.df.parent, grid_row.grid.df.fieldname); | ||||
}); | }); | ||||
} else { | } else { | ||||
@@ -405,12 +406,12 @@ frappe.ui.form.Layout = Class.extend({ | |||||
return false; | return false; | ||||
}, | }, | ||||
focus_on_next_field: function(start_idx, fields) { | |||||
focus_on_next_field: function (start_idx, fields) { | |||||
// loop to find next eligible fields | // loop to find next eligible fields | ||||
for (var i= start_idx + 1, len = fields.length; i < len; i++) { | |||||
for (var i = start_idx + 1, len = fields.length; i < len; i++) { | |||||
var field = fields[i]; | var field = fields[i]; | ||||
if (this.is_visible(field)) { | if (this.is_visible(field)) { | ||||
if (field.df.fieldtype==="Table") { | |||||
if (field.df.fieldtype === "Table") { | |||||
// open table grid | // open table grid | ||||
if (!(field.grid.grid_rows && field.grid.grid_rows.length)) { | if (!(field.grid.grid_rows && field.grid.grid_rows.length)) { | ||||
// empty grid, add a new row | // empty grid, add a new row | ||||
@@ -427,10 +428,10 @@ frappe.ui.form.Layout = Class.extend({ | |||||
} | } | ||||
} | } | ||||
}, | }, | ||||
is_visible: function(field) { | |||||
return field.disp_status==="Write" && (field.$wrapper && field.$wrapper.is(":visible")); | |||||
is_visible: function (field) { | |||||
return field.disp_status === "Write" && (field.$wrapper && field.$wrapper.is(":visible")); | |||||
}, | }, | ||||
set_focus: function(field) { | |||||
set_focus: function (field) { | |||||
// next is table, show the table | // next is table, show the table | ||||
if (field.df.fieldtype=="Table") { | if (field.df.fieldtype=="Table") { | ||||
if (!field.grid.grid_rows.length) { | if (!field.grid.grid_rows.length) { | ||||
@@ -444,10 +445,10 @@ frappe.ui.form.Layout = Class.extend({ | |||||
field.$input.focus(); | field.$input.focus(); | ||||
} | } | ||||
}, | }, | ||||
get_open_grid_row: function() { | |||||
get_open_grid_row: function () { | |||||
return $(".grid-row-open").data("grid_row"); | return $(".grid-row-open").data("grid_row"); | ||||
}, | }, | ||||
refresh_dependency: function() { | |||||
refresh_dependency: function () { | |||||
// Resolve "depends_on" and show / hide accordingly | // Resolve "depends_on" and show / hide accordingly | ||||
var me = this; | var me = this; | ||||
@@ -465,7 +466,7 @@ frappe.ui.form.Layout = Class.extend({ | |||||
if (!has_dep) return; | if (!has_dep) return; | ||||
// show / hide based on values | // show / hide based on values | ||||
for (var i=me.fields_list.length-1;i>=0;i--) { | |||||
for (var i = me.fields_list.length - 1; i >= 0; i--) { | |||||
var f = me.fields_list[i]; | var f = me.fields_list[i]; | ||||
f.guardian_has_value = true; | f.guardian_has_value = true; | ||||
if (f.df.depends_on) { | if (f.df.depends_on) { | ||||
@@ -498,14 +499,14 @@ frappe.ui.form.Layout = Class.extend({ | |||||
this.refresh_section_count(); | 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 set_property = this.evaluate_depends_on_value(condition); | ||||
let value = set_property ? 1 : 0; | let value = set_property ? 1 : 0; | ||||
let form_obj; | let form_obj; | ||||
if (this.frm) { | if (this.frm) { | ||||
form_obj = this.frm; | form_obj = this.frm; | ||||
} else if (this.is_dialog) { | |||||
} else if (this.is_dialog || this.doctype === 'Web Form') { | |||||
form_obj = this; | form_obj = this; | ||||
} | } | ||||
if (form_obj) { | if (form_obj) { | ||||
@@ -513,12 +514,14 @@ frappe.ui.form.Layout = Class.extend({ | |||||
form_obj.setting_dependency = true; | form_obj.setting_dependency = true; | ||||
form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname); | form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname); | ||||
form_obj.setting_dependency = false; | form_obj.setting_dependency = false; | ||||
// refresh child fields | |||||
this.fields_dict[fieldname] && this.fields_dict[fieldname].refresh(); | |||||
} else { | } else { | ||||
form_obj.set_df_property(fieldname, property, value); | form_obj.set_df_property(fieldname, property, value); | ||||
} | } | ||||
} | } | ||||
}, | }, | ||||
evaluate_depends_on_value: function(expression) { | |||||
evaluate_depends_on_value: function (expression) { | |||||
var out = null; | var out = null; | ||||
var doc = this.doc; | var doc = this.doc; | ||||
@@ -544,7 +547,7 @@ frappe.ui.form.Layout = Class.extend({ | |||||
if (parent && parent.istable && expression.includes('is_submittable')) { | if (parent && parent.istable && expression.includes('is_submittable')) { | ||||
out = true; | out = true; | ||||
} | } | ||||
} catch(e) { | |||||
} catch (e) { | |||||
frappe.throw(__('Invalid "depends_on" expression')); | frappe.throw(__('Invalid "depends_on" expression')); | ||||
} | } | ||||
@@ -640,7 +643,7 @@ frappe.ui.form.Section = Class.extend({ | |||||
this.wrapper.toggleClass("hide-control", !!hide); | this.wrapper.toggleClass("hide-control", !!hide); | ||||
}, | }, | ||||
collapse: function(hide) { | |||||
collapse: function (hide) { | |||||
// unknown edge case | // unknown edge case | ||||
if (!(this.head && this.body)) { | if (!(this.head && this.body)) { | ||||
return; | return; | ||||
@@ -659,7 +662,7 @@ frappe.ui.form.Section = Class.extend({ | |||||
// refresh signature fields | // refresh signature fields | ||||
this.fields_list.forEach((f) => { | this.fields_list.forEach((f) => { | ||||
if (f.df.fieldtype=='Signature') { | |||||
if (f.df.fieldtype == 'Signature') { | |||||
f.refresh(); | f.refresh(); | ||||
} | } | ||||
}); | }); | ||||
@@ -669,11 +672,11 @@ frappe.ui.form.Section = Class.extend({ | |||||
return this.body.hasClass('hide'); | return this.body.hasClass('hide'); | ||||
}, | }, | ||||
has_missing_mandatory: function() { | |||||
has_missing_mandatory: function () { | |||||
var missing_mandatory = false; | 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; | 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; | missing_mandatory = true; | ||||
break; | break; | ||||
} | } | ||||
@@ -691,13 +694,13 @@ frappe.ui.form.Column = Class.extend({ | |||||
this.make(); | this.make(); | ||||
this.resize_all_columns(); | this.resize_all_columns(); | ||||
}, | }, | ||||
make: function() { | |||||
make: function () { | |||||
this.wrapper = $('<div class="form-column">\ | this.wrapper = $('<div class="form-column">\ | ||||
<form>\ | <form>\ | ||||
</form>\ | </form>\ | ||||
</div>').appendTo(this.section.body) | </div>').appendTo(this.section.body) | ||||
.find("form") | .find("form") | ||||
.on("submit", function() { | |||||
.on("submit", function () { | |||||
return false; | return false; | ||||
}); | }); | ||||
@@ -706,7 +709,7 @@ frappe.ui.form.Column = Class.extend({ | |||||
+ '</label>').appendTo(this.wrapper); | + '</label>').appendTo(this.wrapper); | ||||
} | } | ||||
}, | }, | ||||
resize_all_columns: function() { | |||||
resize_all_columns: function () { | |||||
// distribute all columns equally | // distribute all columns equally | ||||
var colspan = cint(12 / this.section.wrapper.find(".form-column").length); | 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); | .addClass("col-sm-" + colspan); | ||||
}, | }, | ||||
refresh: function() { | |||||
refresh: function () { | |||||
this.section.refresh(); | this.section.refresh(); | ||||
} | } | ||||
}); | }); |
@@ -226,7 +226,7 @@ frappe.ui.form.Toolbar = class Toolbar { | |||||
this.page.add_action_icon("right", ()=> { | this.page.add_action_icon("right", ()=> { | ||||
this.frm.navigate_records(0); | this.frm.navigate_records(0); | ||||
}, 'next-doc', __("Next")); | }, 'next-doc', __("Next")); | ||||
} | |||||
} | |||||
} | } | ||||
make_menu_items() { | make_menu_items() { | ||||
@@ -470,9 +470,22 @@ frappe.ui.form.Toolbar = class Toolbar { | |||||
me.frm.page.set_view('main'); | me.frm.page.set_view('main'); | ||||
}, 'edit'); | }, 'edit'); | ||||
} else if(status === "Cancel") { | } 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 { | } else { | ||||
var click = { | var click = { | ||||
"Save": function() { | "Save": function() { | ||||
@@ -85,7 +85,7 @@ frappe.ui.form.States = Class.extend({ | |||||
frappe.workflow.get_transitions(this.frm.doc).then(transitions => { | frappe.workflow.get_transitions(this.frm.doc).then(transitions => { | ||||
this.frm.page.clear_actions_menu(); | this.frm.page.clear_actions_menu(); | ||||
transitions.forEach(d => { | 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; | added = true; | ||||
me.frm.page.add_action_item(__(d.action), function() { | me.frm.page.add_action_item(__(d.action), function() { | ||||
// set the workflow_action for use in form scripts | // 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); | |||||
}); | }); | ||||
}, | }, | ||||
@@ -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; | |||||
} | |||||
$('<li><a href="#' + route + '">' + | |||||
__(r.title || r.name) + '</a></li>').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() { | 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() { | get_stats() { | ||||
var me = this; | var me = this; | ||||
frappe.call({ | frappe.call({ | ||||
@@ -135,8 +135,8 @@ $.extend(frappe.model, { | |||||
let cached_timestamp = null; | let cached_timestamp = null; | ||||
let cached_doc = 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) { | if (cached_docs) { | ||||
cached_doc = cached_docs.filter(doc => doc.name === doctype)[0]; | cached_doc = cached_docs.filter(doc => doc.name === doctype)[0]; | ||||
if(cached_doc) { | if(cached_doc) { | ||||
@@ -252,6 +252,10 @@ $.extend(frappe.model, { | |||||
return frappe.boot.user.can_create.indexOf(doctype)!==-1; | 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) { | can_read: function(doctype) { | ||||
return frappe.boot.user.can_read.indexOf(doctype)!==-1; | return frappe.boot.user.can_read.indexOf(doctype)!==-1; | ||||
}, | }, | ||||
@@ -527,7 +527,7 @@ frappe.ui.filter_utils = { | |||||
['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype) | ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype) | ||||
) { | ) { | ||||
df.fieldtype = 'Select'; | 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') { | if (condition === 'is') { | ||||
df.fieldtype = 'Select'; | df.fieldtype = 'Select'; | ||||
@@ -542,7 +542,6 @@ frappe.ui.filter_utils = { | |||||
get_timespan_options(periods) { | get_timespan_options(periods) { | ||||
const period_map = { | const period_map = { | ||||
Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'], | Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'], | ||||
Today: null, | |||||
This: ['Week', 'Month', 'Quarter', 'Year'], | This: ['Week', 'Month', 'Quarter', 'Year'], | ||||
Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'], | Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'], | ||||
}; | }; | ||||
@@ -176,7 +176,7 @@ window.replace_all = function (s, t1, t2) { | |||||
return s.split(t1).join(t2); | return s.split(t1).join(t2); | ||||
} | } | ||||
window.strip_html = function (txt) { | |||||
window.strip_html = function(txt) { | |||||
return cstr(txt).replace(/<[^>]*>/g, ""); | return cstr(txt).replace(/<[^>]*>/g, ""); | ||||
} | } | ||||
@@ -1220,6 +1220,7 @@ Object.assign(frappe.utils, { | |||||
if (Math.floor(number) === number) return 0; | if (Math.floor(number) === number) return 0; | ||||
return number.toString().split(".")[1].length || 0; | return number.toString().split(".")[1].length || 0; | ||||
}, | }, | ||||
build_summary_item(summary) { | build_summary_item(summary) { | ||||
if (summary.type == "separator") { | if (summary.type == "separator") { | ||||
return $(`<div class="summary-separator"> | return $(`<div class="summary-separator"> | ||||
@@ -1242,6 +1243,7 @@ Object.assign(frappe.utils, { | |||||
<div class="summary-value ${color}">${value}</div> | <div class="summary-value ${color}">${value}</div> | ||||
</div>`); | </div>`); | ||||
}, | }, | ||||
get_names_for_mentions() { | get_names_for_mentions() { | ||||
let names_for_mentions = Object.keys(frappe.boot.user_info || []) | let names_for_mentions = Object.keys(frappe.boot.user_info || []) | ||||
.filter(user => { | .filter(user => { | ||||
@@ -112,7 +112,6 @@ frappe.views.CommunicationComposer = Class.extend({ | |||||
{ | { | ||||
label: __("Message"), | label: __("Message"), | ||||
fieldtype: "Text Editor", | fieldtype: "Text Editor", | ||||
reqd: 1, | |||||
fieldname: "content", | fieldname: "content", | ||||
onchange: frappe.utils.debounce( | onchange: frappe.utils.debounce( | ||||
this.save_as_draft.bind(this), | this.save_as_draft.bind(this), | ||||
@@ -124,7 +123,7 @@ frappe.views.CommunicationComposer = Class.extend({ | |||||
label: __("Send me a copy"), | label: __("Send me a copy"), | ||||
fieldtype: "Check", | fieldtype: "Check", | ||||
fieldname: "send_me_a_copy", | fieldname: "send_me_a_copy", | ||||
default: 1 | |||||
default: 1 // frappe.boot.user.send_me_a_copy | |||||
}, | }, | ||||
{ | { | ||||
label: __("Send Read Receipt"), | label: __("Send Read Receipt"), | ||||
@@ -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('<div></div>'); | |||||
} | |||||
render_map_view() { | |||||
this.map_id = frappe.dom.get_unique_id(); | |||||
this.$result.html(`<div id="${this.map_id}" class="map-view-container"></div>`); | |||||
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" | |||||
]; | |||||
} | |||||
}; |
@@ -708,6 +708,32 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||||
super.build_fields(); | 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() { | get_fields() { | ||||
let fields = this.fields.map(f => { | let fields = this.fields.map(f => { | ||||
let column_name = frappe.model.get_full_column_name(f[0], f[1]); | 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.fields.map(f => this.add_currency_column(f[0], f[1])); | ||||
this.reorder_fields(); | |||||
this.build_fields(); | this.build_fields(); | ||||
this.setup_columns(); | this.setup_columns(); | ||||
@@ -94,17 +94,17 @@ frappe.views.TreeView = Class.extend({ | |||||
var me = this; | var me = this; | ||||
this.opts.onload && this.opts.onload(me); | this.opts.onload && this.opts.onload(me); | ||||
}, | }, | ||||
make_filters: function(){ | |||||
make_filters: function() { | |||||
var me = this; | var me = this; | ||||
frappe.treeview_settings.filters = [] | frappe.treeview_settings.filters = [] | ||||
$.each(this.opts.filters || [], function(i, filter) { | $.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.change = function() { | ||||
filter.on_change && filter.on_change(); | |||||
filter.onchange && filter.onchange(); | |||||
var val = this.get_value(); | var val = this.get_value(); | ||||
me.args[filter.fieldname] = val; | me.args[filter.fieldname] = val; | ||||
if (val) { | if (val) { | ||||
@@ -114,7 +114,7 @@ frappe.views.TreeView = Class.extend({ | |||||
} | } | ||||
me.set_title(); | me.set_title(); | ||||
me.make_tree(); | me.make_tree(); | ||||
} | |||||
}; | |||||
} | } | ||||
me.page.add_field(filter); | me.page.add_field(filter); | ||||
@@ -122,7 +122,7 @@ frappe.views.TreeView = Class.extend({ | |||||
if (filter.default) { | if (filter.default) { | ||||
$("[data-fieldname='"+filter.fieldname+"']").trigger("change"); | $("[data-fieldname='"+filter.fieldname+"']").trigger("change"); | ||||
} | } | ||||
}) | |||||
}); | |||||
}, | }, | ||||
get_root: function() { | get_root: function() { | ||||
var me = this; | var me = this; | ||||
@@ -85,6 +85,7 @@ frappe.ready(function() { | |||||
function setup_fields(form_data) { | function setup_fields(form_data) { | ||||
form_data.web_form.web_form_fields.map(df => { | form_data.web_form.web_form_fields.map(df => { | ||||
df.is_web_form = true; | |||||
if (df.fieldtype === "Table") { | if (df.fieldtype === "Table") { | ||||
df.get_data = () => { | df.get_data = () => { | ||||
let data = []; | let data = []; | ||||
@@ -99,14 +100,13 @@ frappe.ready(function() { | |||||
if (field.fieldtype === "Link") { | if (field.fieldtype === "Link") { | ||||
field.only_select = true; | field.only_select = true; | ||||
} | } | ||||
field.is_web_form = true; | |||||
}); | }); | ||||
if (df.fieldtype === "Attach") { | if (df.fieldtype === "Attach") { | ||||
df.is_private = true; | df.is_private = true; | ||||
} | } | ||||
df.is_web_form = true; | |||||
delete df.parent; | delete df.parent; | ||||
delete df.parentfield; | delete df.parentfield; | ||||
delete df.parenttype; | delete df.parenttype; | ||||
@@ -29,11 +29,11 @@ | |||||
} | } | ||||
.hero.align-center { | .hero.align-center { | ||||
h1, .hero-subtitle, .hero-buttons { | |||||
h1, .hero-title, .hero-subtitle, .hero-buttons { | |||||
text-align: center; | text-align: center; | ||||
} | } | ||||
.hero-subtitle { | |||||
.hero-title, .hero-subtitle { | |||||
margin-left: auto; | margin-left: auto; | ||||
margin-right: auto; | margin-right: auto; | ||||
} | } | ||||
@@ -3,6 +3,7 @@ | |||||
<nav aria-label="breadcrumb"> | <nav aria-label="breadcrumb"> | ||||
<ol class="breadcrumb" itemscope itemtype="http://schema.org/BreadcrumbList"> | <ol class="breadcrumb" itemscope itemtype="http://schema.org/BreadcrumbList"> | ||||
{%- set parents = parents[-3:] %} | {%- set parents = parents[-3:] %} | ||||
{% set count = (parents | length) + 1 %} | |||||
{% for parent in parents %} | {% for parent in parents %} | ||||
<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem" class="breadcrumb-item"> | <li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem" class="breadcrumb-item"> | ||||
<a itemprop="item" href="{{ url_prefix }}{{ parent.route | abs_url }}" itemprop="url"> | <a itemprop="item" href="{{ url_prefix }}{{ parent.route | abs_url }}" itemprop="url"> | ||||
@@ -11,8 +12,11 @@ | |||||
</a> | </a> | ||||
</li> | </li> | ||||
{% endfor %} | {% endfor %} | ||||
<li class="breadcrumb-item active" aria-current="page"> | |||||
{{ title or "" }} | |||||
<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem" class="breadcrumb-item active" aria-current="page"> | |||||
<span itemprop="item"> | |||||
<span itemprop="name">{{ title }}</span> | |||||
<meta itemprop="position" content="{{ count }}"/> | |||||
</span> | |||||
</li> | </li> | ||||
</ol> | </ol> | ||||
</nav> | </nav> | ||||
@@ -5,8 +5,11 @@ | |||||
<div>{{ frappe.render_template(df.options, {"doc": doc}) or "" }}</div> | <div>{{ frappe.render_template(df.options, {"doc": doc}) or "" }}</div> | ||||
{%- elif df.fieldtype in ("Text", "Text Editor", "Code", "Long Text") -%} | {%- elif df.fieldtype in ("Text", "Text Editor", "Code", "Long Text") -%} | ||||
{{ render_text_field(df, doc) }} | {{ 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) }} | {{ render_image(df, doc) }} | ||||
{%- elif df.fieldtype=="Geolocation" -%} | {%- elif df.fieldtype=="Geolocation" -%} | ||||
{{ render_geolocation(df, doc) }} | {{ render_geolocation(df, doc) }} | ||||
@@ -137,15 +140,14 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" | |||||
style="width: 12px; height: 12px; margin-top: 5px;"> | style="width: 12px; height: 12px; margin-top: 5px;"> | ||||
<path d="M2 9.66667L5.33333 13L14 3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | <path d="M2 9.66667L5.33333 13L14 3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | ||||
</svg> | </svg> | ||||
{% elif df.fieldtype=="Image" %} | |||||
{% elif df.fieldtype in ("Image", "Attach Image") %} | |||||
<img src="{{ doc[doc.meta.get_field(df.fieldname).options] }}" | <img src="{{ doc[doc.meta.get_field(df.fieldname).options] }}" | ||||
class="img-responsive" | class="img-responsive" | ||||
{%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}> | {%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}> | ||||
{% elif df.fieldtype=="Signature" %} | {% elif df.fieldtype=="Signature" %} | ||||
<img src="{{ doc[df.fieldname] }}" class="signature-img img-responsive" | <img src="{{ doc[df.fieldname] }}" class="signature-img img-responsive" | ||||
{%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}> | {%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}> | ||||
{% 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]) %} | |||||
<img src="{{ doc[df.fieldname] }}" class="img-responsive" | <img src="{{ doc[df.fieldname] }}" class="img-responsive" | ||||
{%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}> | {%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}> | ||||
{% elif df.fieldtype=="HTML" %} | {% elif df.fieldtype=="HTML" %} | ||||
@@ -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) |
@@ -5,6 +5,7 @@ from __future__ import unicode_literals | |||||
import unittest | import unittest | ||||
import frappe | import frappe | ||||
from frappe.desk.doctype.todo.todo import ToDo | from frappe.desk.doctype.todo.todo import ToDo | ||||
from frappe.cache_manager import clear_controller_cache | |||||
class TestHooks(unittest.TestCase): | class TestHooks(unittest.TestCase): | ||||
def test_hooks(self): | def test_hooks(self): | ||||
@@ -17,21 +18,20 @@ class TestHooks(unittest.TestCase): | |||||
hooks.get("doc_events").get("*").get("on_update")) | hooks.get("doc_events").get("*").get("on_update")) | ||||
def test_override_doctype_class(self): | 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') | todo = frappe.get_doc(doctype='ToDo', description='asdf') | ||||
self.assertTrue(isinstance(todo, CustomToDo)) | self.assertTrue(isinstance(todo, CustomToDo)) | ||||
# restore | |||||
frappe.get_hooks = original | |||||
class CustomToDo(ToDo): | class CustomToDo(ToDo): | ||||
pass | pass |
@@ -9,7 +9,7 @@ import frappe.defaults | |||||
import unittest | import unittest | ||||
import frappe.model.meta | import frappe.model.meta | ||||
from frappe.permissions import (add_user_permission, remove_user_permission, | 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.core.page.permission_manager.permission_manager import update, reset | ||||
from frappe.test_runner import make_test_records_for_doctype | from frappe.test_runner import make_test_records_for_doctype | ||||
from frappe.core.doctype.user_permission.user_permission import clear_user_permissions | 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") | post = frappe.get_doc("Blog Post", "-test-blog-post") | ||||
self.assertTrue(post.has_permission("read")) | 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): | def test_user_permissions_in_doc(self): | ||||
add_user_permission("Blog Category", "-test-blog-category-1", | add_user_permission("Blog Category", "-test-blog-category-1", | ||||
"test2@example.com") | "test2@example.com") | ||||
@@ -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() |
@@ -95,6 +95,24 @@ def create_doctype(name, fields): | |||||
"name": name | "name": name | ||||
}).insert() | }).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() | @frappe.whitelist() | ||||
def create_contact_records(): | def create_contact_records(): | ||||
if frappe.db.get_all('Contact', {'first_name': 'Test Form Contact 1'}): | if frappe.db.get_all('Contact', {'first_name': 'Test Form Contact 1'}): | ||||
@@ -190,7 +190,7 @@ def get_full_dict(lang): | |||||
frappe.local.lang_full_dict = load_lang(lang) | frappe.local.lang_full_dict = load_lang(lang) | ||||
try: | try: | ||||
# get user specific transaltion data | |||||
# get user specific translation data | |||||
user_translations = get_user_translations(lang) | user_translations = get_user_translations(lang) | ||||
frappe.local.lang_full_dict.update(user_translations) | frappe.local.lang_full_dict.update(user_translations) | ||||
except Exception: | except Exception: | ||||
@@ -1577,7 +1577,7 @@ Monospace,Monospace, | |||||
More articles on {0},Weitere Artikel zum {0}, | 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., | More content for the bottom of the page.,Zusätzlicher Inhalt für den unteren Teil der Seite., | ||||
Most Used,Am Meisten verwendet, | Most Used,Am Meisten verwendet, | ||||
Move To,Ziehen nach, | |||||
Move To,Bewegen nach, | |||||
Move To Trash,In den Papierkorb verschieben, | Move To Trash,In den Papierkorb verschieben, | ||||
Move to Row Number,Gehe zu Zeilennummer, | Move to Row Number,Gehe zu Zeilennummer, | ||||
Mr,Hr., | Mr,Hr., | ||||
@@ -445,25 +445,29 @@ def get_weekday(datetime=None): | |||||
return weekdays[datetime.weekday()] | return weekdays[datetime.weekday()] | ||||
def get_timespan_date_range(timespan): | def get_timespan_date_range(timespan): | ||||
today = nowdate() | |||||
date_range_map = { | 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"): | def global_date_format(date, format="long"): | ||||
"""returns localized date in the form of January 1, 2012""" | """returns localized date in the form of January 1, 2012""" | ||||
@@ -22,6 +22,7 @@ class UserPermissions: | |||||
self.all_read = [] | self.all_read = [] | ||||
self.can_create = [] | self.can_create = [] | ||||
self.can_select = [] | |||||
self.can_read = [] | self.can_read = [] | ||||
self.can_write = [] | self.can_write = [] | ||||
self.can_cancel = [] | self.can_cancel = [] | ||||
@@ -104,6 +105,9 @@ class UserPermissions: | |||||
if not p.get("read") and (dt in user_shared): | if not p.get("read") and (dt in user_shared): | ||||
p["read"] = 1 | p["read"] = 1 | ||||
if p.get('select'): | |||||
self.can_select.append(dt) | |||||
if not dtp.get('istable'): | if not dtp.get('istable'): | ||||
if p.get('create') and not dtp.get('issingle'): | if p.get('create') and not dtp.get('issingle'): | ||||
if dtp.get('in_create'): | if dtp.get('in_create'): | ||||
@@ -193,9 +197,8 @@ class UserPermissions: | |||||
d.name = self.name | d.name = self.name | ||||
d.roles = self.get_roles() | d.roles = self.get_roles() | ||||
d.defaults = self.get_defaults() | 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", | "in_create", "can_export", "can_import", "can_print", "can_email", | ||||
"can_set_user_permissions"): | "can_set_user_permissions"): | ||||
d[key] = list(set(getattr(self, key))) | d[key] = list(set(getattr(self, key))) | ||||
@@ -21,7 +21,7 @@ | |||||
{%- if post.featured -%} | {%- if post.featured -%} | ||||
<h5 class="mt-1"><span class="text-dark">{{ post.title }}</span></h5> | <h5 class="mt-1"><span class="text-dark">{{ post.title }}</span></h5> | ||||
{%- else -%} | {%- else -%} | ||||
<h5 class="mt-1"><span class="text-dark">{{ post.title }}</span></h3> | |||||
<h5 class="mt-1"><span class="text-dark">{{ post.title }}</span></h5> | |||||
{%- endif -%} | {%- endif -%} | ||||
<p class="post-description text-muted">{{ post.intro }}</p> | <p class="post-description text-muted">{{ post.intro }}</p> | ||||
</div> | </div> | ||||
@@ -38,4 +38,4 @@ | |||||
</div> | </div> | ||||
<a class="stretched-link" href="/{{ post.route }}"></a> | <a class="stretched-link" href="/{{ post.route }}"></a> | ||||
</div> | </div> | ||||
</div> | |||||
</div> |
@@ -18,7 +18,7 @@ $('.dropdown-menu a.dropdown-toggle').on('click', function (e) { | |||||
return false; | return false; | ||||
}); | }); | ||||
frappe.get_modal = function(title, content) { | |||||
frappe.get_modal = function (title, content) { | |||||
return $( | return $( | ||||
`<div class="modal" tabindex="-1" role="dialog"> | `<div class="modal" tabindex="-1" role="dialog"> | ||||
<div class="modal-dialog" role="document"> | <div class="modal-dialog" role="document"> | ||||
@@ -33,6 +33,10 @@ frappe.get_modal = function(title, content) { | |||||
${content} | ${content} | ||||
</div> | </div> | ||||
<div class="modal-footer hidden"> | <div class="modal-footer hidden"> | ||||
<button type="button" class="btn btn-default btn-sm btn-modal-close" data-dismiss="modal"> | |||||
<i class="octicon octicon-x visible-xs" style="padding: 1px 0px;"></i> | |||||
<span class="hidden-xs">${__("Close")}</span> | |||||
</button> | |||||
<button type="button" class="btn btn-sm btn-primary hidden"></button> | <button type="button" class="btn btn-sm btn-primary hidden"></button> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -5,9 +5,7 @@ | |||||
</div> | </div> | ||||
{% endif %} | {% endif %} | ||||
<div class="testimonial-content"> | <div class="testimonial-content"> | ||||
<span>“</span> | |||||
{{ content }} | |||||
<span>”</span> | |||||
“{{ content }}” | |||||
</div> | </div> | ||||
<div class="testimonial-by"> | <div class="testimonial-by"> | ||||
{{ name }} | {{ name }} | ||||
@@ -46,7 +46,7 @@ | |||||
"redis": "^2.8.0", | "redis": "^2.8.0", | ||||
"showdown": "^1.9.1", | "showdown": "^1.9.1", | ||||
"snyk": "^1.425.4", | "snyk": "^1.425.4", | ||||
"socket.io": "^2.3.0", | |||||
"socket.io": "^2.4.0", | |||||
"superagent": "^3.8.2", | "superagent": "^3.8.2", | ||||
"touch": "^3.1.0", | "touch": "^3.1.0", | ||||
"vue": "^2.6.11", | "vue": "^2.6.11", | ||||
@@ -693,13 +693,6 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: | |||||
dependencies: | dependencies: | ||||
tweetnacl "^0.14.3" | 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: | big.js@^3.1.3: | ||||
version "3.2.0" | version "3.2.0" | ||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" | resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" | ||||
@@ -940,11 +933,6 @@ caller-path@^2.0.0: | |||||
dependencies: | dependencies: | ||||
caller-callsite "^2.0.0" | 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: | callsites@^2.0.0: | ||||
version "2.0.0" | version "2.0.0" | ||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" | 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" | resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" | ||||
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= | 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: | component-inherit@0.0.3: | ||||
version "0.0.3" | version "0.0.3" | ||||
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" | 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" | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" | ||||
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= | 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: | cookie@0.4.0, cookie@^0.4.0: | ||||
version "0.4.0" | version "0.4.0" | ||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" | ||||
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== | 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: | cookiejar@^2.1.0: | ||||
version "2.1.2" | version "2.1.2" | ||||
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" | 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" | resolved "https://registry.yarnpkg.com/endian-reader/-/endian-reader-0.3.0.tgz#84eca436b80aed0d0639c47291338b932efe50a0" | ||||
integrity sha1-hOykNrgK7Q0GOcRykTOLky7+UKA= | 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: | dependencies: | ||||
component-emitter "1.2.1" | |||||
component-emitter "~1.3.0" | |||||
component-inherit "0.0.3" | component-inherit "0.0.3" | ||||
debug "~4.1.0" | |||||
debug "~3.1.0" | |||||
engine.io-parser "~2.2.0" | engine.io-parser "~2.2.0" | ||||
has-cors "1.1.0" | has-cors "1.1.0" | ||||
indexof "0.0.1" | 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" | xmlhttprequest-ssl "~1.5.4" | ||||
yeast "0.1.2" | yeast "0.1.2" | ||||
@@ -2010,17 +2003,17 @@ engine.io-parser@~2.2.0: | |||||
blob "0.0.5" | blob "0.0.5" | ||||
has-binary2 "~1.0.2" | 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: | dependencies: | ||||
accepts "~1.3.4" | accepts "~1.3.4" | ||||
base64id "2.0.0" | base64id "2.0.0" | ||||
cookie "0.3.1" | |||||
cookie "~0.4.1" | |||||
debug "~4.1.0" | debug "~4.1.0" | ||||
engine.io-parser "~2.2.0" | engine.io-parser "~2.2.0" | ||||
ws "^7.1.2" | |||||
ws "~7.4.2" | |||||
entities@^1.1.1: | entities@^1.1.1: | ||||
version "1.1.2" | 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" | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" | ||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= | 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: | object-copy@^0.1.0: | ||||
version "0.1.0" | version "0.1.0" | ||||
resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" | 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" | resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" | ||||
integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= | 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: | parseurl@~1.3.3: | ||||
version "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" | resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b" | ||||
integrity sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs= | 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: | dependencies: | ||||
backo2 "1.0.2" | backo2 "1.0.2" | ||||
base64-arraybuffer "0.1.5" | |||||
component-bind "1.0.0" | 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-binary2 "~1.0.2" | ||||
has-cors "1.1.0" | |||||
indexof "0.0.1" | 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" | socket.io-parser "~3.3.0" | ||||
to-array "0.1.4" | to-array "0.1.4" | ||||
@@ -6846,16 +6827,16 @@ socket.io-parser@~3.4.0: | |||||
debug "~4.1.0" | debug "~4.1.0" | ||||
isarray "2.0.1" | 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: | dependencies: | ||||
debug "~4.1.0" | debug "~4.1.0" | ||||
engine.io "~3.4.0" | |||||
engine.io "~3.5.0" | |||||
has-binary2 "~1.0.2" | has-binary2 "~1.0.2" | ||||
socket.io-adapter "~1.1.0" | socket.io-adapter "~1.1.0" | ||||
socket.io-client "2.3.0" | |||||
socket.io-client "2.4.0" | |||||
socket.io-parser "~3.4.0" | socket.io-parser "~3.4.0" | ||||
socks-proxy-agent@^4.0.1: | socks-proxy-agent@^4.0.1: | ||||
@@ -7970,17 +7951,10 @@ write-file-atomic@^3.0.0: | |||||
signal-exit "^3.0.2" | signal-exit "^3.0.2" | ||||
typedarray-to-buffer "^3.1.5" | 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: | xdg-basedir@^4.0.0: | ||||
version "4.0.0" | version "4.0.0" | ||||