diff --git a/frappe/__init__.py b/frappe/__init__.py index 9d7befe2d1..4d67afe492 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -182,9 +182,9 @@ if TYPE_CHECKING: # end: static analysis hack -def init(site: str, sites_path: str = ".", new_site: bool = False) -> None: +def init(site: str, sites_path: str = ".", new_site: bool = False, force=False) -> None: """Initialize frappe for the current site. Reset thread locals `frappe.local`""" - if getattr(local, "initialised", None): + if getattr(local, "initialised", None) and not force: return local.error_log = [] diff --git a/frappe/app.py b/frappe/app.py index a647b251c8..fab8facd3f 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -74,12 +74,18 @@ def application(request: Request): rollback = sync_database(rollback) finally: + # Important note: + # this function *must* always return a response, hence any exception thrown outside of + # try..catch block like this finally block needs to be handled appropriately. + if request.method in UNSAFE_HTTP_METHODS and frappe.db and rollback: frappe.db.rollback() - if getattr(frappe.local, "initialised", False): - for after_request_task in frappe.get_hooks("after_request"): - frappe.call(after_request_task, response=response, request=request) + try: + run_after_request_hooks(request, response) + except Exception as e: + # We can not handle exceptions safely here. + frappe.logger().error("Failed to run after request hook", exc_info=True) log_request(request, response) process_response(response) @@ -89,12 +95,20 @@ def application(request: Request): return response +def run_after_request_hooks(request, response): + if not getattr(frappe.local, "initialised", False): + return + + for after_request_task in frappe.get_hooks("after_request"): + frappe.call(after_request_task, response=response, request=request) + + def init_request(request): frappe.local.request = request frappe.local.is_ajax = frappe.get_request_header("X-Requested-With") == "XMLHttpRequest" site = _site or request.headers.get("X-Frappe-Site-Name") or get_site_name(request.host) - frappe.init(site=site, sites_path=_sites_path) + frappe.init(site=site, sites_path=_sites_path, force=True) if not (frappe.local.conf and frappe.local.conf.db_name): # site does not exist diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index 86bd69eb5f..51e065f710 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -17,6 +17,7 @@ from frappe.core.api.file import ( move_file, unzip_file, ) +from frappe.core.doctype.file.utils import get_extension from frappe.exceptions import ValidationError from frappe.tests.utils import FrappeTestCase from frappe.utils import get_files_path @@ -461,7 +462,7 @@ class TestFile(FrappeTestCase): ).insert(ignore_permissions=True) test_file.make_thumbnail() - self.assertTrue(test_file.thumbnail_url.endswith("_small.jpeg")) + self.assertTrue(test_file.thumbnail_url.endswith("_small.jpg")) # test local image test_file.db_set("thumbnail_url", None) @@ -739,3 +740,10 @@ class TestFileOptimization(FrappeTestCase): size_after_rollback = os.stat(image_path).st_size self.assertEqual(size_before_optimization, size_after_rollback) + + def test_image_header_guessing(self): + file_path = frappe.get_app_path("frappe", "tests/data/sample_image_for_optimization.jpg") + with open(file_path, "rb") as f: + file_content = f.read() + + self.assertEqual(get_extension("", None, file_content), "jpg") diff --git a/frappe/core/doctype/file/utils.py b/frappe/core/doctype/file/utils.py index 17a092e340..1d0d145303 100644 --- a/frappe/core/doctype/file/utils.py +++ b/frappe/core/doctype/file/utils.py @@ -1,5 +1,4 @@ import hashlib -import imghdr import mimetypes import os import re @@ -7,6 +6,7 @@ from io import BytesIO from typing import TYPE_CHECKING, Optional from urllib.parse import unquote +import filetype import requests import requests.exceptions from PIL import Image @@ -76,9 +76,11 @@ def get_extension( mimetype = mimetypes.guess_type(filename + "." + extn)[0] - if mimetype is None or not mimetype.startswith("image/") and content: - # detect file extension by reading image header properties - extn = imghdr.what(filename + "." + (extn or ""), h=content) + if mimetype is None and extn is None and content: + # detect file extension by using filetype matchers + _type_info = filetype.match(content) + if _type_info: + extn = _type_info.extension return extn diff --git a/frappe/core/doctype/language/language.json b/frappe/core/doctype/language/language.json index 7e9bbb1038..c9110bb998 100644 --- a/frappe/core/doctype/language/language.json +++ b/frappe/core/doctype/language/language.json @@ -51,7 +51,7 @@ "icon": "fa fa-globe", "in_create": 1, "links": [], - "modified": "2022-08-14 18:54:03.490836", + "modified": "2023-04-13 13:48:38.127995", "modified_by": "Administrator", "module": "Core", "name": "Language", @@ -66,13 +66,8 @@ "write": 1 }, { - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Guest", - "share": 1 + "role": "All", + "read": 1 } ], "search_fields": "language_name", diff --git a/frappe/core/doctype/report/report.json b/frappe/core/doctype/report/report.json index 37dce73dda..9b6b04afcc 100644 --- a/frappe/core/doctype/report/report.json +++ b/frappe/core/doctype/report/report.json @@ -148,11 +148,13 @@ { "collapsible": 1, "collapsible_depends_on": "filters", + "depends_on": "eval:doc.report_type != \"Custom Report\"", "fieldname": "filters_section", "fieldtype": "Section Break", "label": "Filters" }, { + "depends_on": "eval:doc.report_type != \"Custom Report\"", "fieldname": "filters", "fieldtype": "Table", "label": "Filters", @@ -161,11 +163,13 @@ { "collapsible": 1, "collapsible_depends_on": "columns", + "depends_on": "eval:doc.report_type != \"Custom Report\"", "fieldname": "columns_section", "fieldtype": "Section Break", "label": "Columns" }, { + "depends_on": "eval:doc.report_type != \"Custom Report\"", "fieldname": "columns", "fieldtype": "Table", "label": "Columns", @@ -182,7 +186,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-11-20 14:56:36.578412", + "modified": "2023-04-07 18:18:11.782178", "modified_by": "Administrator", "module": "Core", "name": "Report", diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index ef38387e57..ca1e7724c1 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -169,7 +169,7 @@ class Report(Document): return columns, result - def run_query_report(self, filters, user, ignore_prepared_report=False): + def run_query_report(self, filters=None, user=None, ignore_prepared_report=False): columns, result = [], [] data = frappe.desk.query_report.run( self.name, filters=filters, user=user, ignore_prepared_report=ignore_prepared_report diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index 0e1ed80eda..670b6b7410 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -118,11 +118,10 @@ class TestReport(FrappeTestCase): } ] ), + json.dumps({"user": "Administrator", "doctype": "User"}), ) custom_report = frappe.get_doc("Report", custom_report_name) - columns, result = custom_report.run_query_report( - filters={"user": "Administrator", "doctype": "User"}, user=frappe.session.user - ) + columns, result = custom_report.run_query_report(user=frappe.session.user) self.assertListEqual(["email"], [column.get("fieldname") for column in columns]) admin_dict = frappe.core.utils.find(result, lambda d: d["name"] == "Administrator") diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index bb845cae95..3f906d8f12 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -15,12 +15,13 @@ from frappe.model.utils import render_include from frappe.modules import get_module_path, scrub from frappe.monitor import add_data_to_monitor from frappe.permissions import get_role_permissions -from frappe.utils import cint, cstr, flt, format_duration, get_html_format +from frappe.utils import cint, cstr, flt, format_duration, get_html_format, sbool def get_report_doc(report_name): doc = frappe.get_doc("Report", report_name) doc.custom_columns = [] + doc.custom_filters = [] if doc.report_type == "Custom Report": custom_report_doc = doc @@ -30,7 +31,8 @@ def get_report_doc(report_name): if custom_report_doc.json: data = json.loads(custom_report_doc.json) if data: - doc.custom_columns = data["columns"] + doc.custom_columns = data.get("columns") + doc.custom_filters = data.get("filters") doc.is_custom_report = True if not doc.is_permitted(): @@ -182,6 +184,7 @@ def run( custom_columns=None, is_tree=False, parent_field=None, + are_default_filters=True, ): report = get_report_doc(report_name) if not user: @@ -194,6 +197,9 @@ def run( result = None + if sbool(are_default_filters) and report.custom_filters: + filters = report.custom_filters + if report.prepared_report and not ignore_prepared_report and not custom_columns: if filters: if isinstance(filters, str): @@ -209,6 +215,9 @@ def run( result["add_total_row"] = report.add_total_row and not result.get("skip_total_row", False) + if sbool(are_default_filters) and report.custom_filters: + result["custom_filters"] = report.custom_filters + return result @@ -463,7 +472,7 @@ def get_data_for_custom_report(columns): @frappe.whitelist() -def save_report(reference_report, report_name, columns): +def save_report(reference_report, report_name, columns, filters): report_doc = get_report_doc(reference_report) docname = frappe.db.exists( @@ -479,6 +488,7 @@ def save_report(reference_report, report_name, columns): report = frappe.get_doc("Report", docname) existing_jd = json.loads(report.json) existing_jd["columns"] = json.loads(columns) + existing_jd["filters"] = json.loads(filters) report.update({"json": json.dumps(existing_jd, separators=(",", ":"))}) report.save() frappe.msgprint(_("Report updated successfully")) @@ -489,7 +499,7 @@ def save_report(reference_report, report_name, columns): { "doctype": "Report", "report_name": report_name, - "json": f'{{"columns":{columns}}}', + "json": f'{{"columns":{columns},"filters":{filters}}}', "ref_doctype": report_doc.ref_doctype, "is_standard": "No", "report_type": "Custom Report", diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 00ae27d145..b450b734e9 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -678,7 +678,7 @@ def get_filters_cond( for f in filters: if isinstance(f[1], str) and f[1][0] == "!": flt.append([doctype, f[0], "!=", f[1][1:]]) - elif isinstance(f[1], (list, tuple)) and f[1][0] in ( + elif isinstance(f[1], (list, tuple)) and f[1][0].lower() in ( ">", "<", ">=", diff --git a/frappe/hooks.py b/frappe/hooks.py index effc84a873..b055f9dc8e 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -186,11 +186,13 @@ scheduler_events = { "frappe.oauth.delete_oauth2_data", "frappe.website.doctype.web_page.web_page.check_publish_status", "frappe.twofactor.delete_all_barcodes_for_users", - ] + ], + "0/10 * * * *": [ + "frappe.email.doctype.email_account.email_account.pull", + ], }, "all": [ "frappe.email.queue.flush", - "frappe.email.doctype.email_account.email_account.pull", "frappe.email.doctype.email_account.email_account.notify_unreplied", "frappe.utils.global_search.sync_global_search", "frappe.monitor.flush", diff --git a/frappe/oauth.py b/frappe/oauth.py index 8955078342..2d25b5dfb5 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -3,7 +3,7 @@ import datetime import hashlib import re from http import cookies -from urllib.parse import unquote, urlparse, urljoin +from urllib.parse import unquote, urljoin, urlparse import jwt import pytz diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index d6c50380f2..ee255032bb 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -2,6 +2,9 @@ // MIT License. See license.txt import DataTable from "frappe-datatable"; +// Expose DataTable globally to allow customizations. +window.DataTable = DataTable; + frappe.provide("frappe.widget.utils"); frappe.provide("frappe.views"); frappe.provide("frappe.query_reports"); @@ -539,7 +542,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { if (this.prepared_report) { this.reset_report_view(); } else if (!this._no_refresh) { - this.refresh(); + this.refresh(true); } } }; @@ -595,10 +598,25 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.page.clear_fields(); } - refresh() { + refresh(have_filters_changed) { this.toggle_message(true); this.toggle_report(false); let filters = this.get_filter_values(true); + + // for custom reports, + // are_default_filters is true if the filters haven't been modified and for all filters, + // the filter value is the default value or there's no default value for the filter and the current value is empty. + // are_default_filters is false otherwise. + + let are_default_filters = this.filters + .map((filter) => { + return ( + !have_filters_changed && + (filter.default === filter.value || (!filter.default && !filter.value)) + ); + }) + .every((res) => res === true); + this.show_loading_screen(); // only one refresh at a time @@ -621,6 +639,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { filters: filters, is_tree: this.report_settings.tree, parent_field: this.report_settings.parent_field, + are_default_filters: are_default_filters, }, callback: resolve, always: () => this.page.btn_secondary.prop("disabled", false), @@ -633,6 +652,11 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.execution_time = data.execution_time || 0.1; + if (data.custom_filters) { + this.set_filters(data.custom_filters); + this.previous_filters = data.custom_filters; + } + if (data.prepared_report) { this.prepared_report = true; this.prepared_report_document = data.doc; @@ -933,7 +957,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { if (this.report_settings.get_datatable_options) { datatable_options = this.report_settings.get_datatable_options(datatable_options); } - this.datatable = new DataTable(this.$report[0], datatable_options); + this.datatable = new window.DataTable(this.$report[0], datatable_options); } if (typeof this.report_settings.initial_depth == "number") { @@ -1712,6 +1736,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { reference_report: this.report_name, report_name: values.report_name, columns: this.get_visible_columns(), + filters: this.get_filter_values(), }, callback: function (r) { this.show_save = false; diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss index f22b587405..ae53e99518 100644 --- a/frappe/public/scss/common/css_variables.scss +++ b/frappe/public/scss/common/css_variables.scss @@ -243,6 +243,8 @@ $input-height: 28px !default; --highlight-color: var(--gray-50); --yellow-highlight-color: var(--yellow-50); + --btn-group-border-color: var(--gray-300); + --field-placeholder-color: var(--gray-50); --highlight-shadow: 1px 1px 10px var(--blue-50), 0px 0px 4px var(--blue-600); diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss index e1f210c440..ced95d7f69 100644 --- a/frappe/public/scss/desk/dark.scss +++ b/frappe/public/scss/desk/dark.scss @@ -100,6 +100,8 @@ --highlight-color: var(--gray-700); --yellow-highlight-color: var(--yellow-700); + --btn-group-border-color: var(--gray-800); + --field-placeholder-color: var(--gray-700); --highlight-shadow: 1px 1px 10px var(--blue-900), 0px 0px 4px var(--blue-500); diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss index 9f1b0b56a4..765e51cab9 100644 --- a/frappe/public/scss/desk/global.scss +++ b/frappe/public/scss/desk/global.scss @@ -234,6 +234,21 @@ h2 { font-size: var(--text-md); } +.btn-group { + .btn { + box-shadow: none; + outline: 1px solid var(--btn-group-border-color); + + &:not(:first-child) { + margin-left: 1px; + } + + &:focus { + outline: 2px solid var(--dark-border-color); + } + } +} + .btn-xs { @extend .btn-sm; line-height: 1.2; diff --git a/frappe/public/scss/desk/list.scss b/frappe/public/scss/desk/list.scss index 31d1661abb..30bf1d6499 100644 --- a/frappe/public/scss/desk/list.scss +++ b/frappe/public/scss/desk/list.scss @@ -189,29 +189,12 @@ $level-margin-right: 8px; .list-paging-area, .footnote-area { border-top: 1px solid var(--border-color); - .btn-group { - box-shadow: var(--drop-shadow); - border-radius: var(--border-radius-md); - - &> .btn:nth-child(2) { - border-left: none; - border-right: none; - } - - .btn-paging { - box-shadow: none; - margin-left: 0px !important; - border: 1px solid var(--dark-border-color); - - &.btn-info { - background-color: var(--gray-600); - border-color: var(--gray-600); - color: var(--white); - font-weight: var(--text-bold); - } - } + .btn-group .btn-paging.btn-info { + background-color: var(--gray-600); + border-color: var(--gray-600); + color: var(--white); + font-weight: var(--text-bold); } - } .frappe-card { diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index 7af2bfda8e..01f6e4f7cc 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -236,6 +236,7 @@ class TestWebsite(FrappeTestCase): def test_printview_page(self): frappe.db.value_cache[("DocType", "Language", "name")] = (("Language",),) + frappe.set_user("Administrator") content = get_response_content("/Language/ru") self.assertIn('