Merge branch 'develop' into runtime-type-checks-api
This commit is contained in:
commit
2c498910ba
26 changed files with 265 additions and 178 deletions
2
.github/workflows/linters.yml
vendored
2
.github/workflows/linters.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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("")
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
7
frappe/patches/v13_0/remove_share_for_std_users.py
Normal file
7
frappe/patches/v13_0/remove_share_for_std_users.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import frappe
|
||||
import frappe.share
|
||||
|
||||
|
||||
def execute():
|
||||
for user in frappe.STANDARD_USERS:
|
||||
frappe.share.remove("User", user, user)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block meta_block %}
|
||||
{% include "templates/includes/meta_block.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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('<meta name="name" content="Test Meta Form Title">', content)
|
||||
self.assertIn('<meta property="og:description" content="Test Meta Form Description">', content)
|
||||
self.assertIn('<meta property="og:image" content="https://frappe.io/files/frappe.png">', content)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue