diff --git a/frappe/contacts/doctype/address/address.json b/frappe/contacts/doctype/address/address.json index e85a89ff1a..ce8e435bfa 100644 --- a/frappe/contacts/doctype/address/address.json +++ b/frappe/contacts/doctype/address/address.json @@ -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" -} \ No newline at end of file +} diff --git a/frappe/contacts/doctype/contact/contact.json b/frappe/contacts/doctype/contact/contact.json index 696cd61d6c..d342b2d794 100644 --- a/frappe/contacts/doctype/contact/contact.json +++ b/frappe/contacts/doctype/contact/contact.json @@ -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" -} \ No newline at end of file +} diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 6258241f5d..984e78ae5c 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -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 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 501fad2077..1518c72f95 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -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: diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index b2e271a7c4..8997fb3a35 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -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) diff --git a/frappe/core/doctype/module_def/module_def.js b/frappe/core/doctype/module_def/module_def.js index 8d542e620d..aa26c92c8b 100644 --- a/frappe/core/doctype/module_def/module_def.js +++ b/frappe/core/doctype/module_def/module_def.js @@ -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); + } + } }, }); diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 0d612149a6..5fd5ef8163 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -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", diff --git a/frappe/custom/doctype/custom_field/test_custom_field.py b/frappe/custom/doctype/custom_field/test_custom_field.py index dfdaf7ea9a..cf64e4495b 100644 --- a/frappe/custom/doctype/custom_field/test_custom_field.py +++ b/frappe/custom/doctype/custom_field/test_custom_field.py @@ -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() diff --git a/frappe/custom/fixtures/temp_doctype.json b/frappe/custom/fixtures/temp_doctype.json index 343aa2cb37..8c23227389 100644 --- a/frappe/custom/fixtures/temp_doctype.json +++ b/frappe/custom/fixtures/temp_doctype.json @@ -18,7 +18,6 @@ "beta": 0, "is_virtual": 0, "naming_rule": "", - "name_case": "", "allow_rename": 1, "hide_toolbar": 0, "allow_copy": 0, diff --git a/frappe/custom/fixtures/temp_singles.json b/frappe/custom/fixtures/temp_singles.json index b7e2536f25..d6ecd74420 100644 --- a/frappe/custom/fixtures/temp_singles.json +++ b/frappe/custom/fixtures/temp_singles.json @@ -18,7 +18,6 @@ "beta": 0, "is_virtual": 0, "naming_rule": "", - "name_case": "", "allow_rename": 1, "hide_toolbar": 0, "allow_copy": 0, diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index 83b6f6b171..70b37dfcf8 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -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, diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index bc39449113..7ce3cecff8 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -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, diff --git a/frappe/database/query.py b/frappe/database/query.py index bbdd153afd..6b3d42cfa8 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -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 diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 6aa8d1d80e..5a9c2d906d 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -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) diff --git a/frappe/model/naming.py b/frappe/model/naming.py index e5441dde76..d9dc0ee48c 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -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"): diff --git a/frappe/public/js/frappe/form/controls/signature.js b/frappe/public/js/frappe/form/controls/signature.js index b0b2a370ee..167695ac10 100644 --- a/frappe/public/js/frappe/form/controls/signature.js +++ b/frappe/public/js/frappe/form/controls/signature.js @@ -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 = $('
').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 = $('
').prependTo(me.$input_wrapper); + + new ResizeObserver(() => me.make_pad()).observe(me.body[0]); }); + + this.img_wrapper = $(`
+
+ ${frappe.utils.icon("restriction", "md")} +
`).prependTo(this.$input_wrapper); + this.img = $("") + .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 = $(`
-
- ${frappe.utils.icon("restriction", "md")} -
`).appendTo(this.wrapper); - this.img = $("") - .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; - } }; diff --git a/frappe/public/scss/common/controls.scss b/frappe/public/scss/common/controls.scss index 9685c66ed9..0f42f6af9d 100644 --- a/frappe/public/scss/common/controls.scss +++ b/frappe/public/scss/common/controls.scss @@ -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; diff --git a/frappe/templates/base.html b/frappe/templates/base.html index e3bfea559e..cf3c9b34eb 100644 --- a/frappe/templates/base.html +++ b/frappe/templates/base.html @@ -23,19 +23,7 @@ {%- block head -%} - {% if head_html is defined -%} - {{ head_html or "" }} - {%- endif %} - - {%- if theme.name != 'Standard' -%} - - {%- 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 %} diff --git a/frappe/templates/includes/head.html b/frappe/templates/includes/head.html new file mode 100644 index 0000000000..8ce6a76e50 --- /dev/null +++ b/frappe/templates/includes/head.html @@ -0,0 +1,13 @@ +{% if head_html is defined -%} +{{ head_html or "" }} +{%- endif %} + +{%- if theme.name != 'Standard' -%} + +{%- else -%} +{{ include_style('website.bundle.css') }} +{%- endif -%} + +{%- for link in web_include_css %} +{{ include_style(link) }} +{%- endfor -%} diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py index fb85bcfe25..6465d4566d 100644 --- a/frappe/tests/test_query.py +++ b/frappe/tests/test_query.py @@ -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( diff --git a/frappe/utils/file_manager.py b/frappe/utils/file_manager.py index 478787d52e..ae53f8d9f7 100644 --- a/frappe/utils/file_manager.py +++ b/frappe/utils/file_manager.py @@ -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)) diff --git a/frappe/website/doctype/help_category/help_category.json b/frappe/website/doctype/help_category/help_category.json index c706d000e5..0dc37c2b0b 100644 --- a/frappe/website/doctype/help_category/help_category.json +++ b/frappe/website/doctype/help_category/help_category.json @@ -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 -} \ No newline at end of file +} diff --git a/pyproject.toml b/pyproject.toml index e70a38215c..a552474a3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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",