Merge branch 'develop' into grid-loop-fix
This commit is contained in:
commit
cbb7c7f311
162 changed files with 3711 additions and 2089 deletions
14
.editorconfig
Normal file
14
.editorconfig
Normal file
|
|
@ -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
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -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.
|
||||
4
.github/helper/documentation.py
vendored
4
.github/helper/documentation.py
vendored
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,7 +3,31 @@ context('Depends On', () => {
|
|||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call('frappe.tests.ui_test_helpers.create_doctype', {
|
||||
return frappe.xcall('frappe.tests.ui_test_helpers.create_child_doctype', {
|
||||
name: 'Child Test Depends On',
|
||||
fields: [
|
||||
{
|
||||
"label": "Child Test Field",
|
||||
"fieldname": "child_test_field",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
},
|
||||
{
|
||||
"label": "Child Dependant Field",
|
||||
"fieldname": "child_dependant_field",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
},
|
||||
{
|
||||
"label": "Child Display Dependant Field",
|
||||
"fieldname": "child_display_dependant_field",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
},
|
||||
]
|
||||
});
|
||||
}).then(frappe => {
|
||||
return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', {
|
||||
name: 'Test Depends On',
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -24,6 +48,13 @@ context('Depends On', () => {
|
|||
"fieldtype": "Data",
|
||||
'depends_on': "eval:doc.test_field=='Value'"
|
||||
},
|
||||
{
|
||||
"label": "Child Test Depends On Field",
|
||||
"fieldname": "child_test_depends_on_field",
|
||||
"fieldtype": "Table",
|
||||
'read_only_depends_on': "eval:doc.test_field=='Some Other Value'",
|
||||
'options': "Child Test Depends On"
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
|
@ -48,6 +79,30 @@ context('Depends On', () => {
|
|||
cy.get('body').click();
|
||||
cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled');
|
||||
});
|
||||
it('should set the table and its fields as read only depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.fill_field('dependant_field', 'Some Value');
|
||||
//cy.fill_field('test_field', 'Some Other Value');
|
||||
cy.get('.frappe-control[data-fieldname="child_test_depends_on_field"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').find('[data-idx="1"]').as('row1');
|
||||
cy.get('@row1').find('.btn-open-row').click();
|
||||
cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid');
|
||||
//cy.get('@row1-form_in_grid').find('')
|
||||
cy.fill_table_field('child_test_depends_on_field', '1', 'child_test_field', 'Some Value');
|
||||
cy.fill_table_field('child_test_depends_on_field', '1', 'child_dependant_field', 'Some Other Value');
|
||||
|
||||
cy.get('@row1-form_in_grid').find('.octicon-triangle-up').click();
|
||||
|
||||
// set the table to read-only
|
||||
cy.fill_field('test_field', 'Some Other Value');
|
||||
|
||||
// grid row form fields should be read-only
|
||||
cy.get('@row1').find('.btn-open-row').click();
|
||||
|
||||
cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_test_field"]').should('be.disabled');
|
||||
cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_dependant_field"]').should('be.disabled');
|
||||
});
|
||||
it('should display the field depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible');
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => {
|
|||
|
||||
Cypress.Commands.add('create_records', doc => {
|
||||
return cy
|
||||
.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
|
||||
.call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc})
|
||||
.then(r => r.message);
|
||||
});
|
||||
|
||||
|
|
@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
|
|||
if (fieldtype === 'Select') {
|
||||
cy.get('@input').select(value);
|
||||
} else {
|
||||
cy.get('@input').type(value, { waitForAnimations: false, force: true });
|
||||
cy.get('@input').type(value, {waitForAnimations: false, force: true});
|
||||
}
|
||||
return cy.get('@input');
|
||||
});
|
||||
|
|
@ -204,8 +204,43 @@ Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
|
|||
return cy.get(selector);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
|
||||
cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input');
|
||||
|
||||
if (['Date', 'Time', 'Datetime'].includes(fieldtype)) {
|
||||
cy.get('@input').click().wait(200);
|
||||
cy.get('.datepickers-container .datepicker.active').should('exist');
|
||||
}
|
||||
if (fieldtype === 'Time') {
|
||||
cy.get('@input').clear().wait(200);
|
||||
}
|
||||
|
||||
if (fieldtype === 'Select') {
|
||||
cy.get('@input').select(value);
|
||||
} else {
|
||||
cy.get('@input').type(value, {waitForAnimations: false, force: true});
|
||||
}
|
||||
return cy.get('@input');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => {
|
||||
let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`;
|
||||
selector += ` [data-idx="${row_idx}"]`;
|
||||
selector += ` .form-in-grid`;
|
||||
|
||||
if (fieldtype === 'Text Editor') {
|
||||
selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
|
||||
} else if (fieldtype === 'Code') {
|
||||
selector += ` [data-fieldname="${fieldname}"] .ace_text-input`;
|
||||
} else {
|
||||
selector += ` .form-control[data-fieldname="${fieldname}"]`;
|
||||
}
|
||||
|
||||
return cy.get(selector);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('awesomebar', text => {
|
||||
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 });
|
||||
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('new_form', doctype => {
|
||||
|
|
|
|||
|
|
@ -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 "[]")
|
||||
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
0
frappe/automation/doctype/auto_repeat_day/__init__.py
Normal file
0
frappe/automation/doctype/auto_repeat_day/__init__.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2020-11-10 22:30:53.690228",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"day"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "day",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Day",
|
||||
"options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-10 22:30:53.690228",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Auto Repeat Day",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
10
frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py
Normal file
10
frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class AutoRepeatDay(Document):
|
||||
pass
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
return dictify(emojis)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
|
|||
try:
|
||||
# use sql, so that we do not mess with the timestamp
|
||||
frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec
|
||||
(json.dumps(_comments[-50:]), reference_name))
|
||||
(json.dumps(_comments[-100:]), reference_name))
|
||||
|
||||
except Exception as e:
|
||||
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2017-01-11 04:21:35.217943",
|
||||
|
|
@ -13,6 +14,7 @@
|
|||
"column_break_2",
|
||||
"permlevel",
|
||||
"section_break_4",
|
||||
"select",
|
||||
"read",
|
||||
"write",
|
||||
"create",
|
||||
|
|
@ -211,9 +213,16 @@
|
|||
"fieldtype": "Data",
|
||||
"label": "Reference Document Type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "select",
|
||||
"fieldtype": "Check",
|
||||
"label": "Select"
|
||||
}
|
||||
],
|
||||
"modified": "2019-10-31 16:58:16.157079",
|
||||
"links": [],
|
||||
"modified": "2020-12-03 15:20:48.296730",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Custom DocPerm",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ frappe.ui.form.on('Domain Settings', {
|
|||
checked: active_domains.includes(domain)
|
||||
};
|
||||
});
|
||||
},
|
||||
on_change: () => {
|
||||
frm.dirty();
|
||||
}
|
||||
},
|
||||
render_input: true
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -48,29 +48,33 @@ frappe.ui.form.on('Server Script', {
|
|||
|
||||
setup_help(frm) {
|
||||
frm.get_field('help_html').html(`
|
||||
<h3>Examples</h3>
|
||||
<h4>DocType Event</h4>
|
||||
<pre><code>
|
||||
<p>Add logic for standard doctype events like Before Insert, After Submit, etc.</p>
|
||||
<pre>
|
||||
<code>
|
||||
# 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()
|
||||
</code></pre>
|
||||
if doc.allocated_to:
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'ToDo'
|
||||
owner = doc.allocated_to,
|
||||
description = doc.subject
|
||||
)).insert()
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>API Call</h4>
|
||||
<p>Respond to <code>/api/method/<method-name></code> calls, just like whitelisted methods</p>
|
||||
<pre><code>
|
||||
# respond to API
|
||||
|
||||
|
|
@ -79,6 +83,21 @@ if frappe.form_dict.message == "ping":
|
|||
else:
|
||||
frappe.response['message'] = "ok"
|
||||
</code></pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Permission Query</h4>
|
||||
<p>Add conditions to the where clause of list queries.</p>
|
||||
<pre><code>
|
||||
# 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
|
||||
</code></pre>
|
||||
`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
return script_map
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<td class="danger">{{ item[1] }}</td>
|
||||
<td class="success">{{ item[2] }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
|
@ -93,4 +93,4 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ frappe.PermissionEngine = Class.extend({
|
|||
.css({"margin-top": "15px"});
|
||||
},
|
||||
|
||||
rights: ["read", "write", "create", "delete", "submit", "cancel", "amend",
|
||||
rights: ["select", "read", "write", "create", "delete", "submit", "cancel", "amend",
|
||||
"print", "email", "report", "import", "export", "set_user_permissions", "share"],
|
||||
|
||||
set_show_users: function(cell, role) {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,18 @@ def add(parent, role, permlevel):
|
|||
|
||||
@frappe.whitelist()
|
||||
def update(doctype, role, permlevel, ptype, value=None):
|
||||
"""Update role permission params
|
||||
|
||||
Args:
|
||||
doctype (str): Name of the DocType to update params for
|
||||
role (str): Role to be updated for, eg "Website Manager".
|
||||
permlevel (int): perm level the provided rule applies to
|
||||
ptype (str): permission type, example "read", "delete", etc.
|
||||
value (None, optional): value for ptype, None indicates False
|
||||
|
||||
Returns:
|
||||
str: Refresh flag is permission is updated successfully
|
||||
"""
|
||||
frappe.only_for("System Manager")
|
||||
out = update_permission_property(doctype, role, permlevel, ptype, value)
|
||||
return 'refresh' if out else None
|
||||
|
|
@ -92,7 +104,7 @@ def remove(doctype, role, permlevel):
|
|||
if not frappe.get_all('Custom DocPerm', dict(parent=doctype)):
|
||||
frappe.throw(_('There must be atleast one permission rule.'), title=_('Cannot Remove'))
|
||||
|
||||
validate_permissions_for_doctype(doctype, for_remove=True)
|
||||
validate_permissions_for_doctype(doctype, for_remove=True, alert=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def reset(doctype):
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": "<h4>Email Reply Example</h4>\n\n<pre>Order Overdue\n\nTransaction {{ name }} has exceeded Due Date. Please take necessary action.\n\nDetails\n\n- Customer: {{ customer }}\n- Amount: {{ grand_total }}\n</pre>\n\n<h4>How to get fieldnames</h4>\n\n<p>The 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)</p>\n\n<h4>Templating</h4>\n\n<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=\"strong\" href=\"http://jinja.pocoo.org/docs/dev/templates/\">read this documentation.</a></p>\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",
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
return email_template.get_formatted_email(doc)
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
25
frappe/email/test_smtp.py
Normal file
25
frappe/email/test_smtp.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
class InvalidDatabaseFile(ValidationError): pass
|
||||
class ExecutableNotFound(FileNotFoundError): pass
|
||||
|
|
|
|||
96
frappe/geo/utils.py
Normal file
96
frappe/geo/utils.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
|
||||
from pymysql import InternalError
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_coords(doctype, filters, type):
|
||||
'''Get a geojson dict representing a doctype.'''
|
||||
filters_sql = get_coords_conditions(doctype, filters)[4:]
|
||||
|
||||
coords = None
|
||||
if type == 'location_field':
|
||||
coords = return_location(doctype, filters_sql)
|
||||
elif type == 'coordinates':
|
||||
coords = return_coordinates(doctype, filters_sql)
|
||||
|
||||
out = convert_to_geojson(type, coords)
|
||||
return out
|
||||
|
||||
def convert_to_geojson(type, coords):
|
||||
'''Converts GPS coordinates to geoJSON string.'''
|
||||
geojson = {"type": "FeatureCollection", "features": None}
|
||||
|
||||
if type == 'location_field':
|
||||
geojson['features'] = merge_location_features_in_one(coords)
|
||||
elif type == 'coordinates':
|
||||
geojson['features'] = create_gps_markers(coords)
|
||||
|
||||
return geojson
|
||||
|
||||
|
||||
def merge_location_features_in_one(coords):
|
||||
'''Merging all features from location field.'''
|
||||
geojson_dict = []
|
||||
for element in coords:
|
||||
geojson_loc = frappe.parse_json(element['location'])
|
||||
if not geojson_loc:
|
||||
continue
|
||||
for coord in geojson_loc['features']:
|
||||
coord['properties']['name'] = element['name']
|
||||
geojson_dict.append(coord.copy())
|
||||
|
||||
return geojson_dict
|
||||
|
||||
|
||||
def create_gps_markers(coords):
|
||||
'''Build Marker based on latitude and longitude.'''
|
||||
geojson_dict = []
|
||||
for i in coords:
|
||||
node = {"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": None}}
|
||||
node['properties']['name'] = i.name
|
||||
node['geometry']['coordinates'] = [i.latitude, i.longitude]
|
||||
geojson_dict.append(node.copy())
|
||||
|
||||
return geojson_dict
|
||||
|
||||
|
||||
def return_location(doctype, filters_sql):
|
||||
'''Get name and location fields for Doctype.'''
|
||||
if filters_sql:
|
||||
try:
|
||||
coords = frappe.db.sql('''SELECT name, location FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True)
|
||||
except InternalError:
|
||||
frappe.msgprint(frappe._('This Doctype does not contain location fields'), raise_exception=True)
|
||||
return
|
||||
else:
|
||||
coords = frappe.get_all(doctype, fields=['name', 'location'])
|
||||
return coords
|
||||
|
||||
|
||||
def return_coordinates(doctype, filters_sql):
|
||||
'''Get name, latitude and longitude fields for Doctype.'''
|
||||
if filters_sql:
|
||||
try:
|
||||
coords = frappe.db.sql('''SELECT name, latitude, longitude FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True)
|
||||
except InternalError:
|
||||
frappe.msgprint(frappe._('This Doctype does not contain latitude and longitude fields'), raise_exception=True)
|
||||
return
|
||||
else:
|
||||
coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude'])
|
||||
return coords
|
||||
|
||||
|
||||
def get_coords_conditions(doctype, filters=None):
|
||||
'''Returns SQL conditions with user permissions and filters for event queries.'''
|
||||
from frappe.desk.reportview import get_filters_cond
|
||||
if not frappe.has_permission(doctype):
|
||||
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
|
||||
|
||||
return get_filters_cond(doctype, filters, [], with_match_conditions=True)
|
||||
|
|
@ -18,7 +18,7 @@ app_email = "info@frappe.io"
|
|||
|
||||
docs_app = "frappe_io"
|
||||
|
||||
translator_url = "https://translatev2.erpnext.com"
|
||||
translator_url = "https://translate.erpnext.com"
|
||||
|
||||
before_install = "frappe.utils.install.before_install"
|
||||
after_install = "frappe.utils.install.after_install"
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -18,12 +18,9 @@
|
|||
"bucket",
|
||||
"endpoint_url",
|
||||
"column_break_13",
|
||||
"region",
|
||||
"backup_details_section",
|
||||
"frequency",
|
||||
"backup_files",
|
||||
"column_break_18",
|
||||
"backup_limit"
|
||||
"backup_files"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -42,7 +39,7 @@
|
|||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Note: By default emails for failed backups are sent.",
|
||||
"description": "By default, emails are only sent for failed backups.",
|
||||
"fieldname": "send_email_for_successful_backup",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Email for Successful Backup"
|
||||
|
|
@ -73,14 +70,7 @@
|
|||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "us-east-1",
|
||||
"description": "See https://docs.aws.amazon.com/general/latest/gr/s3.html for details.",
|
||||
"fieldname": "region",
|
||||
"fieldtype": "Select",
|
||||
"label": "Region",
|
||||
"options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\naf-south-1\nap-east-1\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-south-1\neu-north-1\nme-south-1\nsa-east-1"
|
||||
},
|
||||
{
|
||||
"default": "https://s3.amazonaws.com",
|
||||
"fieldname": "endpoint_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Endpoint URL"
|
||||
|
|
@ -92,14 +82,6 @@
|
|||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"description": "Set to 0 for no limit on the number of backups taken",
|
||||
"fieldname": "backup_limit",
|
||||
"fieldtype": "Int",
|
||||
"label": "Backup Limit",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "enabled",
|
||||
"fieldname": "api_access_section",
|
||||
|
|
@ -142,16 +124,12 @@
|
|||
"fieldname": "backup_files",
|
||||
"fieldtype": "Check",
|
||||
"label": "Backup Files"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-07-27 17:27:21.400000",
|
||||
"modified": "2020-12-07 15:30:55.047689",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "S3 Backup Settings",
|
||||
|
|
@ -172,4 +150,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ class S3BackupSettings(Document):
|
|||
|
||||
if not self.endpoint_url:
|
||||
self.endpoint_url = 'https://s3.amazonaws.com'
|
||||
|
||||
conn = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=self.access_key_id,
|
||||
|
|
@ -31,25 +32,21 @@ class S3BackupSettings(Document):
|
|||
endpoint_url=self.endpoint_url
|
||||
)
|
||||
|
||||
bucket_lower = str(self.bucket)
|
||||
|
||||
try:
|
||||
conn.list_buckets()
|
||||
|
||||
except ClientError:
|
||||
frappe.throw(_("Invalid Access Key ID or Secret Access Key."))
|
||||
|
||||
try:
|
||||
# Head_bucket returns a 200 OK if the bucket exists and have access to it.
|
||||
conn.head_bucket(Bucket=bucket_lower)
|
||||
# Requires ListBucket permission
|
||||
conn.head_bucket(Bucket=self.bucket)
|
||||
except ClientError as e:
|
||||
error_code = e.response['Error']['Code']
|
||||
bucket_name = frappe.bold(self.bucket)
|
||||
if error_code == '403':
|
||||
frappe.throw(_("Do not have permission to access {0} bucket.").format(bucket_lower))
|
||||
else: # '400'-Bad request or '404'-Not Found return
|
||||
# try to create bucket
|
||||
conn.create_bucket(Bucket=bucket_lower, CreateBucketConfiguration={
|
||||
'LocationConstraint': self.region})
|
||||
msg = _("Do not have permission to access bucket {0}.").format(bucket_name)
|
||||
elif error_code == '404':
|
||||
msg = _("Bucket {0} not found.").format(bucket_name)
|
||||
else:
|
||||
msg = e.args[0]
|
||||
|
||||
frappe.throw(msg)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -70,11 +67,13 @@ def take_backups_weekly():
|
|||
def take_backups_monthly():
|
||||
take_backups_if("Monthly")
|
||||
|
||||
|
||||
def take_backups_if(freq):
|
||||
if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")):
|
||||
if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq:
|
||||
take_backups_s3()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def take_backups_s3(retry_count=0):
|
||||
try:
|
||||
|
|
@ -146,42 +145,13 @@ def backup_to_s3():
|
|||
if files_filename:
|
||||
upload_file_to_s3(files_filename, folder, conn, bucket)
|
||||
|
||||
delete_old_backups(doc.backup_limit, bucket)
|
||||
|
||||
|
||||
def upload_file_to_s3(filename, folder, conn, bucket):
|
||||
destpath = os.path.join(folder, os.path.basename(filename))
|
||||
try:
|
||||
print("Uploading file:", filename)
|
||||
conn.upload_file(filename, bucket, destpath)
|
||||
conn.upload_file(filename, bucket, destpath) # Requires PutObject permission
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error()
|
||||
print("Error uploading: %s" % (e))
|
||||
|
||||
|
||||
def delete_old_backups(limit, bucket):
|
||||
all_backups = []
|
||||
doc = frappe.get_single("S3 Backup Settings")
|
||||
backup_limit = int(limit)
|
||||
|
||||
s3 = boto3.resource(
|
||||
's3',
|
||||
aws_access_key_id=doc.access_key_id,
|
||||
aws_secret_access_key=doc.get_password('secret_access_key'),
|
||||
endpoint_url=doc.endpoint_url or 'https://s3.amazonaws.com'
|
||||
)
|
||||
|
||||
bucket = s3.Bucket(bucket)
|
||||
objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Delimiter='/')
|
||||
if objects:
|
||||
for obj in objects.get('CommonPrefixes'):
|
||||
all_backups.append(obj.get('Prefix'))
|
||||
|
||||
oldest_backup = sorted(all_backups)[0] if all_backups else ''
|
||||
|
||||
if len(all_backups) > backup_limit:
|
||||
print("Deleting Backup: {0}".format(oldest_backup))
|
||||
for obj in bucket.objects.filter(Prefix=oldest_backup):
|
||||
# delete all keys that are inside the oldest_backup
|
||||
s3.Object(bucket.name, obj.key).delete()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
backup.zip_files()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
58
frappe/model/utils/rename_doc.py
Normal file
58
frappe/model/utils/rename_doc.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
11
frappe/patches/v13_0/delete_package_publish_tool.py
Normal file
11
frappe/patches/v13_0/delete_package_publish_tool.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = `<span class="indicator green">
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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(`
|
||||
<span class="phone-btn">
|
||||
<a class="btn-open no-decoration" title="${__('Make a call')}">
|
||||
<i class="fa fa-phone"></i></a>
|
||||
</span>
|
||||
`)
|
||||
.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);
|
||||
|
|
|
|||
|
|
@ -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: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(this.map);
|
||||
L.tileLayer(frappe.utils.map_defaults.tiles,
|
||||
frappe.utils.map_defaults.options).addTo(this.map);
|
||||
},
|
||||
|
||||
bind_leaflet_locate_control() {
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(', ')])));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 `<span class="rating">
|
||||
|
|
@ -120,11 +128,16 @@ frappe.form.formatters = {
|
|||
return repl('<a onclick="%(onclick)s">%(value)s</a>',
|
||||
{onclick: docfield.link_onclick.replace(/"/g, '"'), value:value});
|
||||
} else if(docfield && doctype) {
|
||||
return `<a class="grey"
|
||||
href="#Form/${encodeURIComponent(doctype)}/${encodeURIComponent(original_value)}"
|
||||
data-doctype="${doctype}"
|
||||
data-name="${original_value}">
|
||||
${__(options && options.label || value)}</a>`
|
||||
if (!frappe.model.can_select(doctype) && frappe.model.can_read(doctype)) {
|
||||
return `<a class="grey"
|
||||
#Form/${encodeURIComponent(doctype)}/${encodeURIComponent(original_value)}
|
||||
data-doctype="${doctype}"
|
||||
data-name="${original_value}">
|
||||
${__(options && options.label || value)}</a>`;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue