Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/version-15' into version-15
Browse files Browse the repository at this point in the history
  • Loading branch information
metalmon committed Dec 13, 2024
2 parents d3a44aa + e6e3cc1 commit 6676af7
Show file tree
Hide file tree
Showing 25 changed files with 2,546 additions and 1,559 deletions.
2 changes: 1 addition & 1 deletion frappe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
)
from .utils.lazy_loader import lazy_import

__version__ = "15.49.1"
__version__ = "15.50.0"
__title__ = "Frappe Framework"

# This if block is never executed when running the code. It is only used for
Expand Down
16 changes: 13 additions & 3 deletions frappe/core/doctype/data_import/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,21 @@ def is_exportable(df):
return fields or []

def get_data_to_export(self):
frappe.permissions.can_export(self.doctype, raise_exception=True)

table_fields = [f for f in self.exportable_fields if f != self.doctype]
data = self.get_data_as_docs()

if not frappe.permissions.can_export(self.doctype):
if frappe.permissions.can_export(self.doctype, is_owner=True):
for doc in data:
if doc.get("owner") != frappe.session.user:
raise frappe.PermissionError(
_("You are not allowed to export {} doctype").format(self.doctype)
)
else:
raise frappe.PermissionError(
_("You are not allowed to export {} doctype").format(self.doctype)
)

for doc in data:
rows = []
rows = self.add_data_row(self.doctype, None, doc, rows, 0)
Expand Down Expand Up @@ -163,7 +173,7 @@ def format_column_name(df):
parent_data = frappe.db.get_list(
self.doctype,
filters=filters,
fields=["name", *parent_fields],
fields=["name", "owner", *parent_fields],
limit_page_length=self.export_page_length,
order_by=order_by,
as_list=0,
Expand Down
4 changes: 4 additions & 0 deletions frappe/core/doctype/file/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ def _save_file(match):
if b"," in content:
content = content.split(b",")[1]

if not content:
# if there is no content, return the original tag
return match.group(0)

try:
content = safe_b64decode(content)
except BinasciiError:
Expand Down
134 changes: 134 additions & 0 deletions frappe/database/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
)
from frappe.exceptions import DoesNotExistError, ImplicitCommitError
from frappe.monitor import get_trace_id
from frappe.query_builder import Case
from frappe.query_builder.functions import Count
from frappe.utils import CallbackManager, cint, get_datetime, get_table_name, getdate, now, sbool
from frappe.utils import cast as cast_fieldtype
Expand Down Expand Up @@ -991,6 +992,139 @@ def set_value(
if dt in self.value_cache:
del self.value_cache[dt]

def bulk_update(
self,
doctype: str,
doc_updates: dict,
*,
chunk_size: int = 100,
modified: str | None = None,
modified_by: str | None = None,
update_modified: bool = True,
debug: bool = False,
):
"""
:param doctype: DocType to update
:param doc_updates: Dictionary of key (docname) and values to update
:param chunk_size: Number of documents to update in a single transaction
:param modified: Use this as the `modified` timestamp.
:param modified_by: Set this user as `modified_by`.
:param update_modified: default True. Update `modified` and `modified_by` fields
:param debug: Print the query in the developer / js console.
doc_updates should be in the following format:
```py
{
"docname1": {
"field1": "value1",
"field2": "value2",
...
},
"docname2": {
"field1": "value1",
"field2": "value2",
...
},
}
```
Note:
- Bigger chunk sizes could be less performant. Use appropriate chunk size based on the number of fields to update.
"""
if not doc_updates:
return

modified_dict = None
if update_modified:
modified_dict = self._get_update_dict(
{}, None, modified=modified, modified_by=modified_by, update_modified=update_modified
)

total_docs = len(doc_updates)
iterator = iter(doc_updates.items())

for __ in range(0, total_docs, chunk_size):
doc_chunk = dict(itertools.islice(iterator, chunk_size))
self._build_and_run_bulk_update_query(doctype, doc_chunk, modified_dict, debug)

@staticmethod
def _build_and_run_bulk_update_query(
doctype: str, doc_updates: dict, modified_dict: dict | None = None, debug: bool = False
):
"""
:param doctype: DocType to update
:param doc_updates: Dictionary of key (docname) and values to update
:param debug: Print the query in the developer / js console.
---
doc_updates should be in the following format:
```py
{
"docname1": {
"field1": "value1",
"field2": "value2",
...
},
"docname2": {
"field1": "value1",
"field2": "value2",
...
},
}
```
---
Query will be built as:
```sql
UPDATE `tabItem`
SET `status` = CASE
WHEN `name` = 'Item-1' THEN 'Close'
WHEN `name` = 'Item-2' THEN 'Open'
WHEN `name` = 'Item-3' THEN 'Close'
WHEN `name` = 'Item-4' THEN 'Cancelled'
ELSE `status`
end,
`description` = CASE
WHEN `name` = 'Item-1' THEN 'This is the first task'
WHEN `name` = 'Item-2' THEN 'This is the second task'
WHEN `name` = 'Item-3' THEN 'This is the third task'
WHEN `name` = 'Item-4' THEN 'This is the fourth task'
ELSE `description`
end
WHERE `name` IN ( 'Item-1', 'Item-2', 'Item-3', 'Item-4' )
```
"""
if not doc_updates:
return

dt = frappe.qb.DocType(doctype)
update_query = frappe.qb.update(dt)

conditions = {}
docnames = list(doc_updates.keys())

for docname, row in doc_updates.items():
for field, value in row.items():
# CASE
if field not in conditions:
conditions[field] = Case()

# WHEN
conditions[field].when(dt.name == docname, value)

for field in conditions:
# ELSE
update_query = update_query.set(dt[field], conditions[field].else_(dt[field]))

if modified_dict:
for column, value in modified_dict.items():
update_query = update_query.set(dt[column], value)

update_query.where(dt.name.isin(docnames)).run(debug=debug)

def set_global(self, key, val, user="__global"):
"""Save a global key value. Global values will be automatically set if they match fieldname."""
self.set_default(key, val, user)
Expand Down
16 changes: 14 additions & 2 deletions frappe/desk/reportview.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,14 +353,16 @@ def export_query():
form_params["limit_page_length"] = None
form_params["as_list"] = True
doctype = form_params.pop("doctype")
if isinstance(form_params["fields"], list):
form_params["fields"].append("owner")
elif isinstance(form_params["fields"], tuple):
form_params["fields"] = form_params["fields"] + ("owner",)
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
translate_values = 1 if form_params.pop("translate_values", None) == "1" else None

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

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

Expand All @@ -374,6 +376,16 @@ def export_query():
db_query = DatabaseQuery(doctype)
ret = db_query.execute(**form_params)

if not frappe.permissions.can_export(doctype):
if frappe.permissions.can_export(doctype, is_owner=True):
for row in ret:
if row[-1] != frappe.session.user:
raise frappe.PermissionError(
_("You are not allowed to export {} doctype").format(doctype)
)
else:
raise frappe.PermissionError(_("You are not allowed to export {} doctype").format(doctype))

if add_totals_row:
ret = append_totals_row(ret)

Expand Down
10 changes: 9 additions & 1 deletion frappe/email/doctype/email_account/email_account.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"imap_folder",
"section_break_12",
"append_emails_to_sent_folder",
"sent_folder_name",
"append_to",
"create_contact",
"enable_automatic_linking",
Expand Down Expand Up @@ -620,12 +621,19 @@
"fieldname": "backend_app_flow",
"fieldtype": "Check",
"label": "Authenticate as Service Principal"
},
{
"depends_on": "eval:!doc.domain && doc.enable_outgoing && doc.enable_incoming && doc.use_imap && doc.append_emails_to_sent_folder",
"fetch_from": "domain.sent_folder_name",
"fieldname": "sent_folder_name",
"fieldtype": "Data",
"label": "Sent Folder Name"
}
],
"icon": "fa fa-inbox",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-07-18 11:05:57.193762",
"modified": "2024-12-05 12:45:15.801652",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",
Expand Down
16 changes: 6 additions & 10 deletions frappe/email/doctype/email_account/email_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,8 @@ class EmailAccount(Document):
password: DF.Password | None
send_notification_to: DF.SmallText | None
send_unsubscribe_message: DF.Check
service: DF.Literal[
"",
"GMail",
"Sendgrid",
"SparkPost",
"Yahoo Mail",
"Outlook.com",
"Yandex.Mail",
]
sent_folder_name: DF.Data | None
service: DF.Literal["", "GMail", "Sendgrid", "SparkPost", "Yahoo Mail", "Outlook.com", "Yandex.Mail"]
signature: DF.TextEditor | None
smtp_port: DF.Data | None
smtp_server: DF.Data | None
Expand Down Expand Up @@ -726,7 +719,10 @@ def append_email_to_sent_folder(self, message):
try:
email_server = self.get_incoming_server(in_receive=True)
message = safe_encode(message)
email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
sent_folder_name = self.sent_folder_name or "Sent"
email_server.imap.append(
sent_folder_name, "\\Seen", imaplib.Time2Internaldate(time.time()), message
)
except Exception:
self.log_error("Unable to add to Sent folder")

Expand Down
15 changes: 12 additions & 3 deletions frappe/email/doctype/email_domain/email_domain.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"use_ssl_for_outgoing",
"column_break_18",
"smtp_port",
"append_emails_to_sent_folder"
"append_emails_to_sent_folder",
"sent_folder_name"
],
"fields": [
{
Expand Down Expand Up @@ -125,6 +126,14 @@
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"default": "Sent",
"depends_on": "eval: doc.append_emails_to_sent_folder",
"description": "Some mailboxes require a different Sent Folder Name e.g. \"INBOX.Sent\"",
"fieldname": "sent_folder_name",
"fieldtype": "Data",
"label": "Sent Folder Name"
}
],
"icon": "icon-inbox",
Expand All @@ -134,7 +143,7 @@
"link_fieldname": "domain"
}
],
"modified": "2023-06-05 12:55:06.434541",
"modified": "2024-12-05 12:41:16.753751",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Domain",
Expand All @@ -154,4 +163,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}
1 change: 1 addition & 0 deletions frappe/email/doctype/email_domain/email_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class EmailDomain(Document):
domain_name: DF.Data
email_server: DF.Data
incoming_port: DF.Data | None
sent_folder_name: DF.Data | None
smtp_port: DF.Data | None
smtp_server: DF.Data
use_imap: DF.Check
Expand Down
Loading

0 comments on commit 6676af7

Please sign in to comment.