Merge branch 'develop' of https://github.com/frappe/frappe into qb-fixes
This commit is contained in:
commit
5703303abb
23 changed files with 214 additions and 132 deletions
|
|
@ -152,7 +152,6 @@
|
|||
"modified_by": "Administrator",
|
||||
"module": "Contacts",
|
||||
"name": "Address",
|
||||
"name_case": "Title Case",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
@ -213,4 +212,4 @@
|
|||
"search_fields": "country, state",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,7 +254,6 @@
|
|||
"modified_by": "Administrator",
|
||||
"module": "Contacts",
|
||||
"name": "Contact",
|
||||
"name_case": "Title Case",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
@ -382,4 +381,4 @@
|
|||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@
|
|||
"sb1",
|
||||
"naming_rule",
|
||||
"autoname",
|
||||
"name_case",
|
||||
"allow_rename",
|
||||
"column_break_15",
|
||||
"description",
|
||||
|
|
@ -220,15 +219,6 @@
|
|||
"oldfieldname": "autoname",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.naming_rule !== \"Autoincrement\"",
|
||||
"fieldname": "name_case",
|
||||
"fieldtype": "Select",
|
||||
"label": "Name Case",
|
||||
"oldfieldname": "name_case",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nTitle Case\nUPPER CASE"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_15",
|
||||
"fieldtype": "Column Break"
|
||||
|
|
@ -746,4 +736,4 @@
|
|||
"states": [],
|
||||
"track_changes": 1,
|
||||
"translated_doctype": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import frappe
|
|||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import call_hook_method, cint, get_files_path, get_hook_method, get_url
|
||||
from frappe.utils.file_manager import is_safe_path
|
||||
from frappe.utils.image import optimize_image, strip_exif_data
|
||||
|
||||
from .exceptions import AttachmentLimitReached, FolderNotEmpty, MaxFileSizeReachedError
|
||||
|
|
@ -86,9 +87,9 @@ class File(Document):
|
|||
self.handle_is_private_changed()
|
||||
|
||||
if not self.is_folder:
|
||||
# get_full_path validates file URL and name
|
||||
full_path = self.get_full_path()
|
||||
self.validate_file_on_disk(full_path)
|
||||
self.validate_file_path()
|
||||
self.validate_file_url()
|
||||
self.validate_file_on_disk()
|
||||
|
||||
self.file_size = frappe.form_dict.file_size or self.file_size
|
||||
|
||||
|
|
@ -139,12 +140,16 @@ class File(Document):
|
|||
def get_successors(self):
|
||||
return frappe.get_all("File", filters={"folder": self.name}, pluck="name")
|
||||
|
||||
def is_file_path_valid(self, file_path):
|
||||
"""Return True if file path is a valid path for a local file"""
|
||||
def validate_file_path(self):
|
||||
if self.is_remote_file:
|
||||
return
|
||||
|
||||
base_path = os.path.realpath(get_files_path(is_private=self.is_private))
|
||||
if os.path.realpath(file_path).startswith(base_path):
|
||||
return True
|
||||
if not os.path.realpath(self.get_full_path()).startswith(base_path):
|
||||
frappe.throw(
|
||||
_("The File URL you've entered is incorrect"),
|
||||
title=_("Invalid File URL"),
|
||||
)
|
||||
|
||||
def validate_file_url(self):
|
||||
if self.is_remote_file or not self.file_url:
|
||||
|
|
@ -271,11 +276,9 @@ class File(Document):
|
|||
elif not self.is_home_folder:
|
||||
self.folder = "Home"
|
||||
|
||||
def validate_file_on_disk(self, full_path=None):
|
||||
def validate_file_on_disk(self):
|
||||
"""Validates existence file"""
|
||||
|
||||
if full_path is None:
|
||||
full_path = self.get_full_path()
|
||||
full_path = self.get_full_path()
|
||||
|
||||
if full_path.startswith(URL_PREFIXES):
|
||||
return True
|
||||
|
|
@ -455,9 +458,6 @@ class File(Document):
|
|||
if "/files/" in file_path and file_path.startswith(site_url):
|
||||
file_path = file_path.split(site_url, 1)[1]
|
||||
|
||||
if file_path.startswith(URL_PREFIXES):
|
||||
return file_path
|
||||
|
||||
if "/" not in file_path:
|
||||
if self.is_private:
|
||||
file_path = f"/private/files/{file_path}"
|
||||
|
|
@ -470,10 +470,13 @@ class File(Document):
|
|||
elif file_path.startswith("/files/"):
|
||||
file_path = get_files_path(*file_path.split("/files/", 1)[1].split("/"))
|
||||
|
||||
elif file_path.startswith(URL_PREFIXES):
|
||||
pass
|
||||
|
||||
elif not self.file_url:
|
||||
frappe.throw(_("There is some problem with the file url: {0}").format(file_path))
|
||||
|
||||
if not self.is_file_path_valid(file_path):
|
||||
if not is_safe_path(file_path):
|
||||
frappe.throw(_("Cannot access file path {0}").format(file_path))
|
||||
|
||||
if os.path.sep in self.file_name:
|
||||
|
|
|
|||
|
|
@ -433,7 +433,6 @@ class TestFile(FrappeTestCase):
|
|||
self.assertRaisesRegex(IOError, "does not exist", test_file.validate)
|
||||
|
||||
test_file.file_url = None
|
||||
test_file.is_private = 1
|
||||
test_file.file_name = "/private/files/_file"
|
||||
self.assertRaisesRegex(ValidationError, "File name cannot have", test_file.validate)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,5 +9,12 @@ frappe.ui.form.on("Module Def", {
|
|||
frm.set_value("app_name", "frappe");
|
||||
}
|
||||
});
|
||||
|
||||
if (!frappe.boot.developer_mode) {
|
||||
frm.set_df_property("custom", "read_only", 1);
|
||||
if (frm.is_new()) {
|
||||
frm.set_value("custom", 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -208,11 +208,11 @@
|
|||
"label": "Security"
|
||||
},
|
||||
{
|
||||
"default": "06:00",
|
||||
"description": "Session Expiry in Hours e.g. 06:00",
|
||||
"default": "24:00",
|
||||
"description": "Example: Setting this to 24:00 will log out a user if they are not active for 24:00 hours.",
|
||||
"fieldname": "session_expiry",
|
||||
"fieldtype": "Data",
|
||||
"label": "Session Expiry"
|
||||
"label": "Session Expiry (idle timeout)"
|
||||
},
|
||||
{
|
||||
"default": "720:00",
|
||||
|
|
@ -538,7 +538,7 @@
|
|||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-06 03:16:59.090906",
|
||||
"modified": "2022-10-30 12:02:46.639170",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
test_records = frappe.get_test_records("Custom Field")
|
||||
|
|
@ -9,8 +10,6 @@ test_records = frappe.get_test_records("Custom Field")
|
|||
|
||||
class TestCustomField(FrappeTestCase):
|
||||
def test_create_custom_fields(self):
|
||||
from .custom_field import create_custom_fields
|
||||
|
||||
create_custom_fields(
|
||||
{
|
||||
"Address": [
|
||||
|
|
@ -37,3 +36,48 @@ class TestCustomField(FrappeTestCase):
|
|||
self.assertTrue(frappe.db.exists("Custom Field", "Address-_test_custom_field_1"))
|
||||
self.assertTrue(frappe.db.exists("Custom Field", "Address-_test_custom_field_2"))
|
||||
self.assertTrue(frappe.db.exists("Custom Field", "Contact-_test_custom_field_2"))
|
||||
|
||||
def test_custom_field_sorting(self):
|
||||
try:
|
||||
custom_fields = {
|
||||
"ToDo": [
|
||||
{"fieldname": "a_test_field", "insert_after": "b_test_field"},
|
||||
{"fieldname": "b_test_field", "insert_after": "status"},
|
||||
{"fieldname": "c_test_field", "insert_after": "unknown_custom_field"},
|
||||
{"fieldname": "d_test_field", "insert_after": "status"},
|
||||
]
|
||||
}
|
||||
|
||||
create_custom_fields(custom_fields, ignore_validate=True)
|
||||
|
||||
fields = frappe.get_meta("ToDo", cached=False).fields
|
||||
|
||||
for i, field in enumerate(fields):
|
||||
if field.fieldname == "b_test_field":
|
||||
self.assertEqual(fields[i - 1].fieldname, "status")
|
||||
|
||||
if field.fieldname == "d_test_field":
|
||||
self.assertEqual(fields[i - 1].fieldname, "a_test_field")
|
||||
|
||||
self.assertEqual(fields[-1].fieldname, "c_test_field")
|
||||
|
||||
finally:
|
||||
frappe.db.delete(
|
||||
"Custom Field",
|
||||
{
|
||||
"dt": "ToDo",
|
||||
"fieldname": (
|
||||
"in",
|
||||
(
|
||||
"a_test_field",
|
||||
"b_test_field",
|
||||
"c_test_field",
|
||||
"d_test_field",
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
# undo changes commited by DDL
|
||||
# nosemgrep
|
||||
frappe.db.commit()
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
"beta": 0,
|
||||
"is_virtual": 0,
|
||||
"naming_rule": "",
|
||||
"name_case": "",
|
||||
"allow_rename": 1,
|
||||
"hide_toolbar": 0,
|
||||
"allow_copy": 0,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
"beta": 0,
|
||||
"is_virtual": 0,
|
||||
"naming_rule": "",
|
||||
"name_case": "",
|
||||
"allow_rename": 1,
|
||||
"hide_toolbar": 0,
|
||||
"allow_copy": 0,
|
||||
|
|
|
|||
|
|
@ -183,7 +183,6 @@ CREATE TABLE `tabDocType` (
|
|||
`app` varchar(255) DEFAULT NULL,
|
||||
`autoname` varchar(255) DEFAULT NULL,
|
||||
`naming_rule` varchar(40) DEFAULT NULL,
|
||||
`name_case` varchar(255) DEFAULT NULL,
|
||||
`title_field` varchar(255) DEFAULT NULL,
|
||||
`image_field` varchar(255) DEFAULT NULL,
|
||||
`timeline_field` varchar(255) DEFAULT NULL,
|
||||
|
|
|
|||
|
|
@ -188,7 +188,6 @@ CREATE TABLE "tabDocType" (
|
|||
"app" varchar(255) DEFAULT NULL,
|
||||
"autoname" varchar(255) DEFAULT NULL,
|
||||
"naming_rule" varchar(40) DEFAULT NULL,
|
||||
"name_case" varchar(255) DEFAULT NULL,
|
||||
"title_field" varchar(255) DEFAULT NULL,
|
||||
"image_field" varchar(255) DEFAULT NULL,
|
||||
"timeline_field" varchar(255) DEFAULT NULL,
|
||||
|
|
|
|||
|
|
@ -664,14 +664,24 @@ class Engine:
|
|||
and (f"`tab{table}`" not in str(field))
|
||||
):
|
||||
has_join = True
|
||||
table_to_join_on = table_from_string(str(field))
|
||||
if joined_tables.get(join) != table_to_join_on:
|
||||
criterion = getattr(criterion, join)(table_to_join_on).on(
|
||||
getattr(table_to_join_on, "parent") == getattr(frappe.qb.DocType(table), "name")
|
||||
)
|
||||
joined_tables[join] = table_to_join_on
|
||||
|
||||
if has_join:
|
||||
|
||||
def _update_pypika_fields(field):
|
||||
if not is_pypika_function_object(field):
|
||||
field = field if isinstance(field, str) else field.get_sql()
|
||||
field = field if isinstance(field, (str, PseudoColumn)) else field.get_sql()
|
||||
if not TABLE_PATTERN.search(str(field)):
|
||||
if isinstance(field, PseudoColumn):
|
||||
field = field.get_sql()
|
||||
return getattr(frappe.qb.DocType(table), field)
|
||||
else:
|
||||
return field
|
||||
else:
|
||||
field.args = [getattr(frappe.qb.DocType(table), arg.get_sql()) for arg in field.args]
|
||||
return field
|
||||
|
|
|
|||
|
|
@ -140,10 +140,13 @@ class Meta(Document):
|
|||
self.init_field_map()
|
||||
return
|
||||
|
||||
self.add_custom_fields()
|
||||
has_custom_fields = self.add_custom_fields()
|
||||
self.apply_property_setters()
|
||||
self.init_field_map()
|
||||
self.sort_fields()
|
||||
|
||||
if has_custom_fields:
|
||||
self.sort_fields()
|
||||
|
||||
self.get_valid_columns()
|
||||
self.set_custom_permissions()
|
||||
self.add_custom_links_and_actions()
|
||||
|
|
@ -319,7 +322,7 @@ class Meta(Document):
|
|||
return list_fields
|
||||
|
||||
def get_custom_fields(self):
|
||||
return [d for d in self.fields if d.get("is_custom_field")]
|
||||
return [d for d in self.fields if getattr(d, "is_custom_field", False)]
|
||||
|
||||
def get_title_field(self):
|
||||
"""Return the title field of this doctype,
|
||||
|
|
@ -358,17 +361,20 @@ class Meta(Document):
|
|||
if not frappe.db.table_exists("Custom Field"):
|
||||
return
|
||||
|
||||
custom_fields = frappe.db.sql(
|
||||
"""
|
||||
SELECT * FROM `tabCustom Field`
|
||||
WHERE dt = %s AND docstatus < 2
|
||||
""",
|
||||
(self.name,),
|
||||
as_dict=1,
|
||||
custom_fields = frappe.db.get_values(
|
||||
"Custom Field",
|
||||
filters={"dt": self.name},
|
||||
fieldname="*",
|
||||
as_dict=True,
|
||||
order_by="idx",
|
||||
update={"is_custom_field": 1},
|
||||
)
|
||||
|
||||
if not custom_fields:
|
||||
return
|
||||
|
||||
self.extend("fields", custom_fields)
|
||||
return True
|
||||
|
||||
def apply_property_setters(self):
|
||||
"""
|
||||
|
|
@ -452,43 +458,33 @@ class Meta(Document):
|
|||
self._fields = {field.fieldname: field for field in self.fields}
|
||||
|
||||
def sort_fields(self):
|
||||
"""sort on basis of insert_after"""
|
||||
custom_fields = sorted(self.get_custom_fields(), key=lambda df: df.idx)
|
||||
"""Sort custom fields on the basis of insert_after"""
|
||||
|
||||
if custom_fields:
|
||||
newlist = []
|
||||
field_order = []
|
||||
insert_after_map = {}
|
||||
|
||||
# if custom field is at top
|
||||
# insert_after is false
|
||||
for c in list(custom_fields):
|
||||
if not c.insert_after:
|
||||
newlist.append(c)
|
||||
custom_fields.pop(custom_fields.index(c))
|
||||
for field in self.fields:
|
||||
if not getattr(field, "is_custom_field", False):
|
||||
field_order.append(field.fieldname)
|
||||
|
||||
# standard fields
|
||||
newlist += [df for df in self.get("fields") if not df.get("is_custom_field")]
|
||||
elif insert_after := getattr(field, "insert_after", None):
|
||||
insert_after_map.setdefault(insert_after, []).append(field.fieldname)
|
||||
|
||||
newlist_fieldnames = [df.fieldname for df in newlist]
|
||||
for i in range(2):
|
||||
for df in list(custom_fields):
|
||||
if df.insert_after in newlist_fieldnames:
|
||||
cf = custom_fields.pop(custom_fields.index(df))
|
||||
idx = newlist_fieldnames.index(df.insert_after)
|
||||
newlist.insert(idx + 1, cf)
|
||||
newlist_fieldnames.insert(idx + 1, cf.fieldname)
|
||||
else:
|
||||
# if custom field is at the top, insert after is None
|
||||
field_order.insert(0, field.fieldname)
|
||||
|
||||
if not custom_fields:
|
||||
break
|
||||
if insert_after_map:
|
||||
_update_field_order_based_on_insert_after(field_order, insert_after_map)
|
||||
|
||||
# worst case, add remaining custom fields to last
|
||||
if custom_fields:
|
||||
newlist += custom_fields
|
||||
sorted_fields = []
|
||||
|
||||
# renum idx
|
||||
for i, f in enumerate(newlist):
|
||||
f.idx = i + 1
|
||||
for idx, fieldname in enumerate(field_order, 1):
|
||||
field = self._fields[fieldname]
|
||||
field.idx = idx
|
||||
sorted_fields.append(field)
|
||||
|
||||
self.fields = newlist
|
||||
self.fields = sorted_fields
|
||||
|
||||
def set_custom_permissions(self):
|
||||
"""Reset `permissions` with Custom DocPerm if exists"""
|
||||
|
|
@ -809,3 +805,28 @@ def trim_table(doctype, dry_run=True):
|
|||
frappe.db.sql_ddl(f"ALTER TABLE `tab{doctype}` {columns_to_remove}")
|
||||
|
||||
return DROPPED_COLUMNS
|
||||
|
||||
|
||||
def _update_field_order_based_on_insert_after(field_order, insert_after_map):
|
||||
"""Update the field order based on insert_after_map"""
|
||||
|
||||
retry_field_insertion = True
|
||||
|
||||
while retry_field_insertion:
|
||||
retry_field_insertion = False
|
||||
|
||||
for fieldname in list(insert_after_map):
|
||||
if fieldname not in field_order:
|
||||
continue
|
||||
|
||||
custom_field_index = field_order.index(fieldname)
|
||||
for custom_field_name in insert_after_map.pop(fieldname):
|
||||
custom_field_index += 1
|
||||
field_order.insert(custom_field_index, custom_field_name)
|
||||
|
||||
retry_field_insertion = True
|
||||
|
||||
if insert_after_map:
|
||||
# insert_after is an invalid fieldname, add these fields to the end
|
||||
for fields in insert_after_map.values():
|
||||
field_order.extend(fields)
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ def set_new_name(doc):
|
|||
if not doc.name:
|
||||
doc.name = make_autoname("hash", doc.doctype)
|
||||
|
||||
doc.name = validate_name(doc.doctype, doc.name, meta.get_field("name_case"))
|
||||
doc.name = validate_name(doc.doctype, doc.name)
|
||||
|
||||
|
||||
def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool:
|
||||
|
|
@ -439,7 +439,7 @@ def get_default_naming_series(doctype: str) -> str | None:
|
|||
return option
|
||||
|
||||
|
||||
def validate_name(doctype: str, name: int | str, case: str | None = None):
|
||||
def validate_name(doctype: str, name: int | str):
|
||||
|
||||
if not name:
|
||||
frappe.throw(_("No Name Specified for {0}").format(doctype))
|
||||
|
|
@ -457,10 +457,6 @@ def validate_name(doctype: str, name: int | str, case: str | None = None):
|
|||
frappe.throw(
|
||||
_("There were some errors setting the name, please contact the administrator"), frappe.NameError
|
||||
)
|
||||
if case == "Title Case":
|
||||
name = name.title()
|
||||
if case == "UPPER CASE":
|
||||
name = name.upper()
|
||||
name = name.strip()
|
||||
|
||||
if not frappe.get_meta(doctype).get("issingle") and (doctype == name) and (name != "DocType"):
|
||||
|
|
|
|||
|
|
@ -5,19 +5,26 @@ frappe.ui.form.ControlSignature = class ControlSignature extends frappe.ui.form.
|
|||
this.loading = false;
|
||||
super.make();
|
||||
|
||||
this.load_lib().then(() => {
|
||||
// make jSignature field
|
||||
this.body = $('<div class="signature-field"></div>').appendTo(me.wrapper);
|
||||
if (this.df.label) {
|
||||
$(this.wrapper).find("label").text(__(this.df.label));
|
||||
}
|
||||
|
||||
if (this.body.is(":visible")) {
|
||||
this.make_pad();
|
||||
} else {
|
||||
$(document).on("frappe.ui.Dialog:shown", () => {
|
||||
this.make_pad();
|
||||
});
|
||||
}
|
||||
frappe.require("/assets/frappe/js/lib/jSignature.min.js").then(() => {
|
||||
// make jSignature field
|
||||
me.body = $('<div class="signature-field"></div>').prependTo(me.$input_wrapper);
|
||||
|
||||
new ResizeObserver(() => me.make_pad()).observe(me.body[0]);
|
||||
});
|
||||
|
||||
this.img_wrapper = $(`<div class="signature-display">
|
||||
<div class="missing-image attach-missing-image">
|
||||
${frappe.utils.icon("restriction", "md")}</i>
|
||||
</div></div>`).prependTo(this.$input_wrapper);
|
||||
this.img = $("<img class='img-responsive attach-image-display'>")
|
||||
.appendTo(this.img_wrapper)
|
||||
.toggle(false);
|
||||
}
|
||||
|
||||
make_pad() {
|
||||
let width = this.body.width();
|
||||
if (width > 0 && !this.$pad) {
|
||||
|
|
@ -25,9 +32,10 @@ frappe.ui.form.ControlSignature = class ControlSignature extends frappe.ui.form.
|
|||
.jSignature({
|
||||
height: 200,
|
||||
color: "var(--text-color)",
|
||||
width: this.body.width(),
|
||||
decorColor: "black",
|
||||
width,
|
||||
lineWidth: 2,
|
||||
"background-color": "var(--control-bg)",
|
||||
backgroundColor: "var(--control-bg)",
|
||||
})
|
||||
.on("change", this.on_save_sign.bind(this));
|
||||
this.load_pad();
|
||||
|
|
@ -43,15 +51,8 @@ frappe.ui.form.ControlSignature = class ControlSignature extends frappe.ui.form.
|
|||
this.on_reset_sign();
|
||||
return false;
|
||||
});
|
||||
this.refresh_input();
|
||||
}
|
||||
|
||||
this.img_wrapper = $(`<div class="signature-display">
|
||||
<div class="missing-image attach-missing-image">
|
||||
${frappe.utils.icon("restriction", "md")}</i>
|
||||
</div></div>`).appendTo(this.wrapper);
|
||||
this.img = $("<img class='img-responsive attach-image-display'>")
|
||||
.appendTo(this.img_wrapper)
|
||||
.toggle(false);
|
||||
}
|
||||
refresh_input() {
|
||||
// signature dom is not ready
|
||||
|
|
@ -131,11 +132,4 @@ frappe.ui.form.ControlSignature = class ControlSignature extends frappe.ui.form.
|
|||
this.set_my_value(base64_img);
|
||||
this.set_image(this.get_value());
|
||||
}
|
||||
|
||||
load_lib() {
|
||||
if (!this.load_lib_promise) {
|
||||
this.load_lib_promise = frappe.require("/assets/frappe/js/lib/jSignature.min.js");
|
||||
}
|
||||
return this.load_lib_promise;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -423,12 +423,11 @@ textarea.form-control {
|
|||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
position: relative;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.signature-display {
|
||||
margin: 7px 0px;
|
||||
background: var(--control-bg);
|
||||
border-radius: var(--border-radius);
|
||||
.attach-missing-image,
|
||||
.attach-image-display {
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -23,19 +23,7 @@
|
|||
<link rel="canonical" href="{{ canonical }}">
|
||||
|
||||
{%- block head -%}
|
||||
{% if head_html is defined -%}
|
||||
{{ head_html or "" }}
|
||||
{%- endif %}
|
||||
|
||||
{%- if theme.name != 'Standard' -%}
|
||||
<link type="text/css" rel="stylesheet" href="{{ theme.theme_url }}">
|
||||
{%- else -%}
|
||||
{{ include_style('website.bundle.css') }}
|
||||
{%- endif -%}
|
||||
|
||||
{%- for link in web_include_css %}
|
||||
{{ include_style(link) }}
|
||||
{%- endfor -%}
|
||||
{% include "templates/includes/head.html" %}
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block head_include %}
|
||||
|
|
|
|||
13
frappe/templates/includes/head.html
Normal file
13
frappe/templates/includes/head.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{% if head_html is defined -%}
|
||||
{{ head_html or "" }}
|
||||
{%- endif %}
|
||||
|
||||
{%- if theme.name != 'Standard' -%}
|
||||
<link type="text/css" rel="stylesheet" href="{{ theme.theme_url }}">
|
||||
{%- else -%}
|
||||
{{ include_style('website.bundle.css') }}
|
||||
{%- endif -%}
|
||||
|
||||
{%- for link in web_include_css %}
|
||||
{{ include_style(link) }}
|
||||
{%- endfor -%}
|
||||
|
|
@ -206,6 +206,19 @@ class TestQuery(FrappeTestCase):
|
|||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.qb.engine.get_query(
|
||||
"Note",
|
||||
filters={"name": "Test Note Title"},
|
||||
fields=["name", "`tabNote Seen By`.`user` as seen_by", "`tabNote Seen By`.`idx` as idx"],
|
||||
).run(as_dict=1),
|
||||
frappe.get_list(
|
||||
"Note",
|
||||
filters={"name": "Test Note Title"},
|
||||
fields=["name", "`tabNote Seen By`.`user` as seen_by", "`tabNote Seen By`.`idx` as idx"],
|
||||
),
|
||||
)
|
||||
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
def test_comment_stripping(self):
|
||||
self.assertNotIn(
|
||||
|
|
|
|||
|
|
@ -442,3 +442,15 @@ def add_attachments(doctype, name, attachments):
|
|||
files.append(f)
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def is_safe_path(path: str) -> bool:
|
||||
if path.startswith(("http://", "https://")):
|
||||
return True
|
||||
|
||||
basedir = frappe.get_site_path()
|
||||
# ref: https://docs.python.org/3/library/os.path.html#os.path.commonpath
|
||||
matchpath = os.path.realpath(os.path.abspath(path))
|
||||
basedir = os.path.realpath(os.path.abspath(basedir))
|
||||
|
||||
return basedir == os.path.commonpath((basedir, matchpath))
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@
|
|||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Help Category",
|
||||
"name_case": "Title Case",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
@ -70,4 +69,4 @@
|
|||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ dependencies = [
|
|||
"Click~=7.1.2",
|
||||
"GitPython~=3.1.14",
|
||||
"Jinja2~=3.1.2",
|
||||
"Pillow~=9.2.0",
|
||||
"Pillow~=9.3.0",
|
||||
"PyJWT~=2.4.0",
|
||||
"PyMySQL~=1.0.2",
|
||||
"PyPDF2~=2.1.0",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue