diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml index d9a6ca6f59..52fa987994 100644 --- a/.github/workflows/patch-mariadb-tests.yml +++ b/.github/workflows/patch-mariadb-tests.yml @@ -10,6 +10,7 @@ concurrency: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 60 name: Patch Test diff --git a/.github/workflows/server-mariadb-tests.yml b/.github/workflows/server-mariadb-tests.yml index 588f357f26..4edf74ba71 100644 --- a/.github/workflows/server-mariadb-tests.yml +++ b/.github/workflows/server-mariadb-tests.yml @@ -14,6 +14,7 @@ concurrency: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 60 strategy: fail-fast: false @@ -128,4 +129,4 @@ jobs: fail_ci_if_error: true files: /home/runner/frappe-bench/sites/coverage.xml verbose: true - flags: server \ No newline at end of file + flags: server diff --git a/.github/workflows/server-postgres-tests.yml b/.github/workflows/server-postgres-tests.yml index 78f379837b..895af5184e 100644 --- a/.github/workflows/server-postgres-tests.yml +++ b/.github/workflows/server-postgres-tests.yml @@ -13,6 +13,7 @@ concurrency: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 60 strategy: fail-fast: false diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index fcc53ba59c..cb502f68a7 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -13,6 +13,7 @@ concurrency: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 60 strategy: fail-fast: false diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js index 0253e8fd43..629ae72eb8 100644 --- a/cypress/integration/report_view.js +++ b/cypress/integration/report_view.js @@ -7,6 +7,8 @@ context('Report View', () => { cy.visit('/app/website'); cy.insert_doc('DocType', custom_submittable_doctype, true); cy.clear_cache(); + }); + it('Field with enabled allow_on_submit should be editable.', () => { cy.insert_doc(doctype_name, { 'title': 'Doc 1', 'description': 'Random Text', @@ -14,8 +16,6 @@ context('Report View', () => { // submit document 'docstatus': 1 }, true).as('doc'); - }); - it('Field with enabled allow_on_submit should be editable.', () => { cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update'); cy.visit(`/app/List/${doctype_name}/Report`); // check status column added from docstatus diff --git a/frappe/__init__.py b/frappe/__init__.py index 4218aa113b..895bdcaddc 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1523,8 +1523,8 @@ def format(*args, **kwargs): import frappe.utils.formatters return frappe.utils.formatters.format_value(*args, **kwargs) -def get_print(doctype=None, name=None, print_format=None, style=None, - html=None, as_pdf=False, doc=None, output=None, no_letterhead=0, password=None): +def get_print(doctype=None, name=None, print_format=None, style=None, html=None, + as_pdf=False, doc=None, output=None, no_letterhead=0, password=None, pdf_options=None): """Get Print Format for given document. :param doctype: DocType of document. @@ -1543,15 +1543,15 @@ def get_print(doctype=None, name=None, print_format=None, style=None, local.form_dict.doc = doc local.form_dict.no_letterhead = no_letterhead - options = None + pdf_options = pdf_options or {} if password: - options = {'password': password} + pdf_options['password'] = password if not html: html = get_response_content("printview") if as_pdf: - return get_pdf(html, output = output, options = options) + return get_pdf(html, options=pdf_options, output=output) else: return html diff --git a/frappe/app.py b/frappe/app.py index 8e1534e7ef..70575fe2f1 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -120,6 +120,8 @@ def init_request(request): else: frappe.connect(set_admin_as_user=False) + request.max_content_length = frappe.local.conf.get('max_file_size') or 10 * 1024 * 1024 + make_form_dict(request) if request.method != "OPTIONS": diff --git a/frappe/commands/site.py b/frappe/commands/site.py index c5f78e2680..3c7f2f5525 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -461,6 +461,7 @@ def migrate(context, skip_failing=False, skip_search_index=False): skip_search_index=skip_search_index ) finally: + print() frappe.destroy() if not context.sites: raise SiteNotSpecifiedError diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index e311b8db6a..41b607b192 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -791,10 +791,11 @@ def request(context, args=None, path=None): @click.command('make-app') @click.argument('destination') @click.argument('app_name') -def make_app(destination, app_name): +@click.option('--no-git', is_flag=True, default=False, help='Do not initialize git repository for the app') +def make_app(destination, app_name, no_git=False): "Creates a boilerplate app" from frappe.utils.boilerplate import make_boilerplate - make_boilerplate(destination, app_name) + make_boilerplate(destination, app_name, no_git=no_git) @click.command('set-config') diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 4d22075b78..54ddbce2c4 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -146,25 +146,43 @@ def add_attachments(name, attachments): }) _file.save(ignore_permissions=True) -@frappe.whitelist(allow_guest=True) -def mark_email_as_seen(name=None): +@frappe.whitelist(allow_guest=True, methods=("GET",)) +def mark_email_as_seen(name: str = None): try: - if name and frappe.db.exists("Communication", name) and not frappe.db.get_value("Communication", name, "read_by_recipient"): - frappe.db.set_value("Communication", name, "read_by_recipient", 1) - frappe.db.set_value("Communication", name, "delivery_status", "Read") - frappe.db.set_value("Communication", name, "read_by_recipient_on", get_datetime()) - frappe.db.commit() + update_communication_as_read(name) + frappe.db.commit() # nosemgrep: this will be called in a GET request + except Exception: frappe.log_error(frappe.get_traceback()) - finally: - # Return image as response under all circumstances - from PIL import Image - import io - im = Image.new('RGBA', (1, 1)) - im.putdata([(255,255,255,0)]) - buffered_obj = io.BytesIO() - im.save(buffered_obj, format="PNG") - frappe.response["type"] = 'binary' - frappe.response["filename"] = "imaginary_pixel.png" - frappe.response["filecontent"] = buffered_obj.getvalue() + finally: + frappe.response.update({ + "type": "binary", + "filename": "imaginary_pixel.png", + "filecontent": ( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" + b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r" + b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0" + b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82" + ) + }) + +def update_communication_as_read(name): + if not name or not isinstance(name, str): + return + + communication = frappe.db.get_value( + "Communication", + name, + "read_by_recipient", + as_dict=True + ) + + if not communication or communication.read_by_recipient: + return + + frappe.db.set_value("Communication", name, { + "read_by_recipient": 1, + "delivery_status": "Read", + "read_by_recipient_on": get_datetime() + }) diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py index 684328a4c7..21faf98e49 100644 --- a/frappe/core/doctype/data_import/exporter.py +++ b/frappe/core/doctype/data_import/exporter.py @@ -191,7 +191,7 @@ class Exporter: [format_column_name(df) for df in self.fields if df.parent == child_table_doctype] ) ) - data = frappe.db.get_list( + data = frappe.db.get_all( child_table_doctype, filters={ "parent": ("in", parent_names), diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 4df9ef3132..0021240106 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -716,13 +716,11 @@ def delete_file(path): os.remove(path) - - +@frappe.whitelist() def get_max_file_size(): return cint(conf.get('max_file_size')) or 10485760 - def has_permission(doc, ptype=None, user=None): has_access = False user = user or frappe.session.user diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index 98d2d72fc2..389e18dd4c 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -2,15 +2,22 @@ # License: MIT. See LICENSE import frappe - from frappe.model.document import Document desk_properties = ("search_bar", "notifications", "list_sidebar", "bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard") +STANDARD_ROLES = ( + "Administrator", + "System Manager", + "Script Manager", + "All", + "Guest" +) + class Role(Document): def before_rename(self, old, new, merge=False): - if old in ("Guest", "Administrator", "System Manager", "All"): + if old in STANDARD_ROLES: frappe.throw(frappe._("Standard roles cannot be renamed")) def after_insert(self): @@ -23,7 +30,7 @@ class Role(Document): self.set_desk_properties() def disable_role(self): - if self.name in ("Guest", "Administrator", "System Manager", "All"): + if self.name in STANDARD_ROLES: frappe.throw(frappe._("Standard roles cannot be disabled")) else: self.remove_roles() diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index ea31e76a57..cf05ce0c15 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -599,7 +599,7 @@ "fieldname": "desk_theme", "fieldtype": "Select", "label": "Desk Theme", - "options": "Light\nDark" + "options": "Light\nDark\nAutomatic" }, { "fieldname": "module_profile", @@ -669,7 +669,7 @@ } ], "max_attachments": 5, - "modified": "2021-10-27 17:17:16.098457", + "modified": "2021-11-17 17:17:16.098457", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 86fd1cb4a6..b127cf5f0c 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1046,7 +1046,7 @@ def generate_keys(user): @frappe.whitelist() def switch_theme(theme): - if theme in ["Dark", "Light"]: + if theme in ["Dark", "Light", "Automatic"]: frappe.db.set_value("User", frappe.session.user, "desk_theme", theme) def get_enabled_users(): diff --git a/frappe/database/database.py b/frappe/database/database.py index a7dd9b6b66..49187f9eaa 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -568,11 +568,10 @@ class Database(object): def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True): names = list(filter(None, names)) - if names: return self.get_all(doctype, - fields=['name', field], - filters=[['name', 'in', names]], + fields=field, + filters=names, debug=debug, as_list=1, run=run) else: return {} diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json index 5768f00f32..2f67c36fc0 100644 --- a/frappe/desk/doctype/event/event.json +++ b/frappe/desk/doctype/event/event.json @@ -53,7 +53,7 @@ }, { "fieldname": "subject", - "fieldtype": "Data", + "fieldtype": "Small Text", "in_global_search": 1, "in_list_view": 1, "label": "Subject", @@ -277,10 +277,11 @@ "icon": "fa fa-calendar", "idx": 1, "links": [], - "modified": "2020-01-14 21:47:15.825287", + "modified": "2021-11-18 05:06:24.881742", "modified_by": "Administrator", "module": "Desk", "name": "Event", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json index fc12022e89..fc535fa405 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.json +++ b/frappe/desk/doctype/notification_settings/notification_settings.json @@ -15,7 +15,9 @@ "enable_email_energy_point", "enable_email_share", "user", - "seen" + "seen", + "system_notifications_section", + "energy_points_system_notifications" ], "fields": [ { @@ -84,15 +86,27 @@ "fieldtype": "Check", "hidden": 1, "label": "Seen" + }, + { + "fieldname": "system_notifications_section", + "fieldtype": "Section Break", + "label": "System Notifications" + }, + { + "default": "1", + "fieldname": "energy_points_system_notifications", + "fieldtype": "Check", + "label": "Energy Points" } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-04 12:54:57.989317", + "modified": "2021-11-16 12:18:46.955501", "modified_by": "Administrator", "module": "Desk", "name": "Notification Settings", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { @@ -111,4 +125,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/installer.py b/frappe/installer.py index d1a13fdaab..9eed44ea15 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -240,6 +240,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) if not dry_run: remove_from_installed_apps(app_name) + frappe.get_single('Installed Applications').update_versions() frappe.db.commit() click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green") diff --git a/frappe/model/naming.py b/frappe/model/naming.py index deea6698b3..f3d68f3715 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -175,6 +175,8 @@ def parse_naming_series(parts, doctype='', doc=''): part = today.strftime("%d") elif e == 'YYYY': part = today.strftime('%Y') + elif e == 'WW': + part = determine_consecutive_week_number(today) elif e == 'timestamp': part = str(today) elif e == 'FY': @@ -193,6 +195,19 @@ def parse_naming_series(parts, doctype='', doc=''): return n +def determine_consecutive_week_number(datetime): + """Determines the consecutive calendar week""" + m = datetime.month + # ISO 8601 calandar week + w = datetime.strftime('%V') + # Ensure consecutiveness for the first and last days of a year + if m == 1 and int(w) >= 52: + w = '00' + elif m == 12 and int(w) <= 1: + w = '53' + return w + + def getseries(key, digits): # series created ? # Using frappe.qb as frappe.get_values does not allow order_by=None diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json index babbae248d..f45de7637d 100644 --- a/frappe/printing/doctype/print_settings/print_settings.json +++ b/frappe/printing/doctype/print_settings/print_settings.json @@ -10,6 +10,8 @@ "repeat_header_footer", "column_break_4", "pdf_page_size", + "pdf_page_height", + "pdf_page_width", "view_link_in_email", "with_letterhead", "allow_print_for_draft", @@ -56,7 +58,7 @@ "fieldname": "pdf_page_size", "fieldtype": "Select", "label": "PDF Page Size", - "options": "A4\nLetter" + "options": "A0\nA1\nA2\nA3\nA4\nA5\nA6\nA7\nA8\nA9\nB0\nB1\nB2\nB3\nB4\nB5\nB6\nB7\nB8\nB9\nB10\nC5E\nComm10E\nDLE\nExecutive\nFolio\nLedger\nLegal\nLetter\nTabloid\nCustom" }, { "fieldname": "view_link_in_email", @@ -156,6 +158,18 @@ "fieldname": "font_size", "fieldtype": "Float", "label": "Font Size" + }, + { + "depends_on": "eval:doc.pdf_page_size == \"Custom\"", + "fieldname": "pdf_page_height", + "fieldtype": "Float", + "label": "PDF Page Height (in mm)" + }, + { + "depends_on": "eval:doc.pdf_page_size == \"Custom\"", + "fieldname": "pdf_page_width", + "fieldtype": "Float", + "label": "PDF Page Width (in mm)" } ], "icon": "fa fa-cog", diff --git a/frappe/printing/doctype/print_settings/print_settings.py b/frappe/printing/doctype/print_settings/print_settings.py index ff00317cf8..3253cea2dc 100644 --- a/frappe/printing/doctype/print_settings/print_settings.py +++ b/frappe/printing/doctype/print_settings/print_settings.py @@ -8,14 +8,23 @@ from frappe.utils import cint from frappe.model.document import Document + class PrintSettings(Document): + def validate(self): + if self.pdf_page_size == "Custom" and not ( + self.pdf_page_height and self.pdf_page_width + ): + frappe.throw(_("Page height and width cannot be zero")) + def on_update(self): frappe.clear_cache() + @frappe.whitelist() def is_print_server_enabled(): - if not hasattr(frappe.local, 'enable_print_server'): - frappe.local.enable_print_server = cint(frappe.db.get_single_value('Print Settings', - 'enable_print_server')) + if not hasattr(frappe.local, "enable_print_server"): + frappe.local.enable_print_server = cint( + frappe.db.get_single_value("Print Settings", "enable_print_server") + ) return frappe.local.enable_print_server diff --git a/frappe/public/images/ui-states/404.png b/frappe/public/images/ui-states/404.png new file mode 100644 index 0000000000..1cbf7eeee0 Binary files /dev/null and b/frappe/public/images/ui-states/404.png differ diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index a53368d67a..2855c6ae7c 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -64,6 +64,19 @@ frappe.Application = class Application { } }); + frappe.ui.add_system_theme_switch_listener(); + const root = document.documentElement; + + const observer = new MutationObserver(() => { + frappe.ui.set_theme(); + }); + observer.observe(root, { + attributes: true, + attributeFilter: ['data-theme-mode'] + }); + + frappe.ui.set_theme(); + // page container this.make_page_container(); this.set_route(); diff --git a/frappe/public/js/frappe/file_uploader/FilePreview.vue b/frappe/public/js/frappe/file_uploader/FilePreview.vue index 43dbacb17d..5972a975f2 100644 --- a/frappe/public/js/frappe/file_uploader/FilePreview.vue +++ b/frappe/public/js/frappe/file_uploader/FilePreview.vue @@ -29,21 +29,26 @@ +
+ + {{ file.error_message }} + +
-
+
- +
@@ -89,18 +94,18 @@ export default { return this.file.doc ? this.file.doc.is_private : this.file.private; }, uploaded() { - return this.file.total && this.file.total === this.file.progress && !this.file.failed; + return this.file.request_succeeded; }, is_image() { return this.file.file_obj.type.startsWith('image'); }, is_optimizable() { let is_svg = this.file.file_obj.type == 'image/svg+xml'; - return this.is_image && !is_svg; + return this.is_image && !is_svg && !this.uploaded && !this.file.failed; }, is_cropable() { let croppable_types = ['image/jpeg', 'image/png']; - return !this.uploaded && !this.file.uploading && croppable_types.includes(this.file.file_obj.type); + return !this.uploaded && !this.file.uploading && !this.file.failed && croppable_types.includes(this.file.file_obj.type); }, progress() { let value = Math.round((this.file.progress * 100) / this.file.total); @@ -208,4 +213,9 @@ export default { align-items: center; padding-top: 0.25rem; } + +.file-error { + font-size: var(--text-sm); + font-weight: var(--text-bold); +} diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue index 90aa545941..167b4955fa 100644 --- a/frappe/public/js/frappe/file_uploader/FileUploader.vue +++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue @@ -197,6 +197,7 @@ export default { show_image_cropper: false, crop_image_with_index: -1, trigger_upload: false, + close_dialog: false, hide_dialog_footer: false, allow_take_photo: false, allow_web_link: true, @@ -218,6 +219,12 @@ export default { } }); } + if (this.restrictions.max_file_size == null) { + frappe.call('frappe.core.doctype.file.file.get_max_file_size') + .then(res => { + this.restrictions.max_file_size = Number(res.message); + }); + } }, watch: { files(newvalue, oldvalue) { @@ -289,6 +296,8 @@ export default { progress: 0, total: 0, failed: false, + request_succeeded: false, + error_message: null, uploading: false, private: !is_image } @@ -329,9 +338,17 @@ export default { if (!is_correct_type) { console.warn('File skipped because of invalid file type', file); + frappe.show_alert({ + message: __('File "{0}" was skipped because of invalid file type', [file.name]), + indicator: 'orange' + }); } if (!valid_file_size) { console.warn('File skipped because of invalid file size', file.size, file); + frappe.show_alert({ + message: __('File "{0}" was skipped because size exceeds {1} MB', [file.name, max_file_size / (1024 * 1024)]), + indicator: 'orange' + }); } return is_correct_type && valid_file_size; @@ -357,9 +374,10 @@ export default { let selected_file = this.$refs.file_browser.selected_node; if (!selected_file.value) { frappe.msgprint(__('Click on a file to select it.')); + this.close_dialog = true; return Promise.reject(); } - + this.close_dialog = true; return this.upload_file({ file_url: selected_file.file_url }); @@ -368,9 +386,11 @@ export default { let file_url = this.$refs.web_link.url; if (!file_url) { frappe.msgprint(__('Invalid URL')); + this.close_dialog = true; return Promise.reject(); } file_url = decodeURI(file_url) + this.close_dialog = true; return this.upload_file({ file_url }); @@ -383,6 +403,7 @@ export default { this.on_success && this.on_success(file); }) ); + this.close_dialog = true; return Promise.all(promises); }, upload_file(file, i) { @@ -410,6 +431,7 @@ export default { xhr.onreadystatechange = () => { if (xhr.readyState == XMLHttpRequest.DONE) { if (xhr.status === 200) { + file.request_succeeded = true; let r = null; let file_doc = null; try { @@ -426,15 +448,24 @@ export default { if (this.on_success) { this.on_success(file_doc, r); } + + if (i == this.files.length - 1 && this.files.every(file => file.request_succeeded)) { + this.close_dialog = true; + } + } else if (xhr.status === 403) { + file.failed = true; let response = JSON.parse(xhr.responseText); - frappe.msgprint({ - title: __('Not permitted'), - indicator: 'red', - message: response._error_message - }); + file.error_message = `Not permitted. ${response._error_message || ''}`; + + } else if (xhr.status === 413) { + file.failed = true; + file.error_message = 'Size exceeds the maximum allowed file size.'; + } else { file.failed = true; + file.error_message = xhr.status === 0 ? 'XMLHttpRequest Error' : `${xhr.status} : ${xhr.statusText}`; + let error = null; try { error = JSON.parse(xhr.responseText); diff --git a/frappe/public/js/frappe/file_uploader/index.js b/frappe/public/js/frappe/file_uploader/index.js index 87bc1c8ec8..ec90b19a1a 100644 --- a/frappe/public/js/frappe/file_uploader/index.js +++ b/frappe/public/js/frappe/file_uploader/index.js @@ -67,6 +67,12 @@ export default class FileUploader { } }); + this.uploader.$watch('close_dialog', (close_dialog) => { + if (close_dialog) { + this.dialog && this.dialog.hide(); + } + }); + this.uploader.$watch('hide_dialog_footer', (hide_dialog_footer) => { if (hide_dialog_footer) { this.dialog && this.dialog.footer.addClass('hide'); @@ -84,10 +90,8 @@ export default class FileUploader { upload_files() { this.dialog && this.dialog.get_primary_btn().prop('disabled', true); - return this.uploader.upload_files() - .then(() => { - this.dialog && this.dialog.hide(); - }); + this.dialog && this.dialog.get_secondary_btn().prop('disabled', true); + return this.uploader.upload_files(); } make_dialog() { diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 080a1cbb48..280eac3941 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -3,6 +3,11 @@ frappe.provide('frappe.utils.utils'); frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.form.ControlData { static horizontal = false + async make() { + await frappe.require(this.required_libs); + super.make(); + } + make_wrapper() { // Create the elements for map area super.make_wrapper(); @@ -196,4 +201,17 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f this.editableLayers.removeLayer(l); }); } + + get required_libs() { + return [ + "assets/frappe/js/lib/leaflet/easy-button.css", + "assets/frappe/js/lib/leaflet/L.Control.Locate.css", + "assets/frappe/js/lib/leaflet/leaflet.draw.css", + "assets/frappe/js/lib/leaflet/leaflet.css", + "assets/frappe/js/lib/leaflet/leaflet.js", + "assets/frappe/js/lib/leaflet/easy-button.js", + "assets/frappe/js/lib/leaflet/leaflet.draw.js", + "assets/frappe/js/lib/leaflet/L.Control.Locate.js", + ]; + } }; diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 9d5e7cbe09..df4dbf09e7 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -14,6 +14,7 @@ frappe.ui.form.Dashboard = class FormDashboard { this.progress_area = this.make_section({ css_class: 'progress-area', hidden: 1, + collapsible: 1, is_dashboard_section: 1, }); @@ -21,6 +22,7 @@ frappe.ui.form.Dashboard = class FormDashboard { label: __("Overview"), css_class: 'form-heatmap', hidden: 1, + collapsible: 1, is_dashboard_section: 1, body_html: `
@@ -32,6 +34,7 @@ frappe.ui.form.Dashboard = class FormDashboard { label: __("Graph"), css_class: 'form-graph', hidden: 1, + collapsible: 1, is_dashboard_section: 1 }); @@ -40,6 +43,7 @@ frappe.ui.form.Dashboard = class FormDashboard { label: __("Stats"), css_class: 'form-stats', hidden: 1, + collapsible: 1, is_dashboard_section: 1, body_html: this.stats_area_row }); @@ -50,6 +54,7 @@ frappe.ui.form.Dashboard = class FormDashboard { label: __("Connections"), css_class: 'form-links', hidden: 1, + collapsible: 1, is_dashboard_section: 1, body_html: this.transactions_area }); @@ -84,9 +89,10 @@ frappe.ui.form.Dashboard = class FormDashboard { hidden, body_html, make_card: true, + collapsible: 1, is_dashboard_section: 1 }; - return new Section(this.frm.layout.wrapper, options).body; + return new Section(this.parent, options).body; } add_progress(title, percent, message) { @@ -203,7 +209,7 @@ frappe.ui.form.Dashboard = class FormDashboard { after_refresh() { // show / hide new buttons (if allowed) this.links_area.body.find('.btn-new').each((i, el) => { - if (this.frm.can_create($(this).attr('data-doctype'))) { + if (this.frm.can_create($(el).attr('data-doctype'))) { $(el).removeClass('hidden'); } }); diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 75d68b12db..27281d8927 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -156,8 +156,11 @@ frappe.ui.form.Form = class FrappeForm { let dashboard_parent = $('
'); - let main_page = this.layout.tabs.length ? this.layout.tabs[0].wrapper : this.layout.wrapper; - main_page.prepend(dashboard_parent); + if (this.layout.tabs.length) { + this.layout.tabs[0].wrapper.prepend(dashboard_parent); + } else { + dashboard_parent.insertAfter(this.layout.wrapper.find('.form-message')); + } this.dashboard = new frappe.ui.form.Dashboard(dashboard_parent, this); this.tour = new frappe.ui.form.FormTour({ diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 7710c82ee7..0de6b1db0d 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -245,7 +245,7 @@ frappe.ui.form.Layout = class Layout { } make_section(df) { - this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout); + this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout, this); // append to layout fields if (df) { diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 2cf2ac38a9..e412b1dec8 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -267,7 +267,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm { render_edit_in_full_page_link() { var me = this; this.dialog.add_custom_action( - `${frappe.utils.icon('edit', 'xs')} ${__("Edit in full page")}`, + `${__("Edit in full page")}`, () => me.open_doc(true) ); } diff --git a/frappe/public/js/frappe/form/section.js b/frappe/public/js/frappe/form/section.js index e0120f6afc..b0ec491ce6 100644 --- a/frappe/public/js/frappe/form/section.js +++ b/frappe/public/js/frappe/form/section.js @@ -1,5 +1,6 @@ export default class Section { - constructor(parent, df, card_layout) { + constructor(parent, df, card_layout, layout) { + this.layout = layout; this.card_layout = card_layout; this.parent = parent; this.df = df || {}; @@ -25,6 +26,7 @@ export default class Section { ${this.df.is_dashboard_section ? "form-dashboard-section" : "form-section"} ${ make_card ? "card-section" : "" }"> `).appendTo(this.parent); + this.layout && this.layout.sections.push(this); if (this.df) { if (this.df.label) { diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index ee6e6d753c..94ec9d4e67 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -24,51 +24,84 @@ export default class BulkOperations { return; } - if (valid_docs.length > 0) { - const dialog = new frappe.ui.Dialog({ - title: __('Print Documents'), - fields: [ - { - 'fieldtype': 'Select', - 'label': __('Letter Head'), - 'fieldname': 'letter_sel', - 'default': __('No Letterhead'), - options: this.get_letterhead_options() - }, - { - 'fieldtype': 'Select', - 'label': __('Print Format'), - 'fieldname': 'print_sel', - options: frappe.meta.get_print_formats(this.doctype) - } - ] - }); - - dialog.set_primary_action(__('Print'), args => { - if (!args) return; - const default_print_format = frappe.get_meta(this.doctype).default_print_format; - const with_letterhead = args.letter_sel == __("No Letterhead") ? 0 : 1; - const print_format = args.print_sel ? args.print_sel : default_print_format; - const json_string = JSON.stringify(valid_docs); - const letterhead = args.letter_sel; - const w = window.open('/api/method/frappe.utils.print_format.download_multi_pdf?' + - 'doctype=' + encodeURIComponent(this.doctype) + - '&name=' + encodeURIComponent(json_string) + - '&format=' + encodeURIComponent(print_format) + - '&no_letterhead=' + (with_letterhead ? '0' : '1') + - '&letterhead=' + encodeURIComponent(letterhead) - ); - - if (!w) { - frappe.msgprint(__('Please enable pop-ups')); - return; - } - }); - - dialog.show(); - } else { + if (valid_docs.length === 0) { frappe.msgprint(__('Select atleast 1 record for printing')); + return; } + + const dialog = new frappe.ui.Dialog({ + title: __('Print Documents'), + fields: [{ + fieldtype: 'Select', + label: __('Letter Head'), + fieldname: 'letter_sel', + default: __('No Letterhead'), + options: this.get_letterhead_options() + }, + { + fieldtype: 'Select', + label: __('Print Format'), + fieldname: 'print_sel', + options: frappe.meta.get_print_formats(this.doctype) + }, + { + fieldtype: 'Select', + label: __('Page Size'), + fieldname: 'page_size', + options: frappe.meta.get_print_sizes(), + default: print_settings.pdf_page_size + }, + { + fieldtype: 'Float', + label: __('Page Height (in mm)'), + fieldname: 'page_height', + depends_on: 'eval:doc.page_size == "Custom"', + default: print_settings.pdf_page_height + }, + { + fieldtype: 'Float', + label: __('Page Width (in mm)'), + fieldname: 'page_width', + depends_on: 'eval:doc.page_size == "Custom"', + default: print_settings.pdf_page_width + }] + }); + + dialog.set_primary_action(__('Print'), args => { + if (!args) return; + const default_print_format = frappe.get_meta(this.doctype).default_print_format; + const with_letterhead = args.letter_sel == __("No Letterhead") ? 0 : 1; + const print_format = args.print_sel ? args.print_sel : default_print_format; + const json_string = JSON.stringify(valid_docs); + const letterhead = args.letter_sel; + + let pdf_options; + if (args.page_size === "Custom") { + if (args.page_height === 0 || args.page_width === 0) { + frappe.throw(__('Page height and width cannot be zero')); + } + pdf_options = JSON.stringify({ "page-height": args.page_height, "page-width": args.page_width }); + } else { + pdf_options = JSON.stringify({ "page-size": args.page_size }); + } + + const w = window.open( + '/api/method/frappe.utils.print_format.download_multi_pdf?' + + 'doctype=' + encodeURIComponent(this.doctype) + + '&name=' + encodeURIComponent(json_string) + + '&format=' + encodeURIComponent(print_format) + + '&no_letterhead=' + (with_letterhead ? '0' : '1') + + '&letterhead=' + encodeURIComponent(letterhead) + + '&options=' + encodeURIComponent(pdf_options) + ); + + if (!w) { + frappe.msgprint(__('Please enable pop-ups')); + return; + } + }); + + dialog.show(); } get_letterhead_options () { diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 07c8acef27..64530e15ef 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -307,6 +307,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } update_checkbox(target) { + if (!this.$checkbox_actions) return; + let $check_all_checkbox = this.$checkbox_actions.find(".list-check-all"); if ($check_all_checkbox.prop("checked") && target && !target.prop("checked")) { diff --git a/frappe/public/js/frappe/model/meta.js b/frappe/public/js/frappe/model/meta.js index 6ee9084adc..3c9ddc4d96 100644 --- a/frappe/public/js/frappe/model/meta.js +++ b/frappe/public/js/frappe/model/meta.js @@ -192,6 +192,15 @@ $.extend(frappe.meta, { } }, + get_print_sizes: function() { + return [ + "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", + "B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "B10", + "C5E", "Comm10E", "DLE", "Executive", "Folio", "Ledger", "Legal", + "Letter", "Tabloid", "Custom" + ]; + }, + get_print_formats: function(doctype) { var print_format_list = ["Standard"]; var default_print_format = locals.DocType[doctype].default_print_format; diff --git a/frappe/public/js/frappe/ui/theme_switcher.js b/frappe/public/js/frappe/ui/theme_switcher.js index 4524472415..2c1d93a2ec 100644 --- a/frappe/public/js/frappe/ui/theme_switcher.js +++ b/frappe/public/js/frappe/ui/theme_switcher.js @@ -42,7 +42,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { } refresh() { - this.current_theme = document.documentElement.getAttribute("data-theme") || "light"; + this.current_theme = document.documentElement.getAttribute("data-theme-mode") || "light"; this.fetch_themes().then(() => { this.render(); }); @@ -54,10 +54,17 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { { name: "light", label: __("Frappe Light"), + info: __("Light Theme") }, { name: "dark", label: __("Timeless Night"), + info: __("Dark Theme") + }, + { + name: "automatic", + label: __("Automatic"), + info: __("Uses system's theme to switch between light and dark mode") } ]; @@ -74,11 +81,15 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { } get_preview_html(theme) { + const is_auto_theme = theme.name === "automatic"; const preview = $(`
-
+
-
${frappe.utils.icon('tick', 'xs')}
+
+ ${frappe.utils.icon('tick', 'xs')} +
@@ -112,13 +123,14 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { toggle_theme(theme) { this.current_theme = theme.toLowerCase(); - document.documentElement.setAttribute("data-theme", this.current_theme); + document.documentElement.setAttribute("data-theme-mode", this.current_theme); frappe.show_alert("Theme Changed", 3); frappe.xcall("frappe.core.doctype.user.user.switch_theme", { theme: toTitle(theme) }); } + show() { this.dialog.show(); } @@ -127,3 +139,22 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { this.dialog.hide(); } }; + +frappe.ui.add_system_theme_switch_listener = () => { + frappe.ui.dark_theme_media_query.addEventListener('change', () => { + frappe.ui.set_theme(); + }); +}; + +frappe.ui.dark_theme_media_query = window.matchMedia("(prefers-color-scheme: dark)"); + +frappe.ui.set_theme = (theme) => { + const root = document.documentElement; + let theme_mode = root.getAttribute("data-theme-mode"); + if (!theme) { + if (theme_mode === "automatic") { + theme = frappe.ui.dark_theme_media_query.matches ? 'dark' : 'light'; + } + } + root.setAttribute("data-theme", theme || theme_mode); +}; \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js index 6d1d7228e3..502837bcd7 100644 --- a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js +++ b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js @@ -305,7 +305,7 @@ frappe.search.AwesomeBar = class AwesomeBar { index: 80, default: "Calculator", onclick: function() { - frappe.msgprint(formatted_value, "Result"); + frappe.msgprint(formatted_value, __("Result")); } }); } catch(e) { @@ -317,10 +317,10 @@ frappe.search.AwesomeBar = class AwesomeBar { make_random(txt) { if(txt.toLowerCase().includes('random')) { this.options.push({ - label: "Generate Random Password", + label: __("Generate Random Password"), value: frappe.utils.get_random(16), onclick: function() { - frappe.msgprint(frappe.utils.get_random(16), "Result"); + frappe.msgprint(frappe.utils.get_random(16), __("Result")); } }) } diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index 32e3669caf..b0d66ccec5 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -129,7 +129,7 @@ function format_currency(v, currency, decimals) { } if (symbol) - return symbol + " " + format_number(v, format, decimals); + return __(symbol) + " " + format_number(v, format, decimals); else return format_number(v, format, decimals); } diff --git a/frappe/public/js/frappe/views/container.js b/frappe/public/js/frappe/views/container.js index 126feea16e..cf1d6c9466 100644 --- a/frappe/public/js/frappe/views/container.js +++ b/frappe/public/js/frappe/views/container.js @@ -42,7 +42,6 @@ frappe.views.Container = class Container { cur_page = this; if(this.page && this.page.label === label) { $(this.page).trigger('show'); - return; } var me = this; diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 04cc1b9880..448b3f6fd2 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -634,6 +634,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.render_datatable(); this.add_chart_buttons_to_toolbar(true); this.add_card_button_to_toolbar(); + this.$report.show(); } else { this.data = []; this.toggle_nothing_to_show(true); @@ -882,7 +883,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { hide_loading_screen() { this.$loading.hide(); - this.$report.show(); } get_chart_options(data) { @@ -1789,6 +1789,19 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.$chart.toggle(flag); this.$summary.toggle(flag); } + + get_checked_items(only_docnames) { + const indexes = this.datatable.rowmanager.getCheckedRows(); + + return indexes.reduce((items, i) => { + if (i === undefined) return items; + + const item = this.data[i]; + items.push(only_docnames ? item.name : item); + return items; + }, []); + } + // backward compatibility get get_values() { return this.get_filter_values; diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 8866a4b2af..c26b63a9f6 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -106,6 +106,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { get_args() { const args = super.get_args(); + delete args.group_by; this.group_by_control.set_args(args); return args; diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 82d056bb31..11e567af42 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -211,7 +211,7 @@ export default class NumberCardWidget extends Widget { const symbol = number_parts[1] || ''; const formatted_number = $(frappe.format(number_parts[0], df)).text(); - this.formatted_number = formatted_number + ' ' + symbol; + this.formatted_number = formatted_number + ' ' + __(symbol); } render_number() { diff --git a/frappe/public/scss/desk/theme_switcher.scss b/frappe/public/scss/desk/theme_switcher.scss index 00e3f35be8..924c2edd9d 100644 --- a/frappe/public/scss/desk/theme_switcher.scss +++ b/frappe/public/scss/desk/theme_switcher.scss @@ -1,6 +1,6 @@ .modal-body .theme-grid { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); grid-gap: 18px; .background { @@ -9,7 +9,7 @@ border-radius: var(--border-radius-lg); overflow: hidden; cursor: pointer; - height: 160px; + height: 120px; position: relative; &:hover { @@ -28,6 +28,7 @@ margin-right: var(--margin-sm); border-radius: var(--border-radius-full); + z-index: 1; } } @@ -72,6 +73,7 @@ border-radius: var(--border-radius-sm); height: 10px; width: 20px; + z-index: 1; } .text { @@ -80,4 +82,17 @@ height: 10px; width: 40px; } +} + +// TODO: Replace with better alternative +[data-is-auto-theme="true"] { + .background::after { + content: ""; + top: 0; + right: 0; + height: 100%; + width: 50%; + background: var(--gray-900); + position: absolute; + } } \ No newline at end of file diff --git a/frappe/public/scss/website/error-state.scss b/frappe/public/scss/website/error-state.scss new file mode 100644 index 0000000000..6f88009ecb --- /dev/null +++ b/frappe/public/scss/website/error-state.scss @@ -0,0 +1,18 @@ +.error-page { + text-align: center; + + .img-404 { + width: 40%; + margin: var(--margin-2xl) auto; + + @include media-breakpoint-down(sm) { + width: 80% + } + } + + .back-to-home { + font-size: var(--text-base); + } +} + + diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss index 2957a0b499..9c84e99a5a 100644 --- a/frappe/public/scss/website/index.scss +++ b/frappe/public/scss/website/index.scss @@ -26,6 +26,7 @@ @import 'doc'; @import 'navbar'; @import 'footer'; +@import 'error-state'; .ql-editor.read-mode { padding: 0; diff --git a/frappe/sessions.py b/frappe/sessions.py index 9a0f19df80..91c8bbdecb 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -158,6 +158,8 @@ def get(): bootinfo["setup_complete"] = cint(frappe.db.get_single_value('System Settings', 'setup_complete')) bootinfo["is_first_startup"] = cint(frappe.db.get_single_value('System Settings', 'is_first_startup')) + bootinfo['desk_theme'] = frappe.db.get_value("User", frappe.session.user, "desk_theme") or 'Light' + return bootinfo @frappe.whitelist() diff --git a/frappe/social/doctype/energy_point_log/energy_point_log.py b/frappe/social/doctype/energy_point_log/energy_point_log.py index 3ffabcd241..86843302e9 100644 --- a/frappe/social/doctype/energy_point_log/energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/energy_point_log.py @@ -32,7 +32,9 @@ class EnergyPointLog(Document): frappe.cache().hdel('energy_points', self.user) frappe.publish_realtime('update_points', after_commit=True) - if self.type != 'Review': + if self.type != 'Review' and \ + frappe.get_cached_value('Notification Settings', self.user, 'energy_points_system_notifications'): + reference_user = self.user if self.type == 'Auto' else self.owner notification_doc = { 'type': 'Energy Point', diff --git a/frappe/social/doctype/energy_point_log/test_energy_point_log.py b/frappe/social/doctype/energy_point_log/test_energy_point_log.py index c2bcbde825..a1f4503c34 100644 --- a/frappe/social/doctype/energy_point_log/test_energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/test_energy_point_log.py @@ -8,6 +8,18 @@ from frappe.utils.testutils import add_custom_field, clear_custom_fields from frappe.desk.form.assign_to import add as assign_to class TestEnergyPointLog(unittest.TestCase): + @classmethod + def setUpClass(cls): + settings = frappe.get_single('Energy Point Settings') + settings.enabled = 1 + settings.save() + + @classmethod + def tearDownClass(cls): + settings = frappe.get_single('Energy Point Settings') + settings.enabled = 0 + settings.save() + def setUp(self): frappe.cache().delete_value('energy_point_rule_map') @@ -336,4 +348,4 @@ def assign_users_to_todo(todo_name, users): 'assign_to': [user], 'doctype': 'ToDo', 'name': todo_name - }) \ No newline at end of file + }) diff --git a/frappe/social/doctype/energy_point_settings/energy_point_settings.json b/frappe/social/doctype/energy_point_settings/energy_point_settings.json index 0001b26529..d1f9aea3d0 100644 --- a/frappe/social/doctype/energy_point_settings/energy_point_settings.json +++ b/frappe/social/doctype/energy_point_settings/energy_point_settings.json @@ -1,229 +1,70 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, + "actions": [], "creation": "2019-03-19 13:17:51.710241", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "enabled", + "section_break_2", + "review_levels", + "point_allocation_periodicity", + "last_point_allocation_date" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fetch_if_empty": 0, + "default": "0", "fieldname": "enabled", "fieldtype": "Check", - "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": "Enabled", - "length": 0, - "no_copy": 0, - "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 + "label": "Enabled" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "enabled", - "fetch_if_empty": 0, "fieldname": "section_break_2", - "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, - "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 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "review_levels", "fieldtype": "Table", - "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": "Review Levels", - "length": 0, - "no_copy": 0, - "options": "Review Level", - "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 + "options": "Review Level" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Weekly", - "fetch_if_empty": 0, "fieldname": "point_allocation_periodicity", "fieldtype": "Select", - "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": "Point Allocation Periodicity", - "length": 0, - "no_copy": 0, - "options": "Daily\nWeekly\nMonthly", - "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 + "options": "Daily\nWeekly\nMonthly" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "last_point_allocation_date", "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": "Last Point Allocation Date", - "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 + "read_only": 1 } ], - "has_web_view": 0, "hide_toolbar": 1, - "idx": 0, - "in_create": 0, - "is_submittable": 0, "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2019-03-26 19:10:14.087840", + "links": [], + "modified": "2021-11-16 23:24:01.366928", "modified_by": "Administrator", "module": "Social", "name": "Energy Point Settings", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, - "report": 0, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], "quick_entry": 1, - "read_only": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/social/doctype/energy_point_settings/test_energy_point_settings.py b/frappe/social/doctype/energy_point_settings/test_energy_point_settings.py new file mode 100644 index 0000000000..3b0a756878 --- /dev/null +++ b/frappe/social/doctype/energy_point_settings/test_energy_point_settings.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt + +# import frappe +import unittest + +class TestEnergyPointSettings(unittest.TestCase): + pass diff --git a/frappe/templates/includes/comments/comments.py b/frappe/templates/includes/comments/comments.py index e352c7368b..99afb580d8 100644 --- a/frappe/templates/includes/comments/comments.py +++ b/frappe/templates/includes/comments/comments.py @@ -28,16 +28,6 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference frappe.msgprint(_('Comments cannot have links or email addresses')) return False - comments_count = frappe.db.count("Comment", { - "comment_type": "Comment", - "comment_email": comment_email, - "creation": (">", add_to_date(now(), hours=-1)) - }) - - if comments_count > 20: - frappe.msgprint(_('Hourly comment limit reached for: {0}').format(frappe.bold(comment_email))) - return False - comment = doc.add_comment( text=comment, comment_email=comment_email, @@ -54,14 +44,17 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference comment.name, _("View Comment"))) - # notify creator - frappe.sendmail( - recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner, - subject=_('New Comment on {0}: {1}').format(doc.doctype, doc.name), - message=content, - reference_doctype=doc.doctype, - reference_name=doc.name - ) + if doc.doctype == "Blog Post" and not doc.enable_email_notification: + pass + else: + # notify creator + frappe.sendmail( + recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner, + subject=_('New Comment on {0}: {1}').format(doc.doctype, doc.name), + message=content, + reference_doctype=doc.doctype, + reference_name=doc.name + ) # revert with template if all clear (no backlinks) template = frappe.get_template("templates/includes/comments/comment.html") diff --git a/frappe/templates/includes/feedback/feedback.py b/frappe/templates/includes/feedback/feedback.py index 62fdc3f746..279ff05e6d 100644 --- a/frappe/templates/includes/feedback/feedback.py +++ b/frappe/templates/includes/feedback/feedback.py @@ -12,8 +12,8 @@ from frappe.website.doctype.blog_settings.blog_settings import get_feedback_limi @rate_limit(key='reference_name', limit=get_feedback_limit, seconds=60*60) def give_feedback(reference_doctype, reference_name, like): like = frappe.parse_json(like) - doc = frappe.get_doc(reference_doctype, reference_name) - if doc.disable_feedback == 1: + ref_doc = frappe.get_doc(reference_doctype, reference_name) + if ref_doc.disable_feedback == 1: return filters = { @@ -33,7 +33,7 @@ def give_feedback(reference_doctype, reference_name, like): doc.save(ignore_permissions=True) subject = _('Feedback on {0}: {1}').format(reference_doctype, reference_name) - send_mail(doc, subject) + ref_doc.enable_email_notification and send_mail(doc, subject) return doc def send_mail(feedback, subject): diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 55c76a00c2..9986e45999 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -195,9 +195,9 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" {% else %} diff --git a/frappe/tests/test_boilerplate.py b/frappe/tests/test_boilerplate.py index 259d5a9194..6a9544b2e9 100644 --- a/frappe/tests/test_boilerplate.py +++ b/frappe/tests/test_boilerplate.py @@ -11,14 +11,7 @@ from frappe.utils.boilerplate import make_boilerplate class TestBoilerPlate(unittest.TestCase): @classmethod - def tearDownClass(cls): - - bench_path = frappe.utils.get_bench_path() - test_app_dir = os.path.join(bench_path, "apps", "test_app") - if os.path.exists(test_app_dir): - shutil.rmtree(test_app_dir) - - def test_create_app(self): + def setUpClass(cls): title = "Test App" description = "This app's description contains 'single quotes' and \"double quotes\"." publisher = "Test Publisher" @@ -27,7 +20,7 @@ class TestBoilerPlate(unittest.TestCase): color = "" app_license = "MIT" - user_input = [ + cls.user_input = [ title, description, publisher, @@ -37,22 +30,21 @@ class TestBoilerPlate(unittest.TestCase): app_license, ] - bench_path = frappe.utils.get_bench_path() - apps_dir = os.path.join(bench_path, "apps") - app_name = "test_app" + cls.bench_path = frappe.utils.get_bench_path() + cls.apps_dir = os.path.join(cls.bench_path, "apps") + cls.app_names = ("test_app", "test_app_no_git") + cls.gitignore_file = ".gitignore" + cls.git_folder = ".git" - with patch("builtins.input", side_effect=user_input): - make_boilerplate(apps_dir, app_name) - - root_paths = [ - app_name, + cls.root_paths = [ "requirements.txt", "README.md", "setup.py", "license.txt", - ".git", + cls.git_folder, + cls.gitignore_file ] - paths_inside_app = [ + cls.paths_inside_app = [ "__init__.py", "hooks.py", "patches.txt", @@ -60,25 +52,68 @@ class TestBoilerPlate(unittest.TestCase): "www", "config", "modules.txt", - "public", - app_name, + "public" ] - new_app_dir = os.path.join(bench_path, apps_dir, app_name) + @classmethod + def tearDownClass(cls): + test_app_dirs = (os.path.join(cls.bench_path, "apps", app_name) for app_name in cls.app_names) + for test_app_dir in test_app_dirs: + if os.path.exists(test_app_dir): + shutil.rmtree(test_app_dir) + def test_create_app(self): + with patch("builtins.input", side_effect=self.user_input): + make_boilerplate(self.apps_dir, self.app_names[0]) + + new_app_dir = os.path.join(self.bench_path, self.apps_dir, self.app_names[0]) + + paths = self.get_paths(new_app_dir, self.app_names[0]) + for path in paths: + self.assertTrue( + os.path.exists(path), + msg=f"{path} should exist in {self.app_names[0]} app" + ) + + self.check_parsable_python_files(new_app_dir) + + def test_create_app_without_git_init(self): + with patch("builtins.input", side_effect=self.user_input): + make_boilerplate(self.apps_dir, self.app_names[1], no_git=True) + + new_app_dir = os.path.join(self.apps_dir, self.app_names[1]) + + paths = self.get_paths(new_app_dir, self.app_names[1]) + for path in paths: + if os.path.basename(path) in (self.git_folder, self.gitignore_file): + self.assertFalse( + os.path.exists(path), + msg=f"{path} shouldn't exist in {self.app_names[1]} app" + ) + else: + self.assertTrue( + os.path.exists(path), + msg=f"{path} should exist in {self.app_names[1]} app" + ) + + self.check_parsable_python_files(new_app_dir) + + def get_paths(self, app_dir, app_name): all_paths = list() - for path in root_paths: - all_paths.append(os.path.join(new_app_dir, path)) + for path in self.root_paths: + all_paths.append(os.path.join(app_dir, path)) - for path in paths_inside_app: - all_paths.append(os.path.join(new_app_dir, app_name, path)) + all_paths.append(os.path.join(app_dir, app_name)) - for path in all_paths: - self.assertTrue(os.path.exists(path), msg=f"{path} should exist in new app") + for path in self.paths_inside_app: + all_paths.append(os.path.join(app_dir, app_name, path)) + return all_paths + + def check_parsable_python_files(self, app_dir): # check if python files are parsable - python_files = glob.glob(new_app_dir + "**/*.py", recursive=True) + python_files = glob.glob(app_dir + "**/*.py", recursive=True) for python_file in python_files: with open(python_file) as p: diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index c048e23949..94389cd7a3 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -5,6 +5,7 @@ import gzip import json import os import shlex +import shutil import subprocess import sys import unittest @@ -102,14 +103,24 @@ def exists_in_backup(doctypes, file): class BaseTestCommands(unittest.TestCase): def execute(self, command, kwargs=None): site = {"site": frappe.local.site} + cmd_input = None if kwargs: + cmd_input = kwargs.get("cmd_input", None) + if cmd_input: + if not isinstance(cmd_input, bytes): + raise Exception( + f"The input should be of type bytes, not {type(cmd_input).__name__}" + ) + + del kwargs["cmd_input"] kwargs.update(site) else: kwargs = site + self.command = " ".join(command.split()).format(**kwargs) print("{0}$ {1}{2}".format(color.silver, self.command, color.nc)) command = shlex.split(self.command) - self._proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self._proc = subprocess.run(command, input=cmd_input, stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.stdout = clean(self._proc.stdout) self.stderr = clean(self._proc.stderr) self.returncode = clean(self._proc.returncode) @@ -466,6 +477,28 @@ class TestCommands(BaseTestCommands): self.assertEqual(self.returncode, 0) self.assertEqual(check_password('Administrator', 'test2'), 'Administrator') + def test_make_app(self): + user_input = [ + b"Test App", # title + b"This app's description contains 'single quotes' and \"double quotes\".", # description + b"Test Publisher", # publisher + b"example@example.org", # email + b"", # icon + b"", # color + b"MIT" # app_license + ] + app_name = "testapp0" + apps_path = os.path.join(frappe.utils.get_bench_path(), "apps") + test_app_path = os.path.join(apps_path, app_name) + self.execute(f"bench make-app {apps_path} {app_name}", {"cmd_input": b'\n'.join(user_input)}) + self.assertEqual(self.returncode, 0) + self.assertTrue( + os.path.exists(test_app_path) + ) + + # cleanup + shutil.rmtree(test_app_path) + class RemoveAppUnitTests(unittest.TestCase): def test_delete_modules(self): diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 9077655dc9..6e49ef3c54 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -34,7 +34,21 @@ class TestDB(unittest.TestCase): self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name >= 't' ORDER BY MODIFIED DESC""")[0][0], frappe.db.get_value("User", {"name": [">=", "t"]})) - self.assertIn("concat_ws", frappe.db.get_value("User", filters={"name": "Administrator"}, fieldname=Concat_ws(" ", "LastName"), run=False).lower()) + self.assertIn( + "concat_ws", + frappe.db.get_value( + "User", + filters={"name": "Administrator"}, + fieldname=Concat_ws(" ", "LastName"), + run=False, + ).lower(), + ) + self.assertEqual( + frappe.db.sql("select email from tabUser where name='Administrator' order by modified DESC"), + frappe.db.get_values( + "User", filters=[["name", "=", "Administrator"]], fieldname="email" + ), + ) def test_set_value(self): todo1 = frappe.get_doc(dict(doctype='ToDo', description = 'test_set_value 1')).insert() diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 47ad029274..29cec8b230 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -157,7 +157,7 @@ class TestDocument(unittest.TestCase): def test_varchar_length(self): d = self.test_insert() - d.subject = "abcde"*100 + d.sender = "abcde"*100 + "@user.com" self.assertRaises(frappe.CharacterLengthExceededError, d.save) def test_xss_filter(self): @@ -251,4 +251,4 @@ class TestDocument(unittest.TestCase): 'doctype': 'Test Formatted', 'currency': 100000 }) - self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') \ No newline at end of file + self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py index 4435a8bb20..3031d3e344 100644 --- a/frappe/tests/test_naming.py +++ b/frappe/tests/test_naming.py @@ -7,6 +7,7 @@ from frappe.utils import now_datetime from frappe.model.naming import getseries from frappe.model.naming import append_number_if_name_exists, revert_series_if_last +from frappe.model.naming import determine_consecutive_week_number, parse_naming_series class TestNaming(unittest.TestCase): def tearDown(self): @@ -60,6 +61,34 @@ class TestNaming(unittest.TestCase): self.assertEqual(todo.name, 'TODO-{month}-{status}-{series}'.format( month=now_datetime().strftime('%m'), status=todo.status, series=series)) + def test_format_autoname_for_consecutive_week_number(self): + ''' + Test if braced params are replaced for consecutive week number in format autoname + ''' + doctype = 'ToDo' + + todo_doctype = frappe.get_doc('DocType', doctype) + todo_doctype.autoname = 'format:TODO-{WW}-{##}' + todo_doctype.save() + + description = 'Format' + + todo = frappe.new_doc(doctype) + todo.description = description + todo.insert() + + series = getseries('', 2) + + series = str(int(series)-1) + + if len(series) < 2: + series = '0' + series + + week = determine_consecutive_week_number(now_datetime()) + + self.assertEqual(todo.name, 'TODO-{week}-{series}'.format( + week=week, series=series)) + def test_revert_series(self): from datetime import datetime year = datetime.now().year @@ -150,3 +179,32 @@ class TestNaming(unittest.TestCase): self.assertEqual(amended_doc.name, "{}-CANC-1".format(original_name)) submittable_doctype.delete() + + def test_parse_naming_series_for_consecutive_week_number(self): + week = determine_consecutive_week_number(now_datetime()) + name = parse_naming_series('PREFIX-.WW.-SUFFIX') + expected_name = 'PREFIX-{}-SUFFIX'.format(week) + self.assertEqual(name, expected_name) + + def test_determine_consecutive_week_number(self): + from datetime import datetime + + dt = datetime.fromisoformat("2019-12-31") + w = determine_consecutive_week_number(dt) + self.assertEqual(w, "53") + + dt = datetime.fromisoformat("2020-01-01") + w = determine_consecutive_week_number(dt) + self.assertEqual(w, "01") + + dt = datetime.fromisoformat("2020-01-15") + w = determine_consecutive_week_number(dt) + self.assertEqual(w, "03") + + dt = datetime.fromisoformat("2021-01-01") + w = determine_consecutive_week_number(dt) + self.assertEqual(w, "00") + + dt = datetime.fromisoformat("2021-12-31") + w = determine_consecutive_week_number(dt) + self.assertEqual(w, "52") diff --git a/frappe/tests/test_printview.py b/frappe/tests/test_printview.py new file mode 100644 index 0000000000..0fc4c4869b --- /dev/null +++ b/frappe/tests/test_printview.py @@ -0,0 +1,22 @@ +import unittest + +import frappe +from frappe.www.printview import get_html_and_style + + +class PrintViewTest(unittest.TestCase): + def test_print_view_without_errors(self): + + user = frappe.get_last_doc("User") + + messages_before = frappe.get_message_log() + ret = get_html_and_style(doc=user.as_json(), print_format="Standard", no_letterhead=1) + messages_after = frappe.get_message_log() + + if len(messages_after) > len(messages_before): + new_messages = messages_after[len(messages_before):] + self.fail("Print view showing error/warnings: \n" + + "\n".join(str(msg) for msg in new_messages)) + + # html should exist + self.assertTrue(bool(ret["html"])) diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index d0dd1669b4..91f7dbb2f8 100755 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -3,7 +3,7 @@ import frappe, os, re, git from frappe.utils import touch_file, cstr -def make_boilerplate(dest, app_name): +def make_boilerplate(dest, app_name, no_git=False): if not os.path.exists(dest): print("Destination directory does not exist") return @@ -63,9 +63,6 @@ def make_boilerplate(dest, app_name): with open(os.path.join(dest, hooks.app_name, "MANIFEST.in"), "w") as f: f.write(frappe.as_unicode(manifest_template.format(**hooks))) - with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f: - f.write(frappe.as_unicode(gitignore_template.format(app_name = hooks.app_name))) - with open(os.path.join(dest, hooks.app_name, "requirements.txt"), "w") as f: f.write("# frappe -- https://github.com/frappe/frappe is installed via 'bench init'") @@ -98,11 +95,16 @@ def make_boilerplate(dest, app_name): with open(os.path.join(dest, hooks.app_name, hooks.app_name, "config", "docs.py"), "w") as f: f.write(frappe.as_unicode(docs_template.format(**hooks))) - # initialize git repository app_directory = os.path.join(dest, hooks.app_name) - app_repo = git.Repo.init(app_directory) - app_repo.git.add(A=True) - app_repo.index.commit("feat: Initialize App") + + if not no_git: + with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f: + f.write(frappe.as_unicode(gitignore_template.format(app_name = hooks.app_name))) + + # initialize git repository + app_repo = git.Repo.init(app_directory) + app_repo.git.add(A=True) + app_repo.index.commit("feat: Initialize App") print("'{app}' created at {path}".format(app=app_name, path=app_directory)) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index f5c46dc184..d39d32d8df 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -868,7 +868,7 @@ def fmt_money(amount, precision=None, currency=None, format=None): if currency and frappe.defaults.get_global_default("hide_currency_symbol") != "Yes": symbol = frappe.db.get_value("Currency", currency, "symbol", cache=True) or currency - amount = symbol + " " + amount + amount = frappe._(symbol) + " " + amount return amount diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py index 90bb4f63de..9a7c0889b5 100644 --- a/frappe/utils/pdf.py +++ b/frappe/utils/pdf.py @@ -95,7 +95,7 @@ def prepare_options(html, options): 'quiet': None, # 'no-outline': None, 'encoding': "UTF-8", - #'load-error-handling': 'ignore' + # 'load-error-handling': 'ignore' }) if not options.get("margin-right"): @@ -111,8 +111,21 @@ def prepare_options(html, options): options.update(get_cookie_options()) # page size - if not options.get("page-size"): - options['page-size'] = frappe.db.get_single_value("Print Settings", "pdf_page_size") or "A4" + pdf_page_size = ( + options.get("page-size") + or frappe.db.get_single_value("Print Settings", "pdf_page_size") + or "A4" + ) + + if pdf_page_size == "Custom": + options["page-height"] = options.get("page-height") or frappe.db.get_single_value( + "Print Settings", "pdf_page_height" + ) + options["page-width"] = options.get("page-width") or frappe.db.get_single_value( + "Print Settings", "pdf_page_width" + ) + else: + options["page-size"] = pdf_page_size return html, options diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py index 6dfa3a350b..06f15ced27 100644 --- a/frappe/utils/print_format.py +++ b/frappe/utils/print_format.py @@ -11,7 +11,7 @@ base_template_path = "www/printview.html" standard_format = "templates/print_formats/standard.html" @frappe.whitelist() -def download_multi_pdf(doctype, name, format=None, no_letterhead=0): +def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=None): """ Concatenate multiple docs as PDF . @@ -54,18 +54,21 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=0): import json output = PdfFileWriter() + if isinstance(options, str): + options = json.loads(options) + if not isinstance(doctype, dict): result = json.loads(name) # Concatenating pdf files for i, ss in enumerate(result): - output = frappe.get_print(doctype, ss, format, as_pdf = True, output = output, no_letterhead=no_letterhead) + output = frappe.get_print(doctype, ss, format, as_pdf=True, output=output, no_letterhead=no_letterhead, pdf_options=options) frappe.local.response.filename = "{doctype}.pdf".format(doctype=doctype.replace(" ", "-").replace("/", "-")) else: for doctype_name in doctype: for doc_name in doctype[doctype_name]: try: - output = frappe.get_print(doctype_name, doc_name, format, as_pdf = True, output = output, no_letterhead=no_letterhead) + output = frappe.get_print(doctype_name, doc_name, format, as_pdf=True, output=output, no_letterhead=no_letterhead, pdf_options=options) except Exception: frappe.log_error("Permission Error on doc {} of doctype {}".format(doc_name, doctype_name)) frappe.local.response.filename = "{}.pdf".format(name) diff --git a/frappe/website/doctype/blog_post/blog_post.json b/frappe/website/doctype/blog_post/blog_post.json index 9400016c48..b05293f28b 100644 --- a/frappe/website/doctype/blog_post/blog_post.json +++ b/frappe/website/doctype/blog_post/blog_post.json @@ -17,6 +17,7 @@ "published", "featured", "hide_cta", + "enable_email_notification", "disable_comments", "disable_feedback", "section_break_5", @@ -197,6 +198,13 @@ "fieldname": "disable_feedback", "fieldtype": "Check", "label": "Disable Feedback" + }, + { + "default": "1", + "description": "Enable email notification for any comment or feedback on your Blog Post.", + "fieldname": "enable_email_notification", + "fieldtype": "Check", + "label": "Enable Email Notification" } ], "has_web_view": 1, @@ -206,7 +214,7 @@ "is_published_field": "published", "links": [], "max_attachments": 5, - "modified": "2021-09-13 17:19:35.436045", + "modified": "2021-11-23 10:42:01.759723", "modified_by": "Administrator", "module": "Website", "name": "Blog Post", @@ -240,4 +248,4 @@ "sort_order": "ASC", "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py index 2b723c107a..3536896a5f 100644 --- a/frappe/website/doctype/blog_post/blog_post.py +++ b/frappe/website/doctype/blog_post/blog_post.py @@ -104,7 +104,7 @@ class BlogPost(WebsiteGenerator): context.parents = [{"name": _("Home"), "route":"/"}, {"name": "Blog", "route": "/blog"}, {"label": context.category.title, "route":context.category.route}] - context.guest_allowed = frappe.db.get_single_value("Blog Settings", "allow_guest_to_comment", cache=True) + context.guest_allowed = frappe.db.get_single_value("Blog Settings", "allow_guest_to_comment") def fetch_cta(self): if frappe.db.get_single_value("Blog Settings", "show_cta_in_blog", cache=True): diff --git a/frappe/workflow/doctype/workflow_state/workflow_state.json b/frappe/workflow/doctype/workflow_state/workflow_state.json index a08f713bb1..be5804f390 100644 --- a/frappe/workflow/doctype/workflow_state/workflow_state.json +++ b/frappe/workflow/doctype/workflow_state/workflow_state.json @@ -112,7 +112,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-02-20 13:33:44.011509", + "modified": "2021-11-22 17:56:40.495232", "modified_by": "Administrator", "module": "Workflow", "name": "Workflow State", @@ -137,6 +137,10 @@ "share": 1, "submit": 0, "write": 1 + }, + { + "role": "All", + "select": 1 } ], "quick_entry": 1, diff --git a/frappe/www/404.html b/frappe/www/404.html index a796924f1a..c03b5d3e96 100644 --- a/frappe/www/404.html +++ b/frappe/www/404.html @@ -3,32 +3,22 @@ {%- block title -%}{{_("Not Found")}}{%- endblock -%} {% block page_content %} - + -
-
- {{_("Page Missing or Moved")}} + +
+ +
+

+ {{ _("There's nothing here") }} +

+
+ {{ _("The page you are looking for have gone missing.") }} +
+
-

{{_("The page you are looking for is missing. This could be because it is moved or there is a typo in the link.")}}

-
-

{{ _("Error Code: {0}").format('404') }}

- + {% endblock %} \ No newline at end of file diff --git a/frappe/www/app.html b/frappe/www/app.html index 68a6dc8e86..37579066e0 100644 --- a/frappe/www/app.html +++ b/frappe/www/app.html @@ -1,5 +1,5 @@ - +