diff --git a/.mergify.yml b/.mergify.yml index f1333362a8..97df91a927 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -7,6 +7,7 @@ pull_request_rules: - author!=gavindsouza - author!=deepeshgarg007 - author!=ankush + - author!=mergify[bot] - or: - base=version-13 - base=version-12 diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js index 053d015366..e62ba6bec5 100644 --- a/cypress/integration/awesome_bar.js +++ b/cypress/integration/awesome_bar.js @@ -26,7 +26,7 @@ context('Awesome Bar', () => { cy.get('.title-text').should('contain', 'To Do'); - cy.findByPlaceholderText('Name') + cy.findByPlaceholderText('ID') .should('have.value', '%test%'); }); diff --git a/cypress/integration/control_data.js b/cypress/integration/control_data.js index 01f9168667..78cece627b 100644 --- a/cypress/integration/control_data.js +++ b/cypress/integration/control_data.js @@ -34,6 +34,12 @@ context('Data Control', () => { }); }); }); + + it('check custom formatters', () => { + cy.visit(`/app/doctype/User`); + cy.get('[data-fieldname="fields"] .grid-row[data-idx="2"] [data-fieldname="fieldtype"] .static-area').should('have.text', '🔵 Section Break'); + }); + it('Verifying data control by inputting different patterns for "Name" field', () => { cy.new_form('Test Data Control'); @@ -54,7 +60,7 @@ context('Data Control', () => { //Checking if the border color of the field changes to red cy.get('.frappe-control[data-fieldname="name1"]').should('have.class', 'has-error'); - cy.findByRole('button', {name: 'Save'}).click(); + cy.save(); //Checking for the error message cy.get('.modal-title').should('have.text', 'Message'); @@ -64,7 +70,7 @@ context('Data Control', () => { cy.get_field('name1', 'Data').clear({force: true}); cy.fill_field('name1', 'Komal{}/!', 'Data'); cy.get('.frappe-control[data-fieldname="name1"]').should('have.class', 'has-error'); - cy.findByRole('button', {name: 'Save'}).click(); + cy.save(); cy.get('.modal-title').should('have.text', 'Message'); cy.get('.msgprint').should('have.text', 'Komal{}/! is not a valid Name'); cy.hide_dialog(); @@ -76,14 +82,14 @@ context('Data Control', () => { cy.get_field('email', 'Data').clear({force: true}); cy.fill_field('email', 'komal', 'Data'); cy.get('.frappe-control[data-fieldname="email"]').should('have.class', 'has-error'); - cy.findByRole('button', {name: 'Save'}).click(); + cy.save(); cy.get('.modal-title').should('have.text', 'Message'); cy.get('.msgprint').should('have.text', 'komal is not a valid Email Address'); cy.hide_dialog(); cy.get_field('email', 'Data').clear({force: true}); cy.fill_field('email', 'komal@test', 'Data'); cy.get('.frappe-control[data-fieldname="email"]').should('have.class', 'has-error'); - cy.findByRole('button', {name: 'Save'}).click(); + cy.save(); cy.get('.modal-title').should('have.text', 'Message'); cy.get('.msgprint').should('have.text', 'komal@test is not a valid Email Address'); cy.hide_dialog(); @@ -125,4 +131,4 @@ context('Data Control', () => { cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click(); cy.click_modal_primary_button('Yes'); }); -}); \ No newline at end of file +}); diff --git a/cypress/integration/custom_buttons.js b/cypress/integration/custom_buttons.js index e2f02668e9..6045d009c2 100644 --- a/cypress/integration/custom_buttons.js +++ b/cypress/integration/custom_buttons.js @@ -4,6 +4,7 @@ const test_button_names = [ "Porcupine Tree (the GOAT)", "AC / DC", `Electronic Dance "music"`, + "l'imperatrice", ]; const add_button = (label, group = "TestGroup") => { diff --git a/cypress/integration/customize_form.js b/cypress/integration/customize_form.js index 70615085c3..3857d7ccd8 100644 --- a/cypress/integration/customize_form.js +++ b/cypress/integration/customize_form.js @@ -1,5 +1,6 @@ context('Customize Form', () => { before(() => { + cy.login(); cy.visit('/app/customize-form'); }); it('Changing to naming rule should update autoname', () => { @@ -19,4 +20,4 @@ context('Customize Form', () => { cy.get_field("autoname", "Data").should("have.value", value); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 6398018e10..c168b0c201 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -27,6 +27,7 @@ import "cypress-real-events/support"; // // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); + Cypress.Commands.add('login', (email, password) => { if (!email) { email = 'Administrator'; @@ -265,9 +266,14 @@ Cypress.Commands.add('get_open_dialog', () => { return cy.get('.modal:visible').last(); }); +Cypress.Commands.add('save', () => { + cy.intercept('/api').as('api'); + cy.get(`button[data-label="Save"]:visible`).click({scrollBehavior: false, force: true}); + cy.wait('@api'); +}); Cypress.Commands.add('hide_dialog', () => { cy.wait(300); - cy.get_open_dialog().find('.btn-modal-close').click(); + cy.get_open_dialog().focus().find('.btn-modal-close').click(); cy.get('.modal:visible').should('not.exist'); }); diff --git a/dev-requirements.txt b/dev-requirements.txt index f4045c6bed..b67e915a16 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,4 @@ coverage==5.5 -Faker~=8.1.0 +Faker~=13.12.1 pyngrok~=5.0.5 unittest-xml-reporting~=3.0.4 diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 3e58146ae7..e834b698d5 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -8,6 +8,7 @@ import os # imports - standard imports import re import shutil +from typing import TYPE_CHECKING, Union # imports - module imports import frappe @@ -35,6 +36,9 @@ from frappe.query_builder.functions import Concat from frappe.utils import cint from frappe.website.utils import clear_cache +if TYPE_CHECKING: + from frappe.custom.doctype.customize_form.customize_form import CustomizeForm + DEPENDS_ON_PATTERN = re.compile(r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+') ILLEGAL_FIELDNAME_PATTERN = re.compile("""['",./%@()<>{}]""") WHITESPACE_PADDING_PATTERN = re.compile(r"^[ \t\n\r]+|[ \t\n\r]+$", flags=re.ASCII) @@ -916,11 +920,11 @@ def validate_series(dt, autoname=None, name=None): frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0])) -def validate_autoincrement_autoname(dt: DocType) -> bool: +def validate_autoincrement_autoname(dt: Union[DocType, "CustomizeForm"]) -> bool: """Checks if can doctype can change to/from autoincrement autoname""" - def get_autoname_before_save(dt: DocType) -> str: - if dt.name == "Customize Form": + def get_autoname_before_save(dt: Union[DocType, "CustomizeForm"]) -> str: + if dt.doctype == "Customize Form": property_value = frappe.db.get_value( "Property Setter", {"doc_type": dt.doc_type, "property": "autoname"}, "value" ) @@ -943,10 +947,10 @@ def validate_autoincrement_autoname(dt: DocType) -> bool: or (not is_autoname_autoincrement and autoname_before_save == "autoincrement") ): - if frappe.get_meta(dt.name).issingle: - if dt.name == "Customize Form": - frappe.throw(_("Cannot change to/from autoincrement autoname in Customize Form")) + if dt.doctype == "Customize Form": + frappe.throw(_("Cannot change to/from autoincrement autoname in Customize Form")) + if frappe.get_meta(dt.name).issingle: return False if not frappe.get_all(dt.name, limit=1): diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 1bcbaf161a..e8b8da76ab 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -341,9 +341,9 @@ class File(Document): size = width, height if crop: - image = ImageOps.fit(image, size, Image.ANTIALIAS) + image = ImageOps.fit(image, size, Image.Resampling.LANCZOS) else: - image.thumbnail(size, Image.ANTIALIAS) + image.thumbnail(size, Image.Resampling.LANCZOS) thumbnail_url = f"{filename}_{suffix}.{extn}" path = os.path.abspath(frappe.get_site_path("public", thumbnail_url.lstrip("/"))) diff --git a/frappe/core/doctype/version/test_version.py b/frappe/core/doctype/version/test_version.py index c35430b17b..3e82f30f06 100644 --- a/frappe/core/doctype/version/test_version.py +++ b/frappe/core/doctype/version/test_version.py @@ -32,6 +32,19 @@ class TestVersion(unittest.TestCase): self.assertEqual(get_old_values(diff)[1], "01-01-2014 00:00:00") self.assertEqual(get_new_values(diff)[1], "07-20-2017 00:00:00") + def test_no_version_on_new_doc(self): + from frappe.desk.form.load import get_versions + + t = frappe.get_doc(doctype="ToDo", description="something") + t.save(ignore_version=False) + + self.assertFalse(get_versions(t)) + + t = frappe.get_doc(t.doctype, t.name) + t.description = "changed" + t.save(ignore_version=False) + self.assertTrue(get_versions(t)) + def get_fieldnames(change_array): return [d[0] for d in change_array] diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py index 863885e85c..fa6ba0a9cf 100644 --- a/frappe/core/doctype/version/version.py +++ b/frappe/core/doctype/version/version.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import json +from typing import Optional import frappe from frappe.model import no_value_fields, table_fields @@ -9,7 +10,15 @@ from frappe.model.document import Document class Version(Document): - def set_diff(self, old, new): + def update_version_info(self, old: Optional[Document], new: Document) -> bool: + """Update changed info and return true if change contains useful data.""" + if not old: + # Check if doc has some information about creation source like data import + return self.for_insert(new) + else: + return self.set_diff(old, new) + + def set_diff(self, old: Document, new: Document) -> bool: """Set the data property with the diff of the docs if present""" diff = get_diff(old, new) if diff: @@ -20,8 +29,11 @@ class Version(Document): else: return False - def for_insert(self, doc): + def for_insert(self, doc: Document) -> bool: updater_reference = doc.flags.updater_reference + if not updater_reference: + return False + data = { "creation": doc.creation, "updater_reference": updater_reference, @@ -29,7 +41,8 @@ class Version(Document): } self.ref_doctype = doc.doctype self.docname = doc.name - self.data = frappe.as_json(data) + self.data = frappe.as_json(data, indent=None, separators=(",", ":")) + return True def get_data(self): return json.loads(self.data) diff --git a/frappe/model/document.py b/frappe/model/document.py index fa1f423d11..22514df75a 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -438,7 +438,7 @@ class Document(BaseDocument): def get_title(self): """Get the document title based on title_field or `title` or `name`""" - return self.get(self.meta.get_title_field()) + return self.get(self.meta.get_title_field()) or "" def set_title_field(self): """Set title field based on template""" @@ -1198,11 +1198,10 @@ class Document(BaseDocument): return version = frappe.new_doc("Version") - if not self._doc_before_save: - version.for_insert(self) - version.insert(ignore_permissions=True) - elif version.set_diff(self._doc_before_save, self): + + if is_useful_diff := version.update_version_info(self._doc_before_save, self): version.insert(ignore_permissions=True) + if not frappe.flags.in_migrate: # follow since you made a change? if frappe.get_cached_value("User", frappe.session.user, "follow_created_documents"): diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 65ab7d39c2..9f5c2e7611 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -252,10 +252,15 @@ class Meta(Document): else: label = { "name": _("ID"), - "owner": _("Created By"), - "modified_by": _("Modified By"), "creation": _("Created On"), - "modified": _("Last Modified On"), + "docstatus": _("Document Status"), + "idx": _("Index"), + "modified": _("Last Updated On"), + "modified_by": _("Last Updated By"), + "owner": _("Created By"), + "_user_tags": _("Tags"), + "_liked_by": _("Liked By"), + "_comments": _("Comments"), "_assign": _("Assigned To"), }.get(fieldname) or _("No Label") return label diff --git a/frappe/public/js/frappe/doctype/index.js b/frappe/public/js/frappe/doctype/index.js index 09f020f370..d8ffa1097a 100644 --- a/frappe/public/js/frappe/doctype/index.js +++ b/frappe/public/js/frappe/doctype/index.js @@ -5,6 +5,21 @@ frappe.provide("frappe.model"); apply to both DocType form and customize form. */ frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form.Controller { + setup() { + // setup formatters for fieldtype + frappe.meta.docfield_map[this.frm.doctype==='DocType' ? 'DocField' : 'Customize Form Field'].fieldtype.formatter = (value) => { + const prefix = { + 'Tab Break': '🔴', + 'Section Break': '🔵', + 'Column Break': '🟡', + }; + if (prefix[value]) { + value = prefix[value] + ' ' + value; + } + return value; + }; + } + max_attachments() { if (!this.frm.doc.max_attachments) { return; diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 0731bdf8fb..c057903a63 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -19,7 +19,7 @@ frappe.ui.form.Dashboard = class FormDashboard { }); this.heatmap_area = this.make_section({ - label: __("Overview"), + label: __("Activity"), css_class: 'form-heatmap', hidden: 1, collapsible: 1, diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 2b0f996661..3bf36c86af 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -15,17 +15,35 @@ frappe.form.formatters = { return "
" + value + "
"; } }, + _apply_custom_formatter: function(value, df) { + /* you can add a custom formatter in df.formatter + example: + frappe.meta.docfield_map[df.parent][df.fieldname].formatter = (value) => { + if (value==='Test') return '😜'; + } + */ + + if (df) { + const std_df = frappe.meta.docfield_map[df.parent] && frappe.meta.docfield_map[df.parent][df.fieldname]; + if (std_df && std_df.formatter && typeof std_df.formatter==='function') { + value = std_df.formatter(value); + } + } + return value; + }, Data: function(value, df) { if (df && df.options == "URL") { return `${value}`; } - return value==null ? "" : value; + value = value==null ? "" : value; + + return frappe.form.formatters._apply_custom_formatter(value, df); }, - Autocomplete: function(value) { - return __(frappe.form.formatters["Data"](value)); + Autocomplete: function(value, df) { + return __(frappe.form.formatters["Data"](value, df)); }, - Select: function(value) { - return __(frappe.form.formatters["Data"](value)); + Select: function(value, df) { + return __(frappe.form.formatters["Data"](value, df)); }, Float: function(value, docfield, options, doc) { // don't allow 0 precision for Floats, hence or'ing with null @@ -183,7 +201,7 @@ frappe.form.formatters = { return ""; } }, - Text: function(value) { + Text: function(value, df) { if(value) { var tags = [" this.refresh_list_view(), diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index 94ec9d4e67..7c8c515643 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -208,7 +208,7 @@ export default class BulkOperations { const default_field = field_options.find(value => status_regex.test(value)); const dialog = new frappe.ui.Dialog({ - title: __('Edit'), + title: __('Bulk Edit'), fields: [ { 'fieldtype': 'Select', @@ -225,7 +225,9 @@ export default class BulkOperations { 'fieldtype': 'Data', 'label': __('Value'), 'fieldname': 'value', - 'reqd': 1 + onchange() { + show_help_text(); + } } ], primary_action: ({ value }) => { @@ -239,7 +241,7 @@ export default class BulkOperations { docnames: docnames, action: 'update', data: { - [fieldname]: value + [fieldname]: value || null } } }).then(r => { @@ -254,10 +256,11 @@ export default class BulkOperations { frappe.show_alert(__('Updated successfully')); }); }, - primary_action_label: __('Update') + primary_action_label: __('Update {0} records', [docnames.length]), }); if (default_field) set_value_field(dialog); // to set `Value` df based on default `Field` + show_help_text(); function set_value_field (dialogObj) { const new_df = Object.assign({}, @@ -275,9 +278,20 @@ export default class BulkOperations { new_df.default = options[0] || options[1]; } new_df.label = __('Value'); - new_df.reqd = 1; + new_df.onchange = show_help_text; + delete new_df.depends_on; dialogObj.replace_field('value', new_df); + show_help_text(); + } + + function show_help_text() { + let value = dialog.get_value('value'); + if (value == null || value === '') { + dialog.set_df_property('value', 'description', __('You have not entered a value. The field will be set to empty.')); + } else { + dialog.set_df_property('value', 'description', ''); + } } dialog.refresh(); diff --git a/frappe/public/js/frappe/list/list_settings.js b/frappe/public/js/frappe/list/list_settings.js index b7318ea780..6c48bd013a 100644 --- a/frappe/public/js/frappe/list/list_settings.js +++ b/frappe/public/js/frappe/list/list_settings.js @@ -310,7 +310,7 @@ export default class ListSettings { let me = this; me.subject_field = { - label: "Name", + label: "ID", fieldname: "name" }; diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index f755d6902e..d57c57a0b6 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -331,7 +331,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.columns.push({ type: "Subject", df: { - label: __("Name"), + label: __("ID"), fieldname: "name", }, }); @@ -398,7 +398,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.columns.push({ type: "Field", df: { - label: __("Name"), + label: __("ID"), fieldname: "name", }, }); diff --git a/frappe/public/js/frappe/model/meta.js b/frappe/public/js/frappe/model/meta.js index 4a7295ed4e..b89374ab23 100644 --- a/frappe/public/js/frappe/model/meta.js +++ b/frappe/public/js/frappe/model/meta.js @@ -109,7 +109,7 @@ $.extend(frappe.meta, { var fields = $.map(frappe.meta.get_docfields(doctype, name), function(df) { return (df.fieldtype==="Link" && df.ignore_user_permissions!==1) ? df : null; }); - fields = fields.concat({label: "Name", fieldname: name, options: doctype}); + fields = fields.concat({label: "ID", fieldname: name, options: doctype}); return fields; }, @@ -177,12 +177,17 @@ $.extend(frappe.meta, { get_label: function(dt, fn, dn) { var standard = { - 'owner': __('Owner'), + 'name': __('ID'), 'creation': __('Created On'), - 'modified': __('Last Modified On'), - 'idx': __('Idx'), - 'name': __('Name'), - 'modified_by': __('Last Modified By') + 'docstatus': __('Document Status'), + 'idx': __('Index'), + 'modified': __('Last Updated On'), + 'modified_by': __('Last Updated By'), + 'owner': __('Created By'), + '_user_tags': __('Tags'), + '_liked_by': __('Liked By'), + '_comments': __('Comments'), + '_assign': __('Assigned To'), } if(standard[fn]) { return standard[fn]; diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js index 70e0862ba5..eded1aefc5 100644 --- a/frappe/public/js/frappe/ui/page.js +++ b/frappe/public/js/frappe/ui/page.js @@ -510,7 +510,7 @@ frappe.ui.Page = class Page { if (!label || !parent) return false; - const item_selector = `${selector}[data-label='${encodeURIComponent(label)}']`; + const item_selector = `${selector}[data-label="${encodeURIComponent(label)}"]`; const existing_items = $(parent).find(item_selector); return existing_items?.length > 0 && existing_items; diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index 7d379d4531..128d7e691a 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -222,7 +222,7 @@ export default class OnboardingWidget extends Widget { const on_finish = () => { let msg_dialog = frappe.msgprint({ message: __("Let's take you back to onboarding"), - title: __("Great Job"), + title: __("Onboarding complete"), primary_action: { action: () => { frappe.set_route(current_route).then(() => { @@ -265,7 +265,7 @@ export default class OnboardingWidget extends Widget { if (success) { args.message = __("Let's take you back to onboarding"); - args.title = __("Looks Great"); + args.title = __("Action Complete"); args.primary_action = { action: () => { frappe.set_route(current_route).then(() => { @@ -278,7 +278,7 @@ export default class OnboardingWidget extends Widget { custom_onhide = () => args.primary_action.action(); } else { args.message = __("Looks like you didn't change the value"); - args.title = __("Oops"); + args.title = __("Try Again"); args.secondary_action = { action: () => frappe.set_route(current_route), label: __("Go Back"), @@ -314,7 +314,7 @@ export default class OnboardingWidget extends Widget { const on_finish = () => { frappe.msgprint({ message: __("Awesome, now try making an entry yourself"), - title: __("Great"), + title: __("Document Saved"), primary_action: { action: () => { frappe.set_route(current_route).then(() => { @@ -337,8 +337,8 @@ export default class OnboardingWidget extends Widget { let callback = () => { frappe.msgprint({ - message: __("You're doing great, let's take you back to the onboarding page."), - title: __("Good Work 🎉"), + message: __("Let's take you back to onboarding"), + title: __("Action Complete"), primary_action: { action: () => { frappe.set_route(current_route).then(() => { @@ -358,7 +358,7 @@ export default class OnboardingWidget extends Widget { frappe.route_hooks.after_save = () => { frappe.msgprint({ message: __("Submit this document to complete this step."), - title: __("Great") + title: __("Document Saved") }); }; frappe.route_hooks.after_submit = callback; @@ -377,7 +377,7 @@ export default class OnboardingWidget extends Widget { if (frappe.get_route_str() != current_route) { let success_dialog = frappe.msgprint({ message: __("Let's take you back to onboarding"), - title: __("Looks Great"), + title: __("Document Saved"), primary_action: { action: () => { success_dialog.hide(); @@ -397,7 +397,7 @@ export default class OnboardingWidget extends Widget { } else { frappe.msgprint({ message: __("Let us continue with the onboarding"), - title: __("Looks Great") + title: __("Document Saved") }); this.mark_complete(step); } diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index d1f89abbcd..07ab6d75a9 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -201,7 +201,7 @@ } .link-btn { - top: 8px; + top: 2px; } .form-control:focus { diff --git a/frappe/public/scss/desk/desktop.scss b/frappe/public/scss/desk/desktop.scss index 8be8abed35..c0fef60162 100644 --- a/frappe/public/scss/desk/desktop.scss +++ b/frappe/public/scss/desk/desktop.scss @@ -421,8 +421,8 @@ body { display: none; } - i { - color: var(--green-600); + .icon { + stroke: var(--white); } span { diff --git a/frappe/public/scss/login.bundle.scss b/frappe/public/scss/login.bundle.scss index 8d0a32846f..488dd4106e 100644 --- a/frappe/public/scss/login.bundle.scss +++ b/frappe/public/scss/login.bundle.scss @@ -16,7 +16,7 @@ body { .for-forgot, .for-signup, .for-email-login { - padding: max(15vh, 70px) 0; + padding: max(10vh, 60px) 0; @include media-breakpoint-up(sm) { .page-card { @@ -177,6 +177,7 @@ body { } h4 { + margin-top: 1rem; font-size: var(--text-xl); color: var(--text-color); } diff --git a/frappe/tests/test_form_load.py b/frappe/tests/test_form_load.py index e92b8c3ff2..22db56eeef 100644 --- a/frappe/tests/test_form_load.py +++ b/frappe/tests/test_form_load.py @@ -181,7 +181,7 @@ class TestFormLoad(unittest.TestCase): self.assertEqual(len(docinfo.comments), 1) self.assertIn("test", docinfo.comments[0].content) - self.assertGreaterEqual(len(docinfo.versions), 2) + self.assertGreaterEqual(len(docinfo.versions), 1) self.assertEqual(set(docinfo.tags.split(",")), {"more_tag", "test_tag"}) diff --git a/frappe/tests/test_pdf.py b/frappe/tests/test_pdf.py index 497546ebd5..8f2a2c1cfa 100644 --- a/frappe/tests/test_pdf.py +++ b/frappe/tests/test_pdf.py @@ -3,7 +3,7 @@ import io import unittest -from PyPDF2 import PdfFileReader +from PyPDF2 import PdfReader import frappe import frappe.utils.pdf as pdfgen @@ -42,7 +42,7 @@ class TestPdf(unittest.TestCase): def test_pdf_encryption(self): password = "qwe" pdf = pdfgen.get_pdf(self.html, options={"password": password}) - reader = PdfFileReader(io.BytesIO(pdf)) + reader = PdfReader(io.BytesIO(pdf)) self.assertTrue(reader.isEncrypted) self.assertTrue(reader.decrypt(password)) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 60770ef6a9..49f9ead437 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1349,7 +1349,7 @@ def get_thumbnail_base64_for_image(src): original_size = image.size size = 50, 50 - image.thumbnail(size, Image.ANTIALIAS) + image.thumbnail(size, Image.Resampling.LANCZOS) base64_string = image_to_base64(image, extn) return { diff --git a/frappe/utils/image.py b/frappe/utils/image.py index 0cbc02fb31..8823ea3dfe 100644 --- a/frappe/utils/image.py +++ b/frappe/utils/image.py @@ -7,8 +7,6 @@ from PIL import Image def resize_images(path, maxdim=700): - from PIL import Image - size = (maxdim, maxdim) for basepath, folders, files in os.walk(path): for fname in files: @@ -16,7 +14,7 @@ def resize_images(path, maxdim=700): if extn in ("jpg", "jpeg", "png", "gif"): im = Image.open(os.path.join(basepath, fname)) if im.size[0] > size[0] or im.size[1] > size[1]: - im.thumbnail(size, Image.ANTIALIAS) + im.thumbnail(size, Image.Resampling.LANCZOS) im.save(os.path.join(basepath, fname)) print("resized {0}".format(os.path.join(basepath, fname))) @@ -56,7 +54,7 @@ def optimize_image( image = Image.open(io.BytesIO(content)) image_format = content_type.split("/")[1] size = max_width, max_height - image.thumbnail(size, Image.LANCZOS) + image.thumbnail(size, Image.Resampling.LANCZOS) output = io.BytesIO() image.save( diff --git a/frappe/utils/password.py b/frappe/utils/password.py index f2c4b9685a..c539891ac7 100644 --- a/frappe/utils/password.py +++ b/frappe/utils/password.py @@ -213,21 +213,16 @@ def decrypt(txt, encryption_key=None): try: cipher_suite = Fernet(encode(encryption_key or get_encryption_key())) - plain_text = cstr(cipher_suite.decrypt(encode(txt))) - return plain_text + return cstr(cipher_suite.decrypt(encode(txt))) except InvalidToken: # encryption_key in site_config is changed and not valid - frappe.throw( - _("Encryption key is invalid") + "!" - if encryption_key - else _(", please check site_config.json.") - ) + frappe.throw(_("Encryption key is invalid! Please check site_config.json")) def get_encryption_key(): - from frappe.installer import update_site_config - if "encryption_key" not in frappe.local.conf: + from frappe.installer import update_site_config + encryption_key = Fernet.generate_key().decode() update_site_config("encryption_key", encryption_key) frappe.local.conf.encryption_key = encryption_key diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py index 952717434c..811a6511fd 100644 --- a/frappe/utils/pdf.py +++ b/frappe/utils/pdf.py @@ -5,10 +5,11 @@ import os import re import subprocess from distutils.version import LooseVersion +from typing import Optional import pdfkit from bs4 import BeautifulSoup -from PyPDF2 import PdfFileReader, PdfFileWriter +from PyPDF2 import PdfReader, PdfWriter import frappe from frappe import _ @@ -23,7 +24,7 @@ PDF_CONTENT_ERRORS = [ ] -def get_pdf(html, options=None, output=None): +def get_pdf(html, options=None, output: Optional[PdfWriter] = None): html = scrub_urls(html) html, options = prepare_options(html, options) @@ -35,11 +36,10 @@ def get_pdf(html, options=None, output=None): try: # Set filename property to false, so no file is actually created - filedata = pdfkit.from_string(html, False, options=options or {}) + filedata = pdfkit.from_string(html, options=options or {}, verbose=True) - # https://pythonhosted.org/PyPDF2/PdfFileReader.html - # create in-memory binary streams from filedata and create a PdfFileReader object - reader = PdfFileReader(io.BytesIO(filedata)) + # create in-memory binary streams from filedata and create a PdfReader object + reader = PdfReader(io.BytesIO(filedata)) except OSError as e: if any([error in str(e) for error in PDF_CONTENT_ERRORS]): if not filedata: @@ -47,8 +47,8 @@ def get_pdf(html, options=None, output=None): frappe.throw(_("PDF generation failed because of broken image links")) # allow pdfs with missing images if file got created - if output: # output is a PdfFileWriter object - output.appendPagesFromReader(reader) + if output: + output.append_pages_from_reader(reader) else: raise finally: @@ -58,11 +58,11 @@ def get_pdf(html, options=None, output=None): password = options["password"] if output: - output.appendPagesFromReader(reader) + output.append_pages_from_reader(reader) return output - writer = PdfFileWriter() - writer.appendPagesFromReader(reader) + writer = PdfWriter() + writer.append_pages_from_reader(reader) if "password" in options: writer.encrypt(password) diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py index 028501f306..a48d7ab84f 100644 --- a/frappe/utils/print_format.py +++ b/frappe/utils/print_format.py @@ -1,6 +1,6 @@ import os -from PyPDF2 import PdfFileWriter +from PyPDF2 import PdfWriter import frappe from frappe import _ @@ -58,7 +58,7 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options= import json - output = PdfFileWriter() + output = PdfWriter() if isinstance(options, str): options = json.loads(options) @@ -152,7 +152,7 @@ def print_by_server( cups.setServer(print_settings.server_ip) cups.setPort(print_settings.port) conn = cups.Connection() - output = PdfFileWriter() + output = PdfWriter() output = frappe.get_print( doctype, name, print_format, doc=doc, no_letterhead=no_letterhead, as_pdf=True, output=output ) diff --git a/frappe/www/login.py b/frappe/www/login.py index fbb34e43e7..1b9a8c239a 100644 --- a/frappe/www/login.py +++ b/frappe/www/login.py @@ -55,10 +55,10 @@ def get_context(context): ) signup_form_template = frappe.get_hooks("signup_form_template") - if signup_form_template and len(signup_form_template) and signup_form_template[0]: - path = signup_form_template[0] + if signup_form_template and len(signup_form_template): + path = signup_form_template[-1] if not guess_is_path(path): - path = frappe.get_attr(signup_form_template[0])() + path = frappe.get_attr(signup_form_template[-1])() else: path = "frappe/templates/signup.html" if path: diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 85ffc671fa..c8595a6f2c 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -63,7 +63,7 @@ def get_context(context): "body": body, "print_style": print_style, "comment": frappe.session.user, - "title": frappe.utils.strip_html(doc.get_title()), + "title": frappe.utils.strip_html(doc.get_title() or doc.name), "lang": frappe.local.lang, "layout_direction": "rtl" if is_rtl() else "ltr", "doctype": frappe.form_dict.doctype, diff --git a/requirements.txt b/requirements.txt index bf798fe747..1a6b6120a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,8 @@ boto3~=1.17.53 braintree~=4.8.0 chardet~=4.0.0 Click~=7.1.2 -croniter~=1.0.11 -cryptography~=3.4.7 +croniter~=1.3.5 +cryptography~=37.0.2 dropbox~=11.7.0 email-reply-parser~=0.5.12 git-url-parse~=1.2.2 @@ -21,8 +21,8 @@ googlemaps~=4.4.5 gunicorn~=20.1.0 html2text==2020.1.16 html5lib~=1.1 -ipython~=7.31.1 -Jinja2~=3.0.1 +ipython~=8.4.0 +Jinja2~=3.1.2 ldap3~=2.9 markdown2~=2.4.0 maxminddb-geolite2==2018.703 @@ -32,10 +32,10 @@ openpyxl~=3.0.7 parse~=1.19.0 passlib~=1.7.4 paytmchecksum~=1.7.0 -pdfkit~=0.6.1 -Pillow~=9.0.0 +pdfkit~=1.0.0 +Pillow~=9.1.1 premailer~=3.8.0 -psutil~=5.8.0 +psutil~=5.9.1 psycopg2-binary~=2.9.1 pyasn1~=0.4.8 pycryptodome~=3.10.1 @@ -43,29 +43,29 @@ PyJWT~=2.0.1 PyMySQL~=1.0.2 pyOpenSSL~=20.0.1 pyotp~=2.6.0 -PyPDF2~=1.26.0 +PyPDF2~=2.1.0 PyPika~=0.48.9 pypng~=0.0.20 PyQRCode~=1.2.1 python-dateutil~=2.8.1 -pytz==2021.1 +pytz==2022.1 PyYAML~=5.4.1 rauth~=0.7.3 razorpay~=1.2.0 redis~=3.5.3 requests-oauthlib~=1.3.0 -requests~=2.25.1 +requests~=2.27.1 RestrictedPython~=5.1 -rq~=1.8.0 +rq~=1.10.1 rsa>=4.1 # not directly required, pinned by Snyk to avoid a vulnerability schedule~=1.1.0 -semantic-version~=2.8.5 +semantic-version~=2.10.0 sqlparse~=0.4.1 stripe~=2.56.0 terminaltables~=3.1.0 traceback-with-variables~=2.0.4 urllib3~=1.26.4 -Werkzeug~=2.0.3 +Werkzeug~=2.1.2 Whoosh~=2.7.4 xlrd~=2.0.1 zxcvbn-python~=4.4.24 @@ -73,4 +73,3 @@ tenacity~=8.0.1 cairocffi==1.2.0 WeasyPrint==52.5 phonenumbers==8.12.40 - diff --git a/setup.py b/setup.py index 92ff63baff..ba4034a766 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,6 @@ setup( zip_safe=False, include_package_data=True, install_requires=install_requires, - dependency_links=["https://github.com/frappe/python-pdfkit.git#egg=pdfkit"], cmdclass={"clean": CleanCommand}, python_requires=">=3.8", )