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",