diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index e20ae8fc6e..351958de07 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -132,6 +132,7 @@ jobs: env: BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} AFTER: ${{ env.GITHUB_EVENT_PATH.after }} + FRAPPE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} TYPE: server DB: ${{ matrix.db }} @@ -142,6 +143,7 @@ jobs: SITE: test_site CI_BUILD_ID: ${{ github.run_id }} BUILD_NUMBER: ${{ matrix.container }} + FRAPPE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} TOTAL_BUILDS: 2 COVERAGE_RCFILE: /home/runner/frappe-bench/apps/frappe/.coveragerc diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 4742d97a37..cbc0f74470 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -120,6 +120,7 @@ jobs: env: BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} AFTER: ${{ env.GITHUB_EVENT_PATH.after }} + FRAPPE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} TYPE: ui DB: mariadb diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js index 03ef96783a..dff04a5693 100644 --- a/cypress/integration/awesome_bar.js +++ b/cypress/integration/awesome_bar.js @@ -2,7 +2,11 @@ context("Awesome Bar", () => { before(() => { cy.visit("/login"); cy.login(); - cy.visit("/app/website"); + cy.visit("/app/todo"); // Make sure ToDo filters are cleared. + cy.clear_filters(); + cy.visit("/app/blog-post"); // Make sure Blog Post filters are cleared. + cy.clear_filters(); + cy.visit("/app/website"); // Go to some other page. }); beforeEach(() => { @@ -11,36 +15,61 @@ context("Awesome Bar", () => { cy.get("@awesome_bar").type("{selectall}"); }); + after(() => { + cy.visit("/app/todo"); // Make sure we're not bleeding any filters to the next spec. + cy.clear_filters(); + }); + it("navigates to doctype list", () => { cy.get("@awesome_bar").type("todo"); - cy.wait(100); + cy.wait(100); // Wait a bit before hitting enter. cy.get(".awesomplete").findByRole("listbox").should("be.visible"); cy.get("@awesome_bar").type("{enter}"); cy.get(".title-text").should("contain", "To Do"); cy.location("pathname").should("eq", "/app/todo"); }); - it("find text in doctype list", () => { + it("finds text in doctype list", () => { cy.get("@awesome_bar").type("test in todo"); - cy.wait(100); + cy.wait(150); // Wait a bit before hitting enter. cy.get("@awesome_bar").type("{enter}"); cy.get(".title-text").should("contain", "To Do"); - cy.wait(200); - const name_filter = cy.get('[data-original-title="ID"] > input'); - name_filter.should("have.value", "%test%"); - cy.clear_filters(); + cy.wait(200); // Wait a bit longer before checking the filter. + cy.get('[data-original-title="ID"] > input').should("have.value", "%test%"); + }); + + it("filter preserved, now finds something else", () => { + cy.visit("/app/todo"); + cy.get(".title-text").should("contain", "To Do"); + cy.wait(200); // Wait a bit longer before checking the filter. + cy.get('[data-original-title="ID"] > input').as("filter"); + cy.get("@filter").should("have.value", "%test%"); + cy.get("@awesome_bar").type("anothertest in todo"); + cy.wait(200); // Wait a bit longer before hitting enter. + cy.get("@awesome_bar").type("{enter}"); + cy.wait(200); // Wait a bit longer before checking the filter. + cy.get("@filter").should("have.value", "%anothertest%"); + }); + + it("navigates to another doctype, filter not bleeding", () => { + cy.get("@awesome_bar").type("blog post"); + cy.wait(150); // Wait a bit before hitting enter. + cy.get("@awesome_bar").type("{enter}"); + cy.get(".title-text").should("contain", "Blog Post"); + cy.wait(200); // Wait a bit longer before checking the filter. + cy.location("search").should("be.empty"); }); it("navigates to new form", () => { cy.get("@awesome_bar").type("new blog post"); - cy.wait(100); + cy.wait(150); // Wait a bit before hitting enter cy.get("@awesome_bar").type("{enter}"); cy.get(".title-text:visible").should("have.text", "New Blog Post"); }); it("calculates math expressions", () => { cy.get("@awesome_bar").type("55 + 32"); - cy.wait(100); + cy.wait(150); // Wait a bit before hitting enter cy.get("@awesome_bar").type("{downarrow}{enter}"); cy.get(".modal-title").should("contain", "Result"); cy.get(".msgprint").should("contain", "55 + 32 = 87"); diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js index 0746f4460e..7f8123645d 100644 --- a/cypress/integration/control_link.js +++ b/cypress/integration/control_link.js @@ -152,7 +152,7 @@ context("Control Link", () => { cy.get(".frappe-control[data-fieldname=link] input").focus().as("input"); cy.wait("@search_link"); - cy.get("@input").type("todo for link"); + cy.get("@input").type("todo for link", { delay: 200 }); cy.wait("@search_link"); cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible"); cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 }); @@ -260,7 +260,7 @@ context("Control Link", () => { cy.get(".frappe-control[data-fieldname=link] input").focus().as("input"); cy.wait("@search_link"); - cy.get("@input").type("Sonstiges", { delay: 100 }); + cy.get("@input").type("Sonstiges", { delay: 200 }); cy.wait("@search_link"); cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible"); cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 }); @@ -291,7 +291,7 @@ context("Control Link", () => { cy.get(".frappe-control[data-fieldname=link] input").focus().as("input"); cy.wait("@search_link"); - cy.get("@input").type("Non-Conforming", { delay: 100 }); + cy.get("@input").type("Non-Conforming", { delay: 200 }); cy.wait("@search_link"); cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible"); cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 }); diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js index 955678a2d6..103b813013 100644 --- a/cypress/integration/control_phone.js +++ b/cypress/integration/control_phone.js @@ -6,6 +6,10 @@ context("Control Phone", () => { cy.visit("/app/website"); }); + afterEach(() => { + cy.clear_dialogs(); + }); + function get_dialog_with_phone() { return cy.dialog({ title: "Phone", @@ -20,31 +24,37 @@ context("Control Phone", () => { it("should set flag and data", () => { get_dialog_with_phone().as("dialog"); + cy.get(".selected-phone").click(); + cy.wait(100); cy.get(".phone-picker .phone-wrapper[id='afghanistan']").click(); + cy.wait(100); + cy.get(".selected-phone .country").should("have.text", "+93"); + cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/af.svg"); + cy.get(".selected-phone").click(); + cy.wait(100); cy.get(".phone-picker .phone-wrapper[id='india']").click(); + cy.wait(100); cy.get(".selected-phone .country").should("have.text", "+91"); cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg"); let phone_number = "9312672712"; cy.get(".selected-phone > img").click().first(); - cy.get_field("phone").first().click({ multiple: true }); + cy.get_field("phone").first().click(); cy.get(".frappe-control[data-fieldname=phone]") .findByRole("textbox") .first() - .type(phone_number, { force: true }); + .type(phone_number); cy.get_field("phone").first().should("have.value", phone_number); - cy.get_field("phone").first().blur({ force: true }); + cy.get_field("phone").first().blur(); cy.wait(100); cy.get("@dialog").then((dialog) => { let value = dialog.get_value("phone"); expect(value).to.equal("+91-" + phone_number); }); - }); - it("case insensitive search for country and clear search", () => { let search_text = "india"; cy.get(".selected-phone").click().first(); cy.get(".phone-picker").get(".search-phones").click().type(search_text); diff --git a/cypress/integration/form.js b/cypress/integration/form.js index facc73f536..73df3b1ab0 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -116,50 +116,6 @@ context("Form", () => { cy.get_field("location").should("have.value", "Bermuda"); }); - it("let user undo/redo field value changes", { scrollBehavior: false }, () => { - const undo = () => cy.get("body").type("{esc}").type("{ctrl+z}").wait(500); - const redo = () => cy.get("body").type("{esc}").type("{ctrl+y}").wait(500); - - cy.new_form("User"); - - jump_to_field("Email"); - type_value("admin@example.com"); - - jump_to_field("Username"); - type_value("admin42"); - - jump_to_field("Send Welcome Email"); - cy.focused().uncheck(); - - // make a mistake - jump_to_field("Username"); - type_value("admin24"); - - // undo behaviour - undo(); - cy.get_field("username").should("have.value", "admin42"); - - // redo behaviour - redo(); - cy.get_field("username").should("have.value", "admin24"); - - // undo everything & redo everything, ensure same values at the end - undo(); - undo(); - undo(); - undo(); - redo(); - redo(); - redo(); - redo(); - - cy.compare_document({ - username: "admin24", - email: "admin@example.com", - send_welcome_email: 0, - }); - }); - it("update docfield property using set_df_property in child table", () => { cy.visit("/app/contact/Test Form Contact 1"); cy.window() diff --git a/cypress/integration/multi_select_dialog.js b/cypress/integration/multi_select_dialog.js index 1be56d3b3d..7b30ad8c85 100644 --- a/cypress/integration/multi_select_dialog.js +++ b/cypress/integration/multi_select_dialog.js @@ -76,6 +76,11 @@ context("MultiSelectDialog", () => { }); it("tests more button", () => { + cy.get_open_dialog() + .get(`.frappe-control[data-fieldname="search_term"]`) + .find('input[data-fieldname="search_term"]') + .should("exist") + .type("Test", { delay: 200 }); cy.get_open_dialog() .get(`.frappe-control[data-fieldname="more_child_btn"]`) .should("exist") diff --git a/cypress/integration/permissions.js b/cypress/integration/permissions.js index 4e371cc17f..9c21a7914d 100644 --- a/cypress/integration/permissions.js +++ b/cypress/integration/permissions.js @@ -1,4 +1,4 @@ -context("Permissions API", () => { +context.skip("Permissions API", () => { before(() => { cy.visit("/login"); cy.remove_role("frappe@example.com", "System Manager"); diff --git a/cypress/integration/theme_switcher_dialog.js b/cypress/integration/theme_switcher_dialog.js deleted file mode 100644 index 53c3323a6d..0000000000 --- a/cypress/integration/theme_switcher_dialog.js +++ /dev/null @@ -1,37 +0,0 @@ -context("Theme Switcher Shortcut", () => { - before(() => { - cy.login(); - cy.visit("/app"); - }); - beforeEach(() => { - cy.reload(); - }); - it("Check Toggle", () => { - cy.open_theme_dialog(); - cy.get(".modal-backdrop").should("exist"); - cy.intercept("POST", "/api/method/frappe.core.doctype.user.user.switch_theme").as( - "set_theme" - ); - cy.findByText("Timeless Night").click(); - cy.wait("@set_theme"); - cy.close_theme("{ctrl+shift+g}"); - cy.get(".modal-backdrop").should("not.exist"); - }); - it("Check Enter", () => { - cy.open_theme_dialog(); - cy.intercept("POST", "/api/method/frappe.core.doctype.user.user.switch_theme").as( - "set_theme" - ); - cy.findByText("Frappe Light").click(); - cy.wait("@set_theme"); - cy.close_theme("{enter}"); - cy.get(".modal-backdrop").should("not.exist"); - }); -}); - -Cypress.Commands.add("open_theme_dialog", () => { - cy.get("body").type("{ctrl+shift+g}"); -}); -Cypress.Commands.add("close_theme", (shortcut_keys) => { - cy.get(".modal-header").type(shortcut_keys); -}); diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js deleted file mode 100644 index f12974d271..0000000000 --- a/cypress/integration/timeline.js +++ /dev/null @@ -1,91 +0,0 @@ -import custom_submittable_doctype from "../fixtures/custom_submittable_doctype"; - -context("Timeline", () => { - before(() => { - cy.visit("/login"); - cy.login(); - }); - - it("Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo", () => { - //Adding new ToDo - cy.new_form("ToDo"); - cy.get('[data-fieldname="description"] .ql-editor.ql-blank') - .type("Test ToDo", { force: true }) - .wait(200); - cy.get(".page-head .page-actions").findByRole("button", { name: "Save" }).click(); - - cy.go_to_list("ToDo"); - cy.clear_filters(); - cy.click_listview_row_item(0); - - //To check if the comment box is initially empty and tying some text into it - cy.get('[data-fieldname="comment"] .ql-editor') - .should("contain", "") - .type("Testing Timeline"); - - //Adding new comment - cy.get(".comment-box").findByRole("button", { name: "Comment" }).click(); - - //To check if the commented text is visible in the timeline content - cy.get(".timeline-content").should("contain", "Testing Timeline"); - - //Editing comment - cy.click_timeline_action_btn("Edit"); - cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(" 123"); - cy.click_timeline_action_btn("Save"); - - //To check if the edited comment text is visible in timeline content - cy.get(".timeline-content").should("contain", "Testing Timeline 123"); - - //Discarding comment - cy.click_timeline_action_btn("Edit"); - cy.click_timeline_action_btn("Dismiss"); - - //To check if after discarding the timeline content is same as previous - cy.get(".timeline-content").should("contain", "Testing Timeline 123"); - - //Deleting the added comment - cy.get(".timeline-message-box .more-actions > .action-btn").click(); //Menu button in timeline item - cy.get(".timeline-message-box .more-actions .dropdown-item") - .contains("Delete") - .click({ force: true }); - cy.get_open_dialog().findByRole("button", { name: "Yes" }).click({ force: true }); - - cy.get(".timeline-content").should("not.contain", "Testing Timeline 123"); - }); - - it("Timeline should have submit and cancel activity information", () => { - cy.visit("/app/doctype"); - - //Creating custom doctype - cy.insert_doc("DocType", custom_submittable_doctype, true); - - cy.visit("/app/custom-submittable-doctype"); - cy.click_listview_primary_button("Add Custom Submittable DocType"); - - //Adding a new entry for the created custom doctype - cy.fill_field("title", "Test"); - cy.click_modal_primary_button("Save"); - cy.click_modal_primary_button("Submit"); - - cy.visit("/app/custom-submittable-doctype"); - cy.click_listview_row_item(0); - - //To check if the submission of the documemt is visible in the timeline content - cy.get(".timeline-content").should("contain", "You submitted this document"); - cy.get('[id="page-Custom Submittable DocType"] .page-actions') - .findByRole("button", { name: "Cancel" }) - .click(); - cy.get_open_dialog().findByRole("button", { name: "Yes" }).click(); - - //To check if the cancellation of the documemt is visible in the timeline content - cy.get(".timeline-content").should("contain", "You cancelled this document"); - - //Deleting the document - cy.visit("/app/custom-submittable-doctype"); - cy.select_listview_row_checkbox(0); - cy.get(".page-actions").findByRole("button", { name: "Actions" }).click(); - cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click(); - cy.click_modal_primary_button("Yes"); - }); -}); diff --git a/frappe/__init__.py b/frappe/__init__.py index 6cb6e4a3d4..b59064e95d 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -43,7 +43,7 @@ from .utils.jinja import ( render_template, ) -__version__ = "15.0.0-dev" +__version__ = "16.0.0-dev" __title__ = "Frappe Framework" controllers = {} @@ -59,6 +59,32 @@ if _dev_server: warnings.simplefilter("always", DeprecationWarning) warnings.simplefilter("always", PendingDeprecationWarning) +# Always initialize sentry SDK if the DSN is sent +if sentry_dsn := os.getenv("FRAPPE_SENTRY_DSN"): + import sentry_sdk + from sentry_sdk.integrations.argv import ArgvIntegration + from sentry_sdk.integrations.atexit import AtexitIntegration + from sentry_sdk.integrations.dedupe import DedupeIntegration + from sentry_sdk.integrations.excepthook import ExcepthookIntegration + from sentry_sdk.integrations.modules import ModulesIntegration + + from frappe.utils.sentry import before_send + + sentry_sdk.init( + dsn=sentry_dsn, + before_send=before_send, + release=__version__, + auto_enabling_integrations=False, + default_integrations=False, + integrations=[ + AtexitIntegration(), + ExcepthookIntegration(), + DedupeIntegration(), + ModulesIntegration(), + ArgvIntegration(), + ], + ) + class _dict(dict): """dict like object that exposes keys as attributes""" @@ -163,6 +189,7 @@ if TYPE_CHECKING: # pragma: no cover from frappe.database.mariadb.database import MariaDBDatabase from frappe.database.postgres.database import PostgresDatabase + from frappe.email.doctype.email_queue.email_queue import EmailQueue from frappe.model.document import Document from frappe.query_builder.builder import MariaDB, Postgres from frappe.utils.redis_wrapper import RedisWrapper @@ -237,7 +264,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False) local.jloader = None local.cache = {} local.form_dict = _dict() - local.preload_assets = {"style": [], "script": []} + local.preload_assets = {"style": [], "script": [], "icons": []} local.session = _dict() local.dev_server = _dev_server local.qb = get_query_builder(local.conf.db_type) @@ -448,6 +475,8 @@ def msgprint( primary_action: str = None, is_minimizable: bool = False, wide: bool = False, + *, + realtime=False, ) -> None: """Print a message to the user (via HTTP response). Messages are sent in the `__server_messages` property in the @@ -461,6 +490,7 @@ def msgprint( :param primary_action: [optional] Bind a primary server/client side action. :param is_minimizable: [optional] Allow users to minimize the modal :param wide: [optional] Show wide modal + :param realtime: Publish message immediately using websocket. """ import inspect import sys @@ -527,7 +557,10 @@ def msgprint( if wide: out.wide = wide - message_log.append(out) + if realtime: + publish_realtime(event="msgprint", message=out) + else: + message_log.append(out) _raise_exception() @@ -659,7 +692,7 @@ def sendmail( print_letterhead=False, with_container=False, email_read_tracker_url=None, -): +) -> Optional["EmailQueue"]: """Send email using user's default **Email Account** or global default **Email Account**. @@ -744,7 +777,7 @@ def sendmail( ) # build email queue and send the email if send_now is True. - builder.process(send_now=now) + return builder.process(send_now=now) whitelisted = [] diff --git a/frappe/app.py b/frappe/app.py index 0284968113..5ddabfbfc9 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -22,7 +22,7 @@ import frappe.rate_limiter import frappe.recorder import frappe.utils.response from frappe import _ -from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest, validate_auth +from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest, validate_auth # noqa from frappe.middlewares import StaticDataMiddleware from frappe.utils import CallbackManager, cint, get_site_name from frappe.utils.data import escape_html diff --git a/frappe/commands/scheduler.py b/frappe/commands/scheduler.py index 5d453e3568..cf760cf4f0 100755 --- a/frappe/commands/scheduler.py +++ b/frappe/commands/scheduler.py @@ -202,7 +202,7 @@ def start_scheduler(): def start_worker( queue, quiet=False, rq_username=None, rq_password=None, burst=False, strategy=None ): - """Start a backgrond worker""" + """Start a background worker""" from frappe.utils.background_jobs import start_worker start_worker( @@ -225,7 +225,7 @@ def start_worker( @click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs") @click.option("--burst", is_flag=True, default=False, help="Run Worker in Burst mode.") def start_worker_pool(queue, quiet=False, num_workers=2, burst=False): - """Start a backgrond worker""" + """Start a pool of background workers""" from frappe.utils.background_jobs import start_worker_pool start_worker_pool( diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index 8de35fa622..3e15774fd1 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -31,6 +31,7 @@ "additional_info", "communication_date", "read_receipt", + "send_after", "column_break_14", "sender_full_name", "read_by_recipient", @@ -125,7 +126,7 @@ "fieldtype": "Select", "hidden": 1, "label": "Delivery Status", - "options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed\nError\nExpired\nSending\nRead" + "options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed\nError\nExpired\nSending\nRead\nScheduled" }, { "fieldname": "section_break_8", @@ -390,12 +391,17 @@ "hidden": 1, "label": "IMAP Folder", "read_only": 1 + }, + { + "fieldname": "send_after", + "fieldtype": "Datetime", + "label": "Send After" } ], "icon": "fa fa-comment", "idx": 1, "links": [], - "modified": "2023-08-29 17:20:52.541483", + "modified": "2023-11-27 20:38:27.467076", "modified_by": "Administrator", "module": "Core", "name": "Communication", diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index adf145a6c7..32500c9158 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -87,6 +87,7 @@ class Communication(Document, CommunicationEmailMixin): "Expired", "Sending", "Read", + "Scheduled", ] email_account: DF.Link | None email_status: DF.Literal["Open", "Spam", "Trash"] @@ -106,6 +107,7 @@ class Communication(Document, CommunicationEmailMixin): reference_name: DF.DynamicLink | None reference_owner: DF.ReadOnly | None seen: DF.Check + send_after: DF.Datetime | None sender: DF.Data | None sender_full_name: DF.Data | None sent_or_received: DF.Literal["Sent", "Received"] @@ -162,6 +164,9 @@ class Communication(Document, CommunicationEmailMixin): self.seen = 1 self.sent_or_received = "Sent" + if not self.send_after: # Handle empty string, always set NULL + self.send_after = None + validate_email(self) if self.communication_medium == "Email": @@ -342,6 +347,9 @@ class Communication(Document, CommunicationEmailMixin): else: self.status = "Closed" + if self.send_after and self.is_new(): + self.delivery_status = "Scheduled" + def mark_email_as_spam(self): if ( self.communication_type == "Communication" diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 5cb7bc668e..a0c9d35f20 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -46,6 +46,7 @@ def make( print_letterhead=True, email_template=None, communication_type=None, + send_after=None, **kwargs, ) -> dict[str, str]: """Make a new communication. Checks for email permissions for specified Document. @@ -64,6 +65,7 @@ def make( :param attachments: List of File names or dicts with keys "fname" and "fcontent" :param send_me_a_copy: Send a copy to the sender (default **False**). :param email_template: Template which is used to compose mail . + :param send_after: Send after the given datetime. """ if kwargs: from frappe.utils.commands import warn @@ -99,6 +101,7 @@ def make( email_template=email_template, communication_type=communication_type, add_signature=False, + send_after=send_after, ) @@ -124,6 +127,7 @@ def _make( email_template=None, communication_type=None, add_signature=True, + send_after=None, ) -> dict[str, str]: """Internal method to make a new communication that ignores Permission checks.""" @@ -151,6 +155,7 @@ def _make( "read_receipt": read_receipt, "has_attachment": 1 if attachments else 0, "communication_type": communication_type, + "send_after": send_after, } ) comm.flags.skip_add_signature = not add_signature diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index bbe5881d7a..81b882113b 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -290,6 +290,7 @@ class CommunicationEmailMixin: "read_receipt": self.read_receipt, "is_notification": (self.sent_or_received == "Received"), "print_letterhead": print_letterhead, + "send_after": self.send_after, } def send_email( diff --git a/frappe/core/doctype/file/utils.py b/frappe/core/doctype/file/utils.py index 9795c73d9e..a1b6612d4c 100644 --- a/frappe/core/doctype/file/utils.py +++ b/frappe/core/doctype/file/utils.py @@ -184,7 +184,7 @@ def remove_file_by_url(file_url: str, doctype: str = None, name: str = None) -> def get_content_hash(content: bytes | str) -> str: if isinstance(content, str): content = content.encode() - return hashlib.md5(content).hexdigest() # nosec + return hashlib.md5(content, usedforsecurity=False).hexdigest() # nosec def generate_file_name(name: str, suffix: str | None = None, is_private: bool = False) -> str: diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py index a32535e109..7b8deb6ef7 100644 --- a/frappe/core/doctype/log_settings/log_settings.py +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -10,20 +10,6 @@ from frappe.model.document import Document from frappe.utils import cint from frappe.utils.caching import site_cache -DEFAULT_LOGTYPES_RETENTION = { - "Error Log": 30, - "Activity Log": 90, - "Email Queue": 30, - "Scheduled Job Log": 90, - "Route History": 90, - "Submission Queue": 30, - "Prepared Report": 30, - "Webhook Request Log": 30, - "Integration Request": 90, - "Unhandled Email": 30, - "Reminder": 30, -} - @runtime_checkable class LogType(Protocol): @@ -81,12 +67,14 @@ class LogSettings(Document): def add_default_logtypes(self): existing_logtypes = {d.ref_doctype for d in self.logs_to_clear} added_logtypes = set() - for logtype, retention in DEFAULT_LOGTYPES_RETENTION.items(): + default_logtypes_retention = frappe.get_hooks("default_log_clearing_doctypes", {}) + + for logtype, retentions in default_logtypes_retention.items(): if logtype not in existing_logtypes and _supports_log_clearing(logtype): if not frappe.db.exists("DocType", logtype): continue - self.append("logs_to_clear", {"ref_doctype": logtype, "days": cint(retention)}) + self.append("logs_to_clear", {"ref_doctype": logtype, "days": cint(retentions[-1])}) added_logtypes.add(logtype) if added_logtypes: diff --git a/frappe/core/doctype/report/report.json b/frappe/core/doctype/report/report.json index f3e9450034..1761b0d574 100644 --- a/frappe/core/doctype/report/report.json +++ b/frappe/core/doctype/report/report.json @@ -62,6 +62,8 @@ "reqd": 1 }, { + "fetch_from": "ref_doctype.module", + "fetch_if_empty": 1, "fieldname": "module", "fieldtype": "Link", "label": "Module", diff --git a/frappe/core/doctype/sms_log/README.md b/frappe/core/doctype/sms_log/README.md new file mode 100644 index 0000000000..9ee2b79ef0 --- /dev/null +++ b/frappe/core/doctype/sms_log/README.md @@ -0,0 +1 @@ +Log of SMS sent via SMS Center. \ No newline at end of file diff --git a/frappe/core/doctype/sms_log/__init__.py b/frappe/core/doctype/sms_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/sms_log/sms_log.js b/frappe/core/doctype/sms_log/sms_log.js new file mode 100644 index 0000000000..ce036f234d --- /dev/null +++ b/frappe/core/doctype/sms_log/sms_log.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("SMS Log", { + refresh: function (frm) {}, +}); diff --git a/frappe/core/doctype/sms_log/sms_log.json b/frappe/core/doctype/sms_log/sms_log.json new file mode 100644 index 0000000000..1bdcec13ee --- /dev/null +++ b/frappe/core/doctype/sms_log/sms_log.json @@ -0,0 +1,371 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "SYS-SMS-.#####", + "beta": 0, + "creation": "2012-03-27 14:36:47", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "editable_grid": 0, + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sender_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Sender Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sent_on", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Sent On", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break0", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0, + "width": "50%" + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "message", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Message", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sec_break1", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "options": "Simple", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "no_of_requested_sms", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "No of Requested SMS", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "requested_numbers", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Requested Numbers", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break1", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0, + "width": "50%" + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "no_of_sent_sms", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "No of Sent SMS", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sent_to", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Sent To", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "fa fa-mobile-phone", + "idx": 1, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2018-08-21 16:15:40.898889", + "modified_by": "Administrator", + "module": "Core", + "name": "SMS Log", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/sms_log/sms_log.py b/frappe/core/doctype/sms_log/sms_log.py new file mode 100644 index 0000000000..8e4c248fd6 --- /dev/null +++ b/frappe/core/doctype/sms_log/sms_log.py @@ -0,0 +1,26 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +from frappe.model.document import Document + + +class SMSLog(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + message: DF.SmallText | None + no_of_requested_sms: DF.Int + no_of_sent_sms: DF.Int + requested_numbers: DF.Code | None + sender_name: DF.Data | None + sent_on: DF.Date | None + sent_to: DF.Code | None + # end: auto-generated types + + pass diff --git a/frappe/core/doctype/sms_log/test_sms_log.py b/frappe/core/doctype/sms_log/test_sms_log.py new file mode 100644 index 0000000000..3ff0202388 --- /dev/null +++ b/frappe/core/doctype/sms_log/test_sms_log.py @@ -0,0 +1,10 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import unittest + +# test_records = frappe.get_test_records('SMS Log') + + +class TestSMSLog(unittest.TestCase): + pass diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 54b23094f8..45fa621cec 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -23,38 +23,23 @@ "float_precision", "currency_precision", "rounding_method", - "sec_backup_limit", - "backup_limit", - "encrypt_backup", - "background_workers", - "enable_scheduler", - "dormant_days", "permissions", "apply_strict_user_permissions", "column_break_21", - "allow_guests_to_upload_files", - "force_web_capture_mode_for_uploads", + "allow_older_web_view_links", + "security_tab", "security", "session_expiry", "document_share_key_expiry", - "column_break_13", + "column_break_txqh", "deny_multiple_sessions", + "disable_user_pass_login", + "login_methods_section", "allow_login_using_mobile_number", "allow_login_using_user_name", - "disable_user_pass_login", + "column_break_uhqk", "login_with_email_link", "login_with_email_link_expiry", - "allow_error_traceback", - "strip_exif_metadata_from_uploaded_images", - "allow_older_web_view_links", - "password_settings", - "logout_on_password_reset", - "force_user_to_reset_password", - "reset_password_link_expiry_duration", - "password_reset_limit", - "column_break_31", - "enable_password_policy", - "minimum_password_score", "brute_force_security", "allow_consecutive_login_attempts", "column_break_34", @@ -66,6 +51,16 @@ "two_factor_method", "lifespan_qrcode_image", "otp_issuer_name", + "password_tab", + "password_settings", + "logout_on_password_reset", + "force_user_to_reset_password", + "reset_password_link_expiry_duration", + "password_reset_limit", + "column_break_31", + "enable_password_policy", + "minimum_password_score", + "email_tab", "email", "email_footer_address", "email_retry_limit", @@ -75,17 +70,31 @@ "attach_view_link", "welcome_email_template", "reset_password_template", - "prepared_report_section", - "max_auto_email_report_per_user", + "files_tab", + "files_section", + "max_file_size", + "allow_guests_to_upload_files", + "force_web_capture_mode_for_uploads", + "strip_exif_metadata_from_uploaded_images", + "column_break_uqma", + "allowed_file_extensions", + "updates_tab", "system_updates_section", "disable_system_update_notification", "disable_change_log_notification", + "backups_tab", + "sec_backup_limit", + "backup_limit", + "encrypt_backup", + "advanced_tab", + "prepared_report_section", + "max_auto_email_report_per_user", + "background_workers", + "enable_scheduler", + "dormant_days", "telemetry_section", - "enable_telemetry", - "files_section", - "max_file_size", - "column_break_uqma", - "allowed_file_extensions" + "allow_error_traceback", + "enable_telemetry" ], "fields": [ { @@ -126,7 +135,6 @@ "read_only": 1 }, { - "collapsible": 1, "fieldname": "date_and_number_format", "fieldtype": "Section Break", "label": "Date and Number Format" @@ -171,10 +179,8 @@ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9" }, { - "collapsible": 1, "fieldname": "sec_backup_limit", - "fieldtype": "Section Break", - "label": "Backups" + "fieldtype": "Section Break" }, { "default": "3", @@ -184,7 +190,6 @@ "label": "Number of Backups" }, { - "collapsible": 1, "fieldname": "background_workers", "fieldtype": "Section Break", "label": "Background Workers" @@ -198,7 +203,6 @@ "label": "Enable Scheduled Jobs" }, { - "collapsible": 1, "fieldname": "permissions", "fieldtype": "Section Break", "label": "Permissions" @@ -211,10 +215,8 @@ "label": "Apply Strict User Permissions" }, { - "collapsible": 1, "fieldname": "security", - "fieldtype": "Section Break", - "label": "Security" + "fieldtype": "Section Break" }, { "default": "170:00", @@ -223,10 +225,6 @@ "fieldtype": "Data", "label": "Session Expiry (idle timeout)" }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break" - }, { "default": "0", "description": "Note: Multiple sessions will be allowed in case of mobile device", @@ -255,7 +253,6 @@ "label": "Show Full Error and Allow Reporting of Issues to the Developer" }, { - "collapsible": 1, "fieldname": "password_settings", "fieldtype": "Section Break", "label": "Password" @@ -286,7 +283,6 @@ "options": "2\n3\n4" }, { - "collapsible": 1, "fieldname": "brute_force_security", "fieldtype": "Section Break", "label": "Brute Force Security" @@ -309,7 +305,6 @@ "label": "Allow Login After Fail" }, { - "collapsible": 1, "fieldname": "two_factor_authentication", "fieldtype": "Section Break", "label": "Two Factor Authentication" @@ -338,6 +333,7 @@ }, { "default": "OTP App", + "depends_on": "enable_two_factor_auth", "description": "Choose authentication method to be used by all users", "fieldname": "two_factor_method", "fieldtype": "Select", @@ -345,7 +341,7 @@ "options": "OTP App\nSMS\nEmail" }, { - "depends_on": "eval:doc.two_factor_method == \"OTP App\"", + "depends_on": "eval:doc.enable_two_factor_auth && doc.two_factor_method == \"OTP App\"", "description": "Time in seconds to retain QR code image on server. Min:240", "fieldname": "lifespan_qrcode_image", "fieldtype": "Int", @@ -359,10 +355,8 @@ "label": "OTP Issuer Name" }, { - "collapsible": 1, "fieldname": "email", - "fieldtype": "Section Break", - "label": "Email" + "fieldtype": "Section Break" }, { "description": "Your organization name and address for the email footer.", @@ -430,7 +424,6 @@ "label": "Include Web View Link in Email" }, { - "collapsible": 1, "fieldname": "prepared_report_section", "fieldtype": "Section Break", "label": "Reports" @@ -456,10 +449,8 @@ "label": "Encrypt Backups" }, { - "collapsible": 1, "fieldname": "system_updates_section", - "fieldtype": "Section Break", - "label": "System Updates" + "fieldtype": "Section Break" }, { "default": "0", @@ -547,7 +538,6 @@ "label": "Disable Document Sharing" }, { - "collapsible": 1, "fieldname": "telemetry_section", "fieldtype": "Section Break", "label": "Telemetry" @@ -578,10 +568,8 @@ "label": "Force Web Capture Mode for Uploads" }, { - "collapsible": 1, "fieldname": "files_section", - "fieldtype": "Section Break", - "label": "Files" + "fieldtype": "Section Break" }, { "fieldname": "max_file_size", @@ -598,12 +586,60 @@ "fieldname": "allowed_file_extensions", "fieldtype": "Small Text", "label": "Allowed File Extensions" + }, + { + "fieldname": "security_tab", + "fieldtype": "Tab Break", + "label": "Login" + }, + { + "fieldname": "email_tab", + "fieldtype": "Tab Break", + "label": "Email" + }, + { + "fieldname": "files_tab", + "fieldtype": "Tab Break", + "label": "Files" + }, + { + "fieldname": "updates_tab", + "fieldtype": "Tab Break", + "label": "Updates" + }, + { + "fieldname": "backups_tab", + "fieldtype": "Tab Break", + "label": "Backups" + }, + { + "fieldname": "advanced_tab", + "fieldtype": "Tab Break", + "label": "Advanced" + }, + { + "fieldname": "password_tab", + "fieldtype": "Tab Break", + "label": "Password" + }, + { + "fieldname": "column_break_txqh", + "fieldtype": "Column Break" + }, + { + "fieldname": "login_methods_section", + "fieldtype": "Section Break", + "label": "Login Methods" + }, + { + "fieldname": "column_break_uhqk", + "fieldtype": "Column Break" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2023-10-17 16:12:28.145496", + "modified": "2023-11-27 14:08:01.927794", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index 1c64a22e54..1a548b580b 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -93,7 +93,6 @@ class SystemSettings(Document): time_zone: DF.Literal two_factor_method: DF.Literal["OTP App", "SMS", "Email"] welcome_email_template: DF.Link | None - # end: auto-generated types def validate(self): from frappe.twofactor import toggle_two_factor_auth diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index d7c333ac44..028af756df 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1227,27 +1227,31 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False): contact_name = get_contact_name(user.email) if not contact_name: - contact = frappe.get_doc( - { - "doctype": "Contact", - "first_name": user.first_name, - "last_name": user.last_name, - "user": user.name, - "gender": user.gender, - } - ) + try: + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": user.first_name, + "last_name": user.last_name, + "user": user.name, + "gender": user.gender, + } + ) - if user.email: - contact.add_email(user.email, is_primary=True) + if user.email: + contact.add_email(user.email, is_primary=True) - if user.phone: - contact.add_phone(user.phone, is_primary_phone=True) + if user.phone: + contact.add_phone(user.phone, is_primary_phone=True) - if user.mobile_no: - contact.add_phone(user.mobile_no, is_primary_mobile_no=True) - contact.insert( - ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory - ) + if user.mobile_no: + contact.add_phone(user.mobile_no, is_primary_mobile_no=True) + + contact.insert( + ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory + ) + except frappe.DuplicateEntryError: + pass else: contact = frappe.get_doc("Contact", contact_name) contact.first_name = user.first_name diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index c64d690fbc..e2fb630af3 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -84,7 +84,7 @@ frappe.ui.form.on("Customize Form", { if (!in_list(["Table", "Table MultiSelect"], f.fieldtype)) return; frm.add_custom_button( - f.options, + __(f.options), () => frm.set_value("doc_type", f.options), __("Customize Child Table") ); @@ -97,7 +97,7 @@ frappe.ui.form.on("Customize Form", { if (frm.doc.doc_type) { frappe.model.with_doctype(frm.doc.doc_type).then(() => { - frm.page.set_title(__("Customize Form - {0}", [frm.doc.doc_type])); + frm.page.set_title(__("Customize Form - {0}", [__(frm.doc.doc_type)])); frappe.customize_form.set_primary_action(frm); frm.add_custom_button( diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index aebb143677..a3aec328bd 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -205,7 +205,7 @@ "label": "Permissions" }, { - "description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age>18", + "description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples):\nmyfield\neval:doc.myfield=='My Value'\neval:doc.age>18", "fieldname": "depends_on", "fieldtype": "Code", "label": "Depends On", diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py index 526543e825..27ffb4ffb8 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.py +++ b/frappe/desk/doctype/bulk_update/bulk_update.py @@ -46,8 +46,29 @@ class BulkUpdate(Document): @frappe.whitelist() def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None): - docnames = frappe.parse_json(docnames) + if isinstance(docnames, str): + docnames = frappe.parse_json(docnames) + if len(docnames) < 20: + return _bulk_action(doctype, docnames, action, data) + elif len(docnames) <= 500: + frappe.msgprint(_("Bulk operation is enqueued in background."), alert=True) + frappe.enqueue( + _bulk_action, + doctype=doctype, + docnames=docnames, + action=action, + data=data, + queue="short", + timeout=1000, + ) + else: + frappe.throw( + _("Bulk operations only support up to 500 documents."), title=_("Too Many Documents") + ) + + +def _bulk_action(doctype, docnames, action, data): if data: data = frappe.parse_json(data) @@ -85,5 +106,4 @@ def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None): def show_progress(docnames, message, i, description): n = len(docnames) - if n >= 10: - frappe.publish_progress(float(i) * 100 / n, title=message, description=description) + frappe.publish_progress(float(i) * 100 / n, title=message, description=description) diff --git a/frappe/desk/doctype/bulk_update/test_bulk_update.py b/frappe/desk/doctype/bulk_update/test_bulk_update.py new file mode 100644 index 0000000000..7611141a0a --- /dev/null +++ b/frappe/desk/doctype/bulk_update/test_bulk_update.py @@ -0,0 +1,48 @@ +# Copyright (c) 2023, Frappe Technologies and Contributors +# See LICENSE + +import time + +import frappe +from frappe.core.doctype.doctype.test_doctype import new_doctype +from frappe.desk.doctype.bulk_update.bulk_update import submit_cancel_or_update_docs +from frappe.tests.utils import FrappeTestCase, timeout + + +class TestBulkUpdate(FrappeTestCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.doctype = new_doctype(is_submittable=1, custom=1).insert().name + frappe.db.commit() + for _ in range(50): + frappe.new_doc(cls.doctype, some_fieldname=frappe.mock("name")).insert() + + @timeout() + def wait_for_assertion(self, assertion): + """Wait till an assertion becomes True""" + while True: + if assertion(): + break + time.sleep(0.2) + + def test_bulk_submit_in_background(self): + unsubmitted = frappe.get_all(self.doctype, {"docstatus": 0}, limit=5, pluck="name") + failed = submit_cancel_or_update_docs(self.doctype, unsubmitted, action="submit") + self.assertEqual(failed, []) + + def check_docstatus(docs, status): + frappe.db.rollback() + matching_docs = frappe.get_all( + self.doctype, {"docstatus": status, "name": ("in", docs)}, pluck="name" + ) + return set(matching_docs) == set(docs) + + unsubmitted = frappe.get_all(self.doctype, {"docstatus": 0}, limit=20, pluck="name") + submit_cancel_or_update_docs(self.doctype, unsubmitted, action="submit") + + self.wait_for_assertion(lambda: check_docstatus(unsubmitted, 1)) + + submitted = frappe.get_all(self.doctype, {"docstatus": 1}, limit=20, pluck="name") + submit_cancel_or_update_docs(self.doctype, submitted, action="cancel") + self.wait_for_assertion(lambda: check_docstatus(submitted, 2)) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 1c331a7fe8..059624d28f 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -209,10 +209,10 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): data = frappe.db.get_list( doctype, - fields=[f"{datefield} as _unit", f"SUM({value_field})", "COUNT(*)"], + fields=[datefield, f"SUM({value_field})", "COUNT(*)"], filters=filters, - group_by="_unit", - order_by="_unit asc", + group_by=datefield, + order_by=datefield, as_list=True, ) diff --git a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json index 854305ad80..e47487eaaf 100644 --- a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json +++ b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json @@ -102,8 +102,7 @@ "fieldname": "url", "fieldtype": "Data", "in_list_view": 1, - "label": "URL", - "options": "URL" + "label": "URL" }, { "depends_on": "eval:doc.doc_view == \"Kanban\"", @@ -116,7 +115,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-07-18 16:12:53.546430", + "modified": "2023-11-27 14:13:38.489737", "modified_by": "Administrator", "module": "Desk", "name": "Workspace Shortcut", diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 8d42b804cd..c9f7929b28 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -196,7 +196,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { this.abort_setup(r.message.fail); } }, - error: () => this.abort_setup("Error in setup"), + error: () => this.abort_setup(), }); } @@ -213,7 +213,11 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { abort_setup(fail_msg) { this.$working_state.find(".state-icon-container").html(""); - fail_msg = fail_msg ? fail_msg : __("Failed to complete setup"); + fail_msg = fail_msg + ? fail_msg + : frappe.last_response.setup_wizard_failure_message + ? frappe.last_response.setup_wizard_failure_message + : __("Failed to complete setup"); this.update_setup_message("Could not start up: " + fail_msg); @@ -463,7 +467,7 @@ frappe.setup.slides_settings = [ fieldtype: "Data", options: "Email", }, - { fieldname: "password", label: __("Password"), fieldtype: "Password" }, + { fieldname: "password", label: __("Password"), fieldtype: "Password", length: 512 }, ], onload: function (slide) { diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 3a2b369a23..e3002e89a5 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -83,11 +83,14 @@ def process_setup_stages(stages, user_input, is_background_task=False): task.get("fn")(task.get("args")) except Exception: handle_setup_exception(user_input) + message = current_task.get("fail_msg") if current_task else "Failed to complete setup" + frappe.log_error(title=f"Setup failed: {message}") if not is_background_task: - return {"status": "fail", "fail": current_task.get("fail_msg")} + frappe.response["setup_wizard_failure_message"] = message + raise frappe.publish_realtime( "setup_task", - {"status": "fail", "fail_msg": current_task.get("fail_msg")}, + {"status": "fail", "fail_msg": message}, user=frappe.session.user, ) else: diff --git a/frappe/email/doctype/email_account/email_account.js b/frappe/email/doctype/email_account/email_account.js index 90f71bf88f..5eb874633d 100644 --- a/frappe/email/doctype/email_account/email_account.js +++ b/frappe/email/doctype/email_account/email_account.js @@ -159,6 +159,15 @@ frappe.ui.form.on("Email Account", { delete frappe.route_flags.delete_user_from_locals; delete locals["User"][frappe.route_flags.linked_user]; } + + if (frappe.boot.developer_mode && !frm.is_dirty() && frm.doc.enable_incoming) { + frm.add_custom_button(__("Pull Emails"), () => { + frm.call({ + method: "pull_emails", + args: { email_account: frm.doc.name }, + }); + }); + } }, authorize_api_access: function (frm) { diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 9bcf328116..d7c75e03a1 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -831,6 +831,14 @@ def pull(now=False): ) +@frappe.whitelist() +def pull_emails(email_account: str) -> None: + """Pull emails from given email account.""" + frappe.has_permission("Email Account", "read", throw=True) + + pull_from_email_account(email_account) + + def pull_from_email_account(email_account): """Runs within a worker process""" email_account = frappe.get_doc("Email Account", email_account) diff --git a/frappe/email/doctype/email_group/email_group.py b/frappe/email/doctype/email_group/email_group.py index c866beebee..f365fa2fb6 100755 --- a/frappe/email/doctype/email_group/email_group.py +++ b/frappe/email/doctype/email_group/email_group.py @@ -1,6 +1,8 @@ # Copyright (c) 2015, Frappe Technologies and contributors # License: MIT. See LICENSE +import contextlib + import frappe from frappe import _ from frappe.model.document import Document @@ -41,7 +43,7 @@ class EmailGroup(Document): added = 0 for user in frappe.get_all(doctype, [email_field, unsubscribed_field or "name"]): - try: + with contextlib.suppress(frappe.UniqueValidationError, frappe.InvalidEmailAddressError): email = parse_addr(user.get(email_field))[1] if user.get(email_field) else None if email: frappe.get_doc( @@ -52,10 +54,7 @@ class EmailGroup(Document): "unsubscribed": user.get(unsubscribed_field) if unsubscribed_field else 0, } ).insert(ignore_permissions=True) - added += 1 - except frappe.UniqueValidationError: - pass frappe.msgprint(_("{0} subscribers added").format(added)) diff --git a/frappe/email/doctype/email_group_member/email_group_member.json b/frappe/email/doctype/email_group_member/email_group_member.json index 0e32135b72..0d68674101 100644 --- a/frappe/email/doctype/email_group_member/email_group_member.json +++ b/frappe/email/doctype/email_group_member/email_group_member.json @@ -28,6 +28,7 @@ "in_global_search": 1, "in_list_view": 1, "label": "Email", + "options": "Email", "reqd": 1 }, { @@ -40,7 +41,7 @@ } ], "links": [], - "modified": "2022-07-11 16:38:34.165271", + "modified": "2023-11-25 16:54:59.828669", "modified_by": "Administrator", "module": "Email", "name": "Email Group Member", diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index da10ae5d16..828ae2e419 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -687,13 +687,13 @@ class QueueBuilder: mail.set_in_reply_to(self.in_reply_to) return mail - def process(self, send_now=False): + def process(self, send_now=False) -> EmailQueue | None: """Build and return the email queues those are created. Sends email incase if it is requested to send now. """ final_recipients = self.final_recipients() - queue_separately = (final_recipients and self.queue_separately) or len(final_recipients) > 20 + queue_separately = (final_recipients and self.queue_separately) or len(final_recipients) > 100 if not (final_recipients + self.final_cc()): return [] @@ -705,6 +705,7 @@ class QueueBuilder: recipients = list(set(final_recipients + self.final_cc() + self.bcc)) q = EmailQueue.new({**queue_data, **{"recipients": recipients}}, ignore_permissions=True) send_now and q.send() + return q else: if send_now and len(final_recipients) >= 1000: # force queueing if there are too many recipients to avoid timeouts diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 5aabe3d85e..2c79f13541 100644 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -50,10 +50,10 @@ class Newsletter(WebsiteGenerator): total_recipients: DF.Int total_views: DF.Int # end: auto-generated types + def validate(self): self.route = f"newsletters/{self.name}" self.validate_sender_address() - self.validate_recipient_address() self.validate_publishing() self.validate_scheduling_date() @@ -135,7 +135,6 @@ class Newsletter(WebsiteGenerator): def validate_newsletter_recipients(self): if not self.newsletter_recipients: frappe.throw(_("Newsletter should have atleast one recipient"), exc=NoRecipientFoundError) - self.validate_recipient_address() def validate_sender_address(self): """Validate self.send_from is a valid email address or not.""" @@ -145,11 +144,6 @@ class Newsletter(WebsiteGenerator): f"{self.sender_name} <{self.sender_email}>" if self.sender_name else self.sender_email ) - def validate_recipient_address(self): - """Validate if self.newsletter_recipients are all valid email addresses or not.""" - for recipient in self.newsletter_recipients: - frappe.utils.validate_email_address(recipient, throw=True) - def validate_publishing(self): if self.send_webview_link and not self.published: frappe.throw(_("Newsletter must be published to send webview link in email")) @@ -308,11 +302,11 @@ def confirmed_unsubscribe(email, group): @frappe.whitelist(allow_guest=True) @rate_limit(limit=10, seconds=60 * 60) -def subscribe(email, email_group=None): # noqa +def subscribe(email, email_group=None): """API endpoint to subscribe an email to a particular email group. Triggers a confirmation email.""" if email_group is None: - email_group = _("Website") + email_group = get_default_email_group() # build subscription confirmation URL api_endpoint = frappe.utils.get_url( @@ -355,13 +349,16 @@ def subscribe(email, email_group=None): # noqa @frappe.whitelist(allow_guest=True) -def confirm_subscription(email, email_group=_("Website")): # noqa +def confirm_subscription(email, email_group=None): """API endpoint to confirm email subscription. This endpoint is called when user clicks on the link sent to their mail. """ if not verify_request(): return + if email_group is None: + email_group = get_default_email_group() + if not frappe.db.exists("Email Group", email_group): frappe.get_doc({"doctype": "Email Group", "title": email_group}).insert(ignore_permissions=True) @@ -438,3 +435,7 @@ def newsletter_email_read(recipient_email=None, reference_doctype=None, referenc finally: frappe.response.update(frappe.utils.get_imaginary_pixel_response()) + + +def get_default_email_group(): + return _("Website", lang=frappe.db.get_default("language")) diff --git a/frappe/hooks.py b/frappe/hooks.py index e134a2a660..ec7436ac19 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -31,6 +31,7 @@ app_include_js = [ "report.bundle.js", "telemetry.bundle.js", ] + app_include_css = [ "desk.bundle.css", "report.bundle.css", @@ -436,6 +437,22 @@ after_job = [ extend_bootinfo = [ "frappe.utils.telemetry.add_bootinfo", "frappe.core.doctype.user_permission.user_permission.send_user_permissions", + "frappe.utils.sentry.add_bootinfo", ] export_python_type_annotations = True + +# log doctype cleanups to automatically add in log settings +default_log_clearing_doctypes = { + "Error Log": 30, + "Activity Log": 90, + "Email Queue": 30, + "Scheduled Job Log": 90, + "Route History": 90, + "Submission Queue": 30, + "Prepared Report": 30, + "Webhook Request Log": 30, + "Integration Request": 90, + "Unhandled Email": 30, + "Reminder": 30, +} diff --git a/frappe/installer.py b/frappe/installer.py index 5d807f0405..89b52e1d4e 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -67,7 +67,12 @@ def _new_site( if not db_name: import hashlib - db_name = "_" + hashlib.sha1(os.path.realpath(frappe.get_site_path()).encode()).hexdigest()[:16] + db_name = ( + "_" + + hashlib.sha1( + os.path.realpath(frappe.get_site_path()).encode(), usedforsecurity=False + ).hexdigest()[:16] + ) try: # enable scheduler post install? diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index d6b173d040..d571b2ba00 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -48,8 +48,7 @@ class ConnectedApp(Document): def validate(self): base_url = frappe.utils.get_url() callback_path = ( - "/api/method/frappe.integrations.doctype.connected_app.connected_app.callback" - + f"?app={self.name}" + "/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/" + self.name ) self.redirect_uri = urljoin(base_url, callback_path) @@ -149,7 +148,7 @@ class ConnectedApp(Document): @frappe.whitelist(methods=["GET"], allow_guest=True) -def callback(code=None, state=None, app=None): +def callback(code=None, state=None): """Handle client's code. Called during the oauthorization flow by the remote oAuth2 server to @@ -162,7 +161,11 @@ def callback(code=None, state=None, app=None): frappe.local.response["location"] = "/login?" + urlencode({"redirect-to": frappe.request.url}) return - connected_app = frappe.get_doc("Connected App", app) + path = frappe.request.path[1:].split("/") + if len(path) != 4 or not path[3]: + frappe.throw(_("Invalid Parameters.")) + + connected_app = frappe.get_doc("Connected App", path[3]) token_cache = frappe.get_doc("Token Cache", connected_app.name + "-" + frappe.session.user) if state != token_cache.state: diff --git a/frappe/integrations/doctype/webhook/__init__.py b/frappe/integrations/doctype/webhook/__init__.py index dcad1c8b5c..1cd08aeca1 100644 --- a/frappe/integrations/doctype/webhook/__init__.py +++ b/frappe/integrations/doctype/webhook/__init__.py @@ -4,60 +4,45 @@ import frappe +def get_all_webhooks(): + # query webhooks + webhooks_list = frappe.get_all( + "Webhook", + fields=["name", "condition", "webhook_docevent", "webhook_doctype"], + filters={"enabled": True}, + ) + + # make webhooks map + webhooks = {} + for w in webhooks_list: + webhooks.setdefault(w.webhook_doctype, []).append(w) + + return webhooks + + def run_webhooks(doc, method): """Run webhooks for this method""" + + frappe_flags = frappe.local.flags + if ( - frappe.flags.in_import - or frappe.flags.in_patch - or frappe.flags.in_install - or frappe.flags.in_migrate + frappe_flags.in_import + or frappe_flags.in_patch + or frappe_flags.in_install + or frappe_flags.in_migrate ): return - if frappe.flags.webhooks_executed is None: - frappe.flags.webhooks_executed = {} - - # TODO: remove this hazardous unnecessary cache in flags - if frappe.flags.webhooks is None: - # load webhooks from cache - webhooks = frappe.cache.get_value("webhooks") - if webhooks is None: - # query webhooks - webhooks_list = frappe.get_all( - "Webhook", - fields=["name", "condition", "webhook_docevent", "webhook_doctype"], - filters={"enabled": True}, - ) - - # make webhooks map for cache - webhooks = {} - for w in webhooks_list: - webhooks.setdefault(w.webhook_doctype, []).append(w) - frappe.cache.set_value("webhooks", webhooks) - - frappe.flags.webhooks = webhooks + # load all webhooks from cache / DB + webhooks = frappe.cache.get_value("webhooks", get_all_webhooks) # get webhooks for this doctype - webhooks_for_doc = frappe.flags.webhooks.get(doc.doctype, None) + webhooks_for_doc = webhooks.get(doc.doctype, None) if not webhooks_for_doc: # no webhooks, quit return - def _webhook_request(webhook): - if webhook.name not in frappe.flags.webhooks_executed.get(doc.name, []): - frappe.enqueue( - "frappe.integrations.doctype.webhook.webhook.enqueue_webhook", - enqueue_after_commit=True, - doc=doc, - webhook=webhook, - ) - - # keep list of webhooks executed for this doc in this request - # so that we don't run the same webhook for the same document multiple times - # in one request - frappe.flags.webhooks_executed.setdefault(doc.name, []).append(webhook.name) - event_list = ["on_update", "after_insert", "on_submit", "on_cancel", "on_trash"] if not doc.flags.in_insert: @@ -76,4 +61,52 @@ def run_webhooks(doc, method): trigger_webhook = True if trigger_webhook and event and webhook.webhook_docevent == event: - _webhook_request(webhook) + _add_webhook_to_queue(webhook, doc) + + +def _add_webhook_to_queue(webhook, doc): + # Maintain a queue and flush on commit + if not getattr(frappe.local, "_webhook_queue", None): + frappe.local._webhook_queue = [] + frappe.db.after_commit.add(flush_webhook_execution_queue) + + frappe.local._webhook_queue.append(frappe._dict(doc=doc, webhook=webhook)) + + +def flush_webhook_execution_queue(): + """Enqueue all pending webhook executions. + + Each webhook can trigger multiple times on same document or even different instance of same + document. We assume that last enqueued version of document is the final document for this DB + transaction. + """ + if not getattr(frappe.local, "_webhook_queue", None): + return + + uniq_hooks = set() + unique_last_instances = [] + + # reverse + frappe.local._webhook_queue.reverse() + + # deduplicate on (doc.name, webhook.name) + # 'doc' holds the last instance values + for execution in frappe.local._webhook_queue: + key = (execution.webhook.get("name"), execution.doc.get("name")) + if key not in uniq_hooks: + uniq_hooks.add(key) + unique_last_instances.append(execution) + + # Clear original queue so next enqueue computation happens correctly. + del frappe.local._webhook_queue + + # reverse again, to get back the original order on which to execute webhooks + unique_last_instances.reverse() + + for instance in unique_last_instances: + frappe.enqueue( + "frappe.integrations.doctype.webhook.webhook.enqueue_webhook", + doc=instance.doc, + webhook=instance.webhook, + now=frappe.flags.in_test, + ) diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py index d308ec95ab..c0148f5f67 100644 --- a/frappe/integrations/doctype/webhook/test_webhook.py +++ b/frappe/integrations/doctype/webhook/test_webhook.py @@ -7,6 +7,7 @@ import responses from responses.matchers import json_params_matcher import frappe +from frappe.integrations.doctype.webhook import flush_webhook_execution_queue from frappe.integrations.doctype.webhook.webhook import ( enqueue_webhook, get_webhook_data, @@ -96,6 +97,7 @@ class TestWebhook(FrappeTestCase): self.test_user = frappe.new_doc("User") self.test_user.email = "user1@integration.webhooks.test.com" self.test_user.first_name = "user1" + self.test_user.send_welcome_email = False self.responses = responses.RequestsMock() self.responses.start() @@ -112,18 +114,19 @@ class TestWebhook(FrappeTestCase): """Test webhook trigger for enabled webhooks""" frappe.cache.delete_value("webhooks") - frappe.flags.webhooks = None # Insert the user to db self.test_user.insert() - self.assertTrue("User" in frappe.flags.webhooks) + webhooks = frappe.cache.get_value("webhooks") + self.assertTrue("User" in webhooks) + self.assertEqual(len(webhooks.get("User")), 1) + # only 1 hook (enabled) must be queued - self.assertEqual(len(frappe.flags.webhooks.get("User")), 1) - self.assertTrue(self.test_user.email in frappe.flags.webhooks_executed) - self.assertEqual( - frappe.flags.webhooks_executed.get(self.test_user.email)[0], self.sample_webhooks[0].name - ) + self.assertEqual(len(frappe.local._webhook_queue), 1) + execution = frappe.local._webhook_queue[0] + self.assertEqual(execution.webhook.name, self.sample_webhooks[0].name) + self.assertEqual(execution.doc.name, self.test_user.name) def test_validate_doc_events(self): "Test creating a submit-related webhook for a non-submittable DocType" @@ -206,7 +209,7 @@ class TestWebhook(FrappeTestCase): wh_config = { "doctype": "Webhook", "webhook_doctype": "Note", - "webhook_docevent": "after_insert", + "webhook_docevent": "on_change", "enabled": 1, "request_url": "https://httpbin.org/post", "request_method": "POST", @@ -223,8 +226,9 @@ class TestWebhook(FrappeTestCase): doc = frappe.new_doc("Note") doc.title = "Test Webhook Note" + final_title = frappe.generate_hash() - expected_req = [{"title": doc.title} for _ in range(3)] + expected_req = [{"title": final_title} for _ in range(3)] self.responses.add( responses.POST, "https://httpbin.org/post", @@ -233,8 +237,15 @@ class TestWebhook(FrappeTestCase): match=[json_params_matcher(expected_req)], ) - with get_test_webhook(wh_config) as wh: - enqueue_webhook(doc, wh) + with get_test_webhook(wh_config): + # It should only execute once in a transaction + doc.insert() + doc.reload() + doc.save() + doc = frappe.get_doc(doc.doctype, doc.name) + doc.title = final_title + doc.save() + flush_webhook_execution_queue() log = frappe.get_last_doc("Webhook Request Log") self.assertEqual(len(json.loads(log.response)), 3) diff --git a/frappe/integrations/utils.py b/frappe/integrations/utils.py index 9e80a9aa34..86f8b0b1ef 100644 --- a/frappe/integrations/utils.py +++ b/frappe/integrations/utils.py @@ -43,6 +43,14 @@ def make_put_request(url, **kwargs): return make_request("PUT", url, **kwargs) +def make_patch_request(url, **kwargs): + return make_request("PATCH", url, **kwargs) + + +def make_delete_request(url, **kwargs): + return make_request("DELETE", url, **kwargs) + + def create_request_log( data, integration_type=None, diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index bdf354ce99..42c575371e 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -344,18 +344,17 @@ class BaseDocument: if ignore_virtual or fieldname not in self.permitted_fieldnames: continue - if value is None: - if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop): - value = getattr(self, fieldname) + if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop): + value = getattr(self, fieldname) - elif options := getattr(df, "options", None): - from frappe.utils.safe_exec import get_safe_globals + elif options := getattr(df, "options", None): + from frappe.utils.safe_exec import get_safe_globals - value = frappe.safe_eval( - code=options, - eval_globals=get_safe_globals(), - eval_locals={"doc": self}, - ) + value = frappe.safe_eval( + code=options, + eval_globals=get_safe_globals(), + eval_locals={"doc": self}, + ) if isinstance(value, list) and df.fieldtype not in table_fields: frappe.throw(_("Value for {0} cannot be a list").format(_(df.label))) diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py index ade401a83c..7363bf4583 100644 --- a/frappe/model/mapper.py +++ b/frappe/model/mapper.py @@ -152,6 +152,9 @@ def get_mapped_doc( True if target_doc.get(target_parentfield) else False ) + if table_map.get("ignore"): + continue + if table_map.get("add_if_empty") and row_exists_for_parentfield.get(target_parentfield): continue @@ -163,6 +166,7 @@ def get_mapped_doc( if postprocess: postprocess(source_doc, target_doc) + ret_doc.run_method("after_mapping", source_doc) ret_doc.set_onload("load_after_mapping", True) if ( diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 267a1667b5..d77671808d 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -153,8 +153,11 @@ def remove_orphan_doctypes(): orphan_doctypes = [] clear_controller_cache() + class_overrides = frappe.get_hooks("override_doctype_class", {}) for doctype in doctype_names: + if doctype in class_overrides: + continue try: get_controller(doctype=doctype) except ImportError: diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 0d7ce13d95..cc51a55d90 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import json +from collections import defaultdict from typing import TYPE_CHECKING, Union import frappe @@ -233,17 +234,30 @@ def get_workflow_field_value(workflow_name, field): @frappe.whitelist() def bulk_workflow_approval(docnames, doctype, action): - from collections import defaultdict + docnames = json.loads(docnames) + if len(docnames) < 20: + _bulk_workflow_action(docnames, doctype, action) + elif len(docnames) <= 500: + frappe.msgprint(_("Bulk {0} is enqueued in background.").format(action), alert=True) + frappe.enqueue( + _bulk_workflow_action, + docnames=docnames, + doctype=doctype, + action=action, + queue="short", + timeout=1000, + ) + else: + frappe.throw(_("Bulk approval only support up to 500 documents."), title=_("Too Many Documents")) + + +def _bulk_workflow_action(docnames, doctype, action): # dictionaries for logging failed_transactions = defaultdict(list) successful_transactions = defaultdict(list) - # WARN: message log is cleared - print("Clearing frappe.message_log...") frappe.clear_messages() - - docnames = json.loads(docnames) for (idx, docname) in enumerate(docnames, 1): message_dict = {} try: @@ -308,7 +322,9 @@ def print_workflow_log(messages, title, doctype, indicator): html = f"
{doc}
" msg += html - frappe.msgprint(msg, title=_("Workflow Status"), indicator=indicator, is_minimizable=True) + frappe.msgprint( + msg, title=_("Workflow Status"), indicator=indicator, is_minimizable=True, realtime=True + ) @frappe.whitelist() diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py index 0295fbaaf2..ee8b4cd014 100644 --- a/frappe/modules/import_file.py +++ b/frappe/modules/import_file.py @@ -20,7 +20,7 @@ def calculate_hash(path: str) -> str: Returns: str: The calculated hash """ - hash_md5 = hashlib.md5() + hash_md5 = hashlib.md5(usedforsecurity=False) with open(path, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) diff --git a/frappe/monitor.py b/frappe/monitor.py index aae54987c8..c64855676f 100644 --- a/frappe/monitor.py +++ b/frappe/monitor.py @@ -84,7 +84,7 @@ class Monitor: if job := rq.get_current_job(): self.data.uuid = job.id - waitdiff = self.data.timestamp - job.enqueued_at + waitdiff = self.data.timestamp - job.enqueued_at.replace(tzinfo=pytz.UTC) self.data.job.wait = int(waitdiff.total_seconds() * 1000000) def add_custom_data(self, **kwargs): diff --git a/frappe/oauth.py b/frappe/oauth.py index ebd6b91ae7..bf7abeb424 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -11,7 +11,7 @@ from oauthlib.openid import RequestValidator import frappe from frappe.auth import LoginManager -from frappe.utils.data import get_system_timezone +from frappe.utils.data import get_system_timezone, now_datetime class OAuthWebRequestValidator(RequestValidator): @@ -240,13 +240,7 @@ class OAuthWebRequestValidator(RequestValidator): def validate_bearer_token(self, token, scopes, request): # Remember to check expiration and scope membership otoken = frappe.get_doc("OAuth Bearer Token", token) - token_expiration_local = otoken.expiration_time.replace( - tzinfo=pytz.timezone(get_system_timezone()) - ) - token_expiration_utc = token_expiration_local.astimezone(pytz.utc) - is_token_valid = ( - datetime.datetime.now(pytz.UTC) < token_expiration_utc - ) and otoken.status != "Revoked" + is_token_valid = (now_datetime() < otoken.expiration_time) and otoken.status != "Revoked" client_scopes = frappe.db.get_value("OAuth Client", otoken.client, "scopes").split( get_url_delimiter() ) diff --git a/frappe/patches.txt b/frappe/patches.txt index d3f6e30aee..790b5a5e3f 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -231,3 +231,4 @@ execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_na frappe.patches.v15_0.move_event_cancelled_to_status frappe.patches.v15_0.set_file_type frappe.core.doctype.data_import.patches.remove_stale_docfields_from_legacy_version +frappe.patches.v15_0.validate_newsletter_recipients diff --git a/frappe/patches/v15_0/validate_newsletter_recipients.py b/frappe/patches/v15_0/validate_newsletter_recipients.py new file mode 100644 index 0000000000..61749d3df8 --- /dev/null +++ b/frappe/patches/v15_0/validate_newsletter_recipients.py @@ -0,0 +1,9 @@ +import frappe +from frappe.utils import validate_email_address + + +def execute(): + for name, email in frappe.get_all("Email Group Member", fields=["name", "email"], as_list=True): + if not validate_email_address(email, throw=False): + frappe.db.set_value("Email Group Member", name, "unsubscribed", 1) + frappe.db.commit() diff --git a/frappe/public/css/fonts/inter/Inter-Black.woff2 b/frappe/public/css/fonts/inter/Inter-Black.woff2 new file mode 100644 index 0000000000..18b35db75c Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-Black.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-BlackItalic.woff2 b/frappe/public/css/fonts/inter/Inter-BlackItalic.woff2 new file mode 100644 index 0000000000..02c9d8ecc2 Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-BlackItalic.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-Bold.woff2 b/frappe/public/css/fonts/inter/Inter-Bold.woff2 new file mode 100644 index 0000000000..0f1b157633 Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-Bold.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-BoldItalic.woff2 b/frappe/public/css/fonts/inter/Inter-BoldItalic.woff2 new file mode 100644 index 0000000000..bc50f24c87 Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-BoldItalic.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-ExtraBold.woff2 b/frappe/public/css/fonts/inter/Inter-ExtraBold.woff2 new file mode 100644 index 0000000000..b1133688a4 Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-ExtraBold.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-ExtraBoldItalic.woff2 b/frappe/public/css/fonts/inter/Inter-ExtraBoldItalic.woff2 new file mode 100644 index 0000000000..a5b76ca8da Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-ExtraBoldItalic.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-ExtraLight.woff2 b/frappe/public/css/fonts/inter/Inter-ExtraLight.woff2 new file mode 100644 index 0000000000..1d77ae8d04 Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-ExtraLight.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-ExtraLightItalic.woff2 b/frappe/public/css/fonts/inter/Inter-ExtraLightItalic.woff2 new file mode 100644 index 0000000000..8c6849209d Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-ExtraLightItalic.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-Italic.var.woff2 b/frappe/public/css/fonts/inter/Inter-Italic.var.woff2 deleted file mode 100644 index 13778e77a3..0000000000 Binary files a/frappe/public/css/fonts/inter/Inter-Italic.var.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/Inter-Italic.woff2 b/frappe/public/css/fonts/inter/Inter-Italic.woff2 new file mode 100644 index 0000000000..4c24ce2815 Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-Italic.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-Light.woff2 b/frappe/public/css/fonts/inter/Inter-Light.woff2 new file mode 100644 index 0000000000..dbe61437a1 Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-Light.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-LightItalic.woff2 b/frappe/public/css/fonts/inter/Inter-LightItalic.woff2 new file mode 100644 index 0000000000..a40d042158 Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-LightItalic.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-Medium.woff2 b/frappe/public/css/fonts/inter/Inter-Medium.woff2 new file mode 100644 index 0000000000..0fd2ee7370 Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-Medium.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-MediumItalic.woff2 b/frappe/public/css/fonts/inter/Inter-MediumItalic.woff2 new file mode 100644 index 0000000000..96767155d9 Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-MediumItalic.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-Regular.woff2 b/frappe/public/css/fonts/inter/Inter-Regular.woff2 new file mode 100644 index 0000000000..b8699af29b Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-Regular.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-SemiBold.woff2 b/frappe/public/css/fonts/inter/Inter-SemiBold.woff2 new file mode 100644 index 0000000000..95c48b184e Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-SemiBold.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-SemiBoldItalic.woff2 b/frappe/public/css/fonts/inter/Inter-SemiBoldItalic.woff2 new file mode 100644 index 0000000000..ddfe19e839 Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-SemiBoldItalic.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-Thin.woff2 b/frappe/public/css/fonts/inter/Inter-Thin.woff2 new file mode 100644 index 0000000000..07909608cd Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-Thin.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter-ThinItalic.woff2 b/frappe/public/css/fonts/inter/Inter-ThinItalic.woff2 new file mode 100644 index 0000000000..a7bf213801 Binary files /dev/null and b/frappe/public/css/fonts/inter/Inter-ThinItalic.woff2 differ diff --git a/frappe/public/css/fonts/inter/Inter.var.woff2 b/frappe/public/css/fonts/inter/Inter.var.woff2 deleted file mode 100644 index 039bfbab24..0000000000 Binary files a/frappe/public/css/fonts/inter/Inter.var.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/InterVariable-Italic.woff2 b/frappe/public/css/fonts/inter/InterVariable-Italic.woff2 new file mode 100644 index 0000000000..f22ec25549 Binary files /dev/null and b/frappe/public/css/fonts/inter/InterVariable-Italic.woff2 differ diff --git a/frappe/public/css/fonts/inter/InterVariable.woff2 b/frappe/public/css/fonts/inter/InterVariable.woff2 new file mode 100644 index 0000000000..22a12b04e1 Binary files /dev/null and b/frappe/public/css/fonts/inter/InterVariable.woff2 differ diff --git a/frappe/public/css/fonts/inter/LICENSE.txt b/frappe/public/css/fonts/inter/LICENSE.txt deleted file mode 100644 index 65ec0f9103..0000000000 --- a/frappe/public/css/fonts/inter/LICENSE.txt +++ /dev/null @@ -1,94 +0,0 @@ -Copyright (c) 2016-2020 The Inter Project Authors. -"Inter" is trademark of Rasmus Andersson. -https://github.com/rsms/inter - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION AND CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/frappe/public/css/fonts/inter/inter.css b/frappe/public/css/fonts/inter/inter.css index 3200c42576..39166e902e 100644 --- a/frappe/public/css/fonts/inter/inter.css +++ b/frappe/public/css/fonts/inter/inter.css @@ -1,166 +1,33 @@ -/* This file is depricated use Inter.scss instead. */ -/* Backward compatibility */ @font-face { - font-family: 'Inter V'; + font-family: InterVariable; + font-style: normal; font-weight: 100 900; font-display: swap; - font-style: normal; - src: url('/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19') format('woff2-variations'), - url('/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19') format('woff2'); - src: url('/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19') format('woff2') tech('variations'); - unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; + src: url("/assets/frappe/css/fonts/inter/InterVariable.woff2") format("woff2"); } @font-face { - font-family: 'Inter V'; + font-family: InterVariable; + font-style: italic; font-weight: 100 900; font-display: swap; - font-style: italic; - src: url('/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19') format('woff2-variations'), - url('/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19') format('woff2'); - src: url('/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19') format('woff2') tech('variations'); - unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; + src: url("/assets/frappe/css/fonts/inter/InterVariable-Italic.woff2") format("woff2"); } -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: italic; - font-weight: 100; - src: url("/assets/frappe/css/fonts/inter/inter_thinitalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_thinitalic.woff") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: normal; - font-weight: 200; - src: url("/assets/frappe/css/fonts/inter/inter_extralight.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_extralight.woff") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: italic; - font-weight: 200; - src: url("/assets/frappe/css/fonts/inter/inter_extralightitalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_extralightitalic.woff") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: normal; - font-weight: 300; - src: url("/assets/frappe/css/fonts/inter/inter_light.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_light.woff") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: italic; - font-weight: 300; - src: url("/assets/frappe/css/fonts/inter/inter_lightitalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_lightitalic.woff") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: normal; - font-weight: 400; - src: url("/assets/frappe/css/fonts/inter/inter_regular.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_regular.woff") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: italic; - font-weight: 400; - src: url("/assets/frappe/css/fonts/inter/inter_italic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_italic.woff") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: normal; - font-weight: 500; - src: url("/assets/frappe/css/fonts/inter/inter_medium.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_medium.woff") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: italic; - font-weight: 500; - src: url("/assets/frappe/css/fonts/inter/inter_mediumitalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_mediumitalic.woff") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: normal; - font-weight: 600; - src: url("/assets/frappe/css/fonts/inter/inter_semibold.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_semibold.woff") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: italic; - font-weight: 600; - src: url("/assets/frappe/css/fonts/inter/inter_semibolditalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_semibolditalic.woff") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: normal; - font-weight: 700; - src: url("/assets/frappe/css/fonts/inter/inter_bold.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_bold.woff") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: italic; - font-weight: 700; - src: url("/assets/frappe/css/fonts/inter/inter_bolditalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_bolditalic.woff") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: normal; - font-weight: 800; - src: url("/assets/frappe/css/fonts/inter/inter_extrabold.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_extrabold.woff") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: italic; - font-weight: 800; - src: url("/assets/frappe/css/fonts/inter/inter_extrabolditalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_extrabolditalic.woff") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: normal; - font-weight: 900; - src: url("/assets/frappe/css/fonts/inter/inter_black.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_black.woff") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-display: swap; - font-style: italic; - font-weight: 900; - src: url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff") format("woff"); -} + /* static fonts */ + @font-face { font-family: "Inter"; font-style: normal; font-weight: 100; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Thin.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: italic; font-weight: 100; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-ThinItalic.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: normal; font-weight: 200; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-ExtraLight.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: italic; font-weight: 200; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-ExtraLightItalic.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: normal; font-weight: 300; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Light.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: italic; font-weight: 300; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-LightItalic.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Regular.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: italic; font-weight: 400; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Italic.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: normal; font-weight: 500; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Medium.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: italic; font-weight: 500; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-MediumItalic.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-SemiBold.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: italic; font-weight: 600; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-SemiBoldItalic.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Bold.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: italic; font-weight: 700; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-BoldItalic.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: normal; font-weight: 800; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-ExtraBold.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: italic; font-weight: 800; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-ExtraBoldItalic.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: normal; font-weight: 900; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Black.woff2") format("woff2"); } + @font-face { font-family: "Inter"; font-style: italic; font-weight: 900; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-BlackItalic.woff2") format("woff2"); } diff --git a/frappe/public/css/fonts/inter/inter.scss b/frappe/public/css/fonts/inter/inter.scss index 705fe2badd..8471e042f6 100644 --- a/frappe/public/css/fonts/inter/inter.scss +++ b/frappe/public/css/fonts/inter/inter.scss @@ -1,167 +1 @@ -// TODO instead of making copy of inter.css find a way to import it. -// workaround for css import as it fails for custom website_theme_template -@font-face { - font-family: "Inter V"; - font-weight: 100 900; - font-display: swap; - font-style: normal; - src: url("/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19") format("woff2-variations"), - url("/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19") format("woff2"); - src: url("/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19") format("woff2") - tech("variations"); -} -@font-face { - font-family: "Inter V"; - font-weight: 100 900; - font-display: swap; - font-style: italic; - src: url("/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19") - format("woff2-variations"), - url("/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19") format("woff2"); - src: url("/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19") format("woff2") - tech("variations"); -} -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: italic; - font-weight: 100; - src: url("/assets/frappe/css/fonts/inter/inter_thinitalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_thinitalic.woff") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: normal; - font-weight: 200; - src: url("/assets/frappe/css/fonts/inter/inter_extralight.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_extralight.woff") format("woff"); -} -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: italic; - font-weight: 200; - src: url("/assets/frappe/css/fonts/inter/inter_extralightitalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_extralightitalic.woff") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: normal; - font-weight: 300; - src: url("/assets/frappe/css/fonts/inter/inter_light.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_light.woff") format("woff"); -} -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: italic; - font-weight: 300; - src: url("/assets/frappe/css/fonts/inter/inter_lightitalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_lightitalic.woff") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: normal; - font-weight: 400; - src: url("/assets/frappe/css/fonts/inter/inter_regular.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_regular.woff") format("woff"); -} -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: italic; - font-weight: 400; - src: url("/assets/frappe/css/fonts/inter/inter_italic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_italic.woff") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: normal; - font-weight: 500; - src: url("/assets/frappe/css/fonts/inter/inter_medium.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_medium.woff") format("woff"); -} -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: italic; - font-weight: 500; - src: url("/assets/frappe/css/fonts/inter/inter_mediumitalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_mediumitalic.woff") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: normal; - font-weight: 600; - src: url("/assets/frappe/css/fonts/inter/inter_semibold.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_semibold.woff") format("woff"); -} -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: italic; - font-weight: 600; - src: url("/assets/frappe/css/fonts/inter/inter_semibolditalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_semibolditalic.woff") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: normal; - font-weight: 700; - src: url("/assets/frappe/css/fonts/inter/inter_bold.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_bold.woff") format("woff"); -} -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: italic; - font-weight: 700; - src: url("/assets/frappe/css/fonts/inter/inter_bolditalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_bolditalic.woff") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: normal; - font-weight: 800; - src: url("/assets/frappe/css/fonts/inter/inter_extrabold.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_extrabold.woff") format("woff"); -} -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: italic; - font-weight: 800; - src: url("/assets/frappe/css/fonts/inter/inter_extrabolditalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_extrabolditalic.woff") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: normal; - font-weight: 900; - src: url("/assets/frappe/css/fonts/inter/inter_black.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_black.woff") format("woff"); -} -@font-face { - font-family: "Inter"; - font-display: swap; - font-style: italic; - font-weight: 900; - src: url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff2") format("woff2"), - url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff") format("woff"); -} +@import "frappe/public/css/fonts/inter/inter.css"; diff --git a/frappe/public/css/fonts/inter/inter_black.woff b/frappe/public/css/fonts/inter/inter_black.woff deleted file mode 100644 index 52af3fe46a..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_black.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_black.woff2 b/frappe/public/css/fonts/inter/inter_black.woff2 deleted file mode 100644 index 89c204c80c..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_black.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_blackitalic.woff b/frappe/public/css/fonts/inter/inter_blackitalic.woff deleted file mode 100644 index 0ad6c7d2c1..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_blackitalic.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_blackitalic.woff2 b/frappe/public/css/fonts/inter/inter_blackitalic.woff2 deleted file mode 100644 index b3f3267352..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_blackitalic.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_bold.woff b/frappe/public/css/fonts/inter/inter_bold.woff deleted file mode 100644 index 80f70f058e..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_bold.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_bold.woff2 b/frappe/public/css/fonts/inter/inter_bold.woff2 deleted file mode 100644 index 622e5f1478..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_bold.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_bolditalic.woff b/frappe/public/css/fonts/inter/inter_bolditalic.woff deleted file mode 100644 index 03238dd48a..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_bolditalic.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_bolditalic.woff2 b/frappe/public/css/fonts/inter/inter_bolditalic.woff2 deleted file mode 100644 index 1b2dafb9da..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_bolditalic.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_extrabold.woff b/frappe/public/css/fonts/inter/inter_extrabold.woff deleted file mode 100644 index fbccaecddc..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_extrabold.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_extrabold.woff2 b/frappe/public/css/fonts/inter/inter_extrabold.woff2 deleted file mode 100644 index 0bcf3f3d14..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_extrabold.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_extrabolditalic.woff b/frappe/public/css/fonts/inter/inter_extrabolditalic.woff deleted file mode 100644 index 20c45c44f5..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_extrabolditalic.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_extrabolditalic.woff2 b/frappe/public/css/fonts/inter/inter_extrabolditalic.woff2 deleted file mode 100644 index cca9829bb2..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_extrabolditalic.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_extralight.woff b/frappe/public/css/fonts/inter/inter_extralight.woff deleted file mode 100644 index 8c9bb8c8fd..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_extralight.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_extralight.woff2 b/frappe/public/css/fonts/inter/inter_extralight.woff2 deleted file mode 100644 index f927cd91a9..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_extralight.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_extralightitalic.woff b/frappe/public/css/fonts/inter/inter_extralightitalic.woff deleted file mode 100644 index 6136f7e6ba..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_extralightitalic.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_extralightitalic.woff2 b/frappe/public/css/fonts/inter/inter_extralightitalic.woff2 deleted file mode 100644 index 8565cd6b48..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_extralightitalic.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_italic.var.woff2 b/frappe/public/css/fonts/inter/inter_italic.var.woff2 deleted file mode 100644 index fe6faaa581..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_italic.var.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_italic.woff b/frappe/public/css/fonts/inter/inter_italic.woff deleted file mode 100644 index 9c21aedc8a..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_italic.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_italic.woff2 b/frappe/public/css/fonts/inter/inter_italic.woff2 deleted file mode 100644 index 734944b11b..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_italic.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_light.woff b/frappe/public/css/fonts/inter/inter_light.woff deleted file mode 100644 index 0df2bc7c53..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_light.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_light.woff2 b/frappe/public/css/fonts/inter/inter_light.woff2 deleted file mode 100644 index b09ea9da2e..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_light.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_lightitalic.woff b/frappe/public/css/fonts/inter/inter_lightitalic.woff deleted file mode 100644 index ee7ebb51bb..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_lightitalic.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_lightitalic.woff2 b/frappe/public/css/fonts/inter/inter_lightitalic.woff2 deleted file mode 100644 index 3d7774aca3..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_lightitalic.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_medium.woff b/frappe/public/css/fonts/inter/inter_medium.woff deleted file mode 100644 index 1d50f8007e..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_medium.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_medium.woff2 b/frappe/public/css/fonts/inter/inter_medium.woff2 deleted file mode 100644 index ffb4206c2e..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_medium.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_mediumitalic.woff b/frappe/public/css/fonts/inter/inter_mediumitalic.woff deleted file mode 100644 index 9eb5b9d99e..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_mediumitalic.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_mediumitalic.woff2 b/frappe/public/css/fonts/inter/inter_mediumitalic.woff2 deleted file mode 100644 index ebee6551da..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_mediumitalic.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_regular.woff b/frappe/public/css/fonts/inter/inter_regular.woff deleted file mode 100644 index 7cb4990b84..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_regular.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_regular.woff2 b/frappe/public/css/fonts/inter/inter_regular.woff2 deleted file mode 100644 index 66691b83a5..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_regular.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_roman.var.woff2 b/frappe/public/css/fonts/inter/inter_roman.var.woff2 deleted file mode 100644 index dbcd4ce78b..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_roman.var.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_semibold.woff b/frappe/public/css/fonts/inter/inter_semibold.woff deleted file mode 100644 index 490bd9d5e4..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_semibold.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_semibold.woff2 b/frappe/public/css/fonts/inter/inter_semibold.woff2 deleted file mode 100644 index 9fd7726ebb..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_semibold.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_semibolditalic.woff b/frappe/public/css/fonts/inter/inter_semibolditalic.woff deleted file mode 100644 index 839fc3d0a1..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_semibolditalic.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_semibolditalic.woff2 b/frappe/public/css/fonts/inter/inter_semibolditalic.woff2 deleted file mode 100644 index 24925694cf..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_semibolditalic.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_thin.woff b/frappe/public/css/fonts/inter/inter_thin.woff deleted file mode 100644 index 1b4f86c165..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_thin.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_thin.woff2 b/frappe/public/css/fonts/inter/inter_thin.woff2 deleted file mode 100644 index 16c7b0f4a9..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_thin.woff2 and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_thinitalic.woff b/frappe/public/css/fonts/inter/inter_thinitalic.woff deleted file mode 100644 index 1bcf69399d..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_thinitalic.woff and /dev/null differ diff --git a/frappe/public/css/fonts/inter/inter_thinitalic.woff2 b/frappe/public/css/fonts/inter/inter_thinitalic.woff2 deleted file mode 100644 index a13ba5b571..0000000000 Binary files a/frappe/public/css/fonts/inter/inter_thinitalic.woff2 and /dev/null differ diff --git a/frappe/public/js/form_builder/components/Autocomplete.vue b/frappe/public/js/form_builder/components/Autocomplete.vue index 3bbaf8b298..7960047b43 100644 --- a/frappe/public/js/form_builder/components/Autocomplete.vue +++ b/frappe/public/js/form_builder/components/Autocomplete.vue @@ -19,7 +19,7 @@
{ - return query.value - ? props.options.filter((option) => { - return option.label.toLowerCase().includes(query.value.toLowerCase()); - }) - : props.options; + if (!query.value) return props.options; + return props.options.filter((option) => { + return option.label.toLocaleLowerCase().includes(query.value.toLocaleLowerCase()); + }); +}); + +const sortedOptions = computed(() => { + return filteredOptions.value.sort((a, b) => { + return a.label.localeCompare(b.label); + }); }); function clear_search() { @@ -126,6 +131,8 @@ watch(showOptions, (val) => { border-radius: var(--border-radius-sm); padding: 6px 10px; width: 100%; + cursor: pointer; + user-select: none; &:hover, &.active { diff --git a/frappe/public/js/form_builder/components/Field.vue b/frappe/public/js/form_builder/components/Field.vue index 08e65473ee..26d488cdf8 100644 --- a/frappe/public/js/form_builder/components/Field.vue +++ b/frappe/public/js/form_builder/components/Field.vue @@ -174,8 +174,14 @@ function edit_filters() { } function is_filter_applied() { - if (props.field.df.link_filters && JSON.parse(props.field.df.link_filters).length > 0) { - return "btn-filter-applied"; + if (props.field.df.link_filters) { + try { + if (JSON.parse(props.field.df.link_filters).length > 0) { + return "btn-filter-applied"; + } + } catch (error) { + return ""; + } } } diff --git a/frappe/public/js/form_builder/components/FieldProperties.vue b/frappe/public/js/form_builder/components/FieldProperties.vue index 7132bfeba9..5f903ed36c 100644 --- a/frappe/public/js/form_builder/components/FieldProperties.vue +++ b/frappe/public/js/form_builder/components/FieldProperties.vue @@ -51,6 +51,11 @@ let docfield_df = computed(() => { } } + // show link_filters docfield only when link field is selected + if (df.fieldname === "link_filters" && store.form.selected_field.fieldtype !== "Link") { + return false; + } + if (search_text.value) { if ( df.label.toLowerCase().includes(search_text.value.toLowerCase()) || @@ -62,7 +67,6 @@ let docfield_df = computed(() => { } return true; }); - return [...fields]; }); diff --git a/frappe/public/js/form_builder/components/Section.vue b/frappe/public/js/form_builder/components/Section.vue index 128227de98..36cff09b4d 100644 --- a/frappe/public/js/form_builder/components/Section.vue +++ b/frappe/public/js/form_builder/components/Section.vue @@ -101,9 +101,9 @@ const selected = computed(() => store.selected(props.section.df.name)); const column = computed(() => props.section.columns[props.section.columns.length - 1]); // section -function add_section_above() { +function add_section_below() { let index = props.tab.sections.indexOf(props.section); - props.tab.sections.splice(index, 0, section_boilerplate()); + props.tab.sections.splice(index + 1, 0, section_boilerplate()); } function is_section_empty() { @@ -262,7 +262,7 @@ const options = computed(() => { { group: "Section", items: [ - { label: "Add section above", onClick: add_section_above }, + { label: "Add section below", onClick: add_section_below }, { label: "Remove section", onClick: remove_section }, ], }, diff --git a/frappe/public/js/form_builder/store.js b/frappe/public/js/form_builder/store.js index 069cd10540..97aa89b58c 100644 --- a/frappe/public/js/form_builder/store.js +++ b/frappe/public/js/form_builder/store.js @@ -202,14 +202,18 @@ export const useStore = defineStore("form-builder-store", () => { ); } - // check if link_filters format is correct or not + if (df.link_filters === "") { + delete df.link_filters; + } + // check if link_filters format is correct or not if (df.link_filters) { try { let link_filters = JSON.parse(df.link_filters); } catch (e) { error_message = __( - `Invalid Filter Format. Try using filter icon on the field to set it correctly` + "Invalid Filter Format for field {0} of type {1}. Try using filter icon on the field to set it correctly", + get_field_data(df) ); } } diff --git a/frappe/public/js/frappe/file_uploader/ImageCropper.vue b/frappe/public/js/frappe/file_uploader/ImageCropper.vue index b91345e440..f57418613c 100644 --- a/frappe/public/js/frappe/file_uploader/ImageCropper.vue +++ b/frappe/public/js/frappe/file_uploader/ImageCropper.vue @@ -128,6 +128,7 @@ watch(