diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000..24f122a8d4
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,14 @@
+# Root editor config file
+root = true
+
+# Common settings
+[*]
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+
+# python, js indentation settings
+[{*.py,*.js}]
+indent_style = tab
+indent_size = 4
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..26bb7ab280
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Community Forum
+ url: https://discuss.erpnext.com/
+ about: For general QnA, discussions and community help.
diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py
index 3fc14ba61b..08d1d1aa9c 100644
--- a/.github/helper/documentation.py
+++ b/.github/helper/documentation.py
@@ -21,8 +21,8 @@ def docs_link_exists(body):
if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word)
if parsed_url.netloc == "github.com":
- _, org, repo, _type, ref = parsed_url.path.split('/')
- if org == "frappe" and repo in docs_repos:
+ parts = parsed_url.path.split('/')
+ if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
return True
diff --git a/.travis.yml b/.travis.yml
index 63895675ea..2331217363 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
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 fac0927428..4040a38e62 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -148,6 +148,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
@@ -326,7 +327,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)
@@ -353,7 +354,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
@@ -627,6 +628,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.
@@ -945,7 +961,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/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js
index a11de1d881..d54ae8d62c 100644
--- a/frappe/automation/doctype/auto_repeat/auto_repeat.js
+++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js
@@ -44,6 +44,22 @@ frappe.ui.form.on('Auto Repeat', {
// auto repeat schedule
frappe.auto_repeat.render_schedule(frm);
+
+ frm.trigger('toggle_submit_on_creation');
+ },
+
+ reference_doctype: function(frm) {
+ frm.trigger('toggle_submit_on_creation');
+ },
+
+ toggle_submit_on_creation: function(frm) {
+ // submit on creation checkbox
+ 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) {
@@ -86,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 8ee6ca1d45..5ff4cbeead 100644
--- a/frappe/automation/doctype/auto_repeat/auto_repeat.json
+++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "format:AUT-AR-{#####}",
@@ -12,6 +13,7 @@
"section_break_3",
"reference_doctype",
"reference_document",
+ "submit_on_creation",
"column_break_5",
"start_date",
"end_date",
@@ -21,6 +23,8 @@
"repeat_on_last_day",
"column_break_12",
"next_schedule_date",
+ "section_break_12",
+ "repeat_on_days",
"notification",
"notify_by_email",
"recipients",
@@ -186,9 +190,28 @@
"fieldname": "repeat_on_last_day",
"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"
+ },
+ {
+ "depends_on": "eval:doc.frequency==='Weekly';",
+ "fieldname": "section_break_12",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "submit_on_creation",
+ "fieldtype": "Check",
+ "label": "Submit on Creation"
}
],
- "modified": "2019-07-17 11:30:51.412317",
+ "links": [],
+ "modified": "2020-12-10 10:43:13.449172",
"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 fcf24bf1a9..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,16 +14,19 @@ 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):
self.update_status()
self.validate_reference_doctype()
+ 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()
@@ -48,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:
@@ -60,6 +64,11 @@ class AutoRepeat(Document):
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype))
+ def validate_submit_on_creation(self):
+ if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable:
+ frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format(
+ frappe.bold('Submit on Creation')))
+
def validate_dates(self):
if frappe.flags.in_patch:
return
@@ -82,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")
@@ -107,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,
@@ -116,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 = {
@@ -126,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
@@ -150,6 +163,9 @@ class AutoRepeat(Document):
self.update_doc(new_doc, reference_doc)
new_doc.insert(ignore_permissions = True)
+ if self.submit_on_creation:
+ new_doc.submit()
+
return new_doc
def update_doc(self, new_doc, reference_doc):
@@ -160,7 +176,7 @@ class AutoRepeat(Document):
if new_doc.meta.get_field('auto_repeat'):
new_doc.set('auto_repeat', self.name)
- for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']:
+ for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']:
if new_doc.meta.get_field(fieldname):
new_doc.set(fieldname, reference_doc.get(fieldname))
@@ -202,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 ''
@@ -282,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'
@@ -328,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)
@@ -337,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())
@@ -349,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']})
@@ -358,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 60fa9cb59e..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)
@@ -111,6 +156,25 @@ class TestAutoRepeat(unittest.TestCase):
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2))
self.assertEqual(getdate(doc.next_schedule_date), current_date)
+ def test_submit_on_creation(self):
+ doctype = 'Test Submittable DocType'
+ create_submittable_doctype(doctype)
+
+ current_date = getdate()
+ submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert()
+ submittable_doc.submit()
+ doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name,
+ start_date=add_days(current_date, -1), submit_on_creation=1)
+
+ data = get_auto_repeat_entries(current_date)
+ create_repeated_entries(data)
+ docnames = frappe.db.get_all(doc.reference_doctype,
+ filters={'auto_repeat': doc.name},
+ fields=['docstatus'],
+ limit=1
+ )
+ self.assertEquals(docnames[0].docstatus, 1)
+
def make_auto_repeat(**args):
args = frappe._dict(args)
@@ -118,13 +182,46 @@ def make_auto_repeat(**args):
'doctype': 'Auto Repeat',
'reference_doctype': args.reference_doctype or 'ToDo',
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'),
+ 'submit_on_creation': args.submit_on_creation or 0,
'frequency': args.frequency or 'Daily',
'start_date': args.start_date or add_days(today(), -1),
'end_date': args.end_date or "",
'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
+
+
+def create_submittable_doctype(doctype):
+ if frappe.db.exists('DocType', doctype):
+ return
+ else:
+ doc = frappe.get_doc({
+ 'doctype': 'DocType',
+ '__newname': doctype,
+ 'module': 'Custom',
+ 'custom': 1,
+ 'is_submittable': 1,
+ 'fields': [{
+ 'fieldname': 'test',
+ 'label': 'Test',
+ 'fieldtype': 'Data'
+ }],
+ 'permissions': [{
+ 'role': 'System Manager',
+ 'read': 1,
+ 'write': 1,
+ 'create': 1,
+ 'delete': 1,
+ 'submit': 1,
+ 'cancel': 1,
+ 'amend': 1
+ }]
+ }).insert()
+
+ doc.allow_auto_repeat = 1
+ doc.save()
\ No newline at end of file
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/build.py b/frappe/build.py
index f14b250a92..f47a7cb32b 100644
--- a/frappe/build.py
+++ b/frappe/build.py
@@ -105,7 +105,7 @@ def download_frappe_assets(verbose=True):
if frappe_head:
try:
url = get_assets_link(frappe_head)
- click.secho("Retreiving assets...", fg="yellow")
+ click.secho("Retrieving assets...", fg="yellow")
prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head)
assets_archive = download_file(url, prefix)
print("\n{0} Downloaded Frappe assets from {1}".format(green('✔'), url))
diff --git a/frappe/chat/util/util.py b/frappe/chat/util/util.py
index 5aa80a85ae..82df6dd127 100644
--- a/frappe/chat/util/util.py
+++ b/frappe/chat/util/util.py
@@ -1,27 +1,21 @@
from __future__ import unicode_literals
+# imports - standard imports
+import json
+from collections.abc import MutableMapping, MutableSequence, Sequence
+
# imports - third-party imports
import requests
-
-# imports - compatibility imports
-import six
-
-# imports - standard imports
-from collections import Sequence, MutableSequence, Mapping, MutableMapping
-if six.PY2:
- from urlparse import urlparse # PY2
-else:
- from urllib.parse import urlparse # PY3
-import json
+from urllib.parse import urlparse
# imports - module imports
-from frappe.model.document import Document
-from frappe.exceptions import DuplicateEntryError
-from frappe import _dict
import frappe
+from frappe.exceptions import DuplicateEntryError
+from frappe.model.document import Document
session = frappe.session
+
def get_user_doc(user = None):
if isinstance(user, Document):
return user
@@ -38,12 +32,12 @@ def squashify(what):
return what
def safe_json_loads(*args):
- results = [ ]
+ results = []
for arg in args:
try:
arg = json.loads(arg)
- except Exception as e:
+ except Exception:
pass
results.append(arg)
@@ -81,7 +75,7 @@ def dictify(arg):
for i, a in enumerate(arg):
arg[i] = dictify(a)
elif isinstance(arg, MutableMapping):
- arg = _dict(arg)
+ arg = frappe._dict(arg)
return arg
@@ -113,4 +107,4 @@ def get_emojis():
emojis = resp.json()
redis.hset('frappe_emojis', 'emojis', emojis)
- return dictify(emojis)
\ No newline at end of file
+ return dictify(emojis)
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index 51c352a931..4a631be3ac 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -9,7 +9,7 @@ import click
import frappe
from frappe.commands import get_site, pass_context
from frappe.exceptions import SiteNotSpecifiedError
-from frappe.utils import get_site_path, touch_file
+from frappe.installer import _new_site
@click.command('new-site')
@@ -42,57 +42,6 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
if len(frappe.utils.get_sites()) == 1:
use(site)
-def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None,
- admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
- no_mariadb_socket=False, reinstall=False, db_password=None, db_type=None, db_host=None,
- db_port=None, new_site=False):
- """Install a new Frappe site"""
-
- if not force and os.path.exists(site):
- print('Site {0} already exists'.format(site))
- sys.exit(1)
-
- if no_mariadb_socket and not db_type == "mariadb":
- print('--no-mariadb-socket requires db_type to be set to mariadb.')
- sys.exit(1)
-
- if not db_name:
- import hashlib
- db_name = '_' + hashlib.sha1(site.encode()).hexdigest()[:16]
-
- from frappe.commands.scheduler import _is_scheduler_enabled
- from frappe.installer import install_db, make_site_dirs
- from frappe.installer import install_app as _install_app
- import frappe.utils.scheduler
-
- frappe.init(site=site)
-
- try:
-
- # enable scheduler post install?
- enable_scheduler = _is_scheduler_enabled()
- except Exception:
- enable_scheduler = False
-
- make_site_dirs()
-
- installing = touch_file(get_site_path('locks', 'installing.lock'))
-
- install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
- admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
- db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
- apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
- for app in apps_to_install:
- _install_app(app, verbose=verbose, set_as_patched=not source_sql)
-
- os.remove(installing)
-
- frappe.utils.scheduler.toggle_scheduler(enable_scheduler)
- frappe.db.commit()
-
- scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
- print("*** Scheduler is", scheduler_status, "***")
-
@click.command('restore')
@click.argument('sql-file-path')
@@ -107,33 +56,41 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
@pass_context
def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
"Restore site database from an sql file"
- from frappe.installer import extract_sql_gzip, extract_files, is_downgrade, validate_database_sql
+ from frappe.installer import (
+ extract_sql_from_archive,
+ extract_files,
+ is_downgrade,
+ is_partial,
+ validate_database_sql
+ )
+
force = context.force or force
+ decompressed_file_name = extract_sql_from_archive(sql_file_path)
- # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
- if not os.path.exists(sql_file_path):
- base_path = '..'
- sql_file_path = os.path.join(base_path, sql_file_path)
- if not os.path.exists(sql_file_path):
- print('Invalid path {0}'.format(sql_file_path[3:]))
- sys.exit(1)
- elif sql_file_path.startswith(os.sep):
- base_path = os.sep
- else:
- base_path = '.'
-
- if sql_file_path.endswith('sql.gz'):
- decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path))
- else:
- decompressed_file_name = sql_file_path
+ # check if partial backup
+ if is_partial(decompressed_file_name):
+ click.secho(
+ "Partial Backup file detected. You cannot use a partial file to restore a Frappe Site.",
+ fg="red"
+ )
+ click.secho(
+ "Use `bench partial-restore` to restore a partial backup to an existing site.",
+ fg="yellow"
+ )
+ sys.exit(1)
+ # check if valid SQL file
validate_database_sql(decompressed_file_name, _raise=not force)
+
site = get_site(context)
frappe.init(site=site)
# dont allow downgrading to older versions of frappe without force
if not force and is_downgrade(decompressed_file_name, verbose=True):
- warn_message = "This is not recommended and may lead to unexpected behaviour. Do you want to continue anyway?"
+ warn_message = (
+ "This is not recommended and may lead to unexpected behaviour. "
+ "Do you want to continue anyway?"
+ )
click.confirm(warn_message, abort=True)
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
@@ -143,22 +100,39 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
# Extract public and/or private files to the restored site, if user has given the path
if with_public_files:
- with_public_files = os.path.join(base_path, with_public_files)
- public = extract_files(site, with_public_files, 'public')
+ public = extract_files(site, with_public_files)
os.remove(public)
if with_private_files:
- with_private_files = os.path.join(base_path, with_private_files)
- private = extract_files(site, with_private_files, 'private')
+ private = extract_files(site, with_private_files)
os.remove(private)
# Removing temporarily created file
if decompressed_file_name != sql_file_path:
os.remove(decompressed_file_name)
- success_message = "Site {0} has been restored{1}".format(site, " with files" if (with_public_files or with_private_files) else "")
+ success_message = "Site {0} has been restored{1}".format(
+ site,
+ " with files" if (with_public_files or with_private_files) else ""
+ )
click.secho(success_message, fg="green")
+
+@click.command('partial-restore')
+@click.argument('sql-file-path')
+@click.option("--verbose", "-v", is_flag=True)
+@pass_context
+def partial_restore(context, sql_file_path, verbose):
+ from frappe.installer import partial_restore
+ verbose = context.verbose or verbose
+
+ site = get_site(context)
+ frappe.init(site=site)
+ frappe.connect(site=site)
+ partial_restore(sql_file_path, verbose)
+ frappe.destroy()
+
+
@click.command('reinstall')
@click.option('--admin-password', help='Administrator Password for reinstalled site')
@click.option('--mariadb-root-username', help='Root username for MariaDB')
@@ -416,16 +390,20 @@ def use(site, sites_path='.'):
@click.command('backup')
@click.option('--with-files', default=False, is_flag=True, help="Take backup with files")
+@click.option('--include', '--only', '-i', default="", type=str, help="Specify the DocTypes to backup seperated by commas")
+@click.option('--exclude', '-e', default="", type=str, help="Specify the DocTypes to not backup seperated by commas")
@click.option('--backup-path', default=None, help="Set path for saving all the files in this operation")
@click.option('--backup-path-db', default=None, help="Set path for saving database file")
@click.option('--backup-path-files', default=None, help="Set path for saving public file")
@click.option('--backup-path-private-files', default=None, help="Set path for saving private file")
@click.option('--backup-path-conf', default=None, help="Set path for saving config file")
+@click.option('--ignore-backup-conf', default=False, is_flag=True, help="Ignore excludes/includes set in config")
@click.option('--verbose', default=False, is_flag=True, help="Add verbosity")
@click.option('--compress', default=False, is_flag=True, help="Compress private and public files")
@pass_context
def backup(context, with_files=False, backup_path=None, backup_path_db=None, backup_path_files=None,
- backup_path_private_files=None, backup_path_conf=None, verbose=False, compress=False):
+ backup_path_private_files=None, backup_path_conf=None, ignore_backup_conf=False, verbose=False,
+ compress=False, include="", exclude=""):
"Backup"
from frappe.utils.backups import scheduled_backup
verbose = verbose or context.verbose
@@ -435,11 +413,27 @@ def backup(context, with_files=False, backup_path=None, backup_path_db=None, bac
try:
frappe.init(site=site)
frappe.connect()
- odb = scheduled_backup(ignore_files=not with_files, backup_path=backup_path, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, backup_path_conf=backup_path_conf, force=True, verbose=verbose, compress=compress)
+ odb = scheduled_backup(
+ ignore_files=not with_files,
+ backup_path=backup_path,
+ backup_path_db=backup_path_db,
+ backup_path_files=backup_path_files,
+ backup_path_private_files=backup_path_private_files,
+ backup_path_conf=backup_path_conf,
+ ignore_conf=ignore_backup_conf,
+ include_doctypes=include,
+ exclude_doctypes=exclude,
+ compress=compress,
+ verbose=verbose,
+ force=True
+ )
except Exception:
click.secho("Backup failed for Site {0}. Database or site_config.json may be corrupted".format(site), fg="red")
+ if verbose:
+ print(frappe.get_traceback())
exit_code = 1
continue
+
odb.print_summary()
click.secho("Backup for Site {0} has been successfully completed{1}".format(site, " with files" if with_files else ""), fg="green")
frappe.destroy()
@@ -512,13 +506,14 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
if force:
pass
else:
- click.echo("="*80)
- click.echo("Error: The operation has stopped because backup of {s}'s database failed.".format(s=site))
- click.echo("Reason: {reason}{sep}".format(reason=str(err), sep="\n"))
- click.echo("Fix the issue and try again.")
- click.echo(
- "Hint: Use 'bench drop-site {s} --force' to force the removal of {s}".format(sep="\n", tab="\t", s=site)
- )
+ messages = [
+ "=" * 80,
+ "Error: The operation has stopped because backup of {0}'s database failed.".format(site),
+ "Reason: {0}\n".format(str(err)),
+ "Fix the issue and try again.",
+ "Hint: Use 'bench drop-site {0} --force' to force the removal of {0}".format(site)
+ ]
+ click.echo("\n".join(messages))
sys.exit(1)
drop_user_and_database(frappe.conf.db_name, root_login, root_password)
@@ -734,5 +729,6 @@ commands = [
stop_recording,
add_to_hosts,
start_ngrok,
- build_search_index
+ build_search_index,
+ partial_restore
]
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/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 cb3d06a29a..3ffebb21ae 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -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,19 +388,33 @@ 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
where doctype=%s and field='name' and value = %s""", (new, new, old))
else:
- frappe.db.sql("rename table `tab%s` to `tab%s`" % (old, new))
+ frappe.db.multisql({
+ "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)
+
+ for site in frappe.utils.get_sites():
+ frappe.cache().delete(f"{site}:doctype_classes", old)
+
+ def after_delete(self):
+ if not self.custom:
+ for site in frappe.utils.get_sites():
+ frappe.cache().delete(f"{site}:doctype_classes", self.name)
def rename_files_and_folders(self, old, new):
# move files
@@ -997,10 +1017,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"):
@@ -1023,9 +1043,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:
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/domain_settings/domain_settings.js b/frappe/core/doctype/domain_settings/domain_settings.js
index 1428727993..7178cb4cd6 100644
--- a/frappe/core/doctype/domain_settings/domain_settings.js
+++ b/frappe/core/doctype/domain_settings/domain_settings.js
@@ -18,6 +18,9 @@ frappe.ui.form.on('Domain Settings', {
checked: active_domains.includes(domain)
};
});
+ },
+ on_change: () => {
+ frm.dirty();
}
},
render_input: true
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index b8bed89a4d..8614740d26 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -30,7 +30,7 @@ import frappe
from frappe import _, conf
from frappe.model.document import Document
from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip
-
+from frappe.utils.image import strip_exif_data
class MaxFileSizeReachedError(frappe.ValidationError):
pass
@@ -93,6 +93,7 @@ class File(Document):
self.set_is_private()
self.set_file_name()
self.validate_duplicate_entry()
+ self.validate_attachment_limit()
self.validate_folder()
if not self.file_url and not self.flags.ignore_file_validate:
@@ -140,6 +141,26 @@ class File(Document):
if self.file_url and (self.is_private != self.file_url.startswith('/private')):
frappe.throw(_('Invalid file URL. Please contact System Administrator.'))
+ def validate_attachment_limit(self):
+ attachment_limit = 0
+ if self.attached_to_doctype and self.attached_to_name:
+ attachment_limit = cint(frappe.get_meta(self.attached_to_doctype).max_attachments)
+
+ if attachment_limit:
+ current_attachment_count = len(frappe.get_all('File', filters={
+ 'attached_to_doctype': self.attached_to_doctype,
+ 'attached_to_name': self.attached_to_name,
+ }, limit=attachment_limit + 1))
+
+ if current_attachment_count >= attachment_limit:
+ frappe.throw(
+ _("Maximum Attachment Limit of {0} has been reached for {1} {2}.").format(
+ frappe.bold(attachment_limit), self.attached_to_doctype, self.attached_to_name
+ ),
+ exc=frappe.exceptions.AttachmentLimitReached,
+ title=_('Attachment Limit Reached')
+ )
+
def set_folder_name(self):
"""Make parent folders if not exists based on reference doctype and name"""
if self.attached_to_doctype and not self.folder:
@@ -435,6 +456,7 @@ class File(Document):
def save_file(self, content=None, decode=False, ignore_existing_file_check=False):
file_exists = False
self.content = content
+
if decode:
if isinstance(content, text_type):
self.content = content.encode("utf-8")
@@ -445,10 +467,19 @@ class File(Document):
if not self.is_private:
self.is_private = 0
- self.file_size = self.check_max_file_size()
- self.content_hash = get_content_hash(self.content)
+
self.content_type = mimetypes.guess_type(self.file_name)[0]
+
+ self.file_size = self.check_max_file_size()
+
+ if (
+ self.content_type and "image" in self.content_type
+ and frappe.get_system_settings("strip_exif_metadata_from_uploaded_images")
+ ):
+ self.content = strip_exif_data(self.content, self.content_type)
+ self.content_hash = get_content_hash(self.content)
+
duplicate_file = None
# check if a file exists with the same content hash and is also in the same folder (public or private)
@@ -612,7 +643,12 @@ def get_extension(filename, extn, content):
return extn
def get_local_image(file_url):
- file_path = frappe.get_site_path("public", file_url.lstrip("/"))
+ if file_url.startswith("/private"):
+ file_url_path = (file_url.lstrip("/"), )
+ else:
+ file_url_path = ("public", file_url.lstrip("/"))
+
+ file_path = frappe.get_site_path(*file_url_path)
try:
image = Image.open(file_path)
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index 85397ea1ee..e627558680 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -160,6 +160,31 @@ class TestSameContent(unittest.TestCase):
def test_saved_content(self):
self.assertFalse(os.path.exists(get_files_path(self.dup_filename)))
+ def test_attachment_limit(self):
+ doctype, docname = make_test_doc()
+ from frappe.custom.doctype.property_setter.property_setter import make_property_setter
+ limit_property = make_property_setter('ToDo', None, 'max_attachments', 1, 'int', for_doctype=True)
+ file1 = frappe.get_doc({
+ "doctype": "File",
+ "file_name": 'test-attachment',
+ "attached_to_doctype": doctype,
+ "attached_to_name": docname,
+ "content": 'test'
+ })
+
+ file1.insert()
+
+ file2 = frappe.get_doc({
+ "doctype": "File",
+ "file_name": 'test-attachment',
+ "attached_to_doctype": doctype,
+ "attached_to_name": docname,
+ "content": 'test2'
+ })
+
+ self.assertRaises(frappe.exceptions.AttachmentLimitReached, file2.insert)
+ limit_property.delete()
+ frappe.clear_cache(doctype='ToDo')
def tearDown(self):
# File gets deleted on rollback, so blank
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/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py
index 2c02d99dad..1d0d6ebb09 100644
--- a/frappe/core/doctype/prepared_report/prepared_report.py
+++ b/frappe/core/doctype/prepared_report/prepared_report.py
@@ -89,20 +89,18 @@ def delete_expired_prepared_reports():
'creation': ['<', frappe.utils.add_days(frappe.utils.now(), -expiry_period)]
})
- args = {
- 'reports': prepared_reports_to_delete,
- 'limit': 50
- }
-
- enqueue(method=delete_prepared_reports, job_name="delete_prepared_reports", **args)
+ batches = frappe.utils.create_batch(prepared_reports_to_delete, 100)
+ for batch in batches:
+ args = {
+ 'reports': batch,
+ }
+ enqueue(method=delete_prepared_reports, job_name="delete_prepared_reports", **args)
@frappe.whitelist()
-def delete_prepared_reports(reports, limit=None):
+def delete_prepared_reports(reports):
reports = frappe.parse_json(reports)
- for index, doc in enumerate(reports):
- if limit and index == limit:
- return
- frappe.delete_doc('Prepared Report', doc['name'], ignore_permissions=True)
+ for report in reports:
+ frappe.delete_doc('Prepared Report', report['name'], ignore_permissions=True)
def create_json_gz_file(data, dt, dn):
# Storing data in CSV file causes information loss
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.js b/frappe/core/doctype/server_script/server_script.js
index 78ef2d0509..a317d69166 100644
--- a/frappe/core/doctype/server_script/server_script.js
+++ b/frappe/core/doctype/server_script/server_script.js
@@ -48,29 +48,33 @@ frappe.ui.form.on('Server Script', {
setup_help(frm) {
frm.get_field('help_html').html(`
-
Examples
DocType Event
-
+Add logic for standard doctype events like Before Insert, After Submit, etc.
+
+
# set property
if "test" in doc.description:
- doc.status = 'Closed'
+ doc.status = 'Closed'
# validate
if "validate" in doc.description:
- raise frappe.ValidationError
+ raise frappe.ValidationError
# auto create another document
-if doc.allocted_to:
- frappe.get_doc(dict(
- doctype = 'ToDo'
- owner = doc.allocated_to,
- description = doc.subject
- )).insert()
-
+if doc.allocated_to:
+ frappe.get_doc(dict(
+ doctype = 'ToDo'
+ owner = doc.allocated_to,
+ description = doc.subject
+ )).insert()
+
+
+
API Call
+Respond to /api/method/<method-name> calls, just like whitelisted methods
# respond to API
@@ -79,6 +83,21 @@ if frappe.form_dict.message == "ping":
else:
frappe.response['message'] = "ok"
+
+
+
+Permission Query
+Add conditions to the where clause of list queries.
+
+# generate dynamic conditions and set it in the conditions variable
+tenant_id = frappe.db.get_value(...)
+conditions = 'tenant_id = {}'.format(tenant_id)
+
+# resulting select query
+select name from \`tabPerson\`
+where tenant_id = 2
+order by creation desc
+
`);
}
diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json
index 420f96ec2f..94a48f196c 100644
--- a/frappe/core/doctype/server_script/server_script.json
+++ b/frappe/core/doctype/server_script/server_script.json
@@ -24,7 +24,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Script Type",
- "options": "DocType Event\nScheduler Event\nAPI",
+ "options": "DocType Event\nScheduler Event\nPermission Query\nAPI",
"reqd": 1
},
{
@@ -35,7 +35,7 @@
"reqd": 1
},
{
- "depends_on": "eval:doc.script_type==='DocType Event'",
+ "depends_on": "eval:['DocType Event', 'Permission Query'].includes(doc.script_type)",
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
@@ -88,7 +88,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-11-11 12:39:41.391052",
+ "modified": "2020-12-03 22:42:02.708148",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 839b784651..88d68dba14 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -4,6 +4,8 @@
from __future__ import unicode_literals
+import ast
+
import frappe
from frappe.model.document import Document
from frappe.utils.safe_exec import safe_exec
@@ -11,9 +13,9 @@ from frappe import _
class ServerScript(Document):
- @staticmethod
- def validate():
+ def validate(self):
frappe.only_for('Script Manager', True)
+ ast.parse(self.script)
@staticmethod
def on_update():
@@ -41,6 +43,12 @@ class ServerScript(Document):
# wrong report type!
raise frappe.DoesNotExistError
+ def get_permission_query_conditions(self, user):
+ locals = {"user": user, "conditions": ""}
+ safe_exec(self.script, None, locals)
+ if locals["conditions"]:
+ return locals["conditions"]
+
@frappe.whitelist()
def setup_scheduler_events(script_name, frequency):
method = frappe.scrub('{0}-{1}'.format(script_name, frequency))
diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py
index e03504f30b..4dc4f12b34 100644
--- a/frappe/core/doctype/server_script/server_script_utils.py
+++ b/frappe/core/doctype/server_script/server_script_utils.py
@@ -50,6 +50,9 @@ def get_server_script_map():
# },
# '_api': {
# '[path]': '[server script]'
+ # },
+ # 'permission_query': {
+ # 'DocType': '[server script]'
# }
# }
if frappe.flags.in_patch and not frappe.db.table_exists('Server Script'):
@@ -57,16 +60,20 @@ def get_server_script_map():
script_map = frappe.cache().get_value('server_script_map')
if script_map is None:
- script_map = {}
+ script_map = {
+ 'permission_query': {}
+ }
enabled_server_scripts = frappe.get_all('Server Script',
fields=('name', 'reference_doctype', 'doctype_event','api_method', 'script_type'),
filters={'disabled': 0})
for script in enabled_server_scripts:
if script.script_type == 'DocType Event':
script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name)
+ elif script.script_type == 'Permission Query':
+ script_map['permission_query'][script.reference_doctype] = script.name
else:
script_map.setdefault('_api', {})[script.api_method] = script.name
frappe.cache().set_value('server_script_map', script_map)
- return script_map
\ No newline at end of file
+ return script_map
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index 3356e584af..8dd6d03fee 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -45,6 +45,22 @@ frappe.response['message'] = 'hello'
allow_guest = 1,
script = '''
frappe.flags = 'hello'
+'''
+ ),
+ dict(
+ name='test_permission_query',
+ script_type = 'Permission Query',
+ reference_doctype = 'ToDo',
+ script = '''
+conditions = '1 = 1'
+'''),
+ dict(
+ name='test_invalid_namespace_method',
+ script_type = 'DocType Event',
+ doctype_event = 'Before Insert',
+ reference_doctype = 'Note',
+ script = '''
+frappe.method_that_doesnt_exist("do some magic")
'''
)
]
@@ -65,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')
@@ -85,3 +102,12 @@ class TestServerScript(unittest.TestCase):
def test_api_return(self):
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')
+
+ def test_permission_query(self):
+ self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', return_query=1))
+ self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list))
+
+ def test_attribute_error(self):
+ """Raise AttributeError if method not found in Namespace"""
+ note = frappe.get_doc({"doctype": "Note", "title": "Test Note: Server Script"})
+ self.assertRaises(AttributeError, note.insert)
diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js
index b6514dea9f..c0c9074cbc 100644
--- a/frappe/core/doctype/system_settings/system_settings.js
+++ b/frappe/core/doctype/system_settings/system_settings.js
@@ -1,37 +1,36 @@
-frappe.ui.form.on("System Settings", "refresh", function(frm) {
- frappe.call({
- method: "frappe.core.doctype.system_settings.system_settings.load",
- callback: function(data) {
- frappe.all_timezones = data.message.timezones;
- frm.set_df_property("time_zone", "options", frappe.all_timezones);
+frappe.ui.form.on("System Settings", {
+ refresh: function(frm) {
+ frappe.call({
+ method: "frappe.core.doctype.system_settings.system_settings.load",
+ callback: function(data) {
+ frappe.all_timezones = data.message.timezones;
+ frm.set_df_property("time_zone", "options", frappe.all_timezones);
- $.each(data.message.defaults, function(key, val) {
- frm.set_value(key, val);
- frappe.sys_defaults[key] = val;
- })
+ $.each(data.message.defaults, function(key, val) {
+ frm.set_value(key, val);
+ frappe.sys_defaults[key] = val;
+ });
+ }
+ });
+ },
+ enable_password_policy: function(frm) {
+ if (frm.doc.enable_password_policy == 0) {
+ frm.set_value("minimum_password_score", "");
+ } else {
+ frm.set_value("minimum_password_score", "2");
}
- });
-});
-
-frappe.ui.form.on("System Settings", "enable_password_policy", function(frm) {
- if(frm.doc.enable_password_policy == 0){
- frm.set_value("minimum_password_score", "");
- } else {
- frm.set_value("minimum_password_score", "2");
- }
-});
-
-frappe.ui.form.on("System Settings", "enable_two_factor_auth", function(frm) {
- if(frm.doc.enable_two_factor_auth == 0){
- frm.set_value("bypass_2fa_for_retricted_ip_users", 0);
- frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0);
- }
-});
-
-frappe.ui.form.on("System Settings", "enable_prepared_report_auto_deletion", function(frm) {
- if (frm.doc.enable_prepared_report_auto_deletion) {
- if (!frm.doc.prepared_report_expiry_period) {
- frm.set_value('prepared_report_expiry_period', 7);
+ },
+ enable_two_factor_auth: function(frm) {
+ if (frm.doc.enable_two_factor_auth == 0) {
+ frm.set_value("bypass_2fa_for_retricted_ip_users", 0);
+ frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0);
+ }
+ },
+ enable_prepared_report_auto_deletion: function(frm) {
+ if (frm.doc.enable_prepared_report_auto_deletion) {
+ if (!frm.doc.prepared_report_expiry_period) {
+ frm.set_value('prepared_report_expiry_period', 7);
+ }
}
}
});
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 17f97b3e1a..565ee373f1 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -37,6 +37,7 @@
"allow_login_using_mobile_number",
"allow_login_using_user_name",
"allow_error_traceback",
+ "strip_exif_metadata_from_uploaded_images",
"password_settings",
"logout_on_password_reset",
"force_user_to_reset_password",
@@ -356,7 +357,7 @@
"collapsible": 1,
"fieldname": "email",
"fieldtype": "Section Break",
- "label": "EMail"
+ "label": "Email"
},
{
"description": "Your organization name and address for the email footer.",
@@ -460,12 +461,18 @@
"fieldname": "prepared_report_section",
"fieldtype": "Section Break",
"label": "Prepared Report"
+ },
+ {
+ "default": "1",
+ "fieldname": "strip_exif_metadata_from_uploaded_images",
+ "fieldtype": "Check",
+ "label": "Strip EXIF tags from uploaded images"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2020-08-12 14:35:45.214327",
+ "modified": "2020-11-30 18:52:22.161391",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
@@ -483,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.py b/frappe/core/doctype/user/user.py
index 7309528da6..da4026d8fd 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -98,15 +98,16 @@ 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:
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.js b/frappe/custom/doctype/customize_form/customize_form.js
index 2d220b864c..17343573ed 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -81,6 +81,11 @@ frappe.ui.form.on("Customize Form", {
} else {
f._sortable = false;
}
+ if (f.fieldtype == "Table") {
+ frm.add_custom_button(f.options, function() {
+ frm.set_value('doc_type', f.options);
+ }, __('Customize Child Table'));
+ }
});
frm.fields_dict.fields.grid.refresh();
},
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index cf674082ab..82513783c7 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -39,7 +39,7 @@ class CustomizeForm(Document):
translation = self.get_name_translation()
self.label = translation.translated_text if translation else ''
- self.create_auto_repeat_custom_field_if_requried(meta)
+ self.create_auto_repeat_custom_field_if_required(meta)
# NOTE doc (self) is sent to clientside by run_method
@@ -74,19 +74,25 @@ class CustomizeForm(Document):
for d in meta.get(fieldname):
self.append(fieldname, d)
- def create_auto_repeat_custom_field_if_requried(self, meta):
+ def create_auto_repeat_custom_field_if_required(self, meta):
+ '''
+ Create auto repeat custom field if it's not already present
+ '''
if self.allow_auto_repeat:
- if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.doc_type}) and \
- not frappe.db.exists('DocField', {'fieldname': 'auto_repeat', 'parent': self.name}):
- insert_after = self.fields[len(self.fields) - 1].fieldname
- df = dict(
- fieldname='auto_repeat',
- label='Auto Repeat',
- fieldtype='Link',
- options='Auto Repeat',
- insert_after=insert_after,
- read_only=1, no_copy=1, print_hide=1)
- create_custom_field(self.doc_type, df)
+ all_fields = [df.fieldname for df in meta.fields]
+
+ if "auto_repeat" in all_fields:
+ return
+
+ insert_after = self.fields[len(self.fields) - 1].fieldname
+ create_custom_field(self.doc_type, dict(
+ fieldname='auto_repeat',
+ label='Auto Repeat',
+ fieldtype='Link',
+ options='Auto Repeat',
+ insert_after=insert_after,
+ read_only=1, no_copy=1, print_hide=1
+ ))
def get_name_translation(self):
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/database/db_manager.py b/frappe/database/db_manager.py
index 3345fce735..b8ffae519b 100644
--- a/frappe/database/db_manager.py
+++ b/frappe/database/db_manager.py
@@ -3,7 +3,6 @@ import frappe
class DbManager:
-
def __init__(self, db):
"""
Pass root_conn here for access to all databases.
@@ -66,10 +65,10 @@ class DbManager:
esc = make_esc('$ ')
from distutils.spawn import find_executable
- pipe = find_executable('pv')
- if pipe:
- pipe = '{pipe} {source} |'.format(
- pipe=pipe,
+ pv = find_executable('pv')
+ if pv:
+ pipe = '{pv} {source} |'.format(
+ pv=pv,
source=source
)
source = ''
@@ -78,7 +77,7 @@ class DbManager:
source = '< {source}'.format(source=source)
if pipe:
- print('Creating Database...')
+ print('Restoring Database file...')
command = '{pipe} mysql -u {user} -p{password} -h{host} ' + ('-P{port}' if frappe.db.port else '') + ' {target} {source}'
command = command.format(
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index 15b0bed699..a52efd01e3 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -233,7 +233,7 @@ CREATE TABLE `tabDocType` (
DROP TABLE IF EXISTS `tabSeries`;
CREATE TABLE `tabSeries` (
- `name` varchar(100) DEFAULT NULL,
+ `name` varchar(100),
`current` int(10) NOT NULL DEFAULT 0,
PRIMARY KEY(`name`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/database/mariadb/setup_db.py b/frappe/database/mariadb/setup_db.py
index a4e4d624ae..9b73d77171 100644
--- a/frappe/database/mariadb/setup_db.py
+++ b/frappe/database/mariadb/setup_db.py
@@ -1,7 +1,7 @@
from __future__ import unicode_literals
import frappe
-import os, sys
+import os
from frappe.database.db_manager import DbManager
expected_settings_10_2_earlier = {
@@ -86,6 +86,8 @@ def drop_user_and_database(db_name, root_login, root_password):
dbman.drop_database(db_name)
def bootstrap_database(db_name, verbose, source_sql=None):
+ import sys
+
frappe.connect(db_name=db_name)
if not check_database_settings():
print('Database settings do not match expected values; stopping database setup.')
@@ -94,9 +96,17 @@ def bootstrap_database(db_name, verbose, source_sql=None):
import_db_from_sql(source_sql, verbose)
frappe.connect(db_name=db_name)
- if not 'tabDefaultValue' in frappe.db.get_tables():
- print('''Database not installed, this can due to lack of permission, or that the database name exists.
- Check your mysql root password, or use --force to reinstall''')
+ if 'tabDefaultValue' not in frappe.db.get_tables():
+ from click import secho
+
+ secho(
+ "Table 'tabDefaultValue' missing in the restored site. "
+ "Database not installed correctly, this can due to lack of "
+ "permission, or that the database name exists. Check your mysql"
+ " root password, validity of the backup file or use --force to"
+ " reinstall",
+ fg="red"
+ )
sys.exit(1)
def import_db_from_sql(source_sql=None, verbose=False):
diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py
index f53872db82..3ee6b6a286 100644
--- a/frappe/database/postgres/setup_db.py
+++ b/frappe/database/postgres/setup_db.py
@@ -1,5 +1,7 @@
-import frappe, subprocess, os
-from six.moves import input
+import os
+
+import frappe
+
def setup_database(force, source_sql=None, verbose=False):
root_conn = get_root_connection()
@@ -10,24 +12,62 @@ def setup_database(force, source_sql=None, verbose=False):
root_conn.sql("CREATE user {0} password '{1}'".format(frappe.conf.db_name,
frappe.conf.db_password))
root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(frappe.conf.db_name))
+ root_conn.close()
+
+ bootstrap_database(frappe.conf.db_name, verbose, source_sql=source_sql)
+ frappe.connect()
+
+def bootstrap_database(db_name, verbose, source_sql=None):
+ frappe.connect(db_name=db_name)
+ import_db_from_sql(source_sql, verbose)
+ frappe.connect(db_name=db_name)
+
+ if 'tabDefaultValue' not in frappe.db.get_tables():
+ import sys
+ from click import secho
+
+ secho(
+ "Table 'tabDefaultValue' missing in the restored site. "
+ "This may be due to incorrect permissions or the result of a restore from a bad backup file. "
+ "Database not installed correctly.",
+ fg="red"
+ )
+ sys.exit(1)
+
+def import_db_from_sql(source_sql=None, verbose=False):
+ from shutil import which
+ from subprocess import run, PIPE
# we can't pass psql password in arguments in postgresql as mysql. So
# set password connection parameter in environment variable
subprocess_env = os.environ.copy()
subprocess_env['PGPASSWORD'] = str(frappe.conf.db_password)
+
# bootstrap db
if not source_sql:
source_sql = os.path.join(os.path.dirname(__file__), 'framework_postgres.sql')
- subprocess.check_output([
- 'psql', frappe.conf.db_name,
- '-h', frappe.conf.db_host or 'localhost',
- '-p', str(frappe.conf.db_port or '5432'),
- '-U', frappe.conf.db_name,
- '-f', source_sql
- ], env=subprocess_env)
+ pv = which('pv')
- frappe.connect()
+ _command = (
+ f"psql {frappe.conf.db_name} "
+ f"-h {frappe.conf.db_host or 'localhost'} -p {str(frappe.conf.db_port or '5432')} "
+ f"-U {frappe.conf.db_name}"
+ )
+
+ if pv:
+ command = f"{pv} {source_sql} | " + _command
+ else:
+ command = _command + f" -f {source_sql}"
+
+ print("Restoring Database file...")
+ if verbose:
+ print(command)
+
+ restore_proc = run(command, env=subprocess_env, shell=True, stdout=PIPE)
+
+ if verbose:
+ print(f"\nSTDOUT by psql:\n{restore_proc.stdout.decode()}\nImported from Database File: {source_sql}")
def setup_help_database(help_db_name):
root_conn = get_root_connection()
@@ -38,19 +78,20 @@ def setup_help_database(help_db_name):
root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(help_db_name))
def get_root_connection(root_login=None, root_password=None):
- import getpass
if not frappe.local.flags.root_connection:
if not root_login:
root_login = frappe.conf.get("root_login") or None
if not root_login:
+ from six.moves import input
root_login = input("Enter postgres super user: ")
if not root_password:
root_password = frappe.conf.get("root_password") or None
if not root_password:
- root_password = getpass.getpass("Postgres super user password: ")
+ from getpass import getpass
+ root_password = getpass("Postgres super user password: ")
frappe.local.flags.root_connection = frappe.database.get_db(user=root_login, password=root_password)
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index b12bcfe27d..fa03bf8f80 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -5,6 +5,7 @@
from __future__ import unicode_literals
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
+from frappe.config import get_modules_from_all_apps_for_user
import frappe
from frappe import _
import json
@@ -42,6 +43,24 @@ class Dashboard(Document):
except ValueError as error:
frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
+
+def get_permission_query_conditions(user):
+ if not user:
+ user = frappe.session.user
+
+ if user == 'Administrator':
+ return
+
+ roles = frappe.get_roles(user)
+ if "System Manager" in roles:
+ return None
+
+ allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
+ module_condition = '`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL'.format(
+ allowed_modules=','.join(allowed_modules))
+
+ return module_condition
+
@frappe.whitelist()
def get_permitted_charts(dashboard_name):
permitted_charts = []
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 7e2d952928..2fa36b5514 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -7,17 +7,18 @@ import frappe
from frappe import _
import datetime
import json
-from frappe.utils.dashboard import cache_source, get_from_date_from_timespan
-from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate,\
- get_datetime, cint, now_datetime
+from frappe.utils.dashboard import cache_source
+from frappe.utils import nowdate, getdate, get_datetime, cint, now_datetime
+from frappe.utils.dateutils import\
+ get_period, get_period_beginning, get_from_date_from_timespan, get_dates_from_timegrain
from frappe.model.naming import append_number_if_name_exists
from frappe.boot import get_allowed_reports
+from frappe.config import get_modules_from_all_apps_for_user
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
def get_permission_query_conditions(user):
-
if not user:
user = frappe.session.user
@@ -30,9 +31,11 @@ def get_permission_query_conditions(user):
doctype_condition = False
report_condition = False
+ module_condition = False
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
allowed_reports = [frappe.db.escape(key) if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()]
+ allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
if allowed_doctypes:
doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format(
@@ -40,18 +43,24 @@ def get_permission_query_conditions(user):
if allowed_reports:
report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format(
allowed_reports=','.join(allowed_reports))
+ if allowed_modules:
+ module_condition = '''`tabDashboard Chart`.`module` in ({allowed_modules})
+ or `tabDashboard Chart`.`module` is NULL'''.format(
+ allowed_modules=','.join(allowed_modules))
return '''
- (`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
- and {doctype_condition})
- or
- (`tabDashboard Chart`.`chart_type` = 'Report'
- and {report_condition})
- '''.format(
- doctype_condition=doctype_condition,
- report_condition=report_condition
- )
-
+ ((`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
+ and {doctype_condition})
+ or
+ (`tabDashboard Chart`.`chart_type` = 'Report'
+ and {report_condition}))
+ and
+ ({module_condition})
+ '''.format(
+ doctype_condition=doctype_condition,
+ report_condition=report_condition,
+ module_condition=module_condition
+ )
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)
@@ -156,6 +165,7 @@ def add_chart_to_dashboard(args):
def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
if not from_date:
from_date = get_from_date_from_timespan(to_date, timespan)
+ from_date = get_period_beginning(from_date, timegrain)
if not to_date:
to_date = now_datetime()
@@ -185,7 +195,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
result = get_result(data, timegrain, from_date, to_date)
chart_config = {
- "labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
+ "labels": [get_period(r[0], timegrain) for r in result],
"datasets": [{
"name": chart.name,
"values": [r[1] for r in result]
@@ -279,16 +289,8 @@ def get_aggregate_function(chart_type):
def get_result(data, timegrain, from_date, to_date):
- start_date = getdate(from_date)
- end_date = getdate(to_date)
-
- result = [[start_date, 0.0]]
-
- while start_date < end_date:
- next_date = get_next_expected_date(start_date, timegrain)
- result.append([next_date, 0.0])
- start_date = next_date
-
+ dates = get_dates_from_timegrain(from_date, to_date, timegrain)
+ result = [[date, 0] for date in dates]
data_index = 0
if data:
for i, d in enumerate(result):
@@ -298,65 +300,6 @@ def get_result(data, timegrain, from_date, to_date):
return result
-def get_next_expected_date(date, timegrain):
- next_date = None
- # given date is always assumed to be the period ending date
- next_date = get_period_ending(add_to_date(date, days=1), timegrain)
- return getdate(next_date)
-
-def get_period_ending(date, timegrain):
- date = getdate(date)
- if timegrain == 'Daily':
- pass
- elif timegrain == 'Weekly':
- date = get_week_ending(date)
- elif timegrain == 'Monthly':
- date = get_month_ending(date)
- elif timegrain == 'Quarterly':
- date = get_quarter_ending(date)
- elif timegrain == 'Yearly':
- date = get_year_ending(date)
-
- return getdate(date)
-
-def get_week_ending(date):
- # week starts on monday
- from datetime import timedelta
- start = date - timedelta(days = date.weekday())
- end = start + timedelta(days=6)
-
- return end
-
-def get_month_ending(date):
- month_of_the_year = int(date.strftime('%m'))
- # first day of next month (note month starts from 1)
-
- date = add_to_date('{}-01-01'.format(date.year), months = month_of_the_year)
- # last day of this month
- return add_to_date(date, days=-1)
-
-def get_quarter_ending(date):
- date = getdate(date)
-
- # find the earliest quarter ending date that is after
- # the given date
- for month in (3, 6, 9, 12):
- quarter_end_month = getdate('{}-{}-01'.format(date.year, month))
- quarter_end_date = getdate(get_last_day(quarter_end_month))
- if date <= quarter_end_date:
- date = quarter_end_date
- break
-
- return date
-
-def get_year_ending(date):
- ''' returns year ending of the given date '''
-
- # first day of next year (note year starts from 1)
- date = add_to_date('{}-01-01'.format(date.year), months = 12)
- # last day of this month
- return add_to_date(date, days=-1)
-
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
index 5e39998e62..3c37ad4a09 100644
--- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
@@ -5,8 +5,8 @@ from __future__ import unicode_literals
import unittest, frappe
from frappe.utils import getdate, formatdate, get_last_day
-from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get,
- get_period_ending)
+from frappe.utils.dateutils import get_period_ending, get_period
+from frappe.desk.doctype.dashboard_chart.dashboard_chart import get
from datetime import datetime
from dateutil.relativedelta import relativedelta
@@ -53,15 +53,11 @@ class TestDashboardChart(unittest.TestCase):
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name='Test Dashboard Chart', refresh=1)
- self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
- if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
- cur_date += relativedelta(months=1)
-
- for idx in range(1, 13):
+ for idx in range(13):
month = get_last_day(cur_date)
month = formatdate(month.strftime('%Y-%m-%d'))
- self.assertEqual(result.get('labels')[idx], month)
+ self.assertEqual(result.get('labels')[idx], get_period(month))
cur_date += relativedelta(months=1)
frappe.db.rollback()
@@ -87,15 +83,11 @@ class TestDashboardChart(unittest.TestCase):
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name ='Test Empty Dashboard Chart', refresh=1)
- self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
- if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
- cur_date += relativedelta(months=1)
-
- for idx in range(1, 13):
+ for idx in range(13):
month = get_last_day(cur_date)
month = formatdate(month.strftime('%Y-%m-%d'))
- self.assertEqual(result.get('labels')[idx], month)
+ self.assertEqual(result.get('labels')[idx], get_period(month))
cur_date += relativedelta(months=1)
frappe.db.rollback()
@@ -124,15 +116,11 @@ class TestDashboardChart(unittest.TestCase):
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1)
- self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
- if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
- cur_date += relativedelta(months=1)
-
- for idx in range(1, 13):
+ for idx in range(13):
month = get_last_day(cur_date)
month = formatdate(month.strftime('%Y-%m-%d'))
- self.assertEqual(result.get('labels')[idx], month)
+ self.assertEqual(result.get('labels')[idx], get_period(month))
cur_date += relativedelta(months=1)
# only 1 data point with value
@@ -183,13 +171,12 @@ class TestDashboardChart(unittest.TestCase):
timeseries = 1
)).insert()
- result = get(chart_name ='Test Daily Dashboard Chart', refresh = 1)
+ result = get(chart_name = 'Test Daily Dashboard Chart', refresh = 1)
self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 300.0, 0.0, 100.0, 0.0])
self.assertEqual(
result.get('labels'),
- [formatdate('2019-01-06'), formatdate('2019-01-07'), formatdate('2019-01-08'),\
- formatdate('2019-01-09'), formatdate('2019-01-10'), formatdate('2019-01-11')]
+ ['06-01-19', '07-01-19', '08-01-19', '09-01-19', '10-01-19', '11-01-19']
)
frappe.db.rollback()
@@ -218,7 +205,10 @@ class TestDashboardChart(unittest.TestCase):
result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1)
self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0])
- self.assertEqual(result.get('labels'), [formatdate('2018-12-30'), formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
+ self.assertEqual(
+ result.get('labels'),
+ ['30-12-18', '06-01-19', '13-01-19', '20-01-19']
+ )
frappe.db.rollback()
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index d4a2b00c57..6bddd09fc7 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -8,6 +8,7 @@ from frappe.model.document import Document
from frappe.utils import cint
from frappe.model.naming import append_number_if_name_exists
from frappe.modules.export_file import export_to_files
+from frappe.config import get_modules_from_all_apps_for_user
class NumberCard(Document):
def autoname(self):
@@ -33,16 +34,24 @@ def get_permission_query_conditions(user=None):
return None
doctype_condition = False
+ module_condition = False
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
+ allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
if allowed_doctypes:
doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format(
allowed_doctypes=','.join(allowed_doctypes))
+ if allowed_modules:
+ module_condition = '''`tabNumber Card`.`module` in ({allowed_modules})
+ or `tabNumber Card`.`module` is NULL'''.format(
+ allowed_modules=','.join(allowed_modules))
return '''
- {doctype_condition}
- '''.format(doctype_condition=doctype_condition)
+ {doctype_condition}
+ and
+ {module_condition}
+ '''.format(doctype_condition=doctype_condition, module_condition=module_condition)
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)
diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py
index 3aa3a4fa88..66164948f2 100644
--- a/frappe/desk/form/document_follow.py
+++ b/frappe/desk/form/document_follow.py
@@ -21,7 +21,7 @@ def follow_document(doctype, doc_name, user, force=False):
avoided for some doctype
follow only if track changes are set to 1
'''
- if (doctype in ("Communication", "ToDo", "Email Unsubscribe", "File", "Comment")
+ if (doctype in ("Communication", "ToDo", "Email Unsubscribe", "File", "Comment", "Email Account", "Email Domain")
or doctype in log_types):
return
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index f249c36746..f4e6543844 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -150,7 +150,8 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
# 2 is the index of _relevance column
order_by = "_relevance, {0}, `tab{1}`.idx desc".format(order_by_based_on_meta, doctype)
- ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype))
+ ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
+ ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype))
if doctype in UNTRANSLATED_DOCTYPES:
page_length = None
diff --git a/frappe/email/doctype/email_template/email_template.json b/frappe/email/doctype/email_template/email_template.json
index 0d0922f16f..dc73acacc1 100644
--- a/frappe/email/doctype/email_template/email_template.json
+++ b/frappe/email/doctype/email_template/email_template.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
@@ -8,6 +9,8 @@
"engine": "InnoDB",
"field_order": [
"subject",
+ "use_html",
+ "response_html",
"response",
"owner",
"section_break_4",
@@ -22,11 +25,12 @@
"reqd": 1
},
{
+ "depends_on": "eval:!doc.use_html",
"fieldname": "response",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Response",
- "reqd": 1
+ "mandatory_depends_on": "eval:!doc.use_html"
},
{
"default": "user",
@@ -45,10 +49,24 @@
"fieldtype": "HTML",
"label": "Email Reply Help",
"options": "Email Reply Example \n\nOrder Overdue\n\nTransaction {{ name }} has exceeded Due Date. Please take necessary action.\n\nDetails\n\n- Customer: {{ customer }}\n- Amount: {{ grand_total }}\n \n\nHow to get fieldnames \n\nThe fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)
\n\nTemplating \n\nTemplates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.
\n"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_html",
+ "fieldtype": "Check",
+ "label": "Use HTML"
+ },
+ {
+ "depends_on": "eval:doc.use_html",
+ "fieldname": "response_html",
+ "fieldtype": "Code",
+ "label": "Response ",
+ "options": "HTML"
}
],
"icon": "fa fa-comment",
- "modified": "2019-10-30 14:15:00.956347",
+ "links": [],
+ "modified": "2020-11-30 14:12:50.321633",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Template",
diff --git a/frappe/email/doctype/email_template/email_template.py b/frappe/email/doctype/email_template/email_template.py
index 2743032331..6708e9dd3f 100644
--- a/frappe/email/doctype/email_template/email_template.py
+++ b/frappe/email/doctype/email_template/email_template.py
@@ -9,7 +9,29 @@ from six import string_types
class EmailTemplate(Document):
def validate(self):
- validate_template(self.response)
+ if self.use_html:
+ validate_template(self.response_html)
+ else:
+ validate_template(self.response)
+
+ def get_formatted_subject(self, doc):
+ return frappe.render_template(self.subject, doc)
+
+ def get_formatted_response(self, doc):
+ if self.use_html:
+ return frappe.render_template(self.response_html, doc)
+
+ return frappe.render_template(self.response, doc)
+
+ def get_formatted_email(self, doc):
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+
+ return {
+ "subject" : self.get_formatted_subject(doc),
+ "message" : self.get_formatted_response(doc)
+ }
+
@frappe.whitelist()
def get_email_template(template_name, doc):
@@ -18,5 +40,4 @@ def get_email_template(template_name, doc):
doc = json.loads(doc)
email_template = frappe.get_doc("Email Template", template_name)
- return {"subject" : frappe.render_template(email_template.subject, doc),
- "message" : frappe.render_template(email_template.response, doc)}
\ No newline at end of file
+ return email_template.get_formatted_email(doc)
\ No newline at end of file
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index a4d60706eb..2791ebb75b 100755
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -85,11 +85,11 @@ class Newsletter(WebsiteGenerator):
self.db_set("scheduled_to_send", len(self.recipients))
def get_message(self):
-
+ if self.content_type == "HTML":
+ return frappe.render_template(self.message_html, {"doc": self.as_dict()})
return {
'Rich Text': self.message,
- 'Markdown': markdown(self.message_md),
- 'HTML': self.message_html
+ 'Markdown': markdown(self.message_md)
}[self.content_type or 'Rich Text']
def get_recipients(self):
diff --git a/frappe/email/doctype/notification/notification.json b/frappe/email/doctype/notification/notification.json
index 73a84e1d3e..c1c877efd4 100644
--- a/frappe/email/doctype/notification/notification.json
+++ b/frappe/email/doctype/notification/notification.json
@@ -207,7 +207,7 @@
"label": "Value To Be Set"
},
{
- "depends_on": "eval:in_list(['Email', 'SMS'], doc.channel)",
+ "depends_on": "eval:doc.channel !=\"Slack\"",
"fieldname": "column_break_5",
"fieldtype": "Section Break",
"label": "Recipients"
@@ -281,7 +281,7 @@
"icon": "fa fa-envelope",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-10-28 11:04:54.955567",
+ "modified": "2020-11-24 14:25:43.245677",
"modified_by": "Administrator",
"module": "Email",
"name": "Notification",
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index 75281d427e..2ea7a3785e 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -181,6 +181,7 @@ def get_context(context):
'document_type': doc.doctype,
'document_name': doc.name,
'subject': subject,
+ 'from_user': doc.modified_by or doc.owner,
'email_content': frappe.render_template(self.message, context),
'attached_file': attachments and json.dumps(attachments[0])
}
diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py
index f53b835757..9ba81fa146 100644
--- a/frappe/email/smtp.py
+++ b/frappe/email/smtp.py
@@ -210,10 +210,9 @@ class SMTPServer:
try:
if self.use_ssl:
if not self.port:
- self.smtp_port = 465
+ self.port = 465
- self._sess = smtplib.SMTP_SSL((self.server or "").encode('utf-8'),
- cint(self.port) or None)
+ self._sess = smtplib.SMTP_SSL((self.server or ""), cint(self.port))
else:
if self.use_tls and not self.port:
self.port = 587
diff --git a/frappe/email/test_smtp.py b/frappe/email/test_smtp.py
new file mode 100644
index 0000000000..869d708430
--- /dev/null
+++ b/frappe/email/test_smtp.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# License: The MIT License
+
+import unittest
+from frappe.email.smtp import SMTPServer
+
+class TestSMTP(unittest.TestCase):
+ def test_smtp_ssl_session(self):
+ for port in [None, 0, 465, "465"]:
+ make_server(port, 1, 0)
+
+ def test_smtp_tls_session(self):
+ for port in [None, 0, 587, "587"]:
+ make_server(port, 0, 1)
+
+
+def make_server(port, ssl, tls):
+ server = SMTPServer(
+ server = "smtp.gmail.com",
+ port = port,
+ use_ssl = ssl,
+ use_tls = tls
+ )
+
+ server.sess
\ No newline at end of file
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index 267f5410af..82fbff7a90 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -106,8 +106,10 @@ class InvalidDates(ValidationError): pass
class DataTooLongException(ValidationError): pass
class FileAlreadyAttachedException(Exception): pass
class DocumentAlreadyRestored(Exception): pass
+class AttachmentLimitReached(Exception): pass
# OAuth exceptions
class InvalidAuthorizationHeader(CSRFTokenError): pass
class InvalidAuthorizationPrefix(CSRFTokenError): pass
class InvalidAuthorizationToken(CSRFTokenError): pass
-class InvalidDatabaseFile(ValidationError): pass
\ No newline at end of file
+class InvalidDatabaseFile(ValidationError): pass
+class ExecutableNotFound(FileNotFoundError): pass
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 d8c8cd841c..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"
@@ -94,6 +94,7 @@ permission_query_conditions = {
"User": "frappe.core.doctype.user.user.get_permission_query_conditions",
"Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions",
"Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions",
+ "Dashboard": "frappe.desk.doctype.dashboard.dashboard.get_permission_query_conditions",
"Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions",
"Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions",
"Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions",
diff --git a/frappe/installer.py b/frappe/installer.py
index be9b04d453..a11c8dfbfa 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -3,8 +3,90 @@
import json
import os
-from frappe.defaults import _clear_cache
+import sys
+
import frappe
+from frappe.defaults import _clear_cache
+
+
+def _new_site(
+ db_name,
+ site,
+ mariadb_root_username=None,
+ mariadb_root_password=None,
+ admin_password=None,
+ verbose=False,
+ install_apps=None,
+ source_sql=None,
+ force=False,
+ no_mariadb_socket=False,
+ reinstall=False,
+ db_password=None,
+ db_type=None,
+ db_host=None,
+ db_port=None,
+ new_site=False,
+):
+ """Install a new Frappe site"""
+
+ if not force and os.path.exists(site):
+ print("Site {0} already exists".format(site))
+ sys.exit(1)
+
+ if no_mariadb_socket and not db_type == "mariadb":
+ print("--no-mariadb-socket requires db_type to be set to mariadb.")
+ sys.exit(1)
+
+ if not db_name:
+ import hashlib
+ db_name = "_" + hashlib.sha1(site.encode()).hexdigest()[:16]
+
+ frappe.init(site=site)
+
+ from frappe.commands.scheduler import _is_scheduler_enabled
+ from frappe.utils import get_site_path, scheduler, touch_file
+
+ try:
+ # enable scheduler post install?
+ enable_scheduler = _is_scheduler_enabled()
+ except Exception:
+ enable_scheduler = False
+
+ make_site_dirs()
+
+ installing = touch_file(get_site_path("locks", "installing.lock"))
+
+ install_db(
+ root_login=mariadb_root_username,
+ root_password=mariadb_root_password,
+ db_name=db_name,
+ admin_password=admin_password,
+ verbose=verbose,
+ source_sql=source_sql,
+ force=force,
+ reinstall=reinstall,
+ db_password=db_password,
+ db_type=db_type,
+ db_host=db_host,
+ db_port=db_port,
+ no_mariadb_socket=no_mariadb_socket,
+ )
+ apps_to_install = (
+ ["frappe"] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
+ )
+
+ for app in apps_to_install:
+ install_app(app, verbose=verbose, set_as_patched=not source_sql)
+
+ os.remove(installing)
+
+ scheduler.toggle_scheduler(enable_scheduler)
+ frappe.db.commit()
+
+ scheduler_status = (
+ "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
+ )
+ print("*** Scheduler is", scheduler_status, "***")
def install_db(root_login="root", root_password=None, db_name=None, source_sql=None,
@@ -36,9 +118,9 @@ def install_db(root_login="root", root_password=None, db_name=None, source_sql=N
def install_app(name, verbose=False, set_as_patched=True):
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
- from frappe.utils.fixtures import sync_fixtures
from frappe.model.sync import sync_for
from frappe.modules.utils import sync_customizations
+ from frappe.utils.fixtures import sync_fixtures
frappe.flags.in_install = name
frappe.flags.ignore_in_install = False
@@ -180,11 +262,11 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
for record in frappe.get_all(doctype, filters={"module": module_name}, pluck="name"):
print(f"* removing {doctype} '{record}'...")
if not dry_run:
- frappe.delete_doc(doctype, record, ignore_on_trash=True)
+ frappe.delete_doc(doctype, record, ignore_on_trash=True, force=True)
print(f"* removing Module Def '{module_name}'...")
if not dry_run:
- frappe.delete_doc("Module Def", module_name, ignore_on_trash=True)
+ frappe.delete_doc("Module Def", module_name, ignore_on_trash=True, force=True)
for doctype in set(drop_doctypes):
print(f"* dropping Table for '{doctype}'...")
@@ -347,6 +429,28 @@ def remove_missing_apps():
frappe.db.set_global("installed_apps", json.dumps(installed_apps))
+def extract_sql_from_archive(sql_file_path):
+ """Return the path of an SQL file if the passed argument is the path of a gzipped
+ SQL file or an SQL file path. The path may be absolute or relative from the bench
+ root directory or the sites sub-directory.
+
+ Args:
+ sql_file_path (str): Path of the SQL file
+
+ Returns:
+ str: Path of the decompressed SQL file
+ """
+ from frappe.utils import get_bench_relative_path
+ sql_file_path = get_bench_relative_path(sql_file_path)
+ # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
+ if sql_file_path.endswith('sql.gz'):
+ decompressed_file_name = extract_sql_gzip(sql_file_path)
+ else:
+ decompressed_file_name = sql_file_path
+
+ return decompressed_file_name
+
+
def extract_sql_gzip(sql_gz_path):
import subprocess
@@ -361,9 +465,13 @@ def extract_sql_gzip(sql_gz_path):
return decompressed_file
-def extract_files(site_name, file_path, folder_name):
- import subprocess
+
+def extract_files(site_name, file_path):
import shutil
+ import subprocess
+ from frappe.utils import get_bench_relative_path
+
+ file_path = get_bench_relative_path(file_path)
# Need to do frappe.init to maintain the site locals
frappe.init(site=site_name)
@@ -391,6 +499,12 @@ def extract_files(site_name, file_path, folder_name):
def is_downgrade(sql_file_path, verbose=False):
"""checks if input db backup will get downgraded on current bench"""
+
+ # This function is only tested with mariadb
+ # TODO: Add postgres support
+ if frappe.conf.db_type not in (None, "mariadb"):
+ return False
+
from semantic_version import Version
head = "INSERT INTO `tabInstalled Application` VALUES"
@@ -424,6 +538,37 @@ def is_downgrade(sql_file_path, verbose=False):
return downgrade
+def is_partial(sql_file_path):
+ with open(sql_file_path) as f:
+ header = " ".join([f.readline() for _ in range(5)])
+ if "Partial Backup" in header:
+ return True
+ return False
+
+
+def partial_restore(sql_file_path, verbose=False):
+ sql_file = extract_sql_from_archive(sql_file_path)
+
+ if frappe.conf.db_type in (None, "mariadb"):
+ from frappe.database.mariadb.setup_db import import_db_from_sql
+ elif frappe.conf.db_type == "postgres":
+ from frappe.database.postgres.setup_db import import_db_from_sql
+ import warnings
+ from click import style
+ warn = style(
+ "Delete the tables you want to restore manually before attempting"
+ " partial restore operation for PostreSQL databases",
+ fg="yellow"
+ )
+ warnings.warn(warn)
+
+ import_db_from_sql(source_sql=sql_file, verbose=verbose)
+
+ # Removing temporarily created file
+ if sql_file != sql_file_path:
+ os.remove(sql_file)
+
+
def validate_database_sql(path, _raise=True):
"""Check if file has contents and if DefaultValue table exists
@@ -431,23 +576,29 @@ def validate_database_sql(path, _raise=True):
path (str): Path of the decompressed SQL file
_raise (bool, optional): Raise exception if invalid file. Defaults to True.
"""
- to_raise = False
+ empty_file = False
+ missing_table = True
+
error_message = ""
if not os.path.getsize(path):
error_message = f"{path} is an empty file!"
- to_raise = True
+ empty_file = True
- if not _raise:
+ # dont bother checking if empty file
+ if not empty_file:
with open(path, "r") as f:
for line in f:
if 'tabDefaultValue' in line:
- error_message = "Table `tabDefaultValue` not found in file."
- to_raise = True
+ missing_table = False
+ break
+
+ if missing_table:
+ error_message = "Table `tabDefaultValue` not found in file."
if error_message:
import click
click.secho(error_message, fg="red")
- if _raise and to_raise:
+ if _raise and (missing_table or empty_file):
raise frappe.InvalidDatabaseFile
diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
index 6b95a3f5bf..71445b44d7 100644
--- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
+++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
@@ -9,7 +9,7 @@ import frappe
import os
from frappe import _
from frappe.model.document import Document
-from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size
+from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size, get_chunk_site
from frappe.integrations.utils import make_post_request
from frappe.utils import (cint, get_request_site_address,
get_files_path, get_backups_path, get_url, encode)
@@ -167,8 +167,9 @@ def upload_file_to_dropbox(filename, folder, dropbox_client):
return
create_folder_if_not_exists(folder, dropbox_client)
- chunk_size = 15 * 1024 * 1024
file_size = os.path.getsize(encode(filename))
+ chunk_size = get_chunk_site(file_size)
+
mode = (dropbox.files.WriteMode.overwrite)
f = open(encode(filename), 'rb')
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/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/frappe_providers/frappecloud.py b/frappe/integrations/frappe_providers/frappecloud.py
index e09f09a44b..f60344ee8f 100644
--- a/frappe/integrations/frappe_providers/frappecloud.py
+++ b/frappe/integrations/frappe_providers/frappecloud.py
@@ -6,7 +6,7 @@ import frappe
def frappecloud_migrator(local_site):
- print("Retreiving Site Migrator...")
+ print("Retrieving Site Migrator...")
remote_site = frappe.conf.frappecloud_url or "frappecloud.com"
request_url = "https://{}/api/method/press.api.script".format(remote_site)
request = requests.get(request_url)
diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py
index c8dfc52c95..a750c8328c 100644
--- a/frappe/integrations/oauth2.py
+++ b/frappe/integrations/oauth2.py
@@ -1,41 +1,50 @@
from __future__ import unicode_literals
-import frappe, json
-from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer
+
+import hashlib
+import json
+from urllib.parse import quote, urlencode, urlparse
+
+import jwt
from oauthlib.oauth2 import FatalClientError, OAuth2Error
-from werkzeug import url_fix
-from six.moves.urllib.parse import quote, urlencode, urlparse
-from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings
+
+import frappe
from frappe import _
+from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer
+from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings
def get_oauth_server():
if not getattr(frappe.local, 'oauth_server', None):
oauth_validator = OAuthWebRequestValidator()
- frappe.local.oauth_server = WebApplicationServer(oauth_validator)
+ frappe.local.oauth_server = WebApplicationServer(oauth_validator)
return frappe.local.oauth_server
-def get_urlparams_from_kwargs(param_kwargs):
+def sanitize_kwargs(param_kwargs):
arguments = param_kwargs
- if arguments.get("data"):
- arguments.pop("data")
- if arguments.get("cmd"):
- arguments.pop("cmd")
+ arguments.pop('data', None)
+ arguments.pop('cmd', None)
- return urlencode(arguments)
+ return arguments
@frappe.whitelist()
def approve(*args, **kwargs):
r = frappe.request
- uri = url_fix(r.url.replace("+"," "))
- http_method = r.method
- body = r.get_data()
- headers = r.headers
try:
- scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(uri, http_method, body, headers)
+ scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(
+ r.url,
+ r.method,
+ r.get_data(),
+ r.headers
+ )
- headers, body, status = get_oauth_server().create_authorization_response(uri=frappe.flags.oauth_credentials['redirect_uri'], \
- body=body, headers=headers, scopes=scopes, credentials=frappe.flags.oauth_credentials)
+ headers, body, status = get_oauth_server().create_authorization_response(
+ uri=frappe.flags.oauth_credentials['redirect_uri'],
+ body=r.get_data(),
+ headers=r.headers,
+ scopes=scopes,
+ credentials=frappe.flags.oauth_credentials
+ )
uri = headers.get('Location', None)
frappe.local.response["type"] = "redirect"
@@ -47,34 +56,28 @@ def approve(*args, **kwargs):
return e
@frappe.whitelist(allow_guest=True)
-def authorize(*args, **kwargs):
- #Fetch provider URL from settings
- oauth_settings = get_oauth_settings()
- params = get_urlparams_from_kwargs(kwargs)
- request_url = urlparse(frappe.request.url)
- success_url = request_url.scheme + "://" + request_url.netloc + "/api/method/frappe.integrations.oauth2.approve?" + params
+def authorize(**kwargs):
+ success_url = "/api/method/frappe.integrations.oauth2.approve?" + encode_params(sanitize_kwargs(kwargs))
failure_url = frappe.form_dict["redirect_uri"] + "?error=access_denied"
- if frappe.session['user']=='Guest':
+ if frappe.session.user == 'Guest':
#Force login, redirect to preauth again.
frappe.local.response["type"] = "redirect"
- frappe.local.response["location"] = "/login?redirect-to=/api/method/frappe.integrations.oauth2.authorize?" + quote(params.replace("+"," "))
-
- elif frappe.session['user']!='Guest':
+ frappe.local.response["location"] = "/login?" + encode_params({'redirect-to': frappe.request.url})
+ else:
try:
r = frappe.request
- uri = url_fix(r.url)
- http_method = r.method
- body = r.get_data()
- headers = r.headers
-
- scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(uri, http_method, body, headers)
+ scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(
+ r.url,
+ r.method,
+ r.get_data(),
+ r.headers
+ )
skip_auth = frappe.db.get_value("OAuth Client", frappe.flags.oauth_credentials['client_id'], "skip_authorization")
unrevoked_tokens = frappe.get_all("OAuth Bearer Token", filters={"status":"Active"})
- if skip_auth or (oauth_settings["skip_authorization"] == "Auto" and len(unrevoked_tokens)):
-
+ if skip_auth or (get_oauth_settings().skip_authorization == "Auto" and unrevoked_tokens):
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = success_url
else:
@@ -87,7 +90,6 @@ def authorize(*args, **kwargs):
})
resp_html = frappe.render_template("templates/includes/oauth_confirmation.html", response_html_params)
frappe.respond_as_web_page("Confirm Access", resp_html)
-
except FatalClientError as e:
return e
except OAuth2Error as e:
@@ -95,20 +97,20 @@ def authorize(*args, **kwargs):
@frappe.whitelist(allow_guest=True)
def get_token(*args, **kwargs):
- r = frappe.request
-
- uri = url_fix(r.url)
- http_method = r.method
- body = r.form
- headers = r.headers
-
#Check whether frappe server URL is set
frappe_server_url = frappe.db.get_value("Social Login Key", "frappe", "base_url") or None
if not frappe_server_url:
frappe.throw(_("Please set Base URL in Social Login Key for Frappe"))
try:
- headers, body, status = get_oauth_server().create_token_response(uri, http_method, body, headers, frappe.flags.oauth_credentials)
+ r = frappe.request
+ headers, body, status = get_oauth_server().create_token_response(
+ r.url,
+ r.method,
+ r.form,
+ r.headers,
+ frappe.flags.oauth_credentials
+ )
out = frappe._dict(json.loads(body))
if not out.error and "openid" in out.scope:
token_user = frappe.db.get_value("OAuth Bearer Token", out.access_token, "user")
@@ -116,7 +118,7 @@ def get_token(*args, **kwargs):
client_secret = frappe.db.get_value("OAuth Client", token_client, "client_secret")
if token_user in ["Guest", "Administrator"]:
frappe.throw(_("Logged in as Guest or Administrator"))
- import hashlib
+
id_token_header = {
"typ":"jwt",
"alg":"HS256"
@@ -128,9 +130,10 @@ def get_token(*args, **kwargs):
"iss": frappe_server_url,
"at_hash": frappe.oauth.calculate_at_hash(out.access_token, hashlib.sha256)
}
- import jwt
+
id_token_encoded = jwt.encode(id_token, client_secret, algorithm='HS256', headers=id_token_header)
- out.update({"id_token":str(id_token_encoded)})
+ out.update({"id_token": str(id_token_encoded)})
+
frappe.local.response = out
except FatalClientError as e:
@@ -140,12 +143,12 @@ def get_token(*args, **kwargs):
@frappe.whitelist(allow_guest=True)
def revoke_token(*args, **kwargs):
r = frappe.request
- uri = url_fix(r.url)
- http_method = r.method
- body = r.form
- headers = r.headers
-
- headers, body, status = get_oauth_server().create_revocation_response(uri, headers=headers, body=body, http_method=http_method)
+ headers, body, status = get_oauth_server().create_revocation_response(
+ r.url,
+ headers=r.headers,
+ body=r.form,
+ http_method=r.method
+ )
frappe.local.response['http_status_code'] = status
if status == 200:
@@ -174,15 +177,22 @@ def openid_profile(*args, **kwargs):
"email": name,
"picture": picture
})
-
+
frappe.local.response = user_profile
def validate_url(url_string):
try:
result = urlparse(url_string)
- if result.scheme and result.scheme in ["http", "https", "ftp", "ftps"]:
- return True
- else:
- return False
+ return result.scheme and result.scheme in ["http", "https", "ftp", "ftps"]
except:
- return False
\ No newline at end of file
+ return False
+
+def encode_params(params):
+ """
+ Encode a dict of params into a query string.
+
+ Use `quote_via=urllib.parse.quote` so that whitespaces will be encoded as
+ `%20` instead of as `+`. This is needed because oauthlib cannot handle `+`
+ as a whitespace.
+ """
+ return urlencode(params, quote_via=quote)
diff --git a/frappe/integrations/offsite_backup_utils.py b/frappe/integrations/offsite_backup_utils.py
index db176538e4..48a2c89107 100644
--- a/frappe/integrations/offsite_backup_utils.py
+++ b/frappe/integrations/offsite_backup_utils.py
@@ -6,8 +6,7 @@ from __future__ import unicode_literals
import frappe
import glob
import os
-from frappe.utils import split_emails, get_backups_path
-
+from frappe.utils import split_emails, cint
def send_email(success, service_name, doctype, email_field, error_status=None):
recipients = get_recipients(doctype, email_field)
@@ -81,6 +80,22 @@ def get_file_size(file_path, unit):
return file_size
+def get_chunk_site(file_size):
+ ''' this function will return chunk size in megabytes based on file size '''
+
+ file_size_in_gb = cint(file_size/1024/1024)
+
+ MB = 1024 * 1024
+ if file_size_in_gb > 5000:
+ return 200 * MB
+ elif file_size_in_gb >= 3000:
+ return 150 * MB
+ elif file_size_in_gb >= 1000:
+ return 100 * MB
+ elif file_size_in_gb >= 500:
+ return 50 * MB
+ else:
+ return 15 * MB
def validate_file_size():
frappe.flags.create_new_backup = True
@@ -98,4 +113,4 @@ def generate_files_backup():
db_type=frappe.conf.db_type, db_port=frappe.conf.db_port)
backup.set_backup_file_name()
- backup.zip_files()
\ No newline at end of file
+ backup.zip_files()
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 0a219b4253..5d86b3bac8 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -802,12 +802,12 @@ class BaseDocument(object):
if translated:
val = _(val)
- if absolute_value and isinstance(val, (int, float)):
- val = abs(self.get(fieldname))
-
if not doc:
doc = getattr(self, "parent_doc", None) or self
+ if (absolute_value or doc.get('absolute_value')) and isinstance(val, (int, float)):
+ val = abs(self.get(fieldname))
+
return format_value(val, df=df, doc=doc, currency=currency)
def is_print_hide(self, fieldname, df=None, for_print=True):
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index ace9b04cec..c799586d61 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -18,6 +18,7 @@ from frappe.client import check_parent_permission
from frappe.model.utils.user_settings import get_user_settings, update_user_settings
from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, get_timespan_date_range
from frappe.model.meta import get_table_columns
+from frappe.core.doctype.server_script.server_script_utils import get_server_script_map
class DatabaseQuery(object):
def __init__(self, doctype, user=None):
@@ -39,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)
@@ -314,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)
@@ -575,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
@@ -683,15 +690,23 @@ class DatabaseQuery(object):
self.match_filters.append(match_filters)
def get_permission_query_conditions(self):
+ conditions = []
condition_methods = frappe.get_hooks("permission_query_conditions", {}).get(self.doctype, [])
if condition_methods:
- conditions = []
for method in condition_methods:
c = frappe.call(frappe.get_attr(method), self.user)
if c:
conditions.append(c)
- return " and ".join(conditions) if conditions else None
+ permision_script_name = get_server_script_map().get("permission_query").get(self.doctype)
+ if permision_script_name:
+ script = frappe.get_doc("Server Script", permision_script_name)
+ condition = script.get_permission_query_conditions(self.user)
+ if condition:
+ conditions.append(condition)
+
+ return " and ".join(conditions) if conditions else ""
+
def run_custom_query(self, query):
if '%(key)s' in query:
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/meta.py b/frappe/model/meta.py
index 8c17a5b19b..c740d495c1 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -209,7 +209,8 @@ class Meta(Document):
'owner': _('Created By'),
'modified_by': _('Modified By'),
'creation': _('Created On'),
- 'modified': _('Last Modified On')
+ 'modified': _('Last Modified On'),
+ '_assign': _('Assigned To')
}.get(fieldname) or _('No Label')
return label
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index 7a2129e76e..2baf0c562c 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -1,14 +1,15 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals, print_function
+from __future__ import print_function, unicode_literals
+
import frappe
from frappe import _, bold
-from frappe.utils import cint
-from frappe.model.naming import validate_name
from frappe.model.dynamic_links import get_dynamic_link_map
-from frappe.utils.password import rename_password
+from frappe.model.naming import validate_name
from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data
+from frappe.utils import cint
+from frappe.utils.password import rename_password
@frappe.whitelist()
@@ -42,16 +43,13 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
force = cint(force)
merge = cint(merge)
-
meta = frappe.get_meta(doctype)
# call before_rename
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)
@@ -249,14 +247,21 @@ def update_link_field_values(link_fields, old, new, doctype):
# or no longer exists
pass
else:
- # because the table hasn't been renamed yet!
- parent = field['parent'] if field['parent']!=new else old
+ parent = field['parent']
+ docfield = field["fieldname"]
+
+ # Handles the case where one of the link fields belongs to
+ # the DocType being renamed.
+ # Here this field could have the current DocType as its value too.
+
+ # In this case while updating link field value, the field's parent
+ # or the current DocType table name hasn't been renamed yet,
+ # so consider it's old name.
+ if parent == new and doctype == "DocType":
+ parent = old
+
+ frappe.db.set_value(parent, {docfield: old}, docfield, new)
- frappe.db.sql("""
- update `tab{table_name}` set `{fieldname}`=%s
- where `{fieldname}`=%s""".format(
- table_name=parent,
- fieldname=field['fieldname']), (new, old))
# update cached link_fields as per new
if doctype=='DocType' and field['parent'] == old:
field['parent'] = new
@@ -306,8 +311,7 @@ def get_link_fields(doctype):
def update_options_for_fieldtype(fieldtype, old, new):
if frappe.conf.developer_mode:
- for name in frappe.db.sql_list("""select parent from
- tabDocField where options=%s""", old):
+ for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"):
doctype = frappe.get_doc("DocType", name)
save = False
for f in doctype.fields:
@@ -413,20 +417,21 @@ def update_parenttype_values(old, new):
child_doctypes += custom_child_doctypes
fields = [d['fieldname'] for d in child_doctypes]
- property_setter_child_doctypes = frappe.db.sql("""\
- select value as options from `tabProperty Setter`
- where doc_type=%s and property='options' and
- field_name in ("%s")""" % ('%s', '", "'.join(fields)),
- (new,))
+ property_setter_child_doctypes = frappe.get_all(
+ "Property Setter",
+ filters={
+ "doc_type": new,
+ "property": "options",
+ "field_name": ("in", fields)
+ },
+ pluck="value"
+ )
+ child_doctypes = list(d['options'] for d in child_doctypes)
child_doctypes += property_setter_child_doctypes
- child_doctypes = (d['options'] for d in child_doctypes)
for doctype in child_doctypes:
- frappe.db.sql("""\
- update `tab%s` set parenttype=%s
- where parenttype=%s""" % (doctype, '%s', '%s'),
- (new, old))
+ frappe.db.sql(f"update `tab{doctype}` set parenttype=%s where parenttype=%s", (new, old))
def rename_dynamic_links(doctype, old, new):
for df in get_dynamic_link_map().get(doctype, []):
@@ -482,60 +487,30 @@ def bulk_rename(doctype, rows=None, via_console = False):
return rename_log
def update_linked_doctypes(doctype, docname, linked_to, value, ignore_doctypes=None):
- """
- linked_doctype_info_list = list formed by get_fetch_fields() function
- docname = Master DocType's name in which modification are made
- value = Value for the field thats set in other DocType's by fetching from Master DocType
- """
- linked_doctype_info_list = get_fetch_fields(doctype, linked_to, ignore_doctypes)
+ from frappe.model.utils.rename_doc import update_linked_doctypes
+ show_deprecation_warning("update_linked_doctypes")
+
+ return update_linked_doctypes(
+ doctype=doctype,
+ docname=docname,
+ linked_to=linked_to,
+ value=value,
+ ignore_doctypes=ignore_doctypes,
+ )
- for d in linked_doctype_info_list:
- frappe.db.sql("""
- update
- `tab{doctype}`
- set
- {linked_to_fieldname} = "{value}"
- where
- {master_fieldname} = {docname}
- and {linked_to_fieldname} != "{value}"
- """.format(
- doctype = d['doctype'],
- linked_to_fieldname = d['linked_to_fieldname'],
- value = value,
- master_fieldname = d['master_fieldname'],
- docname = frappe.db.escape(docname)
- ))
def get_fetch_fields(doctype, linked_to, ignore_doctypes=None):
- """
- doctype = Master DocType in which the changes are being made
- linked_to = DocType name of the field thats being updated in Master
+ from frappe.model.utils.rename_doc import get_fetch_fields
+ show_deprecation_warning("get_fetch_fields")
- This function fetches list of all DocType where both doctype and linked_to is found
- as link fields.
- Forms a list of dict in the form -
- [{doctype: , master_fieldname: , linked_to_fieldname: ]
- where
- doctype = DocType where changes need to be made
- master_fieldname = Fieldname where options = doctype
- linked_to_fieldname = Fieldname where options = linked_to
- """
+ return get_fetch_fields(
+ doctype=doctype, linked_to=linked_to, ignore_doctypes=ignore_doctypes
+ )
- master_list = get_link_fields(doctype)
- linked_to_list = get_link_fields(linked_to)
- out = []
-
- from itertools import product
- product_list = product(master_list, linked_to_list)
-
- for d in product_list:
- linked_doctype_info = frappe._dict()
- if d[0]['parent'] == d[1]['parent'] \
- and (not ignore_doctypes or d[0]['parent'] not in ignore_doctypes) \
- and not d[1]['issingle']:
- linked_doctype_info['doctype'] = d[0]['parent']
- linked_doctype_info['master_fieldname'] = d[0]['fieldname']
- linked_doctype_info['linked_to_fieldname'] = d[1]['fieldname']
- out.append(linked_doctype_info)
-
- return out
+def show_deprecation_warning(funct):
+ from click import secho
+ message = (
+ f"Function frappe.model.rename_doc.{funct} has been deprecated and "
+ "moved to the frappe.model.utils.rename_doc"
+ )
+ secho(message, fg="yellow")
diff --git a/frappe/model/utils/rename_doc.py b/frappe/model/utils/rename_doc.py
new file mode 100644
index 0000000000..bf71d36a42
--- /dev/null
+++ b/frappe/model/utils/rename_doc.py
@@ -0,0 +1,58 @@
+from itertools import product
+
+import frappe
+from frappe.model.rename_doc import get_link_fields
+
+
+def update_linked_doctypes(doctype, docname, linked_to, value, ignore_doctypes=None):
+ """
+ linked_doctype_info_list = list formed by get_fetch_fields() function
+ docname = Master DocType's name in which modification are made
+ value = Value for the field thats set in other DocType's by fetching from Master DocType
+ """
+ linked_doctype_info_list = get_fetch_fields(doctype, linked_to, ignore_doctypes)
+
+ for d in linked_doctype_info_list:
+ frappe.db.set_value(
+ d.doctype,
+ {
+ d.master_fieldname : docname,
+ d.linked_to_fieldname : ("!=", value),
+ },
+ d.linked_to_fieldname,
+ value,
+ )
+
+
+def get_fetch_fields(doctype, linked_to, ignore_doctypes=None):
+ """
+ doctype = Master DocType in which the changes are being made
+ linked_to = DocType name of the field thats being updated in Master
+ This function fetches list of all DocType where both doctype and linked_to is found
+ as link fields.
+ Forms a list of dict in the form -
+ [{doctype: , master_fieldname: , linked_to_fieldname: ]
+ where
+ doctype = DocType where changes need to be made
+ master_fieldname = Fieldname where options = doctype
+ linked_to_fieldname = Fieldname where options = linked_to
+ """
+
+ out = []
+ master_list = get_link_fields(doctype)
+ linked_to_list = get_link_fields(linked_to)
+ product_list = product(master_list, linked_to_list)
+
+ for d in product_list:
+ linked_doctype_info = frappe._dict()
+ if (
+ d[0]["parent"] == d[1]["parent"]
+ and (not ignore_doctypes or d[0]["parent"] not in ignore_doctypes)
+ and not d[1]["issingle"]
+ ):
+ linked_doctype_info.doctype = d[0]["parent"]
+ linked_doctype_info.master_fieldname = d[0]["fieldname"]
+ linked_doctype_info.linked_to_fieldname = d[1]["fieldname"]
+ out.append(linked_doctype_info)
+
+ return out
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index 7239b202bd..3e8125f9b1 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -29,6 +29,8 @@ def get_transitions(doc, workflow = None, raise_exception=False):
if doc.is_new():
return []
+ doc.load_from_db()
+
frappe.has_permission(doc, 'read', throw=True)
roles = frappe.get_roles()
@@ -51,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
)
)
@@ -115,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/oauth.py b/frappe/oauth.py
index bf225ac118..09af5ad809 100644
--- a/frappe/oauth.py
+++ b/frappe/oauth.py
@@ -148,7 +148,7 @@ class OAuthWebRequestValidator(RequestValidator):
print("Failed body authentication: Application %s does not exist".format(cid=request.client_id))
cookie_dict = get_cookie_dict_from_headers(request)
- user_id = unquote(cookie_dict['user_id']) if 'user_id' in cookie_dict else "Guest"
+ user_id = unquote(cookie_dict.get('user_id').value) if 'user_id' in cookie_dict else "Guest"
return frappe.session.user == user_id
def authenticate_client_id(self, client_id, request, *args, **kwargs):
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 0daf29e001..1a086303ba 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -21,6 +21,7 @@ execute:frappe.reload_doc('email', 'doctype', 'document_follow')
execute:frappe.reload_doc('core', 'doctype', 'communication_link') #2019-10-02
execute:frappe.reload_doc('core', 'doctype', 'has_role')
execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02
+execute:frappe.reload_doc('core', 'doctype', 'server_script')
frappe.patches.v11_0.replicate_old_user_permissions
frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03
frappe.patches.v7_1.rename_scheduler_log_to_error_log
@@ -316,3 +317,4 @@ frappe.patches.v13_0.web_template_set_module #2020-10-05
frappe.patches.v13_0.remove_custom_link
execute:frappe.delete_doc("DocType", "Footer Item")
frappe.patches.v13_0.replace_field_target_with_open_in_new_tab
+frappe.patches.v13_0.delete_package_publish_tool
diff --git a/frappe/patches/v13_0/delete_package_publish_tool.py b/frappe/patches/v13_0/delete_package_publish_tool.py
new file mode 100644
index 0000000000..25024f58dd
--- /dev/null
+++ b/frappe/patches/v13_0/delete_package_publish_tool.py
@@ -0,0 +1,11 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+
+def execute():
+ frappe.delete_doc("DocType", "Package Publish Tool", ignore_missing=True)
+ frappe.delete_doc("DocType", "Package Document Type", ignore_missing=True)
+ frappe.delete_doc("DocType", "Package Publish Target", ignore_missing=True)
diff --git a/frappe/permissions.py b/frappe/permissions.py
index 0d766aec8d..a45fbdcd06 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -7,7 +7,7 @@ import frappe, copy, json
from frappe import _, msgprint
from frappe.utils import cint
import frappe.share
-rights = ("read", "write", "create", "delete", "submit", "cancel", "amend",
+rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share")
# TODO:
@@ -73,6 +73,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra
role_permissions = get_role_permissions(meta, user=user)
perm = role_permissions.get(ptype)
+
if not perm:
push_perm_check_log(_('User {0} does not have doctype access via role permission for document {1}').format(frappe.bold(user), frappe.bold(doctype)))
@@ -192,9 +193,9 @@ def get_role_permissions(doctype_meta, user=None):
and ptype != 'create'):
perms['if_owner'][ptype] = 1
# has no access if not owner
- # only provide read access so that user is able to at-least access list
+ # only provide select or read access so that user is able to at-least access list
# (and the documents will be filtered based on owner sin further checks)
- perms[ptype] = 1 if ptype == 'read' else 0
+ perms[ptype] = 1 if ptype in ['select', 'read'] else 0
frappe.local.role_permissions[cache_key] = perms
diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js
index e6599b2496..786f8f97ab 100644
--- a/frappe/printing/doctype/print_format/print_format.js
+++ b/frappe/printing/doctype/print_format/print_format.js
@@ -19,6 +19,7 @@ frappe.ui.form.on("Print Format", {
}
frm.trigger('render_buttons');
frm.toggle_display('standard', frappe.boot.developer_mode);
+ frm.trigger('hide_absolute_value_field');
},
render_buttons: function (frm) {
frm.page.clear_inner_toolbar();
@@ -58,5 +59,20 @@ frappe.ui.form.on("Print Format", {
frm.set_value('show_section_headings', value);
frm.set_value('line_breaks', value);
frm.trigger('render_buttons');
+ },
+ doc_type: function (frm) {
+ frm.trigger('hide_absolute_value_field');
+ },
+ 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].doc_type;
+ if (doctype) {
+ frappe.model.with_doctype(doctype, () => {
+ const meta = frappe.get_meta(doctype);
+ const has_int_float_currency_field = meta.fields.filter(df => in_list(['Int', 'Float', 'Currency'], df.fieldtype));
+ frm.toggle_display('absolute_value', has_int_float_currency_field.length);
+ });
+ }
}
-})
+});
diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json
index 63448ccc39..3a47fb554f 100644
--- a/frappe/printing/doctype/print_format/print_format.json
+++ b/frappe/printing/doctype/print_format/print_format.json
@@ -22,6 +22,7 @@
"align_labels_right",
"show_section_headings",
"line_breaks",
+ "absolute_value",
"column_break_11",
"font",
"css_section",
@@ -196,13 +197,21 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Print Format Builder"
+ },
+ {
+ "default": "0",
+ "depends_on": "doc_type",
+ "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"
}
],
"icon": "fa fa-print",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-10-27 18:27:58.307070",
+ "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..d744da98d1 100755
--- a/frappe/public/build.json
+++ b/frappe/public/build.json
@@ -307,6 +307,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/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js
index 6c17cb4351..477cfb0786 100644
--- a/frappe/public/js/frappe/data_import/import_preview.js
+++ b/frappe/public/js/frappe/data_import/import_preview.js
@@ -101,6 +101,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
.replace('%H', 'HH')
.replace('%M', 'mm')
.replace('%S', 'ss')
+ .replace('%b', 'Mon')
: null;
let column_title = `
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index c8ed29fb76..5fa7a9dbcb 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -148,7 +148,6 @@ frappe.Application = Class.extend({
user: frappe.session.user
},
callback: function(r) {
- console.log(r);
if(r.message.show_alert){
frappe.show_alert({
indicator: 'red',
diff --git a/frappe/public/js/frappe/file_uploader/index.js b/frappe/public/js/frappe/file_uploader/index.js
index 62a7bff822..646f60715a 100644
--- a/frappe/public/js/frappe/file_uploader/index.js
+++ b/frappe/public/js/frappe/file_uploader/index.js
@@ -15,7 +15,11 @@ export default class FileUploader {
allow_multiple,
as_dataurl,
disable_file_browser,
+ frm
} = {}) {
+
+ frm && frm.attachments.max_reached(true);
+
if (!wrapper) {
this.make_dialog();
} else {
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/comment.js b/frappe/public/js/frappe/form/controls/comment.js
index a64df56bca..d00c915065 100644
--- a/frappe/public/js/frappe/form/controls/comment.js
+++ b/frappe/public/js/frappe/form/controls/comment.js
@@ -60,7 +60,7 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({
update_state() {
const value = this.get_value();
- if (strip_html(value).trim() != "") {
+ if (strip_html(value).trim() != "" || value.includes('img')) {
this.button.removeClass('btn-default').addClass('btn-primary');
} else {
this.button.addClass('btn-default').removeClass('btn-primary');
diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js
index 4db2553bd1..401de2ed5d 100644
--- a/frappe/public/js/frappe/form/controls/data.js
+++ b/frappe/public/js/frappe/form/controls/data.js
@@ -22,27 +22,9 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
this.has_input = true;
this.bind_change_event();
this.setup_autoname_check();
- if (this.df.options == 'Phone') {
- this.setup_phone();
- }
// somehow this event does not bubble up to document
// after v7, if you can debug, remove this
},
- setup_phone() {
- if (frappe.phone_call.handler) {
- this.$wrapper.find('.control-input')
- .append(`
-
-
-
-
- `)
- .find('.phone-btn')
- .click(() => {
- frappe.phone_call.handler(this.get_value(), this.frm);
- });
- }
- },
setup_autoname_check: function() {
if (!this.df.parent) return;
this.meta = frappe.get_meta(this.df.parent);
diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js
index 9dfad09299..9e4d1d82ec 100644
--- a/frappe/public/js/frappe/form/controls/geolocation.js
+++ b/frappe/public/js/frappe/form/controls/geolocation.js
@@ -1,3 +1,5 @@
+frappe.provide('frappe.utils.utils');
+
frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
horizontal: false,
@@ -90,11 +92,11 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
});
L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/';
- this.map = L.map(this.map_id).setView([19.0800, 72.8961], 13);
+ this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center,
+ frappe.utils.map_defaults.zoom);
- L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
- attribution: '© OpenStreetMap contributors'
- }).addTo(this.map);
+ L.tileLayer(frappe.utils.map_defaults.tiles,
+ frappe.utils.map_defaults.options).addTo(this.map);
},
bind_leaflet_locate_control() {
diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js
index 56f9430238..111ee7d8f6 100644
--- a/frappe/public/js/frappe/form/controls/link.js
+++ b/frappe/public/js/frappe/form/controls/link.js
@@ -215,6 +215,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 +297,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/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js
index 84f34d4757..159ab8a61b 100644
--- a/frappe/public/js/frappe/form/footer/timeline.js
+++ b/frappe/public/js/frappe/form/footer/timeline.js
@@ -30,7 +30,7 @@ frappe.ui.form.Timeline = class Timeline {
render_input: true,
only_input: true,
on_submit: (val) => {
- if(strip_html(val).trim() != "") {
+ if (strip_html(val).trim() != "" || val.includes('img')) {
this.insert_comment(val, this.comment_area.button);
}
}
@@ -547,10 +547,7 @@ frappe.ui.form.Timeline = class Timeline {
log.color = 'dark';
log.sender = log.owner;
log.comment_type = 'Milestone';
- log.content = __('{0} changed {1} to {2}', [
- frappe.user.full_name(log.owner).bold(),
- frappe.meta.get_label(this.frm.doctype, log.track_field),
- log.value.bold()]);
+ log.content = __('{0} changed {1} to {2}', [ frappe.user.full_name(log.owner).bold(), frappe.meta.get_label(this.frm.doctype, log.track_field), log.value.bold()]);
return log;
});
return milestones;
@@ -613,11 +610,7 @@ frappe.ui.form.Timeline = class Timeline {
const field_display_status = frappe.perm.get_field_display_status(df, null,
me.frm.perm);
if (field_display_status === 'Read' || field_display_status === 'Write') {
- parts.push(__('{0} from {1} to {2}', [
- __(df.label),
- me.format_content_for_timeline(p[1]),
- me.format_content_for_timeline(p[2])
- ]));
+ parts.push(__('{0} from {1} to {2}', [ __(df.label), me.format_content_for_timeline(p[1]), me.format_content_for_timeline(p[2])]));
}
}
}
@@ -648,13 +641,7 @@ frappe.ui.form.Timeline = class Timeline {
null, me.frm.perm);
if (field_display_status === 'Read' || field_display_status === 'Write') {
- parts.push(__('{0} from {1} to {2} in row #{3}', [
- frappe.meta.get_label(me.frm.fields_dict[row[0]].grid.doctype,
- p[0]),
- me.format_content_for_timeline(p[1]),
- me.format_content_for_timeline(p[2]),
- row[1]
- ]));
+ parts.push(__('{0} from {1} to {2} in row #{3}', [ frappe.meta.get_label( me.frm.fields_dict[row[0]].grid.doctype, p[0]), me.format_content_for_timeline(p[1]), me.format_content_for_timeline(p[2]), row[1] ]));
}
}
return parts.length < 3;
@@ -691,8 +678,7 @@ frappe.ui.form.Timeline = class Timeline {
return p;
});
if (parts.length) {
- out.push(me.get_version_comment(version, __("{0} rows for {1}",
- [__(key), parts.join(', ')])));
+ out.push(me.get_version_comment(version, __("{0} rows for {1}", [__(key), parts.join(', ')])));
}
}
});
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index fc348704fa..1e23969afa 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -232,14 +232,10 @@ frappe.ui.form.Form = class FrappeForm {
throw "attach error";
}
- if(me.attachments.max_reached()) {
- frappe.msgprint(__("Maximum Attachment Limit for this record reached."));
- throw "attach error";
- }
-
new frappe.ui.FileUploader({
doctype: me.doctype,
docname: me.docname,
+ frm: me,
files: dataTransfer.files,
folder: 'Home/Attachments',
on_success(file_doc) {
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index 3f422d0a9b..2b8956653b 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 827fbfdee6..ec9cee9c39 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -373,6 +373,7 @@ export default class GridRow {
// no text editor in grid
if (df.fieldtype=='Text Editor') {
+ df = Object.assign({}, df);
df.fieldtype = 'Text';
}
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 22c885e0cb..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,14 +497,14 @@ frappe.ui.form.Layout = Class.extend({
this.refresh_section_count();
},
- set_dependant_property: function(condition, fieldname, property) {
+ set_dependant_property: function (condition, fieldname, property) {
let set_property = this.evaluate_depends_on_value(condition);
let value = set_property ? 1 : 0;
let form_obj;
if (this.frm) {
form_obj = this.frm;
- } else if (this.is_dialog) {
+ } else if (this.is_dialog || this.doctype === 'Web Form') {
form_obj = this;
}
if (form_obj) {
@@ -516,7 +517,7 @@ frappe.ui.form.Layout = Class.extend({
}
}
},
- evaluate_depends_on_value: function(expression) {
+ evaluate_depends_on_value: function (expression) {
var out = null;
var doc = this.doc;
@@ -530,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;
@@ -562,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 || {};
@@ -582,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);
}
@@ -591,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) {
@@ -611,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');
}
@@ -664,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();
}
});
@@ -674,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;
}
@@ -688,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;
});
@@ -711,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);
@@ -720,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/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js
index 165527e281..56b484e7c4 100644
--- a/frappe/public/js/frappe/form/sidebar/attachments.js
+++ b/frappe/public/js/frappe/form/sidebar/attachments.js
@@ -16,15 +16,19 @@ frappe.ui.form.Attachments = Class.extend({
this.add_attachment_wrapper = this.parent.find(".add_attachment").parent();
this.attachments_label = this.parent.find(".attachments-label");
},
- max_reached: function() {
- // no of attachments
- var n = Object.keys(this.get_attachments()).length;
-
- // button if the number of attachments is less than max
- if(n < this.frm.meta.max_attachments || !this.frm.meta.max_attachments) {
- return false;
+ max_reached: function(raise_exception=false) {
+ const attachment_count = Object.keys(this.get_attachments()).length;
+ const attachment_limit = this.frm.meta.max_attachments;
+ if (attachment_limit && attachment_count >= attachment_limit) {
+ if (raise_exception) {
+ frappe.throw({
+ title: __("Attachment Limit Reached"),
+ message: __("Maximum attachment limit of {0} has been reached.", [cstr(attachment_limit).bold()]),
+ });
+ }
+ return true;
}
- return true;
+ return false;
},
refresh: function() {
var me = this;
@@ -140,7 +144,6 @@ frappe.ui.form.Attachments = Class.extend({
});
},
new_attachment: function(fieldname) {
- var me = this;
if (this.dialog) {
// remove upload dialog
this.dialog.$wrapper.remove();
@@ -149,6 +152,7 @@ frappe.ui.form.Attachments = Class.extend({
new frappe.ui.FileUploader({
doctype: this.frm.doctype,
docname: this.frm.docname,
+ frm: this.frm,
folder: 'Home/Attachments',
on_success: (file_doc) => {
this.attachment_uploaded(file_doc);
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/create_new.js b/frappe/public/js/frappe/model/create_new.js
index 7be7fc5baa..f7a5982b96 100644
--- a/frappe/public/js/frappe/model/create_new.js
+++ b/frappe/public/js/frappe/model/create_new.js
@@ -306,6 +306,7 @@ $.extend(frappe.model, {
selected_children: opts.frm ? opts.frm.get_selected() : null
},
freeze: true,
+ freeze_message: opts.freeze_message || '',
callback: function(r) {
if(!r.exc) {
frappe.model.sync(r.message);
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index 308d9bd5f8..e82f64c6fc 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -103,6 +103,31 @@ $.extend(frappe.model, {
return docfield[0];
},
+ get_from_localstorage: function(doctype) {
+ if (localStorage["_doctype:" + doctype]) {
+ return JSON.parse(localStorage["_doctype:" + doctype]);
+ }
+ },
+
+ set_in_localstorage: function(doctype, docs) {
+ try {
+ localStorage["_doctype:" + doctype] = JSON.stringify(docs);
+ } catch(e) {
+ // if quota is exceeded, clear local storage and set item
+ console.warn("localStorage quota exceeded, clearing doctype cache")
+ frappe.model.clear_local_storage();
+ localStorage["_doctype:" + doctype] = JSON.stringify(docs);
+ }
+ },
+
+ clear_local_storage: function() {
+ for(var key in localStorage) {
+ if (key.startsWith("_doctype:")) {
+ localStorage.removeItem(key);
+ }
+ }
+ },
+
with_doctype: function(doctype, callback, async) {
if(locals.DocType[doctype]) {
callback && callback();
@@ -110,13 +135,15 @@ $.extend(frappe.model, {
let cached_timestamp = null;
let cached_doc = null;
- if(localStorage["_doctype:" + doctype]) {
- let cached_docs = JSON.parse(localStorage["_doctype:" + 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) {
cached_timestamp = cached_doc.modified;
}
}
+
return frappe.call({
method:'frappe.desk.form.load.getdoctype',
type: "GET",
@@ -134,7 +161,7 @@ $.extend(frappe.model, {
if(r.message=="use_cache") {
frappe.model.sync(cached_doc);
} else {
- localStorage["_doctype:" + doctype] = JSON.stringify(r.docs);
+ frappe.model.set_in_localstorage(doctype, r.docs)
}
frappe.model.init_doctype(doctype);
@@ -225,6 +252,10 @@ $.extend(frappe.model, {
return frappe.boot.user.can_create.indexOf(doctype)!==-1;
},
+ can_select: function(doctype) {
+ return frappe.boot.user.can_select.indexOf(doctype)!==-1;
+ },
+
can_read: function(doctype) {
return frappe.boot.user.can_read.indexOf(doctype)!==-1;
},
diff --git a/frappe/public/js/frappe/ui/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/utils/common.js b/frappe/public/js/frappe/utils/common.js
index 9ff4ade761..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) {
@@ -234,11 +234,11 @@ frappe.utils.xss_sanitise = function (string, options) {
strategies: ['html', 'js'] // use all strategies.
}
const HTML_ESCAPE_MAP = {
- '<': '<',
- '>': '>',
- '"': '"',
- "'": ''',
- '/': '/'
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": ''',
+ '/': '/'
};
const REGEX_SCRIPT = /",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/website.scss b/frappe/public/scss/website.scss
index e1b7d0a827..5291834aab 100644
--- a/frappe/public/scss/website.scss
+++ b/frappe/public/scss/website.scss
@@ -244,9 +244,18 @@ h5.modal-title {
white-space: nowrap;
text-overflow: ellipsis;
}
+
.about-section {
padding-top: 1rem;
}
+
.about-footer {
padding-top: 1rem;
-}
\ No newline at end of file
+}
+
+.logged-in > .nav-link {
+ max-width: 200px;
+ @extend .ellipsis;
+ max-width: 100%;
+ vertical-align: middle;
+}
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 @@
{%- set parents = parents[-3:] %}
+ {% set count = (parents | length) + 1 %}
{% for parent in parents %}
@@ -11,8 +12,11 @@
{% endfor %}
-
- {{ title or "" }}
+
+
+ {{ title }}
+
+
diff --git a/frappe/templates/includes/oauth_confirmation.html b/frappe/templates/includes/oauth_confirmation.html
index 73425af036..3fbbb75971 100644
--- a/frappe/templates/includes/oauth_confirmation.html
+++ b/frappe/templates/includes/oauth_confirmation.html
@@ -1,7 +1,7 @@
{% if not error %}
-
{{ client_id }} wants to access the following details from your account
+ {{ _("{} wants to access the following details from your account").format(client_id) }}
- Allow
+ {{ _("Allow") }}
- Deny
+ {{ _("Deny") }}
@@ -22,24 +22,24 @@
{% else %}
-
Authorization error for {{ client_id }}
+ {{ _("Authorization error for {}.").format(client_id) }}
-
An unexpected error occurred while authorizing {{ client_id }}.
+
{{ _("An unexpected error occurred while authorizing {}.").format(client_id) }}
{{ error }}
diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html
index 3681a87f53..0d904bb59c 100644
--- a/frappe/templates/print_formats/standard_macros.html
+++ b/frappe/templates/print_formats/standard_macros.html
@@ -137,9 +137,10 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{% elif df.fieldtype=="HTML" %}
{{ frappe.render_template(df.options, {"doc":doc}) }}
{% elif df.fieldtype=="Currency" %}
- {{ doc.get_formatted(df.fieldname, doc, translated=df.translatable) }}
- {% else %}
{{ 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) }}
{% endif %}
{%- endmacro %}
diff --git a/frappe/tests/data/exif_sample_image.jpg b/frappe/tests/data/exif_sample_image.jpg
new file mode 100644
index 0000000000..4a2c1552b9
Binary files /dev/null and b/frappe/tests/data/exif_sample_image.jpg differ
diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py
index 9757a823a6..0786a0e14f 100644
--- a/frappe/tests/test_commands.py
+++ b/frappe/tests/test_commands.py
@@ -1,24 +1,88 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# imports - standard imports
+import gzip
+import json
import os
import shlex
import subprocess
+import sys
import unittest
-from glob import glob
+import glob
# imports - module imports
import frappe
-from frappe.utils.backups import fetch_latest_backups
import frappe.recorder
+from frappe.installer import add_to_installed_apps
+from frappe.utils import add_to_date, get_bench_relative_path, now
+from frappe.utils.backups import fetch_latest_backups
+
+
+# TODO: check frappe.cli.coloured_output to set coloured output!
+def supports_color():
+ """
+ Returns True if the running system's terminal supports color, and False
+ otherwise.
+ """
+ plat = sys.platform
+ supported_platform = plat != 'Pocket PC' and (plat != 'win32' or 'ANSICON' in os.environ)
+ # isatty is not always implemented, #6223.
+ is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
+ return supported_platform and is_a_tty
+
+
+class color(dict):
+ nc = "\033[0m"
+ blue = "\033[94m"
+ green = "\033[92m"
+ yellow = "\033[93m"
+ red = "\033[91m"
+ silver = "\033[90m"
+
+ def __getattr__(self, key):
+ if supports_color():
+ ret = self.get(key)
+ else:
+ ret = ""
+ return ret
def clean(value):
- if isinstance(value, (bytes, str)):
- value = value.decode().strip()
+ """Strips and converts bytes to str
+
+ Args:
+ value ([type]): [description]
+
+ Returns:
+ [type]: [description]
+ """
+ if isinstance(value, bytes):
+ value = value.decode()
+ if isinstance(value, str):
+ value = value.strip()
return value
+def exists_in_backup(doctypes, file):
+ """Checks if the list of doctypes exist in the database.sql.gz file supplied
+
+ Args:
+ doctypes (list): List of DocTypes to be checked
+ file (str): Path of the database file
+
+ Returns:
+ bool: True if all tables exist
+ """
+ predicate = (
+ 'COPY public."tab{}"'
+ if frappe.conf.db_type == "postgres"
+ else "CREATE TABLE `tab{}`"
+ )
+ with gzip.open(file, "rb") as f:
+ content = f.read().decode("utf8")
+ return all([predicate.format(doctype).lower() in content.lower() for doctype in doctypes])
+
+
class BaseTestCommands(unittest.TestCase):
def execute(self, command, kwargs=None):
site = {"site": frappe.local.site}
@@ -26,13 +90,26 @@ class BaseTestCommands(unittest.TestCase):
kwargs.update(site)
else:
kwargs = site
- command = command.replace("\n", " ").format(**kwargs)
- command = shlex.split(command)
+ self.command = " ".join(command.split()).format(**kwargs)
+ print("{0}$ {1}{2}".format(color.silver, self.command, color.nc))
+ command = shlex.split(self.command)
self._proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.stdout = clean(self._proc.stdout)
self.stderr = clean(self._proc.stderr)
self.returncode = clean(self._proc.returncode)
+ def _formatMessage(self, msg, standardMsg):
+ output = super(BaseTestCommands, self)._formatMessage(msg, standardMsg)
+ cmd_execution_summary = "\n".join([
+ "-" * 70,
+ "Last Command Execution Summary:",
+ "Command: {}".format(self.command) if self.command else "",
+ "Standard Output: {}".format(self.stdout) if self.stdout else "",
+ "Standard Error: {}".format(self.stderr) if self.stderr else "",
+ "Return Code: {}".format(self.returncode) if self.returncode else "",
+ ]).strip()
+ return "{}\n\n{}".format(output, cmd_execution_summary)
+
class TestCommands(BaseTestCommands):
def test_execute(self):
@@ -52,9 +129,24 @@ class TestCommands(BaseTestCommands):
# The returned value has quotes which have been trimmed for the test
self.execute("""bench --site {site} execute frappe.bold --kwargs '{{"text": "DocType"}}'""")
self.assertEquals(self.returncode, 0)
- self.assertEquals(self.stdout[1:-1], frappe.bold(text='DocType'))
+ self.assertEquals(self.stdout[1:-1], frappe.bold(text="DocType"))
def test_backup(self):
+ backup = {
+ "includes": {
+ "includes": [
+ "ToDo",
+ "Note",
+ ]
+ },
+ "excludes": {
+ "excludes": [
+ "Activity Log",
+ "Access Log",
+ "Error Log"
+ ]
+ }
+ }
home = os.path.expanduser("~")
site_backup_path = frappe.utils.get_site_path("private", "backups")
@@ -94,16 +186,19 @@ class TestCommands(BaseTestCommands):
"db_path": "database.sql.gz",
"files_path": "public.tar",
"private_path": "private.tar",
- "conf_path": "config.json"
+ "conf_path": "config.json",
}.items()
}
- self.execute("""bench
+ self.execute(
+ """bench
--site {site} backup --with-files
--backup-path-db {db_path}
--backup-path-files {files_path}
--backup-path-private-files {private_path}
- --backup-path-conf {conf_path}""", kwargs)
+ --backup-path-conf {conf_path}""",
+ kwargs,
+ )
self.assertEquals(self.returncode, 0)
for path in kwargs.values():
@@ -111,16 +206,122 @@ class TestCommands(BaseTestCommands):
# test 5: take a backup with --compress
self.execute("bench --site {site} backup --with-files --compress")
-
self.assertEquals(self.returncode, 0)
-
- compressed_files = glob(site_backup_path + "/*.tgz")
+ compressed_files = glob.glob(site_backup_path + "/*.tgz")
self.assertGreater(len(compressed_files), 0)
# test 6: take a backup with --verbose
self.execute("bench --site {site} backup --verbose")
self.assertEquals(self.returncode, 0)
+ # test 7: take a backup with frappe.conf.backup.includes
+ self.execute(
+ "bench --site {site} set-config backup '{includes}' --as-dict",
+ {"includes": json.dumps(backup["includes"])},
+ )
+ self.execute("bench --site {site} backup --verbose")
+ self.assertEquals(self.returncode, 0)
+ database = fetch_latest_backups(partial=True)["database"]
+ self.assertTrue(exists_in_backup(backup["includes"]["includes"], database))
+
+ # test 8: take a backup with frappe.conf.backup.excludes
+ self.execute(
+ "bench --site {site} set-config backup '{excludes}' --as-dict",
+ {"excludes": json.dumps(backup["excludes"])},
+ )
+ self.execute("bench --site {site} backup --verbose")
+ self.assertEquals(self.returncode, 0)
+ database = fetch_latest_backups(partial=True)["database"]
+ self.assertFalse(exists_in_backup(backup["excludes"]["excludes"], database))
+ self.assertTrue(exists_in_backup(backup["includes"]["includes"], database))
+
+ # test 9: take a backup with --include (with frappe.conf.excludes still set)
+ self.execute(
+ "bench --site {site} backup --include '{include}'",
+ {"include": ",".join(backup["includes"]["includes"])},
+ )
+ self.assertEquals(self.returncode, 0)
+ database = fetch_latest_backups(partial=True)["database"]
+ self.assertTrue(exists_in_backup(backup["includes"]["includes"], database))
+
+ # test 10: take a backup with --exclude
+ self.execute(
+ "bench --site {site} backup --exclude '{exclude}'",
+ {"exclude": ",".join(backup["excludes"]["excludes"])},
+ )
+ self.assertEquals(self.returncode, 0)
+ database = fetch_latest_backups(partial=True)["database"]
+ self.assertFalse(exists_in_backup(backup["excludes"]["excludes"], database))
+
+ # test 11: take a backup with --ignore-backup-conf
+ self.execute("bench --site {site} backup --ignore-backup-conf")
+ self.assertEquals(self.returncode, 0)
+ database = fetch_latest_backups()["database"]
+ self.assertTrue(exists_in_backup(backup["excludes"]["excludes"], database))
+
+ def test_restore(self):
+ # step 0: create a site to run the test on
+ global_config = {
+ "admin_password": frappe.conf.admin_password,
+ "root_login": frappe.conf.root_login,
+ "root_password": frappe.conf.root_password,
+ "db_type": frappe.conf.db_type,
+ }
+ site_data = {"another_site": f"{frappe.local.site}-restore.test", **global_config}
+ for key, value in global_config.items():
+ if value:
+ self.execute(f"bench set-config {key} {value} -g")
+ self.execute(
+ "bench new-site {another_site} --admin-password {admin_password} --db-type"
+ " {db_type}",
+ site_data,
+ )
+
+ # test 1: bench restore from full backup
+ self.execute("bench --site {another_site} backup --ignore-backup-conf", site_data)
+ self.execute(
+ "bench --site {another_site} execute frappe.utils.backups.fetch_latest_backups",
+ site_data,
+ )
+ site_data.update({"database": json.loads(self.stdout)["database"]})
+ self.execute("bench --site {another_site} restore {database}", site_data)
+
+ # test 2: restore from partial backup
+ self.execute("bench --site {another_site} backup --exclude 'ToDo'", site_data)
+ site_data.update({"kw": "\"{'partial':True}\""})
+ self.execute(
+ "bench --site {another_site} execute"
+ " frappe.utils.backups.fetch_latest_backups --kwargs {kw}",
+ site_data,
+ )
+ site_data.update({"database": json.loads(self.stdout)["database"]})
+ self.execute("bench --site {another_site} restore {database}", site_data)
+ self.assertEquals(self.returncode, 1)
+
+ def test_partial_restore(self):
+ _now = now()
+ for num in range(10):
+ frappe.get_doc({
+ "doctype": "ToDo",
+ "date": add_to_date(_now, days=num),
+ "description": frappe.mock("paragraph")
+ }).insert()
+ frappe.db.commit()
+ todo_count = frappe.db.count("ToDo")
+
+ # check if todos exist, create a partial backup and see if the state is the same after restore
+ self.assertIsNot(todo_count, 0)
+ self.execute("bench --site {site} backup --only 'ToDo'")
+ db_path = fetch_latest_backups(partial=True)["database"]
+ self.assertTrue("partial" in db_path)
+
+ frappe.db.sql_ddl("DROP TABLE IF EXISTS `tabToDo`")
+ frappe.db.commit()
+
+ self.execute("bench --site {site} partial-restore {path}", {"path": db_path})
+ self.assertEquals(self.returncode, 0)
+ self.assertEquals(frappe.db.count("ToDo"), todo_count)
+
def test_recorder(self):
frappe.recorder.stop()
@@ -133,7 +334,6 @@ class TestCommands(BaseTestCommands):
self.assertEqual(frappe.recorder.status(), False)
def test_remove_from_installed_apps(self):
- from frappe.installer import add_to_installed_apps
app = "test_remove_app"
add_to_installed_apps(app)
@@ -164,3 +364,21 @@ class TestCommands(BaseTestCommands):
else:
installed_apps = set(frappe.get_installed_apps())
self.assertSetEqual(list_apps, installed_apps)
+
+ def test_get_bench_relative_path(self):
+ bench_path = frappe.utils.get_bench_path()
+ test1_path = os.path.join(bench_path, "test1.txt")
+ test2_path = os.path.join(bench_path, "sites", "test2.txt")
+
+ with open(test1_path, "w+") as test1:
+ test1.write("asdf")
+ with open(test2_path, "w+") as test2:
+ test2.write("asdf")
+
+ self.assertTrue("test1.txt" in get_bench_relative_path("test1.txt"))
+ self.assertTrue("sites/test2.txt" in get_bench_relative_path("test2.txt"))
+ with self.assertRaises(SystemExit):
+ get_bench_relative_path("test3.txt")
+
+ os.remove(test1_path)
+ os.remove(test2_path)
diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py
index 8c11df9a9e..2be92be1f5 100644
--- a/frappe/tests/test_document.py
+++ b/frappe/tests/test_document.py
@@ -249,47 +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_non_negative_check(self):
frappe.delete_doc_if_exists("Currency", "Frappe Coin", 1)
@@ -305,4 +264,4 @@ class TestDocument(unittest.TestCase):
d.insert()
self.assertEqual(frappe.db.get_value("Currency", d.name), d.name)
- frappe.delete_doc_if_exists("Currency", "Frappe Coin", 1)
\ No newline at end of file
+ frappe.delete_doc_if_exists("Currency", "Frappe Coin", 1)
diff --git a/frappe/tests/test_oauth20.py b/frappe/tests/test_oauth20.py
index f4ecc8a68d..e2213145b7 100644
--- a/frappe/tests/test_oauth20.py
+++ b/frappe/tests/test_oauth20.py
@@ -6,6 +6,7 @@ import unittest, frappe, requests, time
from frappe.test_runner import make_test_records
from six.moves.urllib.parse import urlparse, parse_qs, urljoin
from urllib.parse import urlencode, quote
+from frappe.integrations.oauth2 import encode_params
class TestOAuth20(unittest.TestCase):
@@ -232,13 +233,3 @@ def login(session):
def get_full_url(endpoint):
"""Turn '/endpoint' into 'http://127.0.0.1:8000/endpoint'."""
return urljoin(frappe.utils.get_url(), endpoint)
-
-def encode_params(params):
- """
- Encode a dict of params into a query string.
-
- Use `quote_via=urllib.parse.quote` so that whitespaces will be encoded as
- `%20` instead of as `+`. This is needed because oauthlib cannot handle `+`
- as a whitespace.
- """
- return urlencode(params, quote_via=quote)
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/test_utils.py b/frappe/tests/test_utils.py
index 8cdfe3e1a9..ebba60b8e8 100644
--- a/frappe/tests/test_utils.py
+++ b/frappe/tests/test_utils.py
@@ -7,6 +7,10 @@ import unittest
from frappe.utils import evaluate_filters, money_in_words, scrub_urls, get_url
from frappe.utils import ceil, floor
+from PIL import Image
+from frappe.utils.image import strip_exif_data
+import io
+
class TestFilters(unittest.TestCase):
def test_simple_dict(self):
self.assertTrue(evaluate_filters({'doctype': 'User', 'status': 'Open'}, {'status': 'Open'}))
@@ -122,3 +126,14 @@ class TestHTMLUtils(unittest.TestCase):
clean = clean_email_html(sample)
self.assertTrue('
Hello ' in clean)
self.assertTrue('
text ' in clean)
+
+class TestImage(unittest.TestCase):
+ def test_strip_exif_data(self):
+ original_image = Image.open("../apps/frappe/frappe/tests/data/exif_sample_image.jpg")
+ original_image_content = io.open("../apps/frappe/frappe/tests/data/exif_sample_image.jpg", mode='rb').read()
+
+ new_image_content = strip_exif_data(original_image_content, "image/jpeg")
+ new_image = Image.open(io.BytesIO(new_image_content))
+
+ self.assertEqual(new_image._getexif(), None)
+ self.assertNotEqual(original_image._getexif(), new_image._getexif())
\ No newline at end of file
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/__init__.py b/frappe/utils/__init__.py
index c209ee13c9..5ac4de618d 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -66,9 +66,14 @@ def get_email_address(user=None):
def get_formatted_email(user, mail=None):
"""get Email Address of user formatted as: `John Doe
`"""
fullname = get_fullname(user)
+
if not mail:
- mail = get_email_address(user)
- return cstr(make_header(decode_header(formataddr((fullname, mail)))))
+ mail = get_email_address(user) or validate_email_address(user)
+
+ if not mail:
+ return ''
+ else:
+ return cstr(make_header(decode_header(formataddr((fullname, mail)))))
def extract_email_id(email):
"""fetch only the email part of the Email Address"""
@@ -729,3 +734,27 @@ def get_build_version():
# .build can sometimes not exist
# this is not a major problem so send fallback
return frappe.utils.random_string(8)
+
+def get_bench_relative_path(file_path):
+ """Fixes paths relative to the bench root directory if exists and returns the absolute path
+
+ Args:
+ file_path (str, Path): Path of a file that exists on the file system
+
+ Returns:
+ str: Absolute path of the file_path
+ """
+ if not os.path.exists(file_path):
+ base_path = '..'
+ elif file_path.startswith(os.sep):
+ base_path = os.sep
+ else:
+ base_path = '.'
+
+ file_path = os.path.join(base_path, file_path)
+
+ if not os.path.exists(file_path):
+ print('Invalid path {0}'.format(file_path[3:]))
+ sys.exit(1)
+
+ return os.path.abspath(file_path)
diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py
index bc13158954..3ae300d3c4 100644
--- a/frappe/utils/backups.py
+++ b/frappe/utils/backups.py
@@ -2,11 +2,12 @@
# MIT License. See license.txt
# imports - standard imports
-import json
+import gzip
import os
from calendar import timegm
from datetime import datetime
from glob import glob
+from shutil import which
# imports - third party imports
import click
@@ -14,24 +15,42 @@ import click
# imports - module imports
import frappe
from frappe import _, conf
-from frappe.utils import get_url, now, now_datetime, get_file_size
+from frappe.utils import get_file_size, get_url, now, now_datetime
# backup variable for backwards compatibility
verbose = False
compress = False
_verbose = verbose
+base_tables = ["__Auth", "__global_search", "__UserSettings"]
class BackupGenerator:
"""
- This class contains methods to perform On Demand Backup
+ This class contains methods to perform On Demand Backup
- To initialize, specify (db_name, user, password, db_file_name=None, db_host="localhost")
- If specifying db_file_name, also append ".sql.gz"
+ To initialize, specify (db_name, user, password, db_file_name=None, db_host="localhost")
+ If specifying db_file_name, also append ".sql.gz"
"""
- def __init__(self, db_name, user, password, backup_path=None, backup_path_db=None,
- backup_path_files=None, backup_path_private_files=None, db_host="localhost", db_port=None,
- verbose=False, db_type='mariadb', backup_path_conf=None, compress_files=False):
+
+ def __init__(
+ self,
+ db_name,
+ user,
+ password,
+ backup_path=None,
+ backup_path_db=None,
+ backup_path_files=None,
+ backup_path_private_files=None,
+ db_host="localhost",
+ db_port=None,
+ db_type="mariadb",
+ backup_path_conf=None,
+ ignore_conf=False,
+ compress_files=False,
+ include_doctypes="",
+ exclude_doctypes="",
+ verbose=False,
+ ):
global _verbose
self.compress_files = compress_files or compress
self.db_host = db_host
@@ -45,23 +64,35 @@ class BackupGenerator:
self.backup_path_db = backup_path_db
self.backup_path_files = backup_path_files
self.backup_path_private_files = backup_path_private_files
+ self.ignore_conf = ignore_conf
+ self.include_doctypes = include_doctypes
+ self.exclude_doctypes = exclude_doctypes
+ self.partial = False
if not self.db_type:
- self.db_type = 'mariadb'
+ self.db_type = "mariadb"
- if not self.db_port and self.db_type == 'mariadb':
- self.db_port = 3306
- elif not self.db_port and self.db_type == 'postgres':
- self.db_port = 5432
+ if not self.db_port:
+ if self.db_type == "mariadb":
+ self.db_port = 3306
+ if self.db_type == "postgres":
+ self.db_port = 5432
site = frappe.local.site or frappe.generate_hash(length=8)
- self.site_slug = site.replace('.', '_')
+ self.site_slug = site.replace(".", "_")
self.verbose = verbose
self.setup_backup_directory()
+ self.setup_backup_tables()
_verbose = verbose
def setup_backup_directory(self):
- specified = self.backup_path or self.backup_path_db or self.backup_path_files or self.backup_path_private_files or self.backup_path_conf
+ specified = (
+ self.backup_path
+ or self.backup_path_db
+ or self.backup_path_files
+ or self.backup_path_private_files
+ or self.backup_path_conf
+ )
if not specified:
backups_folder = get_backup_path()
@@ -71,32 +102,93 @@ class BackupGenerator:
if self.backup_path:
os.makedirs(self.backup_path, exist_ok=True)
- for file_path in set([self.backup_path_files, self.backup_path_db, self.backup_path_private_files, self.backup_path_conf]):
+ for file_path in set(
+ [
+ self.backup_path_files,
+ self.backup_path_db,
+ self.backup_path_private_files,
+ self.backup_path_conf,
+ ]
+ ):
if file_path:
dir = os.path.dirname(file_path)
os.makedirs(dir, exist_ok=True)
+ def setup_backup_tables(self):
+ """Sets self.backup_includes, self.backup_excludes based on passed args"""
+ existing_doctypes = set([x.name for x in frappe.get_all("DocType")])
+
+ def get_tables(doctypes):
+ tables = []
+ for doctype in doctypes:
+ if doctype and doctype in existing_doctypes:
+ if doctype.startswith("tab"):
+ tables.append(doctype)
+ else:
+ tables.append("tab" + doctype)
+ return tables
+
+ passed_tables = {
+ "include": get_tables(self.include_doctypes.strip().split(",")),
+ "exclude": get_tables(self.exclude_doctypes.strip().split(",")),
+ }
+ specified_tables = get_tables(frappe.conf.get("backup", {}).get("includes", []))
+ include_tables = (specified_tables + base_tables) if specified_tables else []
+
+ conf_tables = {
+ "include": include_tables,
+ "exclude": get_tables(frappe.conf.get("backup", {}).get("excludes", [])),
+ }
+
+ self.backup_includes = passed_tables["include"]
+ self.backup_excludes = passed_tables["exclude"]
+
+ if not (self.backup_includes or self.backup_excludes) and not self.ignore_conf:
+ self.backup_includes = self.backup_includes or conf_tables["include"]
+ self.backup_excludes = self.backup_excludes or conf_tables["exclude"]
+
+ self.partial = (self.backup_includes or self.backup_excludes) and not self.ignore_conf
+
@property
def site_config_backup_path(self):
# For backwards compatibility
- click.secho("BackupGenerator.site_config_backup_path has been deprecated in favour of BackupGenerator.backup_path_conf", fg="yellow")
+ click.secho(
+ "BackupGenerator.site_config_backup_path has been deprecated in favour of"
+ " BackupGenerator.backup_path_conf",
+ fg="yellow",
+ )
return getattr(self, "backup_path_conf", None)
def get_backup(self, older_than=24, ignore_files=False, force=False):
"""
- Takes a new dump if existing file is old
- and sends the link to the file as email
+ Takes a new dump if existing file is old
+ and sends the link to the file as email
"""
- #Check if file exists and is less than a day old
- #If not Take Dump
+ # Check if file exists and is less than a day old
+ # If not Take Dump
if not force:
- last_db, last_file, last_private_file, site_config_backup_path = self.get_recent_backup(older_than)
+ (
+ last_db,
+ last_file,
+ last_private_file,
+ site_config_backup_path,
+ ) = self.get_recent_backup(older_than)
else:
- last_db, last_file, last_private_file, site_config_backup_path = False, False, False, False
+ last_db, last_file, last_private_file, site_config_backup_path = (
+ False,
+ False,
+ False,
+ False,
+ )
- self.todays_date = now_datetime().strftime('%Y%m%d_%H%M%S')
+ self.todays_date = now_datetime().strftime("%Y%m%d_%H%M%S")
- if not (self.backup_path_conf and self.backup_path_db and self.backup_path_files and self.backup_path_private_files):
+ if not (
+ self.backup_path_conf
+ and self.backup_path_db
+ and self.backup_path_files
+ and self.backup_path_private_files
+ ):
self.set_backup_file_name()
if not (last_db and last_file and last_private_file and site_config_backup_path):
@@ -112,13 +204,13 @@ class BackupGenerator:
self.backup_path_conf = site_config_backup_path
def set_backup_file_name(self):
- #Generate a random name using today's date and a 8 digit random number
- for_conf = self.todays_date + "-" + self.site_slug + "-site_config_backup.json"
- for_db = self.todays_date + "-" + self.site_slug + "-database.sql.gz"
+ partial = "-partial" if self.partial else ""
ext = "tgz" if self.compress_files else "tar"
- for_public_files = self.todays_date + "-" + self.site_slug + "-files." + ext
- for_private_files = self.todays_date + "-" + self.site_slug + "-private-files." + ext
+ for_conf = f"{self.todays_date}-{self.site_slug}-site_config_backup.json"
+ for_db = f"{self.todays_date}-{self.site_slug}{partial}-database.sql.gz"
+ for_public_files = f"{self.todays_date}-{self.site_slug}-files.{ext}"
+ for_private_files = f"{self.todays_date}-{self.site_slug}-private-files.{ext}"
backup_path = self.backup_path or get_backup_path()
if not self.backup_path_conf:
@@ -130,11 +222,11 @@ class BackupGenerator:
if not self.backup_path_private_files:
self.backup_path_private_files = os.path.join(backup_path, for_private_files)
- def get_recent_backup(self, older_than):
+ def get_recent_backup(self, older_than, partial=False):
backup_path = get_backup_path()
file_type_slugs = {
- "database": "*-{}-database.sql.gz",
+ "database": "*-{{}}-{}database.sql.gz".format('*' if partial else ''),
"public": "*-{}-files.tar",
"private": "*-{}-private-files.tar",
"config": "*-{}-site_config_backup.json",
@@ -158,8 +250,7 @@ class BackupGenerator:
return file_path
latest_backups = {
- file_type: get_latest(pattern)
- for file_type, pattern in file_type_slugs.items()
+ file_type: get_latest(pattern) for file_type, pattern in file_type_slugs.items()
}
recent_backups = {
@@ -175,32 +266,40 @@ class BackupGenerator:
def zip_files(self):
# For backwards compatibility - pre v13
- click.secho("BackupGenerator.zip_files has been deprecated in favour of BackupGenerator.backup_files", fg="yellow")
+ click.secho(
+ "BackupGenerator.zip_files has been deprecated in favour of"
+ " BackupGenerator.backup_files",
+ fg="yellow",
+ )
return self.backup_files()
def get_summary(self):
summary = {
"config": {
"path": self.backup_path_conf,
- "size": get_file_size(self.backup_path_conf, format=True)
+ "size": get_file_size(self.backup_path_conf, format=True),
},
"database": {
"path": self.backup_path_db,
- "size": get_file_size(self.backup_path_db, format=True)
- }
+ "size": get_file_size(self.backup_path_db, format=True),
+ },
}
- if os.path.exists(self.backup_path_files) and os.path.exists(self.backup_path_private_files):
- summary.update({
- "public": {
- "path": self.backup_path_files,
- "size": get_file_size(self.backup_path_files, format=True)
- },
- "private": {
- "path": self.backup_path_private_files,
- "size": get_file_size(self.backup_path_private_files, format=True)
+ if os.path.exists(self.backup_path_files) and os.path.exists(
+ self.backup_path_private_files
+ ):
+ summary.update(
+ {
+ "public": {
+ "path": self.backup_path_files,
+ "size": get_file_size(self.backup_path_files, format=True),
+ },
+ "private": {
+ "path": self.backup_path_private_files,
+ "size": get_file_size(self.backup_path_private_files, format=True),
+ },
}
- })
+ )
return summary
@@ -208,21 +307,29 @@ class BackupGenerator:
backup_summary = self.get_summary()
print("Backup Summary for {0} at {1}".format(frappe.local.site, now()))
+ title = max([len(x) for x in backup_summary])
+ path = max([len(x["path"]) for x in backup_summary.values()])
+
for _type, info in backup_summary.items():
- print("{0:8}: {1:85} {2}".format(_type.title(), info["path"], info["size"]))
+ template = "{{0:{0}}}: {{1:{1}}} {{2}}".format(title, path)
+ print(template.format(_type.title(), info["path"], info["size"]))
def backup_files(self):
import subprocess
for folder in ("public", "private"):
files_path = frappe.get_site_path(folder, "files")
- backup_path = self.backup_path_files if folder=="public" else self.backup_path_private_files
+ backup_path = (
+ self.backup_path_files if folder == "public" else self.backup_path_private_files
+ )
if self.compress_files:
cmd_string = "tar cf - {1} | gzip > {0}"
else:
cmd_string = "tar -cf {0} {1}"
- output = subprocess.check_output(cmd_string.format(backup_path, files_path), shell=True)
+ output = subprocess.check_output(
+ cmd_string.format(backup_path, files_path), shell=True
+ )
if self.verbose and output:
print(output.decode("utf8"))
@@ -236,34 +343,114 @@ class BackupGenerator:
def take_dump(self):
import frappe.utils
+ from frappe.utils.change_log import get_app_branch
+
+ db_exc = {
+ "mariadb": ("mysqldump", which("mysqldump")),
+ "postgres": ("pg_dump", which("pg_dump")),
+ }[self.db_type]
+ gzip_exc = which("gzip")
+
+ if not (gzip_exc and db_exc[1]):
+ _exc = "gzip" if not gzip_exc else db_exc[0]
+ frappe.throw(
+ f"{_exc} not found in PATH! This is required to take a backup.",
+ exc=frappe.ExecutableNotFound
+ )
+ db_exc = db_exc[0]
+
+ database_header_content = [
+ f"Backup generated by Frappe {frappe.__version__} on branch {get_app_branch('frappe') or 'N/A'}",
+ "",
+ ]
# escape reserved characters
- args = dict([item[0], frappe.utils.esc(str(item[1]), '$ ')]
- for item in self.__dict__.copy().items())
+ args = frappe._dict(
+ [item[0], frappe.utils.esc(str(item[1]), "$ ")]
+ for item in self.__dict__.copy().items()
+ )
- cmd_string = """mysqldump --single-transaction --quick --lock-tables=false -u %(user)s -p%(password)s %(db_name)s -h %(db_host)s -P %(db_port)s | gzip > %(backup_path_db)s """ % args
+ if self.backup_includes:
+ backup_info = ("Backing Up Tables: ", ", ".join(self.backup_includes))
+ elif self.backup_excludes:
+ backup_info = ("Skipping Tables: ", ", ".join(self.backup_excludes))
- if self.db_type == 'postgres':
- cmd_string = "pg_dump postgres://{user}:{password}@{db_host}:{db_port}/{db_name} | gzip > {backup_path_db}".format(
- user=args.get('user'),
- password=args.get('password'),
- db_host=args.get('db_host'),
- db_port=args.get('db_port'),
- db_name=args.get('db_name'),
- backup_path_db=args.get('backup_path_db')
+ if self.partial:
+ print(''.join(backup_info), "\n")
+ database_header_content.extend([
+ f"Partial Backup of Frappe Site {frappe.local.site}",
+ ("Backup contains: " if self.backup_includes else "Backup excludes: ") + backup_info[1],
+ "",
+ ])
+
+ generated_header = "\n".join([f"-- {x}" for x in database_header_content]) + "\n"
+
+ with gzip.open(args.backup_path_db, "wt") as f:
+ f.write(generated_header)
+
+ if self.db_type == "postgres":
+ if self.backup_includes:
+ args["include"] = " ".join(
+ ["--table='public.\"{0}\"'".format(table) for table in self.backup_includes]
+ )
+ elif self.backup_excludes:
+ args["exclude"] = " ".join(
+ ["--exclude-table-data='public.\"{0}\"'".format(table) for table in self.backup_excludes]
+ )
+
+ cmd_string = (
+ "{db_exc} postgres://{user}:{password}@{db_host}:{db_port}/{db_name}"
+ " {include} {exclude} | {gzip} >> {backup_path_db}"
)
- err, out = frappe.utils.execute_in_shell(cmd_string)
+ else:
+ if self.backup_includes:
+ args["include"] = " ".join(["'{0}'".format(x) for x in self.backup_includes])
+ elif self.backup_excludes:
+ args["exclude"] = " ".join(
+ [
+ "--ignore-table='{0}.{1}'".format(frappe.conf.db_name, table)
+ for table in self.backup_excludes
+ ]
+ )
+
+ cmd_string = (
+ "{db_exc} --single-transaction --quick --lock-tables=false -u {user}"
+ " -p{password} {db_name} -h {db_host} -P {db_port} {include} {exclude}"
+ " | {gzip} >> {backup_path_db}"
+ )
+
+ command = cmd_string.format(
+ user=args.user,
+ password=args.password,
+ db_exc=db_exc,
+ db_host=args.db_host,
+ db_port=args.db_port,
+ db_name=args.db_name,
+ backup_path_db=args.backup_path_db,
+ exclude=args.get("exclude", ""),
+ include=args.get("include", ""),
+ gzip=gzip_exc,
+ )
+
+ if self.verbose:
+ print(command + "\n")
+
+ err, out = frappe.utils.execute_in_shell(command)
def send_email(self):
"""
- Sends the link to backup file located at erpnext/backups
+ Sends the link to backup file located at erpnext/backups
"""
from frappe.email import get_system_managers
recipient_list = get_system_managers()
- db_backup_url = get_url(os.path.join('backups', os.path.basename(self.backup_path_db)))
- files_backup_url = get_url(os.path.join('backups', os.path.basename(self.backup_path_files)))
+ db_backup_url = get_url(
+ os.path.join("backups", os.path.basename(self.backup_path_db))
+ )
+ files_backup_url = get_url(
+ os.path.join("backups", os.path.basename(self.backup_path_files))
+ )
msg = """Hello,
@@ -275,11 +462,13 @@ Your backups are ready to be downloaded.
This link will be valid for 24 hours. A new backup will be available for
download only after 24 hours.""" % {
"db_backup_url": db_backup_url,
- "files_backup_url": files_backup_url
+ "files_backup_url": files_backup_url,
}
datetime_str = datetime.fromtimestamp(os.stat(self.backup_path_db).st_ctime)
- subject = datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded"""
+ subject = (
+ datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded"""
+ )
frappe.sendmail(recipients=recipient_list, msg=msg, subject=subject)
return recipient_list
@@ -288,20 +477,29 @@ download only after 24 hours.""" % {
@frappe.whitelist()
def get_backup():
"""
- This function is executed when the user clicks on
- Toos > Download Backup
+ This function is executed when the user clicks on
+ Toos > Download Backup
"""
delete_temp_backups()
- odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name,\
- frappe.conf.db_password, db_host = frappe.db.host,\
- db_type=frappe.conf.db_type, db_port=frappe.conf.db_port)
+ odb = BackupGenerator(
+ frappe.conf.db_name,
+ frappe.conf.db_name,
+ frappe.conf.db_password,
+ db_host=frappe.db.host,
+ db_type=frappe.conf.db_type,
+ db_port=frappe.conf.db_port,
+ )
odb.get_backup()
recipient_list = odb.send_email()
- frappe.msgprint(_("Download link for your backup will be emailed on the following email address: {0}").format(', '.join(recipient_list)))
+ frappe.msgprint(
+ _(
+ "Download link for your backup will be emailed on the following email address: {0}"
+ ).format(", ".join(recipient_list))
+ )
@frappe.whitelist()
-def fetch_latest_backups():
+def fetch_latest_backups(partial=False):
"""Fetches paths of the latest backup taken in the last 30 days
Only for: System Managers
@@ -317,43 +515,88 @@ def fetch_latest_backups():
db_type=frappe.conf.db_type,
db_port=frappe.conf.db_port,
)
- database, public, private, config = odb.get_recent_backup(older_than=24 * 30)
+ database, public, private, config = odb.get_recent_backup(older_than=24 * 30, partial=partial)
- return {
- "database": database,
- "public": public,
- "private": private,
- "config": config
- }
+ return {"database": database, "public": public, "private": private, "config": config}
-def scheduled_backup(older_than=6, ignore_files=False, backup_path=None, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, backup_path_conf=None, force=False, verbose=False, compress=False):
+def scheduled_backup(
+ older_than=6,
+ ignore_files=False,
+ backup_path=None,
+ backup_path_db=None,
+ backup_path_files=None,
+ backup_path_private_files=None,
+ backup_path_conf=None,
+ ignore_conf=False,
+ include_doctypes="",
+ exclude_doctypes="",
+ compress=False,
+ force=False,
+ verbose=False,
+):
"""this function is called from scheduler
- deletes backups older than 7 days
- takes backup"""
- odb = new_backup(older_than, ignore_files, backup_path=backup_path, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, backup_path_conf=backup_path_conf, force=force, verbose=verbose, compress=compress)
+ deletes backups older than 7 days
+ takes backup"""
+ odb = new_backup(
+ older_than=older_than,
+ ignore_files=ignore_files,
+ backup_path=backup_path,
+ backup_path_db=backup_path_db,
+ backup_path_files=backup_path_files,
+ backup_path_private_files=backup_path_private_files,
+ backup_path_conf=backup_path_conf,
+ ignore_conf=ignore_conf,
+ include_doctypes=include_doctypes,
+ exclude_doctypes=exclude_doctypes,
+ compress=compress,
+ force=force,
+ verbose=verbose,
+ )
return odb
-def new_backup(older_than=6, ignore_files=False, backup_path=None, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, backup_path_conf=None, force=False, verbose=False, compress=False):
- delete_temp_backups(older_than = frappe.conf.keep_backups_for_hours or 24)
- odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name,\
- frappe.conf.db_password,
- backup_path=backup_path,
- backup_path_db=backup_path_db,
- backup_path_files=backup_path_files,
- backup_path_private_files=backup_path_private_files,
- backup_path_conf=backup_path_conf,
- db_host = frappe.db.host,
- db_port = frappe.db.port,
- db_type = frappe.conf.db_type,
- verbose=verbose,
- compress_files=compress)
+
+def new_backup(
+ older_than=6,
+ ignore_files=False,
+ backup_path=None,
+ backup_path_db=None,
+ backup_path_files=None,
+ backup_path_private_files=None,
+ backup_path_conf=None,
+ ignore_conf=False,
+ include_doctypes="",
+ exclude_doctypes="",
+ compress=False,
+ force=False,
+ verbose=False,
+):
+ delete_temp_backups(older_than=frappe.conf.keep_backups_for_hours or 24)
+ odb = BackupGenerator(
+ frappe.conf.db_name,
+ frappe.conf.db_name,
+ frappe.conf.db_password,
+ db_host=frappe.db.host,
+ db_port=frappe.db.port,
+ db_type=frappe.conf.db_type,
+ backup_path=backup_path,
+ backup_path_db=backup_path_db,
+ backup_path_files=backup_path_files,
+ backup_path_private_files=backup_path_private_files,
+ backup_path_conf=backup_path_conf,
+ ignore_conf=ignore_conf,
+ include_doctypes=include_doctypes,
+ exclude_doctypes=exclude_doctypes,
+ verbose=verbose,
+ compress_files=compress,
+ )
odb.get_backup(older_than, ignore_files, force=force)
return odb
+
def delete_temp_backups(older_than=24):
"""
- Cleans up the backup_link_path directory by deleting files older than 24 hours
+ Cleans up the backup_link_path directory by deleting files older than 24 hours
"""
backup_path = get_backup_path()
if os.path.exists(backup_path):
@@ -363,54 +606,68 @@ def delete_temp_backups(older_than=24):
if is_file_old(this_file_path, older_than):
os.remove(this_file_path)
-def is_file_old(db_file_name, older_than=24):
- """
- Checks if file exists and is older than specified hours
- Returns ->
- True: file does not exist or file is old
- False: file is new
- """
- if os.path.isfile(db_file_name):
- from datetime import timedelta
- #Get timestamp of the file
- file_datetime = datetime.fromtimestamp\
- (os.stat(db_file_name).st_ctime)
- if datetime.today() - file_datetime >= timedelta(hours = older_than):
- if _verbose:
- print("File is old")
- return True
- else:
- if _verbose:
- print("File is recent")
- return False
+
+def is_file_old(file_path, older_than=24):
+ """
+ Checks if file exists and is older than specified hours
+ Returns ->
+ True: file does not exist or file is old
+ False: file is new
+ """
+ if os.path.isfile(file_path):
+ from datetime import timedelta
+
+ # Get timestamp of the file
+ file_datetime = datetime.fromtimestamp(os.stat(file_path).st_ctime)
+ if datetime.today() - file_datetime >= timedelta(hours=older_than):
+ if _verbose:
+ print(f"File {file_path} is older than {older_than} hours")
+ return True
else:
if _verbose:
- print("File does not exist")
- return True
+ print(f"File {file_path} is recent")
+ return False
+ else:
+ if _verbose:
+ print(f"File {file_path} does not exist")
+ return True
+
def get_backup_path():
backup_path = frappe.utils.get_site_path(conf.get("backup_path", "private/backups"))
return backup_path
-def backup(with_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, backup_path_conf=None, quiet=False):
+
+def backup(
+ with_files=False,
+ backup_path_db=None,
+ backup_path_files=None,
+ backup_path_private_files=None,
+ backup_path_conf=None,
+ quiet=False,
+):
"Backup"
- odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, backup_path_conf=backup_path_conf, force=True)
+ odb = scheduled_backup(
+ ignore_files=not with_files,
+ backup_path_db=backup_path_db,
+ backup_path_files=backup_path_files,
+ backup_path_private_files=backup_path_private_files,
+ backup_path_conf=backup_path_conf,
+ force=True,
+ )
return {
"backup_path_db": odb.backup_path_db,
"backup_path_files": odb.backup_path_files,
- "backup_path_private_files": odb.backup_path_private_files
+ "backup_path_private_files": odb.backup_path_private_files,
}
if __name__ == "__main__":
- """
- is_file_old db_name user password db_host db_type db_port
- get_backup db_name user password db_host db_type db_port
- """
import sys
+
cmd = sys.argv[1]
- db_type = 'mariadb'
+ db_type = "mariadb"
try:
db_type = sys.argv[6]
except IndexError:
@@ -423,19 +680,47 @@ if __name__ == "__main__":
pass
if cmd == "is_file_old":
- odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port)
+ odb = BackupGenerator(
+ sys.argv[2],
+ sys.argv[3],
+ sys.argv[4],
+ sys.argv[5] or "localhost",
+ db_type=db_type,
+ db_port=db_port,
+ )
is_file_old(odb.db_file_name)
if cmd == "get_backup":
- odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port)
+ odb = BackupGenerator(
+ sys.argv[2],
+ sys.argv[3],
+ sys.argv[4],
+ sys.argv[5] or "localhost",
+ db_type=db_type,
+ db_port=db_port,
+ )
odb.get_backup()
if cmd == "take_dump":
- odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port)
+ odb = BackupGenerator(
+ sys.argv[2],
+ sys.argv[3],
+ sys.argv[4],
+ sys.argv[5] or "localhost",
+ db_type=db_type,
+ db_port=db_port,
+ )
odb.take_dump()
if cmd == "send_email":
- odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port)
+ odb = BackupGenerator(
+ sys.argv[2],
+ sys.argv[3],
+ sys.argv[4],
+ sys.argv[5] or "localhost",
+ db_type=db_type,
+ db_port=db_port,
+ )
odb.send_email("abc.sql.gz")
if cmd == "delete_temp_backups":
diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py
index 29fee2bac0..33801af722 100644
--- a/frappe/utils/change_log.py
+++ b/frappe/utils/change_log.py
@@ -1,15 +1,17 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-from six.moves import range
-import json, os
-from semantic_version import Version
-import frappe
+import json
+import os
+import subprocess # nosec
+
import requests
-import subprocess # nosec
-from frappe.utils import cstr
+from semantic_version import Version
+from six.moves import range
+
+import frappe
from frappe import _, safe_decode
+from frappe.utils import cstr
def get_change_log(user=None):
@@ -165,9 +167,10 @@ def check_for_update():
add_message_to_redis(updates)
+
def parse_latest_non_beta_release(response):
"""
- Pasrses the response JSON for all the releases and returns the latest non prerelease
+ Parses the response JSON for all the releases and returns the latest non prerelease
Parameters
response (list): response object returned by github
@@ -182,32 +185,51 @@ def parse_latest_non_beta_release(response):
return None
-def check_release_on_github(app):
- # Check if repo remote is on github
- from subprocess import CalledProcessError
+
+def check_release_on_github(app: str):
+ """
+ Check the latest release for a given Frappe application hosted on Github.
+
+ Args:
+ app (str): The name of the Frappe application.
+
+ Returns:
+ tuple(Version, str): The semantic version object of the latest release and the
+ organization name, if the application exists, otherwise None.
+ """
+
+ from giturlparse import parse
+ from giturlparse.parser import ParserError
+
try:
- remote_url = subprocess.check_output("cd ../apps/{} && git ls-remote --get-url".format(app), shell=True).decode()
- except CalledProcessError:
- # Passing this since some apps may not have git initializaed in them
- return None
+ # Check if repo remote is on github
+ remote_url = subprocess.check_output("cd ../apps/{} && git ls-remote --get-url".format(app), shell=True)
+ except subprocess.CalledProcessError:
+ # Passing this since some apps may not have git initialized in them
+ return
if isinstance(remote_url, bytes):
remote_url = remote_url.decode()
- if "github.com" not in remote_url:
- return None
+ try:
+ parsed_url = parse(remote_url)
+ except ParserError:
+ # Invalid URL
+ return
- # Get latest version from github
- if 'https' not in remote_url:
- return None
+ if parsed_url.resource != "github.com":
+ return
- org_name = remote_url.split('/')[3]
- r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(org_name, app))
+ owner = parsed_url.owner
+ repo = parsed_url.name
+
+ # Get latest version from GitHub
+ r = requests.get(f"https://api.github.com/repos/{owner}/{repo}/releases")
if r.ok:
- lastest_non_beta_release = parse_latest_non_beta_release(r.json())
- return Version(lastest_non_beta_release), org_name
- # In case of an improper response or if there are no releases
- return None
+ latest_non_beta_release = parse_latest_non_beta_release(r.json())
+ if latest_non_beta_release:
+ return Version(latest_non_beta_release), owner
+
def add_message_to_redis(update_json):
# "update-message" will store the update message string
diff --git a/frappe/utils/dashboard.py b/frappe/utils/dashboard.py
index 7eaf470767..e386dcd881 100644
--- a/frappe/utils/dashboard.py
+++ b/frappe/utils/dashboard.py
@@ -61,21 +61,6 @@ def generate_and_cache_results(args, function, cache_key, chart):
frappe.db.set_value("Dashboard Chart", args.chart_name, "last_synced_on", frappe.utils.now(), update_modified = False)
return results
-def get_from_date_from_timespan(to_date, timespan):
- days = months = years = 0
- if timespan == "Last Week":
- days = -7
- if timespan == "Last Month":
- months = -1
- elif timespan == "Last Quarter":
- months = -3
- elif timespan == "Last Year":
- years = -1
- elif timespan == "All Time":
- years = -50
- return add_to_date(to_date, years=years, months=months, days=days,
- as_datetime=True)
-
def get_dashboards_with_link(docname, doctype):
dashboards = []
links = []
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index 41f247da45..4a88b5fda1 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -221,6 +221,27 @@ def get_last_day(dt):
"""
return get_first_day(dt, 0, 1) + datetime.timedelta(-1)
+def get_quarter_ending(date):
+ date = getdate(date)
+
+ # find the earliest quarter ending date that is after
+ # the given date
+ for month in (3, 6, 9, 12):
+ quarter_end_month = getdate('{}-{}-01'.format(date.year, month))
+ quarter_end_date = getdate(get_last_day(quarter_end_month))
+ if date <= quarter_end_date:
+ date = quarter_end_date
+ break
+
+ return date
+
+def get_year_ending(date):
+ ''' returns year ending of the given date '''
+
+ # first day of next year (note year starts from 1)
+ date = add_to_date('{}-01-01'.format(date.year), months = 12)
+ # last day of this month
+ return add_to_date(date, days=-1)
def get_time(time_str):
if isinstance(time_str, datetime.datetime):
@@ -348,6 +369,8 @@ def format_duration(seconds, hide_days=False):
example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float
"""
+
+ seconds = cint(seconds)
total_duration = {
'days': math.floor(seconds / (3600 * 24)),
@@ -1300,12 +1323,14 @@ def generate_hash(*args, **kwargs):
def guess_date_format(date_string):
DATE_FORMATS = [
+ r"%d/%b/%y",
r"%d-%m-%Y",
r"%m-%d-%Y",
r"%Y-%m-%d",
r"%d-%m-%y",
r"%m-%d-%y",
r"%y-%m-%d",
+ r"%y-%b-%d",
r"%d/%m/%Y",
r"%m/%d/%Y",
r"%Y/%m/%d",
diff --git a/frappe/utils/dateutils.py b/frappe/utils/dateutils.py
index 90abdeb6cd..06b434a512 100644
--- a/frappe/utils/dateutils.py
+++ b/frappe/utils/dateutils.py
@@ -5,10 +5,9 @@ from __future__ import unicode_literals
import frappe
import frappe.defaults
import datetime
-from frappe.utils import get_datetime
-from frappe.utils import add_to_date, getdate
-from frappe.utils.data import get_last_day_of_week
-from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending
+from frappe.utils import get_datetime, add_to_date, getdate
+from frappe.utils.data import get_first_day, get_first_day_of_week, get_quarter_start, get_year_start,\
+ get_last_day, get_last_day_of_week, get_quarter_ending, get_year_ending
from six import string_types
# global values -- used for caching
@@ -102,4 +101,52 @@ def get_dates_from_timegrain(from_date, to_date, timegrain="Daily"):
else:
date = get_period_ending(add_to_date(dates[-1], years=years, months=months, days=days), timegrain)
dates.append(date)
- return dates
\ No newline at end of file
+ return dates
+
+def get_from_date_from_timespan(to_date, timespan):
+ days = months = years = 0
+ if timespan == "Last Week":
+ days = -7
+ if timespan == "Last Month":
+ months = -1
+ elif timespan == "Last Quarter":
+ months = -3
+ elif timespan == "Last Year":
+ years = -1
+ elif timespan == "All Time":
+ years = -50
+ return add_to_date(to_date, years=years, months=months, days=days,
+ as_datetime=True)
+
+def get_period(date, interval='Monthly'):
+ date = getdate(date)
+ months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+ return {
+ 'Daily': date.strftime('%d-%m-%y'),
+ 'Weekly': date.strftime('%d-%m-%y'),
+ 'Monthly': str(months[date.month - 1]) + ' ' + str(date.year),
+ 'Quarterly': 'Quarter ' + str(((date.month-1)//3)+1) + ' ' + str(date.year),
+ 'Yearly': str(date.year)
+ }[interval]
+
+def get_period_beginning(date, timegrain, as_str=True):
+ return getdate({
+ 'Daily': date,
+ 'Weekly': get_first_day_of_week(date),
+ 'Monthly': get_first_day(date),
+ 'Quarterly': get_quarter_start(date),
+ 'Yearly': get_year_start(date)
+ }[timegrain])
+
+def get_period_ending(date, timegrain):
+ date = getdate(date)
+ if timegrain == 'Daily':
+ return date
+ else:
+ return getdate({
+ 'Daily': date,
+ 'Weekly': get_last_day_of_week(date),
+ 'Monthly': get_last_day(date),
+ 'Quarterly': get_quarter_ending(date),
+ 'Yearly': get_year_ending(date)
+ }[timegrain])
diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py
index e945039d0d..f605c3bf66 100644
--- a/frappe/utils/global_search.py
+++ b/frappe/utils/global_search.py
@@ -10,6 +10,7 @@ import json
import os
from bs4 import BeautifulSoup
from frappe.utils import cint, strip_html_tags
+from frappe.utils.html_utils import unescape_html
from frappe.model.base_document import get_controller
from six import text_type
@@ -345,11 +346,8 @@ def get_formatted_value(value, field):
:return:
"""
- from six.moves.html_parser import HTMLParser
-
if getattr(field, 'fieldtype', None) in ["Text", "Text Editor"]:
- h = HTMLParser()
- value = h.unescape(frappe.safe_decode(value))
+ value = unescape_html(frappe.safe_decode(value))
value = (re.subn(r'<[\s]*(script|style).*?\1>(?s)', '', text_type(value))[0])
value = ' '.join(value.split())
return field.label + " : " + strip_html_tags(text_type(value))
diff --git a/frappe/utils/html_utils.py b/frappe/utils/html_utils.py
index 302813645e..bccdbd9441 100644
--- a/frappe/utils/html_utils.py
+++ b/frappe/utils/html_utils.py
@@ -34,7 +34,7 @@ def clean_email_html(html):
'margin', 'margin-top', 'margin-bottom', 'margin-left', 'margin-right',
'padding', 'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
'font-size', 'font-weight', 'font-family', 'text-decoration',
- 'line-height', 'text-align', 'vertical-align'
+ 'line-height', 'text-align', 'vertical-align', 'display'
],
protocols=['cid', 'http', 'https', 'mailto', 'data'],
strip=True, strip_comments=True)
@@ -106,9 +106,8 @@ def get_icon_html(icon, small=False):
return " ".format(icon=icon)
def unescape_html(value):
- from six.moves.html_parser import HTMLParser
- h = HTMLParser()
- return h.unescape(value)
+ from html import unescape
+ return unescape(value)
# adapted from https://raw.githubusercontent.com/html5lib/html5lib-python/4aa79f113e7486c7ec5d15a6e1777bfe546d3259/html5lib/sanitizer.py
acceptable_elements = [
diff --git a/frappe/utils/image.py b/frappe/utils/image.py
index 1eada5acca..60595464a1 100644
--- a/frappe/utils/image.py
+++ b/frappe/utils/image.py
@@ -5,7 +5,7 @@ from __future__ import unicode_literals, print_function
import os
def resize_images(path, maxdim=700):
- import Image
+ from PIL import Image
size = (maxdim, maxdim)
for basepath, folders, files in os.walk(path):
for fname in files:
@@ -17,3 +17,27 @@ def resize_images(path, maxdim=700):
im.save(os.path.join(basepath, fname))
print("resized {0}".format(os.path.join(basepath, fname)))
+
+def strip_exif_data(content, content_type):
+ """ Strips EXIF from image files which support it.
+
+ Works by creating a new Image object which ignores exif by
+ default and then extracts the binary data back into content.
+
+ Returns:
+ Bytes: Stripped image content
+ """
+
+ from PIL import Image
+ import io
+
+ original_image = Image.open(io.BytesIO(content))
+ output = io.BytesIO()
+
+ new_image = Image.new(original_image.mode, original_image.size)
+ new_image.putdata(list(original_image.getdata()))
+ new_image.save(output, format=content_type.split('/')[1])
+
+ content = output.getvalue()
+
+ return content
\ No newline at end of file
diff --git a/frappe/utils/response.py b/frappe/utils/response.py
index 20b5ea5678..c35ebc751e 100644
--- a/frappe/utils/response.py
+++ b/frappe/utils/response.py
@@ -123,7 +123,7 @@ def make_logs(response = None):
def json_handler(obj):
"""serialize non-serializable data for json"""
# serialize date
- import collections
+ import collections.abc
if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)):
return text_type(obj)
@@ -138,7 +138,7 @@ def json_handler(obj):
doc = obj.as_dict(no_nulls=True)
return doc
- elif isinstance(obj, collections.Iterable):
+ elif isinstance(obj, collections.abc.Iterable):
return list(obj)
elif type(obj)==type or isinstance(obj, Exception):
diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py
index fee6b404ac..2aacf5eda8 100644
--- a/frappe/utils/safe_exec.py
+++ b/frappe/utils/safe_exec.py
@@ -13,7 +13,19 @@ from frappe.www.printview import get_visible_columns
import frappe.exceptions
import frappe.integrations.utils
-class ServerScriptNotEnabled(frappe.PermissionError): pass
+class ServerScriptNotEnabled(frappe.PermissionError):
+ pass
+
+class NamespaceDict(frappe._dict):
+ """Raise AttributeError if function not found in namespace"""
+ def __getattr__(self, key):
+ ret = self.get(key)
+ if (not ret and key.startswith("__")) or (key not in self):
+ def default_function(*args, **kwargs):
+ raise AttributeError(f"module has no attribute '{key}'")
+ return default_function
+ return ret
+
def safe_exec(script, _globals=None, _locals=None):
# script reports must be enabled via site_config.json
@@ -46,13 +58,13 @@ def get_safe_globals():
user = getattr(frappe.local, "session", None) and frappe.local.session.user or "Guest"
- out = frappe._dict(
+ out = NamespaceDict(
# make available limited methods of frappe
json=json,
dict=dict,
log=frappe.log,
_dict=frappe._dict,
- frappe=frappe._dict(
+ frappe=NamespaceDict(
flags=frappe._dict(),
format=frappe.format_value,
format_value=frappe.format_value,
@@ -112,7 +124,7 @@ def get_safe_globals():
out.get_visible_columns = get_visible_columns
out.frappe.date_format = date_format
out.frappe.time_format = time_format
- out.frappe.db = frappe._dict(
+ out.frappe.db = NamespaceDict(
get_list = frappe.get_list,
get_all = frappe.get_all,
get_value = frappe.db.get_value,
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/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/workflow/doctype/workflow/workflow.json b/frappe/workflow/doctype/workflow/workflow.json
index 3cb72d0eed..e8db8dcb10 100644
--- a/frappe/workflow/doctype/workflow/workflow.json
+++ b/frappe/workflow/doctype/workflow/workflow.json
@@ -99,7 +99,7 @@
"icon": "fa fa-random",
"idx": 1,
"links": [],
- "modified": "2020-07-16 04:29:20.898040",
+ "modified": "2020-12-17 20:35:16.898040",
"modified_by": "Administrator",
"module": "Workflow",
"name": "Workflow",
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\nConditions should be written in simple Python. Please use properties available in the form only.
",
+ "options": "doc.grand_total > 0 \n\nConditions should be written in simple Python. Please use properties available in the form only.
\nAllowed functions: \n
\nfrappe.db.get_value \nfrappe.db.get_list \nfrappe.session \nfrappe.utils.now_datetime \nfrappe.utils.get_datetime \nfrappe.utils.add_to_date \nfrappe.utils.now \n \nExample:
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/frappe/www/printview.py b/frappe/www/printview.py
index 545e5d581d..71316dc48c 100644
--- a/frappe/www/printview.py
+++ b/frappe/www/printview.py
@@ -100,6 +100,7 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None,
doc.print_section_headings = print_format.show_section_headings
doc.print_line_breaks = print_format.line_breaks
doc.align_labels_right = print_format.align_labels_right
+ doc.absolute_value = print_format.absolute_value
def get_template_from_string():
return jenv.from_string(get_print_format(doc.doctype,
diff --git a/package.json b/package.json
index c9eb9c0e56..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.1",
+ "highlight.js": "^10.4.1",
"js-sha256": "^0.9.0",
"jsbarcode": "^3.9.0",
"moment": "^2.20.1",
@@ -44,8 +44,8 @@
"qz-tray": "^2.0.8",
"redis": "^2.8.0",
"showdown": "^1.9.1",
- "snyk": "^1.398.1",
- "socket.io": "^2.3.0",
+ "snyk": "^1.425.4",
+ "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 de9e675a67..3cc92264a2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -15,6 +15,7 @@ Faker==2.0.4
future==0.18.2
gitdb2==2.0.6;python_version<'3.4'
GitPython==2.1.15
+git-url-parse==1.2.2
google-api-python-client==1.9.3
google-auth-httplib2==0.0.3
google-auth-oauthlib==0.4.1
@@ -34,7 +35,7 @@ oauthlib==3.1.0
openpyxl==2.6.4
passlib==1.7.3
pdfkit==0.6.1
-Pillow==7.1.0
+Pillow>=8.0.0
premailer==3.6.1
psycopg2-binary==2.8.4
pyasn1==0.4.8
@@ -73,4 +74,4 @@ pycryptodome==3.9.8
paytmchecksum==1.7.0
wrapt==1.10.11
razorpay==1.2.0
-rsa>=4.1 # not directly required, pinned by Snyk to avoid a vulnerability
\ No newline at end of file
+rsa>=4.1 # not directly required, pinned by Snyk to avoid a vulnerability
diff --git a/yarn.lock b/yarn.lock
index 15a6321ae2..3810b88e47 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -93,10 +93,21 @@
source-map-support "^0.5.19"
tslib "^1.13.0"
-"@snyk/docker-registry-v2-client@^1.13.5":
- version "1.13.5"
- resolved "https://registry.yarnpkg.com/@snyk/docker-registry-v2-client/-/docker-registry-v2-client-1.13.5.tgz#8d862f0c53d4a9a25db09cd48b4cd44aa8e385c9"
- integrity sha512-lgJiC071abCpFVLp47OnykU8MMrhdQe386Wt6QaDmjI0s2DQn/S58NfdLrPU7s6l4zoGT7UwRW9+7paozRgFTA==
+"@snyk/dep-graph@^1.19.5":
+ version "1.20.0"
+ resolved "https://registry.yarnpkg.com/@snyk/dep-graph/-/dep-graph-1.20.0.tgz#258ae85f8a066dc63af4444cfca8b8d092b94bc0"
+ integrity sha512-/TOzXGh+JFgAu8pWdo1oLFKDNfFk99TnSQG2lbEu+vKLI2ZrGAk9oGO0geNogAN7Ib4EDQOEhgb7YwqwL7aA7w==
+ dependencies:
+ graphlib "^2.1.8"
+ lodash.isequal "^4.5.0"
+ object-hash "^2.0.3"
+ semver "^6.0.0"
+ tslib "^1.13.0"
+
+"@snyk/docker-registry-v2-client@1.13.9":
+ version "1.13.9"
+ resolved "https://registry.yarnpkg.com/@snyk/docker-registry-v2-client/-/docker-registry-v2-client-1.13.9.tgz#54c2e3071de58fc6fc12c5fef5eaeae174ecda12"
+ integrity sha512-DIFLEhr8m1GrAwsLGInJmpcQMacjuhf3jcbpQTR+LeMvZA9IuKq+B7kqw2O2FzMiHMZmUb5z+tV+BR7+IUHkFQ==
dependencies:
needle "^2.5.0"
parse-link-header "^1.0.1"
@@ -107,10 +118,10 @@
resolved "https://registry.yarnpkg.com/@snyk/gemfile/-/gemfile-1.2.0.tgz#919857944973cce74c650e5428aaf11bcd5c0457"
integrity sha512-nI7ELxukf7pT4/VraL4iabtNNMz8mUo7EXlqCFld8O5z6mIMLX9llps24iPpaIZOwArkY3FWA+4t+ixyvtTSIA==
-"@snyk/java-call-graph-builder@1.13.2":
- version "1.13.2"
- resolved "https://registry.yarnpkg.com/@snyk/java-call-graph-builder/-/java-call-graph-builder-1.13.2.tgz#6e4a9495d5c47bbab9bc69e066d4646473781b67"
- integrity sha512-YN3a93ttscqFQRUeThrxa7i2SJkFPfYn0VpFqdPB6mIJz2fRVLxUkMtlCbG0aSEUvWiLnGVHN0IYxwWEzhq11w==
+"@snyk/java-call-graph-builder@1.16.2":
+ version "1.16.2"
+ resolved "https://registry.yarnpkg.com/@snyk/java-call-graph-builder/-/java-call-graph-builder-1.16.2.tgz#a9f9a34107759cf2be847a114a759e347cef44e8"
+ integrity sha512-tJF+dY/wTfexwYuCgFB3RpWl4RGcf2H9RT9yurkTVi5wwKfvcNwZMUMwSlTDEFOqwmAsJ7e0uNVRlkPQHekCcQ==
dependencies:
ci-info "^2.0.0"
debug "^4.1.1"
@@ -119,11 +130,29 @@
jszip "^3.2.2"
needle "^2.3.3"
progress "^2.0.3"
- snyk-config "^3.0.0"
+ snyk-config "^4.0.0-rc.2"
source-map-support "^0.5.7"
temp-dir "^2.0.0"
tslib "^1.9.3"
+"@snyk/java-call-graph-builder@1.16.5":
+ version "1.16.5"
+ resolved "https://registry.yarnpkg.com/@snyk/java-call-graph-builder/-/java-call-graph-builder-1.16.5.tgz#e57302cc6dc93f1adff7abe1e5eecff26d8a41f4"
+ integrity sha512-6H4hkq/qYljJoH1QnZsTRPMqp9Kt5AOEZYGJAeSHkhJdfUYSLtqwN4WsU6yVR3vWAaDQ8Lllp3m6EL7nstMPZA==
+ dependencies:
+ ci-info "^2.0.0"
+ debug "^4.1.1"
+ glob "^7.1.6"
+ graphlib "^2.1.8"
+ jszip "^3.2.2"
+ needle "^2.3.3"
+ progress "^2.0.3"
+ snyk-config "^4.0.0-rc.2"
+ source-map-support "^0.5.7"
+ temp-dir "^2.0.0"
+ tmp "^0.2.1"
+ tslib "^1.9.3"
+
"@snyk/rpm-parser@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@snyk/rpm-parser/-/rpm-parser-2.0.0.tgz#4ded7fa4b0a8efca7699359e4ca7a79bfbe38bc1"
@@ -142,12 +171,12 @@
source-map-support "^0.5.7"
tslib "^2.0.0"
-"@snyk/snyk-docker-pull@^3.2.0":
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/@snyk/snyk-docker-pull/-/snyk-docker-pull-3.2.0.tgz#07c47b8be2d899d51d720099a73a0d89effe5d99"
- integrity sha512-uWKtjh29I/d0mfmfBN7w6RwwNBQxQVKrauF5ND/gqb0PVsKV22GIpkI+viWjI7KNKso6/B0tMmsv7TX2tsNcLQ==
+"@snyk/snyk-docker-pull@3.2.3":
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/@snyk/snyk-docker-pull/-/snyk-docker-pull-3.2.3.tgz#9743ea624098c7abd0f95c438c76067530494f4b"
+ integrity sha512-hiFiSmWGLc2tOI7FfgIhVdFzO2f69im8O6p3OV4xEZ/Ss1l58vwtqudItoswsk7wj/azRlgfBW8wGu2MjoudQg==
dependencies:
- "@snyk/docker-registry-v2-client" "^1.13.5"
+ "@snyk/docker-registry-v2-client" "1.13.9"
child-process "^1.0.2"
tar-stream "^2.1.2"
tmp "^0.1.0"
@@ -540,15 +569,10 @@ 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@^1.4.0:
- version "1.5.2"
- resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
- integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=
+async@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
+ integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
asynckit@^0.4.0:
version "0.4.0"
@@ -648,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"
@@ -757,6 +774,13 @@ braces@^2.3.1:
split-string "^3.0.2"
to-regex "^3.0.1"
+braces@^3.0.1:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+ integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+ dependencies:
+ fill-range "^7.0.1"
+
browserify-zlib@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d"
@@ -878,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"
@@ -896,7 +915,7 @@ camelcase-keys@^2.0.0:
camelcase "^2.0.0"
map-obj "^1.0.0"
-camelcase@^2.0.0, camelcase@^2.0.1:
+camelcase@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
@@ -1021,15 +1040,6 @@ cli-width@^3.0.0:
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
-cliui@^3.0.3:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
- integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=
- dependencies:
- string-width "^1.0.1"
- strip-ansi "^3.0.1"
- wrap-ansi "^2.0.0"
-
cliui@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
@@ -1145,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"
@@ -1203,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"
@@ -1507,7 +1522,14 @@ debug@^3.1.0, debug@^3.2.5, debug@^3.2.6:
dependencies:
ms "^2.1.1"
-decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
+debug@^4.2.0:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+ integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
+ dependencies:
+ ms "2.1.2"
+
+decamelize@^1.1.2, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@@ -1751,6 +1773,13 @@ electron-to-chromium@^1.3.523:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.551.tgz#a94d243a4ca90705189bd4a5eca4e0f56b745a4f"
integrity sha512-11qcm2xvf2kqeFO5EIejaBx5cKXsW1quAyv3VctCMYwofnyVZLs97y6LCekss3/ghQpr7PYkSO3uId5FmxZsdw==
+elfy@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/elfy/-/elfy-1.0.0.tgz#7a1c86af7d41e0a568cbb4a3fa5b685648d9efcd"
+ integrity sha512-4Kp3AA94jC085IJox+qnvrZ3PudqTi4gQNvIoTZfJJ9IqkRuCoqP60vCVYlIg00c5aYusi5Wjh2bf0cHYt+6gQ==
+ dependencies:
+ endian-reader "^0.3.0"
+
email-validator@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed"
@@ -1783,20 +1812,25 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
dependencies:
once "^1.4.0"
-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==
+endian-reader@^0.3.0:
+ version "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.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"
@@ -1811,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"
@@ -2177,6 +2211,13 @@ fill-range@^4.0.0:
repeat-string "^1.6.1"
to-regex-range "^2.1.0"
+fill-range@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+ integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+ dependencies:
+ to-regex-range "^5.0.1"
+
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
@@ -2246,10 +2287,10 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
-frappe-charts@^1.5.1:
- version "1.5.3"
- resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.3.tgz#0dcb86ea774fa7a3e1b79221e958d29701dfff04"
- integrity sha512-VS5XVxek41ea8mVzetyFF3avNefiwGDcDSDJuHrZyJXgbqiTSXLoqlPFoMqTzuzRm1g+o6TXs+A7wLtVp3Vt0g==
+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"
@@ -2649,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.1:
- version "9.18.1"
- resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.1.tgz#ed21aa001fe6252bb10a3d76d47573c6539fe13c"
- integrity sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg==
+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"
@@ -2678,6 +2719,13 @@ hosted-git-info@^3.0.4:
dependencies:
lru-cache "^6.0.0"
+hosted-git-info@^3.0.7:
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.7.tgz#a30727385ea85acfcee94e0aad9e368c792e036c"
+ integrity sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ==
+ dependencies:
+ lru-cache "^6.0.0"
+
hsl-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e"
@@ -2857,10 +2905,10 @@ inherits@2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
-ini@^1.3.0, 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==
+ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
+ 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"
@@ -2881,11 +2929,6 @@ inquirer@^7.3.3:
strip-ansi "^6.0.0"
through "^2.3.6"
-invert-kv@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
- integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
-
iota-array@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/iota-array/-/iota-array-1.0.0.tgz#81ef57fe5d05814cd58c2483632a99c30a0e8087"
@@ -3122,6 +3165,11 @@ is-number@^3.0.0:
dependencies:
kind-of "^3.0.2"
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
is-obj@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@@ -3447,13 +3495,6 @@ latest-version@^5.0.0:
dependencies:
package-json "^6.3.0"
-lcid@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
- integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=
- dependencies:
- invert-kv "^1.0.0"
-
less@^3.11.1:
version "3.11.1"
resolved "https://registry.yarnpkg.com/less/-/less-3.11.1.tgz#c6bf08e39e02404fe6b307a3dfffafdc55bd36e2"
@@ -3750,6 +3791,14 @@ methods@^1.1.1, methods@~1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
+micromatch@4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+ integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+ dependencies:
+ braces "^3.0.1"
+ picomatch "^2.0.5"
+
micromatch@^3.1.10:
version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
@@ -3896,7 +3945,7 @@ ms@2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
-ms@^2.1.1:
+ms@2.1.2, ms@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
@@ -3928,16 +3977,6 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"
-nconf@^0.10.0:
- version "0.10.0"
- resolved "https://registry.yarnpkg.com/nconf/-/nconf-0.10.0.tgz#da1285ee95d0a922ca6cee75adcf861f48205ad2"
- integrity sha512-fKiXMQrpP7CYWJQzKkPPx9hPgmq+YLDyxcG9N8RpiE9FoCkCbzD0NyW0YhE3xn3Aupe7nnDeIx4PFzYehpHT9Q==
- dependencies:
- async "^1.4.0"
- ini "^1.3.0"
- secure-keys "^1.0.0"
- yargs "^3.19.0"
-
ndarray-linear-interpolate@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ndarray-linear-interpolate/-/ndarray-linear-interpolate-1.0.0.tgz#78bc92b85b9abc15b6e67ee65828f9e2137ae72b"
@@ -4133,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"
@@ -4268,13 +4302,6 @@ os-homedir@^1.0.0, os-homedir@^1.0.1:
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
-os-locale@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
- integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=
- dependencies:
- lcid "^1.0.0"
-
os-name@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801"
@@ -4429,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"
@@ -4508,6 +4531,11 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+picomatch@^2.0.5:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
+ integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
+
pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -5687,11 +5715,6 @@ scss-tokenizer@^0.2.3:
js-base64 "^2.1.8"
source-map "^0.4.2"
-secure-keys@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/secure-keys/-/secure-keys-1.0.0.tgz#f0c82d98a3b139a8776a8808050b824431087fca"
- integrity sha1-8MgtmKOxOah3aogIBQuCRDEIf8o=
-
semver-diff@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
@@ -5862,40 +5885,55 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
-snyk-config@3.1.1, snyk-config@^3.0.0:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/snyk-config/-/snyk-config-3.1.1.tgz#a511ef8bf769545f0564e09d382b5ea3aacb9c6a"
- integrity sha512-wwrMIEDozfLJ8LmakCsCC1FQ0siIX5icCQPCbUKKgRbeVsZ27NjPJs37BpTXX4rcHkaWpe8TbH3yOtp23qmszg==
+snyk-config@4.0.0-rc.2:
+ version "4.0.0-rc.2"
+ resolved "https://registry.yarnpkg.com/snyk-config/-/snyk-config-4.0.0-rc.2.tgz#c6c94afe733e9063df546cd71a7adf6957135594"
+ integrity sha512-HIXpMCRp5IdQDFH/CY6WqOUt5X5Ec55KC9dFVjlMLe/2zeqsImJn1vbjpE5uBoLYIdYi1SteTqtsJhyJZWRK8g==
dependencies:
+ async "^3.2.0"
debug "^4.1.1"
lodash.merge "^4.6.2"
- nconf "^0.10.0"
+ minimist "^1.2.5"
-snyk-cpp-plugin@1.5.0:
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/snyk-cpp-plugin/-/snyk-cpp-plugin-1.5.0.tgz#2ec2068fdcf5e579eb7d9b9eed8bb984fd00a925"
- integrity sha512-nBZ0cBmpT4RVJUFzYydQJOxwjcdXk7NtRJE1UIIOafQa2FcvIl3GBezfrCJ6pu61svOAf5r8Qi/likx6F15K1A==
+snyk-config@^4.0.0-rc.2:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/snyk-config/-/snyk-config-4.0.0.tgz#21d459f19087991246cc07a7ffb4501dce6f4159"
+ integrity sha512-E6jNe0oUjjzVASWBOAc/mA23DhbzABDF9MI6UZvl0gylh2NSXSXw2/LjlqMNOKL2c1qkbSkzLOdIX5XACoLCAQ==
+ dependencies:
+ async "^3.2.0"
+ debug "^4.1.1"
+ lodash.merge "^4.6.2"
+ minimist "^1.2.5"
+
+snyk-cpp-plugin@2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/snyk-cpp-plugin/-/snyk-cpp-plugin-2.2.1.tgz#55891511a43a6448e5a7c836a94f66f70fa705eb"
+ integrity sha512-NFwVLMCqKTocY66gcim0ukF6e31VRDJqDapg5sy3vCHqlD1OCNUXSK/aI4VQEEndDrsnFmQepsL5KpEU0dDRIQ==
dependencies:
"@snyk/dep-graph" "^1.19.3"
chalk "^4.1.0"
debug "^4.1.1"
+ hosted-git-info "^3.0.7"
tslib "^2.0.0"
-snyk-docker-plugin@3.21.0:
- version "3.21.0"
- resolved "https://registry.yarnpkg.com/snyk-docker-plugin/-/snyk-docker-plugin-3.21.0.tgz#a92074c0411578c1a7b86852a06f1421770e985d"
- integrity sha512-A7oJS3QGR7bwm1qeeczCb8PDfi8go1KM6VWph/drJHBQ7JxVKKLb3j4AzrMmIM96mGZFbmyNOL4pznwumaOM8g==
+snyk-docker-plugin@4.12.0:
+ version "4.12.0"
+ resolved "https://registry.yarnpkg.com/snyk-docker-plugin/-/snyk-docker-plugin-4.12.0.tgz#137a159baf627debef6178cfb8b40941a81a7168"
+ integrity sha512-iN5GUTpMR4dx/hmjxh1GnJ9vrMpbOUhD8gsdWgFPZ5Qg+ImPQ2WBJBal/hyfkauM0TaKQEAgIwT6xZ1ovaIvWQ==
dependencies:
+ "@snyk/dep-graph" "^1.19.4"
"@snyk/rpm-parser" "^2.0.0"
- "@snyk/snyk-docker-pull" "^3.2.0"
+ "@snyk/snyk-docker-pull" "3.2.3"
+ chalk "^2.4.2"
debug "^4.1.1"
docker-modem "2.1.3"
dockerfile-ast "0.0.30"
+ elfy "^1.0.0"
event-loop-spinner "^2.0.0"
gunzip-maybe "^1.4.2"
mkdirp "^1.0.4"
semver "^6.1.0"
- snyk-nodejs-lockfile-parser "1.28.1"
+ snyk-nodejs-lockfile-parser "1.30.1"
tar-stream "^2.1.0"
tmp "^0.2.1"
tslib "^1"
@@ -5921,13 +5959,14 @@ snyk-go-plugin@1.16.2:
tmp "0.2.1"
tslib "^1.10.0"
-snyk-gradle-plugin@3.6.3:
- version "3.6.3"
- resolved "https://registry.yarnpkg.com/snyk-gradle-plugin/-/snyk-gradle-plugin-3.6.3.tgz#484059bcb98469b6a674bbcbdc995eafb5581041"
- integrity sha512-j/eQSLSsK3DHmvVX2fNig4+ugYrKlCOV8Xvo6OYFkNzhMpdyNFiGWTS1uyP1HH75Gyc78MaLANMgjlSYePukzQ==
+snyk-gradle-plugin@3.10.2:
+ version "3.10.2"
+ resolved "https://registry.yarnpkg.com/snyk-gradle-plugin/-/snyk-gradle-plugin-3.10.2.tgz#f3e104d42989e49b5c05818f005cae8c544c9803"
+ integrity sha512-gTFKL0BLUN54asUQ4OIoa4lATGn27VZwWDJGQ0VuqSaaoy8I5W16Cbn/KN95oIKa7tgwrmasPLd5uviFWzo/Qw==
dependencies:
"@snyk/cli-interface" "2.9.1"
"@snyk/dep-graph" "^1.19.4"
+ "@snyk/java-call-graph-builder" "1.16.2"
"@types/debug" "^4.1.4"
chalk "^3.0.0"
debug "^4.1.1"
@@ -5960,22 +5999,23 @@ snyk-module@^2.0.2:
debug "^3.1.0"
hosted-git-info "^2.7.1"
-snyk-mvn-plugin@2.19.4:
- version "2.19.4"
- resolved "https://registry.yarnpkg.com/snyk-mvn-plugin/-/snyk-mvn-plugin-2.19.4.tgz#4e29fa82b9ca409789d441939c766797d6a2360f"
- integrity sha512-kYPUKOugnNd31PFqx1YHJTo90pospELYHME4AzBx8dkMDgs5ZPjAmQXSxegQ3AMUqfqcETMSTzlKHe6uHujI8A==
+snyk-mvn-plugin@2.23.4:
+ version "2.23.4"
+ resolved "https://registry.yarnpkg.com/snyk-mvn-plugin/-/snyk-mvn-plugin-2.23.4.tgz#3f43601058aa51e8a0f9e272a7c186cad4b26950"
+ integrity sha512-1dWqvFu6eo2KsXFDqRF28JFwrdzpc0k+GwpIqv7vF2kHarsMxnLnT/akhjbKzs+xlRTNFvqdKhEQxjdq2nSD1Q==
dependencies:
"@snyk/cli-interface" "2.9.1"
- "@snyk/java-call-graph-builder" "1.13.2"
+ "@snyk/java-call-graph-builder" "1.16.5"
debug "^4.1.1"
+ glob "^7.1.6"
needle "^2.5.0"
tmp "^0.1.0"
tslib "1.11.1"
-snyk-nodejs-lockfile-parser@1.28.1:
- version "1.28.1"
- resolved "https://registry.yarnpkg.com/snyk-nodejs-lockfile-parser/-/snyk-nodejs-lockfile-parser-1.28.1.tgz#9eda1354bbca1fc881a4e63a1e1042f80c37bff2"
- integrity sha512-0zbmtidYLI2ia/DQD4rZm2YKrhfHLvHlVBdF2cMAGPwhOoKW5ovG9eBO4wNQdvjxNi7b4VeUyAj8SfuhjDraDQ==
+snyk-nodejs-lockfile-parser@1.30.1:
+ version "1.30.1"
+ resolved "https://registry.yarnpkg.com/snyk-nodejs-lockfile-parser/-/snyk-nodejs-lockfile-parser-1.30.1.tgz#5d54180ae818ddbe8c2b55329528c4d68e390235"
+ integrity sha512-QyhE4pmy7GI7fQrVmZ+qrQB8GGSbxN7OoYueS4BEP9nDxIyH4dJAz8dME5zOUeUxh3frcgBWoWgZoSzE4VOYpg==
dependencies:
"@yarnpkg/lockfile" "^1.1.0"
event-loop-spinner "^2.0.0"
@@ -5987,16 +6027,15 @@ snyk-nodejs-lockfile-parser@1.28.1:
lodash.set "^4.3.2"
lodash.topairs "^4.3.0"
p-map "2.1.0"
- snyk-config "^3.0.0"
- source-map-support "^0.5.7"
+ snyk-config "^4.0.0-rc.2"
tslib "^1.9.3"
- uuid "^3.3.2"
+ uuid "^8.3.0"
yaml "^1.9.2"
-snyk-nuget-plugin@1.19.3:
- version "1.19.3"
- resolved "https://registry.yarnpkg.com/snyk-nuget-plugin/-/snyk-nuget-plugin-1.19.3.tgz#5b4d9a5a61a543810c98bd4e67b9f6b1d95e3c3a"
- integrity sha512-KwKoMumwcXVz/DQH80ifXfX7CTnm29bmHJ2fczjCGohxLGb4EKBGQtA3t7K98O7lTISQGgXDxnWIaM9ZXkxPdw==
+snyk-nuget-plugin@1.19.4:
+ version "1.19.4"
+ resolved "https://registry.yarnpkg.com/snyk-nuget-plugin/-/snyk-nuget-plugin-1.19.4.tgz#cd1163a29f8002d54a965eab9e256345c97d4174"
+ integrity sha512-6BvLJc7gpNdfPJSnvpmTL4BrbaOVbXh/9q1FNMs5OVp8NbnZ3l97iM+bpQXWTJHOa3BJBZz7iEg+3suH4AWoWw==
dependencies:
debug "^4.1.1"
dotnet-deps-parser "5.0.0"
@@ -6022,6 +6061,17 @@ snyk-php-plugin@1.9.2:
"@snyk/composer-lockfile-parser" "^1.4.1"
tslib "1.11.1"
+snyk-poetry-lockfile-parser@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/snyk-poetry-lockfile-parser/-/snyk-poetry-lockfile-parser-1.1.1.tgz#3f062953802916f6ae1767ec13dd1892fff0541e"
+ integrity sha512-G3LX27V2KUsKObwVN4vDDjrYr5BERad9pXHAf+SST5+vZsdPUUZjd1ZUIrHgCv7IQhwq+7mZrtqedY5x7+LIGA==
+ dependencies:
+ "@snyk/cli-interface" "^2.9.2"
+ "@snyk/dep-graph" "^1.19.5"
+ debug "^4.2.0"
+ toml "^3.0.0"
+ tslib "^2.0.0"
+
snyk-policy@1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/snyk-policy/-/snyk-policy-1.14.1.tgz#4e48ea993573aca18e8d883b8c62171b9d35a3e0"
@@ -6037,12 +6087,13 @@ snyk-policy@1.14.1:
snyk-try-require "^1.3.1"
then-fs "^2.0.0"
-snyk-python-plugin@1.17.1:
- version "1.17.1"
- resolved "https://registry.yarnpkg.com/snyk-python-plugin/-/snyk-python-plugin-1.17.1.tgz#303ec2885ef748634d89f22f3099ef1febdc3325"
- integrity sha512-KKklat9Hfbj4hw2y63LRhgmziYzmyRt+cSuzN5KDmBSAGYck0EAoPDtNpJXjrIs1kPNz28EXnE6NDnadXnOjiQ==
+snyk-python-plugin@1.19.1:
+ version "1.19.1"
+ resolved "https://registry.yarnpkg.com/snyk-python-plugin/-/snyk-python-plugin-1.19.1.tgz#91febcd260094a9d900bc54bf200aa0c2632613a"
+ integrity sha512-JoOUHnA76L3pekCblSuE9jQ9CuA5jt+GqXpsLQbEIZ0FQQTBa+0F7vfolg3Q7+s1it4ZdtgSbSWrlxCngIJt8g==
dependencies:
"@snyk/cli-interface" "^2.0.3"
+ snyk-poetry-lockfile-parser "^1.1.1"
tmp "0.0.33"
snyk-resolve-deps@4.4.0:
@@ -6104,10 +6155,10 @@ snyk-try-require@1.3.1, snyk-try-require@^1.1.1, snyk-try-require@^1.3.1:
lru-cache "^4.0.0"
then-fs "^2.0.0"
-snyk@^1.398.1:
- version "1.398.1"
- resolved "https://registry.yarnpkg.com/snyk/-/snyk-1.398.1.tgz#19aec8dfffa60e7412e6309117e96b2cfa960355"
- integrity sha512-jH24ztdJY8DQlqkd1z8n/JutdOqHtTPccCynM2hfOedW20yAp9c108LFjXvqBEk/EH3YyNmWzyLkkHOySeDkwQ==
+snyk@^1.425.4:
+ version "1.431.1"
+ resolved "https://registry.yarnpkg.com/snyk/-/snyk-1.431.1.tgz#1e360dae1b63d83f74fe90979f7b9a0fb1607aa7"
+ integrity sha512-OW48lG89ffLsSZPHwsjfdqQcu3XG6aRQOkwASPCgTAGcVcnXzS9XHB89h0gLsDzk0fZRskEVgYpvXdh4RFjNqA==
dependencies:
"@snyk/cli-interface" "2.9.2"
"@snyk/dep-graph" "1.19.4"
@@ -6120,28 +6171,28 @@ snyk@^1.398.1:
configstore "^5.0.1"
debug "^4.1.1"
diff "^4.0.1"
- glob "^7.1.3"
graphlib "^2.1.8"
inquirer "^7.3.3"
lodash "^4.17.20"
+ micromatch "4.0.2"
needle "2.5.0"
open "^7.0.3"
os-name "^3.0.0"
proxy-agent "^3.1.1"
proxy-from-env "^1.0.0"
semver "^6.0.0"
- snyk-config "3.1.1"
- snyk-cpp-plugin "1.5.0"
- snyk-docker-plugin "3.21.0"
+ snyk-config "4.0.0-rc.2"
+ snyk-cpp-plugin "2.2.1"
+ snyk-docker-plugin "4.12.0"
snyk-go-plugin "1.16.2"
- snyk-gradle-plugin "3.6.3"
+ snyk-gradle-plugin "3.10.2"
snyk-module "3.1.0"
- snyk-mvn-plugin "2.19.4"
- snyk-nodejs-lockfile-parser "1.28.1"
- snyk-nuget-plugin "1.19.3"
+ snyk-mvn-plugin "2.23.4"
+ snyk-nodejs-lockfile-parser "1.30.1"
+ snyk-nuget-plugin "1.19.4"
snyk-php-plugin "1.9.2"
snyk-policy "1.14.1"
- snyk-python-plugin "1.17.1"
+ snyk-python-plugin "1.19.1"
snyk-resolve "1.0.1"
snyk-resolve-deps "4.4.0"
snyk-sbt-plugin "2.11.0"
@@ -6159,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"
@@ -6197,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:
@@ -6760,6 +6808,13 @@ to-regex-range@^2.1.0:
is-number "^3.0.0"
repeat-string "^1.6.1"
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
to-regex@^3.0.1, to-regex@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
@@ -7023,6 +7078,11 @@ uuid@^8.2.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea"
integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==
+uuid@^8.3.0:
+ version "8.3.1"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
+ integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
+
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -7147,11 +7207,6 @@ widest-line@^3.1.0:
dependencies:
string-width "^4.0.0"
-window-size@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876"
- integrity sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=
-
windows-release@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f"
@@ -7164,14 +7219,6 @@ word-wrap@~1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
-wrap-ansi@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
- integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
- dependencies:
- string-width "^1.0.1"
- strip-ansi "^3.0.1"
-
wrap-ansi@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
@@ -7196,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"
@@ -7241,11 +7281,6 @@ xtend@~4.0.1:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
-y18n@^3.2.0:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
- integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
-
y18n@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
@@ -7320,19 +7355,6 @@ yargs@^14.2:
y18n "^4.0.0"
yargs-parser "^15.0.0"
-yargs@^3.19.0:
- version "3.32.0"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995"
- integrity sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=
- dependencies:
- camelcase "^2.0.1"
- cliui "^3.0.3"
- decamelize "^1.1.1"
- os-locale "^1.4.0"
- string-width "^1.0.1"
- window-size "^0.1.4"
- y18n "^3.2.0"
-
yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"