Merge branch 'version-13-beta-pre-release' into version-13-beta
This commit is contained in:
commit
3dec12725c
167 changed files with 3197 additions and 1678 deletions
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
|
|
@ -15,7 +15,7 @@ If your issue is not clear or does not meet the guidelines, then it will be clos
|
|||
### 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.
|
||||
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.
|
||||
|
||||
### Bug Report Guidelines
|
||||
|
|
|
|||
12
.travis.yml
12
.travis.yml
|
|
@ -31,12 +31,12 @@ matrix:
|
|||
- name: "Python 3.7 MariaDB"
|
||||
python: 3.7
|
||||
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"
|
||||
python: 3.7
|
||||
env: DB=postgres TYPE=server
|
||||
script: bench --site test_site run-tests --coverage
|
||||
script: bench --verbose --site test_site run-tests --coverage
|
||||
|
||||
- name: "Cypress"
|
||||
python: 3.7
|
||||
|
|
@ -104,11 +104,11 @@ install:
|
|||
|
||||
- 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
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,31 @@ context('Depends On', () => {
|
|||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call('frappe.tests.ui_test_helpers.create_doctype', {
|
||||
return frappe.xcall('frappe.tests.ui_test_helpers.create_child_doctype', {
|
||||
name: 'Child Test Depends On',
|
||||
fields: [
|
||||
{
|
||||
"label": "Child Test Field",
|
||||
"fieldname": "child_test_field",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
},
|
||||
{
|
||||
"label": "Child Dependant Field",
|
||||
"fieldname": "child_dependant_field",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
},
|
||||
{
|
||||
"label": "Child Display Dependant Field",
|
||||
"fieldname": "child_display_dependant_field",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
},
|
||||
]
|
||||
});
|
||||
}).then(frappe => {
|
||||
return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', {
|
||||
name: 'Test Depends On',
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -24,6 +48,13 @@ context('Depends On', () => {
|
|||
"fieldtype": "Data",
|
||||
'depends_on': "eval:doc.test_field=='Value'"
|
||||
},
|
||||
{
|
||||
"label": "Child Test Depends On Field",
|
||||
"fieldname": "child_test_depends_on_field",
|
||||
"fieldtype": "Table",
|
||||
'read_only_depends_on': "eval:doc.test_field=='Some Other Value'",
|
||||
'options': "Child Test Depends On"
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
|
@ -48,6 +79,30 @@ context('Depends On', () => {
|
|||
cy.get('body').click();
|
||||
cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled');
|
||||
});
|
||||
it('should set the table and its fields as read only depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.fill_field('dependant_field', 'Some Value');
|
||||
//cy.fill_field('test_field', 'Some Other Value');
|
||||
cy.get('.frappe-control[data-fieldname="child_test_depends_on_field"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').find('[data-idx="1"]').as('row1');
|
||||
cy.get('@row1').find('.btn-open-row').click();
|
||||
cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid');
|
||||
//cy.get('@row1-form_in_grid').find('')
|
||||
cy.fill_table_field('child_test_depends_on_field', '1', 'child_test_field', 'Some Value');
|
||||
cy.fill_table_field('child_test_depends_on_field', '1', 'child_dependant_field', 'Some Other Value');
|
||||
|
||||
cy.get('@row1-form_in_grid').find('.octicon-triangle-up').click();
|
||||
|
||||
// set the table to read-only
|
||||
cy.fill_field('test_field', 'Some Other Value');
|
||||
|
||||
// grid row form fields should be read-only
|
||||
cy.get('@row1').find('.btn-open-row').click();
|
||||
|
||||
cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_test_field"]').should('be.disabled');
|
||||
cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_dependant_field"]').should('be.disabled');
|
||||
});
|
||||
it('should display the field depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible');
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => {
|
|||
|
||||
Cypress.Commands.add('create_records', doc => {
|
||||
return cy
|
||||
.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
|
||||
.call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc})
|
||||
.then(r => r.message);
|
||||
});
|
||||
|
||||
|
|
@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
|
|||
if (fieldtype === 'Select') {
|
||||
cy.get('@input').select(value);
|
||||
} else {
|
||||
cy.get('@input').type(value, { waitForAnimations: false, force: true });
|
||||
cy.get('@input').type(value, {waitForAnimations: false, force: true});
|
||||
}
|
||||
return cy.get('@input');
|
||||
});
|
||||
|
|
@ -204,8 +204,43 @@ Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
|
|||
return cy.get(selector);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
|
||||
cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input');
|
||||
|
||||
if (['Date', 'Time', 'Datetime'].includes(fieldtype)) {
|
||||
cy.get('@input').click().wait(200);
|
||||
cy.get('.datepickers-container .datepicker.active').should('exist');
|
||||
}
|
||||
if (fieldtype === 'Time') {
|
||||
cy.get('@input').clear().wait(200);
|
||||
}
|
||||
|
||||
if (fieldtype === 'Select') {
|
||||
cy.get('@input').select(value);
|
||||
} else {
|
||||
cy.get('@input').type(value, {waitForAnimations: false, force: true});
|
||||
}
|
||||
return cy.get('@input');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => {
|
||||
let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`;
|
||||
selector += ` [data-idx="${row_idx}"]`;
|
||||
selector += ` .form-in-grid`;
|
||||
|
||||
if (fieldtype === 'Text Editor') {
|
||||
selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
|
||||
} else if (fieldtype === 'Code') {
|
||||
selector += ` [data-fieldname="${fieldname}"] .ace_text-input`;
|
||||
} else {
|
||||
selector += ` .form-control[data-fieldname="${fieldname}"]`;
|
||||
}
|
||||
|
||||
return cy.get(selector);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('awesomebar', text => {
|
||||
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 });
|
||||
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('new_form', doctype => {
|
||||
|
|
|
|||
|
|
@ -23,11 +23,12 @@ if PY2:
|
|||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
__version__ = '13.0.0-beta.9'
|
||||
__version__ = '13.0.0-beta.10'
|
||||
|
||||
__title__ = "Frappe Framework"
|
||||
|
||||
local = Local()
|
||||
controllers = {}
|
||||
|
||||
class _dict(dict):
|
||||
"""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
|
||||
})
|
||||
local.rollback_observers = []
|
||||
local.before_commit = []
|
||||
local.test_objects = {}
|
||||
|
||||
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 wide: [optional] Show wide modal
|
||||
"""
|
||||
from frappe.utils import encode
|
||||
from frappe.utils import strip_html_tags
|
||||
|
||||
msg = safe_decode(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
|
||||
|
||||
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:
|
||||
out.title = title
|
||||
|
|
@ -628,6 +630,21 @@ def clear_cache(user=None, doctype=None):
|
|||
|
||||
local.role_permissions = {}
|
||||
|
||||
def only_has_select_perm(doctype, user=None, ignore_permissions=False):
|
||||
if ignore_permissions:
|
||||
return False
|
||||
|
||||
if not user:
|
||||
user = local.session.user
|
||||
|
||||
import frappe.permissions
|
||||
permissions = frappe.permissions.get_role_permissions(doctype, user=user)
|
||||
|
||||
if permissions.get('select') and not permissions.get('read'):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False):
|
||||
"""Raises `frappe.PermissionError` if not permitted.
|
||||
|
||||
|
|
@ -946,7 +963,11 @@ def get_installed_apps(sort=False, frappe_last=False):
|
|||
connect()
|
||||
|
||||
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 "[]")
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import os
|
|||
from six import iteritems
|
||||
import logging
|
||||
|
||||
from werkzeug.wrappers import Request
|
||||
from werkzeug.local import LocalManager
|
||||
from werkzeug.wrappers import Request, Response
|
||||
from werkzeug.exceptions import HTTPException, NotFound
|
||||
from werkzeug.middleware.profiler import ProfilerMiddleware
|
||||
from werkzeug.middleware.shared_data import SharedDataMiddleware
|
||||
|
|
@ -57,19 +57,22 @@ def application(request):
|
|||
frappe.monitor.start()
|
||||
frappe.rate_limiter.apply()
|
||||
|
||||
if frappe.local.form_dict.cmd:
|
||||
if request.method == "OPTIONS":
|
||||
response = Response()
|
||||
|
||||
elif frappe.form_dict.cmd:
|
||||
response = frappe.handler.handle()
|
||||
|
||||
elif frappe.request.path.startswith("/api/"):
|
||||
elif request.path.startswith("/api/"):
|
||||
response = frappe.api.handle()
|
||||
|
||||
elif frappe.request.path.startswith('/backups'):
|
||||
elif request.path.startswith('/backups'):
|
||||
response = frappe.utils.response.download_backup(request.path)
|
||||
|
||||
elif frappe.request.path.startswith('/private/files/'):
|
||||
elif request.path.startswith('/private/files/'):
|
||||
response = frappe.utils.response.download_private_file(request.path)
|
||||
|
||||
elif frappe.local.request.method in ('GET', 'HEAD', 'POST'):
|
||||
elif request.method in ('GET', 'HEAD', 'POST'):
|
||||
response = frappe.website.render.render()
|
||||
|
||||
else:
|
||||
|
|
@ -88,13 +91,9 @@ def application(request):
|
|||
rollback = after_request(rollback)
|
||||
|
||||
finally:
|
||||
if frappe.local.request.method in ("POST", "PUT") and frappe.db and rollback:
|
||||
if request.method in ("POST", "PUT") and frappe.db and rollback:
|
||||
frappe.db.rollback()
|
||||
|
||||
# set cookies
|
||||
if response and hasattr(frappe.local, 'cookie_manager'):
|
||||
frappe.local.cookie_manager.flush_cookies(response=response)
|
||||
|
||||
frappe.rate_limiter.update()
|
||||
frappe.monitor.stop(response)
|
||||
frappe.recorder.dump()
|
||||
|
|
@ -110,9 +109,7 @@ def application(request):
|
|||
"http_status_code": getattr(response, "status_code", "NOTFOUND")
|
||||
})
|
||||
|
||||
if response and hasattr(frappe.local, 'rate_limiter'):
|
||||
response.headers.extend(frappe.local.rate_limiter.headers())
|
||||
|
||||
process_response(response)
|
||||
frappe.destroy()
|
||||
|
||||
return response
|
||||
|
|
@ -134,7 +131,46 @@ def init_request(request):
|
|||
|
||||
make_form_dict(request)
|
||||
|
||||
frappe.local.http_request = frappe.auth.HTTPRequest()
|
||||
if request.method != "OPTIONS":
|
||||
frappe.local.http_request = frappe.auth.HTTPRequest()
|
||||
|
||||
def process_response(response):
|
||||
if not response:
|
||||
return
|
||||
|
||||
# set cookies
|
||||
if hasattr(frappe.local, 'cookie_manager'):
|
||||
frappe.local.cookie_manager.flush_cookies(response=response)
|
||||
|
||||
# rate limiter headers
|
||||
if hasattr(frappe.local, 'rate_limiter'):
|
||||
response.headers.extend(frappe.local.rate_limiter.headers())
|
||||
|
||||
# CORS headers
|
||||
if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors:
|
||||
set_cors_headers(response)
|
||||
|
||||
def set_cors_headers(response):
|
||||
origin = frappe.request.headers.get('Origin')
|
||||
if not origin:
|
||||
return
|
||||
|
||||
allow_cors = frappe.conf.allow_cors
|
||||
if allow_cors != "*":
|
||||
if not isinstance(allow_cors, list):
|
||||
allow_cors = [allow_cors]
|
||||
|
||||
if origin not in allow_cors:
|
||||
return
|
||||
|
||||
response.headers.extend({
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,'
|
||||
'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,'
|
||||
'Cache-Control,Content-Type')
|
||||
})
|
||||
|
||||
def make_form_dict(request):
|
||||
import json
|
||||
|
|
|
|||
|
|
@ -54,10 +54,12 @@ frappe.ui.form.on('Auto Repeat', {
|
|||
|
||||
toggle_submit_on_creation: function(frm) {
|
||||
// submit on creation checkbox
|
||||
frappe.model.with_doctype(frm.doc.reference_doctype, () => {
|
||||
let meta = frappe.get_meta(frm.doc.reference_doctype);
|
||||
frm.toggle_display('submit_on_creation', meta.is_submittable);
|
||||
});
|
||||
if (frm.doc.reference_doctype) {
|
||||
frappe.model.with_doctype(frm.doc.reference_doctype, () => {
|
||||
let meta = frappe.get_meta(frm.doc.reference_doctype);
|
||||
frm.toggle_display('submit_on_creation', meta.is_submittable);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
template: function(frm) {
|
||||
|
|
@ -100,10 +102,7 @@ frappe.ui.form.on('Auto Repeat', {
|
|||
|
||||
frappe.auto_repeat.render_schedule = function(frm) {
|
||||
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.add_section(
|
||||
frappe.render_template("auto_repeat_schedule", {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@
|
|||
"repeat_on_last_day",
|
||||
"column_break_12",
|
||||
"next_schedule_date",
|
||||
"section_break_16",
|
||||
"repeat_on_days",
|
||||
"notification",
|
||||
"notify_by_email",
|
||||
"recipients",
|
||||
|
|
@ -189,15 +191,27 @@
|
|||
"fieldtype": "Check",
|
||||
"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",
|
||||
"fieldname": "submit_on_creation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Submit on Creation"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.frequency==='Weekly';",
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-12-10 10:43:13.449172",
|
||||
"modified": "2021-01-12 09:24:49.719611",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Auto Repeat",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from datetime import timedelta
|
||||
from frappe.desk.form import assign_to
|
||||
from frappe.utils.jinja import validate_template
|
||||
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.core.doctype.communication.email import make
|
||||
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}
|
||||
|
||||
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
|
||||
|
||||
class AutoRepeat(Document):
|
||||
def validate(self):
|
||||
|
|
@ -24,6 +26,7 @@ class AutoRepeat(Document):
|
|||
self.validate_submit_on_creation()
|
||||
self.validate_dates()
|
||||
self.validate_email_id()
|
||||
self.validate_auto_repeat_days()
|
||||
self.set_dates()
|
||||
self.update_auto_repeat_id()
|
||||
self.unlink_if_applicable()
|
||||
|
|
@ -49,7 +52,7 @@ class AutoRepeat(Document):
|
|||
if self.disabled:
|
||||
self.next_schedule_date = None
|
||||
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):
|
||||
if self.status == 'Completed' or self.disabled:
|
||||
|
|
@ -88,6 +91,12 @@ class AutoRepeat(Document):
|
|||
else:
|
||||
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):
|
||||
#check if document is already on 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)
|
||||
|
||||
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 = {
|
||||
"reference_document": self.reference_document,
|
||||
"frequency": self.frequency,
|
||||
|
|
@ -122,8 +131,7 @@ class AutoRepeat(Document):
|
|||
schedule_details.append(row)
|
||||
|
||||
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)):
|
||||
row = {
|
||||
|
|
@ -132,8 +140,7 @@ class AutoRepeat(Document):
|
|||
"next_scheduled_date" : next_date
|
||||
}
|
||||
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
|
||||
|
||||
|
|
@ -211,6 +218,75 @@ class AutoRepeat(Document):
|
|||
new_doc.set('from_date', from_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):
|
||||
"""Notify concerned people about recurring document generation"""
|
||||
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):
|
||||
dt = getdate(dt)
|
||||
dt += relativedelta(months=mcount, day=day)
|
||||
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
|
||||
def make_auto_repeat_entry():
|
||||
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)
|
||||
frappe.enqueue(enqueued_method, data=data)
|
||||
|
||||
|
||||
def create_repeated_entries(data):
|
||||
for d in data:
|
||||
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:
|
||||
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:
|
||||
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)
|
||||
|
||||
|
||||
def get_auto_repeat_entries(date=None):
|
||||
if not date:
|
||||
date = getdate(today())
|
||||
|
|
@ -358,6 +418,7 @@ def get_auto_repeat_entries(date=None):
|
|||
['status', '=', 'Active']
|
||||
])
|
||||
|
||||
|
||||
#called through hooks
|
||||
def set_auto_repeat_as_completed():
|
||||
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
|
||||
|
|
@ -367,6 +428,7 @@ def set_auto_repeat_as_completed():
|
|||
doc.status = 'Completed'
|
||||
doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None):
|
||||
if not start_date:
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@ import unittest
|
|||
|
||||
import frappe
|
||||
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
|
||||
|
||||
|
||||
def add_custom_fields():
|
||||
df = dict(
|
||||
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'))
|
||||
|
||||
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):
|
||||
start_date = today()
|
||||
end_date = add_months(start_date, 12)
|
||||
|
|
@ -144,7 +189,8 @@ def make_auto_repeat(**args):
|
|||
'notify_by_email': args.notify or 0,
|
||||
'recipients': args.recipients or "",
|
||||
'subject': args.subject or "",
|
||||
'message': args.message or ""
|
||||
'message': args.message or "",
|
||||
'repeat_on_days': args.days or []
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
return doc
|
||||
|
|
|
|||
0
frappe/automation/doctype/auto_repeat_day/__init__.py
Normal file
0
frappe/automation/doctype/auto_repeat_day/__init__.py
Normal file
|
|
@ -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
|
||||
}
|
||||
10
frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py
Normal file
10
frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py
Normal file
|
|
@ -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")
|
||||
|
||||
def clear_doctype_cache(doctype=None):
|
||||
clear_controller_cache(doctype)
|
||||
cache = frappe.cache()
|
||||
|
||||
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache):
|
||||
|
|
@ -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_document_cache()
|
||||
|
||||
def clear_controller_cache(doctype=None):
|
||||
if not doctype:
|
||||
del frappe.controllers
|
||||
frappe.controllers = {}
|
||||
return
|
||||
|
||||
for site_controllers in frappe.controllers.values():
|
||||
site_controllers.pop(doctype, None)
|
||||
|
||||
def get_doctype_map(doctype, name, filters=None, order_by=None):
|
||||
cache = frappe.cache()
|
||||
cache_key = frappe.scrub(doctype) + '_map'
|
||||
|
|
|
|||
21
frappe/change_log/v13/v13_0_0-beta_10.md
Normal file
21
frappe/change_log/v13/v13_0_0-beta_10.md
Normal file
|
|
@ -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",
|
||||
"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:
|
||||
# use sql, so that we do not mess with the timestamp
|
||||
frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec
|
||||
(json.dumps(_comments[-50:]), reference_name))
|
||||
(json.dumps(_comments[-100:]), reference_name))
|
||||
|
||||
except Exception as e:
|
||||
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2017-01-11 04:21:35.217943",
|
||||
|
|
@ -13,6 +14,7 @@
|
|||
"column_break_2",
|
||||
"permlevel",
|
||||
"section_break_4",
|
||||
"select",
|
||||
"read",
|
||||
"write",
|
||||
"create",
|
||||
|
|
@ -211,9 +213,16 @@
|
|||
"fieldtype": "Data",
|
||||
"label": "Reference Document Type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "select",
|
||||
"fieldtype": "Check",
|
||||
"label": "Select"
|
||||
}
|
||||
],
|
||||
"modified": "2019-10-31 16:58:16.157079",
|
||||
"links": [],
|
||||
"modified": "2020-12-03 15:20:48.296730",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Custom DocPerm",
|
||||
|
|
|
|||
|
|
@ -751,7 +751,7 @@ class Row:
|
|||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"message": _("{0} is a mandatory field asdadsf").format(id_field.label),
|
||||
"message": _("{0} is a mandatory field").format(id_field.label),
|
||||
}
|
||||
)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,775 +1,229 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"beta": 0,
|
||||
"creation": "2013-02-22 01:27:33",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"role_and_level",
|
||||
"role",
|
||||
"if_owner",
|
||||
"column_break_2",
|
||||
"permlevel",
|
||||
"section_break_4",
|
||||
"select",
|
||||
"read",
|
||||
"write",
|
||||
"create",
|
||||
"delete",
|
||||
"column_break_8",
|
||||
"submit",
|
||||
"cancel",
|
||||
"amend",
|
||||
"additional_permissions",
|
||||
"report",
|
||||
"export",
|
||||
"import",
|
||||
"set_user_permissions",
|
||||
"column_break_19",
|
||||
"share",
|
||||
"print",
|
||||
"email"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "role_and_level",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Role and Level",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Role and Level"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "role",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Role",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "role",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Role",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "150px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"description": "Apply this rule if the User is the Owner",
|
||||
"fieldname": "if_owner",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "If user is the owner",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "If user is the owner"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Level",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "40px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "40px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Permissions",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Permissions"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "read",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Read",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "read",
|
||||
"oldfieldtype": "Check",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "write",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Write",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "write",
|
||||
"oldfieldtype": "Check",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "create",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Create",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "create",
|
||||
"oldfieldtype": "Check",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "delete",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Delete",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Delete"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "submit",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Submit",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "submit",
|
||||
"oldfieldtype": "Check",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "cancel",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Cancel",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "cancel",
|
||||
"oldfieldtype": "Check",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "amend",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Amend",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "amend",
|
||||
"oldfieldtype": "Check",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "additional_permissions",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Additional Permissions",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Additional Permissions"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "report",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Report",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "export",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Export",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Export"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "import",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Import",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Import"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"description": "This role update User Permissions for a user",
|
||||
"fieldname": "set_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Set User Permissions",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Set User Permissions"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_19",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "share",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Share",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Share"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "print",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Print",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Print"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Email",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Email"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "select",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Select"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-05-29 11:54:38.613936",
|
||||
"links": [],
|
||||
"modified": "2020-12-03 15:15:30.488212",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocPerm",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
from __future__ import unicode_literals
|
||||
import re, copy, os, shutil
|
||||
import json
|
||||
from frappe.cache_manager import clear_user_cache
|
||||
from frappe.cache_manager import clear_user_cache, clear_controller_cache
|
||||
|
||||
# imports - third party imports
|
||||
import six
|
||||
|
|
@ -290,9 +290,15 @@ class DocType(Document):
|
|||
|
||||
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.make_controller_template()
|
||||
|
||||
|
|
@ -382,13 +388,10 @@ class DocType(Document):
|
|||
if merge:
|
||||
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):
|
||||
"""Change table name using `RENAME TABLE` if table exists. Or update
|
||||
`doctype` property for Single type."""
|
||||
|
||||
if self.issingle:
|
||||
frappe.db.sql("""update tabSingles set doctype=%s where doctype=%s""", (new, old))
|
||||
frappe.db.sql("""update tabSingles set value=%s
|
||||
|
|
@ -398,6 +401,18 @@ class DocType(Document):
|
|||
"mariadb": f"RENAME TABLE `tab{old}` 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):
|
||||
# move files
|
||||
|
|
@ -1000,10 +1015,10 @@ def validate_fields(meta):
|
|||
check_sort_field(meta)
|
||||
check_image_field(meta)
|
||||
|
||||
def validate_permissions_for_doctype(doctype, for_remove=False):
|
||||
def validate_permissions_for_doctype(doctype, for_remove=False, alert=False):
|
||||
"""Validates if permissions are set correctly."""
|
||||
doctype = frappe.get_doc("DocType", doctype)
|
||||
validate_permissions(doctype, for_remove)
|
||||
validate_permissions(doctype, for_remove, alert=alert)
|
||||
|
||||
# save permissions
|
||||
for perm in doctype.get("permissions"):
|
||||
|
|
@ -1026,9 +1041,10 @@ def clear_permissions_cache(doctype):
|
|||
""", doctype):
|
||||
frappe.clear_cache(user=user)
|
||||
|
||||
def validate_permissions(doctype, for_remove=False):
|
||||
def validate_permissions(doctype, for_remove=False, alert=False):
|
||||
permissions = doctype.get("permissions")
|
||||
if not permissions:
|
||||
# Some DocTypes may not have permissions by default, don't show alert for them
|
||||
if not permissions and alert:
|
||||
frappe.msgprint(_('No Permissions Specified'), alert=True, indicator='orange')
|
||||
issingle = issubmittable = isimportable = False
|
||||
if doctype:
|
||||
|
|
@ -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)
|
||||
|
||||
def check_atleast_one_set(d):
|
||||
if not d.read and not d.write and not d.submit and not d.cancel and not d.create:
|
||||
if not d.select and not d.read and not d.write and not d.submit and not d.cancel and not d.create:
|
||||
frappe.throw(_("{0}: No basic permissions set").format(get_txt(d)))
|
||||
|
||||
def check_double(d):
|
||||
|
|
|
|||
|
|
@ -6,8 +6,19 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import evaluate_filters
|
||||
from frappe import _
|
||||
|
||||
class DocumentNamingRule(Document):
|
||||
def validate(self):
|
||||
self.validate_fields_in_conditions()
|
||||
|
||||
def validate_fields_in_conditions(self):
|
||||
if self.has_value_changed("document_type"):
|
||||
docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields]
|
||||
for condition in self.conditions:
|
||||
if condition.field not in docfields:
|
||||
frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type)))
|
||||
|
||||
def apply(self, doc):
|
||||
'''
|
||||
Apply naming rules for the given document. Will set `name` if the rule is matched.
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class ModuleDef(Document):
|
|||
def on_trash(self):
|
||||
"""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
|
||||
|
||||
modules = None
|
||||
|
|
|
|||
0
frappe/core/doctype/module_profile/__init__.py
Normal file
0
frappe/core/doctype/module_profile/__init__.py
Normal file
19
frappe/core/doctype/module_profile/module_profile.js
Normal file
19
frappe/core/doctype/module_profile/module_profile.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
60
frappe/core/doctype/module_profile/module_profile.json
Normal file
60
frappe/core/doctype/module_profile/module_profile.json
Normal file
|
|
@ -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
|
||||
}
|
||||
12
frappe/core/doctype/module_profile/module_profile.py
Normal file
12
frappe/core/doctype/module_profile/module_profile.py
Normal file
|
|
@ -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()])
|
||||
32
frappe/core/doctype/module_profile/test_module_profile.py
Normal file
32
frappe/core/doctype/module_profile/test_module_profile.py
Normal file
|
|
@ -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",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Options"
|
||||
},
|
||||
{
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-17 16:15:46.937267",
|
||||
"modified": "2020-12-05 19:20:00.503097",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Report Filter",
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
"fieldname": "doctype_event",
|
||||
"fieldtype": "Select",
|
||||
"label": "DocType Event",
|
||||
"options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
|
||||
"options": "Before Insert\nBefore Validate\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.script_type==='API'",
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-03 22:42:02.708148",
|
||||
"modified": "2021-01-03 18:50:14.767595",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Server Script",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import frappe
|
|||
EVENT_MAP = {
|
||||
'before_insert': 'Before Insert',
|
||||
'after_insert': 'After Insert',
|
||||
'before_validate': 'Before Validate',
|
||||
'validate': 'Before Save',
|
||||
'on_update': 'After Save',
|
||||
'before_submit': 'Before Submit',
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class TestServerScript(unittest.TestCase):
|
|||
def tearDownClass(cls):
|
||||
frappe.db.commit()
|
||||
frappe.db.sql('truncate `tabServer Script`')
|
||||
frappe.cache().delete_key('server_script_map')
|
||||
|
||||
def setUp(self):
|
||||
frappe.cache().delete_value('server_script_map')
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@
|
|||
"collapsible": 1,
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "EMail"
|
||||
"label": "Email"
|
||||
},
|
||||
{
|
||||
"description": "Your organization name and address for the email footer.",
|
||||
|
|
@ -490,4 +490,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"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) {
|
||||
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)
|
||||
.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",
|
||||
"allowed_in_mentions",
|
||||
"email_signature",
|
||||
"email_inbox",
|
||||
"user_emails",
|
||||
"sb_allow_modules",
|
||||
"module_profile",
|
||||
"modules_html",
|
||||
"block_modules",
|
||||
"home_settings",
|
||||
|
|
@ -577,6 +577,12 @@
|
|||
"fieldtype": "Password",
|
||||
"label": "API Secret",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "module_profile",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module Profile",
|
||||
"options": "Module Profile"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
|
|
@ -642,10 +648,15 @@
|
|||
"group": "Activity",
|
||||
"link_doctype": "ToDo",
|
||||
"link_fieldname": "owner"
|
||||
},
|
||||
{
|
||||
"group": "Integrations",
|
||||
"link_doctype": "Token Cache",
|
||||
"link_fieldname": "user"
|
||||
}
|
||||
],
|
||||
"max_attachments": 5,
|
||||
"modified": "2020-08-26 19:48:49.677800",
|
||||
"modified": "2020-10-18 15:18:53.126800",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "User",
|
||||
|
|
@ -679,4 +690,4 @@
|
|||
"sort_order": "DESC",
|
||||
"title_field": "full_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -75,6 +75,7 @@ class User(Document):
|
|||
self.validate_user_email_inbox()
|
||||
ask_pass_update()
|
||||
self.validate_roles()
|
||||
self.validate_allowed_modules()
|
||||
self.validate_user_image()
|
||||
|
||||
if self.language == "Loading...":
|
||||
|
|
@ -85,9 +86,18 @@ class User(Document):
|
|||
|
||||
def validate_roles(self):
|
||||
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):
|
||||
if self.user_image and len(self.user_image) > 2000:
|
||||
|
|
@ -98,16 +108,17 @@ class User(Document):
|
|||
self.share_with_self()
|
||||
clear_notifications(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)
|
||||
frappe.enqueue(
|
||||
'frappe.core.doctype.user.user.create_contact',
|
||||
user=self,
|
||||
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:
|
||||
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
|
||||
if self.time_zone:
|
||||
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})
|
||||
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):
|
||||
users = frappe.get_all('User', filters={'role_profile_name': role_profile})
|
||||
role_profile = frappe.get_doc('Role Profile', role_profile)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
from frappe.core.doctype.user_permission.user_permission import add_user_permissions
|
||||
from frappe.permissions import has_user_permission
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
|
|
@ -10,7 +11,12 @@ import unittest
|
|||
class TestUserPermission(unittest.TestCase):
|
||||
def setUp(self):
|
||||
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):
|
||||
user = create_user('test_default_permission@example.com')
|
||||
|
|
@ -108,6 +114,45 @@ class TestUserPermission(unittest.TestCase):
|
|||
self.assertIsNone(removed_applicable_second)
|
||||
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"):
|
||||
''' create user with role system manager '''
|
||||
if frappe.db.exists('User', email):
|
||||
|
|
@ -119,7 +164,7 @@ def create_user(email, role="System Manager"):
|
|||
user.add_roles(role)
|
||||
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 '''
|
||||
param = {
|
||||
"user": user.name,
|
||||
|
|
@ -127,7 +172,8 @@ def get_params(user, doctype, docname, is_default=0, applicable=None):
|
|||
"docname":docname,
|
||||
"is_default": is_default,
|
||||
"apply_to_all_doctypes": 1,
|
||||
"applicable_doctypes": []
|
||||
"applicable_doctypes": [],
|
||||
"hide_descendants": hide_descendants
|
||||
}
|
||||
if applicable:
|
||||
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',
|
||||
{ user: frm.doc.user }));
|
||||
frm.trigger('set_applicable_for_constraint');
|
||||
frm.trigger('toggle_hide_descendants');
|
||||
},
|
||||
|
||||
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) {
|
||||
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_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2017-07-17 14:25:27.881871",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"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": [
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "User",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "User",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 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",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Allow",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "DocType",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"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",
|
||||
"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",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 1,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "For Value",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "apply_to_all_doctypes",
|
||||
"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",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "applicable_for",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "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",
|
||||
"module": "Core",
|
||||
"name": "User Permission",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"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]
|
||||
}, or_filters={
|
||||
'applicable_for': cstr(self.applicable_for),
|
||||
'apply_to_all_doctypes': 1
|
||||
'apply_to_all_doctypes': 1,
|
||||
'hide_descendants': cstr(self.hide_descendants)
|
||||
}, limit=1)
|
||||
if overlap_exists:
|
||||
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name)
|
||||
|
|
@ -91,13 +92,13 @@ def get_user_permissions(user=None):
|
|||
|
||||
try:
|
||||
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)):
|
||||
|
||||
meta = frappe.get_meta(perm.allow)
|
||||
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)
|
||||
for doc in decendants:
|
||||
add_doc_to_perm(perm, doc, False)
|
||||
|
|
@ -172,8 +173,8 @@ def check_applicable_doc_perm(user, doctype, docname):
|
|||
"allow": doctype,
|
||||
"for_value":docname,
|
||||
})
|
||||
for d in data:
|
||||
applicable.append(d.applicable_for)
|
||||
for permission in data:
|
||||
applicable.append(permission.applicable_for)
|
||||
return applicable
|
||||
|
||||
|
||||
|
|
@ -194,7 +195,8 @@ def add_user_permissions(data):
|
|||
data = json.loads(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", {
|
||||
"user": data.user,
|
||||
"allow": data.doctype,
|
||||
|
|
@ -202,26 +204,27 @@ def add_user_permissions(data):
|
|||
"apply_to_all_doctypes": 1
|
||||
})
|
||||
if data.apply_to_all_doctypes == 1 and not exists:
|
||||
remove_applicable(d, data.user, data.doctype, data.docname)
|
||||
insert_user_perm(data.user, data.doctype, data.docname, 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
|
||||
elif len(data.applicable_doctypes) > 0 and data.apply_to_all_doctypes != 1:
|
||||
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 :
|
||||
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:
|
||||
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 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.user = user
|
||||
user_perm.allow = doctype
|
||||
user_perm.for_value = docname
|
||||
user_perm.is_default = is_default
|
||||
user_perm.hide_descendants = hide_descendants
|
||||
if applicable:
|
||||
user_perm.applicable_for = applicable
|
||||
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.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`
|
||||
WHERE `user`=%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("apply_to_all_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',
|
||||
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"),
|
||||
fieldname: "applicable_doctypes",
|
||||
|
|
@ -214,6 +232,9 @@ frappe.listview_settings['User Permission'] = {
|
|||
dialog.set_df_property("is_default", "hidden", 0);
|
||||
dialog.set_df_property("apply_to_all_doctypes", "hidden", 0);
|
||||
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) {
|
||||
|
|
@ -233,6 +254,7 @@ frappe.listview_settings['User Permission'] = {
|
|||
dialog.set_df_property("applicable_doctypes", "options", options);
|
||||
dialog.set_df_property("applicable_doctypes", "hidden", 1);
|
||||
}
|
||||
dialog.refresh();
|
||||
},
|
||||
|
||||
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", "hidden", 1);
|
||||
}
|
||||
dialog.refresh_sections();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<td class="danger">{{ item[1] }}</td>
|
||||
<td class="success">{{ item[2] }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
|
@ -93,4 +93,4 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ frappe.PermissionEngine = Class.extend({
|
|||
.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"],
|
||||
|
||||
set_show_users: function(cell, role) {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,18 @@ def add(parent, role, permlevel):
|
|||
|
||||
@frappe.whitelist()
|
||||
def update(doctype, role, permlevel, ptype, value=None):
|
||||
"""Update role permission params
|
||||
|
||||
Args:
|
||||
doctype (str): Name of the DocType to update params for
|
||||
role (str): Role to be updated for, eg "Website Manager".
|
||||
permlevel (int): perm level the provided rule applies to
|
||||
ptype (str): permission type, example "read", "delete", etc.
|
||||
value (None, optional): value for ptype, None indicates False
|
||||
|
||||
Returns:
|
||||
str: Refresh flag is permission is updated successfully
|
||||
"""
|
||||
frappe.only_for("System Manager")
|
||||
out = update_permission_property(doctype, role, permlevel, ptype, value)
|
||||
return 'refresh' if out else None
|
||||
|
|
@ -92,7 +104,7 @@ def remove(doctype, role, permlevel):
|
|||
if not frappe.get_all('Custom DocPerm', dict(parent=doctype)):
|
||||
frappe.throw(_('There must be atleast one permission rule.'), title=_('Cannot Remove'))
|
||||
|
||||
validate_permissions_for_doctype(doctype, for_remove=True)
|
||||
validate_permissions_for_doctype(doctype, for_remove=True, alert=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def reset(doctype):
|
||||
|
|
|
|||
|
|
@ -455,11 +455,15 @@ class CustomizeForm(Document):
|
|||
self.fetch_to_customize()
|
||||
|
||||
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)
|
||||
|
||||
doctype_properties = {
|
||||
|
|
|
|||
|
|
@ -746,6 +746,9 @@ class Database(object):
|
|||
|
||||
def commit(self):
|
||||
"""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")
|
||||
|
||||
frappe.local.rollback_observers = []
|
||||
|
|
@ -753,6 +756,9 @@ class Database(object):
|
|||
enqueue_jobs_after_commit()
|
||||
flush_local_link_count()
|
||||
|
||||
def add_before_commit(self, method, args=None, kwargs=None):
|
||||
frappe.local.before_commit.append([method, args, kwargs])
|
||||
|
||||
@staticmethod
|
||||
def flush_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):
|
||||
|
||||
field_map = frappe._dict(json.loads(field_map))
|
||||
fields = frappe.parse_json(fields)
|
||||
|
||||
doc_meta = frappe.get_meta(doctype)
|
||||
for d in doc_meta.fields:
|
||||
|
|
|
|||
|
|
@ -108,9 +108,18 @@ class Workspace:
|
|||
'extends': self.page_name,
|
||||
'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()
|
||||
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:
|
||||
return True
|
||||
else:
|
||||
allowed_doctypes = [frappe.permissions.get_doctypes_with_read()]
|
||||
allowed_doctypes = frappe.permissions.get_doctypes_with_read()
|
||||
if doc.document_type in allowed_doctypes:
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ frappe.ui.form.on('Desk Page', {
|
|||
refresh: function(frm) {
|
||||
frm.enable_save();
|
||||
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);
|
||||
|
||||
if (frm.doc.for_user) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"onboarding",
|
||||
"column_break_3",
|
||||
"extends_another_page",
|
||||
"is_default",
|
||||
"is_standard",
|
||||
"developer_mode_only",
|
||||
"disable_user_customization",
|
||||
|
|
@ -197,10 +198,18 @@
|
|||
"fieldname": "hide_custom",
|
||||
"fieldtype": "Check",
|
||||
"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": [],
|
||||
"modified": "2020-05-18 19:17:27.206646",
|
||||
"modified": "2021-01-21 12:09:36.156614",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"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()):
|
||||
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):
|
||||
for card in self.cards:
|
||||
try:
|
||||
|
|
@ -45,4 +50,4 @@ def disable_saving_as_standard():
|
|||
frappe.flags.in_patch or \
|
||||
frappe.flags.in_test 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:
|
||||
frappe.errprint(frappe.utils.get_traceback())
|
||||
frappe.msgprint(frappe._("Did not cancel"))
|
||||
raise
|
||||
|
||||
def send_updated_docs(doc):
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ def install():
|
|||
|
||||
@frappe.whitelist()
|
||||
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]
|
||||
for record in records:
|
||||
frappe.get_doc(record).insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
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]
|
||||
for record in records:
|
||||
doc = frappe.new_doc(record.get("doctype"))
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class UserProfile {
|
|||
fieldname: 'user',
|
||||
options: 'User',
|
||||
label: __('User'),
|
||||
reqd: 1
|
||||
}
|
||||
],
|
||||
primary_action_label: __('Go'),
|
||||
|
|
|
|||
|
|
@ -54,6 +54,12 @@ def get_form_params():
|
|||
|
||||
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:
|
||||
key = field.split(" as ")[0]
|
||||
|
||||
|
|
@ -61,21 +67,24 @@ def get_form_params():
|
|||
if key.startswith('sum('): 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
|
||||
|
||||
# remove the field from the query if the report hide flag is set and current view is Report
|
||||
if report_hide and is_report:
|
||||
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
|
||||
data.query = None
|
||||
|
|
@ -83,6 +92,16 @@ def get_form_params():
|
|||
|
||||
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 = {}):
|
||||
"""separate keys and values"""
|
||||
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
|
||||
order_by = "_relevance, {0}, `tab{1}`.idx desc".format(order_by_based_on_meta, doctype)
|
||||
|
||||
ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype))
|
||||
ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
|
||||
ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype))
|
||||
|
||||
if doctype in UNTRANSLATED_DOCTYPES:
|
||||
page_length = None
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class AutoEmailReport(Document):
|
|||
|
||||
if self.format == 'HTML':
|
||||
columns, data = make_links(columns, data)
|
||||
|
||||
columns = update_field_types(columns)
|
||||
return self.get_html_table(columns, data)
|
||||
|
||||
elif self.format == 'XLSX':
|
||||
|
|
@ -236,5 +236,14 @@ def make_links(columns, data):
|
|||
elif col.fieldtype == "Dynamic Link":
|
||||
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])
|
||||
elif col.fieldtype == "Currency":
|
||||
row[col.fieldname] = frappe.format_value(row[col.fieldname], col)
|
||||
|
||||
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)):
|
||||
self.throw_invalid_credentials_exception()
|
||||
else:
|
||||
frappe.throw(e)
|
||||
frappe.throw(cstr(e))
|
||||
|
||||
except socket.error:
|
||||
if in_receive:
|
||||
|
|
|
|||
|
|
@ -2,58 +2,66 @@
|
|||
# License: GNU General Public License v3. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe, unittest
|
||||
from frappe.utils import getdate, add_days
|
||||
import unittest
|
||||
from random import choice
|
||||
|
||||
from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe, send_scheduled_email
|
||||
from six.moves.urllib.parse import unquote
|
||||
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"]
|
||||
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):
|
||||
def setUp(self):
|
||||
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:
|
||||
frappe.get_doc({
|
||||
"doctype": "Email Group",
|
||||
"title": "_Test Email Group"
|
||||
}).insert()
|
||||
for email in emails:
|
||||
frappe.get_doc({
|
||||
"doctype": "Email Group Member",
|
||||
"email": email,
|
||||
"email_group": "_Test Email Group"
|
||||
}).insert()
|
||||
frappe.get_doc({
|
||||
"doctype": "Email Group Member",
|
||||
"email": email,
|
||||
"email_group": "_Test Email Group"
|
||||
}).insert()
|
||||
|
||||
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)
|
||||
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):
|
||||
# test unsubscribe
|
||||
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)
|
||||
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)
|
||||
|
||||
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)
|
||||
recipients = [e.recipients[0].recipient for e in email_queue_list]
|
||||
|
||||
for email in emails:
|
||||
if email != to_unsubscribe:
|
||||
self.assertTrue(email in recipients)
|
||||
|
|
@ -86,7 +94,6 @@ class TestNewsletter(unittest.TestCase):
|
|||
def test_portal(self):
|
||||
self.send_newsletter(1)
|
||||
frappe.set_user("test1@example.com")
|
||||
from frappe.email.doctype.newsletter.newsletter import get_newsletter_list
|
||||
newsletters = get_newsletter_list("Newsletter", None, None, 0)
|
||||
self.assertEqual(len(newsletters), 1)
|
||||
|
||||
|
|
@ -106,4 +113,4 @@ class TestNewsletter(unittest.TestCase):
|
|||
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)
|
||||
self.assertTrue(email in recipients)
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ def set_update(update, producer_site):
|
|||
if data.changed:
|
||||
local_doc.update(data.changed)
|
||||
if data.removed:
|
||||
update_row_removed(local_doc, data.removed)
|
||||
local_doc = update_row_removed(local_doc, data.removed)
|
||||
if data.row_changed:
|
||||
update_row_changed(local_doc, data.row_changed)
|
||||
if data.added:
|
||||
|
|
@ -318,7 +318,17 @@ def update_row_removed(local_doc, removed):
|
|||
for tablename, rownames in iteritems(removed):
|
||||
table = local_doc.get_table_field_doctype(tablename)
|
||||
for row in rownames:
|
||||
frappe.db.delete(table, row)
|
||||
table_rows = local_doc.get(tablename)
|
||||
child_table_row = get_child_table_row(table_rows, row)
|
||||
table_rows.remove(child_table_row)
|
||||
local_doc.set(tablename, table_rows)
|
||||
return local_doc
|
||||
|
||||
|
||||
def get_child_table_row(table_rows, row):
|
||||
for entry in table_rows:
|
||||
if entry.get('name') == row:
|
||||
return entry
|
||||
|
||||
|
||||
def update_row_changed(local_doc, changed):
|
||||
|
|
|
|||
96
frappe/geo/utils.py
Normal file
96
frappe/geo/utils.py
Normal file
|
|
@ -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"
|
||||
|
||||
translator_url = "https://translatev2.erpnext.com"
|
||||
translator_url = "https://translate.erpnext.com"
|
||||
|
||||
before_install = "frappe.utils.install.before_install"
|
||||
after_install = "frappe.utils.install.after_install"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
{
|
||||
"hidden": 0,
|
||||
"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,
|
||||
|
|
|
|||
0
frappe/integrations/doctype/connected_app/__init__.py
Normal file
0
frappe/integrations/doctype/connected_app/__init__.py
Normal file
38
frappe/integrations/doctype/connected_app/connected_app.js
Normal file
38
frappe/integrations/doctype/connected_app/connected_app.js
Normal file
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
166
frappe/integrations/doctype/connected_app/connected_app.json
Normal file
166
frappe/integrations/doctype/connected_app/connected_app.json
Normal file
|
|
@ -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
|
||||
}
|
||||
133
frappe/integrations/doctype/connected_app/connected_app.py
Normal file
133
frappe/integrations/doctype/connected_app/connected_app.py
Normal file
|
|
@ -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()
|
||||
162
frappe/integrations/doctype/connected_app/test_connected_app.py
Normal file
162
frappe/integrations/doctype/connected_app/test_connected_app.py
Normal file
|
|
@ -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()
|
||||
13
frappe/integrations/doctype/connected_app/test_records.json
Normal file
13
frappe/integrations/doctype/connected_app/test_records.json
Normal file
|
|
@ -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",
|
||||
"default_redirect_uri": "http://localhost",
|
||||
"docstatus": 0,
|
||||
|
|
|
|||
0
frappe/integrations/doctype/oauth_scope/__init__.py
Normal file
0
frappe/integrations/doctype/oauth_scope/__init__.py
Normal file
30
frappe/integrations/doctype/oauth_scope/oauth_scope.json
Normal file
30
frappe/integrations/doctype/oauth_scope/oauth_scope.json
Normal file
|
|
@ -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
|
||||
}
|
||||
10
frappe/integrations/doctype/oauth_scope/oauth_scope.py
Normal file
10
frappe/integrations/doctype/oauth_scope/oauth_scope.py
Normal file
|
|
@ -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
frappe/integrations/doctype/query_parameters/__init__.py
Normal file
0
frappe/integrations/doctype/query_parameters/__init__.py
Normal file
|
|
@ -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",
|
||||
"endpoint_url",
|
||||
"column_break_13",
|
||||
"region",
|
||||
"backup_details_section",
|
||||
"frequency",
|
||||
"backup_files",
|
||||
"column_break_18",
|
||||
"backup_limit"
|
||||
"backup_files"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -42,7 +39,7 @@
|
|||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Email for Successful Backup"
|
||||
|
|
@ -73,14 +70,7 @@
|
|||
"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",
|
||||
"fieldtype": "Data",
|
||||
"label": "Endpoint URL"
|
||||
|
|
@ -92,14 +82,6 @@
|
|||
"mandatory_depends_on": "enabled",
|
||||
"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",
|
||||
"fieldname": "api_access_section",
|
||||
|
|
@ -142,16 +124,12 @@
|
|||
"fieldname": "backup_files",
|
||||
"fieldtype": "Check",
|
||||
"label": "Backup Files"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-07-27 17:27:21.400000",
|
||||
"modified": "2020-12-07 15:30:55.047689",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "S3 Backup Settings",
|
||||
|
|
@ -172,4 +150,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ class S3BackupSettings(Document):
|
|||
|
||||
if not self.endpoint_url:
|
||||
self.endpoint_url = 'https://s3.amazonaws.com'
|
||||
|
||||
conn = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=self.access_key_id,
|
||||
|
|
@ -31,25 +32,21 @@ class S3BackupSettings(Document):
|
|||
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:
|
||||
# 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:
|
||||
error_code = e.response['Error']['Code']
|
||||
bucket_name = frappe.bold(self.bucket)
|
||||
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()
|
||||
|
|
@ -70,11 +67,13 @@ def take_backups_weekly():
|
|||
def take_backups_monthly():
|
||||
take_backups_if("Monthly")
|
||||
|
||||
|
||||
def take_backups_if(freq):
|
||||
if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")):
|
||||
if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq:
|
||||
take_backups_s3()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def take_backups_s3(retry_count=0):
|
||||
try:
|
||||
|
|
@ -146,42 +145,13 @@ def backup_to_s3():
|
|||
if files_filename:
|
||||
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):
|
||||
destpath = os.path.join(folder, os.path.basename(filename))
|
||||
try:
|
||||
print("Uploading file:", filename)
|
||||
conn.upload_file(filename, bucket, destpath)
|
||||
conn.upload_file(filename, bucket, destpath) # Requires PutObject permission
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error()
|
||||
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"
|
||||
doc = frappe.get_doc(kwargs)
|
||||
return doc
|
||||
|
||||
def create_or_update_social_login_key():
|
||||
# used in other tests (connected app, oauth20)
|
||||
try:
|
||||
social_login_key = frappe.get_doc("Social Login Key", "frappe")
|
||||
except frappe.DoesNotExistError:
|
||||
social_login_key = frappe.new_doc("Social Login Key")
|
||||
social_login_key.get_social_login_provider("Frappe", initialize=True)
|
||||
social_login_key.base_url = frappe.utils.get_url()
|
||||
social_login_key.enable_social_login = 0
|
||||
social_login_key.save()
|
||||
frappe.db.commit()
|
||||
|
||||
return social_login_key
|
||||
|
|
|
|||
0
frappe/integrations/doctype/token_cache/__init__.py
Normal file
0
frappe/integrations/doctype/token_cache/__init__.py
Normal file
18
frappe/integrations/doctype/token_cache/test_records.json
Normal file
18
frappe/integrations/doctype/token_cache/test_records.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
37
frappe/integrations/doctype/token_cache/test_token_cache.py
Normal file
37
frappe/integrations/doctype/token_cache/test_token_cache.py
Normal file
|
|
@ -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()
|
||||
8
frappe/integrations/doctype/token_cache/token_cache.js
Normal file
8
frappe/integrations/doctype/token_cache/token_cache.js
Normal file
|
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
||||
110
frappe/integrations/doctype/token_cache/token_cache.json
Normal file
110
frappe/integrations/doctype/token_cache/token_cache.json
Normal file
|
|
@ -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
|
||||
}
|
||||
67
frappe/integrations/doctype/token_cache/token_cache.py
Normal file
67
frappe/integrations/doctype/token_cache/token_cache.py
Normal file
|
|
@ -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):
|
||||
try:
|
||||
r = requests.post(webhook.request_url, data=json.dumps(data), headers=headers, timeout=5)
|
||||
r = requests.post(webhook.request_url, data=json.dumps(data, default=str), headers=headers, timeout=5)
|
||||
r.raise_for_status()
|
||||
frappe.logger().debug({"webhook_success": r.text})
|
||||
break
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ def get_oauth_server():
|
|||
return frappe.local.oauth_server
|
||||
|
||||
def sanitize_kwargs(param_kwargs):
|
||||
"""Remove 'data' and 'cmd' keys, if present."""
|
||||
arguments = param_kwargs
|
||||
arguments.pop('data', None)
|
||||
arguments.pop('cmd', None)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ def get_controller(doctype):
|
|||
else:
|
||||
class_overrides = frappe.get_hooks('override_doctype_class')
|
||||
if class_overrides and class_overrides.get(doctype):
|
||||
import_path = frappe.get_hooks('override_doctype_class').get(doctype)[-1]
|
||||
import_path = class_overrides[doctype][-1]
|
||||
module_path, classname = import_path.rsplit('.', 1)
|
||||
module = frappe.get_module(module_path)
|
||||
if not hasattr(module, classname):
|
||||
|
|
@ -69,10 +69,13 @@ def get_controller(doctype):
|
|||
|
||||
if frappe.local.dev_server:
|
||||
return _get_controller()
|
||||
|
||||
key = '{}:doctype_classes'.format(frappe.local.site)
|
||||
return frappe.cache().hget(key, doctype, generator=_get_controller, shared=True)
|
||||
|
||||
|
||||
site_controllers = frappe.controllers.setdefault(frappe.local.site, {})
|
||||
if doctype not in site_controllers:
|
||||
site_controllers[doctype] = _get_controller()
|
||||
|
||||
return site_controllers[doctype]
|
||||
|
||||
class BaseDocument(object):
|
||||
ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,10 @@ class DatabaseQuery(object):
|
|||
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
|
||||
update=None, add_total_row=None, user_settings=None, reference_doctype=None,
|
||||
return_query=False, strict=True, pluck=None, ignore_ddl=False):
|
||||
if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user):
|
||||
if not ignore_permissions and \
|
||||
not frappe.has_permission(self.doctype, "select", user=user) and \
|
||||
not frappe.has_permission(self.doctype, "read", user=user):
|
||||
|
||||
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype))
|
||||
raise frappe.PermissionError(self.doctype)
|
||||
|
||||
|
|
@ -315,7 +318,10 @@ class DatabaseQuery(object):
|
|||
def append_table(self, table_name):
|
||||
self.tables.append(table_name)
|
||||
doctype = table_name[4:-1]
|
||||
if (not self.flags.ignore_permissions) and (not frappe.has_permission(doctype)):
|
||||
ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
|
||||
|
||||
if (not self.flags.ignore_permissions) and\
|
||||
(not frappe.has_permission(doctype, ptype=ptype)):
|
||||
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype))
|
||||
raise frappe.PermissionError(doctype)
|
||||
|
||||
|
|
@ -576,7 +582,7 @@ class DatabaseQuery(object):
|
|||
self.shared = frappe.share.get_shared(self.doctype, self.user)
|
||||
|
||||
if (not meta.istable and
|
||||
not role_permissions.get("read") and
|
||||
not (role_permissions.get("select") or role_permissions.get("read")) and
|
||||
not self.flags.ignore_permissions and
|
||||
not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)):
|
||||
only_if_shared = True
|
||||
|
|
@ -591,7 +597,7 @@ class DatabaseQuery(object):
|
|||
self.match_conditions.append("`tab{0}`.`owner` = {1}".format(self.doctype,
|
||||
frappe.db.escape(self.user, percent=False)))
|
||||
# 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
|
||||
user_permissions = frappe.permissions.get_user_permissions(self.user)
|
||||
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)
|
||||
|
||||
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:
|
||||
delete_controllers(name, doc.module)
|
||||
except (FileNotFoundError, OSError, KeyError):
|
||||
|
|
|
|||
|
|
@ -939,15 +939,17 @@ class Document(BaseDocument):
|
|||
self.load_doc_before_save()
|
||||
self.reset_seen()
|
||||
|
||||
# before_validate method should be executed before ignoring validations
|
||||
if self._action in ("save", "submit"):
|
||||
self.run_method("before_validate")
|
||||
|
||||
if self.flags.ignore_validate:
|
||||
return
|
||||
|
||||
if self._action=="save":
|
||||
self.run_method("before_validate")
|
||||
self.run_method("validate")
|
||||
self.run_method("before_save")
|
||||
elif self._action=="submit":
|
||||
self.run_method("before_validate")
|
||||
self.run_method("validate")
|
||||
self.run_method("before_submit")
|
||||
elif self._action=="cancel":
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ def load_doctype_from_file(doctype):
|
|||
class Meta(Document):
|
||||
_metaclass = True
|
||||
default_fields = list(default_fields)[1:]
|
||||
special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def", 'DocType Action', 'DocType Link')
|
||||
special_doctypes = ("DocField", "DocPerm", "DocType", "Module Def", 'DocType Action', 'DocType Link')
|
||||
|
||||
def __init__(self, doctype):
|
||||
self._fields = {}
|
||||
|
|
@ -450,6 +450,25 @@ class Meta(Document):
|
|||
|
||||
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):
|
||||
'''Returns dashboard setup related to this doctype.
|
||||
|
||||
|
|
@ -484,6 +503,8 @@ class Meta(Document):
|
|||
if not data.transactions:
|
||||
# init groups
|
||||
data.transactions = []
|
||||
|
||||
if not data.non_standard_fieldnames:
|
||||
data.non_standard_fieldnames = {}
|
||||
|
||||
for link in dashboard_links:
|
||||
|
|
|
|||
|
|
@ -21,8 +21,16 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne
|
|||
docname = rename_doc(doctype=doctype, old=docname, new=new_name, merge=merge)
|
||||
|
||||
if old_title and new_title and not old_title == new_title:
|
||||
frappe.db.set_value(doctype, docname, title_field, new_title)
|
||||
frappe.msgprint(_('Saved'), alert=True, indicator='green')
|
||||
try:
|
||||
frappe.db.set_value(doctype, docname, title_field, new_title)
|
||||
frappe.msgprint(_('Saved'), alert=True, indicator='green')
|
||||
except Exception as e:
|
||||
if frappe.db.is_duplicate_entry(e):
|
||||
frappe.throw(
|
||||
_("{0} {1} already exists").format(doctype, frappe.bold(docname)),
|
||||
title=_("Duplicate Name"),
|
||||
exc=frappe.DuplicateEntryError
|
||||
)
|
||||
|
||||
return docname
|
||||
|
||||
|
|
@ -49,9 +57,7 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
|
|||
old_doc = frappe.get_doc(doctype, old)
|
||||
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)
|
||||
|
||||
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:
|
||||
rename_parent_and_child(doctype, old, new, meta)
|
||||
|
|
@ -250,6 +256,7 @@ def update_link_field_values(link_fields, old, new, doctype):
|
|||
pass
|
||||
else:
|
||||
parent = field['parent']
|
||||
docfield = field["fieldname"]
|
||||
|
||||
# Handles the case where one of the link fields belongs to
|
||||
# the DocType being renamed.
|
||||
|
|
@ -261,11 +268,8 @@ def update_link_field_values(link_fields, old, new, doctype):
|
|||
if parent == new and doctype == "DocType":
|
||||
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
|
||||
if doctype=='DocType' and field['parent'] == old:
|
||||
field['parent'] = new
|
||||
|
|
|
|||
|
|
@ -53,14 +53,17 @@ def get_transitions(doc, workflow = None, raise_exception=False):
|
|||
return transitions
|
||||
|
||||
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(
|
||||
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
|
||||
|
||||
@frappe.whitelist()
|
||||
def can_cancel_document(doc):
|
||||
doc = frappe.get_doc(frappe.parse_json(doc))
|
||||
workflow = get_workflow(doc.doctype)
|
||||
def can_cancel_document(doctype):
|
||||
workflow = get_workflow(doctype)
|
||||
for state_doc in workflow.states:
|
||||
if state_doc.doc_status == '2':
|
||||
for transition in workflow.transitions:
|
||||
|
|
|
|||
|
|
@ -2,9 +2,23 @@ import frappe
|
|||
|
||||
def execute():
|
||||
frappe.reload_doctype('Website Theme')
|
||||
frappe.reload_doc('website', 'doctype', 'website_theme_ignore_app')
|
||||
frappe.reload_doc('website', 'doctype', 'color')
|
||||
|
||||
for theme in frappe.get_all('Website Theme'):
|
||||
doc = frappe.get_doc('Website Theme', theme.name)
|
||||
if not doc.get('custom_scss') and doc.theme_scss:
|
||||
# move old theme to new theme
|
||||
doc.custom_scss = doc.theme_scss
|
||||
|
||||
if doc.background_color:
|
||||
setup_color_record(doc.background_color)
|
||||
|
||||
doc.save()
|
||||
|
||||
def setup_color_record(color):
|
||||
frappe.get_doc({
|
||||
"doctype": "Color",
|
||||
"__newname": color,
|
||||
"color": color,
|
||||
}).save()
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import frappe, copy, json
|
|||
from frappe import _, msgprint
|
||||
from frappe.utils import cint
|
||||
import frappe.share
|
||||
rights = ("read", "write", "create", "delete", "submit", "cancel", "amend",
|
||||
rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend",
|
||||
"print", "email", "report", "import", "export", "set_user_permissions", "share")
|
||||
|
||||
# TODO:
|
||||
|
|
@ -73,6 +73,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra
|
|||
|
||||
role_permissions = get_role_permissions(meta, user=user)
|
||||
perm = role_permissions.get(ptype)
|
||||
|
||||
if not perm:
|
||||
push_perm_check_log(_('User {0} does not have doctype access via role permission for document {1}').format(frappe.bold(user), frappe.bold(doctype)))
|
||||
|
||||
|
|
@ -192,9 +193,9 @@ def get_role_permissions(doctype_meta, user=None):
|
|||
and ptype != 'create'):
|
||||
perms['if_owner'][ptype] = 1
|
||||
# has no access if not owner
|
||||
# only provide read access so that user is able to at-least access list
|
||||
# only provide select or read access so that user is able to at-least access list
|
||||
# (and the documents will be filtered based on owner sin further checks)
|
||||
perms[ptype] = 1 if ptype == 'read' else 0
|
||||
perms[ptype] = 1 if ptype in ['select', 'read'] else 0
|
||||
|
||||
frappe.local.role_permissions[cache_key] = perms
|
||||
|
||||
|
|
@ -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:
|
||||
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'''
|
||||
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,
|
||||
is_default=is_default,
|
||||
applicable_for=applicable_for,
|
||||
hide_descendants=hide_descendants,
|
||||
)).insert(ignore_permissions=ignore_permissions)
|
||||
|
||||
def remove_user_permission(doctype, name, user):
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ frappe.ui.form.on("Print Format", {
|
|||
hide_absolute_value_field: function (frm) {
|
||||
// TODO: make it work with frm.doc.doc_type
|
||||
// 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) {
|
||||
frappe.model.with_doctype(doctype, () => {
|
||||
const meta = frappe.get_meta(doctype);
|
||||
|
|
|
|||
|
|
@ -201,17 +201,17 @@
|
|||
{
|
||||
"default": "0",
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show absolute values"
|
||||
"label": "Show Absolute Values"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-print",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-10 18:58:55.598269",
|
||||
"modified": "2020-12-14 11:38:49.132061",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Printing",
|
||||
"name": "Print Format",
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@
|
|||
"public/js/frappe/router_history.js",
|
||||
"public/js/frappe/defaults.js",
|
||||
"public/js/frappe/roles_editor.js",
|
||||
"public/js/frappe/module_editor.js",
|
||||
"public/js/frappe/microtemplate.js",
|
||||
|
||||
"public/js/frappe/ui/page.html",
|
||||
|
|
@ -307,6 +308,7 @@
|
|||
"public/js/frappe/views/calendar/calendar.js",
|
||||
"public/js/frappe/views/dashboard/dashboard_view.js",
|
||||
"public/js/frappe/views/image/image_view.js",
|
||||
"public/js/frappe/views/map/map_view.js",
|
||||
"public/js/frappe/views/kanban/kanban_view.js",
|
||||
"public/js/frappe/views/inbox/inbox_view.js",
|
||||
"public/js/frappe/views/file/file_view.js",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue