Merge branch 'develop' into todo-access-fix

This commit is contained in:
Suraj Shetty 2021-04-02 13:03:10 +05:30 committed by GitHub
commit 1bfd5c8a87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
108 changed files with 1462 additions and 1064 deletions

View file

@ -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');
});
});

View file

@ -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.

View file

@ -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.

View file

@ -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'])

View file

@ -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

View file

@ -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({

View file

@ -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:

View file

@ -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
);
}
}
});

View file

@ -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

View file

@ -7,4 +7,4 @@ from __future__ import unicode_literals
{base_class_import}
class {classname}({base_class}):
pass
{custom_controller}

View file

@ -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);
}

View file

@ -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",

View file

@ -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'

View file

@ -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({

View file

@ -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:

View file

@ -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'

View file

@ -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))

View file

@ -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()

View file

@ -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
}
}

View file

View 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) {
// }
});

View 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
}

View 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)]

View 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

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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.

View file

@ -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:

View file

@ -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

View file

@ -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),

View file

@ -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(' ','')

View file

@ -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:

View file

@ -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)

View file

@ -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():

View file

@ -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:

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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 = {}

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)
))

View file

@ -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()

View file

@ -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",

View file

@ -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

View file

@ -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={}) {

View file

@ -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) {

View file

@ -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();
}

View file

@ -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 = {};
}

View file

@ -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);
});

View file

@ -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 => {

View file

@ -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 };

View file

@ -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);
}
}

View file

@ -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);
}
};

View file

@ -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>`;
}
}

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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();

View file

@ -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;

View file

@ -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(".");

View file

@ -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)

View file

@ -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);

View file

@ -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);
}

View file

@ -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 => {

View file

@ -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,

View file

@ -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);

View file

@ -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];
},

View file

@ -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)

View file

@ -495,6 +495,7 @@ frappe.ui.filter_utils = {
'Dynamic Link',
'Read Only',
'Assign',
'Color',
].indexOf(df.fieldtype) != -1
) {
df.fieldtype = 'Data';

View file

@ -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() {

View file

@ -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',
});

View file

@ -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) {

View file

@ -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);

View file

@ -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);

View file

@ -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;
}
}
}

View file

@ -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 {

View file

@ -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);

View file

@ -23,7 +23,7 @@
@import "notification";
@import "global_search";
@import "desktop";
@import "awesomebar";
@import "../common/awesomeplete";
@import "sidebar";
@import "filters";
@import "list";

View file

@ -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%;

View file

@ -117,6 +117,10 @@
display: none;
}
}
.awesomplete > ul {
min-width: 300px;
}
}
.form-inner-toolbar {

View file

@ -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

View file

@ -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';

View file

@ -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;
}
}

View file

@ -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):

View file

@ -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')

View file

@ -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"

View file

@ -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)

View file

@ -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(

View file

@ -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
)

View file

@ -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))

View file

@ -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`

View file

@ -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')

View file

@ -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)

View file

@ -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:

View file

@ -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)

View file

@ -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,

View file

@ -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