Skip to content

Commit

Permalink
Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr…
Browse files Browse the repository at this point in the history
…-25050

* version-14-hotfix: (29 commits)
  refactor(web list): use CSS class `d-none` instead of `hidden`
  fix: auto filter publish field (#26151)
  fix(desk): Don't trigger minimized dialog action on Ctrl-S
  fix(query_report): don't follow reference report for `prepared_report` field
  fix: avoid popping from empty list
  feat: set the only email address as primary
  fix: shorten_number
  fix: Email tracking without "use_ssl" (backport #26718) (#26734)
  fix(package_release): set path to tarball
  chore: Reword "Recursive Fetch From" message (#26706) (#26707)
  fix: Skip virtual fields in all select queries (backport #26700) (#26701)
  fix: recursive fetch from causes infinite loop (#26695) (#26696)
  fix: Skip letter heads if no permission (backport #26689) (#26691)
  fix: apply perms on letter head selection (backport #26563) (#26582)
  fix(UX): Disable number card filters on standard (#26676) (#26679)
  test: pin cypress
  fix: fail on cypress install failure
  fix: Auto Email Report not working when Add Total Row is enabled (#26667)
  perf: use base32 space for random names instead of base16 (backport #25497) (#26655)
  fix(make_request): don't blindly try to check the content-type
  ...

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
  • Loading branch information
akhilnarang committed Jun 19, 2024
2 parents 7b621b8 + 0cac3f2 commit cf68bf5
Show file tree
Hide file tree
Showing 32 changed files with 181 additions and 53 deletions.
13 changes: 12 additions & 1 deletion frappe/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,18 @@ def clear_cookies():


def validate_ip_address(user):
"""check if IP Address is valid"""
"""
Method to check if the user has IP restrictions enabled, and if so is the IP address they are
connecting from allowlisted.
Certain methods called from our socketio backend need direct access, and so the IP is not
checked for those
"""
if hasattr(frappe.local, "request") and frappe.local.request.path.startswith(
"/api/method/frappe.realtime."
):
return True

from frappe.core.doctype.user.user import get_restricted_ip_list

# Only fetch required fields - for perf
Expand Down
6 changes: 4 additions & 2 deletions frappe/boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ def get_bootinfo():

if frappe.session["user"] != "Guest":
bootinfo.user_info = get_user_info()
bootinfo.sid = frappe.session["sid"]

bootinfo.modules = {}
bootinfo.module_list = []
Expand Down Expand Up @@ -114,7 +113,10 @@ def get_bootinfo():

def get_letter_heads():
letter_heads = {}
for letter_head in frappe.get_all("Letter Head", fields=["name", "content", "footer"]):

if not frappe.has_permission("Letter Head"):
return letter_heads
for letter_head in frappe.get_list("Letter Head", fields=["name", "content", "footer"]):
letter_heads.setdefault(
letter_head.name, {"header": letter_head.content, "footer": letter_head.footer}
)
Expand Down
13 changes: 7 additions & 6 deletions frappe/commands/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import click

import frappe
import frappe.commands
from frappe.commands import get_site, pass_context
from frappe.coverage import CodeCoverage
from frappe.exceptions import SiteNotSpecifiedError
Expand Down Expand Up @@ -916,15 +917,15 @@ def run_ui_tests(
click.secho("Installing Cypress...", fg="yellow")
packages = " ".join(
[
"cypress@^13",
"@4tw/cypress-drag-drop@^2",
"cypress-real-events",
"@testing-library/cypress@^10",
"cypress@13.10.0",
"@4tw/cypress-drag-drop@2.2.5",
"cypress-real-events@1.12.0",
"@testing-library/cypress@10.0.1",
"@testing-library/dom@8.17.1",
"@cypress/code-coverage@^3",
"@cypress/code-coverage@3.12.39",
]
)
frappe.commands.popen(f"yarn add {packages} --no-lockfile")
frappe.commands.popen(f"yarn add {packages} --no-lockfile", raise_err=True)

# run for headless mode
run_or_open = "run --browser chrome" if headless else "open"
Expand Down
9 changes: 8 additions & 1 deletion frappe/contacts/doctype/address/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,15 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
else:
title = "`tabAddress`.city"

# Get additional search fields
if searchfields:
extra_query_fields = ",".join([f"`tabAddress`.{field}" for field in searchfields])
else:
extra_query_fields = "`tabAddress`.country"

return frappe.db.sql(
"""select
`tabAddress`.name, {title}, `tabAddress`.country
`tabAddress`.name, {title}, {extra_query_fields}
from
`tabAddress`
join `tabDynamic Link`
Expand All @@ -279,6 +285,7 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
search_condition=search_condition,
condition=condition or "",
title=title,
extra_query_fields=extra_query_fields,
),
{
"txt": "%" + txt + "%",
Expand Down
3 changes: 3 additions & 0 deletions frappe/contacts/doctype/contact/contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ def set_primary_email(self):
if len([email.email_id for email in self.email_ids if email.is_primary]) > 1:
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold("Email ID")))

if len(self.email_ids) == 1:
self.email_ids[0].is_primary = 1

primary_email_exists = False
for d in self.email_ids:
if d.is_primary == 1:
Expand Down
3 changes: 2 additions & 1 deletion frappe/core/doctype/docfield/docfield.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.is_virtual",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View",
Expand Down Expand Up @@ -565,7 +566,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-10-25 06:53:45.194081",
"modified": "2024-05-28 15:55:44.007917",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",
Expand Down
20 changes: 16 additions & 4 deletions frappe/core/doctype/doctype/doctype.py
Original file line number Diff line number Diff line change
Expand Up @@ -1401,9 +1401,21 @@ def scrub_options_in_select(field):
options_list.append(_option)
field.options = "\n".join(options_list)

def scrub_fetch_from(field):
if hasattr(field, "fetch_from") and field.fetch_from:
field.fetch_from = field.fetch_from.strip("\n").strip()
def validate_fetch_from(field):
if not field.get("fetch_from"):
return

field.fetch_from = field.fetch_from.strip()

if "." not in field.fetch_from:
return
source_field, _target_field = field.fetch_from.split(".", maxsplit=1)

if source_field == field.fieldname:
msg = _(
"{0} contains an invalid Fetch From expression, Fetch From can't be self-referential."
).format(_(field.label, context=field.parent))
frappe.throw(msg, title=_("Recursive Fetch From"))

def validate_data_field_type(docfield):
if docfield.get("is_virtual"):
Expand Down Expand Up @@ -1468,7 +1480,7 @@ def check_no_of_ratings(docfield):
check_unique_and_text(meta.get("name"), d)
check_table_multiselect_option(d)
scrub_options_in_select(d)
scrub_fetch_from(d)
validate_fetch_from(d)
validate_data_field_type(d)

if not frappe.flags.in_migrate:
Expand Down
13 changes: 13 additions & 0 deletions frappe/core/doctype/doctype/test_doctype.py
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,19 @@ def test_not_in_list_view_for_not_allowed_mandatory_field(self):
self.assertTrue(doctype.fields[1].in_list_view)
frappe.delete_doc("DocType", doctype.name)

def test_no_recursive_fetch(self):
recursive_dt = new_doctype(
fields=[
{
"label": "User",
"fieldname": "user",
"fieldtype": "Link",
"fetch_from": "user.email",
}
],
)
self.assertRaises(frappe.ValidationError, recursive_dt.insert)


def new_doctype(
name: str | None = None,
Expand Down
3 changes: 3 additions & 0 deletions frappe/core/doctype/package_release/package_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,8 @@ def make_tarfile(self, package):
)
)

# Set path to tarball
self.path = file.file_url

file.flags.ignore_duplicate_entry_error = True
file.insert()
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.is_virtual",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
Expand Down Expand Up @@ -477,7 +478,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-10-25 06:55:50.718441",
"modified": "2024-05-28 15:56:39.171633",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",
Expand Down
3 changes: 2 additions & 1 deletion frappe/desk/doctype/dashboard_chart/dashboard_chart.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard",
"no_copy": 1,
"read_only_depends_on": "eval: !frappe.boot.developer_mode"
},
{
Expand Down Expand Up @@ -288,7 +289,7 @@
}
],
"links": [],
"modified": "2023-09-18 13:41:05.263676",
"modified": "2024-06-03 13:29:57.960271",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
Expand Down
3 changes: 2 additions & 1 deletion frappe/desk/doctype/kanban_board/kanban_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ def update_order_for_single_card(board_name, docname, from_colname, to_colname,
if from_colname == to_colname:
from_col_order = to_col_order

to_col_order.insert(new_index, from_col_order.pop(old_index))
if from_col_order:
to_col_order.insert(new_index, from_col_order.pop(old_index))

# save updated order
board.columns[from_col_idx].order = frappe.as_json(from_col_order)
Expand Down
6 changes: 6 additions & 0 deletions frappe/desk/doctype/number_card/number_card.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ frappe.ui.form.on("Number Card", {
}

table.on("click", () => {
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
frappe.throw(__("Cannot edit filters for standard number cards"));
}
let dialog = new frappe.ui.Dialog({
title: __("Set Filters"),
fields: fields.filter((f) => !is_dynamic_filter(f)),
Expand Down Expand Up @@ -357,6 +360,9 @@ frappe.ui.form.on("Number Card", {
);

frm.dynamic_filter_table.on("click", () => {
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
frappe.throw(__("Cannot edit filters for standard number cards"));
}
let dialog = new frappe.ui.Dialog({
title: __("Set Dynamic Filters"),
fields: fields,
Expand Down
8 changes: 5 additions & 3 deletions frappe/desk/doctype/todo/todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,17 @@ def update_in_reference(self):
return

try:
assignments = frappe.get_all(
assignments = frappe.db.get_values(
"ToDo",
filters={
{
"reference_type": self.reference_type,
"reference_name": self.reference_name,
"status": ("not in", ("Cancelled", "Closed")),
"allocated_to": ("is", "set"),
},
pluck="allocated_to",
"allocated_to",
pluck=True,
for_update=True,
)
assignments.reverse()

Expand Down
3 changes: 3 additions & 0 deletions frappe/desk/query_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ def get_report_doc(report_name):
doc.custom_filters = data.get("filters")
doc.is_custom_report = True

# Follow whatever the custom report has set for prepared report field
doc.prepared_report = custom_report_doc.prepared_report

if not doc.is_permitted():
frappe.throw(
_("You don't have access to Report: {0}").format(report_name),
Expand Down
3 changes: 2 additions & 1 deletion frappe/desk/reportview.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ def update_wildcard_field_param(data):
isinstance(data.fields, list | tuple) and len(data.fields) == 1 and data.fields[0] == "*"
):
if frappe.get_system_settings("apply_perm_level_on_api_calls"):
data.fields = get_permitted_fields(data.doctype, parenttype=data.parenttype)
parent_type = data.parenttype or data.parent_doctype
data.fields = get_permitted_fields(data.doctype, parenttype=parent_type, ignore_virtual=True)
else:
data.fields = frappe.db.get_table_columns(data.doctype)
return True
Expand Down
4 changes: 3 additions & 1 deletion frappe/email/doctype/auto_email_report/auto_email_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,9 @@ def make_links(columns, data):
elif col.fieldtype == "Currency":
doc = None
if doc_name and col.get("parent") and not frappe.get_meta(col.parent).istable:
doc = frappe.get_doc(col.parent, doc_name)
if frappe.db.exists(col.parent, doc_name):
doc = frappe.get_doc(col.parent, doc_name)

# Pass the Document to get the currency based on docfield option
row[col.fieldname] = frappe.format_value(row[col.fieldname], col, doc=doc)
return columns, data
Expand Down
7 changes: 6 additions & 1 deletion frappe/email/doctype/email_queue/email_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ def __init__(
self.sent_to_atleast_one_recipient = any(
rec.recipient for rec in self.queue_doc.recipients if rec.is_mail_sent()
)
self.email_account_doc = None

def fetch_smtp_server(self):
self.email_account_doc = self.queue_doc.get_email_account(raise_error=True)
Expand Down Expand Up @@ -291,7 +292,11 @@ def build_message(self, recipient_email) -> bytes:
return message

def get_tracker_str(self) -> str:
if frappe.conf.use_ssl and self.email_account_doc.track_email_status:
if (
self.email_account_doc
and self.email_account_doc.track_email_status
and self.queue_doc.communication
):
tracker_url_html = f'<img src="{get_url()}/api/method/frappe.core.doctype.communication.email.mark_email_as_seen?name={self.queue_doc.communication}"/>'
return quopri.encodestring(tracker_url_html.encode()).decode()
return ""
Expand Down
18 changes: 9 additions & 9 deletions frappe/integrations/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ def make_request(method, url, auth=None, headers=None, data=None, json=None, par
)
response.raise_for_status()

content_type = response.headers.get("content-type")
if content_type == "text/plain; charset=utf-8":
return parse_qs(response.text)
elif content_type.startswith("application/") and content_type.split(";")[0].endswith("json"):
return response.json()
elif response.text:
return response.text
else:
return
# Check whether the response has a content-type, before trying to check what it is
if content_type := response.headers.get("content-type"):
if content_type == "text/plain; charset=utf-8":
return parse_qs(response.text)
elif content_type.startswith("application/") and content_type.split(";")[0].endswith("json"):
return response.json()
elif response.text:
return response.text
return
except Exception as exc:
frappe.log_error()
raise exc
Expand Down
2 changes: 1 addition & 1 deletion frappe/model/db_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ def apply_fieldlevel_read_permissions(self):

if wrap_grave_quotes(table) in self.query_tables:
permitted_child_table_fields = get_permitted_fields(
doctype=ch_doctype, parenttype=self.doctype
doctype=ch_doctype, parenttype=self.doctype, ignore_virtual=True
)
if column in permitted_child_table_fields or column in optional_fields:
continue
Expand Down
Loading

0 comments on commit cf68bf5

Please sign in to comment.