Merge branch 'develop' into multiple_imap_folder

This commit is contained in:
mtraeber 2021-11-03 08:55:29 +01:00
commit 8a7889b928
48 changed files with 317 additions and 157 deletions

View file

@ -2,32 +2,58 @@ context('Query Report', () => {
before(() => {
cy.login();
cy.visit('/app/website');
cy.insert_doc('Report', {
'report_name': 'Test ToDo Report',
'ref_doctype': 'ToDo',
'report_type': 'Query Report',
'query': 'select * from tabToDo'
}, true).as('doc');
});
it('add custom column in report', () => {
cy.visit('/app/query-report/Permitted Documents For User');
cy.get('.page-form.flex', { timeout: 60000 }).should('have.length', 1).then(() => {
cy.get('#page-query-report input[data-fieldname="user"]').as('input');
cy.get('@input').focus().type('test@erpnext.com', { delay: 100 }).blur();
cy.get('#page-query-report input[data-fieldname="user"]').as('input-user');
cy.get('@input-user').focus().type('test@erpnext.com', { delay: 100 }).blur();
cy.wait(300);
cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-test');
cy.get('@input-test').focus().type('Role', { delay: 100 }).blur();
cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-role');
cy.get('@input-role').focus().type('Role', { delay: 100 }).blur();
cy.get('.datatable').should('exist');
cy.get('.menu-btn-group button').click({ force: true });
cy.get('.dropdown-menu li').contains('Add Column').click({ force: true });
cy.get('.modal-dialog').should('contain', 'Add Column');
cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true });
cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Add Column').click({ force: true });
cy.get_open_dialog().get('.modal-title').should('contain', 'Add Column');
cy.get('select[data-fieldname="doctype"]').select("Role", { force: true });
cy.get('select[data-fieldname="field"]').select("Role Name", { force: true });
cy.get('select[data-fieldname="insert_after"]').select("Name", { force: true });
cy.get('button').contains('Submit').click({ force: true });
cy.get('.menu-btn-group button').click({ force: true });
cy.get('.dropdown-menu li').contains('Save').click({ force: true });
cy.get('.modal-dialog').should('contain', 'Save Report');
cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ force: true });
cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true });
cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Save').click({ timeout: 100, force: true });
cy.get_open_dialog().get('.modal-title').should('contain', 'Save Report');
cy.get('input[data-fieldname="report_name"]').type("Test Report", { delay: 100, force: true });
cy.get('button').contains('Submit').click({ timeout: 1000, force: true });
cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ timeout: 1000, force: true });
});
});
let save_report_and_open = (report, update_name) => {
cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true });
cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Save').click({ timeout: 100, force: true });
cy.get_open_dialog().get('.modal-title').should('contain', 'Save Report');
cy.get('input[data-fieldname="report_name"]').type(update_name, { delay: 100, force: true });
cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ timeout: 1000, force: true });
cy.visit('/app/query-report/'+report);
cy.get('.datatable').should('exist');
};
it('test multi level query report', () => {
cy.visit('/app/query-report/Test ToDo Report');
cy.get('.datatable').should('exist');
save_report_and_open('Test ToDo Report 1', ' 1');
save_report_and_open('Test ToDo Report 11', '1');
});
});

View file

@ -17,7 +17,7 @@ context('Sidebar', () => {
cy.get('.group-by-item > .dropdown-item').should('contain', 'Me');
//Assigning a doctype to a user
cy.click_listview_row_item(0);
cy.visit('/app/doctype/ToDo');
cy.get('.form-assignments > .flex > .text-muted').click();
cy.get_field('assign_to_me', 'Check').click();
cy.get('.modal-footer > .standard-actions > .btn-primary').click();
@ -44,8 +44,7 @@ context('Sidebar', () => {
cy.clear_filters();
//To remove the assignment
cy.visit('/app/doctype');
cy.click_listview_row_item(0);
cy.visit('/app/doctype/ToDo');
cy.get('.assignments > .avatar-group > .avatar > .avatar-frame').click();
cy.get('.remove-btn').click({force: true});
cy.hide_dialog();
@ -53,4 +52,4 @@ context('Sidebar', () => {
cy.click_sidebar_button("Assigned To");
cy.get('.empty-state').should('contain', 'No filters found');
});
});
});

View file

@ -487,11 +487,11 @@ def get_request_header(key, default=None):
:param default: Default value."""
return request.headers.get(key, default)
def sendmail(recipients=[], sender="", subject="No Subject", message="No Message",
def sendmail(recipients=None, sender="", subject="No Subject", message="No Message",
as_markdown=False, delayed=True, reference_doctype=None, reference_name=None,
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, add_unsubscribe_link=1,
attachments=None, content=None, doctype=None, name=None, reply_to=None, queue_separately=False,
cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None,
cc=None, bcc=None, message_id=None, in_reply_to=None, send_after=None, expose_recipients=None,
send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False,
inline_images=None, template=None, args=None, header=None, print_letterhead=False, with_container=False):
"""Send email using user's default **Email Account** or global default **Email Account**.
@ -521,6 +521,14 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
:param header: Append header in email
:param with_container: Wraps email inside a styled container
"""
if recipients is None:
recipients = []
if cc is None:
cc = []
if bcc is None:
bcc = []
text_content = None
if template:
message, text_content = get_email_from_template(template, args)
@ -718,18 +726,20 @@ def only_has_select_perm(doctype, user=None, ignore_permissions=False):
else:
return False
def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False):
def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False, parent_doctype=None):
"""Raises `frappe.PermissionError` if not permitted.
:param doctype: DocType for which permission is to be check.
:param ptype: Permission type (`read`, `write`, `create`, `submit`, `cancel`, `amend`). Default: `read`.
:param doc: [optional] Checks User permissions for given doc.
:param user: [optional] Check for given user. Default: current user."""
:param user: [optional] Check for given user. Default: current user.
:param parent_doctype: Required when checking permission for a child DocType (unless doc is specified)."""
if not doctype and doc:
doctype = doc.doctype
import frappe.permissions
out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user, raise_exception=throw)
out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user,
raise_exception=throw, parent_doctype=parent_doctype)
if throw and not out:
if doc:
frappe.throw(_("No permission for {0}").format(doc.doctype + " " + doc.name))

View file

@ -659,10 +659,14 @@ def publish_realtime(context, event, message, room, user, doctype, docname, afte
@click.command('browse')
@click.argument('site', required=False)
@click.option('--user', required=False, help='Login as user')
@pass_context
def browse(context, site):
def browse(context, site, user=None):
'''Opens the site on web browser'''
from frappe.auth import LoginManager
from frappe.auth import CookieManager
import webbrowser
site = context.sites[0] if context.sites else site
if not site:
@ -672,7 +676,24 @@ def browse(context, site):
site = site.lower()
if site in frappe.utils.get_sites():
webbrowser.open(frappe.utils.get_site_url(site), new=2)
frappe.init(site=site)
frappe.connect()
sid = ''
if user:
if frappe.conf.developer_mode or user == "Administrator":
frappe.utils.set_request(path="/")
frappe.local.cookie_manager = CookieManager()
frappe.local.login_manager = LoginManager()
frappe.local.login_manager.login_as(user)
sid = f'/app?sid={frappe.session.sid}'
else:
print("Please enable developer mode to login as a user")
url = f'{frappe.utils.get_site_url(site)}{sid}'
if user == "Administrator":
print(f'Login URL: {url}')
webbrowser.open(url, new=2)
else:
click.echo("\nSite named \033[1m{}\033[0m doesn't exist\n".format(site))

View file

@ -16,7 +16,7 @@ def load_address_and_contact(doc, key=None):
["Dynamic Link", "link_name", "=", doc.name],
["Dynamic Link", "parenttype", "=", "Address"],
]
address_list = frappe.get_all("Address", filters=filters, fields=["*"])
address_list = frappe.get_list("Address", filters=filters, fields=["*"])
address_list = [a.update({"display": get_address_display(a)})
for a in address_list]
@ -34,16 +34,16 @@ def load_address_and_contact(doc, key=None):
["Dynamic Link", "link_name", "=", doc.name],
["Dynamic Link", "parenttype", "=", "Contact"],
]
contact_list = frappe.get_all("Contact", filters=filters, fields=["*"])
contact_list = frappe.get_list("Contact", filters=filters, fields=["*"])
for contact in contact_list:
contact["email_ids"] = frappe.get_list("Contact Email", filters={
contact["email_ids"] = frappe.get_all("Contact Email", filters={
"parenttype": "Contact",
"parent": contact.name,
"is_primary": 0
}, fields=["email_id"])
contact["phone_nos"] = frappe.get_list("Contact Phone", filters={
contact["phone_nos"] = frappe.get_all("Contact Phone", filters={
"parenttype": "Contact",
"parent": contact.name,
"is_primary_phone": 0,

View file

@ -262,7 +262,7 @@ def get_contact_with_phone_number(number):
return contacts[0].parent if contacts else None
def get_contact_name(email_id):
contact = frappe.get_list("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1)
contact = frappe.get_all("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1)
return contact[0].parent if contact else None
def get_contacts_linking_to(doctype, docname, fields=None):

View file

@ -154,7 +154,7 @@
"icon": "fa fa-comment",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-28 11:43:57.504565",
"modified": "2021-10-25 11:43:57.504565",
"modified_by": "Administrator",
"module": "Core",
"name": "Activity Log",
@ -182,6 +182,5 @@
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "subject",
"track_changes": 1,
"track_seen": 1
}

View file

@ -407,7 +407,7 @@ def get_contacts(email_strings, auto_create_contact=False):
return contacts
def add_contact_links_to_communication(communication, contact_name):
contact_links = frappe.get_list("Dynamic Link", filters={
contact_links = frappe.get_all("Dynamic Link", filters={
"parenttype": "Contact",
"parent": contact_name
}, fields=["link_doctype", "link_name"])

View file

@ -763,7 +763,9 @@ class Column:
seen = []
fields_column_map = {}
def __init__(self, index, header, doctype, column_values, map_to_field=None, seen=[]):
def __init__(self, index, header, doctype, column_values, map_to_field=None, seen=None):
if seen is None:
seen = []
self.index = index
self.column_number = index + 1
self.doctype = doctype

View file

@ -150,7 +150,7 @@
"fieldtype": "Column Break"
},
{
"default": "1",
"default": "0",
"depends_on": "eval:!doc.istable",
"description": "If enabled, changes to the document are tracked and shown in timeline",
"fieldname": "track_changes",
@ -649,7 +649,7 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2021-09-05 15:39:13.233403",
"modified": "2021-10-29 11:39:13.233403",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -112,7 +112,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-03-14 12:21:44.292471",
"modified": "2021-10-25 12:21:44.292471",
"modified_by": "Administrator",
"module": "Core",
"name": "Error Log",
@ -144,6 +144,5 @@
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
"track_changes": 1,
"track_seen": 0
}
}

View file

@ -359,7 +359,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-12-29 14:40:38.619106",
"modified": "2021-10-25 14:40:38.619106",
"modified_by": "Administrator",
"module": "Core",
"name": "Error Snapshot",
@ -394,6 +394,5 @@
"sort_field": "timestamp",
"sort_order": "DESC",
"title_field": "evalue",
"track_changes": 1,
"track_seen": 0
}
}

View file

@ -38,7 +38,7 @@
}
],
"links": [],
"modified": "2020-01-22 00:00:00.000000",
"modified": "2021-10-25 00:00:00.000000",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Log",
@ -59,6 +59,5 @@
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
"sort_order": "DESC"
}

View file

@ -69,16 +69,6 @@ frappe.method_that_doesnt_exist("do some magic")
disabled = 1,
script = '''
frappe.db.commit()
'''
),
dict(
name='test_cache_methods',
script_type = 'DocType Event',
doctype_event = 'Before Save',
reference_doctype = 'ToDo',
disabled = 1,
script = '''
frappe.cache().set_value('test_key', doc.name)
'''
)
]
@ -149,14 +139,3 @@ class TestServerScript(unittest.TestCase):
server_script.disabled = 1
server_script.save()
def test_cache_methods_in_server_script(self):
server_script = frappe.get_doc('Server Script', 'test_cache_methods')
server_script.disabled = 0
server_script.save()
todo = frappe.get_doc(dict(doctype='ToDo', description='test me')).insert()
self.assertEqual(todo.name, frappe.cache().get_value('test_key'))
server_script.disabled = 1
server_script.save()

View file

@ -125,7 +125,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-09-05 14:22:27.664645",
"modified": "2021-10-25 14:22:27.664645",
"modified_by": "Administrator",
"module": "Core",
"name": "View Log",
@ -158,7 +158,6 @@
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}
}

View file

@ -120,7 +120,7 @@
"hide_toolbar": 1,
"in_create": 1,
"links": [],
"modified": "2020-09-18 17:26:09.703215",
"modified": "2021-10-25 17:26:09.703215",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Log",
@ -139,6 +139,5 @@
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "subject",
"track_changes": 1,
"track_seen": 1
}
}

View file

@ -88,7 +88,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-10-05 13:26:03.106050",
"modified": "2021-10-25 13:26:03.106050",
"modified_by": "Administrator",
"module": "Desk",
"name": "Route History",
@ -121,7 +121,6 @@
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}
}

View file

@ -77,7 +77,7 @@ def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
@frappe.whitelist()
def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=None):
"""
Cancel all linked doctype, optionally ignore doctypes specified in a list.
@ -85,6 +85,8 @@ def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
docs (json str) - It contains list of dictionaries of a linked documents.
ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling.
"""
if ignore_doctypes_on_cancel_all is None:
ignore_doctypes_on_cancel_all = []
docs = json.loads(docs)
if isinstance(ignore_doctypes_on_cancel_all, str):
@ -96,7 +98,7 @@ def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
frappe.publish_progress(percent=i/len(docs) * 100, title=_("Cancelling documents"))
def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]):
def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=None):
"""
Validate a document to be submitted and non-exempted from auto-cancel.
@ -109,7 +111,7 @@ def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]):
"""
#ignore doctype to cancel
if docinfo.get("doctype") in ignore_doctypes_on_cancel_all:
if docinfo.get("doctype") in (ignore_doctypes_on_cancel_all or []):
return False
# skip non-submittable doctypes since they don't need to be cancelled

View file

@ -40,6 +40,10 @@ def get_preview_data(doctype, docname):
for key, val in preview_data.items():
if val and meta.has_field(key) and key not in [image_field, title_field, 'name']:
formatted_preview_data[meta.get_field(key).label] = frappe.format(val, meta.get_field(key).fieldtype)
formatted_preview_data[meta.get_field(key).label] = frappe.format(
val,
meta.get_field(key).fieldtype,
translated=True,
)
return formatted_preview_data

View file

@ -216,7 +216,7 @@ def get_filters_for(doctype):
@frappe.whitelist()
@frappe.read_only()
def get_open_count(doctype, name, items=[]):
def get_open_count(doctype, name, items=None):
'''Get open count for given transactions and filters
:param doctype: Reference DocType
@ -235,7 +235,8 @@ def get_open_count(doctype, name, items=[]):
links = meta.get_dashboard_data()
# compile all items in a list
if not items:
if items is None:
items = []
for group in links.transactions:
items.extend(group.get("items"))

View file

@ -59,6 +59,19 @@ def get_report_doc(report_name):
return doc
def get_report_result(report, filters):
if report.report_type == "Query Report":
res = report.execute_query_report(filters)
elif report.report_type == "Script Report":
res = report.execute_script_report(filters)
elif report.report_type == "Custom Report":
ref_report = get_report_doc(report.report_name)
res = get_report_result(ref_report, filters)
return res
def generate_report_result(report, filters=None, user=None, custom_columns=None):
user = user or frappe.session.user
filters = filters or []
@ -66,13 +79,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
if filters and isinstance(filters, str):
filters = json.loads(filters)
res = []
if report.report_type == "Query Report":
res = report.execute_query_report(filters)
elif report.report_type == "Script Report":
res = report.execute_script_report(filters)
res = get_report_result(report, filters) or []
columns, result, message, chart, report_summary, skip_total_row = ljust_list(res, 6)
columns = [get_column_as_dict(col) for col in columns]

View file

@ -180,15 +180,16 @@ def update_wildcard_field_param(data):
def clean_params(data):
data.pop('cmd', None)
data.pop('data', None)
data.pop('ignore_permissions', None)
data.pop('view', None)
data.pop('user', None)
if "csrf_token" in data:
del data["csrf_token"]
for param in (
"cmd",
"data",
"ignore_permissions",
"view",
"user",
"csrf_token",
"join"
):
data.pop(param, None)
def parse_json(data):
if isinstance(data.get("filters"), str):
@ -214,11 +215,13 @@ def get_parenttype_and_fieldname(field, data):
return parenttype, fieldname
def compress(data, args = {}):
def compress(data, args=None):
"""separate keys and values"""
from frappe.desk.query_report import add_total_row
if not data: return data
if args is None:
args = {}
values = []
keys = list(data[0])
for row in data:
@ -423,15 +426,20 @@ def delete_bulk(doctype, items):
@frappe.whitelist()
@frappe.read_only()
def get_sidebar_stats(stats, doctype, filters=[]):
def get_sidebar_stats(stats, doctype, filters=None):
if filters is None:
filters = []
return {"stats": get_stats(stats, doctype, filters)}
@frappe.whitelist()
@frappe.read_only()
def get_stats(stats, doctype, filters=[]):
def get_stats(stats, doctype, filters=None):
"""get tag info"""
import json
if filters is None:
filters = []
tags = json.loads(stats)
if filters:
filters = json.loads(filters)
@ -480,12 +488,11 @@ def get_stats(stats, doctype, filters=[]):
return stats
@frappe.whitelist()
def get_filter_dashboard_data(stats, doctype, filters=[]):
def get_filter_dashboard_data(stats, doctype, filters=None):
"""get tags info"""
import json
tags = json.loads(stats)
if filters:
filters = json.loads(filters)
filters = json.loads(filters or [])
stats = {}
columns = frappe.db.get_table_columns(doctype)

View file

@ -13,8 +13,8 @@ from email import policy
def get_email(recipients, sender='', msg='', subject='[No Subject]',
text_content = None, footer=None, print_html=None, formatted=None, attachments=None,
content=None, reply_to=None, cc=[], bcc=[], email_account=None, expose_recipients=None,
inline_images=[], header=None):
content=None, reply_to=None, cc=None, bcc=None, email_account=None, expose_recipients=None,
inline_images=None, header=None):
""" Prepare an email with the following format:
- multipart/mixed
- multipart/alternative
@ -25,6 +25,14 @@ def get_email(recipients, sender='', msg='', subject='[No Subject]',
- attachment
"""
content = content or msg
if cc is None:
cc = []
if bcc is None:
bcc = []
if inline_images is None:
inline_images = []
emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, bcc=bcc, email_account=email_account, expose_recipients=expose_recipients)
if not content.strip().startswith("<"):

View file

@ -353,7 +353,7 @@ class EmailServer:
return error_msg
def update_flag(self, folder, uid_list={}):
def update_flag(self, uid_list=None):
""" set all uids mails the flag as seen """
if not uid_list:
return

View file

@ -286,12 +286,16 @@ class FrappeClient(object):
doc.modified = frappe.db.get_single_value(doctype, "modified")
frappe.get_doc(doc).insert()
def get_api(self, method, params={}):
def get_api(self, method, params=None):
if params is None:
params = {}
res = self.session.get(self.url + "/api/method/" + method + "/",
params=params, verify=self.verify, headers=self.headers)
return self.post_process(res)
def post_api(self, method, params={}):
def post_api(self, method, params=None):
if params is None:
params = {}
res = self.session.post(self.url + "/api/method/" + method + "/",
params=params, verify=self.verify, headers=self.headers)
return self.post_process(res)

View file

@ -81,6 +81,9 @@ class BaseDocument(object):
if hasattr(self, "__setup__"):
self.__setup__()
def __getitem__(self, key):
return self.get(key) if hasattr(self, key) else frappe.throw(msg=key, exc=KeyError)
@property
def meta(self):
if not getattr(self, "_meta", None):

View file

@ -35,10 +35,10 @@ class DatabaseQuery(object):
join='left join', distinct=False, start=None, page_length=None, limit=None,
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
update=None, add_total_row=None, user_settings=None, reference_doctype=None,
run=True, strict=True, pluck=None, ignore_ddl=False) -> List:
run=True, strict=True, pluck=None, ignore_ddl=False, parent_doctype=None) -> List:
if not ignore_permissions and \
not frappe.has_permission(self.doctype, "select", user=user) and \
not frappe.has_permission(self.doctype, "read", user=user):
not frappe.has_permission(self.doctype, "select", user=user, parent_doctype=parent_doctype) and \
not frappe.has_permission(self.doctype, "read", user=user, parent_doctype=parent_doctype):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype))
raise frappe.PermissionError(self.doctype)
@ -318,7 +318,8 @@ class DatabaseQuery(object):
doctype = table_name[4:-1]
ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
if not self.flags.ignore_permissions and not frappe.has_permission(doctype, ptype=ptype):
if not self.flags.ignore_permissions and \
not frappe.has_permission(doctype, ptype=ptype, parent_doctype=self.doctype):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype))
raise frappe.PermissionError(doctype)
@ -487,9 +488,9 @@ class DatabaseQuery(object):
f.value = date_range
fallback = "'0001-01-01 00:00:00'"
if (f.fieldname in ('creation', 'modified')):
if f.operator in ('>', '<') and (f.fieldname in ('creation', 'modified')):
value = cstr(f.value)
fallback = "NULL"
fallback = "'0001-01-01 00:00:00'"
elif f.operator.lower() in ('between') and \
(f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))):
@ -544,6 +545,7 @@ class DatabaseQuery(object):
fallback = 0
if isinstance(f.value, Column):
can_be_null = False # added to avoid the ifnull/coalesce addition
quote = '"' if frappe.conf.db_type == 'postgres' else "`"
value = f"{tname}.{quote}{f.value.name}{quote}"

View file

@ -238,7 +238,9 @@ class ParallelTestWithOrchestrator(ParallelTestRunner):
self.call_orchestrator('test-completed')
return super().print_result()
def call_orchestrator(self, endpoint, data={}):
def call_orchestrator(self, endpoint, data=None):
if data is None:
data = {}
# add repo token header
# build id in header
headers = {

View file

@ -178,6 +178,7 @@ frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings
frappe.patches.v13_0.remove_twilio_settings
frappe.patches.v12_0.rename_uploaded_files_with_proper_name
frappe.patches.v13_0.queryreport_columns
execute:frappe.reload_doc('core', 'doctype', 'doctype')
frappe.patches.v13_0.jinja_hook
frappe.patches.v13_0.update_notification_channel_if_empty
frappe.patches.v14_0.drop_data_import_legacy

View file

@ -34,7 +34,7 @@ def print_has_permission_check_logs(func):
return inner
@print_has_permission_check_logs
def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, raise_exception=True):
def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, raise_exception=True, parent_doctype=None):
"""Returns True if user has permission `ptype` for given `doctype`.
If `doc` is passed, it also checks user, share and owner permissions.
@ -47,11 +47,12 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra
doc = doctype
doctype = doc.doctype
if frappe.is_table(doctype):
if user == "Administrator":
return True
if user=="Administrator":
return True
if frappe.is_table(doctype):
return has_child_table_permission(doctype, ptype, doc, verbose,
user, raise_exception, parent_doctype)
meta = frappe.get_meta(doctype)
@ -96,7 +97,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra
if not perm:
perm = false_if_not_shared()
return perm
return bool(perm)
def get_doc_permissions(doc, user=None, ptype=None):
"""Returns a dict of evaluated permissions for given `doc` like `{"read":1, "write":1}`"""
@ -560,3 +561,35 @@ def filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc=
def push_perm_check_log(log):
if frappe.flags.get('has_permission_check_logs') == None: return
frappe.flags.get('has_permission_check_logs').append(_(log))
def has_child_table_permission(child_doctype, ptype="read", child_doc=None,
verbose=False, user=None, raise_exception=True, parent_doctype=None):
parent_doc = None
if child_doc:
parent_doctype = child_doc.get("parenttype")
parent_doc = frappe.get_cached_doc({
"doctype": parent_doctype,
"docname": child_doc.get("parent")
})
if parent_doctype:
if not is_parent_valid(child_doctype, parent_doctype):
frappe.throw(_("{0} is not a valid parent DocType for {1}").format(
frappe.bold(parent_doctype),
frappe.bold(child_doctype)
), title=_("Invalid Parent DocType"))
else:
frappe.throw(_("Please specify a valid parent DocType for {0}").format(
frappe.bold(child_doctype)
), title=_("Parent DocType Required"))
return has_permission(parent_doctype, ptype=ptype, doc=parent_doc,
verbose=verbose, user=user, raise_exception=raise_exception)
def is_parent_valid(child_doctype, parent_doctype):
from frappe.core.utils import find
parent_meta = frappe.get_meta(parent_doctype)
child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype)
return not parent_meta.istable and child_table_field_exists

View file

@ -55,6 +55,10 @@ Quill.register(FontStyle, true);
Quill.register(AlignStyle, true);
Quill.register(DirectionStyle, true);
// direction class
const DirectionClass = Quill.import('attributors/class/direction');
Quill.register(DirectionClass, true);
// replace font tag with span
const Inline = Quill.import('blots/inline');

View file

@ -354,7 +354,7 @@ frappe.search.SearchDialog = class {
get_link(result) {
let link = "";
if (result.route) {
link = `href="#${result.route.join("/")}"`;
link = `href="/app/${result.route.join("/")}"`;
} else if (result.data_path) {
link = `data-path=${result.data_path}"`;
}

View file

@ -13,6 +13,10 @@
.navbar-light {
border-bottom: 1px solid $border-color;
background: $navbar-bg;
.navbar-toggler .icon {
stroke: none;
}
}
.navbar-primary {
@ -25,6 +29,10 @@
}
}
.navbar-brand {
color: white;
}
.navbar-search {
background-color: var(--blue-400);
width: 300px;
@ -36,6 +44,14 @@
}
}
.navbar-toggler {
border-color: rgba(255,255,255, 0.1);
.icon {
stroke: none;
}
}
svg use {
--icon-stroke: white;

View file

@ -112,7 +112,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-06 17:25:40.477044",
"modified": "2021-10-25 17:25:40.477044",
"modified_by": "Administrator",
"module": "Social",
"name": "Energy Point Log",
@ -131,6 +131,5 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "user",
"track_changes": 1
}
"title_field": "user"
}

View file

@ -30,7 +30,11 @@
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
<span>
<svg class="icon icon-lg">
<use href="#icon-menu"></use>
</svg>
</span>
</button>
</div>
</div>

View file

@ -15,7 +15,11 @@
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
<span>
<svg class="icon icon-lg">
<use href="#icon-menu"></use>
</svg>
</span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">

View file

@ -60,7 +60,10 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
# workaround! since there is no separate test db
frappe.clear_cache()
frappe.utils.scheduler.disable_scheduler()
scheduler_disabled_by_user = frappe.utils.scheduler.is_scheduler_disabled()
if not scheduler_disabled_by_user:
frappe.utils.scheduler.disable_scheduler()
set_test_email_config()
frappe.conf.update({'bench_id': 'test_bench', 'use_rq_auth': False})
@ -77,6 +80,9 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
else:
ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast, junit_xml_output=junit_xml_output)
if not scheduler_disabled_by_user:
frappe.utils.scheduler.enable_scheduler()
if frappe.db: frappe.db.commit()
# workaround! since there is no separate test db

View file

@ -142,6 +142,12 @@ class TestReportview(unittest.TestCase):
self.assertTrue({ "name": event1.name } not in data)
self.assertTrue({ "name": event2.name } not in data)
# test between is formatted for creation column
data = DatabaseQuery("Event").execute(
filters={"creation": ["between", ["2016-07-06", "2016-07-07"]]},
fields=["name"])
def test_ignore_permissions_for_get_filters_cond(self):
frappe.set_user('test2@example.com')
self.assertRaises(frappe.PermissionError, get_filters_cond, 'DocType', dict(istable=1), [])

View file

@ -593,3 +593,13 @@ class TestPermissions(unittest.TestCase):
# reset the user
frappe.set_user(current_user)
def test_child_table_permissions(self):
frappe.set_user("test@example.com")
self.assertIsInstance(frappe.get_list("Has Role", parent_doctype="User", limit=1), list)
self.assertRaisesRegex(frappe.exceptions.ValidationError,
".* is not a valid parent DocType for .*", frappe.get_list, doctype="Has Role", parent_doctype="ToDo")
self.assertRaisesRegex(frappe.exceptions.ValidationError,
"Please specify a valid parent DocType for .*", frappe.get_list, "Has Role")
self.assertRaisesRegex(frappe.exceptions.ValidationError,
".* is not a valid parent DocType for .*", frappe.get_list, doctype="Has Role", parent_doctype="Has Role")

View file

@ -11,9 +11,9 @@ from frappe.utils import get_url, get_datetime, time_diff_in_seconds, cint
class ExpiredLoginException(Exception): pass
def toggle_two_factor_auth(state, roles=[]):
def toggle_two_factor_auth(state, roles=None):
'''Enable or disable 2FA in site_config and roles'''
for role in roles:
for role in roles or []:
role = frappe.get_doc('Role', {'role_name': role})
role.two_factor_auth = cint(state)
role.save(ignore_permissions=True)
@ -417,4 +417,4 @@ def reset_otp_secret(user):
enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, is_async=True, job_name=None, now=False, **email_args)
return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login."))
else:
return frappe.throw(_("OTP secret can only be reset by the Administrator."))
return frappe.throw(_("OTP secret can only be reset by the Administrator."))

View file

@ -1025,6 +1025,10 @@ def image_to_base64(image, extn):
def pdf_to_base64(filename):
from frappe.utils.file_manager import get_file_path
if '../' in filename or filename.rsplit('.')[-1] not in ['pdf', 'PDF']:
return
file_path = get_file_path(filename)
if not file_path:
return

View file

@ -302,6 +302,9 @@ def get_file(fname):
def get_file_path(file_name):
"""Returns file path from given file name"""
if '../' in file_name:
return
f = frappe.db.sql("""select file_url from `tabFile`
where name=%s or file_name=%s""", (file_name, file_name))
if f:

View file

@ -68,6 +68,13 @@ def web_blocks(blocks):
return html
def get_dom_id(seed=None):
from frappe import generate_hash
if not seed:
seed = 'DOM'
return 'id-' + generate_hash(seed, 12)
def include_script(path):
path = bundled_asset(path)
return f'<script type="text/javascript" src="{path}"></script>'
@ -94,4 +101,4 @@ def is_rtl(rtl=None):
from frappe import local
if rtl is None:
return local.lang in ["ar", "he", "fa", "ps"]
return rtl
return rtl

View file

@ -1,4 +1,5 @@
import copy
import inspect
import json
import mimetypes
@ -129,7 +130,7 @@ def get_safe_globals():
make_get_request=frappe.integrations.utils.make_get_request,
make_post_request=frappe.integrations.utils.make_post_request,
socketio_port=frappe.conf.socketio_port,
get_hooks=frappe.get_hooks,
get_hooks=get_hooks,
sanitize_html=frappe.utils.sanitize_html,
log_error=frappe.log_error
),
@ -173,8 +174,6 @@ def get_safe_globals():
rollback=frappe.db.rollback,
)
out.frappe.cache = cache
if frappe.response:
out.frappe.response = frappe.response
@ -192,13 +191,9 @@ def get_safe_globals():
return out
def cache():
return NamespaceDict(
get_value = frappe.cache().get_value,
set_value = frappe.cache().set_value,
hset = frappe.cache().hset,
hget = frappe.cache().hget
)
def get_hooks(hook=None, default=None, app_name=None):
hooks = frappe.get_hooks(hook=hook, default=default, app_name=app_name)
return copy.deepcopy(hooks)
def read_sql(query, *args, **kwargs):
'''a wrapper for frappe.db.sql to allow reads'''

View file

@ -59,7 +59,7 @@
],
"in_create": 1,
"links": [],
"modified": "2020-05-05 14:11:24.718770",
"modified": "2021-10-25 14:11:24.718770",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Page View",
@ -82,6 +82,5 @@
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "path",
"track_changes": 1
}
"title_field": "path"
}

View file

@ -15,7 +15,11 @@
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
<span>
<svg class="icon icon-lg">
<use href="#icon-menu"></use>
</svg>
</span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">

View file

@ -72,6 +72,9 @@ def get_list_data(doctype, txt=None, limit_start=0, fields=None, cmd=None, limit
"""Returns processed HTML page for a standard listing."""
limit_start = cint(limit_start)
if frappe.is_table(doctype):
frappe.throw(_("Child DocTypes are not allowed"), title=_("Invalid DocType"))
if not txt and frappe.form_dict.search:
txt = frappe.form_dict.search
del frappe.form_dict['search']
@ -183,8 +186,7 @@ def get_list_context(context, doctype, web_form_name=None):
return list_context
def get_list(doctype, txt, filters, limit_start, limit_page_length=20, ignore_permissions=False,
fields=None, order_by=None):
def get_list(doctype, txt, filters, limit_start, limit_page_length=20, ignore_permissions=False, fields=None, order_by=None):
meta = frappe.get_meta(doctype)
if not filters:
filters = []

View file

@ -554,9 +554,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001196, caniuse-lite@^1.0.30001219:
version "1.0.30001228"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz#bfdc5942cd3326fa51ee0b42fbef4da9d492a7fa"
integrity sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==
version "1.0.30001272"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001272.tgz"
integrity sha512-DV1j9Oot5dydyH1v28g25KoVm7l8MTxazwuiH3utWiAS6iL/9Nh//TGwqFEeqqN8nnWYQ8HHhUq+o4QPt9kvYw==
caseless@~0.12.0:
version "0.12.0"
@ -4934,13 +4934,6 @@ vuedraggable@^2.24.3:
dependencies:
sortablejs "1.10.2"
wcwidth@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
dependencies:
defaults "^1.0.3"
which-boxed-primitive@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"