Merge branch 'develop' into todo-access-fix
This commit is contained in:
commit
1bfd5c8a87
108 changed files with 1462 additions and 1064 deletions
|
|
@ -42,18 +42,33 @@ context('Form', () => {
|
|||
it('validates behaviour of Data options validations in child table', () => {
|
||||
// test email validations for set_invalid controller
|
||||
let website_input = 'website.in';
|
||||
let valid_email = 'user@email.com';
|
||||
let expectBackgroundColor = 'rgb(255, 245, 245)';
|
||||
|
||||
cy.visit('/app/contact/new');
|
||||
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('.grid-body .rows [data-fieldname="email_id"]').click();
|
||||
cy.get('@table').find('input.input-with-feedback.form-control').as('email_input');
|
||||
cy.get('@email_input').type(website_input, { waitForAnimations: false });
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').find('[data-idx="1"]').as('row1');
|
||||
cy.get('@table').find('[data-idx="2"]').as('row2');
|
||||
cy.get('@row1').click();
|
||||
cy.get('@row1').find('input.input-with-feedback.form-control').as('email_input1');
|
||||
|
||||
cy.get('@email_input1').type(website_input, { waitForAnimations: false });
|
||||
cy.fill_field('company_name', 'Test Company');
|
||||
cy.get('@email_input').should($div => {
|
||||
|
||||
cy.get('@row2').click();
|
||||
cy.get('@row2').find('input.input-with-feedback.form-control').as('email_input2');
|
||||
cy.get('@email_input2').type(valid_email, { waitForAnimations: false });
|
||||
|
||||
cy.get('@row1').click();
|
||||
cy.get('@email_input1').should($div => {
|
||||
const style = window.getComputedStyle($div[0]);
|
||||
expect(style.backgroundColor).to.equal(expectBackgroundColor);
|
||||
});
|
||||
cy.get('@email_input1').should('have.class', 'invalid');
|
||||
|
||||
cy.get('@row2').click();
|
||||
cy.get('@email_input2').should('not.have.class', 'invalid');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -555,8 +555,15 @@ def whitelist(allow_guest=False, xss_safe=False, methods=None):
|
|||
|
||||
def innerfn(fn):
|
||||
global whitelisted, guest_methods, xss_safe_methods, allowed_http_methods_for_whitelisted_func
|
||||
whitelisted.append(fn)
|
||||
|
||||
# get function from the unbound / bound method
|
||||
# this is needed because functions can be compared, but not methods
|
||||
method = None
|
||||
if hasattr(fn, '__func__'):
|
||||
method = fn
|
||||
fn = method.__func__
|
||||
|
||||
whitelisted.append(fn)
|
||||
allowed_http_methods_for_whitelisted_func[fn] = methods
|
||||
|
||||
if allow_guest:
|
||||
|
|
@ -565,10 +572,24 @@ def whitelist(allow_guest=False, xss_safe=False, methods=None):
|
|||
if xss_safe:
|
||||
xss_safe_methods.append(fn)
|
||||
|
||||
return fn
|
||||
return method or fn
|
||||
|
||||
return innerfn
|
||||
|
||||
def is_whitelisted(method):
|
||||
from frappe.utils import sanitize_html
|
||||
|
||||
is_guest = session['user'] == 'Guest'
|
||||
if method not in whitelisted or is_guest and method not in guest_methods:
|
||||
throw(_("Not permitted"), PermissionError)
|
||||
|
||||
if is_guest and method not in xss_safe_methods:
|
||||
# strictly sanitize form_dict
|
||||
# escapes html characters like <> except for predefined tags like a, b, ul etc.
|
||||
for key, value in form_dict.items():
|
||||
if isinstance(value, string_types):
|
||||
form_dict[key] = sanitize_html(value)
|
||||
|
||||
def read_only():
|
||||
def innfn(fn):
|
||||
def wrapper_fn(*args, **kwargs):
|
||||
|
|
@ -1378,7 +1399,7 @@ def get_list(doctype, *args, **kwargs):
|
|||
frappe.get_list("ToDo", fields="*", filters = {"description": ("like", "test%")})
|
||||
"""
|
||||
import frappe.model.db_query
|
||||
return frappe.model.db_query.DatabaseQuery(doctype).execute(None, *args, **kwargs)
|
||||
return frappe.model.db_query.DatabaseQuery(doctype).execute(*args, **kwargs)
|
||||
|
||||
def get_all(doctype, *args, **kwargs):
|
||||
"""List database query via `frappe.model.db_query`. Will **not** check for permissions.
|
||||
|
|
|
|||
|
|
@ -215,35 +215,25 @@ class LoginManager:
|
|||
if not (user and pwd):
|
||||
self.fail(_('Incomplete login details'), user=user)
|
||||
|
||||
# Ignore password check if tmp_id is set, 2FA takes care of authentication.
|
||||
validate_password = not bool(frappe.form_dict.get('tmp_id'))
|
||||
user = User.find_by_credentials(user, pwd, validate_password=validate_password)
|
||||
user = User.find_by_credentials(user, pwd)
|
||||
|
||||
if not user:
|
||||
self.fail('Invalid login credentials')
|
||||
|
||||
sys_settings = frappe.get_doc("System Settings")
|
||||
track_login_attempts = (sys_settings.allow_consecutive_login_attempts >0)
|
||||
|
||||
tracker_kwargs = {}
|
||||
if track_login_attempts:
|
||||
tracker_kwargs['lock_interval'] = sys_settings.allow_login_after_fail
|
||||
tracker_kwargs['max_consecutive_login_attempts'] = sys_settings.allow_consecutive_login_attempts
|
||||
|
||||
tracker = LoginAttemptTracker(user.name, **tracker_kwargs)
|
||||
|
||||
if track_login_attempts and not tracker.is_user_allowed():
|
||||
frappe.throw(_("Your account has been locked and will resume after {0} seconds")
|
||||
.format(sys_settings.allow_login_after_fail), frappe.SecurityException)
|
||||
# Current login flow uses cached credentials for authentication while checking OTP.
|
||||
# Incase of OTP check, tracker for auth needs to be disabled(If not, it can remove tracker history as it is going to succeed anyway)
|
||||
# Tracker is activated for 2FA incase of OTP.
|
||||
ignore_tracker = should_run_2fa(user.name) and ('otp' in frappe.form_dict)
|
||||
tracker = None if ignore_tracker else get_login_attempt_tracker(user.name)
|
||||
|
||||
if not user.is_authenticated:
|
||||
tracker.add_failure_attempt()
|
||||
tracker and tracker.add_failure_attempt()
|
||||
self.fail('Invalid login credentials', user=user.name)
|
||||
elif not (user.name == 'Administrator' or user.enabled):
|
||||
tracker.add_failure_attempt()
|
||||
tracker and tracker.add_failure_attempt()
|
||||
self.fail('User disabled or missing', user=user.name)
|
||||
else:
|
||||
tracker.add_success_attempt()
|
||||
tracker and tracker.add_success_attempt()
|
||||
self.user = user.name
|
||||
|
||||
def force_user_to_reset_password(self):
|
||||
|
|
@ -406,6 +396,27 @@ def validate_ip_address(user):
|
|||
|
||||
frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError)
|
||||
|
||||
def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True):
|
||||
"""Get login attempt tracker instance.
|
||||
|
||||
:param user_name: Name of the loggedin user
|
||||
:param raise_locked_exception: If set, raises an exception incase of user not allowed to login
|
||||
"""
|
||||
sys_settings = frappe.get_doc("System Settings")
|
||||
track_login_attempts = (sys_settings.allow_consecutive_login_attempts >0)
|
||||
tracker_kwargs = {}
|
||||
|
||||
if track_login_attempts:
|
||||
tracker_kwargs['lock_interval'] = sys_settings.allow_login_after_fail
|
||||
tracker_kwargs['max_consecutive_login_attempts'] = sys_settings.allow_consecutive_login_attempts
|
||||
|
||||
tracker = LoginAttemptTracker(user_name, **tracker_kwargs)
|
||||
|
||||
if raise_locked_exception and track_login_attempts and not tracker.is_user_allowed():
|
||||
frappe.throw(_("Your account has been locked and will resume after {0} seconds")
|
||||
.format(sys_settings.allow_login_after_fail), frappe.SecurityException)
|
||||
return tracker
|
||||
|
||||
|
||||
class LoginAttemptTracker(object):
|
||||
"""Track login attemts of a user.
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ class AutoRepeat(Document):
|
|||
def is_completed(self):
|
||||
return self.end_date and getdate(self.end_date) < getdate(today())
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_auto_repeat_schedule(self):
|
||||
schedule_details = []
|
||||
start_date = getdate(self.start_date)
|
||||
|
|
@ -328,6 +329,7 @@ class AutoRepeat(Document):
|
|||
make(doctype=new_doc.doctype, name=new_doc.name, recipients=recipients,
|
||||
subject=subject, content=message, attachments=attachments, send_email=1)
|
||||
|
||||
@frappe.whitelist()
|
||||
def fetch_linked_contacts(self):
|
||||
if self.reference_doctype and self.reference_document:
|
||||
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import frappe.model
|
|||
import frappe.utils
|
||||
import json, os
|
||||
from frappe.utils import get_safe_filters
|
||||
from frappe.desk.reportview import validate_args
|
||||
from frappe.model.db_query import check_parent_permission
|
||||
|
||||
from six import iteritems, string_types, integer_types
|
||||
|
||||
|
|
@ -19,7 +21,7 @@ Requests via FrappeClient are also handled here.
|
|||
|
||||
@frappe.whitelist()
|
||||
def get_list(doctype, fields=None, filters=None, order_by=None,
|
||||
limit_start=None, limit_page_length=20, parent=None):
|
||||
limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True):
|
||||
'''Returns a list of records by filters, fields, ordering and limit
|
||||
|
||||
:param doctype: DocType of the data to be queried
|
||||
|
|
@ -31,8 +33,19 @@ def get_list(doctype, fields=None, filters=None, order_by=None,
|
|||
if frappe.is_table(doctype):
|
||||
check_parent_permission(parent, doctype)
|
||||
|
||||
return frappe.get_list(doctype, fields=fields, filters=filters, order_by=order_by,
|
||||
limit_start=limit_start, limit_page_length=limit_page_length, ignore_permissions=False)
|
||||
args = frappe._dict(
|
||||
doctype=doctype,
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
order_by=order_by,
|
||||
limit_start=limit_start,
|
||||
limit_page_length=limit_page_length,
|
||||
debug=debug,
|
||||
as_list=not as_dict
|
||||
)
|
||||
|
||||
validate_args(args)
|
||||
return frappe.get_list(**args)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_count(doctype, filters=None, debug=False, cache=False):
|
||||
|
|
@ -91,14 +104,15 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
|
|||
if frappe.get_meta(doctype).issingle:
|
||||
value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug)
|
||||
else:
|
||||
value = frappe.get_list(doctype, filters=filters, fields=fields, debug=debug, limit=1)
|
||||
value = get_list(doctype, filters=filters, fields=fields, debug=debug, limit_page_length=1, as_dict=as_dict)
|
||||
|
||||
if as_dict:
|
||||
value = value[0] if value else {}
|
||||
else:
|
||||
value = value[0].fieldname
|
||||
return value[0] if value else {}
|
||||
|
||||
return value
|
||||
if not value:
|
||||
return
|
||||
|
||||
return value[0] if len(fields) > 1 else value[0][0]
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_single_value(doctype, field):
|
||||
|
|
@ -378,18 +392,6 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
|
|||
def get_hooks(hook, app_name=None):
|
||||
return frappe.get_hooks(hook, app_name)
|
||||
|
||||
def check_parent_permission(parent, child_doctype):
|
||||
if parent:
|
||||
# User may pass fake parent and get the information from the child table
|
||||
if child_doctype and not frappe.db.exists('DocField',
|
||||
{'parent': parent, 'options': child_doctype}):
|
||||
raise frappe.PermissionError
|
||||
|
||||
if frappe.permissions.has_permission(parent):
|
||||
return
|
||||
# Either parent not passed or the user doesn't have permission on parent doctype of child table!
|
||||
raise frappe.PermissionError
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_document_amended(doctype, docname):
|
||||
if frappe.permissions.has_permission(doctype):
|
||||
|
|
@ -400,4 +402,4 @@ def is_document_amended(doctype, docname):
|
|||
except frappe.db.InternalError:
|
||||
pass
|
||||
|
||||
return False
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -36,48 +36,10 @@ def get_modules_from_all_apps():
|
|||
return modules_list
|
||||
|
||||
def get_modules_from_app(app):
|
||||
try:
|
||||
modules = frappe.get_attr(app + '.config.desktop.get_data')() or {}
|
||||
except ImportError:
|
||||
return []
|
||||
|
||||
active_domains = frappe.get_active_domains()
|
||||
|
||||
if isinstance(modules, dict):
|
||||
active_modules_list = []
|
||||
for m, module in iteritems(modules):
|
||||
module['module_name'] = m
|
||||
module['app'] = app
|
||||
active_modules_list.append(module)
|
||||
else:
|
||||
for m in modules:
|
||||
if m.get("type") == "module" and "category" not in m:
|
||||
m["category"] = "Modules"
|
||||
|
||||
# Only newly formatted modules that have a category to be shown on desk
|
||||
modules = [m for m in modules if m.get("category")]
|
||||
active_modules_list = []
|
||||
|
||||
for m in modules:
|
||||
to_add = True
|
||||
module_name = m.get("module_name")
|
||||
|
||||
# Check Domain
|
||||
if is_domain(m) and module_name not in active_domains:
|
||||
to_add = False
|
||||
|
||||
# Check if config
|
||||
if is_module(m) and not config_exists(app, frappe.scrub(module_name)):
|
||||
to_add = False
|
||||
|
||||
if "condition" in m and not m["condition"]:
|
||||
to_add = False
|
||||
|
||||
if to_add:
|
||||
m["app"] = app
|
||||
active_modules_list.append(m)
|
||||
|
||||
return active_modules_list
|
||||
return frappe.get_all('Module Def',
|
||||
filters={'app_name': app},
|
||||
fields=['module_name', 'app_name as app']
|
||||
)
|
||||
|
||||
def get_all_empty_tables_by_module():
|
||||
empty_tables = set(r[0] for r in frappe.db.multisql({
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
|
|||
"""Updates `_comments` property in parent Document with given dict.
|
||||
|
||||
:param _comments: Dict of comments."""
|
||||
if not reference_doctype or not reference_name or frappe.db.get_value("DocType", reference_doctype, "issingle"):
|
||||
if not reference_doctype or not reference_name or frappe.db.get_value("DocType", reference_doctype, "issingle") or frappe.db.get_value("DocType", reference_doctype, "is_virtual"):
|
||||
return
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -479,43 +479,4 @@ frappe.ui.form.on('Data Import', {
|
|||
</table>
|
||||
`);
|
||||
},
|
||||
|
||||
show_missing_link_values(frm, missing_link_values) {
|
||||
let can_be_created_automatically = missing_link_values.every(
|
||||
d => d.has_one_mandatory_field
|
||||
);
|
||||
|
||||
let html = missing_link_values
|
||||
.map(d => {
|
||||
let doctype = d.doctype;
|
||||
let values = d.missing_values;
|
||||
return `
|
||||
<h5>${doctype}</h5>
|
||||
<ul>${values.map(v => `<li>${v}</li>`).join('')}</ul>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
if (can_be_created_automatically) {
|
||||
// prettier-ignore
|
||||
let message = __('There are some linked records which needs to be created before we can import your file. Do you want to create the following missing records automatically?');
|
||||
frappe.confirm(message + html, () => {
|
||||
frm
|
||||
.call('create_missing_link_values', {
|
||||
missing_link_values
|
||||
})
|
||||
.then(r => {
|
||||
let records = r.message;
|
||||
frappe.msgprint(
|
||||
__('Created {0} records successfully.', [records.length])
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
frappe.msgprint(
|
||||
// prettier-ignore
|
||||
__('The following records needs to be created before we can import your file.') + html
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class DataImport(Document):
|
|||
return
|
||||
validate_google_sheets_url(self.google_sheets_url)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_preview_from_template(self, import_file=None, google_sheets_url=None):
|
||||
if import_file:
|
||||
self.import_file = import_file
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@ from __future__ import unicode_literals
|
|||
{base_class_import}
|
||||
|
||||
class {classname}({base_class}):
|
||||
pass
|
||||
{custom_controller}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ frappe.ui.form.on('DocType', {
|
|||
frm.set_value("custom", 1);
|
||||
}
|
||||
frm.toggle_enable("custom", 0);
|
||||
frm.toggle_enable("is_virtual", 0);
|
||||
frm.toggle_enable("beta", 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"track_views",
|
||||
"custom",
|
||||
"beta",
|
||||
"is_virtual",
|
||||
"fields_section_break",
|
||||
"fields",
|
||||
"sb1",
|
||||
|
|
@ -528,6 +529,12 @@
|
|||
"fieldname": "index_web_pages_for_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "Index Web Pages for Search"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_virtual",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Virtual"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-bolt",
|
||||
|
|
@ -609,7 +616,7 @@
|
|||
"link_fieldname": "reference_doctype"
|
||||
}
|
||||
],
|
||||
"modified": "2021-02-04 15:10:09.227205",
|
||||
"modified": "2021-02-17 20:18:06.212232",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
|
|
|
|||
|
|
@ -127,6 +127,10 @@ class DocType(Document):
|
|||
if not frappe.conf.get("developer_mode") and not self.custom:
|
||||
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError)
|
||||
|
||||
if self.is_virtual and self.custom:
|
||||
frappe.throw(_("Not allowed to create custom Virtual DocType."), CannotCreateStandardDoctypeError)
|
||||
|
||||
|
||||
if frappe.conf.get('developer_mode'):
|
||||
self.owner = 'Administrator'
|
||||
self.modified_by = 'Administrator'
|
||||
|
|
|
|||
|
|
@ -480,8 +480,19 @@ class TestDocType(unittest.TestCase):
|
|||
'link_doctype': "User",
|
||||
'link_fieldname': "a_field_that_does_not_exists"
|
||||
})
|
||||
|
||||
self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc)
|
||||
|
||||
def test_create_virtual_doctype(self):
|
||||
"""Test virtual DOcTYpe."""
|
||||
virtual_doc = new_doctype('Test Virtual Doctype')
|
||||
virtual_doc.is_virtual = 1
|
||||
virtual_doc.insert()
|
||||
virtual_doc.save()
|
||||
doc = frappe.get_doc("DocType", "Test Virtual Doctype")
|
||||
|
||||
self.assertEqual(doc.is_virtual, 1)
|
||||
self.assertFalse(frappe.db.table_exists('Test Virtual Doctype'))
|
||||
|
||||
def new_doctype(name, unique=0, depends_on='', fields=None):
|
||||
doc = frappe.get_doc({
|
||||
|
|
|
|||
|
|
@ -94,52 +94,89 @@ class File(Document):
|
|||
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:
|
||||
if not self.is_folder:
|
||||
if self.is_folder:
|
||||
self.file_url = ""
|
||||
else:
|
||||
self.validate_url()
|
||||
|
||||
self.file_size = frappe.form_dict.file_size or self.file_size
|
||||
|
||||
def validate_url(self):
|
||||
if not self.file_url or self.file_url.startswith(("http://", "https://")):
|
||||
if not self.flags.ignore_file_validate:
|
||||
self.validate_file()
|
||||
self.generate_content_hash()
|
||||
|
||||
if frappe.db.exists('File', {'name': self.name, 'is_folder': 0}):
|
||||
old_file_url = self.file_url
|
||||
if not self.is_folder and (self.is_private != self.db_get('is_private')):
|
||||
private_files = frappe.get_site_path('private', 'files')
|
||||
public_files = frappe.get_site_path('public', 'files')
|
||||
return
|
||||
|
||||
file_name = self.file_url.split('/')[-1]
|
||||
if not self.is_private:
|
||||
shutil.move(os.path.join(private_files, file_name),
|
||||
os.path.join(public_files, file_name))
|
||||
# Probably an invalid web URL
|
||||
if not self.file_url.startswith(("/files/", "/private/files/")):
|
||||
frappe.throw(
|
||||
_("URL must start with http:// or https://"),
|
||||
title=_('Invalid URL')
|
||||
)
|
||||
|
||||
self.file_url = "/files/{0}".format(file_name)
|
||||
# Ensure correct formatting and type
|
||||
self.file_url = unquote(self.file_url)
|
||||
self.is_private = cint(self.is_private)
|
||||
|
||||
else:
|
||||
shutil.move(os.path.join(public_files, file_name),
|
||||
os.path.join(private_files, file_name))
|
||||
self.handle_is_private_changed()
|
||||
|
||||
self.file_url = "/private/files/{0}".format(file_name)
|
||||
base_path = os.path.realpath(get_files_path(is_private=self.is_private))
|
||||
if not os.path.realpath(self.get_full_path()).startswith(base_path):
|
||||
frappe.throw(
|
||||
_("The File URL you've entered is incorrect"),
|
||||
title=_('Invalid File URL')
|
||||
)
|
||||
|
||||
update_existing_file_docs(self)
|
||||
def handle_is_private_changed(self):
|
||||
if not frappe.db.exists(
|
||||
'File', {
|
||||
'name': self.name,
|
||||
'is_private': cint(not self.is_private)
|
||||
}
|
||||
):
|
||||
return
|
||||
|
||||
# update documents image url with new file url
|
||||
if self.attached_to_doctype and self.attached_to_name:
|
||||
if not self.attached_to_field:
|
||||
field_name = None
|
||||
reference_dict = frappe.get_doc(self.attached_to_doctype, self.attached_to_name).as_dict()
|
||||
for key, value in reference_dict.items():
|
||||
if value == old_file_url:
|
||||
field_name = key
|
||||
break
|
||||
self.attached_to_field = field_name
|
||||
if self.attached_to_field:
|
||||
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
|
||||
self.attached_to_field, self.file_url)
|
||||
old_file_url = self.file_url
|
||||
|
||||
self.validate_url()
|
||||
file_name = self.file_url.split('/')[-1]
|
||||
private_file_path = frappe.get_site_path('private', 'files', file_name)
|
||||
public_file_path = frappe.get_site_path('public', 'files', file_name)
|
||||
|
||||
if self.file_url and (self.is_private != self.file_url.startswith('/private')):
|
||||
frappe.throw(_('Invalid file URL. Please contact System Administrator.'))
|
||||
if self.is_private:
|
||||
shutil.move(public_file_path, private_file_path)
|
||||
url_starts_with = "/private/files/"
|
||||
else:
|
||||
shutil.move(private_file_path, public_file_path)
|
||||
url_starts_with = "/files/"
|
||||
|
||||
self.file_url = "{0}{1}".format(url_starts_with, file_name)
|
||||
update_existing_file_docs(self)
|
||||
|
||||
if (
|
||||
not self.attached_to_doctype
|
||||
or not self.attached_to_name
|
||||
or not self.fetch_attached_to_field(old_file_url)
|
||||
):
|
||||
return
|
||||
|
||||
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
|
||||
self.attached_to_field, self.file_url)
|
||||
|
||||
def fetch_attached_to_field(self, old_file_url):
|
||||
if self.attached_to_field:
|
||||
return True
|
||||
|
||||
reference_dict = frappe.get_doc(
|
||||
self.attached_to_doctype, self.attached_to_name).as_dict()
|
||||
|
||||
for key, value in reference_dict.items():
|
||||
if value == old_file_url:
|
||||
self.attached_to_field = key
|
||||
return True
|
||||
|
||||
def validate_attachment_limit(self):
|
||||
attachment_limit = 0
|
||||
|
|
@ -335,8 +372,13 @@ class File(Document):
|
|||
|
||||
def get_content(self):
|
||||
"""Returns [`file_name`, `content`] for given file name `fname`"""
|
||||
if self.is_folder:
|
||||
frappe.throw(_("Cannot get file contents of a Folder"))
|
||||
|
||||
if self.get('content'):
|
||||
return self.content
|
||||
|
||||
self.validate_url()
|
||||
file_path = self.get_full_path()
|
||||
|
||||
# read the file
|
||||
|
|
@ -423,23 +465,6 @@ class File(Document):
|
|||
else:
|
||||
raise Exception
|
||||
|
||||
|
||||
def validate_url(self, df=None):
|
||||
if self.file_url:
|
||||
if not self.file_url.startswith(("http://", "https://", "/files/", "/private/files/")):
|
||||
frappe.throw(_("URL must start with 'http://' or 'https://'"))
|
||||
return
|
||||
|
||||
if not self.file_url.startswith(("http://", "https://")):
|
||||
# local file
|
||||
root_files_path = get_files_path(is_private=self.is_private)
|
||||
if not os.path.commonpath([root_files_path]) == os.path.commonpath([root_files_path, self.get_full_path()]):
|
||||
# basically the file url is skewed to not point to /files/ or /private/files
|
||||
frappe.throw(_("{0} is not a valid file url").format(self.file_url))
|
||||
self.file_url = unquote(self.file_url)
|
||||
self.file_size = frappe.form_dict.file_size or self.file_size
|
||||
|
||||
|
||||
def get_uploaded_content(self):
|
||||
# should not be unicode when reading a file, hence using frappe.form
|
||||
if 'filedata' in frappe.form_dict:
|
||||
|
|
|
|||
|
|
@ -192,13 +192,10 @@ class TestSameContent(unittest.TestCase):
|
|||
|
||||
|
||||
class TestFile(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.delete_test_data()
|
||||
self.upload_file()
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
frappe.get_doc("File", {"file_name": "file_copy.txt"}).delete()
|
||||
|
|
@ -352,6 +349,22 @@ class TestFile(unittest.TestCase):
|
|||
self.assertEqual(file1.file_url, file2.file_url)
|
||||
self.assertTrue(os.path.exists(file2.get_full_path()))
|
||||
|
||||
def test_parent_directory_validation_in_file_url(self):
|
||||
file1 = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": 'parent_dir.txt',
|
||||
"attached_to_doctype": "",
|
||||
"attached_to_name": "",
|
||||
"is_private": 1,
|
||||
"content": test_content1}).insert()
|
||||
|
||||
file1.file_url = '/private/files/../test.txt'
|
||||
self.assertRaises(frappe.exceptions.ValidationError, file1.save)
|
||||
|
||||
# No validation to see if file exists
|
||||
file1.reload()
|
||||
file1.file_url = '/private/files/parent_dir2.txt'
|
||||
file1.save()
|
||||
|
||||
class TestAttachment(unittest.TestCase):
|
||||
test_doctype = 'Test For Attachment'
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ class Report(Document):
|
|||
def get_columns(self):
|
||||
return [d.as_dict(no_default_fields = True) for d in self.columns]
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_doctype_roles(self):
|
||||
if not self.get('roles') and self.is_standard == 'No':
|
||||
meta = frappe.get_meta(self.ref_doctype)
|
||||
|
|
@ -304,7 +305,7 @@ class Report(Document):
|
|||
|
||||
return data
|
||||
|
||||
@Document.whitelist
|
||||
@frappe.whitelist()
|
||||
def toggle_disable(self, disable):
|
||||
self.db_set("disabled", cint(disable))
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from frappe.core.doctype.report.report import is_prepared_report_disabled
|
|||
from frappe.model.document import Document
|
||||
|
||||
class RolePermissionforPageandReport(Document):
|
||||
@frappe.whitelist()
|
||||
def set_report_page_data(self):
|
||||
self.set_custom_roles()
|
||||
self.check_prepared_report_disabled()
|
||||
|
|
@ -35,12 +36,14 @@ class RolePermissionforPageandReport(Document):
|
|||
doc = frappe.get_doc(doctype, docname)
|
||||
return doc.roles
|
||||
|
||||
@frappe.whitelist()
|
||||
def reset_roles(self):
|
||||
roles = self.get_standard_roles()
|
||||
self.set('roles', roles)
|
||||
self.update_custom_roles()
|
||||
self.update_disable_prepared_report()
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_report_page_data(self):
|
||||
self.update_custom_roles()
|
||||
self.update_disable_prepared_report()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_read": 1,
|
||||
"allow_workflow": 1,
|
||||
"creation": "2014-04-17 16:53:52.640856",
|
||||
"doctype": "DocType",
|
||||
"document_type": "System",
|
||||
|
|
@ -460,9 +458,11 @@
|
|||
},
|
||||
{
|
||||
"default": "Frappe",
|
||||
"description": "The application name will be used in the Login page.",
|
||||
"fieldname": "app_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "App Name"
|
||||
"hidden": 1,
|
||||
"label": "Application Name"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
|
|
@ -474,7 +474,7 @@
|
|||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-25 17:54:32.668876",
|
||||
"modified": "2021-03-30 11:47:47.330437",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
@ -492,4 +492,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
0
frappe/core/doctype/test/__init__.py
Normal file
0
frappe/core/doctype/test/__init__.py
Normal file
8
frappe/core/doctype/test/test.js
Normal file
8
frappe/core/doctype/test/test.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2021, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('test', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
42
frappe/core/doctype/test/test.json
Normal file
42
frappe/core/doctype/test/test.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2021-03-31 10:06:57.919697",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"test"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "test",
|
||||
"fieldtype": "Data",
|
||||
"label": "Test"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_virtual": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-31 10:06:57.919697",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "test",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
35
frappe/core/doctype/test/test.py
Normal file
35
frappe/core/doctype/test/test.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
import json
|
||||
|
||||
|
||||
class test(Document):
|
||||
|
||||
def db_insert(self):
|
||||
d = self.get_valid_dict(convert_dates_to_str=True)
|
||||
with open("data_file.json", "w+") as read_file:
|
||||
json.dump(d, read_file)
|
||||
|
||||
def load_from_db(self):
|
||||
with open("data_file.json", "r") as read_file:
|
||||
d = json.load(read_file)
|
||||
super(Document, self).__init__(d)
|
||||
|
||||
def db_update(self):
|
||||
d = self.get_valid_dict(convert_dates_to_str=True)
|
||||
with open("data_file.json", "w+") as read_file:
|
||||
json.dump(d, read_file)
|
||||
|
||||
def get_list(self, args):
|
||||
with open("data_file.json", "r") as read_file:
|
||||
return [json.load(read_file)]
|
||||
|
||||
def get_value(self, fields, filters, **kwargs):
|
||||
# return []
|
||||
with open("data_file.json", "r") as read_file:
|
||||
return [json.load(read_file)]
|
||||
10
frappe/core/doctype/test/test_test.py
Normal file
10
frappe/core/doctype/test/test_test.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class Testtest(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -558,7 +558,7 @@ class User(Document):
|
|||
user['is_authenticated'] = True
|
||||
if validate_password:
|
||||
try:
|
||||
check_password(user['name'], password)
|
||||
check_password(user['name'], password, delete_tracker_cache=False)
|
||||
except frappe.AuthenticationError:
|
||||
user['is_authenticated'] = False
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class CustomizeForm(Document):
|
|||
frappe.db.sql("delete from tabSingles where doctype='Customize Form'")
|
||||
frappe.db.sql("delete from `tabCustomize Form Field`")
|
||||
|
||||
@frappe.whitelist()
|
||||
def fetch_to_customize(self):
|
||||
self.clear_existing_doc()
|
||||
if not self.doc_type:
|
||||
|
|
@ -133,6 +134,7 @@ class CustomizeForm(Document):
|
|||
self.doc_type = doc_type
|
||||
self.name = "Customize Form"
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_customization(self):
|
||||
if not self.doc_type:
|
||||
return
|
||||
|
|
@ -448,6 +450,7 @@ class CustomizeForm(Document):
|
|||
|
||||
self.flags.update_db = True
|
||||
|
||||
@frappe.whitelist()
|
||||
def reset_to_defaults(self):
|
||||
if not self.doc_type:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from frappe.utils import cstr
|
|||
from frappe.data_migration.doctype.data_migration_mapping.data_migration_mapping import get_source_value
|
||||
|
||||
class DataMigrationRun(Document):
|
||||
@frappe.whitelist()
|
||||
def run(self):
|
||||
self.begin()
|
||||
if self.total_pages > 0:
|
||||
|
|
|
|||
|
|
@ -455,6 +455,10 @@ class Database(object):
|
|||
elif (not ignore) and frappe.db.is_table_missing(e):
|
||||
# table not found, look in singles
|
||||
out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update)
|
||||
if not out:
|
||||
# check for virtual doctype
|
||||
out = self.get_values_from_virtual_doctype(fields, filters, doctype, as_dict, debug, update)
|
||||
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
|
|
@ -507,6 +511,10 @@ class Database(object):
|
|||
else:
|
||||
return r and [[i[1] for i in r]] or []
|
||||
|
||||
def get_values_from_virtual_doctype(self, fields, filters, doctype, as_dict=False, debug=False, update=None):
|
||||
"""Reture single values from virtual doctype."""
|
||||
return frappe.get_doc(doctype).get_value(fields, filters, as_dict=False, debug=False, update=None)
|
||||
|
||||
def get_singles_dict(self, doctype, debug = False):
|
||||
"""Get Single DocType as dict.
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ class DBTable:
|
|||
self.get_columns_from_docfields()
|
||||
|
||||
def sync(self):
|
||||
if self.meta.get('is_virtual'):
|
||||
# no schema to sync for virtual doctypes
|
||||
return
|
||||
if self.is_new():
|
||||
self.create()
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ def make_notification_logs(doc, users):
|
|||
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
|
||||
|
||||
for user in users:
|
||||
if frappe.db.exists('User', {"name": user, "enabled": 1}):
|
||||
if frappe.db.exists('User', {"email": user, "enabled": 1}):
|
||||
if is_notifications_enabled(user):
|
||||
if doc.type == 'Energy Point' and not is_energy_point_enabled():
|
||||
return
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ def get_docinfo(doc=None, doctype=None, name=None):
|
|||
"assignment_logs": get_comments(doc.doctype, doc.name, 'assignment'),
|
||||
"permissions": get_doc_permissions(doc),
|
||||
"shared": frappe.share.get_users(doc.doctype, doc.name),
|
||||
"info_logs": get_comments(doc.doctype, doc.name, 'Info'),
|
||||
"share_logs": get_comments(doc.doctype, doc.name, 'share'),
|
||||
"like_logs": get_comments(doc.doctype, doc.name, 'Like'),
|
||||
"views": get_view_logs(doc.doctype, doc.name),
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import json, inspect
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint
|
||||
from six import text_type, string_types
|
||||
|
||||
@frappe.whitelist()
|
||||
def runserverobj(method, docs=None, dt=None, dn=None, arg=None, args=None):
|
||||
"""run controller method - old style"""
|
||||
if not args: args = arg or ""
|
||||
|
||||
if dt: # not called from a doctype (from a page)
|
||||
if not dn: dn = dt # single
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
|
||||
else:
|
||||
doc = frappe.get_doc(json.loads(docs))
|
||||
doc._original_modified = doc.modified
|
||||
doc.check_if_latest()
|
||||
|
||||
if not doc.has_permission("read"):
|
||||
frappe.msgprint(_("Not permitted"), raise_exception = True)
|
||||
|
||||
if doc:
|
||||
try:
|
||||
args = json.loads(args)
|
||||
except ValueError:
|
||||
args = args
|
||||
|
||||
try:
|
||||
fnargs, varargs, varkw, defaults = inspect.getargspec(getattr(doc, method))
|
||||
except ValueError:
|
||||
fnargs = inspect.getfullargspec(getattr(doc, method)).args
|
||||
varargs = inspect.getfullargspec(getattr(doc, method)).varargs
|
||||
varkw = inspect.getfullargspec(getattr(doc, method)).varkw
|
||||
defaults = inspect.getfullargspec(getattr(doc, method)).defaults
|
||||
|
||||
if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"):
|
||||
r = doc.run_method(method)
|
||||
|
||||
elif "args" in fnargs or not isinstance(args, dict):
|
||||
r = doc.run_method(method, args)
|
||||
|
||||
else:
|
||||
r = doc.run_method(method, **args)
|
||||
|
||||
if r:
|
||||
#build output as csv
|
||||
if cint(frappe.form_dict.get('as_csv')):
|
||||
make_csv_output(r, doc.doctype)
|
||||
else:
|
||||
frappe.response['message'] = r
|
||||
|
||||
frappe.response.docs.append(doc)
|
||||
|
||||
def make_csv_output(res, dt):
|
||||
"""send method response as downloadable CSV file"""
|
||||
import frappe
|
||||
|
||||
from six import StringIO
|
||||
import csv
|
||||
|
||||
f = StringIO()
|
||||
writer = csv.writer(f)
|
||||
for r in res:
|
||||
row = []
|
||||
for v in r:
|
||||
if isinstance(v, string_types):
|
||||
v = v.encode("utf-8")
|
||||
row.append(v)
|
||||
writer.writerow(row)
|
||||
|
||||
f.seek(0)
|
||||
|
||||
frappe.response['result'] = text_type(f.read(), 'utf-8')
|
||||
frappe.response['type'] = 'csv'
|
||||
frappe.response['doctype'] = dt.replace(' ','')
|
||||
|
|
@ -8,30 +8,177 @@ import frappe, json
|
|||
from six.moves import range
|
||||
import frappe.permissions
|
||||
from frappe.model.db_query import DatabaseQuery
|
||||
from frappe.model import default_fields, optional_fields
|
||||
from frappe import _
|
||||
from six import string_types, StringIO
|
||||
from frappe.core.doctype.access_log.access_log import make_access_log
|
||||
from frappe.utils import cstr, format_duration
|
||||
from frappe.model.base_document import get_controller
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.read_only()
|
||||
def get():
|
||||
args = get_form_params()
|
||||
|
||||
data = compress(execute(**args), args = args)
|
||||
|
||||
# If virtual doctype get data from controller het_list method
|
||||
if frappe.db.get_value("DocType", filters={"name": args.doctype}, fieldname="is_virtual"):
|
||||
controller = get_controller(args.doctype)
|
||||
data = compress(controller(args.doctype).get_list(args))
|
||||
else:
|
||||
data = compress(execute(**args), args=args)
|
||||
return data
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def get_list():
|
||||
# uncompressed (refactored from frappe.model.db_query.get_list)
|
||||
return execute(**get_form_params())
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def get_count():
|
||||
args = get_form_params()
|
||||
|
||||
distinct = 'distinct ' if args.distinct=='true' else ''
|
||||
args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"]
|
||||
return execute(**args)[0].get('total_count')
|
||||
|
||||
def execute(doctype, *args, **kwargs):
|
||||
return DatabaseQuery(doctype).execute(*args, **kwargs)
|
||||
|
||||
def get_form_params():
|
||||
"""Stringify GET request parameters."""
|
||||
data = frappe._dict(frappe.local.form_dict)
|
||||
clean_params(data)
|
||||
validate_args(data)
|
||||
return data
|
||||
|
||||
is_report = data.get('view') == 'Report'
|
||||
def validate_args(data):
|
||||
parse_json(data)
|
||||
setup_group_by(data)
|
||||
|
||||
validate_fields(data)
|
||||
if data.filters:
|
||||
validate_filters(data, data.filters)
|
||||
if data.or_filters:
|
||||
validate_filters(data, data.or_filters)
|
||||
|
||||
data.strict = None
|
||||
|
||||
return data
|
||||
|
||||
def validate_fields(data):
|
||||
wildcard = update_wildcard_field_param(data)
|
||||
|
||||
for field in data.fields or []:
|
||||
fieldname = extract_fieldname(field)
|
||||
if is_standard(fieldname):
|
||||
continue
|
||||
|
||||
meta, df = get_meta_and_docfield(fieldname, data)
|
||||
|
||||
if not df:
|
||||
if wildcard:
|
||||
continue
|
||||
else:
|
||||
raise_invalid_field(fieldname)
|
||||
|
||||
# remove the field from the query if the report hide flag is set and current view is Report
|
||||
if df.report_hide and data.view == 'Report':
|
||||
data.fields.remove(field)
|
||||
continue
|
||||
|
||||
if df.fieldname in [_df.fieldname for _df in meta.get_high_permlevel_fields()]:
|
||||
if df.get('permlevel') not in meta.get_permlevel_access(parenttype=data.doctype):
|
||||
data.fields.remove(field)
|
||||
|
||||
def validate_filters(data, filters):
|
||||
if isinstance(filters, list):
|
||||
# filters as list
|
||||
for condition in filters:
|
||||
if len(condition)==3:
|
||||
# [fieldname, condition, value]
|
||||
fieldname = condition[0]
|
||||
if is_standard(fieldname):
|
||||
continue
|
||||
meta, df = get_meta_and_docfield(fieldname, data)
|
||||
if not df:
|
||||
raise_invalid_field(condition[0])
|
||||
else:
|
||||
# [doctype, fieldname, condition, value]
|
||||
fieldname = condition[1]
|
||||
if is_standard(fieldname):
|
||||
continue
|
||||
meta = frappe.get_meta(condition[0])
|
||||
if not meta.get_field(fieldname):
|
||||
raise_invalid_field(fieldname)
|
||||
|
||||
else:
|
||||
for fieldname in filters:
|
||||
if is_standard(fieldname):
|
||||
continue
|
||||
meta, df = get_meta_and_docfield(fieldname, data)
|
||||
if not df:
|
||||
raise_invalid_field(fieldname)
|
||||
|
||||
def setup_group_by(data):
|
||||
'''Add columns for aggregated values e.g. count(name)'''
|
||||
if data.group_by:
|
||||
if data.aggregate_function.lower() not in ('count', 'sum', 'avg'):
|
||||
frappe.throw(_('Invalid aggregate function'))
|
||||
if '`' in data.aggregate_on:
|
||||
raise_invalid_field(data.aggregate_on)
|
||||
data.fields.append('{aggregate_function}(`tab{doctype}`.`{aggregate_on}`) AS _aggregate_column'.format(**data))
|
||||
if data.aggregate_on:
|
||||
data.fields.append(data.aggregate_on)
|
||||
|
||||
data.pop('aggregate_on')
|
||||
data.pop('aggregate_function')
|
||||
|
||||
def raise_invalid_field(fieldname):
|
||||
frappe.throw(_('Field not permitted in query') + ': {0}'.format(fieldname), frappe.DataError)
|
||||
|
||||
def is_standard(fieldname):
|
||||
if '.' in fieldname:
|
||||
parenttype, fieldname = get_parenttype_and_fieldname(fieldname, None)
|
||||
return fieldname in default_fields or fieldname in optional_fields
|
||||
|
||||
def extract_fieldname(field):
|
||||
for text in (',', '/*', '#'):
|
||||
if text in field:
|
||||
raise_invalid_field(field)
|
||||
|
||||
fieldname = field
|
||||
for sep in (' as ', ' AS '):
|
||||
if sep in fieldname:
|
||||
fieldname = fieldname.split(sep)[0]
|
||||
|
||||
# certain functions allowed, extract the fieldname from the function
|
||||
if (fieldname.startswith('count(')
|
||||
or fieldname.startswith('sum(')
|
||||
or fieldname.startswith('avg(')):
|
||||
if not fieldname.strip().endswith(')'):
|
||||
raise_invalid_field(field)
|
||||
fieldname = fieldname.split('(', 1)[1][:-1]
|
||||
|
||||
return fieldname
|
||||
|
||||
def get_meta_and_docfield(fieldname, data):
|
||||
parenttype, fieldname = get_parenttype_and_fieldname(fieldname, data)
|
||||
meta = frappe.get_meta(parenttype)
|
||||
df = meta.get_field(fieldname)
|
||||
return meta, df
|
||||
|
||||
def update_wildcard_field_param(data):
|
||||
if ((isinstance(data.fields, string_types) and data.fields == "*")
|
||||
or (isinstance(data.fields, (list, tuple)) and len(data.fields) == 1 and data.fields[0] == "*")):
|
||||
data.fields = frappe.db.get_table_columns(data.doctype)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def clean_params(data):
|
||||
data.pop('cmd', None)
|
||||
data.pop('data', None)
|
||||
data.pop('ignore_permissions', None)
|
||||
|
|
@ -41,8 +188,12 @@ def get_form_params():
|
|||
if "csrf_token" in data:
|
||||
del data["csrf_token"]
|
||||
|
||||
|
||||
def parse_json(data):
|
||||
if isinstance(data.get("filters"), string_types):
|
||||
data["filters"] = json.loads(data["filters"])
|
||||
if isinstance(data.get("or_filters"), string_types):
|
||||
data["or_filters"] = json.loads(data["or_filters"])
|
||||
if isinstance(data.get("fields"), string_types):
|
||||
data["fields"] = json.loads(data["fields"])
|
||||
if isinstance(data.get("docstatus"), string_types):
|
||||
|
|
@ -52,47 +203,8 @@ def get_form_params():
|
|||
else:
|
||||
data["save_user_settings"] = True
|
||||
|
||||
fields = data["fields"]
|
||||
|
||||
if ((isinstance(fields, string_types) and fields == "*")
|
||||
or (isinstance(fields, (list, tuple)) and len(fields) == 1 and fields[0] == "*")):
|
||||
parenttype = data.doctype
|
||||
data["fields"] = frappe.db.get_table_columns(parenttype)
|
||||
fields = data["fields"]
|
||||
|
||||
for field in fields:
|
||||
key = field.split(" as ")[0]
|
||||
|
||||
if key.startswith('count('): continue
|
||||
if key.startswith('sum('): continue
|
||||
if key.startswith('avg('): continue
|
||||
|
||||
parenttype, fieldname = get_parent_dt_and_field(key, data)
|
||||
|
||||
if fieldname == "*":
|
||||
# * inside list is not allowed with other fields
|
||||
fields.remove(field)
|
||||
|
||||
meta = frappe.get_meta(parenttype)
|
||||
df = meta.get_field(fieldname)
|
||||
|
||||
report_hide = df.report_hide if df else None
|
||||
|
||||
# remove the field from the query if the report hide flag is set and current view is Report
|
||||
if report_hide and is_report:
|
||||
fields.remove(field)
|
||||
|
||||
if df and fieldname in [df.fieldname for df in meta.get_high_permlevel_fields()]:
|
||||
if df.get('permlevel') not in meta.get_permlevel_access(parenttype=data.doctype) and field in fields:
|
||||
fields.remove(field)
|
||||
|
||||
# queries must always be server side
|
||||
data.query = None
|
||||
data.strict = None
|
||||
|
||||
return data
|
||||
|
||||
def get_parent_dt_and_field(field, data):
|
||||
def get_parenttype_and_fieldname(field, data):
|
||||
if "." in field:
|
||||
parenttype, fieldname = field.split(".")[0][4:-1], field.split(".")[1].strip("`")
|
||||
else:
|
||||
|
|
@ -101,7 +213,6 @@ def get_parent_dt_and_field(field, data):
|
|||
|
||||
return parenttype, fieldname
|
||||
|
||||
|
||||
def compress(data, args = {}):
|
||||
"""separate keys and values"""
|
||||
from frappe.desk.query_report import add_total_row
|
||||
|
|
@ -327,8 +438,9 @@ def get_stats(stats, doctype, filters=[]):
|
|||
|
||||
try:
|
||||
columns = frappe.db.get_table_columns(doctype)
|
||||
except frappe.db.InternalError:
|
||||
except (frappe.db.InternalError, frappe.db.ProgrammingError):
|
||||
# raised when _user_tags column is added on the fly
|
||||
# raised if its a virtual doctype
|
||||
columns = []
|
||||
|
||||
for tag in tags:
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ from __future__ import unicode_literals
|
|||
import frappe, json
|
||||
from frappe.utils import cstr, unique, cint
|
||||
from frappe.permissions import has_permission
|
||||
from frappe.handler import is_whitelisted
|
||||
from frappe import _
|
||||
from frappe import _, is_whitelisted
|
||||
from six import string_types
|
||||
import re
|
||||
import wrapt
|
||||
|
|
@ -221,4 +220,4 @@ def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
|
|||
if kwargs['doctype'] and not frappe.db.exists('DocType', kwargs['doctype']):
|
||||
return []
|
||||
|
||||
return fn(**kwargs)
|
||||
return fn(**kwargs)
|
||||
|
|
|
|||
|
|
@ -36,20 +36,27 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters):
|
|||
return out
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_children(doctype, parent='', **filters):
|
||||
def get_children(doctype, parent=''):
|
||||
return _get_children(doctype, parent)
|
||||
|
||||
def _get_children(doctype, parent='', ignore_permissions=False):
|
||||
parent_field = 'parent_' + doctype.lower().replace(' ', '_')
|
||||
filters=[['ifnull(`{0}`,"")'.format(parent_field), '=', parent],
|
||||
filters = [['ifnull(`{0}`,"")'.format(parent_field), '=', parent],
|
||||
['docstatus', '<' ,'2']]
|
||||
|
||||
doctype_meta = frappe.get_meta(doctype)
|
||||
data = frappe.get_list(doctype, fields=[
|
||||
'name as value',
|
||||
'{0} as title'.format(doctype_meta.get('title_field') or 'name'),
|
||||
'is_group as expandable'],
|
||||
filters=filters,
|
||||
order_by='name')
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
return data
|
||||
return frappe.get_list(
|
||||
doctype,
|
||||
fields=[
|
||||
'name as value',
|
||||
'{0} as title'.format(meta.get('title_field') or 'name'),
|
||||
'is_group as expandable'
|
||||
],
|
||||
filters=filters,
|
||||
order_by='name',
|
||||
ignore_permissions=ignore_permissions
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_node():
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ class Newsletter(WebsiteGenerator):
|
|||
self.queue_all(test_email=True)
|
||||
frappe.msgprint(_("Test email sent to {0}").format(self.test_email_id))
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_emails(self):
|
||||
"""send emails to leads and customers"""
|
||||
if self.email_sent:
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class FrappeClient(object):
|
|||
'cmd': 'logout',
|
||||
}, verify=self.verify, headers=self.headers)
|
||||
|
||||
def get_list(self, doctype, fields='"*"', filters=None, limit_start=0, limit_page_length=0):
|
||||
def get_list(self, doctype, fields='["name"]', filters=None, limit_start=0, limit_page_length=0):
|
||||
"""Returns list of records of a particular type"""
|
||||
if not isinstance(fields, string_types):
|
||||
fields = json.dumps(fields)
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@
|
|||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
import frappe.utils
|
||||
import frappe.sessions
|
||||
import frappe.desk.form.run_method
|
||||
from frappe.utils.response import build_response
|
||||
from frappe.api import validate_auth
|
||||
from frappe.utils import cint
|
||||
from frappe.api import validate_auth
|
||||
from frappe import _, is_whitelisted
|
||||
from frappe.utils.response import build_response
|
||||
from frappe.utils.csvutils import build_csv_response
|
||||
from frappe.core.doctype.server_script.server_script_utils import run_server_script_api
|
||||
from werkzeug.wrappers import Response
|
||||
from six import string_types
|
||||
|
||||
|
||||
ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
|
|
@ -54,18 +56,14 @@ def execute_cmd(cmd, from_async=False):
|
|||
try:
|
||||
method = get_attr(cmd)
|
||||
except Exception as e:
|
||||
if frappe.local.conf.developer_mode:
|
||||
raise e
|
||||
else:
|
||||
frappe.respond_as_web_page(title='Invalid Method', html='Method not found',
|
||||
indicator_color='red', http_status_code=404)
|
||||
return
|
||||
frappe.throw(_('Invalid Method'))
|
||||
|
||||
if from_async:
|
||||
method = method.queue
|
||||
|
||||
is_whitelisted(method)
|
||||
is_valid_http_method(method)
|
||||
if method != run_doc_method:
|
||||
is_whitelisted(method)
|
||||
is_valid_http_method(method)
|
||||
|
||||
return frappe.call(method, **frappe.form_dict)
|
||||
|
||||
|
|
@ -73,33 +71,15 @@ def is_valid_http_method(method):
|
|||
http_method = frappe.local.request.method
|
||||
|
||||
if http_method not in frappe.allowed_http_methods_for_whitelisted_func[method]:
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
throw_permission_error()
|
||||
|
||||
def is_whitelisted(method):
|
||||
# check if whitelisted
|
||||
if frappe.session['user'] == 'Guest':
|
||||
if (method not in frappe.guest_methods):
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
if method not in frappe.xss_safe_methods:
|
||||
# strictly sanitize form_dict
|
||||
# escapes html characters like <> except for predefined tags like a, b, ul etc.
|
||||
for key, value in frappe.form_dict.items():
|
||||
if isinstance(value, string_types):
|
||||
frappe.form_dict[key] = frappe.utils.sanitize_html(value)
|
||||
|
||||
else:
|
||||
if not method in frappe.whitelisted:
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
def throw_permission_error():
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def version():
|
||||
return frappe.__version__
|
||||
|
||||
@frappe.whitelist()
|
||||
def runserverobj(method, docs=None, dt=None, dn=None, arg=None, args=None):
|
||||
frappe.desk.form.run_method.runserverobj(method, docs=docs, dt=dt, dn=dn, arg=arg, args=args)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def logout():
|
||||
frappe.local.login_manager.logout()
|
||||
|
|
@ -112,15 +92,6 @@ def web_logout():
|
|||
frappe.respond_as_web_page(_("Logged Out"), _("You have been successfully logged out"),
|
||||
indicator_color='green')
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def run_custom_method(doctype, name, custom_method):
|
||||
"""cmd=run_custom_method&doctype={doctype}&name={name}&custom_method={custom_method}"""
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
if getattr(doc, custom_method, frappe._dict()).is_whitelisted:
|
||||
frappe.call(getattr(doc, custom_method), **frappe.local.form_dict)
|
||||
else:
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
@frappe.whitelist()
|
||||
def uploadfile():
|
||||
ret = None
|
||||
|
|
@ -222,6 +193,66 @@ def get_attr(cmd):
|
|||
frappe.log("method:" + cmd)
|
||||
return method
|
||||
|
||||
@frappe.whitelist(allow_guest = True)
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def ping():
|
||||
return "pong"
|
||||
|
||||
|
||||
def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
|
||||
"""run a whitelisted controller method"""
|
||||
import json
|
||||
import inspect
|
||||
|
||||
if not args:
|
||||
args = arg or ""
|
||||
|
||||
if dt: # not called from a doctype (from a page)
|
||||
if not dn:
|
||||
dn = dt # single
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
|
||||
else:
|
||||
doc = frappe.get_doc(json.loads(docs))
|
||||
doc._original_modified = doc.modified
|
||||
doc.check_if_latest()
|
||||
|
||||
if not doc or not doc.has_permission("read"):
|
||||
throw_permission_error()
|
||||
|
||||
try:
|
||||
args = json.loads(args)
|
||||
except ValueError:
|
||||
args = args
|
||||
|
||||
method_obj = getattr(doc, method)
|
||||
fn = getattr(method_obj, '__func__', method_obj)
|
||||
is_whitelisted(fn)
|
||||
is_valid_http_method(fn)
|
||||
|
||||
try:
|
||||
fnargs = inspect.getargspec(method_obj)[0]
|
||||
except ValueError:
|
||||
fnargs = inspect.getfullargspec(method_obj).args
|
||||
|
||||
if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"):
|
||||
response = doc.run_method(method)
|
||||
|
||||
elif "args" in fnargs or not isinstance(args, dict):
|
||||
response = doc.run_method(method, args)
|
||||
|
||||
else:
|
||||
response = doc.run_method(method, **args)
|
||||
|
||||
frappe.response.docs.append(doc)
|
||||
if not response:
|
||||
return
|
||||
|
||||
# build output as csv
|
||||
if cint(frappe.form_dict.get('as_csv')):
|
||||
build_csv_response(response, doc.doctype.replace(' ', ''))
|
||||
return
|
||||
|
||||
frappe.response['message'] = response
|
||||
|
||||
# for backwards compatibility
|
||||
runserverobj = run_doc_method
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class ConnectedApp(Document):
|
|||
scope=self.get_scopes()
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def initiate_web_application_flow(self, user=None, success_uri=None):
|
||||
"""Return an authorization URL for the user. Save state in Token Cache."""
|
||||
user = user or frappe.session.user
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class SocialLoginKey(Document):
|
|||
icon_file = icon_map[self.provider_name]
|
||||
self.icon = '/assets/frappe/icons/social/{0}'.format(icon_file)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_social_login_provider(self, provider, initialize=False):
|
||||
providers = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -555,22 +555,25 @@ class BaseDocument(object):
|
|||
not _df.get('fetch_if_empty')
|
||||
or (_df.get('fetch_if_empty') and not self.get(_df.fieldname))
|
||||
]
|
||||
if not frappe.get_meta(doctype).get('is_virtual'):
|
||||
if not fields_to_fetch:
|
||||
# cache a single value type
|
||||
values = frappe._dict(name=frappe.db.get_value(doctype, docname,
|
||||
'name', cache=True))
|
||||
else:
|
||||
values_to_fetch = ['name'] + [_df.fetch_from.split('.')[-1]
|
||||
for _df in fields_to_fetch]
|
||||
|
||||
if not fields_to_fetch:
|
||||
# cache a single value type
|
||||
values = frappe._dict(name=frappe.db.get_value(doctype, docname,
|
||||
'name', cache=True))
|
||||
else:
|
||||
values_to_fetch = ['name'] + [_df.fetch_from.split('.')[-1]
|
||||
for _df in fields_to_fetch]
|
||||
|
||||
# don't cache if fetching other values too
|
||||
values = frappe.db.get_value(doctype, docname,
|
||||
values_to_fetch, as_dict=True)
|
||||
# don't cache if fetching other values too
|
||||
values = frappe.db.get_value(doctype, docname,
|
||||
values_to_fetch, as_dict=True)
|
||||
|
||||
if frappe.get_meta(doctype).issingle:
|
||||
values.name = doctype
|
||||
|
||||
if frappe.get_meta(doctype).get('is_virtual'):
|
||||
values = frappe.get_doc(doctype, docname)
|
||||
|
||||
if values:
|
||||
setattr(self, df.fieldname, values.name)
|
||||
|
||||
|
|
@ -792,7 +795,7 @@ class BaseDocument(object):
|
|||
|
||||
def _save_passwords(self):
|
||||
"""Save password field values in __Auth table"""
|
||||
from frappe.utils.password import set_encrypted_password
|
||||
from frappe.utils.password import set_encrypted_password, remove_encrypted_password
|
||||
|
||||
if self.flags.ignore_save_passwords is True:
|
||||
return
|
||||
|
|
@ -800,6 +803,10 @@ class BaseDocument(object):
|
|||
for df in self.meta.get('fields', {'fieldtype': ('=', 'Password')}):
|
||||
if self.flags.ignore_save_passwords and df.fieldname in self.flags.ignore_save_passwords: continue
|
||||
new_password = self.get(df.fieldname)
|
||||
|
||||
if not new_password:
|
||||
remove_encrypted_password(self.doctype, self.name, df.fieldname)
|
||||
|
||||
if new_password and not self.is_dummy_password(new_password):
|
||||
# is not a dummy password like '*****'
|
||||
set_encrypted_password(self.doctype, self.name, new_password, df.fieldname)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import frappe.permissions
|
|||
from datetime import datetime
|
||||
import frappe, json, copy, re
|
||||
from frappe.model import optional_fields
|
||||
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
|
||||
|
|
@ -32,7 +31,7 @@ class DatabaseQuery(object):
|
|||
self.flags = frappe._dict()
|
||||
self.reference_doctype = None
|
||||
|
||||
def execute(self, query=None, fields=None, filters=None, or_filters=None,
|
||||
def execute(self, fields=None, filters=None, or_filters=None,
|
||||
docstatus=None, group_by=None, order_by=None, limit_start=False,
|
||||
limit_page_length=None, as_list=False, with_childnames=False, debug=False,
|
||||
ignore_permissions=False, user=None, with_comment_count=False,
|
||||
|
|
@ -104,12 +103,9 @@ class DatabaseQuery(object):
|
|||
# no table & ignore_ddl, return
|
||||
if not self.columns: return []
|
||||
|
||||
if query:
|
||||
result = self.run_custom_query(query)
|
||||
else:
|
||||
result = self.build_and_run()
|
||||
if return_query:
|
||||
return result
|
||||
result = self.build_and_run()
|
||||
if return_query:
|
||||
return result
|
||||
|
||||
if with_comment_count and not as_list and self.doctype:
|
||||
self.add_comment_count(result)
|
||||
|
|
@ -707,12 +703,6 @@ class DatabaseQuery(object):
|
|||
|
||||
return " and ".join(conditions) if conditions else ""
|
||||
|
||||
|
||||
def run_custom_query(self, query):
|
||||
if '%(key)s' in query:
|
||||
query = query.replace('%(key)s', '`name`')
|
||||
return frappe.db.sql(query, as_dict = (not self.as_list))
|
||||
|
||||
def set_order_by(self, args):
|
||||
meta = frappe.get_meta(self.doctype)
|
||||
|
||||
|
|
@ -754,7 +744,7 @@ class DatabaseQuery(object):
|
|||
return
|
||||
|
||||
_lower = parameters.lower()
|
||||
if 'select' in _lower and ' from ' in _lower:
|
||||
if 'select' in _lower and 'from' in _lower:
|
||||
frappe.throw(_('Cannot use sub-query in order by'))
|
||||
|
||||
if re.compile(r".*[^a-z0-9-_ ,`'\"\.\(\)].*").match(_lower):
|
||||
|
|
@ -795,6 +785,18 @@ class DatabaseQuery(object):
|
|||
|
||||
update_user_settings(self.doctype, user_settings)
|
||||
|
||||
def check_parent_permission(parent, child_doctype):
|
||||
if parent:
|
||||
# User may pass fake parent and get the information from the child table
|
||||
if child_doctype and not frappe.db.exists('DocField',
|
||||
{'parent': parent, 'options': child_doctype}):
|
||||
raise frappe.PermissionError
|
||||
|
||||
if frappe.permissions.has_permission(parent):
|
||||
return
|
||||
# Either parent not passed or the user doesn't have permission on parent doctype of child table!
|
||||
raise frappe.PermissionError
|
||||
|
||||
def get_order_by(doctype, meta):
|
||||
order_by = ""
|
||||
|
||||
|
|
@ -819,30 +821,6 @@ def get_order_by(doctype, meta):
|
|||
|
||||
return order_by
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_list(doctype, *args, **kwargs):
|
||||
'''wrapper for DatabaseQuery'''
|
||||
kwargs.pop('cmd', None)
|
||||
kwargs.pop('ignore_permissions', None)
|
||||
kwargs.pop('data', None)
|
||||
kwargs.pop('strict', None)
|
||||
kwargs.pop('user', None)
|
||||
|
||||
# If doctype is child table
|
||||
if frappe.is_table(doctype):
|
||||
# Example frappe.db.get_list('Purchase Receipt Item', {'parent': 'Purchase Receipt'})
|
||||
# Here purchase receipt is the parent doctype of the child doctype Purchase Receipt Item
|
||||
|
||||
if not kwargs.get('parent'):
|
||||
frappe.flags.error_message = _('Parent is required to get child table data')
|
||||
raise frappe.PermissionError(doctype)
|
||||
|
||||
check_parent_permission(kwargs.get('parent'), doctype)
|
||||
del kwargs['parent']
|
||||
|
||||
return DatabaseQuery(doctype).execute(None, *args, **kwargs)
|
||||
|
||||
def is_parent_only_filter(doctype, filters):
|
||||
#check if filters contains only parent doctype
|
||||
only_parent_doctype = True
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
from __future__ import unicode_literals, print_function
|
||||
import frappe
|
||||
import time
|
||||
from frappe import _, msgprint
|
||||
from frappe import _, msgprint, is_whitelisted
|
||||
from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff
|
||||
from frappe.model.base_document import BaseDocument, get_controller
|
||||
from frappe.model.naming import set_new_name
|
||||
|
|
@ -126,10 +126,10 @@ class Document(BaseDocument):
|
|||
raise ValueError('Illegal arguments')
|
||||
|
||||
@staticmethod
|
||||
def whitelist(f):
|
||||
def whitelist(fn):
|
||||
"""Decorator: Whitelist method to be called remotely via REST API."""
|
||||
f.whitelisted = True
|
||||
return f
|
||||
frappe.whitelist()(fn)
|
||||
return fn
|
||||
|
||||
def reload(self):
|
||||
"""Reload document from database"""
|
||||
|
|
@ -697,7 +697,7 @@ class Document(BaseDocument):
|
|||
`self.check_docstatus_transition`."""
|
||||
conflict = False
|
||||
self._action = "save"
|
||||
if not self.get('__islocal'):
|
||||
if not self.get('__islocal') and not self.meta.get('is_virtual'):
|
||||
if self.meta.issingle:
|
||||
modified = frappe.db.sql("""select value from tabSingles
|
||||
where doctype=%s and field='modified' for update""", self.doctype)
|
||||
|
|
@ -1148,12 +1148,12 @@ class Document(BaseDocument):
|
|||
|
||||
return composer
|
||||
|
||||
def is_whitelisted(self, method):
|
||||
fn = getattr(self, method, None)
|
||||
if not fn:
|
||||
raise NotFound("Method {0} not found".format(method))
|
||||
elif not getattr(fn, "whitelisted", False):
|
||||
raise Forbidden("Method {0} not whitelisted".format(method))
|
||||
def is_whitelisted(self, method_name):
|
||||
method = getattr(self, method_name, None)
|
||||
if not method:
|
||||
raise NotFound("Method {0} not found".format(method_name))
|
||||
|
||||
is_whitelisted(getattr(method, '__func__', method))
|
||||
|
||||
def validate_value(self, fieldname, condition, val2, doc=None, raise_exception=None):
|
||||
"""Check that value of fieldname should be 'condition' val2
|
||||
|
|
|
|||
|
|
@ -247,6 +247,21 @@ def make_boilerplate(template, doc, opts=None):
|
|||
base_class = 'NestedSet'
|
||||
base_class_import = 'from frappe.utils.nestedset import NestedSet'
|
||||
|
||||
custom_controller = 'pass'
|
||||
if doc.get('is_virtual'):
|
||||
custom_controller = """
|
||||
def db_insert(self):
|
||||
pass
|
||||
|
||||
def load_from_db(self):
|
||||
pass
|
||||
|
||||
def db_update(self):
|
||||
pass
|
||||
|
||||
def get_list(self, args):
|
||||
pass"""
|
||||
|
||||
with open(target_file_path, 'w') as target:
|
||||
with open(os.path.join(get_module_path("core"), "doctype", scrub(doc.doctype),
|
||||
"boilerplate", template), 'r') as source:
|
||||
|
|
@ -257,5 +272,6 @@ def make_boilerplate(template, doc, opts=None):
|
|||
classname=doc.name.replace(" ", ""),
|
||||
base_class_import=base_class_import,
|
||||
base_class=base_class,
|
||||
doctype=doc.name, **opts)
|
||||
doctype=doc.name, **opts,
|
||||
custom_controller=custom_controller)
|
||||
))
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import frappe
|
|||
|
||||
def execute():
|
||||
if frappe.db.table_exists('List View Setting'):
|
||||
if not frappe.db.table_exists('List View Settings'):
|
||||
frappe.reload_doc("desk", "doctype", "List View Settings")
|
||||
|
||||
existing_list_view_settings = frappe.get_all('List View Settings', as_list=True)
|
||||
for list_view_setting in frappe.get_all('List View Setting', fields = ['disable_count', 'disable_sidebar_stats', 'disable_auto_refresh', 'name']):
|
||||
name = list_view_setting.pop('name')
|
||||
|
|
@ -16,5 +19,6 @@ def execute():
|
|||
# setting name here is necessary because autoname is set as prompt
|
||||
list_view_settings.name = name
|
||||
list_view_settings.insert()
|
||||
|
||||
frappe.delete_doc("DocType", "List View Setting", force=True)
|
||||
frappe.db.commit()
|
||||
|
|
|
|||
|
|
@ -79,9 +79,9 @@
|
|||
"public/less/controls.less",
|
||||
"public/less/chat.less",
|
||||
"public/css/fonts/inter/inter.css",
|
||||
"public/scss/desk.scss",
|
||||
"node_modules/frappe-charts/dist/frappe-charts.min.css",
|
||||
"node_modules/plyr/dist/plyr.css"
|
||||
"node_modules/plyr/dist/plyr.css",
|
||||
"public/scss/desk.scss"
|
||||
],
|
||||
"css/frappe-rtl.css": [
|
||||
"public/css/bootstrap-rtl.css",
|
||||
|
|
|
|||
|
|
@ -693,4 +693,10 @@
|
|||
<symbol viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-star">
|
||||
<path d="M11.5516 2.90849C11.735 2.53687 12.265 2.53687 12.4484 2.90849L14.8226 7.71919C14.8954 7.86677 15.0362 7.96905 15.1991 7.99271L20.508 8.76415C20.9181 8.82374 21.0818 9.32772 20.7851 9.61699L16.9435 13.3616C16.8257 13.4765 16.7719 13.642 16.7997 13.8042L17.7066 19.0916C17.7766 19.5001 17.3479 19.8116 16.9811 19.6187L12.2327 17.1223C12.087 17.0457 11.913 17.0457 11.7673 17.1223L7.01888 19.6187C6.65207 19.8116 6.22335 19.5001 6.29341 19.0916L7.20028 13.8042C7.2281 13.642 7.17433 13.4765 7.05648 13.3616L3.21491 9.61699C2.91815 9.32772 3.08191 8.82374 3.49202 8.76415L8.80094 7.99271C8.9638 7.96905 9.10458 7.86677 9.17741 7.71919L11.5516 2.90849Z" fill="var(--star-fill)" stroke="var(--star-fill)"/>
|
||||
</symbol>
|
||||
<symbol fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" id="icon-map">
|
||||
<g stroke="#111" stroke-miterlimit="10">
|
||||
<path d="M11.467 3.458c1.958 1.957 1.958 5.088.027 7.02L7.97 14l-3.523-3.523a4.945 4.945 0 010-6.993l.026-.026a4.922 4.922 0 016.993 0zm0 0c-.026-.026-.026-.026 0 0z"></path>
|
||||
<path d="M7.971 8.259a1.305 1.305 0 100-2.61 1.305 1.305 0 000 2.61z"></path>
|
||||
</g>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
|
@ -15,7 +15,7 @@ frappe.db = {
|
|||
}
|
||||
return new Promise ((resolve) => {
|
||||
frappe.call({
|
||||
method: 'frappe.model.db_query.get_list',
|
||||
method: 'frappe.desk.reportview.get_list',
|
||||
args: args,
|
||||
type: 'GET',
|
||||
callback: function(r) {
|
||||
|
|
@ -92,25 +92,19 @@ frappe.db = {
|
|||
},
|
||||
count: function(doctype, args={}) {
|
||||
let filters = args.filters || {};
|
||||
const with_child_table_filter = Array.isArray(filters) && filters.some(filter => {
|
||||
|
||||
// has a filter with childtable?
|
||||
const distinct = Array.isArray(filters) && filters.some(filter => {
|
||||
return filter[0] !== doctype;
|
||||
});
|
||||
|
||||
const fields = [
|
||||
// cannot break this line as it adds extra \n's and \t's which breaks the query
|
||||
`count(${with_child_table_filter ? 'distinct': ''} ${frappe.model.get_full_column_name('name', doctype)}) AS total_count`
|
||||
];
|
||||
const fields = [];
|
||||
|
||||
return frappe.call({
|
||||
type: 'GET',
|
||||
method: 'frappe.desk.reportview.get',
|
||||
args: {
|
||||
doctype,
|
||||
filters,
|
||||
fields,
|
||||
}
|
||||
}).then(r => {
|
||||
return r.message.values[0][0];
|
||||
return frappe.xcall('frappe.desk.reportview.get_count', {
|
||||
doctype,
|
||||
filters,
|
||||
fields,
|
||||
distinct,
|
||||
});
|
||||
},
|
||||
get_link_options(doctype, txt = '', filters={}) {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
|
|||
var me = this;
|
||||
if(this.frm && this.frm.docname) {
|
||||
frappe.call({
|
||||
method: "runserverobj",
|
||||
method: "run_doc_method",
|
||||
args: {'docs': this.frm.doc, 'method': this.df.options },
|
||||
btn: this.$input,
|
||||
callback: function(r) {
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
|
|||
refresh() {
|
||||
this._super();
|
||||
let color = this.get_color();
|
||||
if (this.picker.color !== color) {
|
||||
if (this.picker && this.picker.color !== color) {
|
||||
this.picker.color = color;
|
||||
this.picker.refresh();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,11 +83,16 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
|
|||
var doctype = this.get_options();
|
||||
var me = this;
|
||||
|
||||
if(!doctype) return;
|
||||
if (!doctype) return;
|
||||
|
||||
let df = this.df;
|
||||
if (this.frm && this.frm.doctype !== this.df.parent) {
|
||||
// incase of grid use common df set in grid
|
||||
df = this.frm.get_docfield(this.doc.parentfield, this.df.fieldname);
|
||||
}
|
||||
// set values to fill in the new document
|
||||
if(this.df.get_route_options_for_new_doc) {
|
||||
frappe.route_options = this.df.get_route_options_for_new_doc(this);
|
||||
if (df && df.get_route_options_for_new_doc) {
|
||||
frappe.route_options = df.get_route_options_for_new_doc(this);
|
||||
} else {
|
||||
frappe.route_options = {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,17 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({
|
|||
const grid_rows = grid.grid_rows;
|
||||
const doctype = grid.doctype;
|
||||
const row_docname = $(e.target).closest('.grid-row').data('name');
|
||||
const in_grid_form = $(e.target).closest('.form-in-grid').length;
|
||||
|
||||
let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
|
||||
let pasted_data = clipboard_data.getData('Text');
|
||||
|
||||
if (!pasted_data) return;
|
||||
if (!pasted_data || in_grid_form) return;
|
||||
|
||||
let data = frappe.utils.csv_to_array(pasted_data, '\t');
|
||||
|
||||
if (data.length === 1 && data[0].length === 1) return;
|
||||
|
||||
let fieldnames = [];
|
||||
// for raw data with column header
|
||||
if (this.get_field(data[0][0])) {
|
||||
|
|
@ -49,30 +53,30 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({
|
|||
}
|
||||
|
||||
let row_idx = locals[doctype][row_docname].idx;
|
||||
let data_length = data.length;
|
||||
data.forEach((row, i) => {
|
||||
let blank_row = !row.filter(Boolean).length;
|
||||
if (blank_row) return;
|
||||
|
||||
setTimeout(() => {
|
||||
if (row_idx > this.frm.doc[table_field].length) {
|
||||
this.grid.add_new_row();
|
||||
}
|
||||
if (row_idx > 1 && (row_idx - 1) % grid_pagination.page_length === 0) {
|
||||
grid_pagination.go_to_page(grid_pagination.page_index + 1);
|
||||
}
|
||||
|
||||
const row_name = grid_rows[row_idx - 1].doc.name;
|
||||
row.forEach((value, data_index) => {
|
||||
if (fieldnames[data_index]) {
|
||||
frappe.model.set_value(doctype, row_name, fieldnames[data_index], value);
|
||||
let blank_row = !row.filter(Boolean).length;
|
||||
if (!blank_row) {
|
||||
if (row_idx > this.frm.doc[table_field].length) {
|
||||
this.grid.add_new_row();
|
||||
}
|
||||
});
|
||||
row_idx++;
|
||||
|
||||
let progress = i + 1;
|
||||
frappe.show_progress(__('Processing'), progress, data.length);
|
||||
if (progress === data.length) {
|
||||
frappe.hide_progress();
|
||||
if (row_idx > 1 && (row_idx - 1) % grid_pagination.page_length === 0) {
|
||||
grid_pagination.go_to_page(grid_pagination.page_index + 1);
|
||||
}
|
||||
|
||||
const row_name = grid_rows[row_idx - 1].doc.name;
|
||||
row.forEach((value, data_index) => {
|
||||
if (fieldnames[data_index]) {
|
||||
frappe.model.set_value(doctype, row_name, fieldnames[data_index], value);
|
||||
}
|
||||
});
|
||||
row_idx++;
|
||||
if (data_length >= 10) {
|
||||
let progress = i + 1;
|
||||
frappe.show_progress(__('Processing'), progress, data_length, null, true);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ class FormTimeline extends BaseTimeline {
|
|||
this.timeline_items.push(...this.get_custom_timeline_contents());
|
||||
this.timeline_items.push(...this.get_assignment_timeline_contents());
|
||||
this.timeline_items.push(...this.get_attachment_timeline_contents());
|
||||
this.timeline_items.push(...this.get_info_timeline_contents());
|
||||
this.timeline_items.push(...this.get_milestone_timeline_contents());
|
||||
}
|
||||
}
|
||||
|
|
@ -269,6 +270,17 @@ class FormTimeline extends BaseTimeline {
|
|||
return assignment_timeline_contents;
|
||||
}
|
||||
|
||||
get_info_timeline_contents() {
|
||||
let info_timeline_contents = [];
|
||||
(this.doc_info.info_logs || []).forEach(info_log => {
|
||||
info_timeline_contents.push({
|
||||
creation: info_log.creation,
|
||||
content: `${this.get_user_link(info_log.comment_email)} ${info_log.content}`,
|
||||
});
|
||||
});
|
||||
return info_timeline_contents;
|
||||
}
|
||||
|
||||
get_attachment_timeline_contents() {
|
||||
let attachment_timeline_contents = [];
|
||||
(this.doc_info.attachment_logs || []).forEach(attachment_log => {
|
||||
|
|
|
|||
|
|
@ -144,6 +144,27 @@ function get_version_timeline_content(version_doc, frm) {
|
|||
|
||||
|
||||
function get_version_comment(version_doc, text) {
|
||||
// TODO: Replace with a better solution
|
||||
if (text.includes("<a")) {
|
||||
// if text already has linked content in it
|
||||
// then just add a version link to unlinked content
|
||||
let version_comment = "";
|
||||
let unlinked_content = "";
|
||||
|
||||
Array.from($(text)).forEach(element => {
|
||||
if ($(element).is('a')) {
|
||||
version_comment += unlinked_content ? frappe.utils.get_form_link('Version', version_doc.name, true, unlinked_content) : "";
|
||||
unlinked_content = "";
|
||||
version_comment += element.outerHTML;
|
||||
} else {
|
||||
unlinked_content += element.outerHTML || element.textContent;
|
||||
}
|
||||
});
|
||||
if (unlinked_content) {
|
||||
version_comment += frappe.utils.get_form_link('Version', version_doc.name, true, unlinked_content);
|
||||
}
|
||||
return version_comment;
|
||||
}
|
||||
return frappe.utils.get_form_link('Version', version_doc.name, true, text);
|
||||
}
|
||||
|
||||
|
|
@ -164,4 +185,5 @@ function get_user_link(doc) {
|
|||
return frappe.utils.get_form_link('User', user, true, user_display_text);
|
||||
}
|
||||
|
||||
export { get_version_timeline_content };
|
||||
export { get_version_timeline_content };
|
||||
|
||||
|
|
|
|||
|
|
@ -451,7 +451,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
return this.script_manager.trigger("onload_post_render");
|
||||
}
|
||||
},
|
||||
() => this.focus_on_first_input(),
|
||||
() => this.is_new() && this.focus_on_first_input(),
|
||||
() => this.run_after_load_hook(),
|
||||
() => this.dashboard.after_refresh()
|
||||
]);
|
||||
|
|
@ -1075,7 +1075,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
|
||||
refresh_field(fname) {
|
||||
if(this.fields_dict[fname] && this.fields_dict[fname].refresh) {
|
||||
if (this.fields_dict[fname] && this.fields_dict[fname].refresh) {
|
||||
this.fields_dict[fname].refresh();
|
||||
this.layout.refresh_dependency();
|
||||
}
|
||||
|
|
@ -1241,20 +1241,22 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
}
|
||||
|
||||
set_df_property(fieldname, property, value, docname, table_field) {
|
||||
var df;
|
||||
set_df_property(fieldname, property, value, docname, table_field, table_row_name=null) {
|
||||
let df;
|
||||
if (!docname || !table_field) {
|
||||
df = this.get_docfield(fieldname);
|
||||
} else {
|
||||
var grid = this.fields_dict[fieldname].grid,
|
||||
fname = frappe.utils.filter_dict(grid.docfields, {'fieldname': table_field});
|
||||
if (fname && fname.length)
|
||||
df = frappe.meta.get_docfield(fname[0].parent, table_field, docname);
|
||||
const grid = this.fields_dict[fieldname].grid;
|
||||
const filtered_fields = frappe.utils.filter_dict(grid.docfields, {'fieldname': table_field});
|
||||
if (filtered_fields.length) {
|
||||
df = frappe.meta.get_docfield(filtered_fields[0].parent, table_field, table_row_name);
|
||||
}
|
||||
}
|
||||
if (df && df[property] != value) {
|
||||
df[property] = value;
|
||||
if (!docname || !table_field) {
|
||||
// do not refresh childtable fields since `this.fields_dict` doesn't have child table fields
|
||||
if (table_field && table_row_name) {
|
||||
this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name].refresh_field(fieldname);
|
||||
} else {
|
||||
this.refresh_field(fieldname);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,10 @@ frappe.ui.form.FormViewers = class FormViewers {
|
|||
}
|
||||
|
||||
refresh() {
|
||||
// REDESIGN-TODO: fix this
|
||||
// let users = this.frm.get_docinfo()['viewers'];
|
||||
// let currently_viewing = users.current.filter(user => user != frappe.session.user);
|
||||
// let avatar_group = frappe.avatar_group(currently_viewing, 5, {'align': 'left', 'overlap': true});
|
||||
this.parent.empty(); //.append(avatar_group);
|
||||
let users = this.frm.get_docinfo()['viewers'];
|
||||
let currently_viewing = users.current.filter(user => user != frappe.session.user);
|
||||
let avatar_group = frappe.avatar_group(currently_viewing, 5, {'align': 'left', 'overlap': true});
|
||||
this.parent.empty().append(avatar_group);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -293,6 +293,12 @@ frappe.form.formatters = {
|
|||
return frappe.format(value, link_field, options, row);
|
||||
});
|
||||
return formatted_values.join(', ');
|
||||
},
|
||||
Color: (value) => {
|
||||
return `<div>
|
||||
<div class="selected-color" style="background-color: ${value}"></div>
|
||||
<span class="color-value">${value}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ export default class GridRow {
|
|||
constructor(opts) {
|
||||
this.on_grid_fields_dict = {};
|
||||
this.on_grid_fields = [];
|
||||
$.extend(this, opts);
|
||||
if (this.doc) {
|
||||
this.docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
|
||||
}
|
||||
this.columns = {};
|
||||
this.columns_list = [];
|
||||
$.extend(this, opts);
|
||||
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">';
|
||||
this.make();
|
||||
}
|
||||
|
|
@ -153,7 +156,7 @@ export default class GridRow {
|
|||
this.render_row(true);
|
||||
}
|
||||
|
||||
// refersh form fields
|
||||
// refresh form fields
|
||||
if(this.grid_form) {
|
||||
this.grid_form.layout && this.grid_form.layout.refresh(this.doc);
|
||||
}
|
||||
|
|
@ -249,27 +252,28 @@ export default class GridRow {
|
|||
this.focus_set = false;
|
||||
this.grid.setup_visible_columns();
|
||||
|
||||
for(var ci in this.grid.visible_columns) {
|
||||
var df = this.grid.visible_columns[ci][0],
|
||||
colsize = this.grid.visible_columns[ci][1],
|
||||
txt = this.doc ?
|
||||
frappe.format(this.doc[df.fieldname], df, null, this.doc) :
|
||||
__(df.label);
|
||||
this.grid.visible_columns.forEach((col, ci) => {
|
||||
// to get update df for the row
|
||||
let df = this.docfields.find(field => field.fieldname === col[0].fieldname);
|
||||
let colsize = col[1];
|
||||
let txt = this.doc ?
|
||||
frappe.format(this.doc[df.fieldname], df, null, this.doc) :
|
||||
__(df.label);
|
||||
|
||||
if(this.doc && df.fieldtype === "Select") {
|
||||
if (this.doc && df.fieldtype === "Select") {
|
||||
txt = __(txt);
|
||||
}
|
||||
|
||||
if(!this.columns[df.fieldname]) {
|
||||
var column = this.make_column(df, colsize, txt, ci);
|
||||
let column;
|
||||
if (!this.columns[df.fieldname]) {
|
||||
column = this.make_column(df, colsize, txt, ci);
|
||||
} else {
|
||||
var column = this.columns[df.fieldname];
|
||||
column = this.columns[df.fieldname];
|
||||
this.refresh_field(df.fieldname, txt);
|
||||
}
|
||||
|
||||
// background color for cellz
|
||||
if(this.doc) {
|
||||
if(df.reqd && !txt) {
|
||||
// background color for cell
|
||||
if (this.doc) {
|
||||
if (df.reqd && !txt) {
|
||||
column.addClass('error');
|
||||
}
|
||||
if (column.is_invalid) {
|
||||
|
|
@ -278,7 +282,7 @@ export default class GridRow {
|
|||
column.addClass('bold');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
make_column(df, colsize, txt, ci) {
|
||||
|
|
@ -403,9 +407,9 @@ export default class GridRow {
|
|||
|
||||
if (!field.df.onchange_modified) {
|
||||
var field_on_change_function = field.df.onchange;
|
||||
field.df.onchange = function(e) {
|
||||
field.df.onchange = (e) => {
|
||||
field_on_change_function && field_on_change_function(e);
|
||||
me.grid.grid_rows[this.doc.idx - 1].refresh_field(this.df.fieldname);
|
||||
this.refresh_field(field.df.fieldname);
|
||||
};
|
||||
|
||||
field.df.onchange_modified = true;
|
||||
|
|
@ -589,42 +593,37 @@ export default class GridRow {
|
|||
}
|
||||
}
|
||||
refresh_field(fieldname, txt) {
|
||||
var df = this.grid.get_docfield(fieldname) || undefined;
|
||||
let df = this.docfields.find(col => {
|
||||
return col.fieldname === fieldname;
|
||||
});
|
||||
|
||||
// format values if no frm
|
||||
if(!df) {
|
||||
df = this.grid.visible_columns.find((col) => {
|
||||
return col[0].fieldname === fieldname;
|
||||
});
|
||||
if(df && this.doc) {
|
||||
var txt = frappe.format(this.doc[fieldname], df[0],
|
||||
null, this.doc);
|
||||
}
|
||||
if (df && this.doc) {
|
||||
txt = frappe.format(this.doc[fieldname], df, null, this.doc);
|
||||
}
|
||||
|
||||
if(txt===undefined && this.frm) {
|
||||
var txt = frappe.format(this.doc[fieldname], df,
|
||||
null, this.frm.doc);
|
||||
if (!txt && this.frm) {
|
||||
txt = frappe.format(this.doc[fieldname], df, null, this.frm.doc);
|
||||
}
|
||||
|
||||
// reset static value
|
||||
var column = this.columns[fieldname];
|
||||
if(column) {
|
||||
let column = this.columns[fieldname];
|
||||
if (column) {
|
||||
column.static_area.html(txt || "");
|
||||
if(df && df.reqd) {
|
||||
column.toggleClass('error', !!(txt===null || txt===''));
|
||||
if (df && df.reqd) {
|
||||
column.toggleClass('error', !!(txt === null || txt === ''));
|
||||
}
|
||||
}
|
||||
|
||||
let field = this.on_grid_fields_dict[fieldname];
|
||||
// reset field value
|
||||
var field = this.on_grid_fields_dict[fieldname];
|
||||
if(field) {
|
||||
if (field) {
|
||||
field.docname = this.doc.name;
|
||||
field.refresh();
|
||||
}
|
||||
|
||||
// in form
|
||||
if(this.grid_form) {
|
||||
if (this.grid_form) {
|
||||
this.grid_form.refresh_field(fieldname);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ frappe.ui.form.Layout = Class.extend({
|
|||
fieldobj.doctype = me.doc.doctype;
|
||||
fieldobj.docname = me.doc.name;
|
||||
fieldobj.df = frappe.meta.get_docfield(me.doc.doctype,
|
||||
fieldobj.df.fieldname, me.frm ? me.frm.doc.name : me.doc.name) || fieldobj.df;
|
||||
fieldobj.df.fieldname, me.doc.name) || fieldobj.df;
|
||||
|
||||
// on form change, permissions can change
|
||||
if (me.frm) {
|
||||
|
|
@ -512,7 +512,7 @@ frappe.ui.form.Layout = Class.extend({
|
|||
if (form_obj) {
|
||||
if (this.doc && this.doc.parent) {
|
||||
form_obj.setting_dependency = true;
|
||||
form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname);
|
||||
form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname, this.doc.name);
|
||||
form_obj.setting_dependency = false;
|
||||
// refresh child fields
|
||||
this.fields_dict[fieldname] && this.fields_dict[fieldname].refresh();
|
||||
|
|
|
|||
|
|
@ -251,7 +251,6 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
head ? $row.addClass('list-item--head')
|
||||
: $row = $(`<div class="list-item-container" data-item-name="${result.name}"></div>`).append($row);
|
||||
|
||||
$(".modal-dialog .list-item--head").css("z-index", 0);
|
||||
return $row;
|
||||
}
|
||||
|
||||
|
|
@ -264,6 +263,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
this.empty_list();
|
||||
}
|
||||
more_btn.hide();
|
||||
$(".modal-dialog .list-item--head").css("z-index", 0);
|
||||
|
||||
if (results.length === 0) return;
|
||||
if (more) more_btn.show();
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
|
|||
};
|
||||
|
||||
var check_mandatory = function () {
|
||||
var me = this;
|
||||
var has_errors = false;
|
||||
frm.scroll_set = false;
|
||||
|
||||
|
|
@ -124,8 +123,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
|
|||
|
||||
$.each(frappe.meta.docfield_list[doc.doctype] || [], function (i, docfield) {
|
||||
if (docfield.fieldname) {
|
||||
var df = frappe.meta.get_docfield(doc.doctype,
|
||||
docfield.fieldname, frm.doc.name);
|
||||
const df = frappe.meta.get_docfield(doc.doctype,
|
||||
docfield.fieldname, doc.name);
|
||||
|
||||
if (df.fieldtype === "Fold") {
|
||||
folded = frm.layout.folded;
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ frappe.ui.form.ScriptManager = Class.extend({
|
|||
}
|
||||
|
||||
function setup_add_fetch(df) {
|
||||
if((['Data', 'Read Only', 'Text', 'Small Text', 'Currency',
|
||||
if ((['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check',
|
||||
'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select'].includes(df.fieldtype) || df.read_only==1)
|
||||
&& df.fetch_from && df.fetch_from.indexOf(".")!=-1) {
|
||||
var parts = df.fetch_from.split(".");
|
||||
|
|
|
|||
|
|
@ -210,7 +210,10 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
}
|
||||
|
||||
make_viewers() {
|
||||
if (this.frm.viewers) return;
|
||||
if (this.frm.viewers) {
|
||||
this.frm.viewers.parent.empty();
|
||||
return;
|
||||
}
|
||||
this.frm.viewers = new frappe.ui.form.FormViewers({
|
||||
frm: this.frm,
|
||||
parent: $('<div class="form-viewers d-flex"></div>').prependTo(this.frm.page.page_actions)
|
||||
|
|
|
|||
|
|
@ -179,7 +179,8 @@ frappe.views.BaseList = class BaseList {
|
|||
'Calendar': 'calendar',
|
||||
'Gantt': 'gantt',
|
||||
'Kanban': 'kanban',
|
||||
'Dashboard': 'dashboard'
|
||||
'Dashboard': 'dashboard',
|
||||
'Map': 'map',
|
||||
};
|
||||
|
||||
if (frappe.boot.desk_settings.view_switcher) {
|
||||
|
|
@ -285,6 +286,7 @@ frappe.views.BaseList = class BaseList {
|
|||
}
|
||||
|
||||
setup_filter_area() {
|
||||
if (this.hide_filters) return;
|
||||
this.filter_area = new FilterArea(this);
|
||||
|
||||
if (this.filters && this.filters.length > 0) {
|
||||
|
|
@ -293,6 +295,7 @@ frappe.views.BaseList = class BaseList {
|
|||
}
|
||||
|
||||
setup_sort_selector() {
|
||||
if (this.hide_sort_selector) return;
|
||||
this.sort_selector = new frappe.ui.SortSelector({
|
||||
parent: this.$filter_section,
|
||||
doctype: this.doctype,
|
||||
|
|
@ -410,7 +413,7 @@ frappe.views.BaseList = class BaseList {
|
|||
doctype: this.doctype,
|
||||
fields: this.get_fields(),
|
||||
filters: this.get_filters_for_args(),
|
||||
order_by: this.sort_selector.get_sql_string(),
|
||||
order_by: this.sort_selector && this.sort_selector.get_sql_string(),
|
||||
start: this.start,
|
||||
page_length: this.page_length,
|
||||
view: this.view,
|
||||
|
|
@ -821,6 +824,7 @@ frappe.views.view_modes = [
|
|||
"Image",
|
||||
"Inbox",
|
||||
"Tree",
|
||||
"Map",
|
||||
];
|
||||
frappe.views.is_valid = (view_mode) =>
|
||||
frappe.views.view_modes.includes(view_mode);
|
||||
|
|
|
|||
|
|
@ -417,11 +417,11 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
|
||||
get_no_result_message() {
|
||||
let help_link = this.get_documentation_link();
|
||||
let filters = this.filter_area.get();
|
||||
let no_result_message = filters.length
|
||||
let filters = this.filter_area && this.filter_area.get();
|
||||
let no_result_message = filters && filters.length
|
||||
? __("No {0} found", [__(this.doctype)])
|
||||
: __("You haven't created a {0} yet", [__(this.doctype)]);
|
||||
let new_button_label = filters.length
|
||||
let new_button_label = filters && filters.length
|
||||
? __("Create a new {0}", [__(this.doctype)])
|
||||
: __("Create your first {0}", [__(this.doctype)]);
|
||||
let empty_state_image =
|
||||
|
|
@ -461,7 +461,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
before_refresh() {
|
||||
if (frappe.route_options) {
|
||||
if (frappe.route_options && this.filter_area) {
|
||||
this.filters = this.parse_filters_from_route_options();
|
||||
frappe.route_options = null;
|
||||
|
||||
|
|
@ -527,9 +527,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
this.view_name
|
||||
);
|
||||
this.save_view_user_settings({
|
||||
filters: this.filter_area.get(),
|
||||
sort_by: this.sort_selector.sort_by,
|
||||
sort_order: this.sort_selector.sort_order,
|
||||
filters: this.filter_area && this.filter_area.get(),
|
||||
sort_by: this.sort_selector && this.sort_selector.sort_by,
|
||||
sort_order: this.sort_selector && this.sort_selector.sort_order,
|
||||
});
|
||||
this.toggle_paging && this.$paging_area.toggle(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,14 @@ frappe.views.ListViewSelect = class ListViewSelect {
|
|||
kanbans => this.setup_kanban_switcher(kanbans)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
Map: {
|
||||
condition: this.list_view.settings.get_coords_method ||
|
||||
(this.list_view.meta.fields.find(i => i.fieldname === "latitude") &&
|
||||
this.list_view.meta.fields.find(i => i.fieldname === "longitude")) ||
|
||||
(this.list_view.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype == 'Geolocation')),
|
||||
action: () => this.set_route("map")
|
||||
},
|
||||
};
|
||||
|
||||
frappe.views.view_modes.forEach(view => {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ frappe.call = function(opts) {
|
|||
args.cmd = opts.module+'.page.'+opts.page+'.'+opts.page+'.'+opts.method;
|
||||
} else if(opts.doc) {
|
||||
$.extend(args, {
|
||||
cmd: "runserverobj",
|
||||
cmd: "run_doc_method",
|
||||
docs: frappe.get_doc(opts.doc.doctype, opts.doc.name),
|
||||
method: opts.method,
|
||||
args: opts.args,
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ frappe.router = {
|
|||
// return clean sub_path from hash or url
|
||||
// supports both v1 and v2 routing
|
||||
if (!route) {
|
||||
route = window.location.hash || window.location.pathname;
|
||||
route = window.location.hash || (window.location.pathname + window.location.search);
|
||||
}
|
||||
|
||||
return this.strip_prefix(route);
|
||||
|
|
|
|||
|
|
@ -159,9 +159,12 @@ frappe.socketio = {
|
|||
},
|
||||
doc_open: function(doctype, docname) {
|
||||
// notify that the user has opened this doc, if not already notified
|
||||
if(!frappe.socketio.last_doc
|
||||
|| (frappe.socketio.last_doc[0]!=doctype && frappe.socketio.last_doc[1]!=docname)) {
|
||||
if (!frappe.socketio.last_doc
|
||||
|| (frappe.socketio.last_doc[0] != doctype || frappe.socketio.last_doc[1] != docname)) {
|
||||
frappe.socketio.socket.emit('doc_open', doctype, docname);
|
||||
|
||||
frappe.socketio.last_doc &&
|
||||
frappe.socketio.doc_close(frappe.socketio.last_doc[0], frappe.socketio.last_doc[1]);
|
||||
}
|
||||
frappe.socketio.last_doc = [doctype, docname];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -36,6 +36,18 @@ frappe.ui.FieldSelect = Class.extend({
|
|||
var item = me.awesomplete.get_item(value);
|
||||
me.$input.val(item.label);
|
||||
});
|
||||
this.$input.on("awesomplete-open", () => {
|
||||
let modal = this.$input.parents('.modal-dialog')[0];
|
||||
if (modal) {
|
||||
$(modal).removeClass("modal-dialog-scrollable");
|
||||
}
|
||||
});
|
||||
this.$input.on("awesomplete-close", () => {
|
||||
let modal = this.$input.parents('.modal-dialog')[0];
|
||||
if (modal) {
|
||||
$(modal).addClass("modal-dialog-scrollable");
|
||||
}
|
||||
});
|
||||
|
||||
if(this.filter_fields) {
|
||||
for(var i in this.filter_fields)
|
||||
|
|
|
|||
|
|
@ -495,6 +495,7 @@ frappe.ui.filter_utils = {
|
|||
'Dynamic Link',
|
||||
'Read Only',
|
||||
'Assign',
|
||||
'Color',
|
||||
].indexOf(df.fieldtype) != -1
|
||||
) {
|
||||
df.fieldtype = 'Data';
|
||||
|
|
|
|||
|
|
@ -283,6 +283,7 @@ frappe.ui.FilterGroup = class {
|
|||
}
|
||||
|
||||
get_filter_area_template() {
|
||||
/* eslint-disable indent */
|
||||
return $(`
|
||||
<div class="filter-area">
|
||||
<div class="filter-edit-area">
|
||||
|
|
@ -293,19 +294,23 @@ frappe.ui.FilterGroup = class {
|
|||
<hr class="divider"></hr>
|
||||
<div class="filter-action-buttons">
|
||||
<button class="text-muted add-filter btn btn-xs">
|
||||
${__('+ Add a Filter')}
|
||||
+ ${__('Add a Filter')}
|
||||
</button>
|
||||
<div>
|
||||
<button class="btn btn-secondary btn-xs clear-filters">
|
||||
${__('Clear Filters')}
|
||||
</button>
|
||||
<button class="btn btn-primary btn-xs apply-filters">
|
||||
${__('Apply Filters')}
|
||||
</button>
|
||||
${this.filter_button ?
|
||||
`<button class="btn btn-primary btn-xs apply-filters">
|
||||
${__('Apply Filters')}
|
||||
</button>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
);
|
||||
/* eslint-disable indent */
|
||||
}
|
||||
|
||||
get_filters_as_object() {
|
||||
|
|
|
|||
|
|
@ -286,15 +286,6 @@ frappe.ui.GroupBy = class {
|
|||
|
||||
set_args(args) {
|
||||
if (this.aggregate_function && this.group_by) {
|
||||
let aggregate_column, aggregate_on_field;
|
||||
|
||||
if (this.aggregate_function === 'count') {
|
||||
aggregate_column = 'count(`tab' + this.doctype + '`.`name`)';
|
||||
} else {
|
||||
aggregate_column = `${this.aggregate_function}(${this.aggregate_on})`;
|
||||
aggregate_on_field = this.aggregate_on;
|
||||
}
|
||||
|
||||
this.report_view.group_by = this.group_by;
|
||||
this.report_view.sort_by = '_aggregate_column';
|
||||
this.report_view.sort_order = 'desc';
|
||||
|
|
@ -316,17 +307,14 @@ frappe.ui.GroupBy = class {
|
|||
'_aggregate_column',
|
||||
this.aggregate_on_doctype || this.doctype,
|
||||
]);
|
||||
args.fields.push(aggregate_column + ' as _aggregate_column');
|
||||
|
||||
if (aggregate_on_field) {
|
||||
args.fields.push(aggregate_on_field);
|
||||
}
|
||||
|
||||
// setup columns in datatable
|
||||
this.report_view.setup_columns();
|
||||
|
||||
Object.assign(args, {
|
||||
with_comment_count: false,
|
||||
aggregate_on: this.aggregate_on || 'name',
|
||||
aggregate_function: this.aggregate_function || 'count',
|
||||
group_by: this.report_view.group_by || null,
|
||||
order_by: '_aggregate_column desc',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -316,12 +316,17 @@ frappe.verify_password = function(callback) {
|
|||
}, __("Verify Password"), __("Verify"))
|
||||
}
|
||||
|
||||
frappe.show_progress = function(title, count, total=100, description) {
|
||||
if(frappe.cur_progress && frappe.cur_progress.title === title && frappe.cur_progress.is_visible) {
|
||||
var dialog = frappe.cur_progress;
|
||||
frappe.show_progress = (title, count, total = 100, description, hide_on_completion = false) => {
|
||||
let dialog;
|
||||
if (
|
||||
frappe.cur_progress &&
|
||||
frappe.cur_progress.title === title &&
|
||||
frappe.cur_progress.is_visible
|
||||
) {
|
||||
dialog = frappe.cur_progress;
|
||||
} else {
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: title,
|
||||
dialog = new frappe.ui.Dialog({
|
||||
title: title
|
||||
});
|
||||
dialog.progress = $(`<div>
|
||||
<div class="progress">
|
||||
|
|
@ -329,19 +334,24 @@ frappe.show_progress = function(title, count, total=100, description) {
|
|||
</div>
|
||||
<p class="description text-muted small"></p>
|
||||
</div`).appendTo(dialog.body);
|
||||
dialog.progress_bar = dialog.progress.css({"margin-top": "10px"})
|
||||
.find(".progress-bar");
|
||||
dialog.$wrapper.removeClass("fade");
|
||||
dialog.progress_bar = dialog.progress
|
||||
.css({ 'margin-top': '10px' })
|
||||
.find('.progress-bar');
|
||||
dialog.$wrapper.removeClass('fade');
|
||||
dialog.show();
|
||||
frappe.cur_progress = dialog;
|
||||
}
|
||||
if (description) {
|
||||
dialog.progress.find('.description').text(description);
|
||||
}
|
||||
dialog.percent = cint(flt(count) * 100 / total);
|
||||
dialog.progress_bar.css({"width": dialog.percent + "%" });
|
||||
dialog.percent = cint((flt(count) * 100) / total);
|
||||
dialog.progress_bar.css({ width: dialog.percent + '%' });
|
||||
if (hide_on_completion && dialog.percent === 100) {
|
||||
// timeout to avoid abrupt hide
|
||||
setTimeout(frappe.hide_progress, 500);
|
||||
}
|
||||
return dialog;
|
||||
}
|
||||
};
|
||||
|
||||
frappe.hide_progress = function() {
|
||||
if(frappe.cur_progress) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
setup_page() {
|
||||
this.hide_sidebar = true;
|
||||
this.hide_page_form = true;
|
||||
this.hide_filters = true;
|
||||
this.hide_sort_selector = true;
|
||||
super.setup_page();
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +76,10 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
this.toggle_customization_buttons(false);
|
||||
}
|
||||
|
||||
set_primary_action() {
|
||||
// Don't render Add doc button for dashboard view
|
||||
}
|
||||
|
||||
toggle_customization_buttons(show) {
|
||||
this.save_customizations_button.toggle(show);
|
||||
this.discard_customizations_button.toggle(show);
|
||||
|
|
|
|||
|
|
@ -573,14 +573,17 @@ export default class ChartWidget extends Widget {
|
|||
xIsSeries: this.chart_doc.timeseries,
|
||||
shortenYAxisNumbers: 1
|
||||
},
|
||||
tooltipOptions: {
|
||||
};
|
||||
|
||||
if (this.report_result && this.report_result.chart) {
|
||||
chart_args.tooltipOptions = {
|
||||
formatTooltipY: value =>
|
||||
frappe.format(value, {
|
||||
fieldtype: this.report_result.chart.fieldtype,
|
||||
options: this.report_result.chart.options
|
||||
}, { always_show_decimals: true, inline: true })
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (this.chart_doc.type == "Heatmap") {
|
||||
const heatmap_year = parseInt(this.selected_heatmap_year || this.chart_settings.heatmap_year || this.chart_doc.heatmap_year);
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.frappe-control[data-fieldtype='Color'] {
|
||||
.frappe-control[data-fieldtype='Color'] {
|
||||
input {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
|
@ -104,11 +104,20 @@
|
|||
background-color: red;
|
||||
position: absolute;
|
||||
top: calc(50% + 1px);
|
||||
left: 5px;
|
||||
left: 8px;
|
||||
content: ' ';
|
||||
&.no-value {
|
||||
background: url('/assets/frappe/images/color-circle.png');
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
.like-disabled-input {
|
||||
.color-value {
|
||||
padding-left: 25px;
|
||||
}
|
||||
.selected-color {
|
||||
top: 20%;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
--gray-900: #161a1f;
|
||||
|
||||
// Type Colors
|
||||
--text-muted: var(--gray-300);
|
||||
--text-light: var(--gray-400);
|
||||
--text-muted: var(--gray-400);
|
||||
--text-light: var(--gray-300);
|
||||
--text-color: var(--gray-50);
|
||||
--heading-color: var(--gray-50);
|
||||
|
||||
|
|
@ -114,19 +114,21 @@
|
|||
// --criticism-bg: var(--red-600);
|
||||
|
||||
// Frappe Charts Colors
|
||||
--charts-label-color: var(--gray-300);
|
||||
--charts-axis-line-color: var(--gray-500);
|
||||
.chart-container {
|
||||
--charts-label-color: var(--gray-300);
|
||||
--charts-axis-line-color: var(--gray-500);
|
||||
|
||||
--charts-stroke-width: 5px;
|
||||
--charts-dataset-circle-stroke: #ffffff;
|
||||
--charts-dataset-circle-stroke-width: var(--charts-stroke-width);
|
||||
--charts-stroke-width: 5px;
|
||||
--charts-dataset-circle-stroke: #ffffff;
|
||||
--charts-dataset-circle-stroke-width: var(--charts-stroke-width);
|
||||
|
||||
--charts-tooltip-title: var(--charts-label-color);
|
||||
--charts-tooltip-label: var(--charts-label-color);
|
||||
--charts-tooltip-value: white;
|
||||
--charts-tooltip-bg: var(--gray-900);
|
||||
--charts-tooltip-title: var(--charts-label-color);
|
||||
--charts-tooltip-label: var(--charts-label-color);
|
||||
--charts-tooltip-value: white;
|
||||
--charts-tooltip-bg: var(--gray-900);
|
||||
|
||||
--charts-legend-label: var(--charts-label-color);
|
||||
--charts-legend-label: var(--charts-label-color);
|
||||
}
|
||||
|
||||
// find better fix
|
||||
.heatmap-chart {
|
||||
|
|
|
|||
|
|
@ -123,6 +123,10 @@
|
|||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
||||
.dt-cell__content--header-0, .dt-cell__content--col-0 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.dt-scrollable--highlight-all {
|
||||
.dt-cell__content {
|
||||
background: var(--dt-selection-highlight-color);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
@import "notification";
|
||||
@import "global_search";
|
||||
@import "desktop";
|
||||
@import "awesomebar";
|
||||
@import "../common/awesomeplete";
|
||||
@import "sidebar";
|
||||
@import "filters";
|
||||
@import "list";
|
||||
|
|
|
|||
|
|
@ -261,7 +261,6 @@ input.list-check-all, input.list-row-checkbox {
|
|||
input[type=checkbox] {
|
||||
margin: 0;
|
||||
margin-right: 5px;
|
||||
flex: 0 0 12px;
|
||||
}
|
||||
|
||||
.liked-by, .liked-by-filter-button {
|
||||
|
|
@ -332,10 +331,6 @@ input.list-check-all, input.list-row-checkbox {
|
|||
}
|
||||
|
||||
.page-form {
|
||||
// .awesomplete > ul {
|
||||
// min-width: 300px;
|
||||
// }
|
||||
|
||||
.standard-filter-section {
|
||||
flex-wrap: wrap;
|
||||
// width: 65%;
|
||||
|
|
|
|||
|
|
@ -117,6 +117,10 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.awesomplete > ul {
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-inner-toolbar {
|
||||
|
|
|
|||
|
|
@ -169,25 +169,6 @@ $navbar-height-lg: 4.5rem;
|
|||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $font-size-3xl;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h1 + p {
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $font-size-2xl;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
|
|
@ -202,36 +183,6 @@ $navbar-height-lg: 4.5rem;
|
|||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
table {
|
||||
border-color: $gray-200;
|
||||
}
|
||||
|
||||
table thead {
|
||||
background-color: $light;
|
||||
}
|
||||
|
||||
.table-bordered,
|
||||
.table-bordered th,
|
||||
.table-bordered td {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-color: $gray-200;
|
||||
}
|
||||
|
||||
.table-bordered thead th,
|
||||
.table-bordered thead td {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// next links
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
@import "../common/modal";
|
||||
@import "../common/indicator";
|
||||
@import "../common/controls";
|
||||
@import "../common/awesomeplete";
|
||||
@import 'multilevel_dropdown';
|
||||
@import 'website_image';
|
||||
@import 'website_avatar';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,29 @@
|
|||
$font-sizes-desktop: (
|
||||
"sm": 0.75rem,
|
||||
"base": 1rem,
|
||||
"lg": 1.125rem,
|
||||
"xl": 1.41rem,
|
||||
"2xl": 1.6rem,
|
||||
"3xl": 2rem
|
||||
);
|
||||
|
||||
$font-sizes-mobile: (
|
||||
"sm": 0.75rem,
|
||||
"base": 1rem,
|
||||
"lg": 1.125rem,
|
||||
"xl": 1.25rem,
|
||||
"2xl": 1.5rem,
|
||||
"3xl": 1.75rem
|
||||
);
|
||||
|
||||
.from-markdown {
|
||||
color: $gray-700;
|
||||
line-height: 1.625;
|
||||
line-height: 1.7;
|
||||
letter-spacing: -0.011em;
|
||||
|
||||
> * + * {
|
||||
margin-top: 1rem;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
|
|
@ -16,7 +36,7 @@
|
|||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 2.5rem;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
|
|
@ -27,17 +47,27 @@
|
|||
list-style: decimal;
|
||||
}
|
||||
|
||||
li > * + * {
|
||||
margin-top: 1rem;
|
||||
li {
|
||||
text-indent: 0.25rem;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
> ul > * + *,
|
||||
> ol > * + * {
|
||||
margin-top: 1rem;
|
||||
li > ul, li > ol {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
ul > li:first-child {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
ul > * + *,
|
||||
ol > * + * {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
> blockquote {
|
||||
padding: 1.25rem 1rem;
|
||||
padding: 0.75rem 1rem 0.75rem 1.25rem;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: 500;
|
||||
border: 1px solid $gray-200;
|
||||
|
|
@ -55,60 +85,87 @@
|
|||
|
||||
b, strong {
|
||||
color: $gray-800;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: $gray-900;
|
||||
}
|
||||
|
||||
h1 + p {
|
||||
margin-top: 0.75rem;
|
||||
font-size: $font-size-base;
|
||||
h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: map-get($font-sizes-mobile, '3xl');
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.021em;
|
||||
font-weight: 700;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-top: 1.25rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: 1.25rem;
|
||||
font-size: map-get($font-sizes-desktop, '3xl');
|
||||
letter-spacing: -0.024em;
|
||||
}
|
||||
|
||||
// for byline
|
||||
& + p {
|
||||
margin-top: 1.5rem;
|
||||
font-size: map-get($font-sizes-mobile, 'xl');
|
||||
letter-spacing: -0.014em;
|
||||
line-height: 1.4;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: map-get($font-sizes-desktop, 'xl');
|
||||
letter-spacing: -0.0175em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 3.5rem;
|
||||
font-size: map-get($font-sizes-mobile, '2xl');
|
||||
line-height: 1.56;
|
||||
letter-spacing: -0.015em;
|
||||
margin-top: 4rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: map-get($font-sizes-desktop, '2xl');
|
||||
letter-spacing: -0.0195em;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
font-size: $font-size-xl;
|
||||
font-size: map-get($font-sizes-mobile, 'xl');
|
||||
line-height: 1.56;
|
||||
letter-spacing: -0.014em;
|
||||
margin-top: 2.25rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: map-get($font-sizes-desktop, 'xl');
|
||||
letter-spacing: -0.0175em;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: map-get($font-sizes-mobile, 'lg');
|
||||
line-height: 1.56;
|
||||
letter-spacing: -0.014em;
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: $font-size-base;
|
||||
font-size: map-get($font-sizes-mobile, 'base');
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.011em;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: $font-size-sm;
|
||||
font-size: map-get($font-sizes-mobile, 'sm');
|
||||
line-height: 1.35;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
text-transform: uppercase;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
tr > td,
|
||||
|
|
@ -124,6 +181,7 @@
|
|||
.screenshot {
|
||||
border: 1px solid $gray-400;
|
||||
border-radius: 0.375rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.screenshot + em {
|
||||
|
|
@ -138,4 +196,25 @@
|
|||
background: $light;
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
table {
|
||||
border-color: $gray-200;
|
||||
}
|
||||
|
||||
table thead {
|
||||
background-color: $light;
|
||||
}
|
||||
|
||||
.table-bordered,
|
||||
.table-bordered th,
|
||||
.table-bordered td {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-color: $gray-200;
|
||||
}
|
||||
|
||||
.table-bordered thead th,
|
||||
.table-bordered thead td {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -296,8 +296,7 @@ class Session:
|
|||
expiry = get_expiry_in_seconds(session_data.get("session_expiry"))
|
||||
|
||||
if self.time_diff > expiry:
|
||||
print('deleting...')
|
||||
self.delete_session()
|
||||
self._delete_session()
|
||||
data = None
|
||||
|
||||
return data and data.data
|
||||
|
|
@ -316,12 +315,12 @@ class Session:
|
|||
data = frappe._dict(eval(rec and rec[0][1] or '{}'))
|
||||
data.user = rec[0][0]
|
||||
else:
|
||||
self.delete_session()
|
||||
self._delete_session()
|
||||
data = None
|
||||
|
||||
return data
|
||||
|
||||
def delete_session(self):
|
||||
def _delete_session(self):
|
||||
delete_session(self.sid, reason="Session Expired")
|
||||
|
||||
def start_as_guest(self):
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class EnergyPointLog(Document):
|
|||
reference_log.reverted = 0
|
||||
reference_log.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def revert(self, reason, ignore_permissions=False):
|
||||
if not ignore_permissions:
|
||||
frappe.only_for('System Manager')
|
||||
|
|
|
|||
|
|
@ -23,21 +23,6 @@
|
|||
<div class="col-12 col-lg-8">
|
||||
<div class="doc-search-container">
|
||||
<div class="website-search doc-search" id="search-container">
|
||||
<div class="dropdown">
|
||||
<div class="search-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="feather feather-search">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<input type="search" class="form-control" placeholder="Search the docs (Press / to focus)" />
|
||||
<div class="overflow-hidden shadow dropdown-menu w-100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="navbar-toggler" type="button"
|
||||
data-toggle="collapse"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import frappe
|
||||
|
||||
def update_system_settings(args):
|
||||
doc = frappe.get_doc('System Settings')
|
||||
doc.update(args)
|
||||
doc.save()
|
||||
|
||||
def get_system_setting(key):
|
||||
return frappe.db.get_single_value("System Settings", key)
|
||||
|
|
@ -143,8 +143,7 @@ class TestAPI(unittest.TestCase):
|
|||
self.assertFalse(frappe.db.get_value('Note', {'title': 'delete'}))
|
||||
|
||||
def test_auth_via_api_key_secret(self):
|
||||
|
||||
# generate api ke and api secret for administrator
|
||||
# generate API key and API secret for administrator
|
||||
keys = generate_keys("Administrator")
|
||||
frappe.db.commit()
|
||||
generated_secret = frappe.utils.password.get_decrypted_password(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest, frappe
|
||||
import unittest
|
||||
import frappe
|
||||
|
||||
|
||||
class TestClient(unittest.TestCase):
|
||||
def test_set_value(self):
|
||||
|
|
@ -55,3 +57,49 @@ class TestClient(unittest.TestCase):
|
|||
})
|
||||
|
||||
self.assertRaises(frappe.PermissionError, execute_cmd, 'frappe.client.save')
|
||||
|
||||
def test_run_doc_method(self):
|
||||
from frappe.handler import execute_cmd
|
||||
|
||||
if not frappe.db.exists('Report', 'Test Run Doc Method'):
|
||||
report = frappe.get_doc({
|
||||
'doctype': 'Report',
|
||||
'ref_doctype': 'User',
|
||||
'report_name': 'Test Run Doc Method',
|
||||
'report_type': 'Query Report',
|
||||
'is_standard': 'No',
|
||||
'roles': [
|
||||
{'role': 'System Manager'}
|
||||
]
|
||||
}).insert()
|
||||
else:
|
||||
report = frappe.get_doc('Report', 'Test Run Doc Method')
|
||||
|
||||
frappe.local.request = frappe._dict()
|
||||
frappe.local.request.method = 'GET'
|
||||
|
||||
# Whitelisted, works as expected
|
||||
frappe.local.form_dict = frappe._dict({
|
||||
'dt': report.doctype,
|
||||
'dn': report.name,
|
||||
'method': 'toggle_disable',
|
||||
'cmd': 'run_doc_method',
|
||||
'args': 0
|
||||
})
|
||||
|
||||
execute_cmd(frappe.local.form_dict.cmd)
|
||||
|
||||
# Not whitelisted, throws permission error
|
||||
frappe.local.form_dict = frappe._dict({
|
||||
'dt': report.doctype,
|
||||
'dn': report.name,
|
||||
'method': 'create_report_py',
|
||||
'cmd': 'run_doc_method',
|
||||
'args': 0
|
||||
})
|
||||
|
||||
self.assertRaises(
|
||||
frappe.PermissionError,
|
||||
execute_cmd,
|
||||
frappe.local.form_dict.cmd
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ from frappe.utils.testutils import add_custom_field, clear_custom_fields
|
|||
test_dependencies = ['User', 'Blog Post', 'Blog Category', 'Blogger']
|
||||
|
||||
class TestReportview(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_basic(self):
|
||||
self.assertTrue({"name":"DocType"} in DatabaseQuery("DocType").execute(limit_page_length=None))
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,17 @@ class TestPassword(unittest.TestCase):
|
|||
|
||||
self.assertTrue(not get_password_list(doc))
|
||||
|
||||
def test_password_unset(self):
|
||||
doc = self.make_email_account()
|
||||
|
||||
doc.password = 'asdf'
|
||||
doc.save()
|
||||
self.assertEqual(doc.get_password(raise_exception=False), 'asdf')
|
||||
|
||||
doc.password = ''
|
||||
doc.save()
|
||||
self.assertEqual(doc.get_password(raise_exception=False), None)
|
||||
|
||||
|
||||
def get_password_list(doc):
|
||||
return frappe.db.sql("""SELECT `password`
|
||||
|
|
|
|||
|
|
@ -6,23 +6,34 @@ import unittest, frappe, pyotp
|
|||
from frappe.auth import HTTPRequest
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import set_request
|
||||
from frappe.auth import validate_ip_address
|
||||
from frappe.auth import validate_ip_address, get_login_attempt_tracker
|
||||
from frappe.twofactor import (should_run_2fa, authenticate_for_2factor, get_cached_user_pass,
|
||||
two_factor_is_enabled_for_, confirm_otp_token, get_otpsecret_for_, get_verification_obj)
|
||||
from . import update_system_settings, get_system_setting
|
||||
|
||||
import time
|
||||
|
||||
class TestTwoFactor(unittest.TestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestTwoFactor, self).__init__(*args, **kwargs)
|
||||
self.default_allowed_login_attempts = get_system_setting('allow_consecutive_login_attempts')
|
||||
|
||||
def setUp(self):
|
||||
self.http_requests = create_http_request()
|
||||
self.login_manager = frappe.local.login_manager
|
||||
self.user = self.login_manager.user
|
||||
update_system_settings({
|
||||
'allow_consecutive_login_attempts': 2
|
||||
})
|
||||
|
||||
def tearDown(self):
|
||||
frappe.local.response['verification'] = None
|
||||
frappe.local.response['tmp_id'] = None
|
||||
disable_2fa()
|
||||
frappe.clear_cache(user=self.user)
|
||||
update_system_settings({
|
||||
'allow_consecutive_login_attempts': self.default_allowed_login_attempts
|
||||
})
|
||||
|
||||
def test_should_run_2fa(self):
|
||||
'''Should return true if enabled.'''
|
||||
|
|
@ -153,6 +164,33 @@ class TestTwoFactor(unittest.TestCase):
|
|||
enable_2fa()
|
||||
self.assertIsNone(validate_ip_address(self.user))
|
||||
|
||||
def test_otp_attempt_tracker(self):
|
||||
"""Check that OTP login attempts are tracked.
|
||||
"""
|
||||
authenticate_for_2factor(self.user)
|
||||
tmp_id = frappe.local.response['tmp_id']
|
||||
otp = 'wrongotp'
|
||||
with self.assertRaises(frappe.AuthenticationError):
|
||||
confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id)
|
||||
|
||||
with self.assertRaises(frappe.AuthenticationError):
|
||||
confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id)
|
||||
|
||||
# REMOVE ME: current logic allows allow_consecutive_login_attempts+1 attempts
|
||||
# before raising security exception, remove below line when that is fixed.
|
||||
with self.assertRaises(frappe.AuthenticationError):
|
||||
confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id)
|
||||
|
||||
with self.assertRaises(frappe.SecurityException):
|
||||
confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id)
|
||||
|
||||
# Remove tracking cache so that user can try loging in again
|
||||
tracker = get_login_attempt_tracker(self.user, raise_locked_exception=False)
|
||||
tracker.add_success_attempt()
|
||||
|
||||
otp = get_otp(self.user)
|
||||
self.assertTrue(confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id))
|
||||
|
||||
def create_http_request():
|
||||
'''Get http request object.'''
|
||||
set_request(method='POST', path='login')
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ def get_verification_method():
|
|||
|
||||
def confirm_otp_token(login_manager, otp=None, tmp_id=None):
|
||||
'''Confirm otp matches.'''
|
||||
from frappe.auth import get_login_attempt_tracker
|
||||
if not otp:
|
||||
otp = frappe.form_dict.get('otp')
|
||||
if not otp:
|
||||
|
|
@ -130,12 +131,17 @@ def confirm_otp_token(login_manager, otp=None, tmp_id=None):
|
|||
otp_secret = frappe.cache().get(tmp_id + '_otp_secret')
|
||||
if not otp_secret:
|
||||
raise ExpiredLoginException(_('Login session expired, refresh page to retry'))
|
||||
|
||||
tracker = get_login_attempt_tracker(login_manager.user)
|
||||
|
||||
hotp = pyotp.HOTP(otp_secret)
|
||||
if hotp_token:
|
||||
if hotp.verify(otp, int(hotp_token)):
|
||||
frappe.cache().delete(tmp_id + '_token')
|
||||
tracker.add_success_attempt()
|
||||
return True
|
||||
else:
|
||||
tracker.add_failure_attempt()
|
||||
login_manager.fail(_('Incorrect Verification code'), login_manager.user)
|
||||
|
||||
totp = pyotp.TOTP(otp_secret)
|
||||
|
|
@ -144,8 +150,10 @@ def confirm_otp_token(login_manager, otp=None, tmp_id=None):
|
|||
if not frappe.db.get_default(login_manager.user + '_otplogin'):
|
||||
frappe.db.set_default(login_manager.user + '_otplogin', 1)
|
||||
delete_qrimage(login_manager.user)
|
||||
tracker.add_success_attempt()
|
||||
return True
|
||||
else:
|
||||
tracker.add_failure_attempt()
|
||||
login_manager.fail(_('Incorrect Verification code'), login_manager.user)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import socket
|
|||
from six.moves.urllib.parse import urlparse
|
||||
from frappe import get_conf
|
||||
|
||||
config = get_conf()
|
||||
REDIS_KEYS = ('redis_cache', 'redis_queue', 'redis_socketio')
|
||||
|
||||
|
||||
|
|
@ -21,13 +20,15 @@ def is_open(ip, port, timeout=10):
|
|||
|
||||
|
||||
def check_database():
|
||||
config = get_conf()
|
||||
db_type = config.get("db_type", "mariadb")
|
||||
db_host = config.get("db_host", "localhost")
|
||||
db_port = config.get("db_port", 3306 if db_type == "mariadb" else 5342)
|
||||
db_port = config.get("db_port", 3306 if db_type == "mariadb" else 5432)
|
||||
return {db_type: is_open(db_host, db_port)}
|
||||
|
||||
|
||||
def check_redis(redis_services=None):
|
||||
config = get_conf()
|
||||
services = redis_services or REDIS_KEYS
|
||||
status = {}
|
||||
for conn in services:
|
||||
|
|
|
|||
|
|
@ -65,7 +65,14 @@ def set_encrypted_password(doctype, name, pwd, fieldname='password'):
|
|||
raise e
|
||||
|
||||
|
||||
def check_password(user, pwd, doctype='User', fieldname='password'):
|
||||
def remove_encrypted_password(doctype, name, fieldname='password'):
|
||||
frappe.db.sql(
|
||||
'DELETE FROM `__Auth` WHERE doctype = %s and name = %s and fieldname = %s',
|
||||
values=[doctype, name, fieldname]
|
||||
)
|
||||
|
||||
|
||||
def check_password(user, pwd, doctype='User', fieldname='password', delete_tracker_cache=True):
|
||||
'''Checks if user and password are correct, else raises frappe.AuthenticationError'''
|
||||
|
||||
auth = frappe.db.sql("""select `name`, `password` from `__Auth`
|
||||
|
|
@ -77,7 +84,11 @@ def check_password(user, pwd, doctype='User', fieldname='password'):
|
|||
|
||||
# lettercase agnostic
|
||||
user = auth[0].name
|
||||
delete_login_failed_cache(user)
|
||||
|
||||
# TODO: This need to be deleted after checking side effects of it.
|
||||
# We have a `LoginAttemptTracker` that can take care of tracking related cache.
|
||||
if delete_tracker_cache:
|
||||
delete_login_failed_cache(user)
|
||||
|
||||
if not passlibctx.needs_update(auth[0].password):
|
||||
update_password(user, pwd, doctype, fieldname)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class BlogPost(WebsiteGenerator):
|
|||
order_by = "published_on desc"
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_route(self):
|
||||
if not self.route:
|
||||
return frappe.db.get_value('Blog Category', self.blog_category,
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ class PersonalDataDeletionRequest(Document):
|
|||
if self.status != "Pending Approval":
|
||||
frappe.throw(_("This request has not yet been approved by the user."))
|
||||
|
||||
@frappe.whitelist()
|
||||
def trigger_data_deletion(self):
|
||||
"""Redact user data defined in current site's hooks under `user_data_fields`"""
|
||||
self.validate_data_anonymization()
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue