diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 01b5407489..9c0a1f4ce2 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -83,4 +83,4 @@ jobs: - uses: actions/checkout@v3 - run: | pip install pip-audit - pip-audit ${GITHUB_WORKSPACE} + pip-audit ${GITHUB_WORKSPACE} --ignore-vuln GHSA-hcpj-qp55-gfph diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e976230244..4b3ea6d1ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,11 +26,10 @@ repos: - id: pyupgrade args: ['--py310-plus'] - - repo: https://github.com/adityahase/black - rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119 + - repo: https://github.com/frappe/black + rev: 951ccf4d5bb0d692b457a5ebc4215d755618eb68 hooks: - id: black - additional_dependencies: ['click==8.0.4'] - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.7.1 diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 92337e6c6a..e5c6c406eb 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -11,7 +11,6 @@ from frappe.commands import get_site, pass_context from frappe.coverage import CodeCoverage from frappe.exceptions import SiteNotSpecifiedError from frappe.utils import cint, update_progress_bar -from frappe.utils.synchronization import filelock find_executable = which # backwards compatibility DATA_IMPORT_DEPRECATION = ( @@ -55,6 +54,7 @@ def build( ): "Compile JS and CSS source files" from frappe.build import bundle, download_frappe_assets + from frappe.utils.synchronization import filelock frappe.init("") diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 2460720bd8..91de1d29be 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -7,7 +7,7 @@ import frappe import frappe.defaults import frappe.permissions import frappe.share -from frappe import _, msgprint, throw +from frappe import STANDARD_USERS, _, msgprint, throw from frappe.core.doctype.user_type.user_type import user_linked_with_permission_on_doctype from frappe.desk.doctype.notification_settings.notification_settings import ( create_notification_settings, @@ -33,8 +33,6 @@ from frappe.utils.password import update_password as _update_password from frappe.utils.user import get_system_managers from frappe.website.utils import is_signup_disabled -STANDARD_USERS = frappe.STANDARD_USERS - class User(Document): __new_password = None @@ -126,7 +124,8 @@ class User(Document): frappe.enqueue( "frappe.core.doctype.user.user.create_contact", user=self, ignore_mandatory=True, now=now ) - if self.name not in ("Administrator", "Guest") and not self.user_image: + + if self.name not in STANDARD_USERS and not self.user_image: frappe.enqueue("frappe.core.doctype.user.user.update_gravatar", name=self.name, now=now) # Set user selected timezone @@ -238,6 +237,9 @@ class User(Document): ) def share_with_self(self): + if self.name in STANDARD_USERS: + return + frappe.share.add_docshare( self.doctype, self.name, self.name, write=1, share=1, flags={"ignore_share_permission": True} ) diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index d8c806ffcf..7f9846d6c4 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -129,12 +129,11 @@ class MariaDBConnectionUtil: conn_settings["local_infile"] = frappe.conf.local_infile if frappe.conf.db_ssl_ca and frappe.conf.db_ssl_cert and frappe.conf.db_ssl_key: - ssl_params = { + conn_settings["ssl"] = { "ca": frappe.conf.db_ssl_ca, "cert": frappe.conf.db_ssl_cert, "key": frappe.conf.db_ssl_key, } - conn_settings |= ssl_params return conn_settings diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 4843219179..446f842a0b 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -7,45 +7,18 @@ import re import frappe from frappe import _, is_whitelisted +from frappe.database.schema import SPECIAL_CHAR_PATTERN from frappe.permissions import has_permission from frappe.utils import cint, cstr, unique def sanitize_searchfield(searchfield): - blacklisted_keywords = ["select", "delete", "drop", "update", "case", "and", "or", "like"] + if not searchfield: + return - def _raise_exception(searchfield): + if SPECIAL_CHAR_PATTERN.search(searchfield): frappe.throw(_("Invalid Search Field {0}").format(searchfield), frappe.DataError) - if len(searchfield) == 1: - # do not allow special characters to pass as searchfields - regex = re.compile(r'^.*[=;*,\'"$\-+%#@()_].*') - if regex.match(searchfield): - _raise_exception(searchfield) - - if len(searchfield) >= 3: - - # to avoid 1=1 - if "=" in searchfield: - _raise_exception(searchfield) - - # in mysql -- is used for commenting the query - elif " --" in searchfield: - _raise_exception(searchfield) - - # to avoid and, or and like - elif any(f" {keyword} " in searchfield.split() for keyword in blacklisted_keywords): - _raise_exception(searchfield) - - # to avoid select, delete, drop, update and case - elif any(keyword in searchfield.split() for keyword in blacklisted_keywords): - _raise_exception(searchfield) - - else: - regex = re.compile(r'^.*[=;*,\'"$\-+%#@()].*') - if any(regex.match(f) for f in searchfield.split()): - _raise_exception(searchfield) - # this is called by the Link Field @frappe.whitelist() diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py index 474a5b06c4..ec5f205197 100644 --- a/frappe/frappeclient.py +++ b/frappe/frappeclient.py @@ -106,7 +106,9 @@ class FrappeClient: headers=self.headers, ) - def get_list(self, doctype, fields='["name"]', filters=None, limit_start=0, limit_page_length=0): + def get_list( + self, doctype, fields='["name"]', filters=None, limit_start=0, limit_page_length=None + ): """Returns list of records of a particular type""" if not isinstance(fields, str): fields = json.dumps(fields) @@ -115,7 +117,7 @@ class FrappeClient: } if filters: params["filters"] = json.dumps(filters) - if limit_page_length: + if limit_page_length is not None: params["limit_start"] = limit_start params["limit_page_length"] = limit_page_length res = self.session.get( diff --git a/frappe/migrate.py b/frappe/migrate.py index b36745e4cf..3241b14152 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -21,7 +21,6 @@ from frappe.search.website_search import build_index_for_all_routes from frappe.utils.connections import check_connection from frappe.utils.dashboard import sync_dashboards from frappe.utils.fixtures import sync_fixtures -from frappe.utils.synchronization import filelock from frappe.website.utils import clear_website_cache BENCH_START_MESSAGE = dedent( @@ -163,6 +162,8 @@ class SiteMigration: """Run Migrate operation on site specified. This method initializes and destroys connections to the site database. """ + from frappe.utils.synchronization import filelock + if site: frappe.init(site=site) frappe.connect() diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 09cc34891a..79899caf14 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -89,8 +89,10 @@ class BaseDocument: "meta", "_meta", "flags", + "parent_doc", "_table_fields", "_valid_columns", + "_doc_before_save", "_table_fieldnames", "_reserved_keywords", "dont_update_if_missing", @@ -286,7 +288,7 @@ class BaseDocument: return DOCTYPE_TABLE_FIELDS # child tables don't have child tables - if self.doctype in DOCTYPES_FOR_DOCTYPE or getattr(self, "parentfield", None): + if self.doctype in DOCTYPES_FOR_DOCTYPE: return () return self.meta.get_table_fields() diff --git a/frappe/model/document.py b/frappe/model/document.py index 4875d637bf..125c44e994 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -193,9 +193,10 @@ class Document(BaseDocument): self.load_from_db() def get_latest(self): - if not getattr(self, "latest", None): - self.latest = frappe.get_doc(self.doctype, self.name) - return self.latest + if not hasattr(self, "_doc_before_save"): + self.load_doc_before_save() + + return self._doc_before_save def check_permission(self, permtype="read", permlevel=None): """Raise `frappe.PermissionError` if not permitted""" diff --git a/frappe/model/meta.py b/frappe/model/meta.py index d9d18e98ce..1fa1340024 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -114,7 +114,7 @@ class Meta(Document): # from cache if isinstance(doctype, dict): super().__init__(doctype) - self.init_field_map() + self.init_field_caches() return if isinstance(doctype, Document): @@ -137,12 +137,12 @@ class Meta(Document): # don't process for special doctypes # prevent's circular dependency if self.name in self.special_doctypes: - self.init_field_map() + self.init_field_caches() return has_custom_fields = self.add_custom_fields() self.apply_property_setters() - self.init_field_map() + self.init_field_caches() if has_custom_fields: self.sort_fields() @@ -214,12 +214,6 @@ class Meta(Document): return self._set_only_once_fields def get_table_fields(self): - if not hasattr(self, "_table_fields"): - if self.name != "DocType": - self._table_fields = self.get("fields", {"fieldtype": ["in", table_fields]}) - else: - self._table_fields = DOCTYPE_TABLE_FIELDS - return self._table_fields def get_global_search_fields(self): @@ -453,9 +447,16 @@ class Meta(Document): self.set(fieldname, new_list) - def init_field_map(self): + def init_field_caches(self): + # field map self._fields = {field.fieldname: field for field in self.fields} + # table fields + if self.name == "DocType": + self._table_fields = DOCTYPE_TABLE_FIELDS + else: + self._table_fields = self.get("fields", {"fieldtype": ["in", table_fields]}) + def sort_fields(self): """Sort custom fields on the basis of insert_after""" diff --git a/frappe/patches.txt b/frappe/patches.txt index 3eca9fd2ac..cf1e509e78 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -182,6 +182,7 @@ frappe.patches.v13_0.update_notification_channel_if_empty frappe.patches.v13_0.set_first_day_of_the_week frappe.patches.v13_0.encrypt_2fa_secrets frappe.patches.v13_0.reset_corrupt_defaults +frappe.patches.v13_0.remove_share_for_std_users execute:frappe.reload_doc('custom', 'doctype', 'custom_field') frappe.patches.v14_0.update_workspace2 # 20.09.2021 frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 diff --git a/frappe/patches/v13_0/remove_share_for_std_users.py b/frappe/patches/v13_0/remove_share_for_std_users.py new file mode 100644 index 0000000000..39b449995d --- /dev/null +++ b/frappe/patches/v13_0/remove_share_for_std_users.py @@ -0,0 +1,7 @@ +import frappe +import frappe.share + + +def execute(): + for user in frappe.STANDARD_USERS: + frappe.share.remove("User", user, user) diff --git a/frappe/search/website_search.py b/frappe/search/website_search.py index e43286285b..9af827aaa8 100644 --- a/frappe/search/website_search.py +++ b/frappe/search/website_search.py @@ -9,7 +9,6 @@ from whoosh.fields import ID, TEXT, Schema import frappe from frappe.search.full_text_search import FullTextSearch from frappe.utils import set_request, update_progress_bar -from frappe.utils.synchronization import filelock from frappe.website.serve import get_response_content INDEX_NAME = "web_routes" @@ -141,7 +140,9 @@ def remove_document_from_index(path): return ws.remove_document_from_index(path) -@filelock("building_website_search") def build_index_for_all_routes(): - ws = WebsiteSearch(INDEX_NAME) - return ws.build() + from frappe.utils.synchronization import filelock + + with filelock("building_website_search"): + ws = WebsiteSearch(INDEX_NAME) + return ws.build() diff --git a/frappe/tests/test_db_update.py b/frappe/tests/test_db_update.py index 0140bea113..21ef16fa48 100644 --- a/frappe/tests/test_db_update.py +++ b/frappe/tests/test_db_update.py @@ -128,6 +128,14 @@ class TestDBUpdate(FrappeTestCase): doctype.save() self.check_unique_indexes(doctype.name, field) + # New column with a unique index + # This works because index name is same as fieldname. + new_field = frappe.copy_doc(doctype.fields[0]) + new_field.fieldname = "duplicate_field" + doctype.append("fields", new_field) + doctype.save() + self.check_unique_indexes(doctype.name, new_field.fieldname) + doctype.delete() frappe.db.commit() diff --git a/frappe/tests/test_linked_with.py b/frappe/tests/test_linked_with.py index 065d6f4b04..70dddba334 100644 --- a/frappe/tests/test_linked_with.py +++ b/frappe/tests/test_linked_with.py @@ -6,18 +6,18 @@ from frappe.tests.utils import FrappeTestCase class TestLinkedWith(FrappeTestCase): def setUp(self): - parent_doc = new_doctype("Parent Doc") - parent_doc.is_submittable = 1 - parent_doc.insert() + parent_doctype = new_doctype("Parent DocType") + parent_doctype.is_submittable = 1 + parent_doctype.insert() - child_doc1 = new_doctype( - "Child Doc1", + child_doctype1 = new_doctype( + "Child DocType1", fields=[ { - "label": "Parent Doc", - "fieldname": "parent_doc", + "label": "Parent DocType", + "fieldname": "parent_doctype", "fieldtype": "Link", - "options": "Parent Doc", + "options": "Parent DocType", }, { "label": "Reference field", @@ -34,85 +34,99 @@ class TestLinkedWith(FrappeTestCase): ], unique=0, ) - child_doc1.is_submittable = 1 - child_doc1.insert() + child_doctype1.is_submittable = 1 + child_doctype1.insert() - child_doc2 = new_doctype( - "Child Doc2", + child_doctype2 = new_doctype( + "Child DocType2", fields=[ { - "label": "Parent Doc", - "fieldname": "parent_doc", + "label": "Parent DocType", + "fieldname": "parent_doctype", "fieldtype": "Link", - "options": "Parent Doc", + "options": "Parent DocType", }, { - "label": "Child Doc1", - "fieldname": "child_doc1", + "label": "Child DocType1", + "fieldname": "child_doctype1", "fieldtype": "Link", - "options": "Child Doc1", + "options": "Child DocType1", }, ], unique=0, ) - child_doc2.is_submittable = 1 - child_doc2.insert() + child_doctype2.is_submittable = 1 + child_doctype2.insert() def tearDown(self): - for doctype in ["Parent Doc", "Child Doc1", "Child Doc2"]: + for doctype in ["Parent DocType", "Child DocType1", "Child DocType2"]: frappe.delete_doc("DocType", doctype) def test_get_doctype_references_by_link_field(self): - references = linked_with.get_references_across_doctypes_by_link_field(to_doctypes=["Parent Doc"]) - self.assertEqual(len(references["Parent Doc"]), 3) - self.assertIn({"doctype": "Child Doc1", "fieldname": "parent_doc"}, references["Parent Doc"]) - self.assertIn({"doctype": "Child Doc2", "fieldname": "parent_doc"}, references["Parent Doc"]) - - references = linked_with.get_references_across_doctypes_by_link_field(to_doctypes=["Child Doc1"]) - self.assertEqual(len(references["Child Doc1"]), 2) - self.assertIn({"doctype": "Child Doc2", "fieldname": "child_doc1"}, references["Child Doc1"]) + references = linked_with.get_references_across_doctypes_by_link_field( + to_doctypes=["Parent DocType"] + ) + self.assertEqual(len(references["Parent DocType"]), 3) + self.assertIn( + {"doctype": "Child DocType1", "fieldname": "parent_doctype"}, references["Parent DocType"] + ) + self.assertIn( + {"doctype": "Child DocType2", "fieldname": "parent_doctype"}, references["Parent DocType"] + ) references = linked_with.get_references_across_doctypes_by_link_field( - to_doctypes=["Child Doc1", "Parent Doc"], limit_link_doctypes=["Child Doc1"] + to_doctypes=["Child DocType1"] + ) + self.assertEqual(len(references["Child DocType1"]), 2) + self.assertIn( + {"doctype": "Child DocType2", "fieldname": "child_doctype1"}, references["Child DocType1"] + ) + + references = linked_with.get_references_across_doctypes_by_link_field( + to_doctypes=["Child DocType1", "Parent DocType"], limit_link_doctypes=["Child DocType1"] + ) + self.assertEqual(len(references["Child DocType1"]), 1) + self.assertEqual(len(references["Parent DocType"]), 1) + self.assertIn( + {"doctype": "Child DocType1", "fieldname": "parent_doctype"}, references["Parent DocType"] ) - self.assertEqual(len(references["Child Doc1"]), 1) - self.assertEqual(len(references["Parent Doc"]), 1) - self.assertIn({"doctype": "Child Doc1", "fieldname": "parent_doc"}, references["Parent Doc"]) def test_get_doctype_references_by_dlink_field(self): references = linked_with.get_references_across_doctypes_by_dynamic_link_field( - to_doctypes=["Parent Doc"], limit_link_doctypes=["Parent Doc", "Child Doc1", "Child Doc2"] + to_doctypes=["Parent DocType"], + limit_link_doctypes=["Parent DocType", "Child DocType1", "Child DocType2"], ) self.assertFalse(references) - parent_record = frappe.get_doc({"doctype": "Parent Doc"}).insert() + parent_record = frappe.get_doc({"doctype": "Parent DocType"}).insert() child_record = frappe.get_doc( { - "doctype": "Child Doc1", - "reference_doctype": "Parent Doc", + "doctype": "Child DocType1", + "reference_doctype": "Parent DocType", "reference_name": parent_record.name, } ).insert() references = linked_with.get_references_across_doctypes_by_dynamic_link_field( - to_doctypes=["Parent Doc"], limit_link_doctypes=["Parent Doc", "Child Doc1", "Child Doc2"] + to_doctypes=["Parent DocType"], + limit_link_doctypes=["Parent DocType", "Child DocType1", "Child DocType2"], ) - self.assertEqual(len(references["Parent Doc"]), 1) - self.assertEqual(references["Parent Doc"][0]["doctype"], "Child Doc1") - self.assertEqual(references["Parent Doc"][0]["doctype_fieldname"], "reference_doctype") + self.assertEqual(len(references["Parent DocType"]), 1) + self.assertEqual(references["Parent DocType"][0]["doctype"], "Child DocType1") + self.assertEqual(references["Parent DocType"][0]["doctype_fieldname"], "reference_doctype") child_record.delete() parent_record.delete() def test_get_submitted_linked_docs(self): - parent_record = frappe.get_doc({"doctype": "Parent Doc"}).insert() + parent_record = frappe.get_doc({"doctype": "Parent DocType"}).insert() child_record = frappe.get_doc( { - "doctype": "Child Doc1", - "reference_doctype": "Parent Doc", + "doctype": "Child DocType1", + "reference_doctype": "Parent DocType", "reference_name": parent_record.name, "docstatus": 1, } diff --git a/frappe/tests/test_search.py b/frappe/tests/test_search.py index e010d2abc0..24bd8b8057 100644 --- a/frappe/tests/test_search.py +++ b/frappe/tests/test_search.py @@ -1,6 +1,7 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import re import frappe from frappe.app import make_form_dict @@ -26,71 +27,24 @@ class TestSearch(FrappeTestCase): self.assertTrue("User" in result["value"]) # raise exception on injection - self.assertRaises( - frappe.DataError, - search_link, - "DocType", - "Customer", - query=None, - filters=None, - page_length=20, - searchfield="1=1", - ) - - self.assertRaises( - frappe.DataError, - search_link, - "DocType", - "Customer", - query=None, - filters=None, - page_length=20, - searchfield="select * from tabSessions) --", - ) - - self.assertRaises( - frappe.DataError, - search_link, - "DocType", - "Customer", - query=None, - filters=None, - page_length=20, - searchfield="name or (select * from tabSessions)", - ) - - self.assertRaises( - frappe.DataError, - search_link, - "DocType", - "Customer", - query=None, - filters=None, - page_length=20, - searchfield="*", - ) - - self.assertRaises( - frappe.DataError, - search_link, - "DocType", - "Customer", - query=None, - filters=None, - page_length=20, - searchfield=";", - ) - - self.assertRaises( - frappe.DataError, - search_link, - "DocType", - "Customer", - query=None, - filters=None, - page_length=20, - searchfield=";", - ) + for searchfield in ( + "1=1", + "select * from tabSessions) --", + "name or (select * from tabSessions)", + "*", + ";", + "select`sid`from`tabSessions`", + ): + self.assertRaises( + frappe.DataError, + search_link, + "DocType", + "User", + query=None, + filters=None, + page_length=20, + searchfield=searchfield, + ) def test_only_enabled_in_mention(self): email = "test_disabled_user_in_mentions@example.com" diff --git a/frappe/website/doctype/help_article/help_article.json b/frappe/website/doctype/help_article/help_article.json index acb24f7083..520e9dedd0 100644 --- a/frappe/website/doctype/help_article/help_article.json +++ b/frappe/website/doctype/help_article/help_article.json @@ -15,7 +15,11 @@ "section_break_7", "content", "likes", - "route" + "route", + "section_break_cww5", + "helpful", + "cb_00", + "not_helpful" ], "fields": [ { @@ -78,6 +82,30 @@ "fieldtype": "Data", "in_global_search": 1, "label": "Route" + }, + { + "default": "0", + "fieldname": "helpful", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Helpful", + "read_only": 1 + }, + { + "fieldname": "cb_00", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "not_helpful", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Not Helpful", + "read_only": 1 + }, + { + "fieldname": "section_break_cww5", + "fieldtype": "Section Break" } ], "has_web_view": 1, @@ -85,7 +113,7 @@ "index_web_pages_for_search": 1, "is_published_field": "published", "links": [], - "modified": "2022-01-04 16:25:18.577325", + "modified": "2022-12-15 20:05:11.317400", "modified_by": "Administrator", "module": "Website", "name": "Help Article", @@ -116,6 +144,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/frappe/website/doctype/help_article/test_help_article.py b/frappe/website/doctype/help_article/test_help_article.py index 258a08521e..a7576c7168 100644 --- a/frappe/website/doctype/help_article/test_help_article.py +++ b/frappe/website/doctype/help_article/test_help_article.py @@ -7,4 +7,39 @@ from frappe.tests.utils import FrappeTestCase class TestHelpArticle(FrappeTestCase): - pass + @classmethod + def setUpClass(cls) -> None: + cls.help_category = frappe.get_doc( + { + "doctype": "Help Category", + "category_name": "_Test Help Category", + } + ).insert() + + cls.help_article = frappe.get_doc( + { + "doctype": "Help Article", + "title": "_Test Article", + "category": cls.help_category.name, + "content": "_Test Article", + } + ).insert() + + def test_article_is_helpful(self): + from frappe.website.doctype.help_article.help_article import add_feedback + + self.help_article.load_from_db() + self.assertEqual(self.help_article.helpful, 0) + self.assertEqual(self.help_article.not_helpful, 0) + + add_feedback(self.help_article.name, "Yes") + add_feedback(self.help_article.name, "No") + + self.help_article.load_from_db() + self.assertEqual(self.help_article.helpful, 1) + self.assertEqual(self.help_article.not_helpful, 1) + + @classmethod + def tearDownClass(cls) -> None: + frappe.delete_doc(cls.help_article.doctype, cls.help_article.name) + frappe.delete_doc(cls.help_category.doctype, cls.help_category.name) diff --git a/frappe/website/doctype/web_form/templates/web_form.html b/frappe/website/doctype/web_form/templates/web_form.html index a11cbc964b..f02acd5b07 100644 --- a/frappe/website/doctype/web_form/templates/web_form.html +++ b/frappe/website/doctype/web_form/templates/web_form.html @@ -1,5 +1,9 @@ {% extends "templates/web.html" %} +{% block meta_block %} + {% include "templates/includes/meta_block.html" %} +{% endblock %} + {% block breadcrumbs %}{% endblock %} {% block header %} diff --git a/frappe/website/doctype/web_form/test_records.json b/frappe/website/doctype/web_form/test_records.json index 62dea7446a..7139ce25d7 100644 --- a/frappe/website/doctype/web_form/test_records.json +++ b/frappe/website/doctype/web_form/test_records.json @@ -14,6 +14,9 @@ "published": 1, "success_url": "/manage-events", "title": "Manage Events", + "meta_title": "Test Meta Form Title", + "meta_description": "Test Meta Form Description", + "meta_image": "https://frappe.io/files/frappe.png", "web_form_fields": [ { "doctype": "Web Form Field", diff --git a/frappe/website/doctype/web_form/test_web_form.py b/frappe/website/doctype/web_form/test_web_form.py index 5a2269b64d..f86aa21735 100644 --- a/frappe/website/doctype/web_form/test_web_form.py +++ b/frappe/website/doctype/web_form/test_web_form.py @@ -75,3 +75,11 @@ class TestWebForm(FrappeTestCase): self.assertIn('data-doctype="Web Form"', content) self.assertIn('data-path="manage-events/new"', content) self.assertIn('source-type="Generator"', content) + + def test_webform_html_meta_is_added(self): + set_request(method="GET", path="manage-events/new") + content = get_response_content("manage-events/new") + + self.assertIn('', content) + self.assertIn('', content) + self.assertIn('', content) diff --git a/frappe/website/doctype/web_form/web_form.json b/frappe/website/doctype/web_form/web_form.json index f5ab147c64..0c2e416696 100644 --- a/frappe/website/doctype/web_form/web_form.json +++ b/frappe/website/doctype/web_form/web_form.json @@ -48,6 +48,11 @@ "success_url", "column_break_4", "success_message", + "meta_section", + "meta_title", + "meta_description", + "column_break_khxs", + "meta_image", "section_break_6", "client_script", "custom_css" @@ -328,13 +333,38 @@ "fieldname": "section_break_6", "fieldtype": "Section Break", "label": "Scripting / Style" + }, + { + "collapsible": 1, + "fieldname": "meta_section", + "fieldtype": "Section Break", + "label": "Meta" + }, + { + "fieldname": "meta_title", + "fieldtype": "Data", + "label": "Meta Title" + }, + { + "fieldname": "meta_description", + "fieldtype": "Small Text", + "label": "Meta Description" + }, + { + "fieldname": "column_break_khxs", + "fieldtype": "Column Break" + }, + { + "fieldname": "meta_image", + "fieldtype": "Attach Image", + "label": "Meta Image" } ], "has_web_view": 1, "icon": "icon-edit", "is_published_field": "published", "links": [], - "modified": "2022-08-17 18:58:49.451658", + "modified": "2022-12-15 17:14:44.939645", "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 73b2dc2331..ac6276c00b 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -191,10 +191,23 @@ def get_context(context): self.add_custom_context_and_script(context) self.load_translations(context) + self.add_metatags(context) context.boot = get_boot_data() context.boot["link_title_doctypes"] = frappe.boot.get_link_title_doctypes() + def add_metatags(self, context): + description = self.meta_description + + if not description and self.introduction_text: + description = self.introduction_text[:140] + + context.metatags = { + "name": self.meta_title or self.title, + "description": description, + "image": self.meta_image, + } + def load_translations(self, context): translated_messages = frappe.translate.get_dict("doctype", self.doc_type) # Sr is not added by default, had to be added manually diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py index d524d43d41..9ab903b518 100644 --- a/frappe/workflow/doctype/workflow/workflow.py +++ b/frappe/workflow/doctype/workflow/workflow.py @@ -136,7 +136,7 @@ def get_workflow_state_count(doctype, workflow_state_field, states): states = frappe.parse_json(states) result = frappe.get_all( doctype, - fields=[workflow_state_field, "count(*) as count", "docstatus"], + fields=[workflow_state_field, "count(*) as count"], filters={workflow_state_field: ["not in", states]}, group_by=workflow_state_field, ) diff --git a/pyproject.toml b/pyproject.toml index 6b9be6e90a..94277c6b44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "PyPDF2~=2.1.0", "PyPika~=0.48.9", "PyQRCode~=1.2.1", - "PyYAML~=5.4.1", + "PyYAML~=6.0", "RestrictedPython~=6.0", "WeasyPrint==52.5", "Werkzeug~=2.2.2",