@@ -15,7 +15,7 @@ If your issue is not clear or does not meet the guidelines, then it will be clos | |||||
### General Issue Guidelines | ### General Issue Guidelines | ||||
1. **Search existing Issues:** Before raising a Issue, search if it has been raised before. Maybe add a 👍 or give additional help by creating a mockup if it is not already created. | 1. **Search existing Issues:** Before raising a Issue, search if it has been raised before. Maybe add a 👍 or give additional help by creating a mockup if it is not already created. | ||||
2. **Report each issue separately:** Don't club multiple, unreleated issues in one note. | |||||
2. **Report each issue separately:** Don't club multiple, unrelated issues in one note. | |||||
3. **Brief:** Please don't include long explanations. Use screenshots and bullet points instead of descriptive paragraphs. | 3. **Brief:** Please don't include long explanations. Use screenshots and bullet points instead of descriptive paragraphs. | ||||
### Bug Report Guidelines | ### Bug Report Guidelines | ||||
@@ -31,12 +31,12 @@ matrix: | |||||
- name: "Python 3.7 MariaDB" | - name: "Python 3.7 MariaDB" | ||||
python: 3.7 | python: 3.7 | ||||
env: DB=mariadb TYPE=server | env: DB=mariadb TYPE=server | ||||
script: bench --site test_site run-tests --coverage | |||||
script: bench --verbose --site test_site run-tests --coverage | |||||
- name: "Python 3.7 PostgreSQL" | - name: "Python 3.7 PostgreSQL" | ||||
python: 3.7 | python: 3.7 | ||||
env: DB=postgres TYPE=server | env: DB=postgres TYPE=server | ||||
script: bench --site test_site run-tests --coverage | |||||
script: bench --verbose --site test_site run-tests --coverage | |||||
- name: "Cypress" | - name: "Cypress" | ||||
python: 3.7 | python: 3.7 | ||||
@@ -104,11 +104,11 @@ install: | |||||
- cd ./frappe-bench | - cd ./frappe-bench | ||||
- sed -i 's/watch:/# watch:/g' Procfile | |||||
- sed -i 's/schedule:/# schedule:/g' Procfile | |||||
- sed -i 's/^watch:/# watch:/g' Procfile | |||||
- sed -i 's/^schedule:/# schedule:/g' Procfile | |||||
- if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi | |||||
- if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi | |||||
- if [ $TYPE == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; fi | |||||
- if [ $TYPE == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi | |||||
- if [ $TYPE == "ui" ]; then bench setup requirements --node; fi | - if [ $TYPE == "ui" ]; then bench setup requirements --node; fi | ||||
@@ -3,7 +3,31 @@ context('Depends On', () => { | |||||
cy.login(); | cy.login(); | ||||
cy.visit('/desk#workspace/Website'); | cy.visit('/desk#workspace/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 => { | ||||
@@ -23,11 +23,12 @@ if PY2: | |||||
reload(sys) | reload(sys) | ||||
sys.setdefaultencoding("utf-8") | sys.setdefaultencoding("utf-8") | ||||
__version__ = '13.0.0-beta.9' | |||||
__version__ = '13.0.0-beta.10' | |||||
__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""" | ||||
@@ -149,6 +150,7 @@ def init(site, sites_path=None, new_site=False): | |||||
"new_site": new_site | "new_site": new_site | ||||
}) | }) | ||||
local.rollback_observers = [] | local.rollback_observers = [] | ||||
local.before_commit = [] | |||||
local.test_objects = {} | local.test_objects = {} | ||||
local.site = site | local.site = site | ||||
@@ -327,7 +329,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, | |||||
:param is_minimizable: [optional] Allow users to minimize the modal | :param is_minimizable: [optional] Allow users to minimize the modal | ||||
:param wide: [optional] Show wide modal | :param wide: [optional] Show wide modal | ||||
""" | """ | ||||
from frappe.utils import encode | |||||
from frappe.utils import strip_html_tags | |||||
msg = safe_decode(msg) | msg = safe_decode(msg) | ||||
out = _dict(message=msg) | out = _dict(message=msg) | ||||
@@ -354,7 +356,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, | |||||
out.as_list = 1 | out.as_list = 1 | ||||
if flags.print_messages and out.message: | if flags.print_messages and out.message: | ||||
print(f"Message: {repr(out.message).encode('utf-8')}") | |||||
print(f"Message: {strip_html_tags(out.message)}") | |||||
if title: | if title: | ||||
out.title = title | out.title = title | ||||
@@ -628,6 +630,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. | ||||
@@ -946,7 +963,11 @@ def get_installed_apps(sort=False, frappe_last=False): | |||||
connect() | connect() | ||||
if not local.all_apps: | if not local.all_apps: | ||||
local.all_apps = get_all_apps(True) | |||||
local.all_apps = cache().get_value('all_apps', get_all_apps) | |||||
#cache bench apps | |||||
if not cache().get_value('all_apps'): | |||||
cache().set_value('all_apps', local.all_apps) | |||||
installed = json.loads(db.get_global("installed_apps") or "[]") | installed = json.loads(db.get_global("installed_apps") or "[]") | ||||
@@ -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) { | ||||
@@ -100,10 +102,7 @@ frappe.ui.form.on('Auto Repeat', { | |||||
frappe.auto_repeat.render_schedule = function(frm) { | frappe.auto_repeat.render_schedule = function(frm) { | ||||
if (!frm.is_dirty() && frm.doc.status !== 'Disabled') { | if (!frm.is_dirty() && frm.doc.status !== 'Disabled') { | ||||
frappe.call({ | |||||
method: "get_auto_repeat_schedule", | |||||
doc: frm.doc | |||||
}).done((r) => { | |||||
frm.call("get_auto_repeat_schedule").then(r => { | |||||
frm.dashboard.wrapper.empty(); | frm.dashboard.wrapper.empty(); | ||||
frm.dashboard.add_section( | frm.dashboard.add_section( | ||||
frappe.render_template("auto_repeat_schedule", { | frappe.render_template("auto_repeat_schedule", { | ||||
@@ -23,6 +23,8 @@ | |||||
"repeat_on_last_day", | "repeat_on_last_day", | ||||
"column_break_12", | "column_break_12", | ||||
"next_schedule_date", | "next_schedule_date", | ||||
"section_break_16", | |||||
"repeat_on_days", | |||||
"notification", | "notification", | ||||
"notify_by_email", | "notify_by_email", | ||||
"recipients", | "recipients", | ||||
@@ -189,15 +191,27 @@ | |||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"label": "Repeat on Last Day of the Month" | "label": "Repeat on Last Day of the Month" | ||||
}, | }, | ||||
{ | |||||
"depends_on": "eval:doc.frequency==='Weekly';", | |||||
"fieldname": "repeat_on_days", | |||||
"fieldtype": "Table", | |||||
"label": "Repeat on Days", | |||||
"options": "Auto Repeat Day" | |||||
}, | |||||
{ | { | ||||
"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", | ||||
@@ -5,6 +5,7 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
from datetime import timedelta | |||||
from frappe.desk.form import assign_to | from frappe.desk.form import assign_to | ||||
from frappe.utils.jinja import validate_template | from frappe.utils.jinja import validate_template | ||||
from dateutil.relativedelta import relativedelta | from dateutil.relativedelta import relativedelta | ||||
@@ -13,9 +14,10 @@ from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_ | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.core.doctype.communication.email import make | from frappe.core.doctype.communication.email import make | ||||
from frappe.utils.background_jobs import get_jobs | from frappe.utils.background_jobs import get_jobs | ||||
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated | |||||
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} | month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} | ||||
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} | |||||
class AutoRepeat(Document): | class AutoRepeat(Document): | ||||
def validate(self): | def validate(self): | ||||
@@ -24,6 +26,7 @@ class AutoRepeat(Document): | |||||
self.validate_submit_on_creation() | self.validate_submit_on_creation() | ||||
self.validate_dates() | self.validate_dates() | ||||
self.validate_email_id() | self.validate_email_id() | ||||
self.validate_auto_repeat_days() | |||||
self.set_dates() | self.set_dates() | ||||
self.update_auto_repeat_id() | self.update_auto_repeat_id() | ||||
self.unlink_if_applicable() | self.unlink_if_applicable() | ||||
@@ -49,7 +52,7 @@ class AutoRepeat(Document): | |||||
if self.disabled: | if self.disabled: | ||||
self.next_schedule_date = None | self.next_schedule_date = None | ||||
else: | else: | ||||
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date) | |||||
self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date) | |||||
def unlink_if_applicable(self): | def unlink_if_applicable(self): | ||||
if self.status == 'Completed' or self.disabled: | if self.status == 'Completed' or self.disabled: | ||||
@@ -88,6 +91,12 @@ class AutoRepeat(Document): | |||||
else: | else: | ||||
frappe.throw(_("'Recipients' not specified")) | frappe.throw(_("'Recipients' not specified")) | ||||
def validate_auto_repeat_days(self): | |||||
auto_repeat_days = self.get_auto_repeat_days() | |||||
if not len(set(auto_repeat_days)) == len(auto_repeat_days): | |||||
repeated_days = get_repeated(auto_repeat_days) | |||||
frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days))) | |||||
def update_auto_repeat_id(self): | def update_auto_repeat_id(self): | ||||
#check if document is already on auto repeat | #check if document is already on auto repeat | ||||
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat") | auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat") | ||||
@@ -113,7 +122,7 @@ class AutoRepeat(Document): | |||||
end_date = getdate(self.end_date) | end_date = getdate(self.end_date) | ||||
if not self.end_date: | if not self.end_date: | ||||
next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day) | |||||
next_date = self.get_next_schedule_date(schedule_date=start_date) | |||||
row = { | row = { | ||||
"reference_document": self.reference_document, | "reference_document": self.reference_document, | ||||
"frequency": self.frequency, | "frequency": self.frequency, | ||||
@@ -122,8 +131,7 @@ class AutoRepeat(Document): | |||||
schedule_details.append(row) | schedule_details.append(row) | ||||
if self.end_date: | if self.end_date: | ||||
next_date = get_next_schedule_date( | |||||
start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True) | |||||
next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True) | |||||
while (getdate(next_date) < getdate(end_date)): | while (getdate(next_date) < getdate(end_date)): | ||||
row = { | row = { | ||||
@@ -132,8 +140,7 @@ class AutoRepeat(Document): | |||||
"next_scheduled_date" : next_date | "next_scheduled_date" : next_date | ||||
} | } | ||||
schedule_details.append(row) | schedule_details.append(row) | ||||
next_date = get_next_schedule_date( | |||||
next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True) | |||||
next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True) | |||||
return schedule_details | return schedule_details | ||||
@@ -211,6 +218,75 @@ class AutoRepeat(Document): | |||||
new_doc.set('from_date', from_date) | new_doc.set('from_date', from_date) | ||||
new_doc.set('to_date', to_date) | new_doc.set('to_date', to_date) | ||||
def get_next_schedule_date(self, schedule_date, for_full_schedule=False): | |||||
""" | |||||
Returns the next schedule date for auto repeat after a recurring document has been created. | |||||
Adds required offset to the schedule_date param and returns the next schedule date. | |||||
:param schedule_date: The date when the last recurring document was created. | |||||
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule. | |||||
""" | |||||
if month_map.get(self.frequency): | |||||
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 | |||||
else: | |||||
month_count = 0 | |||||
day_count = 0 | |||||
if month_count and self.repeat_on_last_day: | |||||
day_count = 31 | |||||
next_date = get_next_date(self.start_date, month_count, day_count) | |||||
elif month_count and self.repeat_on_day: | |||||
day_count = self.repeat_on_day | |||||
next_date = get_next_date(self.start_date, month_count, day_count) | |||||
elif month_count: | |||||
next_date = get_next_date(self.start_date, month_count) | |||||
else: | |||||
days = self.get_days(schedule_date) | |||||
next_date = add_days(schedule_date, days) | |||||
# next schedule date should be after or on current date | |||||
if not for_full_schedule: | |||||
while getdate(next_date) < getdate(today()): | |||||
if month_count: | |||||
month_count += month_map.get(self.frequency, 0) | |||||
next_date = get_next_date(self.start_date, month_count, day_count) | |||||
else: | |||||
days = self.get_days(next_date) | |||||
next_date = add_days(next_date, days) | |||||
return next_date | |||||
def get_days(self, schedule_date): | |||||
if self.frequency == "Weekly": | |||||
days = self.get_offset_for_weekly_frequency(schedule_date) | |||||
else: | |||||
# daily frequency | |||||
days = 1 | |||||
return days | |||||
def get_offset_for_weekly_frequency(self, schedule_date): | |||||
# if weekdays are not set, offset is 7 from current schedule date | |||||
if not self.repeat_on_days: | |||||
return 7 | |||||
repeat_on_days = self.get_auto_repeat_days() | |||||
current_schedule_day = getdate(schedule_date).weekday() | |||||
weekdays = list(week_map.keys()) | |||||
# if repeats on more than 1 day or | |||||
# start date's weekday is not in repeat days, then get next weekday | |||||
# else offset is 7 | |||||
if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days: | |||||
weekday = get_next_weekday(current_schedule_day, repeat_on_days) | |||||
next_weekday_number = week_map.get(weekday, 0) | |||||
# offset for upcoming weekday | |||||
return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days | |||||
return 7 | |||||
def get_auto_repeat_days(self): | |||||
return [d.day for d in self.get('repeat_on_days', [])] | |||||
def send_notification(self, new_doc): | def send_notification(self, new_doc): | ||||
"""Notify concerned people about recurring document generation""" | """Notify concerned people about recurring document generation""" | ||||
subject = self.subject or '' | subject = self.subject or '' | ||||
@@ -291,42 +367,24 @@ class AutoRepeat(Document): | |||||
) | ) | ||||
def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False): | |||||
if month_map.get(frequency): | |||||
month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1 | |||||
else: | |||||
month_count = 0 | |||||
day_count = 0 | |||||
if month_count and repeat_on_last_day: | |||||
day_count = 31 | |||||
next_date = get_next_date(start_date, month_count, day_count) | |||||
elif month_count and repeat_on_day: | |||||
day_count = repeat_on_day | |||||
next_date = get_next_date(start_date, month_count, day_count) | |||||
elif month_count: | |||||
next_date = get_next_date(start_date, month_count) | |||||
else: | |||||
days = 7 if frequency == 'Weekly' else 1 | |||||
next_date = add_days(schedule_date, days) | |||||
# next schedule date should be after or on current date | |||||
if not for_full_schedule: | |||||
while getdate(next_date) < getdate(today()): | |||||
if month_count: | |||||
month_count += month_map.get(frequency) | |||||
next_date = get_next_date(start_date, month_count, day_count) | |||||
elif days: | |||||
next_date = add_days(next_date, days) | |||||
return next_date | |||||
def get_next_date(dt, mcount, day=None): | def get_next_date(dt, mcount, day=None): | ||||
dt = getdate(dt) | dt = getdate(dt) | ||||
dt += relativedelta(months=mcount, day=day) | dt += relativedelta(months=mcount, day=day) | ||||
return dt | return dt | ||||
def get_next_weekday(current_schedule_day, weekdays): | |||||
days = list(week_map.keys()) | |||||
if current_schedule_day > 0: | |||||
days = days[(current_schedule_day + 1):] + days[:current_schedule_day] | |||||
else: | |||||
days = days[(current_schedule_day + 1):] | |||||
for entry in days: | |||||
if entry in weekdays: | |||||
return entry | |||||
#called through hooks | #called through hooks | ||||
def make_auto_repeat_entry(): | def make_auto_repeat_entry(): | ||||
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' | enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' | ||||
@@ -337,6 +395,7 @@ def make_auto_repeat_entry(): | |||||
data = get_auto_repeat_entries(date) | data = get_auto_repeat_entries(date) | ||||
frappe.enqueue(enqueued_method, data=data) | frappe.enqueue(enqueued_method, data=data) | ||||
def create_repeated_entries(data): | def create_repeated_entries(data): | ||||
for d in data: | for d in data: | ||||
doc = frappe.get_doc('Auto Repeat', d.name) | doc = frappe.get_doc('Auto Repeat', d.name) | ||||
@@ -346,10 +405,11 @@ def create_repeated_entries(data): | |||||
if schedule_date == current_date and not doc.disabled: | if schedule_date == current_date and not doc.disabled: | ||||
doc.create_documents() | doc.create_documents() | ||||
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date) | |||||
schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date) | |||||
if schedule_date and not doc.disabled: | if schedule_date and not doc.disabled: | ||||
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) | frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) | ||||
def get_auto_repeat_entries(date=None): | def get_auto_repeat_entries(date=None): | ||||
if not date: | if not date: | ||||
date = getdate(today()) | date = getdate(today()) | ||||
@@ -358,6 +418,7 @@ def get_auto_repeat_entries(date=None): | |||||
['status', '=', 'Active'] | ['status', '=', 'Active'] | ||||
]) | ]) | ||||
#called through hooks | #called through hooks | ||||
def set_auto_repeat_as_completed(): | def set_auto_repeat_as_completed(): | ||||
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']}) | auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']}) | ||||
@@ -367,6 +428,7 @@ def set_auto_repeat_as_completed(): | |||||
doc.status = 'Completed' | doc.status = 'Completed' | ||||
doc.save() | doc.save() | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None): | def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None): | ||||
if not start_date: | if not start_date: | ||||
@@ -7,10 +7,9 @@ import unittest | |||||
import frappe | import frappe | ||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field | from frappe.custom.doctype.custom_field.custom_field import create_custom_field | ||||
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries | |||||
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map | |||||
from frappe.utils import today, add_days, getdate, add_months | from frappe.utils import today, add_days, getdate, add_months | ||||
def add_custom_fields(): | def add_custom_fields(): | ||||
df = dict( | df = dict( | ||||
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender', | fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender', | ||||
@@ -42,6 +41,52 @@ class TestAutoRepeat(unittest.TestCase): | |||||
self.assertEqual(todo.get('description'), new_todo.get('description')) | self.assertEqual(todo.get('description'), new_todo.get('description')) | ||||
def test_weekly_auto_repeat(self): | |||||
todo = frappe.get_doc( | |||||
dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert() | |||||
doc = make_auto_repeat(reference_doctype='ToDo', | |||||
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7)) | |||||
self.assertEqual(doc.next_schedule_date, today()) | |||||
data = get_auto_repeat_entries(getdate(today())) | |||||
create_repeated_entries(data) | |||||
frappe.db.commit() | |||||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) | |||||
self.assertEqual(todo.auto_repeat, doc.name) | |||||
new_todo = frappe.db.get_value('ToDo', | |||||
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') | |||||
new_todo = frappe.get_doc('ToDo', new_todo) | |||||
self.assertEqual(todo.get('description'), new_todo.get('description')) | |||||
def test_weekly_auto_repeat_with_weekdays(self): | |||||
todo = frappe.get_doc( | |||||
dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert() | |||||
weekdays = list(week_map.keys()) | |||||
current_weekday = getdate().weekday() | |||||
days = [ | |||||
{'day': weekdays[current_weekday]}, | |||||
{'day': weekdays[(current_weekday + 2) % 7]} | |||||
] | |||||
doc = make_auto_repeat(reference_doctype='ToDo', | |||||
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days) | |||||
self.assertEqual(doc.next_schedule_date, today()) | |||||
data = get_auto_repeat_entries(getdate(today())) | |||||
create_repeated_entries(data) | |||||
frappe.db.commit() | |||||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) | |||||
self.assertEqual(todo.auto_repeat, doc.name) | |||||
doc.reload() | |||||
self.assertEqual(doc.next_schedule_date, add_days(getdate(), 2)) | |||||
def test_monthly_auto_repeat(self): | def test_monthly_auto_repeat(self): | ||||
start_date = today() | start_date = today() | ||||
end_date = add_months(start_date, 12) | end_date = add_months(start_date, 12) | ||||
@@ -144,7 +189,8 @@ def make_auto_repeat(**args): | |||||
'notify_by_email': args.notify or 0, | 'notify_by_email': args.notify or 0, | ||||
'recipients': args.recipients or "", | 'recipients': args.recipients or "", | ||||
'subject': args.subject or "", | 'subject': args.subject or "", | ||||
'message': args.message or "" | |||||
'message': args.message or "", | |||||
'repeat_on_days': args.days or [] | |||||
}).insert(ignore_permissions=True) | }).insert(ignore_permissions=True) | ||||
return doc | return doc | ||||
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"actions": [], | |||||
"creation": "2020-11-10 22:30:53.690228", | |||||
"doctype": "DocType", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"day" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"fieldname": "day", | |||||
"fieldtype": "Select", | |||||
"in_list_view": 1, | |||||
"label": "Day", | |||||
"options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", | |||||
"reqd": 1 | |||||
} | |||||
], | |||||
"index_web_pages_for_search": 1, | |||||
"istable": 1, | |||||
"links": [], | |||||
"modified": "2020-11-10 22:30:53.690228", | |||||
"modified_by": "Administrator", | |||||
"module": "Automation", | |||||
"name": "Auto Repeat Day", | |||||
"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 AutoRepeatDay(Document): | |||||
pass |
@@ -72,6 +72,7 @@ def clear_document_cache(): | |||||
frappe.cache().delete_key("document_cache") | frappe.cache().delete_key("document_cache") | ||||
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): | ||||
@@ -104,6 +105,15 @@ def clear_doctype_cache(doctype=None): | |||||
# Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured | # Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured | ||||
clear_document_cache() | 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' | ||||
@@ -0,0 +1,21 @@ | |||||
### Version 13.0.0 Beta 10 Release Notes | |||||
#### Features and Enhancements | |||||
- Option to hide child records for a nested DocType via User Permissions ([12209](https://github.com/frappe/frappe/pull/12209)) | |||||
- Added option to grant only `Select` access to document ([12063](https://github.com/frappe/frappe/pull/12063)) | |||||
- Introduced map view ([11202](https://github.com/frappe/frappe/pull/11202)) | |||||
- Enabled image rendering from links in Print View ([12101](https://github.com/frappe/frappe/pull/12101)) | |||||
- Introduced "Yesterday" and "Tomorrow" options for Timespan filter ([12179](https://github.com/frappe/frappe/pull/12179)) | |||||
#### Fixes | |||||
- Fixed HTML download of Auto Email Report that used to break in some cases ([12202](https://github.com/frappe/frappe/pull/12202)) | |||||
- Fixed reset customizations functionality ([12152](https://github.com/frappe/frappe/pull/12152)) | |||||
- Fixed the rendering of percentage stat in Dashboard Chart ([12090](https://github.com/frappe/frappe/pull/12090)) | |||||
- Fixed permission issues in Dashboard Chart ([12243](https://github.com/frappe/frappe/pull/12243)) | |||||
- Fixed an issue with grid row index ([12188](https://github.com/frappe/frappe/pull/12188)) | |||||
- Fixed an issue where fields used to get reordered after adding new columns ([12058](https://github.com/frappe/frappe/pull/12058)) | |||||
- Fixed currency formatting in Print Format ([11897](https://github.com/frappe/frappe/pull/11897)) | |||||
- Added a fieldlevel permission check for report data ([12163](https://github.com/frappe/frappe/pull/12163)) | |||||
- Fixed an issue with percent precision ([12010](https://github.com/frappe/frappe/pull/12010)) |
@@ -77,6 +77,11 @@ def get_data(): | |||||
"name": "OAuth Provider Settings", | "name": "OAuth Provider Settings", | ||||
"description": _("Settings for OAuth Provider"), | "description": _("Settings for OAuth Provider"), | ||||
}, | }, | ||||
{ | |||||
"type": "doctype", | |||||
"name": "Connected App", | |||||
"description": _("Connect to any OAuth Provider"), | |||||
}, | |||||
] | ] | ||||
}, | }, | ||||
{ | { | ||||
@@ -150,7 +150,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 | ||||
@@ -290,9 +290,15 @@ class DocType(Document): | |||||
self.update_fields_to_fetch() | self.update_fields_to_fetch() | ||||
from frappe import conf | |||||
allow_doctype_export = frappe.flags.allow_doctype_export or (not frappe.flags.in_test and conf.get('developer_mode')) | |||||
if not self.custom and not frappe.flags.in_import and allow_doctype_export: | |||||
allow_doctype_export = ( | |||||
not self.custom | |||||
and not frappe.flags.in_import | |||||
and ( | |||||
frappe.conf.developer_mode | |||||
or frappe.flags.allow_doctype_export | |||||
) | |||||
) | |||||
if allow_doctype_export: | |||||
self.export_doc() | self.export_doc() | ||||
self.make_controller_template() | self.make_controller_template() | ||||
@@ -382,13 +388,10 @@ class DocType(Document): | |||||
if merge: | if merge: | ||||
frappe.throw(_("DocType can not be merged")) | frappe.throw(_("DocType can not be merged")) | ||||
# Do not rename and move files and folders for custom doctype | |||||
if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch: | |||||
self.rename_files_and_folders(old, new) | |||||
def after_rename(self, old, new, merge=False): | def after_rename(self, old, new, merge=False): | ||||
"""Change table name using `RENAME TABLE` if table exists. Or update | """Change table name using `RENAME TABLE` if table exists. Or update | ||||
`doctype` property for Single type.""" | `doctype` property for Single type.""" | ||||
if self.issingle: | if self.issingle: | ||||
frappe.db.sql("""update tabSingles set doctype=%s where doctype=%s""", (new, old)) | frappe.db.sql("""update tabSingles set doctype=%s where doctype=%s""", (new, old)) | ||||
frappe.db.sql("""update tabSingles set value=%s | frappe.db.sql("""update tabSingles set value=%s | ||||
@@ -398,6 +401,18 @@ class DocType(Document): | |||||
"mariadb": f"RENAME TABLE `tab{old}` TO `tab{new}`", | "mariadb": f"RENAME TABLE `tab{old}` TO `tab{new}`", | ||||
"postgres": f"ALTER TABLE `tab{old}` RENAME TO `tab{new}`" | "postgres": f"ALTER TABLE `tab{old}` RENAME TO `tab{new}`" | ||||
}) | }) | ||||
frappe.db.commit() | |||||
# Do not rename and move files and folders for custom doctype | |||||
if not self.custom: | |||||
if not frappe.flags.in_patch: | |||||
self.rename_files_and_folders(old, new) | |||||
clear_controller_cache(old) | |||||
def after_delete(self): | |||||
if not self.custom: | |||||
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 | ||||
@@ -1000,10 +1015,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"): | ||||
@@ -1026,9 +1041,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: | ||||
@@ -1040,7 +1056,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. | ||||
@@ -43,7 +43,7 @@ class ModuleDef(Document): | |||||
def on_trash(self): | def on_trash(self): | ||||
"""Delete module name from modules.txt""" | """Delete module name from modules.txt""" | ||||
if frappe.flags.in_uninstall or self.custom: | |||||
if not frappe.conf.get('developer_mode') or frappe.flags.in_uninstall or self.custom: | |||||
return | return | ||||
modules = None | modules = None | ||||
@@ -0,0 +1,19 @@ | |||||
// Copyright (c) 2020, Frappe Technologies and contributors | |||||
// For license information, please see license.txt | |||||
frappe.ui.form.on('Module Profile', { | |||||
refresh: function(frm) { | |||||
if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) { | |||||
if (!frm.module_editor && frm.doc.__onload && frm.doc.__onload.all_modules) { | |||||
let module_area = $('<div style="min-height: 300px">') | |||||
.appendTo(frm.fields_dict.module_html.wrapper); | |||||
frm.module_editor = new frappe.ModuleEditor(frm, module_area); | |||||
} | |||||
} | |||||
if (frm.module_editor) { | |||||
frm.module_editor.refresh(); | |||||
} | |||||
} | |||||
}); |
@@ -0,0 +1,60 @@ | |||||
{ | |||||
"actions": [], | |||||
"autoname": "field:module_profile_name", | |||||
"creation": "2020-12-22 22:00:30.614475", | |||||
"doctype": "DocType", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"module_profile_name", | |||||
"module_html", | |||||
"block_modules" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"fieldname": "module_profile_name", | |||||
"fieldtype": "Data", | |||||
"in_list_view": 1, | |||||
"label": "Module Profile Name", | |||||
"reqd": 1, | |||||
"unique": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "module_html", | |||||
"fieldtype": "HTML", | |||||
"label": "Module HTML" | |||||
}, | |||||
{ | |||||
"fieldname": "block_modules", | |||||
"fieldtype": "Table", | |||||
"hidden": 1, | |||||
"label": "Block Modules", | |||||
"options": "Block Module", | |||||
"read_only": 1 | |||||
} | |||||
], | |||||
"index_web_pages_for_search": 1, | |||||
"links": [], | |||||
"modified": "2021-01-03 15:36:52.622696", | |||||
"modified_by": "Administrator", | |||||
"module": "Core", | |||||
"name": "Module Profile", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "System Manager", | |||||
"share": 1, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_changes": 1 | |||||
} |
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2020, Frappe Technologies and contributors | |||||
# For license information, please see license.txt | |||||
from __future__ import unicode_literals | |||||
from frappe.model.document import Document | |||||
class ModuleProfile(Document): | |||||
def onload(self): | |||||
from frappe.config import get_modules_from_all_apps | |||||
self.set_onload('all_modules', | |||||
[m.get("module_name") for m in get_modules_from_all_apps()]) |
@@ -0,0 +1,32 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2020, Frappe Technologies and Contributors | |||||
# See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
import unittest | |||||
class TestModuleProfile(unittest.TestCase): | |||||
def test_make_new_module_profile(self): | |||||
if not frappe.db.get_value('Module Profile', '_Test Module Profile'): | |||||
frappe.get_doc({ | |||||
'doctype': 'Module Profile', | |||||
'module_profile_name': '_Test Module Profile', | |||||
'block_modules': [ | |||||
{'module': 'Accounts'} | |||||
] | |||||
}).insert() | |||||
# add to user and check | |||||
if not frappe.db.get_value('User', 'test-for-module_profile@example.com'): | |||||
new_user = frappe.get_doc({ | |||||
'doctype': 'User', | |||||
'email':'test-for-module_profile@example.com', | |||||
'first_name':'Test User' | |||||
}).insert() | |||||
else: | |||||
new_user = frappe.get_doc('User', 'test-for-module_profile@example.com') | |||||
new_user.module_profile = '_Test Module Profile' | |||||
new_user.save() | |||||
self.assertEqual(new_user.block_modules[0].module, 'Accounts') |
@@ -44,7 +44,7 @@ | |||||
}, | }, | ||||
{ | { | ||||
"fieldname": "options", | "fieldname": "options", | ||||
"fieldtype": "Data", | |||||
"fieldtype": "Small Text", | |||||
"label": "Options" | "label": "Options" | ||||
}, | }, | ||||
{ | { | ||||
@@ -58,7 +58,7 @@ | |||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"istable": 1, | "istable": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2020-08-17 16:15:46.937267", | |||||
"modified": "2020-12-05 19:20:00.503097", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "Report Filter", | "name": "Report Filter", | ||||
@@ -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', | ||||
@@ -81,6 +81,7 @@ class TestServerScript(unittest.TestCase): | |||||
def tearDownClass(cls): | def tearDownClass(cls): | ||||
frappe.db.commit() | frappe.db.commit() | ||||
frappe.db.sql('truncate `tabServer Script`') | frappe.db.sql('truncate `tabServer Script`') | ||||
frappe.cache().delete_key('server_script_map') | |||||
def setUp(self): | def setUp(self): | ||||
frappe.cache().delete_value('server_script_map') | frappe.cache().delete_value('server_script_map') | ||||
@@ -357,7 +357,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.", | ||||
@@ -490,4 +490,4 @@ | |||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "ASC", | "sort_order": "ASC", | ||||
"track_changes": 1 | "track_changes": 1 | ||||
} | |||||
} |
@@ -37,6 +37,25 @@ frappe.ui.form.on('User', { | |||||
} | } | ||||
}, | }, | ||||
module_profile: function(frm) { | |||||
if (frm.doc.module_profile) { | |||||
frappe.call({ | |||||
"method": "frappe.core.doctype.user.user.get_module_profile", | |||||
args: { | |||||
module_profile: frm.doc.module_profile | |||||
}, | |||||
callback: function(data) { | |||||
frm.set_value("block_modules", []); | |||||
$.each(data.message || [], function(i, v) { | |||||
let d = frm.add_child("block_modules"); | |||||
d.module = v.module; | |||||
}); | |||||
frm.module_editor && frm.module_editor.refresh(); | |||||
} | |||||
}); | |||||
} | |||||
}, | |||||
onload: function(frm) { | onload: function(frm) { | ||||
frm.can_edit_roles = has_access_to_edit_user(); | frm.can_edit_roles = has_access_to_edit_user(); | ||||
@@ -255,43 +274,3 @@ function get_roles_for_editing_user() { | |||||
.filter(perm => perm.permlevel >= 1 && perm.write) | .filter(perm => perm.permlevel >= 1 && perm.write) | ||||
.map(perm => perm.role) || ['System Manager']; | .map(perm => perm.role) || ['System Manager']; | ||||
} | } | ||||
frappe.ModuleEditor = Class.extend({ | |||||
init: function(frm, wrapper) { | |||||
this.wrapper = $('<div class="row module-block-list"></div>').appendTo(wrapper); | |||||
this.frm = frm; | |||||
this.make(); | |||||
}, | |||||
make: function() { | |||||
var me = this; | |||||
this.frm.doc.__onload.all_modules.forEach(function(m) { | |||||
$(repl('<div class="col-sm-6"><div class="checkbox">\ | |||||
<label><input type="checkbox" class="block-module-check" data-module="%(module)s">\ | |||||
%(module)s</label></div></div>', {module: m})).appendTo(me.wrapper); | |||||
}); | |||||
this.bind(); | |||||
}, | |||||
refresh: function() { | |||||
var me = this; | |||||
this.wrapper.find(".block-module-check").prop("checked", true); | |||||
$.each(this.frm.doc.block_modules, function(i, d) { | |||||
me.wrapper.find(".block-module-check[data-module='"+ d.module +"']").prop("checked", false); | |||||
}); | |||||
}, | |||||
bind: function() { | |||||
var me = this; | |||||
this.wrapper.on("change", ".block-module-check", function() { | |||||
var module = $(this).attr('data-module'); | |||||
if($(this).prop("checked")) { | |||||
// remove from block_modules | |||||
me.frm.doc.block_modules = $.map(me.frm.doc.block_modules || [], function(d) { | |||||
if (d.module != module) { | |||||
return d; | |||||
} | |||||
}); | |||||
} else { | |||||
me.frm.add_child("block_modules", {"module": module}); | |||||
} | |||||
}); | |||||
} | |||||
}); |
@@ -51,9 +51,9 @@ | |||||
"send_me_a_copy", | "send_me_a_copy", | ||||
"allowed_in_mentions", | "allowed_in_mentions", | ||||
"email_signature", | "email_signature", | ||||
"email_inbox", | |||||
"user_emails", | "user_emails", | ||||
"sb_allow_modules", | "sb_allow_modules", | ||||
"module_profile", | |||||
"modules_html", | "modules_html", | ||||
"block_modules", | "block_modules", | ||||
"home_settings", | "home_settings", | ||||
@@ -577,6 +577,12 @@ | |||||
"fieldtype": "Password", | "fieldtype": "Password", | ||||
"label": "API Secret", | "label": "API Secret", | ||||
"read_only": 1 | "read_only": 1 | ||||
}, | |||||
{ | |||||
"fieldname": "module_profile", | |||||
"fieldtype": "Link", | |||||
"label": "Module Profile", | |||||
"options": "Module Profile" | |||||
} | } | ||||
], | ], | ||||
"icon": "fa fa-user", | "icon": "fa fa-user", | ||||
@@ -642,10 +648,15 @@ | |||||
"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, | ||||
"modified": "2020-08-26 19:48:49.677800", | |||||
"modified": "2020-10-18 15:18:53.126800", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "User", | "name": "User", | ||||
@@ -679,4 +690,4 @@ | |||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"title_field": "full_name", | "title_field": "full_name", | ||||
"track_changes": 1 | "track_changes": 1 | ||||
} | |||||
} |
@@ -75,6 +75,7 @@ class User(Document): | |||||
self.validate_user_email_inbox() | self.validate_user_email_inbox() | ||||
ask_pass_update() | ask_pass_update() | ||||
self.validate_roles() | self.validate_roles() | ||||
self.validate_allowed_modules() | |||||
self.validate_user_image() | self.validate_user_image() | ||||
if self.language == "Loading...": | if self.language == "Loading...": | ||||
@@ -85,9 +86,18 @@ class User(Document): | |||||
def validate_roles(self): | def validate_roles(self): | ||||
if self.role_profile_name: | if self.role_profile_name: | ||||
role_profile = frappe.get_doc('Role Profile', self.role_profile_name) | |||||
self.set('roles', []) | |||||
self.append_roles(*[role.role for role in role_profile.roles]) | |||||
role_profile = frappe.get_doc('Role Profile', self.role_profile_name) | |||||
self.set('roles', []) | |||||
self.append_roles(*[role.role for role in role_profile.roles]) | |||||
def validate_allowed_modules(self): | |||||
if self.module_profile: | |||||
module_profile = frappe.get_doc('Module Profile', self.module_profile) | |||||
self.set('block_modules', []) | |||||
for d in module_profile.get('block_modules'): | |||||
self.append('block_modules', { | |||||
'module': d.module | |||||
}) | |||||
def validate_user_image(self): | def validate_user_image(self): | ||||
if self.user_image and len(self.user_image) > 2000: | if self.user_image and len(self.user_image) > 2000: | ||||
@@ -98,16 +108,17 @@ class User(Document): | |||||
self.share_with_self() | self.share_with_self() | ||||
clear_notifications(user=self.name) | clear_notifications(user=self.name) | ||||
frappe.clear_cache(user=self.name) | frappe.clear_cache(user=self.name) | ||||
now=frappe.flags.in_test or frappe.flags.in_install | |||||
self.send_password_notification(self.__new_password) | self.send_password_notification(self.__new_password) | ||||
frappe.enqueue( | frappe.enqueue( | ||||
'frappe.core.doctype.user.user.create_contact', | 'frappe.core.doctype.user.user.create_contact', | ||||
user=self, | user=self, | ||||
ignore_mandatory=True, | ignore_mandatory=True, | ||||
now=frappe.flags.in_test or frappe.flags.in_install | |||||
now=now | |||||
) | ) | ||||
if self.name not in ('Administrator', 'Guest') and not self.user_image: | if self.name not in ('Administrator', 'Guest') and not self.user_image: | ||||
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name) | |||||
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name, now=now) | |||||
# Set user selected timezone | # Set user selected timezone | ||||
if self.time_zone: | if self.time_zone: | ||||
frappe.defaults.set_default("time_zone", self.time_zone, self.name) | frappe.defaults.set_default("time_zone", self.time_zone, self.name) | ||||
@@ -1041,6 +1052,11 @@ def get_role_profile(role_profile): | |||||
roles = frappe.get_doc('Role Profile', {'role_profile': role_profile}) | roles = frappe.get_doc('Role Profile', {'role_profile': role_profile}) | ||||
return roles.roles | return roles.roles | ||||
@frappe.whitelist() | |||||
def get_module_profile(module_profile): | |||||
module_profile = frappe.get_doc('Module Profile', {'module_profile_name': module_profile}) | |||||
return module_profile.get('block_modules') | |||||
def update_roles(role_profile): | def update_roles(role_profile): | ||||
users = frappe.get_all('User', filters={'role_profile_name': role_profile}) | users = frappe.get_all('User', filters={'role_profile_name': role_profile}) | ||||
role_profile = frappe.get_doc('Role Profile', role_profile) | role_profile = frappe.get_doc('Role Profile', role_profile) | ||||
@@ -3,6 +3,7 @@ | |||||
# See license.txt | # See license.txt | ||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
from frappe.core.doctype.user_permission.user_permission import add_user_permissions | from frappe.core.doctype.user_permission.user_permission import add_user_permissions | ||||
from frappe.permissions import has_user_permission | |||||
import frappe | import frappe | ||||
import unittest | import unittest | ||||
@@ -10,7 +11,12 @@ import unittest | |||||
class TestUserPermission(unittest.TestCase): | class TestUserPermission(unittest.TestCase): | ||||
def setUp(self): | def setUp(self): | ||||
frappe.db.sql("""DELETE FROM `tabUser Permission` | frappe.db.sql("""DELETE FROM `tabUser Permission` | ||||
WHERE `user` in ('test_bulk_creation_update@example.com', 'test_user_perm1@example.com')""") | |||||
WHERE `user` in ( | |||||
'test_bulk_creation_update@example.com', | |||||
'test_user_perm1@example.com', | |||||
'nested_doc_user@example.com')""") | |||||
frappe.delete_doc_if_exists("DocType", "Person") | |||||
frappe.db.sql_ddl("DROP TABLE IF EXISTS `tabPerson`") | |||||
def test_default_user_permission_validation(self): | def test_default_user_permission_validation(self): | ||||
user = create_user('test_default_permission@example.com') | user = create_user('test_default_permission@example.com') | ||||
@@ -108,6 +114,45 @@ class TestUserPermission(unittest.TestCase): | |||||
self.assertIsNone(removed_applicable_second) | self.assertIsNone(removed_applicable_second) | ||||
self.assertEquals(is_created, 1) | self.assertEquals(is_created, 1) | ||||
def test_user_perm_for_nested_doctype(self): | |||||
"""Test if descendants' visibility is controlled for a nested DocType.""" | |||||
from frappe.core.doctype.doctype.test_doctype import new_doctype | |||||
user = create_user("nested_doc_user@example.com", "Blogger") | |||||
if not frappe.db.exists("DocType", "Person"): | |||||
doc = new_doctype("Person", | |||||
fields=[ | |||||
{ | |||||
"label": "Person Name", | |||||
"fieldname": "person_name", | |||||
"fieldtype": "Data" | |||||
} | |||||
], unique=0) | |||||
doc.is_tree = 1 | |||||
doc.insert() | |||||
parent_record = frappe.get_doc( | |||||
{"doctype": "Person", "person_name": "Parent", "is_group": 1} | |||||
).insert() | |||||
child_record = frappe.get_doc( | |||||
{"doctype": "Person", "person_name": "Child", "is_group": 0, "parent_person": parent_record.name} | |||||
).insert() | |||||
add_user_permissions(get_params(user, "Person", parent_record.name)) | |||||
# check if adding perm on a group record, makes child record visible | |||||
self.assertTrue(has_user_permission(frappe.get_doc("Person", parent_record.name), user.name)) | |||||
self.assertTrue(has_user_permission(frappe.get_doc("Person", child_record.name), user.name)) | |||||
frappe.db.set_value("User Permission", {"allow": "Person", "for_value": parent_record.name}, "hide_descendants", 1) | |||||
frappe.cache().delete_value("user_permissions") | |||||
# check if adding perm on a group record with hide_descendants enabled, | |||||
# hides child records | |||||
self.assertTrue(has_user_permission(frappe.get_doc("Person", parent_record.name), user.name)) | |||||
self.assertFalse(has_user_permission(frappe.get_doc("Person", child_record.name), user.name)) | |||||
def create_user(email, role="System Manager"): | def create_user(email, role="System Manager"): | ||||
''' create user with role system manager ''' | ''' create user with role system manager ''' | ||||
if frappe.db.exists('User', email): | if frappe.db.exists('User', email): | ||||
@@ -119,7 +164,7 @@ def create_user(email, role="System Manager"): | |||||
user.add_roles(role) | user.add_roles(role) | ||||
return user | return user | ||||
def get_params(user, doctype, docname, is_default=0, applicable=None): | |||||
def get_params(user, doctype, docname, is_default=0, hide_descendants=0, applicable=None): | |||||
''' Return param to insert ''' | ''' Return param to insert ''' | ||||
param = { | param = { | ||||
"user": user.name, | "user": user.name, | ||||
@@ -127,7 +172,8 @@ def get_params(user, doctype, docname, is_default=0, applicable=None): | |||||
"docname":docname, | "docname":docname, | ||||
"is_default": is_default, | "is_default": is_default, | ||||
"apply_to_all_doctypes": 1, | "apply_to_all_doctypes": 1, | ||||
"applicable_doctypes": [] | |||||
"applicable_doctypes": [], | |||||
"hide_descendants": hide_descendants | |||||
} | } | ||||
if applicable: | if applicable: | ||||
param.update({"apply_to_all_doctypes": 0}) | param.update({"apply_to_all_doctypes": 0}) | ||||
@@ -26,11 +26,15 @@ frappe.ui.form.on('User Permission', { | |||||
() => frappe.set_route('query-report', 'Permitted Documents For User', | () => frappe.set_route('query-report', 'Permitted Documents For User', | ||||
{ user: frm.doc.user })); | { user: frm.doc.user })); | ||||
frm.trigger('set_applicable_for_constraint'); | frm.trigger('set_applicable_for_constraint'); | ||||
frm.trigger('toggle_hide_descendants'); | |||||
}, | }, | ||||
allow: frm => { | allow: frm => { | ||||
if(frm.doc.for_value) { | |||||
frm.set_value('for_value', null); | |||||
if (frm.doc.allow) { | |||||
if (frm.doc.for_value) { | |||||
frm.set_value('for_value', null); | |||||
} | |||||
frm.trigger('toggle_hide_descendants'); | |||||
} | } | ||||
}, | }, | ||||
@@ -43,6 +47,11 @@ frappe.ui.form.on('User Permission', { | |||||
if (frm.doc.apply_to_all_doctypes) { | if (frm.doc.apply_to_all_doctypes) { | ||||
frm.set_value('applicable_for', null); | frm.set_value('applicable_for', null); | ||||
} | } | ||||
}, | |||||
toggle_hide_descendants: frm => { | |||||
let show = frappe.boot.nested_set_doctypes.includes(frm.doc.allow); | |||||
frm.toggle_display('hide_descendants', show); | |||||
} | } | ||||
@@ -1,330 +1,116 @@ | |||||
{ | { | ||||
"allow_copy": 0, | |||||
"allow_events_in_timeline": 0, | |||||
"allow_guest_to_view": 0, | |||||
"actions": [], | |||||
"allow_import": 1, | "allow_import": 1, | ||||
"allow_rename": 0, | |||||
"beta": 0, | |||||
"creation": "2017-07-17 14:25:27.881871", | "creation": "2017-07-17 14:25:27.881871", | ||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | "doctype": "DocType", | ||||
"document_type": "", | |||||
"editable_grid": 1, | "editable_grid": 1, | ||||
"engine": "InnoDB", | "engine": "InnoDB", | ||||
"field_order": [ | |||||
"user", | |||||
"allow", | |||||
"column_break_3", | |||||
"for_value", | |||||
"is_default", | |||||
"advanced_control_section", | |||||
"apply_to_all_doctypes", | |||||
"applicable_for", | |||||
"column_break_9", | |||||
"hide_descendants" | |||||
], | |||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fetch_if_empty": 0, | |||||
"fieldname": "user", | "fieldname": "user", | ||||
"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": 1, | "in_standard_filter": 1, | ||||
"label": "User", | "label": "User", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "User", | "options": "User", | ||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | "reqd": 1, | ||||
"search_index": 1, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
"search_index": 1 | |||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fetch_if_empty": 0, | |||||
"fieldname": "allow", | "fieldname": "allow", | ||||
"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": 1, | "in_standard_filter": 1, | ||||
"label": "Allow", | "label": "Allow", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "DocType", | "options": "DocType", | ||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
"reqd": 1 | |||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fetch_if_empty": 0, | |||||
"fieldname": "column_break_3", | "fieldname": "column_break_3", | ||||
"fieldtype": "Column Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
"fieldtype": "Column Break" | |||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fetch_if_empty": 0, | |||||
"fieldname": "for_value", | "fieldname": "for_value", | ||||
"fieldtype": "Dynamic Link", | "fieldtype": "Dynamic Link", | ||||
"hidden": 0, | |||||
"ignore_user_permissions": 1, | "ignore_user_permissions": 1, | ||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 1, | "in_standard_filter": 1, | ||||
"label": "For Value", | "label": "For Value", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "allow", | "options": "allow", | ||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
"reqd": 1 | |||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fetch_if_empty": 0, | |||||
"default": "0", | |||||
"fieldname": "is_default", | "fieldname": "is_default", | ||||
"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": "Is Default", | |||||
"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": "Is Default" | |||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fetch_if_empty": 0, | |||||
"fieldname": "advanced_control_section", | "fieldname": "advanced_control_section", | ||||
"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": "Advanced Control", | |||||
"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": "Advanced Control" | |||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "1", | "default": "1", | ||||
"fetch_if_empty": 0, | |||||
"fieldname": "apply_to_all_doctypes", | "fieldname": "apply_to_all_doctypes", | ||||
"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": "Apply To All Document Types", | |||||
"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": "Apply To All Document Types" | |||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"depends_on": "eval:!doc.apply_to_all_doctypes", | "depends_on": "eval:!doc.apply_to_all_doctypes", | ||||
"fetch_if_empty": 0, | |||||
"fieldname": "applicable_for", | "fieldname": "applicable_for", | ||||
"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": "Applicable For", | "label": "Applicable For", | ||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "DocType", | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
"options": "DocType" | |||||
}, | |||||
{ | |||||
"fieldname": "column_break_9", | |||||
"fieldtype": "Column Break" | |||||
}, | |||||
{ | |||||
"default": "0", | |||||
"description": "Hide descendant records of <b>For Value</b>.", | |||||
"fieldname": "hide_descendants", | |||||
"fieldtype": "Check", | |||||
"hidden": 1, | |||||
"label": "Hide Descendants" | |||||
} | } | ||||
], | ], | ||||
"has_web_view": 0, | |||||
"hide_toolbar": 0, | |||||
"idx": 0, | |||||
"in_create": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 0, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2019-04-16 19:17:23.644724", | |||||
"links": [], | |||||
"modified": "2021-01-21 18:14:10.839381", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "User Permission", | "name": "User Permission", | ||||
"name_case": "", | |||||
"owner": "Administrator", | "owner": "Administrator", | ||||
"permissions": [ | "permissions": [ | ||||
{ | { | ||||
"amend": 0, | |||||
"cancel": 0, | |||||
"create": 1, | "create": 1, | ||||
"delete": 1, | "delete": 1, | ||||
"email": 1, | "email": 1, | ||||
"export": 1, | "export": 1, | ||||
"if_owner": 0, | |||||
"import": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | "print": 1, | ||||
"read": 1, | "read": 1, | ||||
"report": 1, | "report": 1, | ||||
"role": "System Manager", | "role": "System Manager", | ||||
"set_user_permissions": 0, | |||||
"share": 1, | "share": 1, | ||||
"submit": 0, | |||||
"write": 1 | "write": 1 | ||||
} | } | ||||
], | ], | ||||
"quick_entry": 0, | |||||
"read_only": 0, | |||||
"show_name_in_global_search": 0, | |||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"title_field": "user", | "title_field": "user", | ||||
"track_changes": 1, | |||||
"track_seen": 0, | |||||
"track_views": 0 | |||||
"track_changes": 1 | |||||
} | } |
@@ -49,7 +49,8 @@ class UserPermission(Document): | |||||
'name': ['!=', self.name] | 'name': ['!=', self.name] | ||||
}, or_filters={ | }, or_filters={ | ||||
'applicable_for': cstr(self.applicable_for), | 'applicable_for': cstr(self.applicable_for), | ||||
'apply_to_all_doctypes': 1 | |||||
'apply_to_all_doctypes': 1, | |||||
'hide_descendants': cstr(self.hide_descendants) | |||||
}, limit=1) | }, limit=1) | ||||
if overlap_exists: | if overlap_exists: | ||||
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name) | ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name) | ||||
@@ -91,13 +92,13 @@ def get_user_permissions(user=None): | |||||
try: | try: | ||||
for perm in frappe.get_all('User Permission', | for perm in frappe.get_all('User Permission', | ||||
fields=['allow', 'for_value', 'applicable_for', 'is_default'], | |||||
fields=['allow', 'for_value', 'applicable_for', 'is_default', 'hide_descendants'], | |||||
filters=dict(user=user)): | filters=dict(user=user)): | ||||
meta = frappe.get_meta(perm.allow) | meta = frappe.get_meta(perm.allow) | ||||
add_doc_to_perm(perm, perm.for_value, perm.is_default) | add_doc_to_perm(perm, perm.for_value, perm.is_default) | ||||
if meta.is_nested_set(): | |||||
if meta.is_nested_set() and not perm.hide_descendants: | |||||
decendants = frappe.db.get_descendants(perm.allow, perm.for_value) | decendants = frappe.db.get_descendants(perm.allow, perm.for_value) | ||||
for doc in decendants: | for doc in decendants: | ||||
add_doc_to_perm(perm, doc, False) | add_doc_to_perm(perm, doc, False) | ||||
@@ -172,8 +173,8 @@ def check_applicable_doc_perm(user, doctype, docname): | |||||
"allow": doctype, | "allow": doctype, | ||||
"for_value":docname, | "for_value":docname, | ||||
}) | }) | ||||
for d in data: | |||||
applicable.append(d.applicable_for) | |||||
for permission in data: | |||||
applicable.append(permission.applicable_for) | |||||
return applicable | return applicable | ||||
@@ -194,7 +195,8 @@ def add_user_permissions(data): | |||||
data = json.loads(data) | data = json.loads(data) | ||||
data = frappe._dict(data) | data = frappe._dict(data) | ||||
d = check_applicable_doc_perm(data.user, data.doctype, data.docname) | |||||
# get all doctypes on whom this permission is applied | |||||
perm_applied_docs = check_applicable_doc_perm(data.user, data.doctype, data.docname) | |||||
exists = frappe.db.exists("User Permission", { | exists = frappe.db.exists("User Permission", { | ||||
"user": data.user, | "user": data.user, | ||||
"allow": data.doctype, | "allow": data.doctype, | ||||
@@ -202,26 +204,27 @@ def add_user_permissions(data): | |||||
"apply_to_all_doctypes": 1 | "apply_to_all_doctypes": 1 | ||||
}) | }) | ||||
if data.apply_to_all_doctypes == 1 and not exists: | if data.apply_to_all_doctypes == 1 and not exists: | ||||
remove_applicable(d, data.user, data.doctype, data.docname) | |||||
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, apply_to_all = 1) | |||||
remove_applicable(perm_applied_docs, data.user, data.doctype, data.docname) | |||||
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, apply_to_all=1) | |||||
return 1 | return 1 | ||||
elif len(data.applicable_doctypes) > 0 and data.apply_to_all_doctypes != 1: | elif len(data.applicable_doctypes) > 0 and data.apply_to_all_doctypes != 1: | ||||
remove_apply_to_all(data.user, data.doctype, data.docname) | remove_apply_to_all(data.user, data.doctype, data.docname) | ||||
update_applicable(d, data.applicable_doctypes, data.user, data.doctype, data.docname) | |||||
update_applicable(perm_applied_docs, data.applicable_doctypes, data.user, data.doctype, data.docname) | |||||
for applicable in data.applicable_doctypes : | for applicable in data.applicable_doctypes : | ||||
if applicable not in d: | |||||
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, applicable = applicable) | |||||
if applicable not in perm_applied_docs: | |||||
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, applicable=applicable) | |||||
elif exists: | elif exists: | ||||
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, applicable = applicable) | |||||
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, applicable=applicable) | |||||
return 1 | return 1 | ||||
return 0 | return 0 | ||||
def insert_user_perm(user, doctype, docname, is_default=0, apply_to_all=None, applicable=None): | |||||
def insert_user_perm(user, doctype, docname, is_default=0, hide_descendants=0, apply_to_all=None, applicable=None): | |||||
user_perm = frappe.new_doc("User Permission") | user_perm = frappe.new_doc("User Permission") | ||||
user_perm.user = user | user_perm.user = user | ||||
user_perm.allow = doctype | user_perm.allow = doctype | ||||
user_perm.for_value = docname | user_perm.for_value = docname | ||||
user_perm.is_default = is_default | user_perm.is_default = is_default | ||||
user_perm.hide_descendants = hide_descendants | |||||
if applicable: | if applicable: | ||||
user_perm.applicable_for = applicable | user_perm.applicable_for = applicable | ||||
user_perm.apply_to_all_doctypes = 0 | user_perm.apply_to_all_doctypes = 0 | ||||
@@ -229,8 +232,8 @@ def insert_user_perm(user, doctype, docname, is_default=0, apply_to_all=None, ap | |||||
user_perm.apply_to_all_doctypes = 1 | user_perm.apply_to_all_doctypes = 1 | ||||
user_perm.insert() | user_perm.insert() | ||||
def remove_applicable(d, user, doctype, docname): | |||||
for applicable_for in d: | |||||
def remove_applicable(perm_applied_docs, user, doctype, docname): | |||||
for applicable_for in perm_applied_docs: | |||||
frappe.db.sql("""DELETE FROM `tabUser Permission` | frappe.db.sql("""DELETE FROM `tabUser Permission` | ||||
WHERE `user`=%s | WHERE `user`=%s | ||||
AND `applicable_for`=%s | AND `applicable_for`=%s | ||||
@@ -19,6 +19,7 @@ frappe.listview_settings['User Permission'] = { | |||||
dialog.set_df_property("is_default", "hidden", 1); | dialog.set_df_property("is_default", "hidden", 1); | ||||
dialog.set_df_property("apply_to_all_doctypes", "hidden", 1); | dialog.set_df_property("apply_to_all_doctypes", "hidden", 1); | ||||
dialog.set_df_property("applicable_doctypes", "hidden", 1); | dialog.set_df_property("applicable_doctypes", "hidden", 1); | ||||
dialog.set_df_property("hide_descendants", "hidden", 1); | |||||
} | } | ||||
}, | }, | ||||
{ | { | ||||
@@ -54,6 +55,10 @@ frappe.listview_settings['User Permission'] = { | |||||
} | } | ||||
} | } | ||||
}, | }, | ||||
{ | |||||
fieldtype: "Section Break", | |||||
hide_border: 1 | |||||
}, | |||||
{ | { | ||||
fieldname: 'is_default', | fieldname: 'is_default', | ||||
label: __('Is Default'), | label: __('Is Default'), | ||||
@@ -74,6 +79,19 @@ frappe.listview_settings['User Permission'] = { | |||||
} | } | ||||
} | } | ||||
}, | }, | ||||
{ | |||||
fieldtype: "Column Break" | |||||
}, | |||||
{ | |||||
fieldname: 'hide_descendants', | |||||
label: __('Hide Descendants'), | |||||
fieldtype: 'Check', | |||||
hidden: 1 | |||||
}, | |||||
{ | |||||
fieldtype: "Section Break", | |||||
hide_border: 1 | |||||
}, | |||||
{ | { | ||||
label: __("Applicable Document Types"), | label: __("Applicable Document Types"), | ||||
fieldname: "applicable_doctypes", | fieldname: "applicable_doctypes", | ||||
@@ -214,6 +232,9 @@ frappe.listview_settings['User Permission'] = { | |||||
dialog.set_df_property("is_default", "hidden", 0); | dialog.set_df_property("is_default", "hidden", 0); | ||||
dialog.set_df_property("apply_to_all_doctypes", "hidden", 0); | dialog.set_df_property("apply_to_all_doctypes", "hidden", 0); | ||||
dialog.set_value("apply_to_all_doctypes", "checked", 1); | dialog.set_value("apply_to_all_doctypes", "checked", 1); | ||||
let show = frappe.boot.nested_set_doctypes.includes(dialog.get_value("doctype")); | |||||
dialog.set_df_property("hide_descendants", "hidden", !show); | |||||
dialog.refresh(); | |||||
}, | }, | ||||
on_docname_change: function(dialog, options, applicable) { | on_docname_change: function(dialog, options, applicable) { | ||||
@@ -233,6 +254,7 @@ frappe.listview_settings['User Permission'] = { | |||||
dialog.set_df_property("applicable_doctypes", "options", options); | dialog.set_df_property("applicable_doctypes", "options", options); | ||||
dialog.set_df_property("applicable_doctypes", "hidden", 1); | dialog.set_df_property("applicable_doctypes", "hidden", 1); | ||||
} | } | ||||
dialog.refresh(); | |||||
}, | }, | ||||
on_apply_to_all_doctypes_change: function(dialog, options) { | on_apply_to_all_doctypes_change: function(dialog, options) { | ||||
@@ -243,5 +265,6 @@ frappe.listview_settings['User Permission'] = { | |||||
dialog.set_df_property("applicable_doctypes", "options", options); | dialog.set_df_property("applicable_doctypes", "options", options); | ||||
dialog.set_df_property("applicable_doctypes", "hidden", 1); | dialog.set_df_property("applicable_doctypes", "hidden", 1); | ||||
} | } | ||||
dialog.refresh_sections(); | |||||
} | } | ||||
}; | |||||
}; |
@@ -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> |
@@ -269,7 +269,7 @@ frappe.PermissionEngine = Class.extend({ | |||||
.css({"margin-top": "15px"}); | .css({"margin-top": "15px"}); | ||||
}, | }, | ||||
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"], | ||||
set_show_users: function(cell, role) { | set_show_users: function(cell, role) { | ||||
@@ -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): | ||||
@@ -455,11 +455,15 @@ class CustomizeForm(Document): | |||||
self.fetch_to_customize() | self.fetch_to_customize() | ||||
def reset_customization(doctype): | def reset_customization(doctype): | ||||
frappe.db.sql(""" | |||||
DELETE FROM `tabProperty Setter` WHERE doc_type=%s | |||||
and `field_name`!='naming_series' | |||||
and `property`!='options' | |||||
""", doctype) | |||||
setters = frappe.get_all("Property Setter", filters={ | |||||
'doc_type': doctype, | |||||
'field_name': ['!=', 'naming_series'], | |||||
'property': ['!=', 'options'] | |||||
}, pluck='name') | |||||
for setter in setters: | |||||
frappe.delete_doc("Property Setter", setter) | |||||
frappe.clear_cache(doctype=doctype) | frappe.clear_cache(doctype=doctype) | ||||
doctype_properties = { | doctype_properties = { | ||||
@@ -746,6 +746,9 @@ class Database(object): | |||||
def commit(self): | def commit(self): | ||||
"""Commit current transaction. Calls SQL `COMMIT`.""" | """Commit current transaction. Calls SQL `COMMIT`.""" | ||||
for method in frappe.local.before_commit: | |||||
frappe.call(method[0], *(method[1] or []), **(method[2] or {})) | |||||
self.sql("commit") | self.sql("commit") | ||||
frappe.local.rollback_observers = [] | frappe.local.rollback_observers = [] | ||||
@@ -753,6 +756,9 @@ class Database(object): | |||||
enqueue_jobs_after_commit() | enqueue_jobs_after_commit() | ||||
flush_local_link_count() | flush_local_link_count() | ||||
def add_before_commit(self, method, args=None, kwargs=None): | |||||
frappe.local.before_commit.append([method, args, kwargs]) | |||||
@staticmethod | @staticmethod | ||||
def flush_realtime_log(): | def flush_realtime_log(): | ||||
for args in frappe.local.realtime_log: | for args in frappe.local.realtime_log: | ||||
@@ -29,6 +29,7 @@ def get_event_conditions(doctype, filters=None): | |||||
def get_events(doctype, start, end, field_map, filters=None, fields=None): | def get_events(doctype, start, end, field_map, filters=None, fields=None): | ||||
field_map = frappe._dict(json.loads(field_map)) | field_map = frappe._dict(json.loads(field_map)) | ||||
fields = frappe.parse_json(fields) | |||||
doc_meta = frappe.get_meta(doctype) | doc_meta = frappe.get_meta(doctype) | ||||
for d in doc_meta.fields: | for d in doc_meta.fields: | ||||
@@ -108,9 +108,18 @@ class Workspace: | |||||
'extends': self.page_name, | 'extends': self.page_name, | ||||
'for_user': frappe.session.user | 'for_user': frappe.session.user | ||||
} | } | ||||
pages = frappe.get_all("Desk Page", filters=filters, limit=1) | |||||
if pages: | |||||
return frappe.get_cached_doc("Desk Page", pages[0]) | |||||
user_pages = frappe.get_all("Desk Page", filters=filters, limit=1) | |||||
if user_pages: | |||||
return frappe.get_cached_doc("Desk Page", user_pages[0]) | |||||
filters = { | |||||
'extends_another_page': 1, | |||||
'extends': self.page_name, | |||||
'is_default': 1 | |||||
} | |||||
default_page = frappe.get_all("Desk Page", filters=filters, limit=1) | |||||
if default_page: | |||||
return frappe.get_cached_doc("Desk Page", default_page[0]) | |||||
self.get_pages_to_extend() | self.get_pages_to_extend() | ||||
return frappe.get_cached_doc("Desk Page", self.page_name) | return frappe.get_cached_doc("Desk Page", self.page_name) | ||||
@@ -73,7 +73,7 @@ def has_permission(doc, ptype, user): | |||||
if doc.report_name in allowed_reports: | if doc.report_name in allowed_reports: | ||||
return True | return True | ||||
else: | else: | ||||
allowed_doctypes = [frappe.permissions.get_doctypes_with_read()] | |||||
allowed_doctypes = frappe.permissions.get_doctypes_with_read() | |||||
if doc.document_type in allowed_doctypes: | if doc.document_type in allowed_doctypes: | ||||
return True | return True | ||||
@@ -5,7 +5,6 @@ frappe.ui.form.on('Desk Page', { | |||||
refresh: function(frm) { | refresh: function(frm) { | ||||
frm.enable_save(); | frm.enable_save(); | ||||
frm.get_field("is_standard").toggle(frappe.boot.developer_mode); | frm.get_field("is_standard").toggle(frappe.boot.developer_mode); | ||||
frm.get_field("extends_another_page").toggle(frappe.boot.developer_mode); | |||||
frm.get_field("developer_mode_only").toggle(frappe.boot.developer_mode); | frm.get_field("developer_mode_only").toggle(frappe.boot.developer_mode); | ||||
if (frm.doc.for_user) { | if (frm.doc.for_user) { | ||||
@@ -16,6 +16,7 @@ | |||||
"onboarding", | "onboarding", | ||||
"column_break_3", | "column_break_3", | ||||
"extends_another_page", | "extends_another_page", | ||||
"is_default", | |||||
"is_standard", | "is_standard", | ||||
"developer_mode_only", | "developer_mode_only", | ||||
"disable_user_customization", | "disable_user_customization", | ||||
@@ -197,10 +198,18 @@ | |||||
"fieldname": "hide_custom", | "fieldname": "hide_custom", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"label": "Hide Custom DocTypes and Reports" | "label": "Hide Custom DocTypes and Reports" | ||||
}, | |||||
{ | |||||
"default": "0", | |||||
"depends_on": "extends_another_page", | |||||
"description": "Sets the current page as default for all users", | |||||
"fieldname": "is_default", | |||||
"fieldtype": "Check", | |||||
"label": "Is Default" | |||||
} | } | ||||
], | ], | ||||
"links": [], | "links": [], | ||||
"modified": "2020-05-18 19:17:27.206646", | |||||
"modified": "2021-01-21 12:09:36.156614", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Desk", | "module": "Desk", | ||||
"name": "Desk Page", | "name": "Desk Page", | ||||
@@ -15,6 +15,11 @@ class DeskPage(Document): | |||||
if (self.is_standard and not frappe.conf.developer_mode and not disable_saving_as_standard()): | if (self.is_standard and not frappe.conf.developer_mode and not disable_saving_as_standard()): | ||||
frappe.throw(_("You need to be in developer mode to edit this document")) | frappe.throw(_("You need to be in developer mode to edit this document")) | ||||
if self.is_default and self.name and frappe.db.exists("Desk Page", { | |||||
"name": ["!=", self.name], 'is_default': 1, 'extends': self.extends | |||||
}): | |||||
frappe.throw(_("You can only have one default page that extends a particular standard page.")) | |||||
def validate_cards_json(self): | def validate_cards_json(self): | ||||
for card in self.cards: | for card in self.cards: | ||||
try: | try: | ||||
@@ -45,4 +50,4 @@ def disable_saving_as_standard(): | |||||
frappe.flags.in_patch or \ | frappe.flags.in_patch or \ | ||||
frappe.flags.in_test or \ | frappe.flags.in_test or \ | ||||
frappe.flags.in_fixtures or \ | frappe.flags.in_fixtures or \ | ||||
frappe.flags.in_migrate | |||||
frappe.flags.in_migrate |
@@ -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): | ||||
@@ -18,14 +18,14 @@ def install(): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def update_genders(): | def update_genders(): | ||||
default_genders = [_("Male"), _("Female"), _("Other"),_("Transgender"), _("Genderqueer"), _("Non-Conforming"),_("Prefer not to say")] | |||||
default_genders = ["Male", "Female", "Other","Transgender", "Genderqueer", "Non-Conforming","Prefer not to say"] | |||||
records = [{'doctype': 'Gender', 'gender': d} for d in default_genders] | records = [{'doctype': 'Gender', 'gender': d} for d in default_genders] | ||||
for record in records: | for record in records: | ||||
frappe.get_doc(record).insert(ignore_permissions=True, ignore_if_duplicate=True) | frappe.get_doc(record).insert(ignore_permissions=True, ignore_if_duplicate=True) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def update_salutations(): | def update_salutations(): | ||||
default_salutations = [_("Mr"), _("Ms"), _('Mx'), _("Dr"), _("Mrs"), _("Madam"), _("Miss"), _("Master"), _("Prof")] | |||||
default_salutations = ["Mr", "Ms", 'Mx', "Dr", "Mrs", "Madam", "Miss", "Master", "Prof"] | |||||
records = [{'doctype': 'Salutation', 'salutation': d} for d in default_salutations] | records = [{'doctype': 'Salutation', 'salutation': d} for d in default_salutations] | ||||
for record in records: | for record in records: | ||||
doc = frappe.new_doc(record.get("doctype")) | doc = frappe.new_doc(record.get("doctype")) | ||||
@@ -76,6 +76,7 @@ class UserProfile { | |||||
fieldname: 'user', | fieldname: 'user', | ||||
options: 'User', | options: 'User', | ||||
label: __('User'), | label: __('User'), | ||||
reqd: 1 | |||||
} | } | ||||
], | ], | ||||
primary_action_label: __('Go'), | primary_action_label: __('Go'), | ||||
@@ -54,6 +54,12 @@ def get_form_params(): | |||||
fields = data["fields"] | fields = data["fields"] | ||||
if ((isinstance(fields, string_types) and fields == "*") | |||||
or (isinstance(fields, (list, tuple)) and len(fields) == 1 and fields[0] == "*")): | |||||
parenttype = data.doctype | |||||
data["fields"] = frappe.db.get_table_columns(parenttype) | |||||
fields = data["fields"] | |||||
for field in fields: | for field in fields: | ||||
key = field.split(" as ")[0] | key = field.split(" as ")[0] | ||||
@@ -61,21 +67,24 @@ def get_form_params(): | |||||
if key.startswith('sum('): continue | if key.startswith('sum('): continue | ||||
if key.startswith('avg('): continue | if key.startswith('avg('): continue | ||||
if "." in key: | |||||
parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`") | |||||
else: | |||||
parenttype = data.doctype | |||||
fieldname = field.strip("`") | |||||
parenttype, fieldname = get_parent_dt_and_field(key, data) | |||||
df = frappe.get_meta(parenttype).get_field(fieldname) | |||||
if fieldname == "*": | |||||
# * inside list is not allowed with other fields | |||||
fields.remove(field) | |||||
meta = frappe.get_meta(parenttype) | |||||
df = meta.get_field(fieldname) | |||||
fieldname = df.fieldname if df else None | |||||
report_hide = df.report_hide if df else None | report_hide = df.report_hide if df else None | ||||
# remove the field from the query if the report hide flag is set and current view is Report | # remove the field from the query if the report hide flag is set and current view is Report | ||||
if report_hide and is_report: | if report_hide and is_report: | ||||
fields.remove(field) | fields.remove(field) | ||||
if df and fieldname in [df.fieldname for df in meta.get_high_permlevel_fields()]: | |||||
if df.get('permlevel') not in meta.get_permlevel_access(parenttype=data.doctype) and field in fields: | |||||
fields.remove(field) | |||||
# queries must always be server side | # queries must always be server side | ||||
data.query = None | data.query = None | ||||
@@ -83,6 +92,16 @@ def get_form_params(): | |||||
return data | return data | ||||
def get_parent_dt_and_field(field, data): | |||||
if "." in field: | |||||
parenttype, fieldname = field.split(".")[0][4:-1], field.split(".")[1].strip("`") | |||||
else: | |||||
parenttype = data.doctype | |||||
fieldname = field.strip("`") | |||||
return parenttype, fieldname | |||||
def compress(data, args = {}): | def compress(data, args = {}): | ||||
"""separate keys and values""" | """separate keys and values""" | ||||
from frappe.desk.query_report import add_total_row | from frappe.desk.query_report import add_total_row | ||||
@@ -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 | ||||
@@ -81,7 +81,7 @@ class AutoEmailReport(Document): | |||||
if self.format == 'HTML': | if self.format == 'HTML': | ||||
columns, data = make_links(columns, data) | columns, data = make_links(columns, data) | ||||
columns = update_field_types(columns) | |||||
return self.get_html_table(columns, data) | return self.get_html_table(columns, data) | ||||
elif self.format == 'XLSX': | elif self.format == 'XLSX': | ||||
@@ -236,5 +236,14 @@ def make_links(columns, data): | |||||
elif col.fieldtype == "Dynamic Link": | elif col.fieldtype == "Dynamic Link": | ||||
if col.options and row.get(col.fieldname) and row.get(col.options): | if col.options and row.get(col.fieldname) and row.get(col.options): | ||||
row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname]) | row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname]) | ||||
elif col.fieldtype == "Currency": | |||||
row[col.fieldname] = frappe.format_value(row[col.fieldname], col) | |||||
return columns, data | return columns, data | ||||
def update_field_types(columns): | |||||
for col in columns: | |||||
if col.fieldtype in ("Link", "Dynamic Link", "Currency") and col.options != "Currency": | |||||
col.fieldtype = "Data" | |||||
col.options = "" | |||||
return columns |
@@ -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: | ||||
@@ -2,58 +2,66 @@ | |||||
# License: GNU General Public License v3. See license.txt | # License: GNU General Public License v3. See license.txt | ||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe, unittest | |||||
from frappe.utils import getdate, add_days | |||||
from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe, send_scheduled_email | |||||
from six.moves.urllib.parse import unquote | |||||
import unittest | |||||
from random import choice | |||||
import frappe | |||||
from frappe.email.doctype.newsletter.newsletter import ( | |||||
confirmed_unsubscribe, | |||||
send_scheduled_email, | |||||
) | |||||
from frappe.email.doctype.newsletter.newsletter import get_newsletter_list | |||||
from frappe.email.queue import flush | |||||
from frappe.utils import add_days, getdate | |||||
test_dependencies = ["Email Group"] | test_dependencies = ["Email Group"] | ||||
emails = [ | |||||
"test_subscriber1@example.com", | |||||
"test_subscriber2@example.com", | |||||
"test_subscriber3@example.com", | |||||
"test1@example.com", | |||||
] | |||||
emails = ["test_subscriber1@example.com", "test_subscriber2@example.com", | |||||
"test_subscriber3@example.com", "test1@example.com"] | |||||
class TestNewsletter(unittest.TestCase): | class TestNewsletter(unittest.TestCase): | ||||
def setUp(self): | def setUp(self): | ||||
frappe.set_user("Administrator") | frappe.set_user("Administrator") | ||||
frappe.db.sql('delete from `tabEmail Group Member`') | |||||
frappe.db.sql("delete from `tabEmail Group Member`") | |||||
if not frappe.db.exists("Email Group", "_Test Email Group"): | |||||
frappe.get_doc({"doctype": "Email Group", "title": "_Test Email Group"}).insert() | |||||
group_exist=frappe.db.exists("Email Group", "_Test Email Group") | |||||
if len(group_exist) == 0: | |||||
for email in emails: | |||||
frappe.get_doc({ | frappe.get_doc({ | ||||
"doctype": "Email Group", | |||||
"title": "_Test Email Group" | |||||
"doctype": "Email Group Member", | |||||
"email": email, | |||||
"email_group": "_Test Email Group" | |||||
}).insert() | }).insert() | ||||
for email in emails: | |||||
frappe.get_doc({ | |||||
"doctype": "Email Group Member", | |||||
"email": email, | |||||
"email_group": "_Test Email Group" | |||||
}).insert() | |||||
def test_send(self): | def test_send(self): | ||||
name = self.send_newsletter() | |||||
self.send_newsletter() | |||||
email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] | |||||
email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")] | |||||
self.assertEqual(len(email_queue_list), 4) | self.assertEqual(len(email_queue_list), 4) | ||||
recipients = [e.recipients[0].recipient for e in email_queue_list] | |||||
for email in emails: | |||||
self.assertTrue(email in recipients) | |||||
recipients = set([e.recipients[0].recipient for e in email_queue_list]) | |||||
self.assertTrue(set(emails).issubset(recipients)) | |||||
def test_unsubscribe(self): | def test_unsubscribe(self): | ||||
# test unsubscribe | |||||
name = self.send_newsletter() | name = self.send_newsletter() | ||||
from frappe.email.queue import flush | |||||
to_unsubscribe = choice(emails) | |||||
group = frappe.get_all("Newsletter Email Group", filters={"parent": name}, fields=["email_group"]) | |||||
flush(from_test=True) | flush(from_test=True) | ||||
to_unsubscribe = unquote(frappe.local.flags.signed_query_string.split("email=")[1].split("&")[0]) | |||||
group = frappe.get_all("Newsletter Email Group", filters={"parent" : name}, fields=["email_group"]) | |||||
confirmed_unsubscribe(to_unsubscribe, group[0].email_group) | confirmed_unsubscribe(to_unsubscribe, group[0].email_group) | ||||
name = self.send_newsletter() | name = self.send_newsletter() | ||||
email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] | |||||
email_queue_list = [ | |||||
frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue") | |||||
] | |||||
self.assertEqual(len(email_queue_list), 3) | self.assertEqual(len(email_queue_list), 3) | ||||
recipients = [e.recipients[0].recipient for e in email_queue_list] | recipients = [e.recipients[0].recipient for e in email_queue_list] | ||||
for email in emails: | for email in emails: | ||||
if email != to_unsubscribe: | if email != to_unsubscribe: | ||||
self.assertTrue(email in recipients) | self.assertTrue(email in recipients) | ||||
@@ -86,7 +94,6 @@ class TestNewsletter(unittest.TestCase): | |||||
def test_portal(self): | def test_portal(self): | ||||
self.send_newsletter(1) | self.send_newsletter(1) | ||||
frappe.set_user("test1@example.com") | frappe.set_user("test1@example.com") | ||||
from frappe.email.doctype.newsletter.newsletter import get_newsletter_list | |||||
newsletters = get_newsletter_list("Newsletter", None, None, 0) | newsletters = get_newsletter_list("Newsletter", None, None, 0) | ||||
self.assertEqual(len(newsletters), 1) | self.assertEqual(len(newsletters), 1) | ||||
@@ -106,4 +113,4 @@ class TestNewsletter(unittest.TestCase): | |||||
self.assertEqual(len(email_queue_list), 4) | self.assertEqual(len(email_queue_list), 4) | ||||
recipients = [e.recipients[0].recipient for e in email_queue_list] | recipients = [e.recipients[0].recipient for e in email_queue_list] | ||||
for email in emails: | for email in emails: | ||||
self.assertTrue(email in recipients) | |||||
self.assertTrue(email in recipients) |
@@ -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" | ||||
@@ -13,7 +13,7 @@ | |||||
{ | { | ||||
"hidden": 0, | "hidden": 0, | ||||
"label": "Authentication", | "label": "Authentication", | ||||
"links": "[\n {\n \"description\": \"Enter keys to enable login via Facebook, Google, GitHub.\",\n \"label\": \"Social Login Key\",\n \"name\": \"Social Login Key\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Ldap settings\",\n \"label\": \"LDAP Settings\",\n \"name\": \"LDAP Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Register OAuth Client App\",\n \"label\": \"OAuth Client\",\n \"name\": \"OAuth Client\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for OAuth Provider\",\n \"label\": \"OAuth Provider Settings\",\n \"name\": \"OAuth Provider Settings\",\n \"type\": \"doctype\"\n }\n]" | |||||
"links": "[\n {\n \"description\": \"Enter keys to enable login via Facebook, Google, GitHub.\",\n \"label\": \"Social Login Key\",\n \"name\": \"Social Login Key\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Ldap settings\",\n \"label\": \"LDAP Settings\",\n \"name\": \"LDAP Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Register OAuth Client App\",\n \"label\": \"OAuth Client\",\n \"name\": \"OAuth Client\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for OAuth Provider\",\n \"label\": \"OAuth Provider Settings\",\n \"name\": \"OAuth Provider Settings\",\n \"type\": \"doctype\"\n }\n ,\n {\n \"description\": \"Connect to any OAuth Provider\",\n \"label\": \"Connected App\",\n \"name\": \"Connected App\",\n \"type\": \"doctype\"\n }\n]" | |||||
}, | }, | ||||
{ | { | ||||
"hidden": 0, | "hidden": 0, | ||||
@@ -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 |
@@ -18,12 +18,9 @@ | |||||
"bucket", | "bucket", | ||||
"endpoint_url", | "endpoint_url", | ||||
"column_break_13", | "column_break_13", | ||||
"region", | |||||
"backup_details_section", | "backup_details_section", | ||||
"frequency", | "frequency", | ||||
"backup_files", | |||||
"column_break_18", | |||||
"backup_limit" | |||||
"backup_files" | |||||
], | ], | ||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
@@ -42,7 +39,7 @@ | |||||
}, | }, | ||||
{ | { | ||||
"default": "1", | "default": "1", | ||||
"description": "Note: By default emails for failed backups are sent.", | |||||
"description": "By default, emails are only sent for failed backups.", | |||||
"fieldname": "send_email_for_successful_backup", | "fieldname": "send_email_for_successful_backup", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"label": "Send Email for Successful Backup" | "label": "Send Email for Successful Backup" | ||||
@@ -73,14 +70,7 @@ | |||||
"reqd": 1 | "reqd": 1 | ||||
}, | }, | ||||
{ | { | ||||
"default": "us-east-1", | |||||
"description": "See https://docs.aws.amazon.com/general/latest/gr/s3.html for details.", | |||||
"fieldname": "region", | |||||
"fieldtype": "Select", | |||||
"label": "Region", | |||||
"options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\naf-south-1\nap-east-1\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-south-1\neu-north-1\nme-south-1\nsa-east-1" | |||||
}, | |||||
{ | |||||
"default": "https://s3.amazonaws.com", | |||||
"fieldname": "endpoint_url", | "fieldname": "endpoint_url", | ||||
"fieldtype": "Data", | "fieldtype": "Data", | ||||
"label": "Endpoint URL" | "label": "Endpoint URL" | ||||
@@ -92,14 +82,6 @@ | |||||
"mandatory_depends_on": "enabled", | "mandatory_depends_on": "enabled", | ||||
"reqd": 1 | "reqd": 1 | ||||
}, | }, | ||||
{ | |||||
"description": "Set to 0 for no limit on the number of backups taken", | |||||
"fieldname": "backup_limit", | |||||
"fieldtype": "Int", | |||||
"label": "Backup Limit", | |||||
"mandatory_depends_on": "enabled", | |||||
"reqd": 1 | |||||
}, | |||||
{ | { | ||||
"depends_on": "enabled", | "depends_on": "enabled", | ||||
"fieldname": "api_access_section", | "fieldname": "api_access_section", | ||||
@@ -142,16 +124,12 @@ | |||||
"fieldname": "backup_files", | "fieldname": "backup_files", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"label": "Backup Files" | "label": "Backup Files" | ||||
}, | |||||
{ | |||||
"fieldname": "column_break_18", | |||||
"fieldtype": "Column Break" | |||||
} | } | ||||
], | ], | ||||
"hide_toolbar": 1, | "hide_toolbar": 1, | ||||
"issingle": 1, | "issingle": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2020-07-27 17:27:21.400000", | |||||
"modified": "2020-12-07 15:30:55.047689", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Integrations", | "module": "Integrations", | ||||
"name": "S3 Backup Settings", | "name": "S3 Backup Settings", | ||||
@@ -172,4 +150,4 @@ | |||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"track_changes": 1 | "track_changes": 1 | ||||
} | |||||
} |
@@ -24,6 +24,7 @@ class S3BackupSettings(Document): | |||||
if not self.endpoint_url: | if not self.endpoint_url: | ||||
self.endpoint_url = 'https://s3.amazonaws.com' | self.endpoint_url = 'https://s3.amazonaws.com' | ||||
conn = boto3.client( | conn = boto3.client( | ||||
's3', | 's3', | ||||
aws_access_key_id=self.access_key_id, | aws_access_key_id=self.access_key_id, | ||||
@@ -31,25 +32,21 @@ class S3BackupSettings(Document): | |||||
endpoint_url=self.endpoint_url | endpoint_url=self.endpoint_url | ||||
) | ) | ||||
bucket_lower = str(self.bucket) | |||||
try: | |||||
conn.list_buckets() | |||||
except ClientError: | |||||
frappe.throw(_("Invalid Access Key ID or Secret Access Key.")) | |||||
try: | try: | ||||
# Head_bucket returns a 200 OK if the bucket exists and have access to it. | # Head_bucket returns a 200 OK if the bucket exists and have access to it. | ||||
conn.head_bucket(Bucket=bucket_lower) | |||||
# Requires ListBucket permission | |||||
conn.head_bucket(Bucket=self.bucket) | |||||
except ClientError as e: | except ClientError as e: | ||||
error_code = e.response['Error']['Code'] | error_code = e.response['Error']['Code'] | ||||
bucket_name = frappe.bold(self.bucket) | |||||
if error_code == '403': | if error_code == '403': | ||||
frappe.throw(_("Do not have permission to access {0} bucket.").format(bucket_lower)) | |||||
else: # '400'-Bad request or '404'-Not Found return | |||||
# try to create bucket | |||||
conn.create_bucket(Bucket=bucket_lower, CreateBucketConfiguration={ | |||||
'LocationConstraint': self.region}) | |||||
msg = _("Do not have permission to access bucket {0}.").format(bucket_name) | |||||
elif error_code == '404': | |||||
msg = _("Bucket {0} not found.").format(bucket_name) | |||||
else: | |||||
msg = e.args[0] | |||||
frappe.throw(msg) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@@ -70,11 +67,13 @@ def take_backups_weekly(): | |||||
def take_backups_monthly(): | def take_backups_monthly(): | ||||
take_backups_if("Monthly") | take_backups_if("Monthly") | ||||
def take_backups_if(freq): | def take_backups_if(freq): | ||||
if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")): | if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")): | ||||
if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq: | if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq: | ||||
take_backups_s3() | take_backups_s3() | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def take_backups_s3(retry_count=0): | def take_backups_s3(retry_count=0): | ||||
try: | try: | ||||
@@ -146,42 +145,13 @@ def backup_to_s3(): | |||||
if files_filename: | if files_filename: | ||||
upload_file_to_s3(files_filename, folder, conn, bucket) | upload_file_to_s3(files_filename, folder, conn, bucket) | ||||
delete_old_backups(doc.backup_limit, bucket) | |||||
def upload_file_to_s3(filename, folder, conn, bucket): | def upload_file_to_s3(filename, folder, conn, bucket): | ||||
destpath = os.path.join(folder, os.path.basename(filename)) | destpath = os.path.join(folder, os.path.basename(filename)) | ||||
try: | try: | ||||
print("Uploading file:", filename) | print("Uploading file:", filename) | ||||
conn.upload_file(filename, bucket, destpath) | |||||
conn.upload_file(filename, bucket, destpath) # Requires PutObject permission | |||||
except Exception as e: | except Exception as e: | ||||
frappe.log_error() | frappe.log_error() | ||||
print("Error uploading: %s" % (e)) | print("Error uploading: %s" % (e)) | ||||
def delete_old_backups(limit, bucket): | |||||
all_backups = [] | |||||
doc = frappe.get_single("S3 Backup Settings") | |||||
backup_limit = int(limit) | |||||
s3 = boto3.resource( | |||||
's3', | |||||
aws_access_key_id=doc.access_key_id, | |||||
aws_secret_access_key=doc.get_password('secret_access_key'), | |||||
endpoint_url=doc.endpoint_url or 'https://s3.amazonaws.com' | |||||
) | |||||
bucket = s3.Bucket(bucket) | |||||
objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Delimiter='/') | |||||
if objects: | |||||
for obj in objects.get('CommonPrefixes'): | |||||
all_backups.append(obj.get('Prefix')) | |||||
oldest_backup = sorted(all_backups)[0] if all_backups else '' | |||||
if len(all_backups) > backup_limit: | |||||
print("Deleting Backup: {0}".format(oldest_backup)) | |||||
for obj in bucket.objects.filter(Prefix=oldest_backup): | |||||
# delete all keys that are inside the oldest_backup | |||||
s3.Object(bucket.name, obj.key).delete() |
@@ -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 | ||||
@@ -591,7 +597,7 @@ class DatabaseQuery(object): | |||||
self.match_conditions.append("`tab{0}`.`owner` = {1}".format(self.doctype, | self.match_conditions.append("`tab{0}`.`owner` = {1}".format(self.doctype, | ||||
frappe.db.escape(self.user, percent=False))) | frappe.db.escape(self.user, percent=False))) | ||||
# add user permission only if role has read perm | # add user permission only if role has read perm | ||||
elif role_permissions.get("read"): | |||||
elif role_permissions.get("read") or role_permissions.get("select"): | |||||
# get user permissions | # get user permissions | ||||
user_permissions = frappe.permissions.get_user_permissions(self.user) | user_permissions = frappe.permissions.get_user_permissions(self.user) | ||||
self.add_user_permissions(user_permissions) | self.add_user_permissions(user_permissions) | ||||
@@ -76,7 +76,12 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa | |||||
delete_from_table(doctype, name, ignore_doctypes, None) | delete_from_table(doctype, name, ignore_doctypes, None) | ||||
if not (for_reload or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_uninstall or frappe.flags.in_test): | |||||
if frappe.conf.developer_mode and not doc.custom and not ( | |||||
for_reload | |||||
or frappe.flags.in_migrate | |||||
or frappe.flags.in_install | |||||
or frappe.flags.in_uninstall | |||||
): | |||||
try: | try: | ||||
delete_controllers(name, doc.module) | delete_controllers(name, doc.module) | ||||
except (FileNotFoundError, OSError, KeyError): | except (FileNotFoundError, OSError, KeyError): | ||||
@@ -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 = {} | ||||
@@ -450,6 +450,25 @@ class Meta(Document): | |||||
return self.high_permlevel_fields | return self.high_permlevel_fields | ||||
def get_permlevel_access(self, permission_type='read', parenttype=None): | |||||
has_access_to = [] | |||||
roles = frappe.get_roles() | |||||
for perm in self.get_permissions(parenttype): | |||||
if perm.role in roles and perm.permlevel > 0 and perm.get(permission_type): | |||||
if perm.permlevel not in has_access_to: | |||||
has_access_to.append(perm.permlevel) | |||||
return has_access_to | |||||
def get_permissions(self, parenttype=None): | |||||
if self.istable and parenttype: | |||||
# use parent permissions | |||||
permissions = frappe.get_meta(parenttype).permissions | |||||
else: | |||||
permissions = self.get('permissions', []) | |||||
return permissions | |||||
def get_dashboard_data(self): | def get_dashboard_data(self): | ||||
'''Returns dashboard setup related to this doctype. | '''Returns dashboard setup related to this doctype. | ||||
@@ -484,6 +503,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 | ||||
@@ -49,9 +57,7 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F | |||||
old_doc = frappe.get_doc(doctype, old) | old_doc = frappe.get_doc(doctype, old) | ||||
out = old_doc.run_method("before_rename", old, new, merge) or {} | out = old_doc.run_method("before_rename", old, new, merge) or {} | ||||
new = (out.get("new") or new) if isinstance(out, dict) else (out or new) | new = (out.get("new") or new) if isinstance(out, dict) else (out or new) | ||||
if doctype != "DocType": | |||||
new = validate_rename(doctype, new, meta, merge, force, ignore_permissions) | |||||
new = validate_rename(doctype, new, meta, merge, force, ignore_permissions) | |||||
if not merge: | if not merge: | ||||
rename_parent_and_child(doctype, old, new, meta) | rename_parent_and_child(doctype, old, new, meta) | ||||
@@ -250,6 +256,7 @@ def update_link_field_values(link_fields, old, new, doctype): | |||||
pass | pass | ||||
else: | else: | ||||
parent = field['parent'] | parent = field['parent'] | ||||
docfield = field["fieldname"] | |||||
# Handles the case where one of the link fields belongs to | # Handles the case where one of the link fields belongs to | ||||
# the DocType being renamed. | # the DocType being renamed. | ||||
@@ -261,11 +268,8 @@ def update_link_field_values(link_fields, old, new, doctype): | |||||
if parent == new and doctype == "DocType": | if parent == new and doctype == "DocType": | ||||
parent = old | parent = old | ||||
frappe.db.sql(""" | |||||
update `tab{table_name}` set `{fieldname}`=%s | |||||
where `{fieldname}`=%s""".format( | |||||
table_name=parent, | |||||
fieldname=field['fieldname']), (new, old)) | |||||
frappe.db.set_value(parent, {docfield: old}, docfield, new) | |||||
# update cached link_fields as per new | # update cached link_fields as per new | ||||
if doctype=='DocType' and field['parent'] == old: | if doctype=='DocType' and field['parent'] == old: | ||||
field['parent'] = new | field['parent'] = new | ||||
@@ -53,14 +53,17 @@ def get_transitions(doc, workflow = None, raise_exception=False): | |||||
return transitions | return transitions | ||||
def get_workflow_safe_globals(): | def get_workflow_safe_globals(): | ||||
# access to frappe.db.get_value and frappe.db.get_list | |||||
# access to frappe.db.get_value, frappe.db.get_list, and date time utils. | |||||
return dict( | return dict( | ||||
frappe=frappe._dict( | frappe=frappe._dict( | ||||
db=frappe._dict( | |||||
get_value=frappe.db.get_value, | |||||
get_list=frappe.db.get_list | |||||
db=frappe._dict(get_value=frappe.db.get_value, get_list=frappe.db.get_list), | |||||
session=frappe.session, | |||||
utils=frappe._dict( | |||||
now_datetime=frappe.utils.now_datetime, | |||||
add_to_date=frappe.utils.add_to_date, | |||||
get_datetime=frappe.utils.get_datetime, | |||||
now=frappe.utils.now, | |||||
), | ), | ||||
session=frappe.session | |||||
) | ) | ||||
) | ) | ||||
@@ -117,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: | ||||
@@ -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 | ||||
@@ -397,7 +398,8 @@ def set_user_permission_if_allowed(doctype, name, user, with_message=False): | |||||
if get_role_permissions(frappe.get_meta(doctype), user).set_user_permissions!=1: | if get_role_permissions(frappe.get_meta(doctype), user).set_user_permissions!=1: | ||||
add_user_permission(doctype, name, user) | add_user_permission(doctype, name, user) | ||||
def add_user_permission(doctype, name, user, ignore_permissions=False, applicable_for=None, is_default=0): | |||||
def add_user_permission(doctype, name, user, ignore_permissions=False, applicable_for=None, | |||||
is_default=0, hide_descendants=0): | |||||
'''Add user permission''' | '''Add user permission''' | ||||
from frappe.core.doctype.user_permission.user_permission import user_permission_exists | from frappe.core.doctype.user_permission.user_permission import user_permission_exists | ||||
@@ -412,6 +414,7 @@ def add_user_permission(doctype, name, user, ignore_permissions=False, applicabl | |||||
for_value=name, | for_value=name, | ||||
is_default=is_default, | is_default=is_default, | ||||
applicable_for=applicable_for, | applicable_for=applicable_for, | ||||
hide_descendants=hide_descendants, | |||||
)).insert(ignore_permissions=ignore_permissions) | )).insert(ignore_permissions=ignore_permissions) | ||||
def remove_user_permission(doctype, name, user): | def remove_user_permission(doctype, name, user): | ||||
@@ -66,7 +66,7 @@ frappe.ui.form.on("Print Format", { | |||||
hide_absolute_value_field: function (frm) { | hide_absolute_value_field: function (frm) { | ||||
// TODO: make it work with frm.doc.doc_type | // TODO: make it work with frm.doc.doc_type | ||||
// Problem: frm isn't updated in some random cases | // Problem: frm isn't updated in some random cases | ||||
const doctype = locals[frm.doc.doctype][frm.doc.name]; | |||||
const doctype = locals[frm.doc.doctype][frm.doc.name].doc_type; | |||||
if (doctype) { | if (doctype) { | ||||
frappe.model.with_doctype(doctype, () => { | frappe.model.with_doctype(doctype, () => { | ||||
const meta = frappe.get_meta(doctype); | const meta = frappe.get_meta(doctype); | ||||
@@ -201,17 +201,17 @@ | |||||
{ | { | ||||
"default": "0", | "default": "0", | ||||
"depends_on": "doc_type", | "depends_on": "doc_type", | ||||
"description": "If checked, negative numberic values of Currency, Quantity or Count would be shown as positive", | |||||
"description": "If checked, negative numeric values of Currency, Quantity or Count would be shown as positive", | |||||
"fieldname": "absolute_value", | "fieldname": "absolute_value", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"label": "Show absolute values" | |||||
"label": "Show Absolute Values" | |||||
} | } | ||||
], | ], | ||||
"icon": "fa fa-print", | "icon": "fa fa-print", | ||||
"idx": 1, | "idx": 1, | ||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2020-12-10 18:58:55.598269", | |||||
"modified": "2020-12-14 11:38:49.132061", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Printing", | "module": "Printing", | ||||
"name": "Print Format", | "name": "Print Format", | ||||
@@ -161,6 +161,7 @@ | |||||
"public/js/frappe/router_history.js", | "public/js/frappe/router_history.js", | ||||
"public/js/frappe/defaults.js", | "public/js/frappe/defaults.js", | ||||
"public/js/frappe/roles_editor.js", | "public/js/frappe/roles_editor.js", | ||||
"public/js/frappe/module_editor.js", | |||||
"public/js/frappe/microtemplate.js", | "public/js/frappe/microtemplate.js", | ||||
"public/js/frappe/ui/page.html", | "public/js/frappe/ui/page.html", | ||||
@@ -307,6 +308,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", | ||||