Skip to content

Commit

Permalink
Merge pull request #25646 from frappe/version-14-hotfix
Browse files Browse the repository at this point in the history
chore: release v14
  • Loading branch information
ankush committed Mar 27, 2024
2 parents 0c14590 + 2331db4 commit b8617b0
Show file tree
Hide file tree
Showing 24 changed files with 477 additions and 219 deletions.
11 changes: 4 additions & 7 deletions frappe/core/doctype/data_export/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,23 +427,20 @@ def add_data_row(self, rows, dt, parentfield, doc, rowidx):
row[_column_start_end.start + i + 1] = value

def build_response_as_excel(self):
from frappe.desk.utils import provide_binary_file
from frappe.utils.xlsxutils import make_xlsx

filename = frappe.generate_hash(length=10)
with open(filename, "wb") as f:
f.write(cstr(self.writer.getvalue()).encode("utf-8"))
f = open(filename)
reader = csv.reader(f)

from frappe.utils.xlsxutils import make_xlsx

xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else "Data Export")

f.close()
os.remove(filename)

# write out response as a xlsx type
frappe.response["filename"] = _(self.doctype) + ".xlsx"
frappe.response["filecontent"] = xlsx_file.getvalue()
frappe.response["type"] = "binary"
provide_binary_file(self.doctype, "xlsx", xlsx_file.getvalue())

def _append_name_column(self, dt=None):
self.append_field_column(
Expand Down
12 changes: 2 additions & 10 deletions frappe/core/doctype/data_import/exporter.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE

import typing

import frappe
from frappe import _
from frappe.model import display_fieldtypes, no_value_fields
Expand Down Expand Up @@ -243,15 +241,9 @@ def get_csv_array_for_export(self):

def build_response(self):
if self.file_type == "CSV":
self.build_csv_response()
build_csv_response(self.get_csv_array_for_export(), _(self.doctype))
elif self.file_type == "Excel":
self.build_xlsx_response()

def build_csv_response(self):
build_csv_response(self.get_csv_array_for_export(), _(self.doctype))

def build_xlsx_response(self):
build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype))
build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype))

def group_children_data_by_parent(self, children_data: dict[str, list]):
return groupby_metric(children_data, key="parent")
22 changes: 15 additions & 7 deletions frappe/core/doctype/system_settings/system_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"disable_user_pass_login",
"login_with_email_link",
"login_with_email_link_expiry",
"rate_limit_email_link_login",
"allow_error_traceback",
"strip_exif_metadata_from_uploaded_images",
"allow_older_web_view_links",
Expand Down Expand Up @@ -437,11 +438,11 @@
"label": "Include Web View Link in Email"
},
{
"collapsible": 1,
"fieldname": "prepared_report_section",
"fieldtype": "Section Break",
"label": "Reports"
},
"collapsible": 1,
"fieldname": "prepared_report_section",
"fieldtype": "Section Break",
"label": "Reports"
},
{
"default": "Frappe",
"description": "The application name will be used in the Login page.",
Expand Down Expand Up @@ -600,12 +601,19 @@
"fieldname": "store_attached_pdf_document",
"fieldtype": "Check",
"label": "Store Attached PDF Document"
},
{
"depends_on": "login_with_email_link",
"description": "You can set a high value here if multiple users will be logging in from the same network.",
"fieldname": "rate_limit_email_link_login",
"fieldtype": "Int",
"label": "Rate limit for email link login"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2024-03-14 15:18:01.465057",
"modified": "2024-03-22 16:35:52.338727",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
Expand All @@ -624,4 +632,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}
63 changes: 33 additions & 30 deletions frappe/desk/query_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import frappe.desk.reportview
from frappe import _
from frappe.core.utils import ljust_list
from frappe.desk.reportview import clean_params, parse_json
from frappe.model.utils import render_include
from frappe.modules import get_module_path, scrub
from frappe.monitor import add_data_to_monitor
Expand Down Expand Up @@ -318,47 +319,49 @@ def get_report_data(doc, data):
@frappe.whitelist()
def export_query():
"""export from query reports"""
data = frappe._dict(frappe.local.form_dict)
data.pop("cmd", None)
data.pop("csrf_token", None)
from frappe.desk.utils import get_csv_bytes, pop_csv_params, provide_binary_file

if isinstance(data.get("filters"), str):
filters = json.loads(data["filters"])
form_params = frappe._dict(frappe.local.form_dict)
csv_params = pop_csv_params(form_params)
clean_params(form_params)
parse_json(form_params)

if data.get("report_name"):
report_name = data["report_name"]
frappe.permissions.can_export(
frappe.get_cached_value("Report", report_name, "ref_doctype"),
raise_exception=True,
)
report_name = form_params.report_name
frappe.permissions.can_export(
frappe.get_cached_value("Report", report_name, "ref_doctype"),
raise_exception=True,
)

file_format_type = data.get("file_format_type")
custom_columns = frappe.parse_json(data.get("custom_columns", "[]"))
include_indentation = data.get("include_indentation")
visible_idx = data.get("visible_idx")
file_format_type = form_params.file_format_type
custom_columns = frappe.parse_json(form_params.custom_columns or "[]")
include_indentation = form_params.include_indentation
visible_idx = form_params.visible_idx

if isinstance(visible_idx, str):
visible_idx = json.loads(visible_idx)

if file_format_type == "Excel":
data = run(report_name, filters, custom_columns=custom_columns, are_default_filters=False)
data = frappe._dict(data)
if not data.columns:
frappe.respond_as_web_page(
_("No data to export"),
_("You can try changing the filters of your report."),
)
return
data = run(report_name, form_params.filters, custom_columns=custom_columns)
data = frappe._dict(data)
if not data.columns:
frappe.respond_as_web_page(
_("No data to export"),
_("You can try changing the filters of your report."),
)
return

format_duration_fields(data)
xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation)

if file_format_type == "CSV":
content = get_csv_bytes(xlsx_data, csv_params)
file_extension = "csv"
elif file_format_type == "Excel":
from frappe.utils.xlsxutils import make_xlsx

format_duration_fields(data)
xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths)
file_extension = "xlsx"
content = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths).getvalue()

frappe.response["filename"] = _(report_name) + ".xlsx"
frappe.response["filecontent"] = xlsx_file.getvalue()
frappe.response["type"] = "binary"
provide_binary_file(report_name, file_extension, content)


def format_duration_fields(data: frappe._dict) -> None:
Expand Down
94 changes: 38 additions & 56 deletions frappe/desk/reportview.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""build query for doclistview and return results"""

import json
from io import StringIO

import frappe
import frappe.permissions
Expand Down Expand Up @@ -347,30 +346,21 @@ def delete_report(name):
@frappe.read_only()
def export_query():
"""export from report builder"""
title = frappe.form_dict.title
frappe.form_dict.pop("title", None)
from frappe.desk.utils import get_csv_bytes, pop_csv_params, provide_binary_file

form_params = get_form_params()
form_params["limit_page_length"] = None
form_params["as_list"] = True
doctype = form_params.doctype
add_totals_row = None
file_format_type = form_params["file_format_type"]
title = title or doctype

del form_params["doctype"]
del form_params["file_format_type"]

if "add_totals_row" in form_params and form_params["add_totals_row"] == "1":
add_totals_row = 1
del form_params["add_totals_row"]
doctype = form_params.pop("doctype")
file_format_type = form_params.pop("file_format_type")
title = form_params.pop("title", doctype)
csv_params = pop_csv_params(form_params)
add_totals_row = 1 if form_params.pop("add_totals_row", None) == "1" else None

frappe.permissions.can_export(doctype, raise_exception=True)

if "selected_items" in form_params:
si = json.loads(frappe.form_dict.get("selected_items"))
form_params["filters"] = {"name": ("in", si)}
del form_params["selected_items"]
if selection := form_params.pop("selected_items", None):
form_params["filters"] = {"name": ("in", json.loads(selection))}

make_access_log(
doctype=doctype,
Expand All @@ -386,36 +376,24 @@ def export_query():
ret = append_totals_row(ret)

data = [[_("Sr"), *get_labels(db_query.fields, doctype)]]
for i, row in enumerate(ret):
data.append([i + 1, *list(row)])

data.extend([i + 1, *list(row)] for i, row in enumerate(ret))
data = handle_duration_fieldtype_values(doctype, data, db_query.fields)

if file_format_type == "CSV":
# convert to csv
import csv

from frappe.utils.xlsxutils import handle_html

f = StringIO()
writer = csv.writer(f)
for r in data:
# encode only unicode type strings and not int, floats etc.
writer.writerow([handle_html(frappe.as_unicode(v)) if isinstance(v, str) else v for v in r])

f.seek(0)
frappe.response["result"] = cstr(f.read())
frappe.response["type"] = "csv"
frappe.response["doctype"] = title

file_extension = "csv"
content = get_csv_bytes(
[[handle_html(frappe.as_unicode(v)) if isinstance(v, str) else v for v in r] for r in data],
csv_params,
)
elif file_format_type == "Excel":
from frappe.utils.xlsxutils import make_xlsx

xlsx_file = make_xlsx(data, doctype)
file_extension = "xlsx"
content = make_xlsx(data, doctype).getvalue()

frappe.response["filename"] = _(title) + ".xlsx"
frappe.response["filecontent"] = xlsx_file.getvalue()
frappe.response["type"] = "binary"
provide_binary_file(title, file_extension, content)


def append_totals_row(data):
Expand All @@ -442,16 +420,12 @@ def get_labels(fields, doctype):
"""get column labels based on column names"""
labels = []
for key in fields:
key = key.split(" as ")[0]

if key.startswith(("count(", "sum(", "avg(")):
try:
parenttype, fieldname = parse_field(key)
except ValueError:
continue

if "." in key:
parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`")
else:
parenttype = doctype
fieldname = fieldname.strip("`")
parenttype = parenttype or doctype

if parenttype == doctype and fieldname == "name":
label = _("ID", context="Label of name column in report")
Expand All @@ -470,17 +444,12 @@ def get_labels(fields, doctype):

def handle_duration_fieldtype_values(doctype, data, fields):
for field in fields:
key = field.split(" as ")[0]

if key.startswith(("count(", "sum(", "avg(")):
try:
parenttype, fieldname = parse_field(field)
except ValueError:
continue

if "." in key:
parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`")
else:
parenttype = doctype
fieldname = field.strip("`")

parenttype = parenttype or doctype
df = frappe.get_meta(parenttype).get_field(fieldname)

if df and df.fieldtype == "Duration":
Expand All @@ -493,6 +462,19 @@ def handle_duration_fieldtype_values(doctype, data, fields):
return data


def parse_field(field: str) -> tuple[str | None, str]:
"""Parse a field into parenttype and fieldname."""
key = field.split(" as ")[0]

if key.startswith(("count(", "sum(", "avg(")):
raise ValueError

if "." in key:
return key.split(".")[0][4:-1], key.split(".")[1].strip("`")

return None, key.strip("`")


@frappe.whitelist()
def delete_items():
"""delete selected items"""
Expand Down
2 changes: 1 addition & 1 deletion frappe/desk/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def search_widget(
formatted_fields = [f"`tab{meta.name}`.`{f.strip()}`" for f in fields]

# Insert title field query after name
if meta.show_title_field_in_link:
if meta.show_title_field_in_link and meta.title_field:
formatted_fields.insert(1, f"`tab{meta.name}`.{meta.title_field} as `label`")

# In order_by, `idx` gets second priority, because it stores link count
Expand Down
31 changes: 31 additions & 0 deletions frappe/desk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,34 @@ def validate_route_conflict(doctype, name):

def slug(name):
return name.lower().replace(" ", "-")


def pop_csv_params(form_dict):
"""Pop csv params from form_dict and return them as a dict."""
from csv import QUOTE_NONNUMERIC

from frappe.utils.data import cint, cstr

return {
"delimiter": cstr(form_dict.pop("csv_delimiter", ","))[0],
"quoting": cint(form_dict.pop("csv_quoting", QUOTE_NONNUMERIC)),
}


def get_csv_bytes(data: list[list], csv_params: dict) -> bytes:
"""Convert data to csv bytes."""
from csv import writer
from io import StringIO

file = StringIO()
csv_writer = writer(file, **csv_params)
csv_writer.writerows(data)

return file.getvalue().encode("utf-8")


def provide_binary_file(filename: str, extension: str, content: bytes) -> None:
"""Provide a binary file to the client."""
frappe.response["type"] = "binary"
frappe.response["filecontent"] = content
frappe.response["filename"] = f"{filename}.{extension}"

0 comments on commit b8617b0

Please sign in to comment.