Merge branch 'develop' of https://github.com/frappe/frappe into qb-fixes

This commit is contained in:
Aradhya 2022-11-01 13:45:11 +05:30
commit 5703303abb
23 changed files with 214 additions and 132 deletions

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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
}
}

View file

@ -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:

View file

@ -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)

View file

@ -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);
}
}
},
});

View file

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

View file

@ -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()

View file

@ -18,7 +18,6 @@
"beta": 0,
"is_virtual": 0,
"naming_rule": "",
"name_case": "",
"allow_rename": 1,
"hide_toolbar": 0,
"allow_copy": 0,

View file

@ -18,7 +18,6 @@
"beta": 0,
"is_virtual": 0,
"naming_rule": "",
"name_case": "",
"allow_rename": 1,
"hide_toolbar": 0,
"allow_copy": 0,

View file

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

View file

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

View file

@ -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

View file

@ -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)

View file

@ -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"):

View file

@ -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;
}
};

View file

@ -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;

View file

@ -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 %}

View 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 -%}

View file

@ -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(

View file

@ -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))

View file

@ -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
}
}

View file

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