From c2e44754d17b0056152b24fd09a67d3bb17a431e Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Thu, 1 Sep 2022 18:43:05 +0530 Subject: [PATCH 001/152] feat: allow syncing new fields in Doctype Layout --- .../doctype/custom_field/custom_field.py | 14 +++ .../doctype/doctype_layout/doctype_layout.js | 89 +++++++++++++++---- .../doctype_layout/doctype_layout.json | 9 +- .../doctype/doctype_layout/doctype_layout.py | 59 ++++++++++++ 4 files changed, 152 insertions(+), 19 deletions(-) diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index f50ceb1992..7b55b4bc6b 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -102,6 +102,20 @@ class CustomField(Document): # delete property setter entries frappe.db.delete("Property Setter", {"doc_type": self.dt, "field_name": self.fieldname}) + + # update doctype layouts + doctype_layouts = frappe.get_all( + "DocType Layout", filters={"document_type": self.dt}, pluck="name" + ) + + for layout in doctype_layouts: + layout_doc = frappe.get_doc("DocType Layout", layout) + for field in layout_doc.fields: + if field.fieldname == self.fieldname: + layout_doc.remove(field) + layout_doc.save() + break + frappe.clear_cache(doctype=self.dt) def validate_insert_after(self, meta): diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.js b/frappe/custom/doctype/doctype_layout/doctype_layout.js index f91f04f762..45c2de8447 100644 --- a/frappe/custom/doctype/doctype_layout/doctype_layout.js +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.js @@ -2,31 +2,88 @@ // For license information, please see license.txt frappe.ui.form.on("DocType Layout", { - refresh: function (frm) { - frm.trigger("document_type"); - frm.events.set_button(frm); + onload_post_render(frm) { + // disallow users from manually adding/deleting rows; this doctype should only + // be used for managing layout, and docfields and custom fields should be used + // to manage other field metadata (hidden, etc.) + frm.set_df_property("fields", "cannot_add_rows", true); + frm.set_df_property("fields", "cannot_delete_rows", true); + }, + + refresh(frm) { + frm.events.add_buttons(frm); }, document_type(frm) { - frm.set_fields_as_options("fields", frm.doc.document_type, null, [], "fieldname").then( - () => { - // child table empty? then show all fields as default - if (frm.doc.document_type) { - if (!(frm.doc.fields || []).length) { - for (let f of frappe.get_doc("DocType", frm.doc.document_type).fields) { - frm.add_child("fields", { fieldname: f.fieldname, label: f.label }); - } - } - } - } - ); + if (frm.doc.document_type) { + frm.set_value("fields", []); + frm.events.sync_fields(frm, false); + } }, - set_button(frm) { + add_buttons(frm) { if (!frm.is_new()) { frm.add_custom_button(__("Go to {0} List", [frm.doc.name]), () => { window.open(`/app/${frappe.router.slug(frm.doc.name)}`); }); + + frm.add_custom_button(__("Sync {0} Fields", [frm.doc.name]), async () => { + await frm.events.sync_fields(frm, true); + }); + } + }, + + async sync_fields(frm, notify) { + frappe.dom.freeze("Fetching fields..."); + const response = await frm.call({ doc: frm.doc, method: "sync_fields" }); + frm.refresh_field("fields"); + frappe.dom.unfreeze(); + + if (!response.message) { + frappe.msgprint(__("No changes to sync")); + return; + } + + frm.dirty(); + if (notify) { + const addedFields = response.message.added; + const removedFields = response.message.removed; + + const getChangedMessage = (fields) => { + let changes = ""; + for (const field of fields) { + if (field.label) { + changes += `
  • Row #${field.idx}: ${field.fieldname.bold()} (${ + field.label + })
  • `; + } else { + changes += `
  • Row #${field.idx}: ${field.fieldname.bold()}
  • `; + } + } + return changes; + }; + + let message = ""; + + if (addedFields.length) { + message += `The following fields have been added:

    `; + } + + if (removedFields.length) { + message += `The following fields have been removed:

    `; + } + + if (message) { + frappe.msgprint({ + message: __(message), + indicator: "green", + title: __("Synced Fields"), + }); + } } }, }); diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.json b/frappe/custom/doctype/doctype_layout/doctype_layout.json index e47c9e03e0..0b627f78ce 100644 --- a/frappe/custom/doctype/doctype_layout/doctype_layout.json +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.json @@ -1,7 +1,7 @@ { "actions": [], "allow_rename": 1, - "autoname": "Prompt", + "autoname": "prompt", "creation": "2020-11-16 17:05:35.306846", "doctype": "DocType", "editable_grid": 1, @@ -19,7 +19,8 @@ "in_list_view": 1, "label": "Document Type", "options": "DocType", - "reqd": 1 + "reqd": 1, + "set_only_once": 1 }, { "fieldname": "fields", @@ -42,10 +43,11 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-10 15:01:04.352184", + "modified": "2022-09-01 03:22:33.973058", "modified_by": "Administrator", "module": "Custom", "name": "DocType Layout", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { @@ -68,5 +70,6 @@ "route": "doctype-layout", "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py index ea8e9acc99..778f2aa024 100644 --- a/frappe/custom/doctype/doctype_layout/doctype_layout.py +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py @@ -1,11 +1,70 @@ # Copyright (c) 2020, Frappe Technologies and contributors # License: MIT. See LICENSE +from typing import TYPE_CHECKING + +import frappe from frappe.desk.utils import slug from frappe.model.document import Document +if TYPE_CHECKING: + from frappe.core.doctype.docfield.docfield import DocField + class DocTypeLayout(Document): def validate(self): if not self.route: self.route = slug(self.name) + + @frappe.whitelist() + def sync_fields(self): + layout_fieldnames = {field.fieldname for field in self.fields} + doctype_fields = frappe.get_meta(self.document_type).fields + doctype_fieldnames = {field.fieldname for field in doctype_fields} + + added_fields = list(doctype_fieldnames - layout_fieldnames) + removed_fields = list(layout_fieldnames - doctype_fieldnames) + + if not (added_fields or removed_fields): + return + + added = self.add_fields(added_fields, doctype_fields) + removed = self.remove_fields(removed_fields) + + for index, field in enumerate(self.fields): + field.idx = index + 1 + + return {"added": added, "removed": removed} + + def add_fields(self, added_fields: list[str], doctype_fields: list["DocField"]) -> list[dict]: + added = [] + for field in added_fields: + field_details = next((f for f in doctype_fields if f.fieldname == field), None) + if not field_details: + continue + + # remove 'doctype' data from the DocField to allow adding it to the layout + row = self.append("fields", field_details.as_dict(no_default_fields=True)) + if field_details.insert_after: + insert_after = next( + (f for f in self.fields if f.fieldname == field_details.insert_after), + None, + ) + + # initialize new row to just after the insert_after field + self.fields.insert(insert_after.idx, row) + self.fields.pop() + + added.append({"idx": insert_after.idx + 1, "fieldname": row.fieldname, "label": row.label}) + else: + added.append(row.as_dict()) + return added + + def remove_fields(self, removed_fields: list[str]) -> list[dict]: + removed = [] + for field in removed_fields: + field_details = next((f for f in self.fields if f.fieldname == field), None) + if field_details: + self.remove(field_details) + removed.append(field_details.as_dict()) + return removed From 3203dc8c1fa051e80f0f46fab7cf4a7045a0abce Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 21 Sep 2022 12:15:03 +0530 Subject: [PATCH 002/152] fix: filter out inactive workers (#18200) [skip ci] --- frappe/core/doctype/rq_worker/rq_worker.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/rq_worker/rq_worker.py b/frappe/core/doctype/rq_worker/rq_worker.py index bc1326f522..b2d1f1209d 100644 --- a/frappe/core/doctype/rq_worker/rq_worker.py +++ b/frappe/core/doctype/rq_worker/rq_worker.py @@ -23,8 +23,10 @@ class RQWorker(Document): start = cint(args.get("start")) or 0 page_length = cint(args.get("page_length")) or 20 - workers = get_workers()[start : start + page_length] - return [serialize_worker(worker) for worker in workers] + workers = get_workers() + + valid_workers = [w for w in workers if w.pid][start : start + page_length] + return [serialize_worker(worker) for worker in valid_workers] @staticmethod def get_count(args) -> int: From 5cfcc4a075ad3a4fed3549888e75e98c314bba33 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Wed, 21 Sep 2022 16:20:44 +0530 Subject: [PATCH 003/152] fix: Chart date format on x-axis is inaccurate (#18191) --- .../desk/doctype/dashboard_chart/dashboard_chart.py | 2 +- .../doctype/dashboard_chart/test_dashboard_chart.py | 10 +++++----- frappe/utils/data.py | 12 ++++++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index a85f269c8f..6f675831a7 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -222,7 +222,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): return { "labels": [ - format_date(get_period(r[0], timegrain)) + format_date(get_period(r[0], timegrain), parse_day_first=True) if timegrain in ("Daily", "Weekly") else get_period(r[0], timegrain) for r in result diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index 820f3c0555..ddbabedcb4 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -173,7 +173,7 @@ class TestDashboardChart(FrappeTestCase): self.assertEqual(result.get("datasets")[0].get("values"), [200.0, 400.0, 300.0, 0.0, 100.0, 0.0]) self.assertEqual( result.get("labels"), - ["06-01-2019", "07-01-2019", "08-01-2019", "09-01-2019", "10-01-2019", "11-01-2019"], + ["01-06-2019", "01-07-2019", "01-08-2019", "01-09-2019", "01-10-2019", "01-11-2019"], ) def test_weekly_dashboard_chart(self): @@ -203,7 +203,7 @@ class TestDashboardChart(FrappeTestCase): result = get(chart_name="Test Weekly Dashboard Chart", refresh=1) self.assertEqual(result.get("datasets")[0].get("values"), [50.0, 300.0, 800.0, 0.0]) - self.assertEqual(result.get("labels"), ["12-30-2018", "06-01-2019", "01-13-2019", "01-20-2019"]) + self.assertEqual(result.get("labels"), ["12-30-2018", "01-06-2019", "01-13-2019", "01-20-2019"]) def test_avg_dashboard_chart(self): insert_test_records() @@ -230,7 +230,7 @@ class TestDashboardChart(FrappeTestCase): with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"): result = get(chart_name="Test Average Dashboard Chart", refresh=1) - self.assertEqual(result.get("labels"), ["12-30-2018", "06-01-2019", "01-13-2019", "01-20-2019"]) + self.assertEqual(result.get("labels"), ["12-30-2018", "01-06-2019", "01-13-2019", "01-20-2019"]) self.assertEqual(result.get("datasets")[0].get("values"), [50.0, 150.0, 266.6666666666667, 0.0]) def test_user_date_label_dashboard_chart(self): @@ -255,13 +255,13 @@ class TestDashboardChart(FrappeTestCase): with patch.object(frappe.utils.data, "get_user_date_format", return_value="dd.mm.yyyy"): result = get(chart_name="Test Dashboard Chart Date Label") self.assertEqual( - sorted(result.get("labels")), sorted(["01.05.2019", "01.12.2019", "19.01.2019"]) + sorted(result.get("labels")), sorted(["05.01.2019", "12.01.2019", "19.01.2019"]) ) with patch.object(frappe.utils.data, "get_user_date_format", return_value="mm-dd-yyyy"): result = get(chart_name="Test Dashboard Chart Date Label") self.assertEqual( - sorted(result.get("labels")), sorted(["01-19-2019", "05-01-2019", "12-01-2019"]) + sorted(result.get("labels")), sorted(["01-19-2019", "01-05-2019", "01-12-2019"]) ) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 6d4a96ce5f..9f794be0fc 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -72,7 +72,9 @@ def is_invalid_date_string(date_string: str) -> bool: ) -def getdate(string_date: Optional["DateTimeLikeObject"] = None) -> datetime.date | None: +def getdate( + string_date: Optional["DateTimeLikeObject"] = None, parse_day_first: bool = False +) -> datetime.date | None: """ Converts string date (yyyy-mm-dd) to datetime.date object. If no input is provided, current date is returned. @@ -91,7 +93,7 @@ def getdate(string_date: Optional["DateTimeLikeObject"] = None) -> datetime.date if is_invalid_date_string(string_date): return None try: - return parser.parse(string_date).date() + return parser.parse(string_date, dayfirst=parse_day_first).date() except ParserError: frappe.throw( frappe._("{} is not a valid date string.").format(frappe.bold(string_date)), @@ -548,7 +550,9 @@ def get_user_time_format() -> str: return frappe.local.user_time_format or "HH:mm:ss" -def format_date(string_date=None, format_string: str | None = None) -> str: +def format_date( + string_date=None, format_string: str | None = None, parse_day_first: bool = False +) -> str: """Converts the given string date to :data:`user_date_format` User format specified in defaults @@ -564,7 +568,7 @@ def format_date(string_date=None, format_string: str | None = None) -> str: if not string_date: return "" - date = getdate(string_date) + date = getdate(string_date, parse_day_first) if not format_string: format_string = get_user_date_format() format_string = format_string.replace("mm", "MM").replace("Y", "y") From 4263aace2750e903039a3122df2fea6083e190d0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 22 Sep 2022 14:52:26 +0530 Subject: [PATCH 004/152] chore: dont run cypress on forks --- .github/workflows/ui-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 4aa6ed0393..96d629fb90 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -37,7 +37,7 @@ jobs: test: runs-on: ubuntu-latest needs: checkrun - if: ${{ needs.checkrun.outputs.build == 'strawberry' }} + if: ${{ needs.checkrun.outputs.build == 'strawberry' && env.GITHUB_REPOSITORY_OWNER == 'frappe' }} timeout-minutes: 60 strategy: From 002b27dbddcefb4c6e63611c5d2637556b3b157a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 23 Sep 2022 15:46:31 +0530 Subject: [PATCH 005/152] fix: pypika does not parse datetime.time (backport #18184) (#18205) * fix: pypika does not parse datetime.time (cherry picked from commit b8f5a4304cccda954822cfe69d4805a18d9f07a7) * fix: style - imports sort order fixed (cherry picked from commit c3562c643ab9bff38d230202675d8ce1ae190912) * test: add test for query builder parsing datetime.time (cherry picked from commit 99889c270199da4b26b0d3858ae0f0a416d49d90) * fix: format_time instead of format_datetime, test was failing! (cherry picked from commit 5d697a22ac8cb98c8f8f44b3f99c57b932c5223c) * fix(style): linter issues (cherry picked from commit e231e1b0eadf63e84c9f81cfed9e2926e086fc44) * test: add test for postgres (cherry picked from commit 695591c43af9672cb83ce856a46c97e0bddfa9c3) * fix: converting datepart and timepart to strings for Combinedatetime (cherry picked from commit 1ebda943a607688bfb3db9fb42c80723a9050973) * fix: style, linter issues (cherry picked from commit c01262ad02b8831fdb0c5332b6973499e8adf6ad) Co-authored-by: Anoop Kurungadam Co-authored-by: Aradhya --- frappe/query_builder/functions.py | 4 +++ frappe/query_builder/terms.py | 9 ++++--- frappe/tests/test_query_builder.py | 43 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/frappe/query_builder/functions.py b/frappe/query_builder/functions.py index e725dff828..24e2ee0e5f 100644 --- a/frappe/query_builder/functions.py +++ b/frappe/query_builder/functions.py @@ -1,3 +1,4 @@ +from datetime import time from enum import Enum from pypika.functions import * @@ -47,6 +48,9 @@ Match = ImportMapper({db_type_is.MARIADB: MATCH, db_type_is.POSTGRES: TO_TSVECTO class _PostgresTimestamp(ArithmeticExpression): def __init__(self, datepart, timepart, alias=None): + """Postgres would need both datepart and timepart to be a string for concatenation""" + if isinstance(timepart, time) or isinstance(datepart, time): + timepart, datepart = str(timepart), str(datepart) if isinstance(datepart, str): datepart = Cast(datepart, "date") if isinstance(timepart, str): diff --git a/frappe/query_builder/terms.py b/frappe/query_builder/terms.py index 4d3c98b6a3..c1af0d9aa2 100644 --- a/frappe/query_builder/terms.py +++ b/frappe/query_builder/terms.py @@ -1,11 +1,11 @@ -from datetime import timedelta +from datetime import time, timedelta from typing import Any from pypika.queries import QueryBuilder from pypika.terms import Criterion, Function, ValueWrapper from pypika.utils import format_alias_sql -from frappe.utils.data import format_timedelta +from frappe.utils.data import format_time, format_timedelta class NamedParameterWrapper: @@ -55,9 +55,12 @@ class ParameterizedValueWrapper(ValueWrapper): value_sql = self.get_value_sql(quote_char=quote_char, **kwargs) sql = param_wrapper.get_sql(param_value=value_sql, **kwargs) else: - # * BUG: pypika doesen't parse timedeltas + # * BUG: pypika doesen't parse timedeltas and datetime.time if isinstance(self.value, timedelta): self.value = format_timedelta(self.value) + elif isinstance(self.value, time): + self.value = format_time(self.value) + sql = self.get_value_sql( quote_char=quote_char, secondary_quote_char=secondary_quote_char, diff --git a/frappe/tests/test_query_builder.py b/frappe/tests/test_query_builder.py index 06f2f5f6a7..b80e3ad8e2 100644 --- a/frappe/tests/test_query_builder.py +++ b/frappe/tests/test_query_builder.py @@ -1,5 +1,6 @@ import unittest from collections.abc import Callable +from datetime import time import frappe from frappe.query_builder import Case @@ -74,6 +75,26 @@ class TestCustomFunctionsMariaDB(FrappeTestCase): str(select_query).lower(), ) + def test_time(self): + note = frappe.qb.DocType("Note") + self.assertEqual( + "TIMESTAMP('2021-01-01','00:00:21')", CombineDatetime("2021-01-01", time(0, 0, 21)).get_sql() + ) + + select_query = frappe.qb.from_(note).select( + CombineDatetime(note.posting_date, note.posting_time) + ) + self.assertIn("select timestamp(`posting_date`,`posting_time`)", str(select_query).lower()) + + select_query = select_query.where( + CombineDatetime(note.posting_date, note.posting_time) + >= CombineDatetime("2021-01-01", time(0, 0, 1)) + ) + self.assertIn( + "timestamp(`posting_date`,`posting_time`)>=timestamp('2021-01-01','00:00:01')", + str(select_query).lower(), + ) + def test_cast(self): note = frappe.qb.DocType("Note") self.assertEqual("CONCAT(name,'')", Cast_(note.name, "varchar").get_sql()) @@ -141,6 +162,28 @@ class TestCustomFunctionsPostgres(FrappeTestCase): '"tabnote"."posting_date"+"tabnote"."posting_time" "timestamp"', str(select_query).lower() ) + def test_time(self): + note = frappe.qb.DocType("Note") + + self.assertEqual( + "CAST('2021-01-01' AS DATE)+CAST('00:00:21' AS TIME)", + CombineDatetime("2021-01-01", time(0, 0, 21)).get_sql(), + ) + + select_query = frappe.qb.from_(note).select( + CombineDatetime(note.posting_date, note.posting_time) + ) + self.assertIn('select "posting_date"+"posting_time"', str(select_query).lower()) + + select_query = select_query.where( + CombineDatetime(note.posting_date, note.posting_time) + >= CombineDatetime("2021-01-01", time(0, 0, 1)) + ) + self.assertIn( + """where "posting_date"+"posting_time">=cast('2021-01-01' as date)+cast('00:00:01' as time)""", + str(select_query).lower(), + ) + def test_cast(self): note = frappe.qb.DocType("Note") self.assertEqual("CAST(name AS VARCHAR)", Cast_(note.name, "varchar").get_sql()) From 967cc26faba46177aacb4ba48be4242d71095433 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 23 Sep 2022 16:46:33 +0530 Subject: [PATCH 006/152] feat: capture custom values in frappe monitor lets monitor add extra info for: - Save/Submit by doctype - Report generation time by standard report name --- .github/helper/db/mariadb.json | 1 + .../core/doctype/prepared_report/prepared_report.py | 3 +++ frappe/desk/form/save.py | 3 +++ frappe/desk/query_report.py | 3 +++ frappe/monitor.py | 11 +++++++++++ 5 files changed, 21 insertions(+) diff --git a/.github/helper/db/mariadb.json b/.github/helper/db/mariadb.json index 8bb654da66..e86e701dc3 100644 --- a/.github/helper/db/mariadb.json +++ b/.github/helper/db/mariadb.json @@ -13,5 +13,6 @@ "root_login": "root", "root_password": "travis", "host_name": "http://test_site:8000", + "monitor": 1, "server_script_enabled": true } diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index 0ff4ce3070..0b8e25229d 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -8,6 +8,7 @@ import frappe from frappe.desk.form.load import get_attachments from frappe.desk.query_report import generate_report_result from frappe.model.document import Document +from frappe.monitor import add_data_to_monitor from frappe.utils import gzip_compress, gzip_decompress from frappe.utils.background_jobs import enqueue @@ -25,6 +26,8 @@ def run_background(prepared_report): instance = frappe.get_doc("Prepared Report", prepared_report) report = frappe.get_doc("Report", instance.ref_report_doctype) + add_data_to_monitor(report=instance.ref_report_doctype) + try: report.custom_columns = [] diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py index f3e7b6294f..d709f7b592 100644 --- a/frappe/desk/form/save.py +++ b/frappe/desk/form/save.py @@ -5,6 +5,7 @@ import json import frappe from frappe.desk.form.load import run_onload +from frappe.monitor import add_data_to_monitor @frappe.whitelist() @@ -25,6 +26,8 @@ def savedocs(doc, action): run_onload(doc) send_updated_docs(doc) + add_data_to_monitor(doctype=doc.doctype, action=action) + frappe.msgprint(frappe._("Saved"), indicator="green", alert=True) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index e88a453e64..2971dbe9e0 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -12,6 +12,7 @@ from frappe import _ from frappe.core.utils import ljust_list 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.translate import send_translations from frappe.utils import ( @@ -254,6 +255,8 @@ def run( result["add_total_row"] = report.add_total_row and not result.get("skip_total_row", False) + add_data_to_monitor(report=report) + return result diff --git a/frappe/monitor.py b/frappe/monitor.py index 8d5391cb77..52e55e7559 100644 --- a/frappe/monitor.py +++ b/frappe/monitor.py @@ -25,6 +25,13 @@ def stop(response=None): frappe.local.monitor.dump(response) +def add_data_to_monitor(**kwargs) -> None: + """Add additional custom key-value pairs along with monitor log. + Note: Key-value pairs should be simple JSON exportable types.""" + if hasattr(frappe.local, "monitor"): + frappe.local.monitor.add_custom_data(**kwargs) + + def log_file(): return os.path.join(frappe.utils.get_bench_path(), "logs", "monitor.json.log") @@ -71,6 +78,10 @@ class Monitor: waitdiff = self.data.timestamp - job.enqueued_at self.data.job.wait = int(waitdiff.total_seconds() * 1000000) + def add_custom_data(self, **kwargs): + if self.data: + self.data.update(kwargs) + def dump(self, response=None): try: timediff = datetime.utcnow() - self.data.timestamp From 939bf872ae15e27d5195af967755f019c566f3a4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 24 Sep 2022 10:06:57 +0530 Subject: [PATCH 007/152] fix: use compact JSON representation for monitor --- .github/workflows/ui-tests.yml | 2 +- frappe/monitor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 96d629fb90..33c83bf5e2 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -37,7 +37,7 @@ jobs: test: runs-on: ubuntu-latest needs: checkrun - if: ${{ needs.checkrun.outputs.build == 'strawberry' && env.GITHUB_REPOSITORY_OWNER == 'frappe' }} + if: ${{ needs.checkrun.outputs.build == 'strawberry' && github.repository_owner == 'frappe' }} timeout-minutes: 60 strategy: diff --git a/frappe/monitor.py b/frappe/monitor.py index 52e55e7559..8db1e25d32 100644 --- a/frappe/monitor.py +++ b/frappe/monitor.py @@ -105,7 +105,7 @@ class Monitor: def store(self): if frappe.cache().llen(MONITOR_REDIS_KEY) > MONITOR_MAX_ENTRIES: frappe.cache().ltrim(MONITOR_REDIS_KEY, 1, -1) - serialized = json.dumps(self.data, sort_keys=True, default=str) + serialized = json.dumps(self.data, sort_keys=True, default=str, separators=(",", ":")) frappe.cache().rpush(MONITOR_REDIS_KEY, serialized) From 5330814d7ad5982fdd0c45237044a5a73c0d4ab7 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Sun, 25 Sep 2022 05:44:13 -0400 Subject: [PATCH 008/152] fix: make module to export required. (#18204) --- frappe/custom/doctype/customize_form/customize_form.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index a35db2ca18..8d8249b532 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -178,6 +178,7 @@ frappe.ui.form.on("Customize Form", { fieldname: "module", options: "Module Def", label: __("Module to Export"), + reqd: 1, }, { fieldtype: "Check", From 9c84d078faf22b049bf5280c2a2eb87653076902 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 26 Sep 2022 10:51:02 +0530 Subject: [PATCH 009/152] fix: remove mariadb specific comments from column MariaDB has additional syntax for comments which are like python. --- frappe/utils/data.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 9f794be0fc..8f0065b04c 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -45,6 +45,7 @@ URL_NOTATION_PATTERN = re.compile( ) # background-image: url('/assets/...') DURATION_PATTERN = re.compile(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$") HTML_TAG_PATTERN = re.compile("<[^>]+>") +MARIADB_SPECIFIC_COMMENT = re.compile(r"#.*") class Weekday(Enum): @@ -1827,8 +1828,11 @@ def sanitize_column(column_name: str) -> None: from frappe import _ - regex = re.compile("^.*[,'();].*") column_name = sqlparse.format(column_name, strip_comments=True, keyword_case="lower") + if frappe.db and frappe.db.db_type == "mariadb": + # strip mariadb specific comments which are like python single line comments + column_name = MARIADB_SPECIFIC_COMMENT.sub("", column_name) + blacklisted_keywords = [ "select", "create", @@ -1844,6 +1848,7 @@ def sanitize_column(column_name: str) -> None: def _raise_exception(): frappe.throw(_("Invalid field name {0}").format(column_name), frappe.DataError) + regex = re.compile("^.*[,'();].*") if "ifnull" in column_name: if regex.match(column_name): # to avoid and, or From c76277f1bf7426adc95a859b59dc0e2074577bd5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 26 Sep 2022 11:26:03 +0530 Subject: [PATCH 010/152] fix: correctly order args and kwargs --- frappe/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 359926b5b0..7845d814c6 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -456,7 +456,7 @@ def get_site_base_path(): def get_site_path(*path): - return get_path(base=get_site_base_path(), *path) + return get_path(*path, base=get_site_base_path()) def get_files_path(*path, **kwargs): From 6ba28061f6583fb3d2ebe07837ef27e0fa9ea371 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 26 Sep 2022 12:33:20 +0530 Subject: [PATCH 011/152] test: flaky API tests due to autherror (#18221) --- frappe/tests/test_commands.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index 9fd505a9c3..a7f1a7eeb9 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -413,6 +413,14 @@ class TestCommands(BaseTestCommands): self.execute("bench --site {site} set-admin-password test2") self.assertEqual(self.returncode, 0) self.assertEqual(check_password("Administrator", "test2"), "Administrator") + frappe.db.commit() + + # Reset it back to original password + original_password = frappe.conf.admin_password or "admin" + self.execute("bench --site {site} set-admin-password %s" % original_password) + self.assertEqual(self.returncode, 0) + self.assertEqual(check_password("Administrator", original_password), "Administrator") + frappe.db.commit() @skipIf( not ( From 56920e409aa3d6f2e164527f88de7ef145b36c22 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 26 Sep 2022 17:50:49 +0530 Subject: [PATCH 012/152] perf: send all translations for setup wizard (#18226) Computing all translations is far slower than just sending everything. This is how boot works too. --- frappe/desk/page/setup_wizard/setup_wizard.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 3422602720..408776fcb9 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -2,11 +2,10 @@ # License: MIT. See LICENSE import json -import os import frappe from frappe.geo.country_info import get_country_info -from frappe.translate import get_dict, send_translations, set_default_language +from frappe.translate import get_messages_for_boot, send_translations, set_default_language from frappe.utils import cint, strip from frappe.utils.password import update_password @@ -290,15 +289,7 @@ def load_messages(language): frappe.clear_cache() set_default_language(get_language_code(language)) frappe.db.commit() - m = get_dict("page", "setup-wizard") - - for path in frappe.get_hooks("setup_wizard_requires"): - # common folder `assets` served from `sites/` - js_file_path = os.path.abspath(frappe.get_site_path("..", *path.strip("/").split("/"))) - m.update(get_dict("jsfile", js_file_path)) - - m.update(get_dict("boot")) - send_translations(m) + send_translations(get_messages_for_boot()) return frappe.local.lang From 13e6aca115360a27bb9c198c20606b7b8394f1dc Mon Sep 17 00:00:00 2001 From: aliX40 <55487948+aliX40@users.noreply.github.com> Date: Mon, 26 Sep 2022 16:03:22 +0200 Subject: [PATCH 013/152] fix: chart label not being translated (#18212) * Fix chart translation issue also used .html() instead of .text() in line 58, because otherwise it resulted in html escape errors. In french ' is used in many words and when using the text() method it results in this string being inserted ' * refactor: use data attribute for semantic consistency Co-authored-by: Ankush Menat --- .../public/js/frappe/utils/dashboard_utils.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js index eb009ef76a..488aa20742 100644 --- a/frappe/public/js/frappe/utils/dashboard_utils.js +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -15,7 +15,7 @@ frappe.dashboard_utils = { let chart_filter_html = `