diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index e1f16970fe..5be3a87884 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -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
diff --git a/.travis.yml b/.travis.yml
index 63895675ea..23fb525138 100644
--- a/.travis.yml
+++ b/.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
diff --git a/cypress/integration/depends_on.js b/cypress/integration/depends_on.js
index 93417014c5..aa80afb59a 100644
--- a/cypress/integration/depends_on.js
+++ b/cypress/integration/depends_on.js
@@ -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');
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 7816d5526f..3e54a9cd4c 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -160,7 +160,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => {
Cypress.Commands.add('create_records', doc => {
return cy
- .call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
+ .call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc})
.then(r => r.message);
});
@@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
- cy.get('@input').type(value, { waitForAnimations: false, force: true });
+ cy.get('@input').type(value, {waitForAnimations: false, force: true});
}
return cy.get('@input');
});
@@ -204,8 +204,43 @@ Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
return cy.get(selector);
});
+Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
+ cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input');
+
+ if (['Date', 'Time', 'Datetime'].includes(fieldtype)) {
+ cy.get('@input').click().wait(200);
+ cy.get('.datepickers-container .datepicker.active').should('exist');
+ }
+ if (fieldtype === 'Time') {
+ cy.get('@input').clear().wait(200);
+ }
+
+ if (fieldtype === 'Select') {
+ cy.get('@input').select(value);
+ } else {
+ cy.get('@input').type(value, {waitForAnimations: false, force: true});
+ }
+ return cy.get('@input');
+});
+
+Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => {
+ let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`;
+ selector += ` [data-idx="${row_idx}"]`;
+ selector += ` .form-in-grid`;
+
+ if (fieldtype === 'Text Editor') {
+ selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
+ } else if (fieldtype === 'Code') {
+ selector += ` [data-fieldname="${fieldname}"] .ace_text-input`;
+ } else {
+ selector += ` .form-control[data-fieldname="${fieldname}"]`;
+ }
+
+ return cy.get(selector);
+});
+
Cypress.Commands.add('awesomebar', text => {
- cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 });
+ cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100});
});
Cypress.Commands.add('new_form', doctype => {
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 4729993dee..7929e62acb 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -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 "[]")
diff --git a/frappe/app.py b/frappe/app.py
index 82471c4e32..adf2bfa8c9 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -7,8 +7,8 @@ import os
from six import iteritems
import logging
-from werkzeug.wrappers import Request
from werkzeug.local import LocalManager
+from werkzeug.wrappers import Request, Response
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.middleware.profiler import ProfilerMiddleware
from werkzeug.middleware.shared_data import SharedDataMiddleware
@@ -57,19 +57,22 @@ def application(request):
frappe.monitor.start()
frappe.rate_limiter.apply()
- if frappe.local.form_dict.cmd:
+ if request.method == "OPTIONS":
+ response = Response()
+
+ elif frappe.form_dict.cmd:
response = frappe.handler.handle()
- elif frappe.request.path.startswith("/api/"):
+ elif request.path.startswith("/api/"):
response = frappe.api.handle()
- elif frappe.request.path.startswith('/backups'):
+ elif request.path.startswith('/backups'):
response = frappe.utils.response.download_backup(request.path)
- elif frappe.request.path.startswith('/private/files/'):
+ elif request.path.startswith('/private/files/'):
response = frappe.utils.response.download_private_file(request.path)
- elif frappe.local.request.method in ('GET', 'HEAD', 'POST'):
+ elif request.method in ('GET', 'HEAD', 'POST'):
response = frappe.website.render.render()
else:
@@ -88,13 +91,9 @@ def application(request):
rollback = after_request(rollback)
finally:
- if frappe.local.request.method in ("POST", "PUT") and frappe.db and rollback:
+ if request.method in ("POST", "PUT") and frappe.db and rollback:
frappe.db.rollback()
- # set cookies
- if response and hasattr(frappe.local, 'cookie_manager'):
- frappe.local.cookie_manager.flush_cookies(response=response)
-
frappe.rate_limiter.update()
frappe.monitor.stop(response)
frappe.recorder.dump()
@@ -110,9 +109,7 @@ def application(request):
"http_status_code": getattr(response, "status_code", "NOTFOUND")
})
- if response and hasattr(frappe.local, 'rate_limiter'):
- response.headers.extend(frappe.local.rate_limiter.headers())
-
+ process_response(response)
frappe.destroy()
return response
@@ -134,7 +131,46 @@ def init_request(request):
make_form_dict(request)
- frappe.local.http_request = frappe.auth.HTTPRequest()
+ if request.method != "OPTIONS":
+ frappe.local.http_request = frappe.auth.HTTPRequest()
+
+def process_response(response):
+ if not response:
+ return
+
+ # set cookies
+ if hasattr(frappe.local, 'cookie_manager'):
+ frappe.local.cookie_manager.flush_cookies(response=response)
+
+ # rate limiter headers
+ if hasattr(frappe.local, 'rate_limiter'):
+ response.headers.extend(frappe.local.rate_limiter.headers())
+
+ # CORS headers
+ if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors:
+ set_cors_headers(response)
+
+def set_cors_headers(response):
+ origin = frappe.request.headers.get('Origin')
+ if not origin:
+ return
+
+ allow_cors = frappe.conf.allow_cors
+ if allow_cors != "*":
+ if not isinstance(allow_cors, list):
+ allow_cors = [allow_cors]
+
+ if origin not in allow_cors:
+ return
+
+ response.headers.extend({
+ 'Access-Control-Allow-Origin': origin,
+ 'Access-Control-Allow-Credentials': 'true',
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+ 'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,'
+ 'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,'
+ 'Cache-Control,Content-Type')
+ })
def make_form_dict(request):
import json
diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js
index 121b4bd2f0..d54ae8d62c 100644
--- a/frappe/automation/doctype/auto_repeat/auto_repeat.js
+++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js
@@ -54,10 +54,12 @@ frappe.ui.form.on('Auto Repeat', {
toggle_submit_on_creation: function(frm) {
// submit on creation checkbox
- frappe.model.with_doctype(frm.doc.reference_doctype, () => {
- let meta = frappe.get_meta(frm.doc.reference_doctype);
- frm.toggle_display('submit_on_creation', meta.is_submittable);
- });
+ if (frm.doc.reference_doctype) {
+ frappe.model.with_doctype(frm.doc.reference_doctype, () => {
+ let meta = frappe.get_meta(frm.doc.reference_doctype);
+ frm.toggle_display('submit_on_creation', meta.is_submittable);
+ });
+ }
},
template: function(frm) {
@@ -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", {
diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json
index 80975dd4f5..74965346fd 100644
--- a/frappe/automation/doctype/auto_repeat/auto_repeat.json
+++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json
@@ -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",
diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py
index 31d6539e61..830af68de7 100644
--- a/frappe/automation/doctype/auto_repeat/auto_repeat.py
+++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py
@@ -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:
diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py
index e40b12e3b9..0d6229cd9e 100644
--- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py
+++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py
@@ -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
diff --git a/frappe/automation/doctype/auto_repeat_day/__init__.py b/frappe/automation/doctype/auto_repeat_day/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json
new file mode 100644
index 0000000000..6f5c3060cd
--- /dev/null
+++ b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json
@@ -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
+}
\ No newline at end of file
diff --git a/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py
new file mode 100644
index 0000000000..3a7ced1370
--- /dev/null
+++ b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class AutoRepeatDay(Document):
+ pass
diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py
index 3b3d188999..ed5c7b64ad 100644
--- a/frappe/cache_manager.py
+++ b/frappe/cache_manager.py
@@ -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'
diff --git a/frappe/change_log/v13/v13_0_0-beta_10.md b/frappe/change_log/v13/v13_0_0-beta_10.md
new file mode 100644
index 0000000000..ff98a8f9c2
--- /dev/null
+++ b/frappe/change_log/v13/v13_0_0-beta_10.md
@@ -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))
\ No newline at end of file
diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py
index a7ac20065f..672c0c4acc 100644
--- a/frappe/config/integrations.py
+++ b/frappe/config/integrations.py
@@ -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"),
+ },
]
},
{
diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py
index a2105c1511..04ecc83b38 100644
--- a/frappe/core/doctype/comment/comment.py
+++ b/frappe/core/doctype/comment/comment.py
@@ -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):
diff --git a/frappe/core/doctype/custom_docperm/custom_docperm.json b/frappe/core/doctype/custom_docperm/custom_docperm.json
index f8f7f58be1..93f5431903 100644
--- a/frappe/core/doctype/custom_docperm/custom_docperm.json
+++ b/frappe/core/doctype/custom_docperm/custom_docperm.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "hash",
"creation": "2017-01-11 04:21:35.217943",
@@ -13,6 +14,7 @@
"column_break_2",
"permlevel",
"section_break_4",
+ "select",
"read",
"write",
"create",
@@ -211,9 +213,16 @@
"fieldtype": "Data",
"label": "Reference Document Type",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "select",
+ "fieldtype": "Check",
+ "label": "Select"
}
],
- "modified": "2019-10-31 16:58:16.157079",
+ "links": [],
+ "modified": "2020-12-03 15:20:48.296730",
"modified_by": "Administrator",
"module": "Core",
"name": "Custom DocPerm",
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index 7880648b6f..dde3dfaee9 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -751,7 +751,7 @@ class Row:
self.warnings.append(
{
"row": self.row_number,
- "message": _("{0} is a mandatory field asdadsf").format(id_field.label),
+ "message": _("{0} is a mandatory field").format(id_field.label),
}
)
return
diff --git a/frappe/core/doctype/docperm/docperm.json b/frappe/core/doctype/docperm/docperm.json
index 1a23118a29..4411a67435 100644
--- a/frappe/core/doctype/docperm/docperm.json
+++ b/frappe/core/doctype/docperm/docperm.json
@@ -1,775 +1,229 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
+ "actions": [],
"autoname": "hash",
- "beta": 0,
"creation": "2013-02-22 01:27:33",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "role_and_level",
+ "role",
+ "if_owner",
+ "column_break_2",
+ "permlevel",
+ "section_break_4",
+ "select",
+ "read",
+ "write",
+ "create",
+ "delete",
+ "column_break_8",
+ "submit",
+ "cancel",
+ "amend",
+ "additional_permissions",
+ "report",
+ "export",
+ "import",
+ "set_user_permissions",
+ "column_break_19",
+ "share",
+ "print",
+ "email"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "role_and_level",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Role and Level",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Role and Level"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "role",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Role",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "role",
"oldfieldtype": "Link",
"options": "Role",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "150px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "150px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"description": "Apply this rule if the User is the Owner",
"fieldname": "if_owner",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "If user is the owner",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "If user is the owner"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Level",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "permlevel",
"oldfieldtype": "Int",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "40px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "40px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "section_break_4",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Permissions",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Permissions"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "read",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Read",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "read",
"oldfieldtype": "Check",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "write",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Write",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "write",
"oldfieldtype": "Check",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "create",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Create",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "create",
"oldfieldtype": "Check",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "delete",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Delete",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Delete"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_8",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"fieldname": "submit",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Submit",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "submit",
"oldfieldtype": "Check",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"fieldname": "cancel",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Cancel",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "cancel",
"oldfieldtype": "Check",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"fieldname": "amend",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Amend",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "amend",
"oldfieldtype": "Check",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "additional_permissions",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Additional Permissions",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Additional Permissions"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "report",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Report",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"print_width": "32px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "32px"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "export",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Export",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Export"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"fieldname": "import",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Import",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Import"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"description": "This role update User Permissions for a user",
"fieldname": "set_user_permissions",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Set User Permissions",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Set User Permissions"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_19",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "share",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Share",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Share"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "print",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Print",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Print"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "email",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Email",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Email"
+ },
+ {
+ "default": "0",
+ "fieldname": "select",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Select"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
"idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
"istable": 1,
- "max_attachments": 0,
- "modified": "2018-05-29 11:54:38.613936",
+ "links": [],
+ "modified": "2020-12-03 15:15:30.488212",
"modified_by": "Administrator",
"module": "Core",
"name": "DocPerm",
"owner": "Administrator",
"permissions": [],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_order": "ASC",
- "track_changes": 0,
- "track_seen": 0
+ "sort_field": "modified",
+ "sort_order": "ASC"
}
\ No newline at end of file
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index cce5968f9c..80a576230c 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import re, copy, os, shutil
import json
-from frappe.cache_manager import clear_user_cache
+from frappe.cache_manager import clear_user_cache, clear_controller_cache
# imports - third party imports
import six
@@ -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):
diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py
index 3ff47facc3..4b34293af6 100644
--- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py
+++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py
@@ -6,8 +6,19 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils.data import evaluate_filters
+from frappe import _
class DocumentNamingRule(Document):
+ def validate(self):
+ self.validate_fields_in_conditions()
+
+ def validate_fields_in_conditions(self):
+ if self.has_value_changed("document_type"):
+ docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields]
+ for condition in self.conditions:
+ if condition.field not in docfields:
+ frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type)))
+
def apply(self, doc):
'''
Apply naming rules for the given document. Will set `name` if the rule is matched.
diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py
index 930c46e60b..7e63572162 100644
--- a/frappe/core/doctype/module_def/module_def.py
+++ b/frappe/core/doctype/module_def/module_def.py
@@ -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
diff --git a/frappe/core/doctype/module_profile/__init__.py b/frappe/core/doctype/module_profile/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/module_profile/module_profile.js b/frappe/core/doctype/module_profile/module_profile.js
new file mode 100644
index 0000000000..9c92042dda
--- /dev/null
+++ b/frappe/core/doctype/module_profile/module_profile.js
@@ -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 = $('
')
+ .appendTo(frm.fields_dict.module_html.wrapper);
+
+ frm.module_editor = new frappe.ModuleEditor(frm, module_area);
+ }
+ }
+
+ if (frm.module_editor) {
+ frm.module_editor.refresh();
+ }
+ }
+});
diff --git a/frappe/core/doctype/module_profile/module_profile.json b/frappe/core/doctype/module_profile/module_profile.json
new file mode 100644
index 0000000000..0e4e56962e
--- /dev/null
+++ b/frappe/core/doctype/module_profile/module_profile.json
@@ -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
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/module_profile/module_profile.py b/frappe/core/doctype/module_profile/module_profile.py
new file mode 100644
index 0000000000..4f392353ac
--- /dev/null
+++ b/frappe/core/doctype/module_profile/module_profile.py
@@ -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()])
diff --git a/frappe/core/doctype/module_profile/test_module_profile.py b/frappe/core/doctype/module_profile/test_module_profile.py
new file mode 100644
index 0000000000..400053d22c
--- /dev/null
+++ b/frappe/core/doctype/module_profile/test_module_profile.py
@@ -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')
diff --git a/frappe/core/doctype/report_filter/report_filter.json b/frappe/core/doctype/report_filter/report_filter.json
index 9d277db11d..964294b96e 100644
--- a/frappe/core/doctype/report_filter/report_filter.json
+++ b/frappe/core/doctype/report_filter/report_filter.json
@@ -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",
diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json
index 94a48f196c..9aa7b5afe5 100644
--- a/frappe/core/doctype/server_script/server_script.json
+++ b/frappe/core/doctype/server_script/server_script.json
@@ -47,7 +47,7 @@
"fieldname": "doctype_event",
"fieldtype": "Select",
"label": "DocType Event",
- "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
+ "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
},
{
"depends_on": "eval:doc.script_type==='API'",
@@ -88,7 +88,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-12-03 22:42:02.708148",
+ "modified": "2021-01-03 18:50:14.767595",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",
diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py
index 4dc4f12b34..12a8fa47fa 100644
--- a/frappe/core/doctype/server_script/server_script_utils.py
+++ b/frappe/core/doctype/server_script/server_script_utils.py
@@ -6,6 +6,7 @@ import frappe
EVENT_MAP = {
'before_insert': 'Before Insert',
'after_insert': 'After Insert',
+ 'before_validate': 'Before Validate',
'validate': 'Before Save',
'on_update': 'After Save',
'before_submit': 'Before Submit',
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index 957cbbf72d..8dd6d03fee 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -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')
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 79fb84923a..565ee373f1 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -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
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index b8e16bfe25..5493baf553 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -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 = $('
').appendTo(wrapper);
- this.frm = frm;
- this.make();
- },
- make: function() {
- var me = this;
- this.frm.doc.__onload.all_modules.forEach(function(m) {
- $(repl('
', {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});
- }
- });
- }
-});
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 2073f41fdd..53e05bb916 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -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
-}
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 7309528da6..dcca4f4a25 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -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)
diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py
index 82dd2ab27e..7e0b4a49c6 100644
--- a/frappe/core/doctype/user_permission/test_user_permission.py
+++ b/frappe/core/doctype/user_permission/test_user_permission.py
@@ -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})
diff --git a/frappe/core/doctype/user_permission/user_permission.js b/frappe/core/doctype/user_permission/user_permission.js
index 9f824b1350..4c3f5b4eb8 100644
--- a/frappe/core/doctype/user_permission/user_permission.js
+++ b/frappe/core/doctype/user_permission/user_permission.js
@@ -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);
}
diff --git a/frappe/core/doctype/user_permission/user_permission.json b/frappe/core/doctype/user_permission/user_permission.json
index 33a8d58bbb..9cea0856c9 100644
--- a/frappe/core/doctype/user_permission/user_permission.json
+++ b/frappe/core/doctype/user_permission/user_permission.json
@@ -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
For Value.",
+ "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
}
\ No newline at end of file
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index ba14583c2f..e04000c0b3 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -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
diff --git a/frappe/core/doctype/user_permission/user_permission_list.js b/frappe/core/doctype/user_permission/user_permission_list.js
index 3e822f0007..5539a26438 100644
--- a/frappe/core/doctype/user_permission/user_permission_list.js
+++ b/frappe/core/doctype/user_permission/user_permission_list.js
@@ -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();
}
-};
\ No newline at end of file
+};
diff --git a/frappe/core/doctype/version/version_view.html b/frappe/core/doctype/version/version_view.html
index 5383be82a1..67f005ed4c 100644
--- a/frappe/core/doctype/version/version_view.html
+++ b/frappe/core/doctype/version/version_view.html
@@ -21,7 +21,7 @@
{{ item[1] }} |
{{ item[2] }} |
- {% endif %}
+ {% endfor %}
{% endif %}
@@ -58,7 +58,7 @@
- {% endif %}
+ {% endfor %}
@@ -93,4 +93,4 @@
{% endfor %}
{% endif %}
-
\ No newline at end of file
+
diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js
index 0d3267c7d5..02fbf943d5 100644
--- a/frappe/core/page/permission_manager/permission_manager.js
+++ b/frappe/core/page/permission_manager/permission_manager.js
@@ -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) {
diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py
index 637b526d5c..be8921e2ff 100644
--- a/frappe/core/page/permission_manager/permission_manager.py
+++ b/frappe/core/page/permission_manager/permission_manager.py
@@ -77,6 +77,18 @@ def add(parent, role, permlevel):
@frappe.whitelist()
def update(doctype, role, permlevel, ptype, value=None):
+ """Update role permission params
+
+ Args:
+ doctype (str): Name of the DocType to update params for
+ role (str): Role to be updated for, eg "Website Manager".
+ permlevel (int): perm level the provided rule applies to
+ ptype (str): permission type, example "read", "delete", etc.
+ value (None, optional): value for ptype, None indicates False
+
+ Returns:
+ str: Refresh flag is permission is updated successfully
+ """
frappe.only_for("System Manager")
out = update_permission_property(doctype, role, permlevel, ptype, value)
return 'refresh' if out else None
@@ -92,7 +104,7 @@ def remove(doctype, role, permlevel):
if not frappe.get_all('Custom DocPerm', dict(parent=doctype)):
frappe.throw(_('There must be atleast one permission rule.'), title=_('Cannot Remove'))
- validate_permissions_for_doctype(doctype, for_remove=True)
+ validate_permissions_for_doctype(doctype, for_remove=True, alert=True)
@frappe.whitelist()
def reset(doctype):
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 82513783c7..50acab46b5 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -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 = {
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 616dd3c3ec..179206a4af 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -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:
diff --git a/frappe/desk/calendar.py b/frappe/desk/calendar.py
index ce9fb7f177..064d870092 100644
--- a/frappe/desk/calendar.py
+++ b/frappe/desk/calendar.py
@@ -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:
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 4dab313892..a476573b1a 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -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)
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 2fa36b5514..b19f6cf9f0 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -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
diff --git a/frappe/desk/doctype/desk_page/desk_page.js b/frappe/desk/doctype/desk_page/desk_page.js
index 503859eb61..86fea6df40 100644
--- a/frappe/desk/doctype/desk_page/desk_page.js
+++ b/frappe/desk/doctype/desk_page/desk_page.js
@@ -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) {
diff --git a/frappe/desk/doctype/desk_page/desk_page.json b/frappe/desk/doctype/desk_page/desk_page.json
index 2b8aea5e6c..016d6c89d4 100644
--- a/frappe/desk/doctype/desk_page/desk_page.json
+++ b/frappe/desk/doctype/desk_page/desk_page.json
@@ -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",
diff --git a/frappe/desk/doctype/desk_page/desk_page.py b/frappe/desk/doctype/desk_page/desk_page.py
index e92844ac0b..fcc3c08135 100644
--- a/frappe/desk/doctype/desk_page/desk_page.py
+++ b/frappe/desk/doctype/desk_page/desk_page.py
@@ -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
\ No newline at end of file
+ frappe.flags.in_migrate
diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py
index 5219a98cbd..da43b14fce 100644
--- a/frappe/desk/form/save.py
+++ b/frappe/desk/form/save.py
@@ -42,7 +42,6 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat
except Exception:
frappe.errprint(frappe.utils.get_traceback())
- frappe.msgprint(frappe._("Did not cancel"))
raise
def send_updated_docs(doc):
diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py
index 60e1f3242a..6d3aaee22b 100644
--- a/frappe/desk/page/setup_wizard/install_fixtures.py
+++ b/frappe/desk/page/setup_wizard/install_fixtures.py
@@ -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"))
diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js
index 5f91b376e8..1057cce2f3 100644
--- a/frappe/desk/page/user_profile/user_profile.js
+++ b/frappe/desk/page/user_profile/user_profile.js
@@ -76,6 +76,7 @@ class UserProfile {
fieldname: 'user',
options: 'User',
label: __('User'),
+ reqd: 1
}
],
primary_action_label: __('Go'),
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
index 9f5a5d84c8..36870d40bb 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -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
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index f249c36746..f4e6543844 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -150,7 +150,8 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
# 2 is the index of _relevance column
order_by = "_relevance, {0}, `tab{1}`.idx desc".format(order_by_based_on_meta, doctype)
- ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype))
+ ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
+ ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype))
if doctype in UNTRANSLATED_DOCTYPES:
page_length = None
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py
index 539f6c9db8..de27fafee3 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -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
\ No newline at end of file
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 343141c66d..ca4dbb83e2 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -210,7 +210,7 @@ class EmailAccount(Document):
elif not in_receive and any(map(lambda t: t in message, auth_error_codes)):
self.throw_invalid_credentials_exception()
else:
- frappe.throw(e)
+ frappe.throw(cstr(e))
except socket.error:
if in_receive:
diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py
index ee7f123b7e..bd8fadc29c 100644
--- a/frappe/email/doctype/newsletter/test_newsletter.py
+++ b/frappe/email/doctype/newsletter/test_newsletter.py
@@ -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
-
-from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe, send_scheduled_email
-from six.moves.urllib.parse import unquote
+import unittest
+from random import choice
+
+import frappe
+from frappe.email.doctype.newsletter.newsletter import (
+ confirmed_unsubscribe,
+ send_scheduled_email,
+)
+from frappe.email.doctype.newsletter.newsletter import get_newsletter_list
+from frappe.email.queue import flush
+from frappe.utils import add_days, getdate
test_dependencies = ["Email Group"]
+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:
+ for email in emails:
frappe.get_doc({
- "doctype": "Email Group",
- "title": "_Test Email Group"
+ "doctype": "Email Group Member",
+ "email": email,
+ "email_group": "_Test Email Group"
}).insert()
- for email in emails:
- 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)
\ No newline at end of file
+ self.assertTrue(email in recipients)
diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py
index d8a6a55510..e43b4d131c 100644
--- a/frappe/event_streaming/doctype/event_producer/event_producer.py
+++ b/frappe/event_streaming/doctype/event_producer/event_producer.py
@@ -295,7 +295,7 @@ def set_update(update, producer_site):
if data.changed:
local_doc.update(data.changed)
if data.removed:
- update_row_removed(local_doc, data.removed)
+ local_doc = update_row_removed(local_doc, data.removed)
if data.row_changed:
update_row_changed(local_doc, data.row_changed)
if data.added:
@@ -318,7 +318,17 @@ def update_row_removed(local_doc, removed):
for tablename, rownames in iteritems(removed):
table = local_doc.get_table_field_doctype(tablename)
for row in rownames:
- frappe.db.delete(table, row)
+ table_rows = local_doc.get(tablename)
+ child_table_row = get_child_table_row(table_rows, row)
+ table_rows.remove(child_table_row)
+ local_doc.set(tablename, table_rows)
+ return local_doc
+
+
+def get_child_table_row(table_rows, row):
+ for entry in table_rows:
+ if entry.get('name') == row:
+ return entry
def update_row_changed(local_doc, changed):
diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py
new file mode 100644
index 0000000000..d94a13ea41
--- /dev/null
+++ b/frappe/geo/utils.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+
+from pymysql import InternalError
+
+
+@frappe.whitelist()
+def get_coords(doctype, filters, type):
+ '''Get a geojson dict representing a doctype.'''
+ filters_sql = get_coords_conditions(doctype, filters)[4:]
+
+ coords = None
+ if type == 'location_field':
+ coords = return_location(doctype, filters_sql)
+ elif type == 'coordinates':
+ coords = return_coordinates(doctype, filters_sql)
+
+ out = convert_to_geojson(type, coords)
+ return out
+
+def convert_to_geojson(type, coords):
+ '''Converts GPS coordinates to geoJSON string.'''
+ geojson = {"type": "FeatureCollection", "features": None}
+
+ if type == 'location_field':
+ geojson['features'] = merge_location_features_in_one(coords)
+ elif type == 'coordinates':
+ geojson['features'] = create_gps_markers(coords)
+
+ return geojson
+
+
+def merge_location_features_in_one(coords):
+ '''Merging all features from location field.'''
+ geojson_dict = []
+ for element in coords:
+ geojson_loc = frappe.parse_json(element['location'])
+ if not geojson_loc:
+ continue
+ for coord in geojson_loc['features']:
+ coord['properties']['name'] = element['name']
+ geojson_dict.append(coord.copy())
+
+ return geojson_dict
+
+
+def create_gps_markers(coords):
+ '''Build Marker based on latitude and longitude.'''
+ geojson_dict = []
+ for i in coords:
+ node = {"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": None}}
+ node['properties']['name'] = i.name
+ node['geometry']['coordinates'] = [i.latitude, i.longitude]
+ geojson_dict.append(node.copy())
+
+ return geojson_dict
+
+
+def return_location(doctype, filters_sql):
+ '''Get name and location fields for Doctype.'''
+ if filters_sql:
+ try:
+ coords = frappe.db.sql('''SELECT name, location FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True)
+ except InternalError:
+ frappe.msgprint(frappe._('This Doctype does not contain location fields'), raise_exception=True)
+ return
+ else:
+ coords = frappe.get_all(doctype, fields=['name', 'location'])
+ return coords
+
+
+def return_coordinates(doctype, filters_sql):
+ '''Get name, latitude and longitude fields for Doctype.'''
+ if filters_sql:
+ try:
+ coords = frappe.db.sql('''SELECT name, latitude, longitude FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True)
+ except InternalError:
+ frappe.msgprint(frappe._('This Doctype does not contain latitude and longitude fields'), raise_exception=True)
+ return
+ else:
+ coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude'])
+ return coords
+
+
+def get_coords_conditions(doctype, filters=None):
+ '''Returns SQL conditions with user permissions and filters for event queries.'''
+ from frappe.desk.reportview import get_filters_cond
+ if not frappe.has_permission(doctype):
+ frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
+
+ return get_filters_cond(doctype, filters, [], with_match_conditions=True)
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 3d7ae0abb4..ea0a91a639 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -18,7 +18,7 @@ app_email = "info@frappe.io"
docs_app = "frappe_io"
-translator_url = "https://translatev2.erpnext.com"
+translator_url = "https://translate.erpnext.com"
before_install = "frappe.utils.install.before_install"
after_install = "frappe.utils.install.after_install"
diff --git a/frappe/integrations/desk_page/integrations/integrations.json b/frappe/integrations/desk_page/integrations/integrations.json
index 1acf4e6c4a..97e2b29d1a 100644
--- a/frappe/integrations/desk_page/integrations/integrations.json
+++ b/frappe/integrations/desk_page/integrations/integrations.json
@@ -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,
diff --git a/frappe/integrations/doctype/connected_app/__init__.py b/frappe/integrations/doctype/connected_app/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js
new file mode 100644
index 0000000000..4d20f65559
--- /dev/null
+++ b/frappe/integrations/doctype/connected_app/connected_app.js
@@ -0,0 +1,38 @@
+// Copyright (c) 2019, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Connected App', {
+ refresh: frm => {
+ frm.add_custom_button(__('Get OpenID Configuration'), async () => {
+ if (!frm.doc.openid_configuration) {
+ frappe.msgprint(__('Please enter OpenID Configuration URL'));
+ } else {
+ try {
+ const response = await fetch(frm.doc.openid_configuration);
+ const oidc = await response.json();
+ frm.set_value('authorization_uri', oidc.authorization_endpoint);
+ frm.set_value('token_uri', oidc.token_endpoint);
+ frm.set_value('userinfo_uri', oidc.userinfo_endpoint);
+ frm.set_value('introspection_uri', oidc.introspection_endpoint);
+ frm.set_value('revocation_uri', oidc.revocation_endpoint);
+ } catch (error) {
+ frappe.msgprint(__('Please check OpenID Configuration URL'));
+ }
+ }
+ });
+
+ if (!frm.is_new()) {
+ frm.add_custom_button(__('Connect to {}', [frm.doc.provider_name]), async () => {
+ frappe.call({
+ method: 'initiate_web_application_flow',
+ doc: frm.doc,
+ callback: function(r) {
+ window.open(r.message, '_blank');
+ }
+ });
+ });
+ }
+
+ frm.toggle_display('sb_client_credentials_section', !frm.is_new());
+ }
+});
diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json
new file mode 100644
index 0000000000..e5dbb0472a
--- /dev/null
+++ b/frappe/integrations/doctype/connected_app/connected_app.json
@@ -0,0 +1,166 @@
+{
+ "actions": [],
+ "beta": 1,
+ "creation": "2019-01-24 15:51:06.362222",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "provider_name",
+ "cb_00",
+ "openid_configuration",
+ "sb_client_credentials_section",
+ "client_id",
+ "redirect_uri",
+ "cb_01",
+ "client_secret",
+ "sb_scope_section",
+ "scopes",
+ "sb_endpoints_section",
+ "authorization_uri",
+ "token_uri",
+ "revocation_uri",
+ "cb_02",
+ "userinfo_uri",
+ "introspection_uri",
+ "section_break_18",
+ "query_parameters"
+ ],
+ "fields": [
+ {
+ "fieldname": "provider_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Provider Name",
+ "reqd": 1
+ },
+ {
+ "fieldname": "cb_00",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "openid_configuration",
+ "fieldtype": "Data",
+ "label": "OpenID Configuration"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "sb_client_credentials_section",
+ "fieldtype": "Section Break",
+ "label": "Client Credentials"
+ },
+ {
+ "fieldname": "client_id",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Client Id"
+ },
+ {
+ "fieldname": "redirect_uri",
+ "fieldtype": "Data",
+ "label": "Redirect URI",
+ "read_only": 1
+ },
+ {
+ "fieldname": "cb_01",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "client_secret",
+ "fieldtype": "Password",
+ "label": "Client Secret"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "sb_scope_section",
+ "fieldtype": "Section Break",
+ "label": "Scopes"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "sb_endpoints_section",
+ "fieldtype": "Section Break",
+ "label": "Endpoints"
+ },
+ {
+ "fieldname": "cb_02",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "scopes",
+ "fieldtype": "Table",
+ "label": "Scopes",
+ "options": "OAuth Scope"
+ },
+ {
+ "fieldname": "authorization_uri",
+ "fieldtype": "Data",
+ "label": "Authorization URI"
+ },
+ {
+ "fieldname": "token_uri",
+ "fieldtype": "Data",
+ "label": "Token URI"
+ },
+ {
+ "fieldname": "revocation_uri",
+ "fieldtype": "Data",
+ "label": "Revocation URI"
+ },
+ {
+ "fieldname": "userinfo_uri",
+ "fieldtype": "Data",
+ "label": "Userinfo URI"
+ },
+ {
+ "fieldname": "introspection_uri",
+ "fieldtype": "Data",
+ "label": "Introspection URI"
+ },
+ {
+ "fieldname": "section_break_18",
+ "fieldtype": "Section Break",
+ "label": "Extra Parameters"
+ },
+ {
+ "fieldname": "query_parameters",
+ "fieldtype": "Table",
+ "label": "Query Parameters",
+ "options": "Query Parameters"
+ }
+ ],
+ "links": [
+ {
+ "link_doctype": "Token Cache",
+ "link_fieldname": "connected_app"
+ }
+ ],
+ "modified": "2020-11-16 16:29:50.277405",
+ "modified_by": "Administrator",
+ "module": "Integrations",
+ "name": "Connected App",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "read": 1,
+ "role": "All"
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "provider_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py
new file mode 100644
index 0000000000..ec08f8e4be
--- /dev/null
+++ b/frappe/integrations/doctype/connected_app/connected_app.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import os
+from urllib.parse import urljoin
+from urllib.parse import urlencode
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from requests_oauthlib import OAuth2Session
+
+if any((os.getenv('CI'), frappe.conf.developer_mode, frappe.conf.allow_tests)):
+ # Disable mandatory TLS in developer mode and tests
+ os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
+
+class ConnectedApp(Document):
+ """Connect to a remote oAuth Server. Retrieve and store user's access token
+ in a Token Cache.
+ """
+
+ def validate(self):
+ base_url = frappe.utils.get_url()
+ callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name
+ self.redirect_uri = urljoin(base_url, callback_path)
+
+ def get_oauth2_session(self, user=None, init=False):
+ token = None
+ token_updater = None
+
+ if not init:
+ user = user or frappe.session.user
+ token_cache = self.get_user_token(user)
+ token = token_cache.get_json()
+ token_updater = token_cache.update_data
+
+ return OAuth2Session(
+ client_id=self.client_id,
+ token=token,
+ token_updater=token_updater,
+ auto_refresh_url=self.token_uri,
+ redirect_uri=self.redirect_uri,
+ scope=self.get_scopes()
+ )
+
+ def initiate_web_application_flow(self, user=None, success_uri=None):
+ """Return an authorization URL for the user. Save state in Token Cache."""
+ user = user or frappe.session.user
+ oauth = self.get_oauth2_session(init=True)
+ query_params = self.get_query_params()
+ authorization_url, state = oauth.authorization_url(self.authorization_uri, **query_params)
+ token_cache = self.get_token_cache(user)
+
+ if not token_cache:
+ token_cache = frappe.new_doc('Token Cache')
+ token_cache.user = user
+ token_cache.connected_app = self.name
+
+ token_cache.success_uri = success_uri
+ token_cache.state = state
+ token_cache.save(ignore_permissions=True)
+ frappe.db.commit()
+
+ return authorization_url
+
+ def get_user_token(self, user=None, success_uri=None):
+ """Return an existing user token or initiate a Web Application Flow."""
+ user = user or frappe.session.user
+ token_cache = self.get_token_cache(user)
+
+ if token_cache:
+ return token_cache
+
+ redirect = self.initiate_web_application_flow(user, success_uri)
+ frappe.local.response['type'] = 'redirect'
+ frappe.local.response['location'] = redirect
+ return redirect
+
+ def get_token_cache(self, user):
+ token_cache = None
+ token_cache_name = self.name + '-' + user
+
+ if frappe.db.exists('Token Cache', token_cache_name):
+ token_cache = frappe.get_doc('Token Cache', token_cache_name)
+
+ return token_cache
+
+ def get_scopes(self):
+ return [row.scope for row in self.scopes]
+
+ def get_query_params(self):
+ return {param.key: param.value for param in self.query_parameters}
+
+
+@frappe.whitelist(allow_guest=True)
+def callback(code=None, state=None):
+ """Handle client's code.
+
+ Called during the oauthorization flow by the remote oAuth2 server to
+ transmit a code that can be used by the local server to obtain an access
+ token.
+ """
+ if frappe.request.method != 'GET':
+ frappe.throw(_('Invalid request method: {}').format(frappe.request.method))
+
+ if frappe.session.user == 'Guest':
+ frappe.local.response['type'] = 'redirect'
+ frappe.local.response['location'] = '/login?' + urlencode({'redirect-to': frappe.request.url})
+ return
+
+ path = frappe.request.path[1:].split('/')
+ if len(path) != 4 or not path[3]:
+ frappe.throw(_('Invalid Parameters.'))
+
+ connected_app = frappe.get_doc('Connected App', path[3])
+ token_cache = frappe.get_doc('Token Cache', connected_app.name + '-' + frappe.session.user)
+
+ if state != token_cache.state:
+ frappe.throw(_('Invalid state.'))
+
+ oauth_session = connected_app.get_oauth2_session(init=True)
+ query_params = connected_app.get_query_params()
+ token = oauth_session.fetch_token(connected_app.token_uri,
+ code=code,
+ client_secret=connected_app.get_password('client_secret'),
+ include_client_id=True,
+ **query_params
+ )
+ token_cache.update_data(token)
+
+ frappe.local.response['type'] = 'redirect'
+ frappe.local.response['location'] = token_cache.get('success_uri') or connected_app.get_url()
diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py
new file mode 100644
index 0000000000..6faa542a60
--- /dev/null
+++ b/frappe/integrations/doctype/connected_app/test_connected_app.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies and contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+import requests
+from urllib.parse import urljoin
+
+import frappe
+from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key
+
+
+def get_user(usr, pwd):
+ user = frappe.new_doc('User')
+ user.email = usr
+ user.enabled = 1
+ user.first_name = "_Test"
+ user.new_password = pwd
+ user.roles = []
+ user.append('roles', {
+ 'doctype': 'Has Role',
+ 'parentfield': 'roles',
+ 'role': 'System Manager'
+ })
+ user.insert()
+
+ return user
+
+
+def get_connected_app():
+ doctype = 'Connected App'
+ connected_app = frappe.new_doc(doctype)
+ connected_app.provider_name = 'frappe'
+ connected_app.scopes = []
+ connected_app.append('scopes', {'scope': 'all'})
+ connected_app.insert()
+
+ return connected_app
+
+
+def get_oauth_client():
+ oauth_client = frappe.new_doc('OAuth Client')
+ oauth_client.app_name = '_Test Connected App'
+ oauth_client.redirect_uris = 'to be replaced'
+ oauth_client.default_redirect_uri = 'to be replaced'
+ oauth_client.grant_type = 'Authorization Code'
+ oauth_client.response_type = 'Code'
+ oauth_client.skip_authorization = 1
+ oauth_client.insert()
+
+ return oauth_client
+
+
+class TestConnectedApp(unittest.TestCase):
+
+ def setUp(self):
+ """Set up a Connected App that connects to our own oAuth provider.
+
+ Frappe comes with it's own oAuth2 provider that we can test against. The
+ client credentials can be obtained from an "OAuth Client". All depends
+ on "Social Login Key" so we create one as well.
+
+ The redirect URIs from "Connected App" and "OAuth Client" have to match.
+ Frappe's "Authorization URL" and "Access Token URL" (actually they're
+ just endpoints) are stored in "Social Login Key" so we get them from
+ there.
+ """
+ self.user_name = 'test-connected-app@example.com'
+ self.user_password = 'Eastern_43A1W'
+
+ self.user = get_user(self.user_name, self.user_password)
+ self.connected_app = get_connected_app()
+ self.oauth_client = get_oauth_client()
+ social_login_key = create_or_update_social_login_key()
+ self.base_url = social_login_key.get('base_url')
+
+ frappe.db.commit()
+ self.connected_app.reload()
+ self.oauth_client.reload()
+
+ redirect_uri = self.connected_app.get('redirect_uri')
+ self.oauth_client.update({
+ 'redirect_uris': redirect_uri,
+ 'default_redirect_uri': redirect_uri
+ })
+ self.oauth_client.save()
+
+ self.connected_app.update({
+ 'authorization_uri': urljoin(self.base_url, social_login_key.get('authorize_url')),
+ 'client_id': self.oauth_client.get('client_id'),
+ 'client_secret': self.oauth_client.get('client_secret'),
+ 'token_uri': urljoin(self.base_url, social_login_key.get('access_token_url'))
+ })
+ self.connected_app.save()
+
+ frappe.db.commit()
+ self.connected_app.reload()
+ self.oauth_client.reload()
+
+ def test_web_application_flow(self):
+ """Simulate a logged in user who opens the authorization URL."""
+ def login():
+ return session.get(urljoin(self.base_url, '/api/method/login'), params={
+ 'usr': self.user_name,
+ 'pwd': self.user_password
+ })
+
+ session = requests.Session()
+
+ # first login of a new user on a new site fails with "401 UNAUTHORIZED"
+ # when anybody fixes that, the two lines below can be removed
+ first_login = login()
+ self.assertEqual(first_login.status_code, 401)
+
+ second_login = login()
+ self.assertEqual(second_login.status_code, 200)
+
+ authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name)
+
+ auth_response = session.get(authorization_url)
+ self.assertEqual(auth_response.status_code, 200)
+
+ callback_response = session.get(auth_response.url)
+ self.assertEqual(callback_response.status_code, 200)
+
+ self.token_cache = self.connected_app.get_token_cache(self.user_name)
+ token = self.token_cache.get_password('access_token')
+ self.assertNotEqual(token, None)
+
+ oauth2_session = self.connected_app.get_oauth2_session(self.user_name)
+ resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user'))
+ self.assertEqual(resp.json().get('message'), self.user_name)
+
+ def tearDown(self):
+ def delete_if_exists(attribute):
+ doc = getattr(self, attribute, None)
+ if doc:
+ doc.delete()
+
+ delete_if_exists('token_cache')
+ delete_if_exists('connected_app')
+
+ if getattr(self, 'oauth_client', None):
+ tokens = frappe.get_all('OAuth Bearer Token', filters={
+ 'client': self.oauth_client.name
+ })
+ for token in tokens:
+ doc = frappe.get_doc('OAuth Bearer Token', token.name)
+ doc.delete()
+
+ codes = frappe.get_all('OAuth Authorization Code', filters={
+ 'client': self.oauth_client.name
+ })
+ for code in codes:
+ doc = frappe.get_doc('OAuth Authorization Code', code.name)
+ doc.delete()
+
+ delete_if_exists('user')
+ delete_if_exists('oauth_client')
+
+ frappe.db.commit()
diff --git a/frappe/integrations/doctype/connected_app/test_records.json b/frappe/integrations/doctype/connected_app/test_records.json
new file mode 100644
index 0000000000..4d19369248
--- /dev/null
+++ b/frappe/integrations/doctype/connected_app/test_records.json
@@ -0,0 +1,13 @@
+[
+ {
+ "doctype": "Connected App",
+ "provider_name": "frappe",
+ "client_id": "test_client_id",
+ "client_secret": "test_client_secret",
+ "scopes": [
+ {
+ "scope": "all"
+ }
+ ]
+ }
+]
diff --git a/frappe/integrations/doctype/oauth_client/test_records.json b/frappe/integrations/doctype/oauth_client/test_records.json
index cff06457c5..11e6338a87 100644
--- a/frappe/integrations/doctype/oauth_client/test_records.json
+++ b/frappe/integrations/doctype/oauth_client/test_records.json
@@ -1,7 +1,6 @@
[
{
- "app_name": "_Test OAuth Client",
- "client_id": "test_client_id",
+ "app_name": "_Test OAuth Client",
"client_secret": "test_client_secret",
"default_redirect_uri": "http://localhost",
"docstatus": 0,
diff --git a/frappe/integrations/doctype/oauth_scope/__init__.py b/frappe/integrations/doctype/oauth_scope/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.json b/frappe/integrations/doctype/oauth_scope/oauth_scope.json
new file mode 100644
index 0000000000..3a6e528999
--- /dev/null
+++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.json
@@ -0,0 +1,30 @@
+{
+ "actions": [],
+ "creation": "2020-07-15 22:08:14.616585",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "scope"
+ ],
+ "fields": [
+ {
+ "fieldname": "scope",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Scope"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-07-15 22:15:18.930632",
+ "modified_by": "Administrator",
+ "module": "Integrations",
+ "name": "OAuth Scope",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.py b/frappe/integrations/doctype/oauth_scope/oauth_scope.py
new file mode 100644
index 0000000000..a5dfe7e1ce
--- /dev/null
+++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class OAuthScope(Document):
+ pass
diff --git a/frappe/integrations/doctype/query_parameters/__init__.py b/frappe/integrations/doctype/query_parameters/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/integrations/doctype/query_parameters/query_parameters.json b/frappe/integrations/doctype/query_parameters/query_parameters.json
new file mode 100644
index 0000000000..de31c28df7
--- /dev/null
+++ b/frappe/integrations/doctype/query_parameters/query_parameters.json
@@ -0,0 +1,37 @@
+{
+ "actions": [],
+ "creation": "2020-11-16 14:54:37.226914",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "key",
+ "value"
+ ],
+ "fields": [
+ {
+ "fieldname": "key",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Key",
+ "reqd": 1
+ },
+ {
+ "fieldname": "value",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Value",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-11-16 15:18:35.887149",
+ "modified_by": "Administrator",
+ "module": "Integrations",
+ "name": "Query Parameters",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/query_parameters/query_parameters.py b/frappe/integrations/doctype/query_parameters/query_parameters.py
new file mode 100644
index 0000000000..bfb8eae0b6
--- /dev/null
+++ b/frappe/integrations/doctype/query_parameters/query_parameters.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class QueryParameters(Document):
+ pass
diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json
index 123bb21e88..2ca1723cb2 100755
--- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json
+++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json
@@ -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
-}
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
index 7c90d37f82..308d34c5c2 100755
--- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
+++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
@@ -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()
diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py
index 58bd48d64a..e0b99ad391 100644
--- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py
+++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py
@@ -22,3 +22,17 @@ def make_social_login_key(**kwargs):
kwargs["provider_name"] = "Test OAuth2 Provider"
doc = frappe.get_doc(kwargs)
return doc
+
+def create_or_update_social_login_key():
+ # used in other tests (connected app, oauth20)
+ try:
+ social_login_key = frappe.get_doc("Social Login Key", "frappe")
+ except frappe.DoesNotExistError:
+ social_login_key = frappe.new_doc("Social Login Key")
+ social_login_key.get_social_login_provider("Frappe", initialize=True)
+ social_login_key.base_url = frappe.utils.get_url()
+ social_login_key.enable_social_login = 0
+ social_login_key.save()
+ frappe.db.commit()
+
+ return social_login_key
diff --git a/frappe/integrations/doctype/token_cache/__init__.py b/frappe/integrations/doctype/token_cache/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/integrations/doctype/token_cache/test_records.json b/frappe/integrations/doctype/token_cache/test_records.json
new file mode 100644
index 0000000000..05840221a6
--- /dev/null
+++ b/frappe/integrations/doctype/token_cache/test_records.json
@@ -0,0 +1,18 @@
+[
+ {
+ "doctype": "Token Cache",
+ "user": "test@example.com",
+ "access_token": "test-access-token",
+ "refresh_token": "test-refresh-token",
+ "token_type": "Bearer",
+ "expires_in": 1000,
+ "scopes": [
+ {
+ "scope": "all"
+ },
+ {
+ "scope": "openid"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py
new file mode 100644
index 0000000000..73c9f38fce
--- /dev/null
+++ b/frappe/integrations/doctype/token_cache/test_token_cache.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies and contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+import frappe
+
+test_dependencies = ['User', 'Connected App', 'Token Cache']
+
+class TestTokenCache(unittest.TestCase):
+
+ def setUp(self):
+ self.token_cache = frappe.get_last_doc('Token Cache')
+ self.token_cache.update({'connected_app': frappe.get_last_doc('Connected App').name})
+ self.token_cache.save()
+
+ def test_get_auth_header(self):
+ self.token_cache.get_auth_header()
+
+ def test_update_data(self):
+ self.token_cache.update_data({
+ 'access_token': 'new-access-token',
+ 'refresh_token': 'new-refresh-token',
+ 'token_type': 'bearer',
+ 'expires_in': 2000,
+ 'scope': 'new scope'
+ })
+
+ def test_get_expires_in(self):
+ self.token_cache.get_expires_in()
+
+ def test_is_expired(self):
+ self.token_cache.is_expired()
+
+ def get_json(self):
+ self.token_cache.get_json()
diff --git a/frappe/integrations/doctype/token_cache/token_cache.js b/frappe/integrations/doctype/token_cache/token_cache.js
new file mode 100644
index 0000000000..b7cac9b804
--- /dev/null
+++ b/frappe/integrations/doctype/token_cache/token_cache.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Token Cache', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json
new file mode 100644
index 0000000000..c016405031
--- /dev/null
+++ b/frappe/integrations/doctype/token_cache/token_cache.json
@@ -0,0 +1,110 @@
+{
+ "actions": [],
+ "autoname": "format:{connected_app}-{user}",
+ "beta": 1,
+ "creation": "2019-01-24 16:56:55.631096",
+ "doctype": "DocType",
+ "document_type": "System",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "connected_app",
+ "provider_name",
+ "access_token",
+ "refresh_token",
+ "expires_in",
+ "state",
+ "scopes",
+ "success_uri",
+ "token_type"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "User",
+ "options": "User",
+ "read_only": 1
+ },
+ {
+ "fieldname": "connected_app",
+ "fieldtype": "Link",
+ "label": "Connected App",
+ "options": "Connected App",
+ "read_only": 1
+ },
+ {
+ "fieldname": "access_token",
+ "fieldtype": "Password",
+ "label": "Access Token",
+ "read_only": 1
+ },
+ {
+ "fieldname": "refresh_token",
+ "fieldtype": "Password",
+ "label": "Refresh Token",
+ "read_only": 1
+ },
+ {
+ "fieldname": "expires_in",
+ "fieldtype": "Int",
+ "label": "Expires In",
+ "read_only": 1
+ },
+ {
+ "fieldname": "state",
+ "fieldtype": "Data",
+ "label": "State",
+ "read_only": 1
+ },
+ {
+ "fieldname": "scopes",
+ "fieldtype": "Table",
+ "label": "Scopes",
+ "options": "OAuth Scope",
+ "read_only": 1
+ },
+ {
+ "fieldname": "success_uri",
+ "fieldtype": "Data",
+ "label": "Success URI",
+ "read_only": 1
+ },
+ {
+ "fieldname": "token_type",
+ "fieldtype": "Data",
+ "label": "Token Type",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "connected_app.provider_name",
+ "fieldname": "provider_name",
+ "fieldtype": "Data",
+ "label": "Provider Name",
+ "read_only": 1
+ }
+ ],
+ "links": [],
+ "modified": "2020-11-13 13:35:53.714352",
+ "modified_by": "Administrator",
+ "module": "Integrations",
+ "name": "Token Cache",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "delete": 1,
+ "read": 1,
+ "role": "System Manager"
+ },
+ {
+ "delete": 1,
+ "if_owner": 1,
+ "read": 1,
+ "role": "All"
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py
new file mode 100644
index 0000000000..7cac58fae0
--- /dev/null
+++ b/frappe/integrations/doctype/token_cache/token_cache.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+from datetime import datetime, timedelta
+
+import frappe
+from frappe import _
+from frappe.utils import cstr, cint
+from frappe.model.document import Document
+
+class TokenCache(Document):
+
+ def get_auth_header(self):
+ if self.access_token:
+ headers = {'Authorization': 'Bearer ' + self.get_password('access_token')}
+ return headers
+
+ raise frappe.exceptions.DoesNotExistError
+
+ def update_data(self, data):
+ """
+ Store data returned by authorization flow.
+
+ Params:
+ data - Dict with access_token, refresh_token, expires_in and scope.
+ """
+ token_type = cstr(data.get('token_type', '')).lower()
+ if token_type not in ['bearer', 'mac']:
+ frappe.throw(_('Received an invalid token type.'))
+ # 'Bearer' or 'MAC'
+ token_type = token_type.title() if token_type == 'bearer' else token_type.upper()
+
+ self.token_type = token_type
+ self.access_token = cstr(data.get('access_token', ''))
+ self.refresh_token = cstr(data.get('refresh_token', ''))
+ self.expires_in = cint(data.get('expires_in', 0))
+
+ new_scopes = data.get('scope')
+ if new_scopes:
+ if isinstance(new_scopes, str):
+ new_scopes = new_scopes.split(' ')
+ if isinstance(new_scopes, list):
+ self.scopes = None
+ for scope in new_scopes:
+ self.append('scopes', {'scope': scope})
+
+ self.state = None
+ self.save(ignore_permissions=True)
+ frappe.db.commit()
+ return self
+
+ def get_expires_in(self):
+ expiry_time = frappe.utils.get_datetime(self.modified) + timedelta(self.expires_in)
+ return (datetime.now() - expiry_time).total_seconds()
+
+ def is_expired(self):
+ return self.get_expires_in() < 0
+
+ def get_json(self):
+ return {
+ 'access_token': self.get_password('access_token', ''),
+ 'refresh_token': self.get_password('refresh_token', ''),
+ 'expires_in': self.get_expires_in(),
+ 'token_type': self.token_type
+ }
diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py
index f1556aa661..ad64d9f714 100644
--- a/frappe/integrations/doctype/webhook/webhook.py
+++ b/frappe/integrations/doctype/webhook/webhook.py
@@ -85,7 +85,7 @@ def enqueue_webhook(doc, webhook):
for i in range(3):
try:
- r = requests.post(webhook.request_url, data=json.dumps(data), headers=headers, timeout=5)
+ r = requests.post(webhook.request_url, data=json.dumps(data, default=str), headers=headers, timeout=5)
r.raise_for_status()
frappe.logger().debug({"webhook_success": r.text})
break
diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py
index a750c8328c..07db778a2d 100644
--- a/frappe/integrations/oauth2.py
+++ b/frappe/integrations/oauth2.py
@@ -20,6 +20,7 @@ def get_oauth_server():
return frappe.local.oauth_server
def sanitize_kwargs(param_kwargs):
+ """Remove 'data' and 'cmd' keys, if present."""
arguments = param_kwargs
arguments.pop('data', None)
arguments.pop('cmd', None)
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 5d86b3bac8..7a90ecaca5 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -48,7 +48,7 @@ def get_controller(doctype):
else:
class_overrides = frappe.get_hooks('override_doctype_class')
if class_overrides and class_overrides.get(doctype):
- import_path = frappe.get_hooks('override_doctype_class').get(doctype)[-1]
+ import_path = class_overrides[doctype][-1]
module_path, classname = import_path.rsplit('.', 1)
module = frappe.get_module(module_path)
if not hasattr(module, classname):
@@ -69,10 +69,13 @@ def get_controller(doctype):
if frappe.local.dev_server:
return _get_controller()
-
- key = '{}:doctype_classes'.format(frappe.local.site)
- return frappe.cache().hget(key, doctype, generator=_get_controller, shared=True)
-
+
+ site_controllers = frappe.controllers.setdefault(frappe.local.site, {})
+ if doctype not in site_controllers:
+ site_controllers[doctype] = _get_controller()
+
+ return site_controllers[doctype]
+
class BaseDocument(object):
ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index b936251b50..6f4e1fc53c 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -40,7 +40,10 @@ class DatabaseQuery(object):
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
update=None, add_total_row=None, user_settings=None, reference_doctype=None,
return_query=False, strict=True, pluck=None, ignore_ddl=False):
- if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user):
+ if not ignore_permissions and \
+ not frappe.has_permission(self.doctype, "select", user=user) and \
+ not frappe.has_permission(self.doctype, "read", user=user):
+
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype))
raise frappe.PermissionError(self.doctype)
@@ -315,7 +318,10 @@ class DatabaseQuery(object):
def append_table(self, table_name):
self.tables.append(table_name)
doctype = table_name[4:-1]
- if (not self.flags.ignore_permissions) and (not frappe.has_permission(doctype)):
+ ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
+
+ if (not self.flags.ignore_permissions) and\
+ (not frappe.has_permission(doctype, ptype=ptype)):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype))
raise frappe.PermissionError(doctype)
@@ -576,7 +582,7 @@ class DatabaseQuery(object):
self.shared = frappe.share.get_shared(self.doctype, self.user)
if (not meta.istable and
- not role_permissions.get("read") and
+ not (role_permissions.get("select") or role_permissions.get("read")) and
not self.flags.ignore_permissions and
not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)):
only_if_shared = True
@@ -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)
diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py
index 862abe375c..15de673e4b 100644
--- a/frappe/model/delete_doc.py
+++ b/frappe/model/delete_doc.py
@@ -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):
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 3789e20b19..9efd8b6c94 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -939,15 +939,17 @@ class Document(BaseDocument):
self.load_doc_before_save()
self.reset_seen()
+ # before_validate method should be executed before ignoring validations
+ if self._action in ("save", "submit"):
+ self.run_method("before_validate")
+
if self.flags.ignore_validate:
return
if self._action=="save":
- self.run_method("before_validate")
self.run_method("validate")
self.run_method("before_save")
elif self._action=="submit":
- self.run_method("before_validate")
self.run_method("validate")
self.run_method("before_submit")
elif self._action=="cancel":
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index c740d495c1..5dc7ca2d4d 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -68,7 +68,7 @@ def load_doctype_from_file(doctype):
class Meta(Document):
_metaclass = True
default_fields = list(default_fields)[1:]
- special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def", 'DocType Action', 'DocType Link')
+ special_doctypes = ("DocField", "DocPerm", "DocType", "Module Def", 'DocType Action', 'DocType Link')
def __init__(self, doctype):
self._fields = {}
@@ -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:
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index 35fbf94dc6..2c9dc5d823 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -21,8 +21,16 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne
docname = rename_doc(doctype=doctype, old=docname, new=new_name, merge=merge)
if old_title and new_title and not old_title == new_title:
- frappe.db.set_value(doctype, docname, title_field, new_title)
- frappe.msgprint(_('Saved'), alert=True, indicator='green')
+ try:
+ frappe.db.set_value(doctype, docname, title_field, new_title)
+ frappe.msgprint(_('Saved'), alert=True, indicator='green')
+ except Exception as e:
+ if frappe.db.is_duplicate_entry(e):
+ frappe.throw(
+ _("{0} {1} already exists").format(doctype, frappe.bold(docname)),
+ title=_("Duplicate Name"),
+ exc=frappe.DuplicateEntryError
+ )
return docname
@@ -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
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index 72ce8c9ce4..3e8125f9b1 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -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:
diff --git a/frappe/patches/v13_0/website_theme_custom_scss.py b/frappe/patches/v13_0/website_theme_custom_scss.py
index 0035283428..a5f08324e8 100644
--- a/frappe/patches/v13_0/website_theme_custom_scss.py
+++ b/frappe/patches/v13_0/website_theme_custom_scss.py
@@ -2,9 +2,23 @@ import frappe
def execute():
frappe.reload_doctype('Website Theme')
+ frappe.reload_doc('website', 'doctype', 'website_theme_ignore_app')
+ frappe.reload_doc('website', 'doctype', 'color')
+
for theme in frappe.get_all('Website Theme'):
doc = frappe.get_doc('Website Theme', theme.name)
if not doc.get('custom_scss') and doc.theme_scss:
# move old theme to new theme
doc.custom_scss = doc.theme_scss
+
+ if doc.background_color:
+ setup_color_record(doc.background_color)
+
doc.save()
+
+def setup_color_record(color):
+ frappe.get_doc({
+ "doctype": "Color",
+ "__newname": color,
+ "color": color,
+ }).save()
diff --git a/frappe/permissions.py b/frappe/permissions.py
index 0d766aec8d..abb1f6653a 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -7,7 +7,7 @@ import frappe, copy, json
from frappe import _, msgprint
from frappe.utils import cint
import frappe.share
-rights = ("read", "write", "create", "delete", "submit", "cancel", "amend",
+rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share")
# TODO:
@@ -73,6 +73,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra
role_permissions = get_role_permissions(meta, user=user)
perm = role_permissions.get(ptype)
+
if not perm:
push_perm_check_log(_('User {0} does not have doctype access via role permission for document {1}').format(frappe.bold(user), frappe.bold(doctype)))
@@ -192,9 +193,9 @@ def get_role_permissions(doctype_meta, user=None):
and ptype != 'create'):
perms['if_owner'][ptype] = 1
# has no access if not owner
- # only provide read access so that user is able to at-least access list
+ # only provide select or read access so that user is able to at-least access list
# (and the documents will be filtered based on owner sin further checks)
- perms[ptype] = 1 if ptype == 'read' else 0
+ perms[ptype] = 1 if ptype in ['select', 'read'] else 0
frappe.local.role_permissions[cache_key] = perms
@@ -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):
diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js
index 9ef5652dda..786f8f97ab 100644
--- a/frappe/printing/doctype/print_format/print_format.js
+++ b/frappe/printing/doctype/print_format/print_format.js
@@ -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);
diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json
index 6e64e802c9..3a47fb554f 100644
--- a/frappe/printing/doctype/print_format/print_format.json
+++ b/frappe/printing/doctype/print_format/print_format.json
@@ -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",
diff --git a/frappe/public/build.json b/frappe/public/build.json
index a3622499d5..ebc96e6f6b 100755
--- a/frappe/public/build.json
+++ b/frappe/public/build.json
@@ -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",
diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css
index 5ae77c73ca..88ad147d33 100644
--- a/frappe/public/css/list.css
+++ b/frappe/public/css/list.css
@@ -401,6 +401,13 @@ input.list-row-checkbox {
.pswp__more-item img {
max-height: 100%;
}
+.map-view-container {
+ display: flex;
+ flex-wrap: wrap;
+ width: 100%;
+ height: calc(100vh - 284px);
+ z-index: 0;
+}
.list-paging-area .gantt-view-mode {
margin-left: 15px;
margin-right: 15px;
diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js
index 319aa067cc..d7f873bee0 100644
--- a/frappe/public/js/frappe/form/controls/base_control.js
+++ b/frappe/public/js/frappe/form/controls/base_control.js
@@ -40,23 +40,31 @@ frappe.ui.form.Control = Class.extend({
return this.df.get_status(this);
}
- if((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form') {
+ if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) {
// like in case of a dialog box
if (cint(this.df.hidden)) {
// eslint-disable-next-line
- if(explain) console.log("By Hidden: None");
+ if (explain) console.log("By Hidden: None"); // eslint-disable-line no-console
return "None";
} else if (cint(this.df.hidden_due_to_dependency)) {
// eslint-disable-next-line
- if(explain) console.log("By Hidden Dependency: None");
+ if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console
return "None";
} else if (cint(this.df.read_only)) {
// eslint-disable-next-line
- if(explain) console.log("By Read Only: Read");
+ if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console
return "Read";
+ } else if ((this.grid &&
+ this.grid.display_status == 'Read') ||
+ (this.layout &&
+ this.layout.grid &&
+ this.layout.grid.display_status == 'Read')) {
+ // parent grid is read
+ if (explain) console.log("By Parent Grid Read-only: Read"); // eslint-disable-line no-console
+ return "Read";
}
return "Write";
@@ -65,13 +73,22 @@ frappe.ui.form.Control = Class.extend({
var status = frappe.perm.get_field_display_status(this.df,
frappe.model.get_doc(this.doctype, this.docname), this.perm || (this.frm && this.frm.perm), explain);
+ // Match parent grid controls read only status
+ if (status === 'Write' && (this.grid || (this.layout && this.layout.grid))) {
+ var grid = this.grid || this.layout.grid;
+ if (grid.display_status == 'Read') {
+ status = 'Read';
+ if (explain) console.log("By Parent Grid Read-only: Read"); // eslint-disable-line no-console
+ }
+ }
+
// hide if no value
if (this.doctype && status==="Read" && !this.only_input
&& is_null(frappe.model.get_value(this.doctype, this.docname, this.df.fieldname))
&& !in_list(["HTML", "Image", "Button"], this.df.fieldtype)) {
// eslint-disable-next-line
- if(explain) console.log("By Hide Read-only, null fields: None");
+ if (explain) console.log("By Hide Read-only, null fields: None"); // eslint-disable-line no-console
status = "None";
}
diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js
index 2f051a4701..acce79da40 100644
--- a/frappe/public/js/frappe/form/controls/base_input.js
+++ b/frappe/public/js/frappe/form/controls/base_input.js
@@ -20,7 +20,7 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
\
\
').appendTo(this.parent);
diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js
index f3c51e0232..6df7094c26 100644
--- a/frappe/public/js/frappe/form/controls/code.js
+++ b/frappe/public/js/frappe/form/controls/code.js
@@ -10,7 +10,7 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
.appendTo(this.input_area);
this.expanded = false;
- this.$expand_button = $(``).click(() => {
+ this.$expand_button = $(``).click(() => {
this.expanded = !this.expanded;
this.refresh_height();
this.toggle_label();
@@ -38,8 +38,11 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
},
toggle_label() {
- const button_label = this.expanded ? __('Collapse') : __('Expand');
- this.$expand_button && this.$expand_button.text(button_label);
+ this.$expand_button && this.$expand_button.text(this.get_button_label());
+ },
+
+ get_button_label() {
+ return this.expanded ? __('Collapse', null, 'Shrink code field.') : __('Expand', null, 'Enlarge code field.');
},
set_language() {
diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js
index 9dfad09299..dfd0f4d174 100644
--- a/frappe/public/js/frappe/form/controls/geolocation.js
+++ b/frappe/public/js/frappe/form/controls/geolocation.js
@@ -1,3 +1,5 @@
+frappe.provide('frappe.utils.utils');
+
frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
horizontal: false,
@@ -15,7 +17,7 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
this.map_area.prependTo($input_wrapper);
this.$wrapper.find('.control-input').addClass("hidden");
- if ($input_wrapper.is(':visible')) {
+ if (this.frm) {
this.make_map();
} else {
$(document).on('frappe.ui.Dialog:shown', () => {
@@ -90,11 +92,11 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
});
L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/';
- this.map = L.map(this.map_id).setView([19.0800, 72.8961], 13);
+ this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center,
+ frappe.utils.map_defaults.zoom);
- L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
- attribution: '© OpenStreetMap contributors'
- }).addTo(this.map);
+ L.tileLayer(frappe.utils.map_defaults.tiles,
+ frappe.utils.map_defaults.options).addTo(this.map);
},
bind_leaflet_locate_control() {
diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js
index 56f9430238..4c0fe39f60 100644
--- a/frappe/public/js/frappe/form/controls/link.js
+++ b/frappe/public/js/frappe/form/controls/link.js
@@ -49,6 +49,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
this.translate_values = true;
this.setup_buttons();
this.setup_awesomeplete();
+ this.bind_change_event();
},
get_options: function() {
return this.df.options;
@@ -215,6 +216,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
}
me.$input.cache[doctype][term] = r.results;
me.awesomplete.list = me.$input.cache[doctype][term];
+ me.toggle_href(doctype);
}
});
}, 500));
@@ -296,6 +298,15 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
// returns [{value: 'Manufacturer 1', 'description': 'mobile part 1, mobile part 2'}]
},
+ toggle_href(doctype) {
+ if (frappe.model.can_select(doctype) && !frappe.model.can_read(doctype)) {
+ // remove href from link field as user has only select perm
+ this.$input_area.find(".link-btn").addClass('hide');
+ } else {
+ this.$input_area.find(".link-btn").removeClass('hide');
+ }
+ },
+
get_filter_description(filters) {
let doctype = this.get_options();
let filter_array = [];
diff --git a/frappe/public/js/frappe/form/controls/rating.js b/frappe/public/js/frappe/form/controls/rating.js
index 34e890d45c..191db35538 100644
--- a/frappe/public/js/frappe/form/controls/rating.js
+++ b/frappe/public/js/frappe/form/controls/rating.js
@@ -47,7 +47,7 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({
});
},
get_value() {
- return cint(this.value);
+ return cint(this.value, null);
},
set_formatted_input(value) {
let el = $(this.input_area).find('i');
diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js
index 14fad1c010..a87a4ad2a6 100644
--- a/frappe/public/js/frappe/form/controls/table.js
+++ b/frappe/public/js/frappe/form/controls/table.js
@@ -9,7 +9,8 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({
frm: this.frm,
df: this.df,
perm: this.perm || (this.frm && this.frm.perm) || this.df.perm,
- parent: this.wrapper
+ parent: this.wrapper,
+ control: this
});
if(this.frm) {
this.frm.grids[this.frm.grids.length] = this;
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 90b628f269..1e23969afa 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -1271,7 +1271,10 @@ frappe.ui.form.Form = class FrappeForm {
}
if (df && df[property] != value) {
df[property] = value;
- this.refresh_field(fieldname);
+ if (!docname || !table_field) {
+ // do not refresh childtable fields since `this.fields_dict` doesn't have child table fields
+ this.refresh_field(fieldname);
+ }
}
}
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index 3f422d0a9b..be3f10fd0c 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -50,7 +50,15 @@ frappe.form.formatters = {
return frappe.form.formatters._right(value==null ? "" : cint(value), options)
},
Percent: function(value, docfield, options) {
- return frappe.form.formatters._right(flt(value, 2) + "%", options)
+ const precision = (
+ docfield.precision
+ || cint(
+ frappe.boot.sysdefaults
+ && frappe.boot.sysdefaults.float_precision
+ )
+ || 2
+ );
+ return frappe.form.formatters._right(flt(value, precision) + "%", options);
},
Rating: function(value) {
return `
@@ -120,11 +128,16 @@ frappe.form.formatters = {
return repl('%(value)s',
{onclick: docfield.link_onclick.replace(/"/g, '"'), value:value});
} else if(docfield && doctype) {
- return `
- ${__(options && options.label || value)}`
+ if (!frappe.model.can_select(doctype) && frappe.model.can_read(doctype)) {
+ return `
+ ${__(options && options.label || value)}`;
+ } else {
+ return value;
+ }
+
} else {
return value;
}
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index 9c916ccc4a..8ef5860d0d 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -264,6 +264,8 @@ export default class Grid {
if (this.frm) {
this.display_status = frappe.perm.get_field_display_status(this.df, this.frm.doc,
this.perm);
+ } else if (this.df.is_web_form && this.control) {
+ this.display_status = this.control.get_status();
} else {
// not in form
this.display_status = 'Write';
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index ec9cee9c39..466032dbef 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -568,13 +568,15 @@ export default class GridRow {
this.wrapper.removeClass("grid-row-open");
}
open_prev() {
- if(this.grid.grid_rows[this.doc.idx-2]) {
- this.grid.grid_rows[this.doc.idx-2].toggle_view(true);
+ const row_index = this.wrapper.index();
+ if (this.grid.grid_rows[row_index - 1]) {
+ this.grid.grid_rows[row_index - 1].toggle_view(true);
}
}
open_next() {
- if(this.grid.grid_rows[this.doc.idx]) {
- this.grid.grid_rows[this.doc.idx].toggle_view(true);
+ const row_index = this.wrapper.index();
+ if (this.grid.grid_rows[row_index + 1]) {
+ this.grid.grid_rows[row_index + 1].toggle_view(true);
} else {
this.grid.add_new_row(null, null, true);
}
diff --git a/frappe/public/js/frappe/form/grid_row_form.js b/frappe/public/js/frappe/form/grid_row_form.js
index f93640936f..71c0c6e679 100644
--- a/frappe/public/js/frappe/form/grid_row_form.js
+++ b/frappe/public/js/frappe/form/grid_row_form.js
@@ -16,6 +16,9 @@ export default class GridRowForm {
body: this.form_area,
no_submit_on_enter: true,
frm: this.row.frm,
+ grid: this.row.grid,
+ grid_row: this.row,
+ grid_row_form: this,
});
this.layout.make();
diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js
index 3505cf4857..e0aa2d645e 100644
--- a/frappe/public/js/frappe/form/layout.js
+++ b/frappe/public/js/frappe/form/layout.js
@@ -1,7 +1,7 @@
import '../class';
frappe.ui.form.Layout = Class.extend({
- init: function(opts) {
+ init: function (opts) {
this.views = {};
this.pages = [];
this.sections = [];
@@ -10,24 +10,24 @@ frappe.ui.form.Layout = Class.extend({
$.extend(this, opts);
},
- make: function() {
- if(!this.parent && this.body) {
+ make: function () {
+ if (!this.parent && this.body) {
this.parent = this.body;
}
this.wrapper = $('').appendTo(this.parent);
this.message = $('
').appendTo(this.wrapper);
- if(!this.fields) {
+ if (!this.fields) {
this.fields = this.get_doctype_fields();
}
this.setup_tabbing();
this.render();
},
- show_empty_form_message: function() {
- if(!(this.wrapper.find(".frappe-control:visible").length || this.wrapper.find(".section-head.collapsed").length)) {
+ show_empty_form_message: function () {
+ if (!(this.wrapper.find(".frappe-control:visible").length || this.wrapper.find(".section-head.collapsed").length)) {
this.show_message(__("This form does not have any input"));
}
},
- get_doctype_fields: function() {
+ get_doctype_fields: function () {
let fields = [
{
parent: this.frm.doctype,
@@ -36,7 +36,7 @@ frappe.ui.form.Layout = Class.extend({
reqd: 1,
hidden: 1,
label: __('Name'),
- get_status: function(field) {
+ get_status: function (field) {
if (field.frm && field.frm.is_new()
&& field.frm.meta.autoname
&& ['prompt', 'name'].includes(field.frm.meta.autoname.toLowerCase())) {
@@ -49,14 +49,14 @@ frappe.ui.form.Layout = Class.extend({
fields = fields.concat(frappe.meta.sort_docfields(frappe.meta.docfield_map[this.doctype]));
return fields;
},
- show_message: function(html, color) {
+ show_message: function (html, color) {
if (this.message_color) {
// remove previous color
this.message.removeClass(this.message_color);
}
this.message_color = (color && ['yellow', 'blue'].includes(color)) ? color : 'blue';
- if(html) {
- if(html.substr(0, 1)!=='<') {
+ if (html) {
+ if (html.substr(0, 1) !== '<') {
// wrap in a block
html = '
' + html + '
';
}
@@ -66,7 +66,7 @@ frappe.ui.form.Layout = Class.extend({
this.message.empty().addClass('hidden');
}
},
- render: function(new_fields) {
+ render: function (new_fields) {
var me = this;
var fields = new_fields || this.fields;
@@ -80,8 +80,8 @@ frappe.ui.form.Layout = Class.extend({
if (this.no_opening_section()) {
this.make_section();
}
- $.each(fields, function(i, df) {
- switch(df.fieldtype) {
+ $.each(fields, function (i, df) {
+ switch (df.fieldtype) {
case "Fold":
me.make_page(df);
break;
@@ -98,11 +98,11 @@ frappe.ui.form.Layout = Class.extend({
},
- no_opening_section: function() {
- return (this.fields[0] && this.fields[0].fieldtype!="Section Break") || !this.fields.length;
+ no_opening_section: function () {
+ return (this.fields[0] && this.fields[0].fieldtype != "Section Break") || !this.fields.length;
},
- setup_dashboard_section: function() {
+ setup_dashboard_section: function () {
if (this.no_opening_section()) {
this.fields.unshift({fieldtype: 'Section Break'});
}
@@ -117,7 +117,7 @@ frappe.ui.form.Layout = Class.extend({
});
},
- replace_field: function(fieldname, df, render) {
+ replace_field: function (fieldname, df, render) {
df.fieldname = fieldname; // change of fieldname is avoided
if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) {
const fieldobj = this.init_field(df, render);
@@ -133,14 +133,14 @@ frappe.ui.form.Layout = Class.extend({
}
},
- make_field: function(df, colspan, render) {
+ make_field: function (df, colspan, render) {
!this.section && this.make_section();
!this.column && this.make_column();
const fieldobj = this.init_field(df, render);
this.fields_list.push(fieldobj);
this.fields_dict[df.fieldname] = fieldobj;
- if(this.frm) {
+ if (this.frm) {
fieldobj.perm = this.frm.perm;
}
@@ -149,31 +149,32 @@ frappe.ui.form.Layout = Class.extend({
fieldobj.section = this.section;
},
- init_field: function(df, render = false) {
+ init_field: function (df, render = false) {
const fieldobj = frappe.ui.form.make_control({
df: df,
doctype: this.doctype,
parent: this.column.wrapper.get(0),
frm: this.frm,
render_input: render,
- doc: this.doc
+ doc: this.doc,
+ layout: this
});
fieldobj.layout = this;
return fieldobj;
},
- make_page: function(df) {
+ make_page: function (df) { // eslint-disable-line no-unused-vars
var me = this,
head = $('
').appendTo(this.wrapper);
this.page = $('
').appendTo(this.wrapper);
- this.fold_btn = head.find(".btn-fold").on("click", function() {
+ this.fold_btn = head.find(".btn-fold").on("click", function () {
var page = $(this).parent().next();
- if(page.hasClass("hide")) {
+ if (page.hasClass("hide")) {
$(this).removeClass("btn-fold").html(__("Hide details"));
page.removeClass("hide");
frappe.utils.scroll_to($(this), true, 30);
@@ -189,15 +190,15 @@ frappe.ui.form.Layout = Class.extend({
this.folded = true;
},
- unfold: function() {
+ unfold: function () {
this.fold_btn.trigger('click');
},
- make_section: function(df) {
+ make_section: function (df) {
this.section = new frappe.ui.form.Section(this, df);
// append to layout fields
- if(df) {
+ if (df) {
this.fields_dict[df.fieldname] = this.section;
this.fields_list.push(this.section);
}
@@ -205,16 +206,16 @@ frappe.ui.form.Layout = Class.extend({
this.column = null;
},
- make_column: function(df) {
+ make_column: function (df) {
this.column = new frappe.ui.form.Column(this.section, df);
- if(df && df.fieldname) {
+ if (df && df.fieldname) {
this.fields_list.push(this.column);
}
},
- refresh: function(doc) {
+ refresh: function (doc) {
var me = this;
- if(doc) this.doc = doc;
+ if (doc) this.doc = doc;
if (this.frm) {
this.wrapper.find(".empty-form-alert").remove();
@@ -223,7 +224,7 @@ frappe.ui.form.Layout = Class.extend({
// NOTE this might seem redundant at first, but it needs to be executed when frm.refresh_fields is called
me.attach_doc_and_docfields(true);
- if(this.frm && this.frm.wrapper) {
+ if (this.frm && this.frm.wrapper) {
$(this.frm.wrapper).trigger("refresh-fields");
}
@@ -234,26 +235,26 @@ frappe.ui.form.Layout = Class.extend({
this.refresh_sections();
// collapse sections
- if(this.frm) {
+ if (this.frm) {
this.refresh_section_collapse();
}
},
- refresh_sections: function() {
+ refresh_sections: function () {
var cnt = 0;
// hide invisible sections and set alternate background color
- this.wrapper.find(".form-section:not(.hide-control)").each(function() {
+ this.wrapper.find(".form-section:not(.hide-control)").each(function () {
var $this = $(this).removeClass("empty-section")
.removeClass("visible-section")
.removeClass("shaded-section");
- if(!$this.find(".frappe-control:not(.hide-control)").length
+ if (!$this.find(".frappe-control:not(.hide-control)").length
&& !$this.hasClass('form-dashboard')) {
// nothing visible, hide the section
$this.addClass("empty-section");
} else {
$this.addClass("visible-section");
- if(cnt % 2) {
+ if (cnt % 2) {
$this.addClass("shaded-section");
}
cnt++;
@@ -261,36 +262,36 @@ frappe.ui.form.Layout = Class.extend({
});
},
- refresh_fields: function(fields) {
+ refresh_fields: function (fields) {
let fieldnames = fields.map((field) => {
- if(field.fieldname) return field.fieldname;
+ if (field.fieldname) return field.fieldname;
});
this.fields_list.map(fieldobj => {
- if(fieldnames.includes(fieldobj.df.fieldname)) {
+ if (fieldnames.includes(fieldobj.df.fieldname)) {
fieldobj.refresh();
- if(fieldobj.df["default"]) {
+ if (fieldobj.df["default"]) {
fieldobj.set_input(fieldobj.df["default"]);
}
}
});
},
- add_fields: function(fields) {
+ add_fields: function (fields) {
this.render(fields);
this.refresh_fields(fields);
},
- refresh_section_collapse: function() {
- if(!this.doc) return;
+ refresh_section_collapse: function () {
+ if (!this.doc) return;
- for(var i=0; i
=0;i--) {
+ for (var i = me.fields_list.length - 1; i >= 0; i--) {
var f = me.fields_list[i];
f.guardian_has_value = true;
if (f.df.depends_on) {
@@ -473,12 +474,12 @@ frappe.ui.form.Layout = Class.extend({
// show / hide
if (f.guardian_has_value) {
- if(f.df.hidden_due_to_dependency) {
+ if (f.df.hidden_due_to_dependency) {
f.df.hidden_due_to_dependency = false;
f.refresh();
}
} else {
- if(!f.df.hidden_due_to_dependency) {
+ if (!f.df.hidden_due_to_dependency) {
f.df.hidden_due_to_dependency = true;
f.refresh();
}
@@ -496,25 +497,27 @@ frappe.ui.form.Layout = Class.extend({
this.refresh_section_count();
},
- set_dependant_property: function(condition, fieldname, property) {
+ set_dependant_property: function (condition, fieldname, property) {
let set_property = this.evaluate_depends_on_value(condition);
let value = set_property ? 1 : 0;
let form_obj;
if (this.frm) {
form_obj = this.frm;
- } else if (this.is_dialog) {
+ } else if (this.is_dialog || this.doctype === 'Web Form') {
form_obj = this;
}
if (form_obj) {
if (this.doc && this.doc.parent) {
form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname);
+ // refresh child fields
+ this.fields_dict[fieldname] && this.fields_dict[fieldname].refresh();
} else {
form_obj.set_df_property(fieldname, property, value);
}
}
},
- evaluate_depends_on_value: function(expression) {
+ evaluate_depends_on_value: function (expression) {
var out = null;
var doc = this.doc;
@@ -528,27 +531,27 @@ frappe.ui.form.Layout = Class.extend({
var parent = this.frm ? this.frm.doc : this.doc || null;
- if(typeof(expression) === 'boolean') {
+ if (typeof (expression) === 'boolean') {
out = expression;
- } else if(typeof(expression) === 'function') {
+ } else if (typeof (expression) === 'function') {
out = expression(doc);
- } else if(expression.substr(0,5)=='eval:') {
+ } else if (expression.substr(0, 5) == 'eval:') {
try {
out = eval(expression.substr(5));
- if(parent && parent.istable && expression.includes('is_submittable')) {
+ if (parent && parent.istable && expression.includes('is_submittable')) {
out = true;
}
- } catch(e) {
+ } catch (e) {
frappe.throw(__('Invalid "depends_on" expression'));
}
- } else if(expression.substr(0,3)=='fn:' && this.frm) {
+ } else if (expression.substr(0, 3) == 'fn:' && this.frm) {
out = this.frm.script_manager.trigger(expression.substr(3), this.doctype, this.docname);
} else {
var value = doc[expression];
- if($.isArray(value)) {
+ if ($.isArray(value)) {
out = !!value.length;
} else {
out = !!value;
@@ -560,7 +563,7 @@ frappe.ui.form.Layout = Class.extend({
});
frappe.ui.form.Section = Class.extend({
- init: function(layout, df) {
+ init: function (layout, df) {
var me = this;
this.layout = layout;
this.df = df || {};
@@ -580,8 +583,8 @@ frappe.ui.form.Section = Class.extend({
this.refresh();
},
- make: function() {
- if(!this.layout.page) {
+ make: function () {
+ if (!this.layout.page) {
this.layout.page = $('').appendTo(this.layout.wrapper);
}
@@ -589,15 +592,15 @@ frappe.ui.form.Section = Class.extend({
.appendTo(this.layout.page);
this.layout.sections.push(this);
- if(this.df) {
- if(this.df.label) {
+ if (this.df) {
+ if (this.df.label) {
this.make_head();
}
- if(this.df.description) {
+ if (this.df.description) {
$('' + __(this.df.description) + '
')
.appendTo(this.wrapper);
}
- if(this.df.cssClass) {
+ if (this.df.cssClass) {
this.wrapper.addClass(this.df.cssClass);
}
if (this.df.hide_border) {
@@ -609,49 +612,49 @@ frappe.ui.form.Section = Class.extend({
this.body = $('').appendTo(this.wrapper);
},
- make_head: function() {
+ make_head: function () {
var me = this;
- if(!this.df.collapsible) {
+ if (!this.df.collapsible) {
$('
')
.appendTo(this.wrapper);
} else {
this.head = $('
').appendTo(this.wrapper);
+ + __(this.df.label) + '
').appendTo(this.wrapper);
// show / hide based on status
- this.collapse_link = this.head.on("click", function() {
+ this.collapse_link = this.head.on("click", function () {
me.collapse();
});
this.indicator = this.head.find(".collapse-indicator");
}
},
- refresh: function() {
- if(!this.df)
+ refresh: function () {
+ if (!this.df)
return;
// hide if explictly hidden
var hide = this.df.hidden || this.df.hidden_due_to_dependency;
// hide if no perm
- if(!hide && this.layout && this.layout.frm && !this.layout.frm.get_perm(this.df.permlevel || 0, "read")) {
+ if (!hide && this.layout && this.layout.frm && !this.layout.frm.get_perm(this.df.permlevel || 0, "read")) {
hide = true;
}
this.wrapper.toggleClass("hide-control", !!hide);
},
- collapse: function(hide) {
+ collapse: function (hide) {
// unknown edge case
if (!(this.head && this.body)) {
return;
}
- if(hide===undefined) {
+ if (hide === undefined) {
hide = !this.body.hasClass("hide");
}
- if (this.df.fieldname==='_form_dashboard') {
+ if (this.df.fieldname === '_form_dashboard') {
localStorage.setItem('collapseFormDashboard', hide ? 'yes' : 'no');
}
@@ -662,7 +665,7 @@ frappe.ui.form.Section = Class.extend({
// refresh signature fields
this.fields_list.forEach((f) => {
- if (f.df.fieldtype=='Signature') {
+ if (f.df.fieldtype == 'Signature') {
f.refresh();
}
});
@@ -672,11 +675,11 @@ frappe.ui.form.Section = Class.extend({
return this.body.hasClass('hide');
},
- has_missing_mandatory: function() {
+ has_missing_mandatory: function () {
var missing_mandatory = false;
- for (var j=0, l=this.fields_list.length; j < l; j++) {
+ for (var j = 0, l = this.fields_list.length; j < l; j++) {
var section_df = this.fields_list[j].df;
- if (section_df.reqd && this.layout.doc[section_df.fieldname]==null) {
+ if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) {
missing_mandatory = true;
break;
}
@@ -686,21 +689,21 @@ frappe.ui.form.Section = Class.extend({
});
frappe.ui.form.Column = Class.extend({
- init: function(section, df) {
- if(!df) df = {};
+ init: function (section, df) {
+ if (!df) df = {};
this.df = df;
this.section = section;
this.make();
this.resize_all_columns();
},
- make: function() {
+ make: function () {
this.wrapper = $('\
\
').appendTo(this.section.body)
.find("form")
- .on("submit", function() {
+ .on("submit", function () {
return false;
});
@@ -709,7 +712,7 @@ frappe.ui.form.Column = Class.extend({
+ '').appendTo(this.wrapper);
}
},
- resize_all_columns: function() {
+ resize_all_columns: function () {
// distribute all columns equally
var colspan = cint(12 / this.section.wrapper.find(".form-column").length);
@@ -718,7 +721,7 @@ frappe.ui.form.Column = Class.extend({
.addClass("col-sm-" + colspan);
},
- refresh: function() {
+ refresh: function () {
this.section.refresh();
}
});
diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js
index 2da7b8f236..eed49e070b 100644
--- a/frappe/public/js/frappe/form/quick_entry.js
+++ b/frappe/public/js/frappe/form/quick_entry.js
@@ -36,9 +36,14 @@ frappe.ui.form.QuickEntryForm = Class.extend({
this.render_dialog();
resolve(this);
} else {
+ // no quick entry, open full form
frappe.quick_entry = null;
frappe.set_route('Form', this.doctype, this.doc.name)
.then(() => resolve(this));
+ // call init_callback for consistency
+ if (this.init_callback) {
+ this.init_callback(this.doc);
+ }
}
});
});
diff --git a/frappe/public/js/frappe/form/script_helpers.js b/frappe/public/js/frappe/form/script_helpers.js
index 83ba191d4d..0465624975 100644
--- a/frappe/public/js/frappe/form/script_helpers.js
+++ b/frappe/public/js/frappe/form/script_helpers.js
@@ -16,17 +16,19 @@ window.refresh_field = function(n, docname, table_field) {
if(typeof n==typeof [])
refresh_many(n, docname, table_field);
- if (n && typeof n==='string' && table_field){
+ if (n && typeof n==='string' && table_field) {
var grid = cur_frm.fields_dict[table_field].grid,
- field = frappe.utils.filter_dict(grid.docfields, {fieldname: n});
- if (field && field.length){
+ field = frappe.utils.filter_dict(grid.docfields, {fieldname: n}),
+ grid_row = grid.grid_rows_by_docname[docname];
+
+ if (field && field.length) {
field = field[0];
var meta = frappe.meta.get_docfield(field.parent, field.fieldname, docname);
$.extend(field, meta);
- if (docname){
- cur_frm.fields_dict[table_field].grid.grid_rows_by_docname[docname].refresh_field(n);
+ if (grid_row) {
+ grid_row.refresh_field(n);
} else {
- cur_frm.fields_dict[table_field].grid.refresh();
+ grid.refresh();
}
}
} else if(cur_frm) {
diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js
index eab09c1e10..eb70b255eb 100644
--- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js
+++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js
@@ -99,7 +99,7 @@ frappe.ui.form.Sidebar = class {
__("{0} edited this {1}", [
frappe.user.full_name(this.frm.doc.modified_by).bold(),
"
" + comment_when(this.frm.doc.modified),
- ])
+ ], "For example, 'Jon Doe edited this 5 minutes ago'.")
);
this.sidebar
.find(".created-by")
@@ -107,7 +107,7 @@ frappe.ui.form.Sidebar = class {
__("{0} created this {1}", [
frappe.user.full_name(this.frm.doc.owner).bold(),
"
" + comment_when(this.frm.doc.creation),
- ])
+ ], "For example, 'Jon Doe created this 5 minutes ago'.")
);
this.refresh_like();
diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js
index c7fb69a2b5..d8a2b91277 100644
--- a/frappe/public/js/frappe/form/toolbar.js
+++ b/frappe/public/js/frappe/form/toolbar.js
@@ -441,9 +441,23 @@ frappe.ui.form.Toolbar = Class.extend({
me.frm.page.set_view('main');
}, 'octicon octicon-pencil');
} else if(status === "Cancel") {
- this.page.set_secondary_action(__(status), function() {
- me.frm.savecancel(this);
- }, "octicon octicon-circle-slash");
+ let add_cancel_button = () => {
+ this.page.set_secondary_action(__(status), function() {
+ me.frm.savecancel(this);
+ }, "octicon octicon-circle-slash");
+ };
+ if (this.has_workflow()) {
+ frappe.xcall(
+ 'frappe.model.workflow.can_cancel_document', {
+ 'doctype': this.frm.doc.doctype,
+ }).then((can_cancel) => {
+ if (can_cancel) {
+ add_cancel_button();
+ }
+ });
+ } else {
+ add_cancel_button();
+ }
} else {
var click = {
"Save": function() {
diff --git a/frappe/public/js/frappe/form/workflow.js b/frappe/public/js/frappe/form/workflow.js
index 4c59e8219b..16d9f8676b 100644
--- a/frappe/public/js/frappe/form/workflow.js
+++ b/frappe/public/js/frappe/form/workflow.js
@@ -85,7 +85,7 @@ frappe.ui.form.States = Class.extend({
frappe.workflow.get_transitions(this.frm.doc).then(transitions => {
this.frm.page.clear_actions_menu();
transitions.forEach(d => {
- if(frappe.user_roles.includes(d.allowed) && has_approval_access(d)) {
+ if (frappe.user_roles.includes(d.allowed) && has_approval_access(d)) {
added = true;
me.frm.page.add_action_item(__(d.action), function() {
// set the workflow_action for use in form scripts
@@ -103,17 +103,8 @@ frappe.ui.form.States = Class.extend({
});
}
});
- if (!added) {
- //call function and clear cancel button if Cancel doc state is defined in the workfloe
- frappe.xcall('frappe.model.workflow.can_cancel_document', {doc: this.frm.doc}).then((can_cancel) => {
- if (!can_cancel) {
- this.frm.page.clear_secondary_action();
- }
- });
- } else {
- this.setup_btn(added);
- }
+ this.setup_btn(added);
});
},
diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js
index bdc7dc0827..83b5a0bdfe 100644
--- a/frappe/public/js/frappe/list/base_list.js
+++ b/frappe/public/js/frappe/list/base_list.js
@@ -693,5 +693,5 @@ class FilterArea {
}
// utility function to validate view modes
-frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox', 'Report', 'Dashboard'];
+frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Map', 'Inbox', 'Report', 'Dashboard'];
frappe.views.is_valid = view_mode => frappe.views.view_modes.includes(view_mode);
diff --git a/frappe/public/js/frappe/list/list_sidebar.html b/frappe/public/js/frappe/list/list_sidebar.html
index dcbbe7ac5e..c5b75782b5 100644
--- a/frappe/public/js/frappe/list/list_sidebar.html
+++ b/frappe/public/js/frappe/list/list_sidebar.html
@@ -30,6 +30,8 @@
{%= __("Dashboard") %}
{%= __("Images") %}
+
+ {%= __("Map") %}
{%= __("Gantt") %}
diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js
index 2a25e64bf3..5db18dd280 100644
--- a/frappe/public/js/frappe/list/list_sidebar.js
+++ b/frappe/public/js/frappe/list/list_sidebar.js
@@ -89,6 +89,14 @@ frappe.views.ListSidebar = class ListSidebar {
this.sidebar.find('.list-link[data-view="Image"]').removeClass('hide');
show_list_link = true;
}
+
+ if (this.list_view.settings.get_coords_method ||
+ (this.list_view.meta.fields.find(i => i.fieldname === "latitude") &&
+ this.list_view.meta.fields.find(i => i.fieldname === "longitude")) ||
+ (this.list_view.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype == 'Geolocation'))) {
+ this.sidebar.find('.list-link[data-view="Map"]').removeClass('hide');
+ show_list_link = true;
+ }
if (show_list_link) {
this.sidebar.find('.list-link[data-view="List"]').removeClass('hide');
@@ -209,7 +217,7 @@ frappe.views.ListSidebar = class ListSidebar {
let email_account = (account.email_id == "All Accounts") ? "All Accounts" : account.email_account;
let route = ["List", "Communication", "Inbox", email_account].join('/');
let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes(account.email_id) ? __(account.email_id) : account.email_id;
-
+
if (!divider) {
this.get_divider().appendTo($dropdown);
divider = true;
diff --git a/frappe/public/js/frappe/microtemplate.js b/frappe/public/js/frappe/microtemplate.js
index d233a47893..7b45db952e 100644
--- a/frappe/public/js/frappe/microtemplate.js
+++ b/frappe/public/js/frappe/microtemplate.js
@@ -89,11 +89,19 @@ frappe.render_template = function(name, data) {
}
frappe.render_grid = function(opts) {
// build context
- if(opts.grid) {
+ if (opts.grid) {
opts.columns = opts.grid.getColumns();
opts.data = opts.grid.getData().getItems();
}
+ if (
+ opts.print_settings &&
+ opts.print_settings.orientation &&
+ opts.print_settings.orientation.toLowerCase() === "landscape"
+ ) {
+ opts.landscape = true;
+ }
+
// show landscape view if columns more than 10
if (opts.landscape == null) {
if(opts.columns && opts.columns.length > 10) {
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index 1d302215dd..e82f64c6fc 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -135,8 +135,8 @@ $.extend(frappe.model, {
let cached_timestamp = null;
let cached_doc = null;
- let cached_docs = frappe.model.get_from_localstorage(doctype)
-
+ let cached_docs = frappe.model.get_from_localstorage(doctype);
+
if (cached_docs) {
cached_doc = cached_docs.filter(doc => doc.name === doctype)[0];
if(cached_doc) {
@@ -252,6 +252,10 @@ $.extend(frappe.model, {
return frappe.boot.user.can_create.indexOf(doctype)!==-1;
},
+ can_select: function(doctype) {
+ return frappe.boot.user.can_select.indexOf(doctype)!==-1;
+ },
+
can_read: function(doctype) {
return frappe.boot.user.can_read.indexOf(doctype)!==-1;
},
diff --git a/frappe/public/js/frappe/module_editor.js b/frappe/public/js/frappe/module_editor.js
new file mode 100644
index 0000000000..35037a3e62
--- /dev/null
+++ b/frappe/public/js/frappe/module_editor.js
@@ -0,0 +1,39 @@
+frappe.ModuleEditor = Class.extend({
+ init: function(frm, wrapper) {
+ this.wrapper = $('').appendTo(wrapper);
+ this.frm = frm;
+ this.make();
+ },
+ make: function() {
+ var me = this;
+ this.frm.doc.__onload.all_modules.forEach(function(m) {
+ $(repl('', {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});
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js
index d432e553f1..c37ea57dae 100644
--- a/frappe/public/js/frappe/ui/field_group.js
+++ b/frappe/public/js/frappe/ui/field_group.js
@@ -86,7 +86,10 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({
var f = this.fields_dict[key];
if (f.get_value) {
var v = f.get_value();
- if (f.df.reqd && is_null(v))
+ if (
+ f.df.reqd &&
+ is_null(typeof v === 'string' ? strip_html(v) : v)
+ )
errors.push(__(f.df.label));
if (f.df.reqd
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js
index da19ce7eb0..4a047c76ae 100644
--- a/frappe/public/js/frappe/ui/filters/filter.js
+++ b/frappe/public/js/frappe/ui/filters/filter.js
@@ -518,7 +518,7 @@ frappe.ui.filter_utils = {
['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)
) {
df.fieldtype = 'Select';
- df.options = this.get_timespan_options(['Last', 'Today', 'This', 'Next']);
+ df.options = this.get_timespan_options(['Last', 'Yesterday', 'Today', 'Tomorrow', 'This', 'Next']);
}
if (condition === 'is') {
df.fieldtype = 'Select';
@@ -533,7 +533,6 @@ frappe.ui.filter_utils = {
get_timespan_options(periods) {
const period_map = {
Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
- Today: null,
This: ['Week', 'Month', 'Quarter', 'Year'],
Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
};
diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js
index 0a145b098b..20eb4393a3 100644
--- a/frappe/public/js/frappe/utils/common.js
+++ b/frappe/public/js/frappe/utils/common.js
@@ -108,7 +108,7 @@ window.replace_all = function(s, t1, t2) {
}
window.strip_html = function(txt) {
- return txt.replace(/<[^>]*>/g, "");
+ return cstr(txt).replace(/<[^>]*>/g, "");
}
window.strip = function(s, chars) {
diff --git a/frappe/public/js/frappe/utils/user.js b/frappe/public/js/frappe/utils/user.js
index 311f208750..64db23d306 100644
--- a/frappe/public/js/frappe/utils/user.js
+++ b/frappe/public/js/frappe/utils/user.js
@@ -55,7 +55,7 @@ $.extend(frappe.user, {
name: 'Guest',
full_name: function(uid) {
return uid === frappe.session.user ?
- __("You") :
+ __("You", null, "Name of the current user. For example: You edited this 5 hours ago.") :
frappe.user_info(uid).fullname;
},
image: function(uid) {
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index f8f25293b3..32be29df92 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -1051,6 +1051,14 @@ Object.assign(frappe.utils, {
return number_system_map[country];
},
+ map_defaults: {
+ center: [19.0800, 72.8961],
+ zoom: 13,
+ tiles: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
+ options: {
+ attribution: '© OpenStreetMap contributors'
+ }
+ },
});
// Array de duplicate
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index 29b21242af..0389770783 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -55,38 +55,85 @@ frappe.views.CommunicationComposer = Class.extend({
get_fields: function() {
let contactList = [];
var fields= [
- {label:__("To"), fieldtype:"MultiSelect", reqd: 0, fieldname:"recipients",options:contactList},
- {fieldtype: "Section Break", collapsible: 1, label: __("CC, BCC & Email Template")},
- {label:__("CC"), fieldtype:"MultiSelect", fieldname:"cc",options:contactList},
- {label:__("BCC"), fieldtype:"MultiSelect", fieldname:"bcc",options:contactList},
- {label:__("Email Template"), fieldtype:"Link", options:"Email Template",
- fieldname:"email_template"},
- {fieldtype: "Section Break"},
- {label:__("Subject"), fieldtype:"Data", reqd: 1,
- fieldname:"subject", length:524288},
- {fieldtype: "Section Break"},
{
- label:__("Message"),
- fieldtype:"Text Editor", reqd: 1,
- fieldname:"content",
+ label: __("To"),
+ fieldtype: "MultiSelect",
+ reqd: 0,
+ fieldname: "recipients",
+ options: contactList
+ },
+ {
+ fieldtype: "Section Break",
+ collapsible: 1,
+ label: __("CC, BCC & Email Template")
+ },
+ {
+ label: __("CC"),
+ fieldtype: "MultiSelect",
+ fieldname: "cc",
+ options: contactList
+ },
+ {
+ label: __("BCC"),
+ fieldtype: "MultiSelect",
+ fieldname: "bcc",
+ options: contactList
+ },
+ {
+ label: __("Email Template"),
+ fieldtype: "Link",
+ options: "Email Template",
+ fieldname: "email_template"
+ },
+ { fieldtype: "Section Break" },
+ {
+ label: __("Subject"),
+ fieldtype: "Data",
+ reqd: 1,
+ fieldname: "subject",
+ length: 524288
+ },
+ { fieldtype: "Section Break" },
+ {
+ label: __("Message"),
+ fieldtype: "Text Editor",
+ fieldname: "content",
onchange: frappe.utils.debounce(this.save_as_draft.bind(this), 300)
},
-
- {fieldtype: "Section Break"},
- {fieldtype: "Column Break"},
- {label:__("Send me a copy"), fieldtype:"Check",
- fieldname:"send_me_a_copy", 'default': frappe.boot.user.send_me_a_copy},
- {label:__("Send Read Receipt"), fieldtype:"Check",
- fieldname:"send_read_receipt"},
- {label:__("Attach Document Print"), fieldtype:"Check",
- fieldname:"attach_document_print"},
- {label:__("Select Print Format"), fieldtype:"Select",
- fieldname:"select_print_format"},
- {label:__("Select Languages"), fieldtype:"Select",
- fieldname:"language_sel"},
- {fieldtype: "Column Break"},
- {label:__("Select Attachments"), fieldtype:"HTML",
- fieldname:"select_attachments"}
+ { fieldtype: "Section Break" },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Send me a copy"),
+ fieldtype: "Check",
+ fieldname: "send_me_a_copy",
+ 'default': frappe.boot.user.send_me_a_copy
+ },
+ {
+ label: __("Send Read Receipt"),
+ fieldtype: "Check",
+ fieldname: "send_read_receipt"
+ },
+ {
+ label: __("Attach Document Print"),
+ fieldtype: "Check",
+ fieldname: "attach_document_print"
+ },
+ {
+ label: __("Select Print Format"),
+ fieldtype: "Select",
+ fieldname: "select_print_format"
+ },
+ {
+ label: __("Select Languages"),
+ fieldtype: "Select",
+ fieldname: "language_sel"
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Select Attachments"),
+ fieldtype: "HTML",
+ fieldname: "select_attachments"
+ }
];
// add from if user has access to multiple email accounts
@@ -625,10 +672,19 @@ frappe.views.CommunicationComposer = Class.extend({
}
},
- setup_earlier_reply: function() {
+ get_default_outgoing_email_account_signature: function() {
+ return frappe.db.get_value('Email Account', { 'default_outgoing': 1, 'add_signature': 1 }, 'signature');
+ },
+
+ setup_earlier_reply: async function() {
let fields = this.dialog.fields_dict;
let signature = frappe.boot.user.email_signature || "";
+ if (!signature) {
+ const res = await this.get_default_outgoing_email_account_signature();
+ signature = res.message.signature;
+ }
+
if(!frappe.utils.is_html(signature)) {
signature = signature.replace(/\n/g, "
");
}
@@ -709,4 +765,3 @@ frappe.views.CommunicationComposer = Class.extend({
return text.replace(/\n{3,}/g, '\n\n');
}
});
-
diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js
new file mode 100644
index 0000000000..878311b9bd
--- /dev/null
+++ b/frappe/public/js/frappe/views/map/map_view.js
@@ -0,0 +1,85 @@
+/**
+ * frappe.views.MapView
+ */
+frappe.provide('frappe.utils.utils');
+frappe.provide("frappe.views");
+
+frappe.views.MapView = class MapView extends frappe.views.ListView {
+ get view_name() {
+ return 'Map';
+ }
+
+ setup_defaults() {
+ super.setup_defaults();
+ this.page_title = __('{0} Map', [this.page_title]);
+ }
+
+ setup_view() {
+ }
+
+ on_filter_change() {
+ this.get_coords();
+ }
+
+ render() {
+ this.get_coords()
+ .then(() => {
+ this.render_map_view();
+ });
+ this.$paging_area.find('.level-left').append('');
+ }
+
+ render_map_view() {
+ this.map_id = frappe.dom.get_unique_id();
+
+ this.$result.html(``);
+
+ L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/';
+ this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center,
+ frappe.utils.map_defaults.zoom);
+
+ L.tileLayer(frappe.utils.map_defaults.tiles,
+ frappe.utils.map_defaults.options).addTo(this.map);
+
+ L.control.scale().addTo(this.map);
+ if (this.coords.features && this.coords.features.length) {
+ this.coords.features.forEach(
+ coords => L.geoJSON(coords).bindPopup(coords.properties.name).addTo(this.map)
+ );
+ let lastCoords = this.coords.features[0].geometry.coordinates.reverse();
+ this.map.panTo(lastCoords, 8);
+ }
+ }
+
+ get_coords() {
+ let get_coords_method = this.settings && this.settings.get_coords_method || 'frappe.geo.utils.get_coords';
+
+ if (cur_list.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype === 'Geolocation')) {
+ this.type = 'location_field';
+ } else if (cur_list.meta.fields.find(i => i.fieldname === "latitude") &&
+ cur_list.meta.fields.find(i => i.fieldname === "longitude")) {
+ this.type = 'coordinates';
+ }
+ return frappe.call({
+ method: get_coords_method,
+ args: {
+ doctype: this.doctype,
+ filters: cur_list.filter_area.get(),
+ type: this.type
+ }
+ }).then(r => {
+ this.coords = r.message;
+
+ });
+ }
+
+
+ get required_libs() {
+ return [
+ "assets/frappe/js/lib/leaflet/leaflet.css",
+ "assets/frappe/js/lib/leaflet/leaflet.js"
+ ];
+ }
+
+
+};
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index 60abb187ae..eccfa9c089 100644
--- a/frappe/public/js/frappe/views/reports/query_report.js
+++ b/frappe/public/js/frappe/views/reports/query_report.js
@@ -1112,7 +1112,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}
get_filter_values(raise) {
- const mandatory = this.filters.filter(f => f.df.reqd);
+
+ // check for mandatory property for filters added via UI
+ const mandatory = this.filters.filter(f => (f.df.reqd || f.df.mandatory));
const missing_mandatory = mandatory.filter(f => !f.get_value());
if (raise && missing_mandatory.length > 0) {
let message = __('Please set filters');
diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js
index 026e120c50..13c07a21e7 100644
--- a/frappe/public/js/frappe/views/reports/report_view.js
+++ b/frappe/public/js/frappe/views/reports/report_view.js
@@ -708,6 +708,32 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
super.build_fields();
}
+ reorder_fields() {
+ // generate table fields in the required format ["name", "DocType"]
+ // these are fields in the column before adding new fields
+ let table_fields = this.columns.map(df => [df.field, df.docfield.parent]);
+
+ // filter fields that are already in table
+ // iterate over table_fields to preserve the existing order of fields
+ // The filter will ensure the unchecked fields are removed
+ let fields_already_in_table = table_fields.filter(df => {
+ return this.fields.find((field) => {
+ return df[0] == field[0] && df[1] == field[1]
+ })
+ })
+
+ // find new fields that didn't already exists
+ // This will be appended to the end of the table
+ let fields_to_add = this.fields.filter(df => {
+ return !table_fields.find((field) => {
+ return df[0] == field[0] && df[1] == field[1]
+ })
+ })
+
+ // rebuild fields
+ this.fields = [...fields_already_in_table, ...fields_to_add];
+ }
+
get_fields() {
let fields = this.fields.map(f => {
let column_name = frappe.model.get_full_column_name(f[0], f[1]);
@@ -1329,6 +1355,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
this.fields.map(f => this.add_currency_column(f[0], f[1]));
+ this.reorder_fields();
this.build_fields();
this.setup_columns();
diff --git a/frappe/public/js/frappe/views/treeview.js b/frappe/public/js/frappe/views/treeview.js
index 777ce14da6..1a53c14974 100644
--- a/frappe/public/js/frappe/views/treeview.js
+++ b/frappe/public/js/frappe/views/treeview.js
@@ -93,17 +93,17 @@ frappe.views.TreeView = Class.extend({
var me = this;
this.opts.onload && this.opts.onload(me);
},
- make_filters: function(){
+ make_filters: function() {
var me = this;
frappe.treeview_settings.filters = []
$.each(this.opts.filters || [], function(i, filter) {
- if(frappe.route_options && frappe.route_options[filter.fieldname]) {
- filter.default = frappe.route_options[filter.fieldname]
+ if (frappe.route_options && frappe.route_options[filter.fieldname]) {
+ filter.default = frappe.route_options[filter.fieldname];
}
- if(!filter.disable_onchange) {
+ if (!filter.disable_onchange) {
filter.change = function() {
- filter.on_change && filter.on_change();
+ filter.onchange && filter.onchange();
var val = this.get_value();
me.args[filter.fieldname] = val;
if (val) {
@@ -113,7 +113,7 @@ frappe.views.TreeView = Class.extend({
}
me.set_title();
me.make_tree();
- }
+ };
}
me.page.add_field(filter);
@@ -121,7 +121,7 @@ frappe.views.TreeView = Class.extend({
if (filter.default) {
$("[data-fieldname='"+filter.fieldname+"']").trigger("change");
}
- })
+ });
},
get_root: function() {
var me = this;
diff --git a/frappe/public/js/frappe/web_form/webform_script.js b/frappe/public/js/frappe/web_form/webform_script.js
index c3211de99f..6df526e7ac 100644
--- a/frappe/public/js/frappe/web_form/webform_script.js
+++ b/frappe/public/js/frappe/web_form/webform_script.js
@@ -85,6 +85,7 @@ frappe.ready(function() {
function setup_fields(form_data) {
form_data.web_form.web_form_fields.map(df => {
+ df.is_web_form = true;
if (df.fieldtype === "Table") {
df.get_data = () => {
let data = [];
@@ -99,14 +100,13 @@ frappe.ready(function() {
if (field.fieldtype === "Link") {
field.only_select = true;
}
+ field.is_web_form = true;
});
if (df.fieldtype === "Attach") {
df.is_private = true;
}
- df.is_web_form = true;
-
delete df.parent;
delete df.parentfield;
delete df.parenttype;
diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js
index c41f9bc6e7..ccc9ea7bfd 100644
--- a/frappe/public/js/frappe/widgets/number_card_widget.js
+++ b/frappe/public/js/frappe/widgets/number_card_widget.js
@@ -172,7 +172,7 @@ export default class NumberCardWidget extends Widget {
get_number_for_custom_card(res) {
if (typeof res === 'object') {
this.number = res.value;
- this.get_formatted_number(res);
+ this.set_formatted_number(res);
} else {
this.formatted_number = res;
}
@@ -184,7 +184,7 @@ export default class NumberCardWidget extends Widget {
return frappe.model.with_doctype(this.card_doc.document_type, () => {
const based_on_df =
frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on);
- this.get_formatted_number(based_on_df);
+ this.set_formatted_number(based_on_df);
});
} else {
this.formatted_number = res;
@@ -199,10 +199,10 @@ export default class NumberCardWidget extends Widget {
}, []);
const col = res.columns.find(col => col.fieldname == field);
this.number = frappe.report_utils.get_result_of_fn(this.card_doc.report_function, vals);
- this.get_formatted_number(col);
+ this.set_formatted_number(col);
}
- get_formatted_number(df) {
+ set_formatted_number(df) {
const default_country = frappe.sys_defaults.country;
const shortened_number = frappe.utils.shorten_number(this.number, default_country, 5);
let number_parts = shortened_number.split(' ');
@@ -250,10 +250,16 @@ export default class NumberCardWidget extends Widget {
};
const stats_qualifier = stats_qualifier_map[this.card_doc.stats_time_interval];
+ let get_stat = () => {
+ const parts = this.percentage_stat.split(' ');
+ const symbol = parts[1] || '';
+ return Math.abs(parts[0]) + ' ' + symbol;
+ };
+
$(this.body).find('.widget-content').append(`
${caret_html}
- ${Math.abs(this.percentage_stat)} %
+ ${get_stat()} %
${stats_qualifier}
diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js
index abacc6f354..8ef003cc67 100644
--- a/frappe/public/js/frappe/widgets/onboarding_widget.js
+++ b/frappe/public/js/frappe/widgets/onboarding_widget.js
@@ -444,7 +444,7 @@ export default class OnboardingWidget extends Widget {
set_actions() {
this.action_area.empty();
const dismiss = $(
- `${__('Dismiss')}
`
+ `${__('Dismiss', null, 'Stop showing the onboarding widget.')}
`
);
dismiss.on("click", () => {
let dismissed = JSON.parse(
diff --git a/frappe/public/js/lib/highlight.pack.js b/frappe/public/js/lib/highlight.pack.js
deleted file mode 100755
index ecee8ad109..0000000000
--- a/frappe/public/js/lib/highlight.pack.js
+++ /dev/null
@@ -1 +0,0 @@
-var hljs=new function(){function e(e){return e.replace(/&/gm,"&").replace(//gm,">")}function t(e){return e.nodeName.toLowerCase()}function n(e,t){var n=e&&e.exec(t);return n&&0==n.index}function r(e){var t=(e.className+" "+(e.parentNode?e.parentNode.className:"")).split(/\s+/);return t=t.map(function(e){return e.replace(/^lang(uage)?-/,"")}),t.filter(function(e){return m(e)||/no(-?)highlight/.test(e)})[0]}function i(e,t){var n={};for(var r in e)n[r]=e[r];if(t)for(var r in t)n[r]=t[r];return n}function a(e){var n=[];return function r(e,i){for(var a=e.firstChild;a;a=a.nextSibling)3==a.nodeType?i+=a.nodeValue.length:1==a.nodeType&&(n.push({event:"start",offset:i,node:a}),i=r(a,i),t(a).match(/br|hr|img|input/)||n.push({event:"stop",offset:i,node:a}));return i}(e,0),n}function s(n,r,i){function a(){return n.length&&r.length?n[0].offset!=r[0].offset?n[0].offset"}function o(e){l+=""+t(e)+">"}function c(e){("start"==e.event?s:o)(e.node)}for(var u=0,l="",f=[];n.length||r.length;){var h=a();if(l+=e(i.substr(u,h[0].offset-u)),u=h[0].offset,h==n){f.reverse().forEach(o);do c(h.splice(0,1)[0]),h=a();while(h==n&&h.length&&h[0].offset==u);f.reverse().forEach(s)}else"start"==h[0].event?f.push(h[0].node):f.pop(),c(h.splice(0,1)[0])}return l+e(i.substr(u))}function o(e){function t(e){return e&&e.source||e}function n(n,r){return RegExp(t(n),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,s){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},c=function(t,n){e.cI&&(n=n.toLowerCase()),n.split(" ").forEach(function(e){var n=e.split("|");o[n[0]]=[t,n[1]?Number(n[1]):1]})};"string"==typeof a.k?c("keyword",a.k):Object.keys(a.k).forEach(function(e){c(e,a.k[e])}),a.k=o}a.lR=n(a.l||/\b[A-Za-z0-9_]+\b/,!0),s&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=n(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=n(a.e)),a.tE=t(a.e)||"",a.eW&&s.tE&&(a.tE+=(a.e?"|":"")+s.tE)),a.i&&(a.iR=n(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var u=[];a.c.forEach(function(e){e.v?e.v.forEach(function(t){u.push(i(e,t))}):u.push("self"==e?a:e)}),a.c=u,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,s);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(t).filter(Boolean);a.t=l.length?n(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function c(t,r,i,a){function s(e,t){for(var r=0;r";return a+=e+'">',a+t+s}function p(){if(!w.k)return e(B);var t="",n=0;w.lR.lastIndex=0;for(var r=w.lR.exec(B);r;){t+=e(B.substr(n,r.index-n));var i=h(w,r);i?(y+=i[1],t+=g(i[0],e(r[0]))):t+=e(r[0]),n=w.lR.lastIndex,r=w.lR.exec(B)}return t+e(B.substr(n))}function v(){if(w.sL&&!E[w.sL])return e(B);var t=w.sL?c(w.sL,B,!0,L[w.sL]):u(B);return w.r>0&&(y+=t.r),"continuous"==w.subLanguageMode&&(L[w.sL]=t.top),g(t.language,t.value,!1,!0)}function b(){return void 0!==w.sL?v():p()}function d(t,n){var r=t.cN?g(t.cN,"",!0):"";t.rB?(M+=r,B=""):t.eB?(M+=e(n)+r,B=""):(M+=r,B=n),w=Object.create(t,{parent:{value:w}})}function R(t,n){if(B+=t,void 0===n)return M+=b(),0;var r=s(n,w);if(r)return M+=b(),d(r,n),r.rB?0:n.length;var i=l(w,n);if(i){var a=w;a.rE||a.eE||(B+=n),M+=b();do w.cN&&(M+=""),y+=w.r,w=w.parent;while(w!=i.parent);return a.eE&&(M+=e(n)),B="",i.starts&&d(i.starts,""),a.rE?0:n.length}if(f(n,w))throw new Error('Illegal lexeme "'+n+'" for mode "'+(w.cN||"
")+'"');return B+=n,n.length||1}var x=m(t);if(!x)throw new Error('Unknown language: "'+t+'"');o(x);for(var w=a||x,L={},M="",k=w;k!=x;k=k.parent)k.cN&&(M=g(k.cN,"",!0)+M);var B="",y=0;try{for(var C,I,j=0;;){if(w.t.lastIndex=j,C=w.t.exec(r),!C)break;I=R(r.substr(j,C.index-j),C[0]),j=C.index+I}R(r.substr(j));for(var k=w;k.parent;k=k.parent)k.cN&&(M+="");return{r:y,value:M,language:t,top:w}}catch(A){if(-1!=A.message.indexOf("Illegal"))return{r:0,value:e(r)};throw A}}function u(t,n){n=n||N.languages||Object.keys(E);var r={r:0,value:e(t)},i=r;return n.forEach(function(e){if(m(e)){var n=c(e,t,!1);n.language=e,n.r>i.r&&(i=n),n.r>r.r&&(i=r,r=n)}}),i.language&&(r.second_best=i),r}function l(e){return N.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,t){return t.replace(/\t/g,N.tabReplace)})),N.useBR&&(e=e.replace(/\n/g,"
")),e}function f(e,t,n){var r=t?R[t]:n,i=[e.trim()];return e.match(/(\s|^)hljs(\s|$)/)||i.push("hljs"),r&&i.push(r),i.join(" ").trim()}function h(e){var t=r(e);if(!/no(-?)highlight/.test(t)){var n;N.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(/
/g,"\n")):n=e;var i=n.textContent,o=t?c(t,i,!0):u(i),h=a(n);if(h.length){var g=document.createElementNS("http://www.w3.org/1999/xhtml","div");g.innerHTML=o.value,o.value=s(h,a(g),i)}o.value=l(o.value),e.innerHTML=o.value,e.className=f(e.className,t,o.language),e.result={language:o.language,re:o.r},o.second_best&&(e.second_best={language:o.second_best.language,re:o.second_best.r})}}function g(e){N=i(N,e)}function p(){if(!p.called){p.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,h)}}function v(){addEventListener("DOMContentLoaded",p,!1),addEventListener("load",p,!1)}function b(e,t){var n=E[e]=t(this);n.aliases&&n.aliases.forEach(function(t){R[t]=e})}function d(){return Object.keys(E)}function m(e){return E[e]||E[R[e]]}var N={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},E={},R={};this.highlight=c,this.highlightAuto=u,this.fixMarkup=l,this.highlightBlock=h,this.configure=g,this.initHighlighting=p,this.initHighlightingOnLoad=v,this.registerLanguage=b,this.listLanguages=d,this.getLanguage=m,this.inherit=i,this.IR="[a-zA-Z][a-zA-Z0-9_]*",this.UIR="[a-zA-Z_][a-zA-Z0-9_]*",this.NR="\\b\\d+(\\.\\d+)?",this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",this.BNR="\\b(0b[01]+)",this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",this.BE={b:"\\\\[\\s\\S]",r:0},this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE]},this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE]},this.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such)\b/},this.CLCM={cN:"comment",b:"//",e:"$",c:[this.PWM]},this.CBCM={cN:"comment",b:"/\\*",e:"\\*/",c:[this.PWM]},this.HCM={cN:"comment",b:"#",e:"$",c:[this.PWM]},this.NM={cN:"number",b:this.NR,r:0},this.CNM={cN:"number",b:this.CNR,r:0},this.BNM={cN:"number",b:this.BNR,r:0},this.CSSNM={cN:"number",b:this.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},this.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[this.BE,{b:/\[/,e:/\]/,r:0,c:[this.BE]}]},this.TM={cN:"title",b:this.IR,r:0},this.UTM={cN:"title",b:this.UIR,r:0}};hljs.registerLanguage("markdown",function(){return{aliases:["md","mkdown","mkd"],c:[{cN:"header",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"blockquote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{cN:"horizontal_rule",b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"link_label",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link_url",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"link_reference",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:"^\\[.+\\]:",rB:!0,c:[{cN:"link_reference",b:"\\[",e:"\\]:",eB:!0,eE:!0,starts:{cN:"link_url",e:"$"}}]}]}});hljs.registerLanguage("javascript",function(r){return{aliases:["js"],k:{keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document"},c:[{cN:"pi",b:/^\s*('|")use strict('|")/,r:10},r.ASM,r.QSM,r.CLCM,r.CBCM,r.CNM,{b:"("+r.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[r.CLCM,r.CBCM,r.RM,{b:/,e:/>;/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[r.inherit(r.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,c:[r.CLCM,r.CBCM],i:/["'\(]/}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+r.IR,r:0}]}});hljs.registerLanguage("json",function(e){var t={literal:"true false null"},i=[e.QSM,e.CNM],l={cN:"value",e:",",eW:!0,eE:!0,c:i,k:t},c={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:!0,eE:!0,c:[e.BE],i:"\\n",starts:l}],i:"\\S"},n={b:"\\[",e:"\\]",c:[e.inherit(l,{cN:null})],i:"\\S"};return i.splice(i.length,0,c,n),{c:i,k:t,i:"\\S"}});hljs.registerLanguage("python",function(e){var r={cN:"prompt",b:/^(>>>|\.\.\.) /},b={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[r],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[r],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},e.ASM,e.QSM]},i={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},l={cN:"params",b:/\(/,e:/\)/,c:["self",r,i,b]},n={e:/:/,i:/[${=;\n]/,c:[e.UTM,l]};return{aliases:["py","gyp"],k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)/,c:[r,i,b,e.HCM,e.inherit(n,{cN:"function",bK:"def",r:10}),e.inherit(n,{cN:"class",bK:"class"}),{cN:"decorator",b:/@/,e:/$/},{b:/\b(print|exec)\(/}]}});hljs.registerLanguage("xml",function(){var t="[A-Za-z0-9\\._:-]+",e={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php",subLanguageMode:"continuous"},c={eW:!0,i:/,r:0,c:[e,{cN:"attribute",b:t,r:0},{b:"=",r:0,c:[{cN:"value",c:[e],v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s\/>]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xsl","plist"],cI:!0,c:[{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"",rE:!0,sL:"css"}},{cN:"tag",b:"",rE:!0,sL:"javascript"}},e,{cN:"pi",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"?",e:"/?>",c:[{cN:"title",b:/[^ \/><\n\t]+/,r:0},c]}]}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",a={cN:"function",b:c+"\\(",rB:!0,eE:!0,e:"\\("};return{cI:!0,i:"[=/|']",c:[e.CBCM,{cN:"id",b:"\\#[A-Za-z0-9_-]+"},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"pseudo",b:":(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\\\"\\']+"},{cN:"at_rule",b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{cN:"at_rule",b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[a,e.ASM,e.QSM,e.CSSNM]}]},{cN:"tag",b:c,r:0},{cN:"rules",b:"{",e:"}",i:"[^\\s]",r:0,c:[e.CBCM,{cN:"rule",b:"[^\\s]",rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:!0,i:"[^\\s]",starts:{cN:"value",eW:!0,eE:!0,c:[a,e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"hexcolor",b:"#[0-9A-Fa-f]+"},{cN:"important",b:"!important"}]}}]}]}]}});
\ No newline at end of file
diff --git a/frappe/public/less/list.less b/frappe/public/less/list.less
index 7e57d23fdc..fe2e1cf48d 100644
--- a/frappe/public/less/list.less
+++ b/frappe/public/less/list.less
@@ -483,6 +483,15 @@ input.list-check-all, input.list-row-checkbox {
padding-top: 2px;
}
+// map
+.map-view-container {
+ display: flex;
+ flex-wrap: wrap;
+ width: 100%;
+ height: calc(100vh - 284px);
+ z-index: 0;
+}
+
// list view
.modal-body {
diff --git a/frappe/public/scss/page-builder.scss b/frappe/public/scss/page-builder.scss
index 24dbca3e21..1803e52cf7 100644
--- a/frappe/public/scss/page-builder.scss
+++ b/frappe/public/scss/page-builder.scss
@@ -29,11 +29,11 @@
}
.hero.align-center {
- h1, .hero-subtitle, .hero-buttons {
+ h1, .hero-title, .hero-subtitle, .hero-buttons {
text-align: center;
}
- .hero-subtitle {
+ .hero-title, .hero-subtitle {
margin-left: auto;
margin-right: auto;
}
diff --git a/frappe/templates/includes/breadcrumbs.html b/frappe/templates/includes/breadcrumbs.html
index e281c4b111..ccc77de253 100644
--- a/frappe/templates/includes/breadcrumbs.html
+++ b/frappe/templates/includes/breadcrumbs.html
@@ -3,6 +3,7 @@
diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html
index 168547798b..7a0dce7f5e 100644
--- a/frappe/templates/print_formats/standard_macros.html
+++ b/frappe/templates/print_formats/standard_macros.html
@@ -5,8 +5,11 @@
{{ frappe.render_template(df.options, {"doc": doc}) or "" }}
{%- elif df.fieldtype in ("Text", "Text Editor", "Code", "Long Text") -%}
{{ render_text_field(df, doc) }}
- {%- elif df.fieldtype in ("Image", "Attach Image", "Attach")
- and (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") -%}
+ {%- elif df.fieldtype in ("Image", "Attach Image")
+ and (
+ (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/")
+ or doc[df.fieldname].startswith("http")
+ ) -%}
{{ render_image(df, doc) }}
{%- elif df.fieldtype=="Geolocation" -%}
{{ render_geolocation(df, doc) }}
@@ -123,19 +126,20 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{% include doc.print_templates[df.fieldname] %}
{% elif df.fieldtype=="Check" %}
- {% elif df.fieldtype=="Image" %}
+ {% elif df.fieldtype in ("Image", "Attach Image") %}
{% elif df.fieldtype=="Signature" %}
- {% elif df.fieldtype in ("Attach", "Attach Image") and doc[df.fieldname]
- and frappe.utils.is_image(doc[df.fieldname]) %}
+ {% elif df.fieldtype == "Attach" and doc[df.fieldname] and frappe.utils.is_image(doc[df.fieldname]) %}
{% elif df.fieldtype=="HTML" %}
{{ frappe.render_template(df.options, {"doc":doc}) }}
+ {% elif df.fieldtype=="Currency" %}
+ {{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }}
{% else %}
{%- set parent = parent_doc or doc -%}
{{ doc.get_formatted(df.fieldname, parent, translated=df.translatable, absolute_value=parent.absolute_value) }}
diff --git a/frappe/tests/test_cors.py b/frappe/tests/test_cors.py
new file mode 100644
index 0000000000..d4ed260f61
--- /dev/null
+++ b/frappe/tests/test_cors.py
@@ -0,0 +1,57 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+from __future__ import unicode_literals
+
+import frappe, unittest
+from werkzeug.wrappers import Response
+from frappe.app import process_response
+
+HEADERS = ('Access-Control-Allow-Origin', 'Access-Control-Allow-Credentials',
+ 'Access-Control-Allow-Methods', 'Access-Control-Allow-Headers')
+
+class TestCORS(unittest.TestCase):
+ def make_request_and_test(self, origin='http://example.com', absent=False):
+ self.origin = origin
+
+ headers = {}
+ if origin:
+ headers = {'Origin': origin}
+
+ frappe.utils.set_request(headers=headers)
+
+ self.response = Response()
+ process_response(self.response)
+
+ for header in HEADERS:
+ if absent:
+ self.assertNotIn(header, self.response.headers)
+ else:
+ if header == 'Access-Control-Allow-Origin':
+ self.assertEqual(self.response.headers.get(header), self.origin)
+ else:
+ self.assertIn(header, self.response.headers)
+
+ def test_cors_disabled(self):
+ frappe.conf.allow_cors = None
+ self.make_request_and_test('http://example.com', True)
+
+ def test_request_without_origin(self):
+ frappe.conf.allow_cors = 'http://example.com'
+ self.make_request_and_test(None, True)
+
+ def test_valid_origin(self):
+ frappe.conf.allow_cors = 'http://example.com'
+ self.make_request_and_test()
+
+ frappe.conf.allow_cors = "*"
+ self.make_request_and_test()
+
+ frappe.conf.allow_cors = ['http://example.com', 'https://example.com']
+ self.make_request_and_test()
+
+ def test_invalid_origin(self):
+ frappe.conf.allow_cors = 'http://example1.com'
+ self.make_request_and_test(absent=True)
+
+ frappe.conf.allow_cors = ['http://example1.com', 'https://example.com']
+ self.make_request_and_test(absent=True)
diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py
index 4d2bef9479..836bb4bbf5 100644
--- a/frappe/tests/test_db_query.py
+++ b/frappe/tests/test_db_query.py
@@ -7,9 +7,14 @@ import frappe, unittest
from frappe.model.db_query import DatabaseQuery
from frappe.desk.reportview import get_filters_cond
+from frappe.core.page.permission_manager.permission_manager import update, reset, add
from frappe.permissions import add_user_permission, clear_user_permissions_for_doctype
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter
+from frappe.handler import execute_cmd
-test_dependencies = ['User', 'Blog Post']
+from frappe.utils.testutils import add_custom_field, clear_custom_fields
+
+test_dependencies = ['User', 'Blog Post', 'Blog Category', 'Blogger']
class TestReportview(unittest.TestCase):
def test_basic(self):
@@ -355,6 +360,79 @@ class TestReportview(unittest.TestCase):
owners = DatabaseQuery("DocType").execute(filters={"name": "DocType"}, pluck="owner")
self.assertEqual(owners, ["Administrator"])
+ def test_reportview_get(self):
+ user = frappe.get_doc("User", "test@example.com")
+ add_child_table_to_blog_post()
+
+ user_roles = frappe.get_roles()
+ user.remove_roles(*user_roles)
+ user.add_roles("Blogger")
+
+ make_property_setter("Blog Post", "published", "permlevel", 1, "Int")
+ reset("Blog Post")
+ add("Blog Post", "Website Manager", 1)
+ update("Blog Post", "Website Manager", 1, "write", 1)
+
+ frappe.set_user(user.name)
+
+ frappe.local.request = frappe._dict()
+ frappe.local.request.method = "POST"
+
+ frappe.local.form_dict = frappe._dict({
+ "doctype": "Blog Post",
+ "fields": ["published", "title", "`tabTest Child`.`test_field`"],
+ })
+
+ # even if * is passed, fields which are not accessible should be filtered out
+ response = execute_cmd("frappe.desk.reportview.get")
+ self.assertListEqual(response["keys"], ["title"])
+ frappe.local.form_dict = frappe._dict({
+ "doctype": "Blog Post",
+ "fields": ["*"],
+ })
+
+ response = execute_cmd("frappe.desk.reportview.get")
+ self.assertNotIn("published", response["keys"])
+
+ frappe.set_user("Administrator")
+ user.add_roles("Website Manager")
+ frappe.set_user(user.name)
+
+ frappe.set_user("Administrator")
+
+ # Admin should be able to see access all fields
+ frappe.local.form_dict = frappe._dict({
+ "doctype": "Blog Post",
+ "fields": ["published", "title", "`tabTest Child`.`test_field`"],
+ })
+
+ response = execute_cmd("frappe.desk.reportview.get")
+ self.assertListEqual(response["keys"], ['published', 'title', 'test_field'])
+
+ # reset user roles
+ user.remove_roles("Blogger", "Website Manager")
+ user.add_roles(*user_roles)
+
+
+def add_child_table_to_blog_post():
+ child_table = frappe.get_doc({
+ 'doctype': 'DocType',
+ 'istable': 1,
+ 'custom': 1,
+ 'name': 'Test Child',
+ 'module': 'Custom',
+ 'autoname': 'Prompt',
+ 'fields': [{
+ 'fieldname': 'test_field',
+ 'fieldtype': 'Data',
+ 'permlevel': 1
+ }],
+ })
+
+ child_table.insert(ignore_permissions=True, ignore_if_duplicate=True)
+ clear_custom_fields('Blog Post')
+ add_custom_field('Blog Post', 'child_table', 'Table', child_table.name)
+
def create_event(subject="_Test Event", starts_on=None):
""" create a test event """
diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py
index 4f595c9419..2be92be1f5 100644
--- a/frappe/tests/test_document.py
+++ b/frappe/tests/test_document.py
@@ -249,82 +249,6 @@ class TestDocument(unittest.TestCase):
self.assertEqual(cint(old_current) - 1, new_current)
- def test_rename_doc(self):
- from random import choice, sample
-
- available_documents = []
- doctype = "ToDo"
-
- # data generation: 4 todo documents
- for num in range(1, 5):
- doc = frappe.get_doc({
- "doctype": doctype,
- "date": add_to_date(now(), days=num),
- "description": "this is todo #{}".format(num)
- }).insert()
- available_documents.append(doc.name)
-
- # test 1: document renaming
- old_name = choice(available_documents)
- new_name = old_name + '.new'
- self.assertEqual(new_name, frappe.rename_doc(doctype, old_name, new_name, force=True))
- available_documents.remove(old_name)
- available_documents.append(new_name)
-
- # test 2: merge documents
- first_todo, second_todo = sample(available_documents, 2)
-
- second_todo_doc = frappe.get_doc(doctype, second_todo)
- second_todo_doc.priority = "High"
- second_todo_doc.save()
-
- merged_todo = frappe.rename_doc(doctype, first_todo, second_todo, merge=True, force=True)
- merged_todo_doc = frappe.get_doc(doctype, merged_todo)
- available_documents.remove(first_todo)
-
- with self.assertRaises(DoesNotExistError):
- frappe.get_doc(doctype, first_todo)
-
- self.assertEqual(merged_todo_doc.priority, second_todo_doc.priority)
-
- for docname in available_documents:
- frappe.delete_doc(doctype, docname)
-
- def test_rename_doctype(self):
- from frappe.core.doctype.doctype.test_doctype import new_doctype
-
- fields =[{
- "label": "Linked To",
- "fieldname": "linked_to_doctype",
- "fieldtype": "Link",
- "options": "DocType",
- "unique": 0
- }]
- if not frappe.db.exists("DocType", "Rename This"):
- new_doctype("Rename This", unique=0, fields=fields).insert()
-
- to_rename_record = frappe.get_doc({
- "doctype": "Rename This",
- "linked_to_doctype": "Rename This"
- })
- to_rename_record.insert()
-
- # Rename doctype
- self.assertEqual("Renamed Doc", frappe.rename_doc("DocType", "Rename This", "Renamed Doc", force=True))
-
- # Test if Doctype value has changed in Link field
- renamed_doctype_record = frappe.get_doc("Renamed Doc", to_rename_record.name)
- self.assertEqual(renamed_doctype_record.linked_to_doctype, "Renamed Doc")
-
- # Test if there are conflicts between a record and a DocType
- # having the same name
- old_name = to_rename_record.name
- new_name = "ToDo"
- self.assertEqual(new_name, frappe.rename_doc("Renamed Doc", old_name, new_name, force=True))
-
- frappe.delete_doc_if_exists("Renamed Doc", "ToDo")
- frappe.delete_doc_if_exists("DocType", "Renamed Doc")
-
def test_non_negative_check(self):
frappe.delete_doc_if_exists("Currency", "Frappe Coin", 1)
diff --git a/frappe/tests/test_hooks.py b/frappe/tests/test_hooks.py
index f19904c8fc..ff71e2414c 100644
--- a/frappe/tests/test_hooks.py
+++ b/frappe/tests/test_hooks.py
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.desk.doctype.todo.todo import ToDo
+from frappe.cache_manager import clear_controller_cache
class TestHooks(unittest.TestCase):
def test_hooks(self):
@@ -17,21 +18,20 @@ class TestHooks(unittest.TestCase):
hooks.get("doc_events").get("*").get("on_update"))
def test_override_doctype_class(self):
- # mock get_hooks
- original = frappe.get_hooks
- def get_hooks(hook=None, default=None, app_name=None):
- if hook == 'override_doctype_class':
- return {
- 'ToDo': ['frappe.tests.test_hooks.CustomToDo']
- }
- return original(hook, default, app_name)
- frappe.get_hooks = get_hooks
+ from frappe import hooks
+
+ # Set hook
+ hooks.override_doctype_class = {
+ 'ToDo': ['frappe.tests.test_hooks.CustomToDo']
+ }
+
+ # Clear cache
+ frappe.cache().delete_value('app_hooks')
+ clear_controller_cache('ToDo')
todo = frappe.get_doc(doctype='ToDo', description='asdf')
self.assertTrue(isinstance(todo, CustomToDo))
- # restore
- frappe.get_hooks = original
class CustomToDo(ToDo):
pass
diff --git a/frappe/tests/test_permissions.py b/frappe/tests/test_permissions.py
index dddc790c94..6897d500c9 100644
--- a/frappe/tests/test_permissions.py
+++ b/frappe/tests/test_permissions.py
@@ -9,7 +9,7 @@ import frappe.defaults
import unittest
import frappe.model.meta
from frappe.permissions import (add_user_permission, remove_user_permission,
- clear_user_permissions_for_doctype, get_doc_permissions, add_permission)
+ clear_user_permissions_for_doctype, get_doc_permissions, add_permission, update_permission_property)
from frappe.core.page.permission_manager.permission_manager import update, reset
from frappe.test_runner import make_test_records_for_doctype
from frappe.core.doctype.user_permission.user_permission import clear_user_permissions
@@ -58,6 +58,24 @@ class TestPermissions(unittest.TestCase):
post = frappe.get_doc("Blog Post", "-test-blog-post")
self.assertTrue(post.has_permission("read"))
+ def test_select_permission(self):
+ # grant only select perm to blog post
+ add_permission('Blog Post', 'Sales User', 0)
+ update_permission_property('Blog Post', 'Sales User', 0, 'select', 1)
+ update_permission_property('Blog Post', 'Sales User', 0, 'read', 0)
+ update_permission_property('Blog Post', 'Sales User', 0, 'write', 0)
+
+ frappe.clear_cache(doctype="Blog Post")
+ frappe.set_user("test3@example.com")
+
+ # validate select perm
+ post = frappe.get_doc("Blog Post", "-test-blog-post")
+ self.assertTrue(post.has_permission("select"))
+
+ # validate does not have read and write perm
+ self.assertFalse(post.has_permission("read"))
+ self.assertRaises(frappe.PermissionError, post.save)
+
def test_user_permissions_in_doc(self):
add_user_permission("Blog Category", "-test-blog-category-1",
"test2@example.com")
diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py
new file mode 100644
index 0000000000..58cc5bb125
--- /dev/null
+++ b/frappe/tests/test_rename_doc.py
@@ -0,0 +1,159 @@
+import os
+import unittest
+
+import frappe
+from frappe.utils import add_to_date, now
+from frappe.exceptions import DoesNotExistError
+
+from random import choice, sample
+from frappe.model.base_document import get_controller
+from frappe.modules.utils import get_doc_path
+
+
+class TestRenameDoc(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ """Setting Up data for the tests defined under TestRenameDoc"""
+ # set developer_mode to rename doc controllers
+ self._original_developer_flag = frappe.conf.developer_mode
+ frappe.conf.developer_mode = 1
+
+ # data generation: for base and merge tests
+ self.available_documents = []
+ self.test_doctype = "ToDo"
+
+ for num in range(1, 5):
+ doc = frappe.get_doc({
+ "doctype": self.test_doctype,
+ "date": add_to_date(now(), days=num),
+ "description": "this is todo #{}".format(num),
+ }).insert()
+ self.available_documents.append(doc.name)
+
+ # data generation: for controllers tests
+ self.doctype = frappe._dict({
+ "old": "Test Rename Document Old",
+ "new": "Test Rename Document New",
+ })
+
+ frappe.get_doc({
+ "doctype": "DocType",
+ "module": "Custom",
+ "name": self.doctype.old,
+ "custom": 0,
+ "fields": [
+ {"label": "Some Field", "fieldname": "some_fieldname", "fieldtype": "Data"}
+ ],
+ "permissions": [{"role": "System Manager", "read": 1}],
+ }).insert()
+
+ @classmethod
+ def tearDownClass(self):
+ """Deleting data generated for the tests defined under TestRenameDoc"""
+ # delete the documents created
+ for docname in self.available_documents:
+ frappe.delete_doc(self.test_doctype, docname)
+
+ for dt in self.doctype.values():
+ if frappe.db.exists("DocType", dt):
+ frappe.delete_doc("DocType", dt)
+ frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{dt}`")
+
+ frappe.delete_doc_if_exists("Renamed Doc", "ToDo")
+
+ # reset original value of developer_mode conf
+ frappe.conf.developer_mode = self._original_developer_flag
+
+ def setUp(self):
+ frappe.flags.link_fields = {}
+ super().setUp()
+
+ def test_rename_doc(self):
+ """Rename an existing document via frappe.rename_doc"""
+ old_name = choice(self.available_documents)
+ new_name = old_name + ".new"
+ self.assertEqual(new_name, frappe.rename_doc(self.test_doctype, old_name, new_name, force=True))
+ self.available_documents.remove(old_name)
+ self.available_documents.append(new_name)
+
+ def test_merging_docs(self):
+ """Merge two documents via frappe.rename_doc"""
+ first_todo, second_todo = sample(self.available_documents, 2)
+
+ second_todo_doc = frappe.get_doc(self.test_doctype, second_todo)
+ second_todo_doc.priority = "High"
+ second_todo_doc.save()
+
+ merged_todo = frappe.rename_doc(
+ self.test_doctype, first_todo, second_todo, merge=True, force=True
+ )
+ merged_todo_doc = frappe.get_doc(self.test_doctype, merged_todo)
+ self.available_documents.remove(first_todo)
+
+ with self.assertRaises(DoesNotExistError):
+ frappe.get_doc(self.test_doctype, first_todo)
+
+ self.assertEqual(merged_todo_doc.priority, second_todo_doc.priority)
+
+ def test_rename_controllers(self):
+ """Rename doctypes with controller code paths"""
+ # check if module exists exists;
+ # if custom, get_controller will return Document class
+ # if not custom, a different class will be returned
+ self.assertNotEqual(get_controller(self.doctype.old), frappe.model.document.Document)
+
+ old_doctype_path = get_doc_path("Custom", "DocType", self.doctype.old)
+
+ # rename doc via wrapper API accessible via /desk
+ frappe.rename_doc("DocType", self.doctype.old, self.doctype.new)
+
+ # check if database and controllers are updated
+ self.assertTrue(frappe.db.exists("DocType", self.doctype.new))
+ self.assertFalse(frappe.db.exists("DocType", self.doctype.old))
+ self.assertFalse(os.path.exists(old_doctype_path))
+
+ def test_rename_doctype(self):
+ """Rename DocType via frappe.rename_doc"""
+ from frappe.core.doctype.doctype.test_doctype import new_doctype
+
+ if not frappe.db.exists("DocType", "Rename This"):
+ new_doctype(
+ "Rename This",
+ fields=[
+ {
+ "label": "Linked To",
+ "fieldname": "linked_to_doctype",
+ "fieldtype": "Link",
+ "options": "DocType",
+ "unique": 0,
+ }
+ ],
+ ).insert()
+
+ to_rename_record = frappe.get_doc(
+ {"doctype": "Rename This", "linked_to_doctype": "Rename This"}
+ ).insert()
+
+ # Rename doctype
+ self.assertEqual(
+ "Renamed Doc", frappe.rename_doc("DocType", "Rename This", "Renamed Doc", force=True)
+ )
+
+ # Test if Doctype value has changed in Link field
+ linked_to_doctype = frappe.db.get_value(
+ "Renamed Doc", to_rename_record.name, "linked_to_doctype"
+ )
+ self.assertEqual(linked_to_doctype, "Renamed Doc")
+
+ # Test if there are conflicts between a record and a DocType
+ # having the same name
+ old_name = to_rename_record.name
+ new_name = "ToDo"
+ self.assertEqual(
+ new_name, frappe.rename_doc("Renamed Doc", old_name, new_name, force=True)
+ )
+
+ # delete_doc doesnt drop tables
+ # this is done to bypass inconsistencies in the db
+ frappe.delete_doc_if_exists("DocType", "Renamed Doc")
+ frappe.db.sql_ddl("drop table if exists `tabRenamed Doc`")
diff --git a/frappe/tests/test_translate.py b/frappe/tests/test_translate.py
index 4dcaf3e979..4f1b69cc76 100644
--- a/frappe/tests/test_translate.py
+++ b/frappe/tests/test_translate.py
@@ -18,6 +18,7 @@ class TestTranslate(unittest.TestCase):
frappe.local.lang = 'fr'
self.assertEqual(_('Change'), 'Changement')
self.assertEqual(_('Change', context='Coins'), 'la monnaie')
+ frappe.local.lang = 'en'
expected_output = [
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', 'This is some context', 2),
diff --git a/frappe/tests/tests_geo_utils.py b/frappe/tests/tests_geo_utils.py
new file mode 100644
index 0000000000..2067a6aa97
--- /dev/null
+++ b/frappe/tests/tests_geo_utils.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import unittest
+
+import frappe
+from frappe.geo.utils import get_coords
+
+
+class TestGeoUtils(unittest.TestCase):
+ def setUp(self):
+ self.todo = frappe.get_doc(
+ dict(doctype='ToDo', description='Test description', assigned_by='Administrator')).insert()
+
+ self.test_location_dict = {'type': 'FeatureCollection', 'features': [
+ {'type': 'Feature', 'properties': {}, "geometry": {'type': 'Point', 'coordinates': [49.20433, 55.753395]}}]}
+ self.test_location = frappe.get_doc({'name': 'Test Location', 'doctype': 'Location',
+ 'location': str(self.test_location_dict)})
+
+ self.test_filter_exists = [['Location', 'name', 'like', '%Test Location%']]
+ self.test_filter_not_exists = [['Location', 'name', 'like', '%Test Location Not exists%']]
+ self.test_filter_todo = [['ToDo', 'description', 'like', '%Test description%']]
+
+ def test_get_coords_location_with_filter_exists(self):
+ coords = get_coords('Location', self.test_filter_exists, 'location_field')
+ self.assertEqual(self.test_location_dict['features'][0]['geometry'], coords['features'][0]['geometry'])
+
+ def test_get_coords_location_with_filter_not_exists(self):
+ coords = get_coords('Location', self.test_filter_not_exists, 'location_field')
+ self.assertEqual(coords, {'type': 'FeatureCollection', 'features': []})
+
+ def test_get_coords_from_not_existable_location(self):
+ self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'location_field')
+
+ def test_get_coords_from_not_existable_coords(self):
+ self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'coordinates')
+
+ def tearDown(self):
+ self.todo.delete()
diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py
index ef572c6971..54a5a24acf 100644
--- a/frappe/tests/ui_test_helpers.py
+++ b/frappe/tests/ui_test_helpers.py
@@ -95,6 +95,24 @@ def create_doctype(name, fields):
"name": name
}).insert()
+@frappe.whitelist()
+def create_child_doctype(name, fields):
+ fields = frappe.parse_json(fields)
+ if frappe.db.exists('DocType', name):
+ return
+ frappe.get_doc({
+ "doctype": "DocType",
+ "module": "Core",
+ "istable": 1,
+ "custom": 1,
+ "fields": fields,
+ "permissions": [{
+ "role": "System Manager",
+ "read": 1
+ }],
+ "name": name
+ }).insert()
+
@frappe.whitelist()
def create_contact_records():
if frappe.db.get_all('Contact', {'first_name': 'Test Form Contact 1'}):
diff --git a/frappe/translate.py b/frappe/translate.py
index 3685daf986..2cee8c34b5 100644
--- a/frappe/translate.py
+++ b/frappe/translate.py
@@ -190,7 +190,7 @@ def get_full_dict(lang):
frappe.local.lang_full_dict = load_lang(lang)
try:
- # get user specific transaltion data
+ # get user specific translation data
user_translations = get_user_translations(lang)
frappe.local.lang_full_dict.update(user_translations)
except Exception:
diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv
index f1d72c1443..5b45d8c217 100644
--- a/frappe/translations/de.csv
+++ b/frappe/translations/de.csv
@@ -1577,7 +1577,7 @@ Monospace,Monospace,
More articles on {0},Weitere Artikel zum {0},
More content for the bottom of the page.,Zusätzlicher Inhalt für den unteren Teil der Seite.,
Most Used,Am Meisten verwendet,
-Move To,Ziehen nach,
+Move To,Bewegen nach,
Move To Trash,In den Papierkorb verschieben,
Move to Row Number,Gehe zu Zeilennummer,
Mr,Hr.,
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index 4a88b5fda1..da2c910e20 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -154,14 +154,22 @@ def get_time_zone():
return frappe.cache().get_value("time_zone", _get_time_zone)
-def convert_utc_to_user_timezone(utc_timestamp):
+def convert_utc_to_timezone(utc_timestamp, time_zone):
from pytz import timezone, UnknownTimeZoneError
utcnow = timezone('UTC').localize(utc_timestamp)
try:
- return utcnow.astimezone(timezone(get_time_zone()))
+ return utcnow.astimezone(timezone(time_zone))
except UnknownTimeZoneError:
return utcnow
+def get_datetime_in_timezone(time_zone):
+ utc_timestamp = datetime.datetime.utcnow()
+ return convert_utc_to_timezone(utc_timestamp, time_zone)
+
+def convert_utc_to_user_timezone(utc_timestamp):
+ time_zone = get_time_zone()
+ return convert_utc_to_timezone(utc_timestamp, time_zone)
+
def now():
"""return current datetime as yyyy-mm-dd hh:mm:ss"""
if frappe.flags.current_date:
@@ -369,7 +377,7 @@ def format_duration(seconds, hide_days=False):
example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float
"""
-
+
seconds = cint(seconds)
total_duration = {
@@ -444,25 +452,29 @@ def get_weekday(datetime=None):
return weekdays[datetime.weekday()]
def get_timespan_date_range(timespan):
+ today = nowdate()
date_range_map = {
- "last week": [add_to_date(nowdate(), days=-7), nowdate()],
- "last month": [add_to_date(nowdate(), months=-1), nowdate()],
- "last quarter": [add_to_date(nowdate(), months=-3), nowdate()],
- "last 6 months": [add_to_date(nowdate(), months=-6), nowdate()],
- "last year": [add_to_date(nowdate(), years=-1), nowdate()],
- "today": [nowdate(), nowdate()],
- "this week": [get_first_day_of_week(nowdate(), as_str=True), nowdate()],
- "this month": [get_first_day(nowdate(), as_str=True), nowdate()],
- "this quarter": [get_quarter_start(nowdate(), as_str=True), nowdate()],
- "this year": [get_year_start(nowdate(), as_str=True), nowdate()],
- "next week": [nowdate(), add_to_date(nowdate(), days=7)],
- "next month": [nowdate(), add_to_date(nowdate(), months=1)],
- "next quarter": [nowdate(), add_to_date(nowdate(), months=3)],
- "next 6 months": [nowdate(), add_to_date(nowdate(), months=6)],
- "next year": [nowdate(), add_to_date(nowdate(), years=1)],
+ "last week": lambda: (add_to_date(today, days=-7), today),
+ "last month": lambda: (add_to_date(today, months=-1), today),
+ "last quarter": lambda: (add_to_date(today, months=-3), today),
+ "last 6 months": lambda: (add_to_date(today, months=-6), today),
+ "last year": lambda: (add_to_date(today, years=-1), today),
+ "yesterday": lambda: (add_to_date(today, days=-1),) * 2,
+ "today": lambda: (today, today),
+ "tomorrow": lambda: (add_to_date(today, days=1),) * 2,
+ "this week": lambda: (get_first_day_of_week(today, as_str=True), today),
+ "this month": lambda: (get_first_day(today, as_str=True), today),
+ "this quarter": lambda: (get_quarter_start(today, as_str=True), today),
+ "this year": lambda: (get_year_start(today, as_str=True), today),
+ "next week": lambda: (today, add_to_date(today, days=7)),
+ "next month": lambda: (today, add_to_date(today, months=1)),
+ "next quarter": lambda: (today, add_to_date(today, months=3)),
+ "next 6 months": lambda: (today, add_to_date(today, months=6)),
+ "next year": lambda: (today, add_to_date(today, years=1)),
}
- return date_range_map.get(timespan)
+ if timespan in date_range_map:
+ return date_range_map[timespan]()
def global_date_format(date, format="long"):
"""returns localized date in the form of January 1, 2012"""
diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py
index c4dfd3dc11..e7672cedb3 100644
--- a/frappe/utils/oauth.py
+++ b/frappe/utils/oauth.py
@@ -230,12 +230,19 @@ def update_oauth_user(user, data, provider):
save = True
user = frappe.new_doc("User")
+
+ gender = (data.get("gender") or "").title()
+
+ if not frappe.db.exists("Gender", gender):
+ doc = frappe.new_doc("Gender", {"gender": gender})
+ doc.insert(ignore_permissions=True)
+
user.update({
"doctype":"User",
"first_name": get_first_name(data),
"last_name": get_last_name(data),
"email": get_email(data),
- "gender": (data.get("gender") or "").title(),
+ "gender": gender,
"enabled": 1,
"new_password": frappe.generate_hash(get_email(data)),
"location": data.get("location"),
@@ -306,7 +313,7 @@ def redirect_post_login(desk_user, redirect_to=None, provider=None):
frappe.local.response["type"] = "redirect"
if not redirect_to:
- # the #desktop is added to prevent a facebook redirect bug
+ # the #workspace is added to prevent a facebook redirect bug
desk_uri = "/desk#workspace" if provider == 'facebook' else '/desk'
redirect_to = desk_uri if desk_user else "/me"
redirect_to = frappe.utils.get_url(redirect_to)
diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py
index 2aacf5eda8..06a192c05e 100644
--- a/frappe/utils/safe_exec.py
+++ b/frappe/utils/safe_exec.py
@@ -222,6 +222,7 @@ VALID_UTILS = (
"get_last_day_of_week",
"get_last_day",
"get_time",
+"get_datetime_in_timezone",
"get_datetime_str",
"get_date_str",
"get_time_str",
diff --git a/frappe/utils/user.py b/frappe/utils/user.py
index 7ee47cb197..ee9ee5dae9 100755
--- a/frappe/utils/user.py
+++ b/frappe/utils/user.py
@@ -22,6 +22,7 @@ class UserPermissions:
self.all_read = []
self.can_create = []
+ self.can_select = []
self.can_read = []
self.can_write = []
self.can_cancel = []
@@ -104,6 +105,9 @@ class UserPermissions:
if not p.get("read") and (dt in user_shared):
p["read"] = 1
+ if p.get('select'):
+ self.can_select.append(dt)
+
if not dtp.get('istable'):
if p.get('create') and not dtp.get('issingle'):
if dtp.get('in_create'):
@@ -193,9 +197,8 @@ class UserPermissions:
d.name = self.name
d.roles = self.get_roles()
d.defaults = self.get_defaults()
-
- for key in ("can_create", "can_write", "can_read", "can_cancel", "can_delete",
- "can_get_report", "allow_modules", "all_read", "can_search",
+ for key in ("can_select", "can_create", "can_write", "can_read", "can_cancel",
+ "can_delete", "can_get_report", "allow_modules", "all_read", "can_search",
"in_create", "can_export", "can_import", "can_print", "can_email",
"can_set_user_permissions"):
d[key] = list(set(getattr(self, key)))
diff --git a/frappe/website/doctype/blog_post/templates/blog_post_row.html b/frappe/website/doctype/blog_post/templates/blog_post_row.html
index 7daf27adc8..53539c33e0 100644
--- a/frappe/website/doctype/blog_post/templates/blog_post_row.html
+++ b/frappe/website/doctype/blog_post/templates/blog_post_row.html
@@ -21,7 +21,7 @@
{%- if post.featured -%}
{{ post.title }}
{%- else -%}
- {{ post.title }}
+ {{ post.title }}
{%- endif -%}
{{ post.intro }}
@@ -38,4 +38,4 @@
-
\ No newline at end of file
+
diff --git a/frappe/website/doctype/website_theme/website_theme.json b/frappe/website/doctype/website_theme/website_theme.json
index 78c3c696e9..ee4b33d854 100644
--- a/frappe/website/doctype/website_theme/website_theme.json
+++ b/frappe/website/doctype/website_theme/website_theme.json
@@ -65,8 +65,10 @@
},
{
"fieldname": "theme_url",
- "fieldtype": "Read Only",
- "label": "Theme URL"
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Theme URL",
+ "read_only": 1
},
{
"collapsible": 1,
@@ -179,7 +181,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-09-24 11:42:33.867840",
+ "modified": "2021-01-18 17:43:39.804765",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Theme",
diff --git a/frappe/website/js/bootstrap-4.js b/frappe/website/js/bootstrap-4.js
index dbe837b101..da720eedaf 100644
--- a/frappe/website/js/bootstrap-4.js
+++ b/frappe/website/js/bootstrap-4.js
@@ -18,7 +18,7 @@ $('.dropdown-menu a.dropdown-toggle').on('click', function (e) {
return false;
});
-frappe.get_modal = function(title, content) {
+frappe.get_modal = function (title, content) {
return $(
`
@@ -33,6 +33,10 @@ frappe.get_modal = function(title, content) {
${content}
diff --git a/frappe/website/js/syntax_highlight.js b/frappe/website/js/syntax_highlight.js
index 199174b1e5..80914d9d99 100644
--- a/frappe/website/js/syntax_highlight.js
+++ b/frappe/website/js/syntax_highlight.js
@@ -1,4 +1,4 @@
-const hljs = require('highlight.js/lib/highlight');
+const hljs = require('highlight.js/lib/core');
hljs.registerLanguage('javascript', require('highlight.js/lib/languages/javascript'));
hljs.registerLanguage('python', require('highlight.js/lib/languages/python'));
diff --git a/frappe/website/web_template/testimonial/testimonial.html b/frappe/website/web_template/testimonial/testimonial.html
index b656d3b03d..f860abbae6 100644
--- a/frappe/website/web_template/testimonial/testimonial.html
+++ b/frappe/website/web_template/testimonial/testimonial.html
@@ -5,9 +5,7 @@
{% endif %}
- “
- {{ content }}
- ”
+ “{{ content }}”
{{ name }}
diff --git a/frappe/workflow/doctype/workflow_transition/workflow_transition.json b/frappe/workflow/doctype/workflow_transition/workflow_transition.json
index 8bc06bf18a..5e5cec5880 100644
--- a/frappe/workflow/doctype/workflow_transition/workflow_transition.json
+++ b/frappe/workflow/doctype/workflow_transition/workflow_transition.json
@@ -295,7 +295,7 @@
"label": "Example",
"length": 0,
"no_copy": 0,
- "options": "
doc.grand_total > 0
\n\n
Conditions should be written in simple Python. Please use properties available in the form only.
",
+ "options": "
doc.grand_total > 0
\n\n
Conditions should be written in simple Python. Please use properties available in the form only.
\n
Allowed functions: \n
\n- frappe.db.get_value
\n- frappe.db.get_list
\n- frappe.session
\n- frappe.utils.now_datetime
\n- frappe.utils.get_datetime
\n- frappe.utils.add_to_date
\n- frappe.utils.now
\n
\n
Example:
doc.creation > frappe.utils.add_to_date(frappe.utils.now_datetime(), days=-5, as_string=True, as_datetime=True)
",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@@ -320,7 +320,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
- "modified": "2018-10-09 10:28:53.294908",
+ "modified": "2020-11-08 12:11:00.294908",
"modified_by": "Administrator",
"module": "Workflow",
"name": "Workflow Transition",
diff --git a/package.json b/package.json
index d1a94d0e35..fcbc349307 100644
--- a/package.json
+++ b/package.json
@@ -28,11 +28,11 @@
"driver.js": "^0.9.8",
"express": "^4.17.1",
"fast-deep-equal": "^2.0.1",
- "frappe-charts": "^1.5.1",
+ "frappe-charts": "^1.5.5",
"frappe-datatable": "^1.15.3",
"frappe-gantt": "^0.5.0",
"fuse.js": "^3.4.6",
- "highlight.js": "^9.18.2",
+ "highlight.js": "^10.4.1",
"js-sha256": "^0.9.0",
"jsbarcode": "^3.9.0",
"moment": "^2.20.1",
@@ -45,7 +45,7 @@
"redis": "^2.8.0",
"showdown": "^1.9.1",
"snyk": "^1.425.4",
- "socket.io": "^2.3.0",
+ "socket.io": "^2.4.0",
"superagent": "^3.8.2",
"touch": "^3.1.0",
"vue": "^2.6.11",
diff --git a/requirements.txt b/requirements.txt
index 3cc92264a2..e128790e45 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -49,7 +49,7 @@ pypng==0.0.20
PyQRCode==1.2.1
python-dateutil==2.8.1
pytz==2019.3
-PyYAML==5.3.1
+PyYAML==5.4
rauth==0.7.3
redis==3.5.3
requests-oauthlib==1.3.0
diff --git a/yarn.lock b/yarn.lock
index 26797675c6..3810b88e47 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -569,11 +569,6 @@ async-foreach@^0.1.3:
resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=
-async-limiter@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
- integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==
-
async@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
@@ -677,13 +672,6 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2:
dependencies:
tweetnacl "^0.14.3"
-better-assert@~1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
- integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=
- dependencies:
- callsite "1.0.0"
-
big.js@^3.1.3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
@@ -914,11 +902,6 @@ caller-path@^2.0.0:
dependencies:
caller-callsite "^2.0.0"
-callsite@1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
- integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
-
callsites@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
@@ -1172,6 +1155,11 @@ component-emitter@1.2.1, component-emitter@^1.2.0, component-emitter@^1.2.1:
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
+component-emitter@~1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+ integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+
component-inherit@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
@@ -1230,16 +1218,16 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
-cookie@0.3.1:
- version "0.3.1"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
- integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
-
cookie@0.4.0, cookie@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
+cookie@~0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
+ integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
+
cookiejar@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c"
@@ -1829,20 +1817,20 @@ endian-reader@^0.3.0:
resolved "https://registry.yarnpkg.com/endian-reader/-/endian-reader-0.3.0.tgz#84eca436b80aed0d0639c47291338b932efe50a0"
integrity sha1-hOykNrgK7Q0GOcRykTOLky7+UKA=
-engine.io-client@~3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700"
- integrity sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA==
+engine.io-client@~3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.5.0.tgz#fc1b4d9616288ce4f2daf06dcf612413dec941c7"
+ integrity sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==
dependencies:
- component-emitter "1.2.1"
+ component-emitter "~1.3.0"
component-inherit "0.0.3"
- debug "~4.1.0"
+ debug "~3.1.0"
engine.io-parser "~2.2.0"
has-cors "1.1.0"
indexof "0.0.1"
- parseqs "0.0.5"
- parseuri "0.0.5"
- ws "~6.1.0"
+ parseqs "0.0.6"
+ parseuri "0.0.6"
+ ws "~7.4.2"
xmlhttprequest-ssl "~1.5.4"
yeast "0.1.2"
@@ -1857,17 +1845,17 @@ engine.io-parser@~2.2.0:
blob "0.0.5"
has-binary2 "~1.0.2"
-engine.io@~3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.0.tgz#3a962cc4535928c252759a00f98519cb46c53ff3"
- integrity sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w==
+engine.io@~3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.5.0.tgz#9d6b985c8a39b1fe87cd91eb014de0552259821b"
+ integrity sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==
dependencies:
accepts "~1.3.4"
base64id "2.0.0"
- cookie "0.3.1"
+ cookie "~0.4.1"
debug "~4.1.0"
engine.io-parser "~2.2.0"
- ws "^7.1.2"
+ ws "~7.4.2"
entities@^1.1.1:
version "1.1.2"
@@ -2299,10 +2287,10 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
-frappe-charts@^1.5.1:
- version "1.5.4"
- resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.4.tgz#5870f77ac6ffc8ea4dab32adda1d4e5e4fbda64b"
- integrity sha512-hBr7cRLmsCC5VBj/HwKOCgdwyXnkeAO5CAvOd5H4IYFbk84VD9jOjx9fSaqAE0MygVVbY1nCN+5nb08WThW4Xw==
+frappe-charts@^1.5.5:
+ version "1.5.5"
+ resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.5.tgz#5f44a3639aecc6f8fc7d15752abc80bb68e26734"
+ integrity sha512-L9pJTsrSuRobS/EaBKT8i1x+DVOjkXyUwT85cteZAPqynU/7K+uqjQOy4tMSTv5zsTWJNWFJ37ax68T73YdR3g==
frappe-datatable@^1.15.3:
version "1.15.3"
@@ -2702,10 +2690,10 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
-highlight.js@^9.18.2:
- version "9.18.5"
- resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825"
- integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==
+highlight.js@^10.4.1:
+ version "10.4.1"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0"
+ integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==
homedir-polyfill@^1.0.1:
version "1.0.3"
@@ -2918,9 +2906,9 @@ inherits@2.0.3:
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
- integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+ integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
inquirer@^7.3.3:
version "7.3.3"
@@ -4184,11 +4172,6 @@ object-assign@^4.0.1, object-assign@^4.1.0:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
-object-component@0.0.3:
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
- integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=
-
object-copy@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
@@ -4473,19 +4456,15 @@ parse-passwd@^1.0.0:
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
-parseqs@0.0.5:
- version "0.0.5"
- resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
- integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=
- dependencies:
- better-assert "~1.0.0"
+parseqs@0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
+ integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
-parseuri@0.0.5:
- version "0.0.5"
- resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a"
- integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=
- dependencies:
- better-assert "~1.0.0"
+parseuri@0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
+ integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
parseurl@~1.3.3:
version "1.3.3"
@@ -6231,23 +6210,20 @@ socket.io-adapter@~1.1.0:
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b"
integrity sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=
-socket.io-client@2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4"
- integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==
+socket.io-client@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.4.0.tgz#aafb5d594a3c55a34355562fc8aea22ed9119a35"
+ integrity sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==
dependencies:
backo2 "1.0.2"
- base64-arraybuffer "0.1.5"
component-bind "1.0.0"
- component-emitter "1.2.1"
- debug "~4.1.0"
- engine.io-client "~3.4.0"
+ component-emitter "~1.3.0"
+ debug "~3.1.0"
+ engine.io-client "~3.5.0"
has-binary2 "~1.0.2"
- has-cors "1.1.0"
indexof "0.0.1"
- object-component "0.0.3"
- parseqs "0.0.5"
- parseuri "0.0.5"
+ parseqs "0.0.6"
+ parseuri "0.0.6"
socket.io-parser "~3.3.0"
to-array "0.1.4"
@@ -6269,16 +6245,16 @@ socket.io-parser@~3.4.0:
debug "~4.1.0"
isarray "2.0.1"
-socket.io@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb"
- integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==
+socket.io@^2.4.0:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.4.1.tgz#95ad861c9a52369d7f1a68acf0d4a1b16da451d2"
+ integrity sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==
dependencies:
debug "~4.1.0"
- engine.io "~3.4.0"
+ engine.io "~3.5.0"
has-binary2 "~1.0.2"
socket.io-adapter "~1.1.0"
- socket.io-client "2.3.0"
+ socket.io-client "2.4.0"
socket.io-parser "~3.4.0"
socks-proxy-agent@^4.0.1:
@@ -7267,17 +7243,10 @@ write-file-atomic@^3.0.0:
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"
-ws@^7.1.2:
- version "7.2.1"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
- integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==
-
-ws@~6.1.0:
- version "6.1.4"
- resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
- integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==
- dependencies:
- async-limiter "~1.0.0"
+ws@~7.4.2:
+ version "7.4.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd"
+ integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==
xdg-basedir@^4.0.0:
version "4.0.0"