diff --git a/attributions.md b/attributions.md index 5afc9f9d46..611b9b1d9a 100644 --- a/attributions.md +++ b/attributions.md @@ -11,6 +11,7 @@ The following 3rd-party software packages may be used by or distributed with ### Icon Fonts diff --git a/cypress/integration/web_form.js b/cypress/integration/web_form.js index f8d0ba38d9..9173bfaeb3 100644 --- a/cypress/integration/web_form.js +++ b/cypress/integration/web_form.js @@ -142,10 +142,9 @@ context("Web Form", () => { it("Custom Breadcrumbs", () => { cy.visit("/app/web-form/note"); - cy.findByRole("tab", { name: "Form Settings" }).click(); - cy.get(".form-section .section-head").contains("Customization").click(); + cy.findByRole("tab", { name: "Customization" }).click(); cy.fill_field("breadcrumbs", '[{"label": _("Notes"), "route":"note"}]', "Code"); - cy.get(".form-section .section-head").contains("Customization").click(); + cy.get(".form-tabs .nav-item .nav-link").contains("Customization").click(); cy.save(); cy.visit("/note/Note 1"); @@ -188,6 +187,7 @@ context("Web Form", () => { cy.fill_field("title", " Edited"); cy.get(".web-form-actions button").contains("Save").click(); + cy.get(".success-page .edit-button").click(); cy.get_field("title").should("have.value", "Note 1 Edited"); }); diff --git a/frappe/config/__init__.py b/frappe/config/__init__.py index ebd75cd70a..947f61e9e0 100644 --- a/frappe/config/__init__.py +++ b/frappe/config/__init__.py @@ -8,10 +8,8 @@ from frappe.desk.moduleview import ( ) -def get_modules_from_all_apps_for_user(user=None): - if not user: - user = frappe.session.user - +def get_modules_from_all_apps_for_user(user: str = None) -> list[dict]: + user = user or frappe.session.user all_modules = get_modules_from_all_apps() global_blocked_modules = frappe.get_doc("User", "Administrator").get_blocked_modules() user_blocked_modules = frappe.get_doc("User", user).get_blocked_modules() @@ -61,7 +59,7 @@ def get_all_empty_tables_by_module(): empty_tables_by_module = {} for doctype, module in results: - if "tab" + doctype in empty_tables: + if f"tab{doctype}" in empty_tables: if module in empty_tables_by_module: empty_tables_by_module[module].append(doctype) else: diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py index 9a127e567e..093418e345 100644 --- a/frappe/core/notifications.py +++ b/frappe/core/notifications.py @@ -32,11 +32,10 @@ def get_things_todo(as_list=False): if as_list: return data - else: - return data[0][0] + return data[0][0] -def get_todays_events(as_list=False): +def get_todays_events(as_list: bool = False): """Returns a count of todays events in calendar""" from frappe.desk.doctype.event.event import get_events from frappe.utils import nowdate diff --git a/frappe/coverage.py b/frappe/coverage.py index ffa3576818..8519b04c55 100644 --- a/frappe/coverage.py +++ b/frappe/coverage.py @@ -24,6 +24,15 @@ STANDARD_EXCLUSIONS = [ "*/patches/*", ] +# tested via commands' test suite +TESTED_VIA_CLI = [ + "*/frappe/installer.py", + "*/frappe/build.py", + "*/frappe/database/__init__.py", + "*/frappe/database/db_manager.py", + "*/frappe/database/**/setup_db.py", +] + FRAPPE_EXCLUSIONS = [ "*/tests/*", "*/commands/*", @@ -33,7 +42,7 @@ FRAPPE_EXCLUSIONS = [ "*frappe/setup.py", "*/doctype/*/*_dashboard.py", "*/patches/*", -] +] + TESTED_VIA_CLI class CodeCoverage: diff --git a/frappe/database/__init__.py b/frappe/database/__init__.py index 423442d344..d1b7729fee 100644 --- a/frappe/database/__init__.py +++ b/frappe/database/__init__.py @@ -54,16 +54,3 @@ def get_db(host=None, user=None, password=None, port=None, read_only=False): return frappe.database.mariadb.database.MariaDBDatabase( host, user, password, port=port, read_only=read_only ) - - -def setup_help_database(help_db_name): - import frappe - - if frappe.conf.db_type == "postgres": - import frappe.database.postgres.setup_db - - return frappe.database.postgres.setup_db.setup_help_database(help_db_name) - else: - import frappe.database.mariadb.setup_db - - return frappe.database.mariadb.setup_db.setup_help_database(help_db_name) diff --git a/frappe/database/mariadb/setup_db.py b/frappe/database/mariadb/setup_db.py index ef246712b1..392421bc7c 100644 --- a/frappe/database/mariadb/setup_db.py +++ b/frappe/database/mariadb/setup_db.py @@ -63,23 +63,6 @@ def setup_database(force, source_sql, verbose, no_mariadb_socket=False): bootstrap_database(db_name, verbose, source_sql) -def setup_help_database(help_db_name): - dbman = DbManager(get_root_connection(frappe.flags.root_login, frappe.flags.root_password)) - dbman.drop_database(help_db_name) - - # make database - if not help_db_name in dbman.get_database_list(): - try: - dbman.create_user(help_db_name, help_db_name) - except Exception as e: - # user already exists - if e.args[0] != 1396: - raise - dbman.create_database(help_db_name) - dbman.grant_all_privileges(help_db_name, help_db_name) - dbman.flush_privileges() - - def drop_user_and_database(db_name, root_login, root_password): frappe.local.db = get_root_connection(root_login, root_password) dbman = DbManager(frappe.local.db) diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index 7eee8081c0..ff14510c9c 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -75,15 +75,6 @@ def import_db_from_sql(source_sql=None, verbose=False): ) -def setup_help_database(help_db_name): - root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password) - root_conn.sql(f"DROP DATABASE IF EXISTS `{help_db_name}`") - root_conn.sql(f"DROP USER IF EXISTS {help_db_name}") - root_conn.sql(f"CREATE DATABASE `{help_db_name}`") - root_conn.sql(f"CREATE user {help_db_name} password '{help_db_name}'") - root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(help_db_name)) - - def get_root_connection(root_login=None, root_password=None): if not frappe.local.flags.root_connection: if not root_login: diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index e9104ef897..7bcb8207a7 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -205,7 +205,7 @@ def send_event_digest(): @frappe.whitelist() -def get_events(start, end, user=None, for_reminder=False, filters=None): +def get_events(start, end, user=None, for_reminder=False, filters=None) -> list[frappe._dict]: if not user: user = frappe.session.user diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 928484a5f4..ee13d31d9f 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -982,6 +982,20 @@ Object.assign(frappe.utils, { } }); }, + setup_timer(start, end, $element) { + const increment = end > start; + let counter = start; + + let interval = setInterval(() => { + increment ? counter++ : counter--; + if (increment ? counter > end : counter < end) { + clearInterval(interval); + return; + } + $element.text(counter); + }, 1000); + }, + deep_equal(a, b) { return deep_equal(a, b); }, diff --git a/frappe/public/js/frappe/web_form/web_form.js b/frappe/public/js/frappe/web_form/web_form.js index cb622ea704..ccd88f0ba3 100644 --- a/frappe/public/js/frappe/web_form/web_form.js +++ b/frappe/public/js/frappe/web_form/web_form.js @@ -20,6 +20,7 @@ export default class WebForm extends frappe.ui.FieldGroup { } make() { + this.parent.empty(); super.make(); this.set_page_breaks(); this.set_field_values(); @@ -29,7 +30,6 @@ export default class WebForm extends frappe.ui.FieldGroup { this.setup_primary_action(); } - this.setup_footer_actions(); this.setup_previous_next_button(); this.toggle_section(); @@ -73,14 +73,6 @@ export default class WebForm extends frappe.ui.FieldGroup { this.is_multi_step_form = true; } - setup_footer_actions() { - if (this.is_multi_step_form) return; - - if ($(".web-form-container").height() > 600) { - $(".web-form-footer").removeClass("hide"); - } - } - setup_previous_next_button() { let me = this; @@ -380,45 +372,45 @@ export default class WebForm extends frappe.ui.FieldGroup { return false; } - edit() { - window.location.href = window.location.pathname + "/edit"; - } - - cancel() { - let path = window.location.pathname; - if (this.is_new) { - path = path.replace("/new", ""); - } else { - path = path.replace("/edit", ""); - } - window.location.href = path; - } - handle_success(data) { // TODO: remove this (used for payments app) if (this.accept_payment && !this.doc.paid) { window.location.href = data; } - const success_message = this.success_message || __("Submitted"); + if (!this.is_new) { + $(".success-title").text(__("Updated")); + $(".success-message").text(__("Your form has been successfully updated")); + } - frappe.toast({ message: success_message, indicator: "green" }); + $(".web-form-container").hide(); + $(".success-page").removeClass("hide"); - // redirect - setTimeout(() => { - let path = window.location.pathname; + if (this.success_url) { + frappe.utils.setup_timer(5, 0, $(".time")); + setTimeout(() => { + window.location.href = this.success_url; + }, 5000); + } else { + this.render_success_page(data); + } + } - if (this.success_url) { - path = this.success_url; - } else if (this.login_required) { - if (this.is_new && data.name) { - path = path.replace("/new", ""); - path = path + "/" + data.name; - } else if (this.is_form_editable) { - path = path.replace("/edit", ""); - } - } - window.location.href = path; - }, 3000); + render_success_page(data) { + if (this.allow_edit && data.name) { + $(".success-page").append(` + + ${__("Edit your response", null, "Button in web form")} + + `); + } + + if (this.login_required && !this.allow_multiple && !this.show_list && data.name) { + $(".success-page").append(` + + ${__("View your response", null, "Button in web form")} + + `); + } } } diff --git a/frappe/public/scss/website/web_form.scss b/frappe/public/scss/website/web_form.scss index 17aa4fba9c..0a4350e0bf 100644 --- a/frappe/public/scss/website/web_form.scss +++ b/frappe/public/scss/website/web_form.scss @@ -6,36 +6,69 @@ margin: auto; h1 { - font-size: 1.9rem; + font-size: 2.25rem; margin-top: 0; margin-bottom: 0; } - .web-form-container { + .web-form-banner-image { + margin: -4rem -14rem 5rem; + padding-top: 3rem; + position: relative; + + img { + position: absolute; + object-fit: cover; + width: 100%; + height: 250px; + z-index: -1; + } + } + + .web-form-header { border: 1px solid var(--dark-border-color); - border-radius: var(--border-radius-md); - padding: 2rem; + border-bottom: none; + border-top-left-radius: var(--border-radius-md); + border-top-right-radius: var(--border-radius-md); + background-color: var(--fg-color); + padding: 2rem 2rem 0; - .web-form-header { - display: flex; - justify-content: space-between; - margin: 0 -2rem 1rem; - padding: 0 2rem 1rem; - border-bottom: 1px solid var(--border-color); + .breadcrumb-container { + padding: 0px; + margin: 0 0 2rem; - .web-form-actions { - align-self: center; + ol.breadcrumb { + padding: 0px; } } - .web-form-introduction { - color: var(--text-muted); - margin-bottom: 2rem; + .web-form-head { + border-bottom: 1px solid var(--dark-border-color); + padding-bottom: 1.25rem; - p { + .title { + display: flex; + justify-content: space-between; + } + + .web-form-introduction { color: var(--text-muted); + margin-top: 1.25rem; + + p { + color: var(--text-muted); + } } } + } + + .web-form { + background-color: var(--fg-color); + padding: 1.25rem 2rem 2rem; + border: 1px solid var(--dark-border-color); + border-top: none; + border-bottom-left-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); .web-form-wrapper { .form-control { @@ -52,7 +85,7 @@ } .form-column { - padding: 0 var(--padding-md); + padding: 0 var(--padding-sm); &:first-child { padding-left: 0; @@ -66,6 +99,34 @@ padding: 0; } } + + .web-form-skeleton { + .box-group { + display: flex; + gap: 20px; + margin-bottom: 15px; + + .box-container { + width: 100%; + + .box { + background-color: var(--control-bg); + border-radius: var(--border-radius); + } + + .box-label { + height: 20px; + width: 100px; + margin-bottom: 0.5rem; + } + + .box-area { + height: 34px; + width: 100%; + } + } + } + } } .web-form-footer { @@ -83,33 +144,115 @@ padding: 0.5rem; display: flex; align-items: center; - } - } - } - .attachments { - margin: 1rem -2rem 0; - padding: 1rem 2rem 0; - border-top: 1px solid var(--border-color); + .slides-progress { + display: flex; + margin-right: .5rem; - .attachment { - display: flex; - justify-content: space-between; - gap: 6px; - max-width: 300px; - color: var(--text-muted); - font-size: var(--text-md); + .slide-step { + @include flex(flex, center, center, null); - &:hover { - text-decoration: none; - .file-name span { - text-decoration: underline; + height: 18px; + width: 18px; + border-radius: var(--border-radius-full); + border: 1px solid var(--gray-300); + margin: 0 var(--margin-xs); + background-color: var(--card-bg); + + .slide-step-indicator { + height: 6px; + width: 6px; + background-color: var(--gray-300); + border-radius: var(--border-radius-full); + } + + .slide-step-complete { + display: none; + + .icon-xs { + height: 10px; + width: 10px; + } + } + + &.active { + border: 1px solid var(--primary); + + .slide-step-indicator { + display: block; + background-color: var(--primary); + } + } + + &.step-success:not(.active) { + background-color: var(--primary); + border: 1px solid var(--primary); + + .slide-step-indicator { + display: none; + } + + .slide-step-complete { + display: flex; + + .icon use { + stroke-width: 2; + stroke: var(--white); + } + } + } + } } } } } } + .attachments { + margin-top: 2rem; + padding: 2rem; + border-radius: var(--border-radius); + border: 1px solid var(--dark-border-color); + + .attachment { + display: flex; + justify-content: space-between; + gap: 6px; + color: var(--text-muted); + font-size: var(--text-md); + + &:hover { + text-decoration: none; + .file-name span { + text-decoration: underline; + } + } + } + } + + .success-page { + background-color: var(--fg-color); + padding: 2rem; + border: 1px solid var(--dark-border-color); + border-radius: var(--border-radius); + text-align: center; + + svg.icon { + width: 5rem; + height: 5rem; + margin: 1rem; + } + + h2 { + margin-top: 0; + margin-bottom: 0; + } + + .success-message { + margin-bottom: 1.6rem; + } + } + .web-list-container { min-height: 470px; border: 1px solid var(--dark-border-color); @@ -119,6 +262,8 @@ .web-list-header { display: flex; justify-content: space-between; + border-bottom: 1px solid var(--dark-border-color); + padding-bottom: 1.25rem; .web-list-actions { align-self: center; @@ -128,9 +273,7 @@ .web-list-filters { display: flex; flex-wrap: wrap; - margin: 1rem -2rem 0; - padding: 1rem 2rem 0; - border-top: 1px solid var(--border-color); + margin: 1.25rem 0; gap: 10px; .form-group.frappe-control { @@ -158,7 +301,6 @@ .web-list-table { overflow: auto; - margin: 1rem -2rem 0; .table { border-bottom: 1px solid var(--border-color); @@ -171,14 +313,6 @@ font-weight: normal; color: var(--text-muted); - &:first-child { - padding-left: 1.5rem; - } - - &:last-child { - padding-right: 1.5rem; - } - input[type="checkbox"] { margin-bottom: -2px; } @@ -192,19 +326,10 @@ td { font-size: 13px; border-top: 1px solid var(--border-color); - - &:first-child { - padding-left: 1.5rem; - } - - &:last-child { - padding-right: 1.5rem; - } } } input[type="checkbox"] { - margin-left: 0.5rem; margin-top: 2px; } @@ -234,63 +359,4 @@ } } } - - .slides-progress { - display: flex; - margin-right: .5rem; - - .slide-step { - @include flex(flex, center, center, null); - - height: 18px; - width: 18px; - border-radius: var(--border-radius-full); - border: 1px solid var(--gray-300); - margin: 0 var(--margin-xs); - background-color: var(--card-bg); - - .slide-step-indicator { - height: 6px; - width: 6px; - background-color: var(--gray-300); - border-radius: var(--border-radius-full); - } - - .slide-step-complete { - display: none; - - .icon-xs { - height: 10px; - width: 10px; - } - } - - &.active { - border: 1px solid var(--primary); - - .slide-step-indicator { - display: block; - background-color: var(--primary); - } - } - - &.step-success:not(.active) { - background-color: var(--primary); - border: 1px solid var(--primary); - - .slide-step-indicator { - display: none; - } - - .slide-step-complete { - display: flex; - - .icon use { - stroke-width: 2; - stroke: var(--white); - } - } - } - } - } } diff --git a/frappe/tests/data/load_sleep.py b/frappe/tests/data/load_sleep.py new file mode 100644 index 0000000000..1794bcf6fe --- /dev/null +++ b/frappe/tests/data/load_sleep.py @@ -0,0 +1,4 @@ +# File for testing lazy_import util via test_lazy_import_module +import time + +print("Module `frappe.tests.data.load_sleep` loaded") diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index cb8c3d266b..86d9075561 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -713,3 +713,12 @@ class TestBenchBuild(BaseTestCommands): CURRENT_SIZE * (1 + JS_ASSET_THRESHOLD), f"Default JS bundle size increased by {JS_ASSET_THRESHOLD:.2%} or more", ) + + +class TestCommandUtils(unittest.TestCase): + def test_bench_helper(self): + from frappe.utils.bench_helper import get_app_groups + + app_groups = get_app_groups() + self.assertIn("frappe", app_groups) + self.assertIsInstance(app_groups["frappe"], click.Group) diff --git a/frappe/tests/test_config.py b/frappe/tests/test_config.py new file mode 100644 index 0000000000..21ceb47bb3 --- /dev/null +++ b/frappe/tests/test_config.py @@ -0,0 +1,17 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE +import unittest + +import frappe +from frappe.config import get_modules_from_all_apps_for_user + + +class TestConfig(unittest.TestCase): + def test_get_modules(self): + frappe_modules = frappe.get_all("Module Def", filters={"app_name": "frappe"}, pluck="name") + all_modules_data = get_modules_from_all_apps_for_user() + first_module_entry = all_modules_data[0] + all_modules = [x["module_name"] for x in all_modules_data] + self.assertIn("links", first_module_entry) + self.assertIsInstance(all_modules_data, list) + self.assertFalse([x for x in frappe_modules if x not in all_modules]) diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index e68e8372af..42094e145f 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -4,10 +4,12 @@ import io import json import os +import sys import unittest from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum +from io import StringIO from mimetypes import guess_type from unittest.mock import patch @@ -16,15 +18,18 @@ from PIL import Image import frappe from frappe.installer import parse_app_name +from frappe.model.document import Document from frappe.utils import ( ceil, evaluate_filters, floor, format_timedelta, get_bench_path, + get_gravatar, get_url, money_in_words, parse_timedelta, + random_string, scrub_urls, validate_email_address, validate_url, @@ -42,10 +47,25 @@ from frappe.utils.data import ( ) from frappe.utils.dateutils import get_dates_from_timegrain from frappe.utils.diff import _get_value_from_version, get_version_diff, version_query +from frappe.utils.identicon import Identicon from frappe.utils.image import optimize_image, strip_exif_data +from frappe.utils.make_random import can_make, get_random, how_many from frappe.utils.response import json_handler +class Capturing(list): + # ref: https://stackoverflow.com/a/16571630/10309266 + def __enter__(self): + self._stdout = sys.stdout + sys.stdout = self._stringio = StringIO() + return self + + def __exit__(self, *args): + self.extend(self._stringio.getvalue().splitlines()) + del self._stringio + sys.stdout = self._stdout + + class TestFilters(unittest.TestCase): def test_simple_dict(self): self.assertTrue(evaluate_filters({"doctype": "User", "status": "Open"}, {"status": "Open"})) @@ -677,3 +697,53 @@ class TestIntrospectionMagic(unittest.TestCase): # No args self.assertEqual(frappe.get_newargs(lambda: None, args), {}) + + +class TestMakeRandom(unittest.TestCase): + def test_get_random(self): + self.assertIsInstance(get_random("DocType", doc=True), Document) + self.assertIsInstance(get_random("DocType"), str) + + def test_can_make(self): + self.assertIsInstance(can_make("User"), bool) + + def test_how_many(self): + self.assertIsInstance(how_many("User"), int) + + +class TestLazyLoader(unittest.TestCase): + def test_lazy_import_module(self): + from frappe.utils.lazy_loader import lazy_import + + with Capturing() as output: + ls = lazy_import("frappe.tests.data.load_sleep") + self.assertEqual(output, []) + + with Capturing() as output: + ls.time + self.assertEqual(["Module `frappe.tests.data.load_sleep` loaded"], output) + + +class TestIdenticon(unittest.TestCase): + def test_get_gravatar(self): + # developers@frappe.io has a gravatar linked so str URL will be returned + frappe.flags.in_test = False + gravatar_url = get_gravatar("developers@frappe.io") + frappe.flags.in_test = True + self.assertIsInstance(gravatar_url, str) + self.assertTrue(gravatar_url.startswith("http")) + + # random email will require Identicon to be generated, which will be a base64 string + gravatar_url = get_gravatar(f"developers{random_string(6)}@frappe.io") + self.assertIsInstance(gravatar_url, str) + self.assertTrue(gravatar_url.startswith("data:image/png;base64,")) + + def test_generate_identicon(self): + identicon = Identicon(random_string(6)) + with patch.object(identicon.image, "show") as show: + identicon.generate() + show.assert_called_once() + + identicon_bs64 = identicon.base64() + self.assertIsInstance(identicon_bs64, str) + self.assertTrue(identicon_bs64.startswith("data:image/png;base64,")) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index db176a5c0b..9f25b33266 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -283,12 +283,7 @@ def get_gravatar_url(email): def get_gravatar(email): from frappe.utils.identicon import Identicon - gravatar_url = has_gravatar(email) - - if not gravatar_url: - gravatar_url = Identicon(email).base64() - - return gravatar_url + return has_gravatar(email) or Identicon(email).base64() def get_traceback(with_context=False) -> str: diff --git a/frappe/utils/bench_helper.py b/frappe/utils/bench_helper.py index a0b011acc1..01e611f021 100644 --- a/frappe/utils/bench_helper.py +++ b/frappe/utils/bench_helper.py @@ -3,6 +3,7 @@ import json import os import traceback import warnings +from pathlib import Path import click @@ -18,22 +19,18 @@ def main(): click.Group(commands=commands)(prog_name="bench") -def get_app_groups(): +def get_app_groups() -> dict[str, click.Group]: """Get all app groups, put them in main group "frappe" since bench is designed to only handle that""" - commands = dict() + commands = {} for app in get_apps(): - app_commands = get_app_commands(app) - if app_commands: - commands.update(app_commands) - - ret = dict(frappe=click.group(name="frappe", commands=commands)(app_group)) - return ret + if app_commands := get_app_commands(app): + commands |= app_commands + return dict(frappe=click.group(name="frappe", commands=commands)(app_group)) -def get_app_group(app): - app_commands = get_app_commands(app) - if app_commands: +def get_app_group(app: str) -> click.Group: + if app_commands := get_app_commands(app): return click.group(name=app, commands=app_commands)(app_group) @@ -48,7 +45,7 @@ def app_group(ctx, site=False, force=False, verbose=False, profile=False): ctx.info_name = "" -def get_sites(site_arg): +def get_sites(site_arg: str) -> list[str]: if site_arg == "all": return frappe.utils.get_sites() elif site_arg: @@ -57,25 +54,23 @@ def get_sites(site_arg): return [os.environ.get("FRAPPE_SITE")] elif os.path.exists("currentsite.txt"): with open("currentsite.txt") as f: - site = f.read().strip() - if site: + if site := f.read().strip(): return [site] return [] -def get_app_commands(app): - if os.path.exists(os.path.join("..", "apps", app, app, "commands.py")) or os.path.exists( - os.path.join("..", "apps", app, app, "commands", "__init__.py") - ): - try: - app_command_module = importlib.import_module(app + ".commands") - except Exception: - traceback.print_exc() - return [] - else: - return [] - +def get_app_commands(app: str) -> dict: ret = {} + app_path = Path("..", "apps", app, app) + + if not ((app_path / "commands.py").exists() or (app_path / "commands" / "__init__.py").exists()): + return ret + + try: + app_command_module = importlib.import_module(f"{app}.commands") + except Exception: + traceback.print_exc() + return ret for command in getattr(app_command_module, "commands", []): ret[command.name] = command return ret diff --git a/frappe/utils/identicon.py b/frappe/utils/identicon.py index 1267b8f9c3..645898f23b 100644 --- a/frappe/utils/identicon.py +++ b/frappe/utils/identicon.py @@ -1,7 +1,13 @@ -import base64 -import random +""" +This module provides a class Identicon that can be used to generate identicons +from strings. + +It provides a (slighltly modified) version of https://github.com/evuez/identicons +which has been released under the MIT license, as described in attributions.md. +""" +from base64 import b64encode from hashlib import md5 -from io import StringIO +from io import BytesIO from PIL import Image, ImageDraw @@ -33,32 +39,7 @@ class Identicon: First three bytes are used to generate the color, remaining bytes are used to create the drawing """ - # color = (self.hash & 0xff, self.hash >> 8 & 0xff, self.hash >> 16 & 0xff) - color = random.choice( - ( - (254, 196, 197), - (253, 138, 139), - (254, 231, 206), - (254, 208, 159), - (210, 211, 253), - (163, 165, 252), - (247, 213, 247), - (242, 172, 238), - (235, 247, 206), - (217, 241, 157), - (211, 248, 237), - (167, 242, 221), - (255, 249, 207), - (254, 245, 161), - (211, 241, 254), - (168, 228, 254), - (207, 245, 210), - (159, 235, 164), - ) - ) - # print color - # color = (254, 232, 206) - + color = (self.hash & 0xFF, self.hash >> 8 & 0xFF, self.hash >> 16 & 0xFF) self.hash >>= 24 # skip first three bytes square_x = square_y = 0 # init square position for x in range(GRID_SIZE * (GRID_SIZE + 1) // 2): @@ -86,22 +67,17 @@ class Identicon: def base64(self, format="PNG"): """ - usage: i = Identicon('xx') - print(i.base64()) - return: this image's base64 code - created by: liuzheng712 - bug report: https://github.com/liuzheng712/identicons/issues + Return the identicon's base64 + + Created by: liuzheng712 + Bug report: https://github.com/liuzheng712/identicons/issues """ self.calculate() - fp = StringIO() + self.image.encoderinfo = {} self.image.encoderconfig = () - if format.upper() not in Image.SAVE: - Image.init() - save_handler = Image.SAVE[format.upper()] - try: - save_handler(self.image, fp, "") - finally: - fp.seek(0) - return f"data:image/png;base64,{base64.b64encode(fp.read())}" # noqa + buff = BytesIO() + self.image.save(buff, format=format.upper()) + + return f"data:image/png;base64,{b64encode(buff.getvalue()).decode()}" diff --git a/frappe/utils/make_random.py b/frappe/utils/make_random.py index 1915cbb620..e0bf31b3d4 100644 --- a/frappe/utils/make_random.py +++ b/frappe/utils/make_random.py @@ -1,7 +1,11 @@ import random +from typing import TYPE_CHECKING import frappe +if TYPE_CHECKING: + from frappe.model.document import Document + settings = frappe._dict( prob={ "default": {"make": 0.6, "qty": (1, 5)}, @@ -9,7 +13,7 @@ settings = frappe._dict( ) -def add_random_children(doc, fieldname, rows, randomize, unique=None): +def add_random_children(doc: "Document", fieldname: str, rows, randomize: dict, unique=None): nrows = rows if rows > 1: nrows = random.randrange(1, rows) @@ -29,15 +33,13 @@ def add_random_children(doc, fieldname, rows, randomize, unique=None): doc.append(fieldname, d) -def get_random(doctype, filters=None, doc=False): +def get_random(doctype: str, filters: dict = None, doc: bool = False): condition = [] if filters: - for key, val in filters.items(): - condition.append("{}='{}'".format(key, str(val).replace("'", "'"))) - if condition: - condition = " where " + " and ".join(condition) - else: - condition = "" + condition.extend( + "{}='{}'".format(key, str(val).replace("'", "'")) for key, val in filters.items() + ) + condition = " where " + " and ".join(condition) if condition else "" out = frappe.db.multisql( { @@ -54,13 +56,12 @@ def get_random(doctype, filters=None, doc=False): if doc and out: return frappe.get_doc(doctype, out) - else: - return out + return out -def can_make(doctype): +def can_make(doctype: str) -> bool: return random.random() < settings.prob.get(doctype, settings.prob["default"])["make"] -def how_many(doctype): +def how_many(doctype: str) -> int: return random.randrange(*settings.prob.get(doctype, settings.prob["default"])["qty"]) diff --git a/frappe/website/doctype/web_form/templates/web_form.html b/frappe/website/doctype/web_form/templates/web_form.html index 05af654e47..fce6401457 100644 --- a/frappe/website/doctype/web_form/templates/web_form.html +++ b/frappe/website/doctype/web_form/templates/web_form.html @@ -20,7 +20,7 @@ {% macro action_buttons() %} {% if is_new or is_form_editable %}
- + {% if is_form_editable %} {{ _("Reset Form", null, "Button in web form") }} @@ -38,33 +38,53 @@ {% endmacro %} {% block page_content %} - - - {% if has_header and login_required and show_list %} - {% include "templates/includes/breadcrumbs.html" %} - {% else %} -
+ + {% if banner_image %} +
+ Banner Image +
{% endif %} - -
+ +
+ + {% if not banner_image and has_header and login_required and show_list %} + {% include "templates/includes/breadcrumbs.html" %} + {% else %} +
+ {% endif %} + +
-

{{ _(title) }}

-
- {{ header_buttons() }} + {% if banner_image and has_header and login_required and show_list %} + {% include "templates/includes/breadcrumbs.html" %} + {% endif %} +
+
+

{{ _(title) }}

+
+ {{ header_buttons() }} +
+
+ {% if is_new and introduction_text %} +
{{ introduction_text }}
+ {% endif %}
-
- {% if is_new and introduction_text %} -
{{ introduction_text }}
- {% endif %} -
+ + + +
+
+ {% include "website/doctype/web_form/templates/web_form_skeleton.html" %} +
+
-
+ {% if show_attachments and not is_new and attachments %} @@ -81,17 +101,45 @@ {% endfor %}
{% endif %} {# attachments #} - - - {% if allow_comments and not is_new and not is_list -%} -
-

{{ _("Comments") }}

- {% include 'templates/includes/comments/comments.html' %} -
- {%- else -%} -
- {%- endif %} {# comments #} + + {% if allow_comments and not is_new and not is_list -%} +
+

{{ _("Comments") }}

+ {% include 'templates/includes/comments/comments.html' %} +
+ {%- else -%} +
+ {%- endif %} {# comments #} +
+ + +
{% endblock page_content %} diff --git a/frappe/website/doctype/web_form/templates/web_form_skeleton.html b/frappe/website/doctype/web_form/templates/web_form_skeleton.html new file mode 100644 index 0000000000..82fb2bccac --- /dev/null +++ b/frappe/website/doctype/web_form/templates/web_form_skeleton.html @@ -0,0 +1,38 @@ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/frappe/website/doctype/web_form/web_form.json b/frappe/website/doctype/web_form/web_form.json index 64bf04f3e8..8faa263e5b 100644 --- a/frappe/website/doctype/web_form/web_form.json +++ b/frappe/website/doctype/web_form/web_form.json @@ -30,12 +30,6 @@ "form_fields", "web_form_fields", "max_attachment_size", - "actions", - "breadcrumbs", - "button_label", - "column_break_29", - "success_message", - "success_url", "list_settings_tab", "list_setting_message", "show_list", @@ -44,6 +38,16 @@ "sidebar_settings_tab", "show_sidebar", "website_sidebar", + "customization_tab", + "button_label", + "banner_image", + "column_break_37", + "breadcrumbs", + "section_break_43", + "success_title", + "success_url", + "column_break_41", + "success_message", "scripting_style_tab", "client_script", "custom_css" @@ -162,7 +166,7 @@ }, { "fieldname": "introduction_text", - "fieldtype": "Small Text", + "fieldtype": "Text Editor", "ignore_xss_filter": 1, "label": "Introduction" }, @@ -183,12 +187,6 @@ "fieldtype": "Code", "label": "Client Script" }, - { - "collapsible": 1, - "fieldname": "actions", - "fieldtype": "Section Break", - "label": "Customization" - }, { "default": "Save", "fieldname": "button_label", @@ -196,7 +194,7 @@ "label": "Submit Button Label" }, { - "description": "Message to be displayed on successful completion (only for Guest users)", + "description": "Message to be displayed on successful completion", "fieldname": "success_message", "fieldtype": "Text", "label": "Success Message" @@ -217,7 +215,8 @@ "description": "List as [{\"label\": _(\"Jobs\"), \"route\":\"jobs\"}]", "fieldname": "breadcrumbs", "fieldtype": "Code", - "label": "Breadcrumbs" + "label": "Breadcrumbs", + "max_height": "140px" }, { "fieldname": "custom_css", @@ -272,10 +271,6 @@ "label": "Website Sidebar", "options": "Website Sidebar" }, - { - "fieldname": "column_break_29", - "fieldtype": "Column Break" - }, { "fieldname": "list_setting_message", "fieldtype": "HTML", @@ -303,13 +298,41 @@ "fieldname": "scripting_style_tab", "fieldtype": "Tab Break", "label": "Scripting / Style" + }, + { + "fieldname": "customization_tab", + "fieldtype": "Tab Break", + "label": "Customization" + }, + { + "fieldname": "success_title", + "fieldtype": "Data", + "label": "Success Title" + }, + { + "fieldname": "banner_image", + "fieldtype": "Attach Image", + "label": "Banner Image" + }, + { + "fieldname": "column_break_41", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_43", + "fieldtype": "Section Break", + "label": "After Submission" + }, + { + "fieldname": "column_break_37", + "fieldtype": "Column Break" } ], "has_web_view": 1, "icon": "icon-edit", "is_published_field": "published", "links": [], - "modified": "2022-08-10 15:38:28.611328", + "modified": "2022-08-11 16:27:25.914627", "modified_by": "Administrator", "module": "Website", "name": "Web Form", diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index eee7827500..718088212f 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -281,7 +281,7 @@ def get_context(context): context.title = strip_html( context.reference_doc.get(context.reference_doc.meta.get_title_field()) ) - if context.is_form_editable: + if context.is_form_editable and context.parents: context.parents.append( { "label": _(context.title),