Merge remote-tracking branch 'upstream/develop' into dev-granular-printing

This commit is contained in:
Rohan Bansal 2021-11-04 14:38:27 +05:30
commit c4ed8cf0e7
69 changed files with 508 additions and 236 deletions

View file

@ -12,4 +12,4 @@ jobs:
- name: curl
run: |
apk add curl bash
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.CI_PAT }}" https://api.github.com/repos/frappe/frappe_docker/actions/workflows/build_stable.yml/dispatches -d '{"ref":"main"}'

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

@ -137,10 +137,10 @@ lang = local("lang")
if typing.TYPE_CHECKING:
from frappe.database.mariadb.database import MariaDBDatabase
from frappe.database.postgres.database import PostgresDatabase
from pypika import Query
from frappe.query_builder.builder import MariaDB, Postgres
db: typing.Union[MariaDBDatabase, PostgresDatabase]
qb: Query
qb: typing.Union[MariaDB, Postgres]
# end: static analysis hack
@ -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

@ -1,6 +1,7 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
from frappe.utils import cstr
from tenacity import retry, retry_if_exception_type, stop_after_attempt
from frappe.model.document import Document
@ -24,25 +25,21 @@ def make_access_log(
page=None,
columns=None,
):
user = frappe.session.user
in_request = frappe.request and frappe.request.method == "GET"
doc = frappe.get_doc(
{
"doctype": "Access Log",
"user": user,
"export_from": doctype,
"reference_document": document,
"file_type": file_type,
"report_name": report_name,
"page": page,
"method": method,
"filters": frappe.utils.cstr(filters) if filters else None,
"columns": columns,
}
)
doc.insert(ignore_permissions=True)
frappe.get_doc({
"doctype": "Access Log",
"user": user,
"export_from": doctype,
"reference_document": document,
"file_type": file_type,
"report_name": report_name,
"page": page,
"method": method,
"filters": cstr(filters) or None,
"columns": columns,
}).db_insert()
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview`
# dont commit in test mode

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

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

@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
# Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE
import hashlib
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder import DocType
from frappe.utils import cint, now_datetime
import hashlib
class TransactionLog(Document):
def before_insert(self):
@ -44,10 +46,14 @@ class TransactionLog(Document):
def get_current_index():
current = frappe.db.sql("""SELECT `current`
FROM `tabSeries`
WHERE `name` = 'TRANSACTLOG'
FOR UPDATE""")
series = DocType("Series")
current = (
frappe.qb.from_(series)
.where(series.name == "TRANSACTLOG")
.for_update()
.select("current")
).run()
if current and current[0][0] is not None:
current = current[0][0]

View file

@ -717,8 +717,10 @@ def ask_pass_update():
# update the sys defaults as to awaiting users
from frappe.utils import set_default
users = frappe.db.sql("""SELECT DISTINCT(parent) as user FROM `tabUser Email`
WHERE awaiting_password = 1""", as_dict=True)
doctype = DocType("User Email")
users = frappe.qb.from_(doctype).where(doctype.awaiting_password == 1).select(
doctype.parent.as_("user")
).distinct().run(as_dict=True)
password_list = [ user.get("user") for user in users ]
set_default("email_user_password", u','.join(password_list))

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

@ -1,4 +1,4 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
@ -12,13 +12,17 @@ def execute(filters=None):
return columns, data
def get_data(filters=None):
logs = frappe.db.sql("SELECT * FROM `tabTransaction Log` order by creation desc ", as_dict=1)
result = []
logs = frappe.get_all("Transaction Log", fields=["*"], order_by="creation desc")
for l in logs:
row_index = int(l.row_index)
if row_index > 1:
previous_hash = frappe.db.sql("SELECT chaining_hash FROM `tabTransaction Log` WHERE row_index = {0}".format(row_index - 1))
previous_hash = frappe.get_all(
"Transaction Log",
fields=["chaining_hash"],
filters={"row_index": row_index - 1},
)
if not previous_hash:
integrity = False
else:

View file

@ -170,6 +170,12 @@ class Database(object):
frappe.errprint('Syntax error in query:')
frappe.errprint(query)
elif self.is_deadlocked(e):
raise frappe.QueryDeadlockError
elif self.is_timedout(e):
raise frappe.QueryTimeoutError
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
pass
else:

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

@ -4,6 +4,7 @@
import frappe
from frappe.model.document import Document
from frappe.utils import unique
from frappe.query_builder import DocType
class Tag(Document):
pass
@ -42,10 +43,12 @@ def remove_tag(tag, dt, dn):
@frappe.whitelist()
def get_tagged_docs(doctype, tag):
frappe.has_permission(doctype, throw=True)
return frappe.db.sql("""SELECT name
FROM `tab{0}`
WHERE _user_tags LIKE '%{1}%'""".format(doctype, tag))
doctype = DocType(doctype)
return (
frappe.qb.from_(doctype)
.where(doctype._user_tags.like(tag))
.select(doctype.name)
).run()
@frappe.whitelist()
def get_tags(doctype, txt):

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

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

View file

@ -101,6 +101,8 @@ class DataTooLongException(ValidationError): pass
class FileAlreadyAttachedException(Exception): pass
class DocumentAlreadyRestored(ValidationError): pass
class AttachmentLimitReached(ValidationError): pass
class QueryTimeoutError(Exception): pass
class QueryDeadlockError(Exception): pass
# OAuth exceptions
class InvalidAuthorizationHeader(CSRFTokenError): pass
class InvalidAuthorizationPrefix(CSRFTokenError): pass

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

@ -17,6 +17,7 @@ from frappe import _
from frappe.utils import now_datetime, cint, cstr
import re
from frappe.model import log_types
from frappe.query_builder import DocType
def set_new_name(doc):
@ -194,7 +195,15 @@ def parse_naming_series(parts, doctype='', doc=''):
def getseries(key, digits):
# series created ?
current = frappe.db.sql("SELECT `current` FROM `tabSeries` WHERE `name`=%s FOR UPDATE", (key,))
# Using frappe.qb as frappe.get_values does not allow order_by=None
series = DocType("Series")
current = (
frappe.qb.from_(series)
.where(series.name == key)
.for_update()
.select("current")
).run()
if current and current[0][0] is not None:
current = current[0][0]
# yes, update it
@ -260,7 +269,13 @@ def revert_series_if_last(key, name, doc=None):
prefix = parse_naming_series(prefix.split('.'), doc=doc)
count = cint(name.replace(prefix, ""))
current = frappe.db.sql("SELECT `current` FROM `tabSeries` WHERE `name`=%s FOR UPDATE", (prefix,))
series = DocType("Series")
current = (
frappe.qb.from_(series)
.where(series.name == prefix)
.for_update()
.select("current")
).run()
if current and current[0][0]==count:
frappe.db.sql("UPDATE `tabSeries` SET `current` = `current` - 1 WHERE `name`=%s", prefix)

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

@ -15,11 +15,6 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp
.addClass("input-with-feedback form-control")
.prependTo(this.input_area);
if (in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'],
this.df.fieldtype)) {
this.$input.attr("maxlength", this.df.length || 140);
}
this.$input.on('paste', (e) => {
let pasted_data = frappe.utils.get_clipboard_data(e);
let maxlength = this.$input.attr('maxlength');
@ -199,6 +194,13 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp
}
}
set_input_attributes() {
if (in_list(
['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only'],
this.df.fieldtype
)) {
this.$input.attr("maxlength", this.df.length || 140);
}
this.$input
.attr("data-fieldtype", this.df.fieldtype)
.attr("data-fieldname", this.df.fieldname)

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

@ -30,7 +30,7 @@ frappe.ui.form.Footer = class FormFooter {
fieldname: 'comment'
},
on_submit: (comment) => {
if (strip_html(comment).trim() != "") {
if (strip_html(comment).trim() != "" || comment.includes('img')) {
this.frm.comment_box.disable();
frappe.xcall("frappe.desk.form.utils.add_comment", {
reference_doctype: this.frm.doctype,

View file

@ -206,6 +206,25 @@ frappe.request.call = function(opts) {
}
};
var exception_handlers = {
'QueryTimeoutError': function() {
frappe.utils.play_sound("error");
frappe.msgprint({
title: __('Request Timeout'),
indicator: 'red',
message: __("Server was too busy to process this request. Please try again.")
});
},
'QueryDeadlockError': function() {
frappe.utils.play_sound("error");
frappe.msgprint({
title: __('Deadlock Occurred'),
indicator: 'red',
message: __("Server was too busy to process this request. Please try again.")
});
}
};
var ajax_args = {
url: opts.url || frappe.request.url,
data: opts.args,
@ -272,13 +291,25 @@ frappe.request.call = function(opts) {
})
.fail(function(xhr, textStatus) {
try {
if (xhr.responseText) {
var data = JSON.parse(xhr.responseText);
if (data.exception) {
// frappe.exceptions.CustomError -> CustomError
var exception = data.exception.split('.').at(-1);
var exception_handler = exception_handlers[exception];
if (exception_handler) {
exception_handler(data);
return;
}
}
}
var status_code_handler = statusCode[xhr.statusCode().status];
if (status_code_handler) {
status_code_handler(xhr);
} else {
// if not handled by error handler!
opts.error_callback && opts.error_callback(xhr);
return;
}
// if not handled by error handler!
opts.error_callback && opts.error_callback(xhr);
} catch(e) {
console.log("Unable to handle failed response"); // eslint-disable-line
console.trace(e); // eslint-disable-line

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

@ -135,7 +135,7 @@ body {
}
.social-logins {
margin: var(--margin-md) 0;
margin-top: var(--margin-md);
font-size: var(--text-md);
.social-login-buttons {
@ -147,7 +147,11 @@ body {
}
min-width: 50%;
padding: 0 4px;
margin-bottom: var(--margin-sm);
margin-bottom: var(--margin-md);
&:last-child {
margin-bottom: 0;
}
}
}
}

View file

@ -1 +1 @@
@import './website/index';
@import './website/index';

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

@ -145,11 +145,6 @@
.section-with-cards .card {
@include transition();
border: none;
.card-body {
padding: 0 1.5rem 2rem 0;
}
&:hover {
border-color: $gray-500;

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

@ -10,7 +10,7 @@ class TestWebsite(unittest.TestCase):
frappe.set_user('Guest')
def tearDown(self):
frappe.db.value_cache = {}
frappe.db.delete('Access Log')
frappe.set_user('Administrator')
def test_home_page(self):

View file

@ -20,7 +20,7 @@ from typing import List, Union, Tuple
import frappe
from frappe.model.utils import InvalidIncludePath, render_include
from frappe.utils import get_bench_path, is_html, strip, strip_html_tags
from frappe.query_builder import Field
from frappe.query_builder import Field, DocType
from pypika.terms import PseudoColumn
@ -334,14 +334,15 @@ def clear_cache():
def get_messages_for_app(app, deduplicate=True):
"""Returns all messages (list) for a specified `app`"""
messages = []
modules = ", ".join('"{}"'.format(m.title().replace("_", " ")) \
for m in frappe.local.app_modules[app])
modules = [frappe.unscrub(m) for m in frappe.local.app_modules[app]]
# doctypes
if modules:
if isinstance(modules, str):
modules = [modules]
filtered_doctypes = frappe.qb.from_("DocType").where(
Field("module").isin(modules)
).select("name").run()
).select("name").run(pluck=True)
for name in filtered_doctypes:
messages.extend(get_messages_from_doctype(name))
@ -355,9 +356,14 @@ def get_messages_for_app(app, deduplicate=True):
# reports
for name in frappe.db.sql_list("""select tabReport.name from tabDocType, tabReport
where tabReport.ref_doctype = tabDocType.name
and tabDocType.module in ({})""".format(modules)):
report = DocType("Report")
doctype = DocType("DocType")
names = (
frappe.qb.from_(doctype)
.from_(report)
.where((report.ref_doctype == doctype.name) & doctype.module.isin(modules))
.select(report.name).run(pluck=True))
for name in names:
messages.append((None, name))
messages.extend(get_messages_from_report(name))
for i in messages:

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

@ -5,6 +5,7 @@ from typing import Optional
import frappe
import operator
import json
import base64
import re, datetime, math, time
from code import compile_command
from urllib.parse import quote, urljoin
@ -1013,7 +1014,6 @@ def get_thumbnail_base64_for_image(src):
return cache().hget('thumbnail_base64', src, generator=_get_base64)
def image_to_base64(image, extn):
import base64
from io import BytesIO
buffered = BytesIO()
@ -1023,6 +1023,20 @@ def image_to_base64(image, extn):
img_str = base64.b64encode(buffered.getvalue())
return img_str
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
with open(file_path, 'rb') as pdf_file:
base64_string = base64.b64encode(pdf_file.read())
return base64_string
# from Jinja2 code
_striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)')

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

@ -14,6 +14,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import now
from frappe.query_builder import DocType, Order
class NestedSetRecursionError(frappe.ValidationError): pass
class NestedSetMultipleRootsError(frappe.ValidationError): pass
@ -146,10 +147,21 @@ def rebuild_tree(doctype, parent_field):
frappe.only_for('System Manager')
# get all roots
right = 1
table = DocType(doctype)
column = getattr(table, parent_field)
result = (
frappe.qb.from_(table)
.where(
(column == "") | (column.isnull())
)
.orderby(table.name, order=Order.asc)
.select(table.name)
).run()
frappe.db.auto_commit_on_many_writes = 1
right = 1
result = frappe.db.sql("SELECT name FROM `tab%s` WHERE `%s`='' or `%s` IS NULL ORDER BY name ASC" % (doctype, parent_field, parent_field))
for r in result:
right = rebuild_node(doctype, r[0], right, parent_field)
@ -159,22 +171,23 @@ def rebuild_node(doctype, parent, left, parent_field):
"""
reset lft, rgt and recursive call for all children
"""
from frappe.utils import now
n = now()
# the right value of this node is the left value + 1
right = left+1
# get all children of this node
result = frappe.db.sql("SELECT name FROM `tab{0}` WHERE `{1}`=%s"
.format(doctype, parent_field), (parent))
table = DocType(doctype)
column = getattr(table, parent_field)
result = (
frappe.qb.from_(table).where(column == parent).select(table.name)
).run()
for r in result:
right = rebuild_node(doctype, r[0], right, parent_field)
# we've got the left value, and now that we've processed
# the children of this node we also know the right value
frappe.db.sql("""UPDATE `tab{0}` SET lft=%s, rgt=%s, modified=%s
WHERE name=%s""".format(doctype), (left,right,n,parent))
frappe.db.set_value(doctype, parent, {"lft": left, "rgt": right}, for_update=False)
#return the right value of this node + 1
return right+1

View file

@ -26,7 +26,10 @@ def report_error(status_code):
allow_traceback = cint(frappe.db.get_system_setting('allow_error_traceback')) if frappe.db else True
if (allow_traceback and (status_code!=404 or frappe.conf.logging)
and not frappe.local.flags.disable_traceback):
frappe.errprint(frappe.utils.get_traceback())
traceback = frappe.utils.get_traceback()
if traceback:
frappe.errprint(traceback)
frappe.local.response.exception = traceback.splitlines()[-1]
response = build_response("json")
response.status_code = status_code

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'''
@ -327,6 +322,7 @@ VALID_UTILS = (
"is_image",
"get_thumbnail_base64_for_image",
"image_to_base64",
"pdf_to_base64",
"strip_html",
"escape_html",
"pretty_date",

View file

@ -1,8 +1,20 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See LICENSE
import click
import frappe
from weasyprint import HTML, CSS
try:
from weasyprint import HTML, CSS
except OSError:
click.secho(
"\n".join(["WeasyPrint depdends on additional system dependencies.",
"Follow instructions specific to your operating system:",
"https://doc.courtbouillon.org/weasyprint/stable/first_steps.html"]),
fg="yellow"
)
raise
@frappe.whitelist()

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

@ -79,7 +79,11 @@
<form class="form-signin form-login" role="form">
{%- if social_login -%}
<div class="page-card-body">
<form class="form-signin form-login" role="form">
{{ email_login_body() }}
</form>
<div class="social-logins text-center">
<p class="text-muted login-divider">{{ _("or") }}</p>
<div class="social-login-buttons">
{% for provider in provider_logins %}
<div class="login-button-wrapper">
@ -91,12 +95,8 @@
{{ _("Login With {0}").format(provider.provider_name) }}</a>
</div>
{% endfor %}
<p class="text-muted login-divider">{{ _("or") }}</p>
</div>
</div>
<a href="#email"
class="btn btn-block btn-default btn-sm btn-login-option">
{{ _("Login With Email") }}</a>
</div>
{% else %}
{{ email_login_body() }}

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"