Merge branch 'develop' into gc-perms

This commit is contained in:
Raffael Meyer 2023-04-14 17:27:11 +02:00 committed by GitHub
commit caeaa3bdf0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 264 additions and 201 deletions

View file

@ -182,9 +182,9 @@ if TYPE_CHECKING:
# end: static analysis hack
def init(site: str, sites_path: str = ".", new_site: bool = False) -> None:
def init(site: str, sites_path: str = ".", new_site: bool = False, force=False) -> None:
"""Initialize frappe for the current site. Reset thread locals `frappe.local`"""
if getattr(local, "initialised", None):
if getattr(local, "initialised", None) and not force:
return
local.error_log = []

View file

@ -74,12 +74,18 @@ def application(request: Request):
rollback = sync_database(rollback)
finally:
# Important note:
# this function *must* always return a response, hence any exception thrown outside of
# try..catch block like this finally block needs to be handled appropriately.
if request.method in UNSAFE_HTTP_METHODS and frappe.db and rollback:
frappe.db.rollback()
if getattr(frappe.local, "initialised", False):
for after_request_task in frappe.get_hooks("after_request"):
frappe.call(after_request_task, response=response, request=request)
try:
run_after_request_hooks(request, response)
except Exception as e:
# We can not handle exceptions safely here.
frappe.logger().error("Failed to run after request hook", exc_info=True)
log_request(request, response)
process_response(response)
@ -89,12 +95,20 @@ def application(request: Request):
return response
def run_after_request_hooks(request, response):
if not getattr(frappe.local, "initialised", False):
return
for after_request_task in frappe.get_hooks("after_request"):
frappe.call(after_request_task, response=response, request=request)
def init_request(request):
frappe.local.request = request
frappe.local.is_ajax = frappe.get_request_header("X-Requested-With") == "XMLHttpRequest"
site = _site or request.headers.get("X-Frappe-Site-Name") or get_site_name(request.host)
frappe.init(site=site, sites_path=_sites_path)
frappe.init(site=site, sites_path=_sites_path, force=True)
if not (frappe.local.conf and frappe.local.conf.db_name):
# site does not exist

View file

@ -130,7 +130,7 @@ def load_desktop_data(bootinfo):
from frappe.desk.desktop import get_workspace_sidebar_items
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get("pages")
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
bootinfo.module_wise_workspaces = get_controller("Workspace").get_module_wise_workspaces()
bootinfo.dashboards = frappe.get_all("Dashboard")

View file

@ -66,11 +66,16 @@ class CommunicationEmailMixin:
cc = self.cc_list()
# Need to inform parent document owner incase communication is created through inbound mail
if include_sender:
cc.append(self.sender_mailid)
sender = self.sender_mailid
# if user has selected send_me_a_copy, use their email as sender
if frappe.session.user not in frappe.STANDARD_USERS:
sender = frappe.db.get_value("User", frappe.session.user, "email")
cc.append(sender)
if is_inbound_mail_communcation:
if (doc_owner := self.get_owner()) and (doc_owner not in frappe.STANDARD_USERS):
# inform parent document owner incase communication is created through inbound mail
if doc_owner := self.get_owner():
cc.append(doc_owner)
cc = set(cc) - {self.sender_mailid}
cc.update(self.get_assignees())
@ -82,7 +87,7 @@ class CommunicationEmailMixin:
if is_inbound_mail_communcation:
cc = cc - set(self.cc_list() + self.to_list())
self._final_cc = [m for m in cc if m not in frappe.STANDARD_USERS]
self._final_cc = [m for m in cc if m and m not in frappe.STANDARD_USERS]
return self._final_cc
def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender=False):

View file

@ -308,6 +308,7 @@ class TestCommunicationEmailMixin(FrappeTestCase):
"recipients": recipients,
"cc": cc,
"bcc": bcc,
"sender": "sender@test.com",
}
).insert(ignore_permissions=True)
@ -327,14 +328,26 @@ class TestCommunicationEmailMixin(FrappeTestCase):
comm.delete()
def test_cc(self):
to_list = ["to@test.com"]
cc_list = ["cc+1@test.com", "cc <cc+2@test.com>", "to@test.com"]
user = self.new_user(email="cc+1@test.com", thread_notify=0)
comm = self.new_communication(recipients=to_list, cc=cc_list)
res = comm.get_mail_cc_with_displayname()
self.assertCountEqual(res, ["cc <cc+2@test.com>"])
user.delete()
comm.delete()
def test(assertion, cc_list=None, set_user_as=None, include_sender=False, thread_notify=False):
if set_user_as:
frappe.set_user(set_user_as)
user = self.new_user(email="cc+1@test.com", thread_notify=thread_notify)
comm = self.new_communication(recipients=["to@test.com"], cc=cc_list)
res = comm.get_mail_cc_with_displayname(include_sender=include_sender)
frappe.set_user("Administrator")
user.delete()
comm.delete()
self.assertEqual(res, assertion)
# test filter_thread_notification_disbled_users and filter_mail_recipients
test(["cc <cc+2@test.com>"], cc_list=["cc+1@test.com", "cc <cc+2@test.com>", "to@test.com"])
# test include_sender
test(["sender@test.com"], include_sender=True, thread_notify=True)
test(["cc+1@test.com"], include_sender=True, thread_notify=True, set_user_as="cc+1@test.com")
def test_bcc(self):
bcc_list = [

View file

@ -17,6 +17,7 @@ from frappe.core.api.file import (
move_file,
unzip_file,
)
from frappe.core.doctype.file.utils import get_extension
from frappe.exceptions import ValidationError
from frappe.tests.utils import FrappeTestCase
from frappe.utils import get_files_path
@ -461,7 +462,7 @@ class TestFile(FrappeTestCase):
).insert(ignore_permissions=True)
test_file.make_thumbnail()
self.assertTrue(test_file.thumbnail_url.endswith("_small.jpeg"))
self.assertTrue(test_file.thumbnail_url.endswith("_small.jpg"))
# test local image
test_file.db_set("thumbnail_url", None)
@ -739,3 +740,10 @@ class TestFileOptimization(FrappeTestCase):
size_after_rollback = os.stat(image_path).st_size
self.assertEqual(size_before_optimization, size_after_rollback)
def test_image_header_guessing(self):
file_path = frappe.get_app_path("frappe", "tests/data/sample_image_for_optimization.jpg")
with open(file_path, "rb") as f:
file_content = f.read()
self.assertEqual(get_extension("", None, file_content), "jpg")

View file

@ -1,5 +1,4 @@
import hashlib
import imghdr
import mimetypes
import os
import re
@ -7,6 +6,7 @@ from io import BytesIO
from typing import TYPE_CHECKING, Optional
from urllib.parse import unquote
import filetype
import requests
import requests.exceptions
from PIL import Image
@ -76,9 +76,11 @@ def get_extension(
mimetype = mimetypes.guess_type(filename + "." + extn)[0]
if mimetype is None or not mimetype.startswith("image/") and content:
# detect file extension by reading image header properties
extn = imghdr.what(filename + "." + (extn or ""), h=content)
if mimetype is None and extn is None and content:
# detect file extension by using filetype matchers
_type_info = filetype.match(content)
if _type_info:
extn = _type_info.extension
return extn

View file

@ -51,7 +51,7 @@
"icon": "fa fa-globe",
"in_create": 1,
"links": [],
"modified": "2022-08-14 18:54:03.490836",
"modified": "2023-04-13 13:48:38.127995",
"modified_by": "Administrator",
"module": "Core",
"name": "Language",
@ -66,13 +66,8 @@
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Guest",
"share": 1
"role": "All",
"read": 1
}
],
"search_fields": "language_name",

View file

@ -148,11 +148,13 @@
{
"collapsible": 1,
"collapsible_depends_on": "filters",
"depends_on": "eval:doc.report_type != \"Custom Report\"",
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"depends_on": "eval:doc.report_type != \"Custom Report\"",
"fieldname": "filters",
"fieldtype": "Table",
"label": "Filters",
@ -161,11 +163,13 @@
{
"collapsible": 1,
"collapsible_depends_on": "columns",
"depends_on": "eval:doc.report_type != \"Custom Report\"",
"fieldname": "columns_section",
"fieldtype": "Section Break",
"label": "Columns"
},
{
"depends_on": "eval:doc.report_type != \"Custom Report\"",
"fieldname": "columns",
"fieldtype": "Table",
"label": "Columns",
@ -182,7 +186,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-11-20 14:56:36.578412",
"modified": "2023-04-07 18:18:11.782178",
"modified_by": "Administrator",
"module": "Core",
"name": "Report",

View file

@ -169,7 +169,7 @@ class Report(Document):
return columns, result
def run_query_report(self, filters, user, ignore_prepared_report=False):
def run_query_report(self, filters=None, user=None, ignore_prepared_report=False):
columns, result = [], []
data = frappe.desk.query_report.run(
self.name, filters=filters, user=user, ignore_prepared_report=ignore_prepared_report

View file

@ -118,11 +118,10 @@ class TestReport(FrappeTestCase):
}
]
),
json.dumps({"user": "Administrator", "doctype": "User"}),
)
custom_report = frappe.get_doc("Report", custom_report_name)
columns, result = custom_report.run_query_report(
filters={"user": "Administrator", "doctype": "User"}, user=frappe.session.user
)
columns, result = custom_report.run_query_report(user=frappe.session.user)
self.assertListEqual(["email"], [column.get("fieldname") for column in columns])
admin_dict = frappe.core.utils.find(result, lambda d: d["name"] == "Administrator")

View file

@ -37,20 +37,16 @@ class TestTranslation(FrappeTestCase):
frappe.local.lang = "es"
clear_translation_cache()
self.assertTrue(_(data[0][0]), data[0][1])
clear_translation_cache()
self.assertTrue(_(data[1][0]), data[1][1])
frappe.local.lang = "es-MX"
# different translation for es-MX
clear_translation_cache()
self.assertTrue(_(data[2][0]), data[2][1])
# from spanish (general)
clear_translation_cache()
self.assertTrue(_(data[1][0]), data[1][1])
def test_multi_language_translations(self):
@ -112,7 +108,3 @@ def create_translation(key, val):
translation.translated_text = val[1]
translation.save()
return translation
def clear_translation_cache():
frappe.cache().delete_key("translations_from_apps", shared=True)

View file

@ -1,6 +1,7 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# License: MIT. See LICENSE
from collections import defaultdict
from json import loads
import frappe
@ -49,12 +50,22 @@ class Workspace(Document):
delete_folder(self.module, "Workspace", self.title)
@staticmethod
def get_module_page_map():
pages = frappe.get_all(
"Workspace", fields=["name", "module"], filters={"for_user": ""}, as_list=1
def get_module_wise_workspaces():
workspaces = frappe.get_all(
"Workspace",
fields=["name", "module"],
filters={"for_user": "", "public": 1},
order_by="creation",
)
return {page[1]: page[0] for page in pages if page[1]}
module_workspaces = defaultdict(list)
for workspace in workspaces:
if not workspace.module:
continue
module_workspaces[workspace.module].append(workspace.name)
return module_workspaces
def get_link_groups(self):
cards = []

View file

@ -15,12 +15,13 @@ from frappe.model.utils import render_include
from frappe.modules import get_module_path, scrub
from frappe.monitor import add_data_to_monitor
from frappe.permissions import get_role_permissions
from frappe.utils import cint, cstr, flt, format_duration, get_html_format
from frappe.utils import cint, cstr, flt, format_duration, get_html_format, sbool
def get_report_doc(report_name):
doc = frappe.get_doc("Report", report_name)
doc.custom_columns = []
doc.custom_filters = []
if doc.report_type == "Custom Report":
custom_report_doc = doc
@ -30,7 +31,8 @@ def get_report_doc(report_name):
if custom_report_doc.json:
data = json.loads(custom_report_doc.json)
if data:
doc.custom_columns = data["columns"]
doc.custom_columns = data.get("columns")
doc.custom_filters = data.get("filters")
doc.is_custom_report = True
if not doc.is_permitted():
@ -182,6 +184,7 @@ def run(
custom_columns=None,
is_tree=False,
parent_field=None,
are_default_filters=True,
):
report = get_report_doc(report_name)
if not user:
@ -194,6 +197,9 @@ def run(
result = None
if sbool(are_default_filters) and report.custom_filters:
filters = report.custom_filters
if report.prepared_report and not ignore_prepared_report and not custom_columns:
if filters:
if isinstance(filters, str):
@ -209,6 +215,9 @@ def run(
result["add_total_row"] = report.add_total_row and not result.get("skip_total_row", False)
if sbool(are_default_filters) and report.custom_filters:
result["custom_filters"] = report.custom_filters
return result
@ -463,7 +472,7 @@ def get_data_for_custom_report(columns):
@frappe.whitelist()
def save_report(reference_report, report_name, columns):
def save_report(reference_report, report_name, columns, filters):
report_doc = get_report_doc(reference_report)
docname = frappe.db.exists(
@ -479,6 +488,7 @@ def save_report(reference_report, report_name, columns):
report = frappe.get_doc("Report", docname)
existing_jd = json.loads(report.json)
existing_jd["columns"] = json.loads(columns)
existing_jd["filters"] = json.loads(filters)
report.update({"json": json.dumps(existing_jd, separators=(",", ":"))})
report.save()
frappe.msgprint(_("Report updated successfully"))
@ -489,7 +499,7 @@ def save_report(reference_report, report_name, columns):
{
"doctype": "Report",
"report_name": report_name,
"json": f'{{"columns":{columns}}}',
"json": f'{{"columns":{columns},"filters":{filters}}}',
"ref_doctype": report_doc.ref_doctype,
"is_standard": "No",
"report_type": "Custom Report",

View file

@ -678,7 +678,7 @@ def get_filters_cond(
for f in filters:
if isinstance(f[1], str) and f[1][0] == "!":
flt.append([doctype, f[0], "!=", f[1][1:]])
elif isinstance(f[1], (list, tuple)) and f[1][0] in (
elif isinstance(f[1], (list, tuple)) and f[1][0].lower() in (
">",
"<",
">=",

View file

@ -151,7 +151,7 @@ class EmailServer:
except _socket.error:
# log performs rollback and logs error in Error Log
self.log_error("POP: Unable to connect")
frappe.log_error("POP: Unable to connect")
# Invalid mail server -- due to refusing connection
frappe.msgprint(_("Invalid Mail Server. Please rectify and try again."))
@ -332,7 +332,7 @@ class EmailServer:
else:
# log performs rollback and logs error in Error Log
self.log_error("Unable to fetch email", self.make_error_msg(msg_num, incoming_mail))
frappe.log_error("Unable to fetch email", self.make_error_msg(msg_num, incoming_mail))
self.errors = True
frappe.db.rollback()

View file

@ -186,11 +186,13 @@ scheduler_events = {
"frappe.oauth.delete_oauth2_data",
"frappe.website.doctype.web_page.web_page.check_publish_status",
"frappe.twofactor.delete_all_barcodes_for_users",
]
],
"0/10 * * * *": [
"frappe.email.doctype.email_account.email_account.pull",
],
},
"all": [
"frappe.email.queue.flush",
"frappe.email.doctype.email_account.email_account.pull",
"frappe.email.doctype.email_account.email_account.notify_unreplied",
"frappe.utils.global_search.sync_global_search",
"frappe.monitor.flush",

View file

@ -79,7 +79,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-02-24 14:59:24.743552",
"modified": "2023-04-12 11:50:01.702862",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook Request Log",
@ -101,6 +101,5 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
"states": []
}

View file

@ -3,7 +3,7 @@ import datetime
import hashlib
import re
from http import cookies
from urllib.parse import unquote, urlparse
from urllib.parse import unquote, urljoin, urlparse
import jwt
import pytz
@ -575,7 +575,7 @@ def get_userinfo(user):
if frappe.utils.validate_url(user.user_image, valid_schemes=valid_url_schemes):
picture = user.user_image
else:
picture = frappe_server_url + "/" + user.user_image
picture = urljoin(frappe_server_url, user.user_image)
userinfo = frappe._dict(
{

View file

@ -1189,7 +1189,10 @@ export default class Grid {
this.docfields.find((d) => d.fieldname === fieldname)[property] = value;
if (this.user_defined_columns && this.user_defined_columns.length > 0) {
this.user_defined_columns.find((d) => d.fieldname === fieldname)[property] = value;
let field = this.user_defined_columns.find((d) => d.fieldname === fieldname);
if (field && Object.keys(field).includes(property)) {
field[property] = value;
}
}
this.debounced_refresh();

View file

@ -1296,7 +1296,7 @@ export default class GridRow {
.find(".grid-delete-row")
.toggle(!(this.grid.df && this.grid.df.cannot_delete_rows));
frappe.dom.freeze("", "dark");
frappe.dom.freeze("", "dark grid-form");
if (cur_frm) cur_frm.cur_grid = this;
this.wrapper.addClass("grid-row-open");
if (

View file

@ -82,25 +82,33 @@ frappe.breadcrumbs = {
this.$breadcrumbs.append(html);
},
get last_route() {
return frappe.route_history.slice(-2)[0];
},
set_workspace_breadcrumb(breadcrumbs) {
// get preferred module for breadcrumbs, based on sent via module
// get preferred module for breadcrumbs, based on history and module
if (!breadcrumbs.workspace) {
this.set_workspace(breadcrumbs);
}
if (breadcrumbs.workspace) {
if (
!breadcrumbs.module_info.blocked &&
frappe.visible_modules.includes(breadcrumbs.module_info.module)
) {
$(
`<li><a href="/app/${frappe.router.slug(breadcrumbs.workspace)}">${__(
breadcrumbs.workspace
)}</a></li>`
).appendTo(this.$breadcrumbs);
}
if (!breadcrumbs.workspace) {
return;
}
if (
breadcrumbs.module_info &&
(breadcrumbs.module_info.blocked ||
!frappe.visible_modules.includes(breadcrumbs.module_info.module))
) {
return;
}
$(
`<li><a href="/app/${frappe.router.slug(breadcrumbs.workspace)}">${__(
breadcrumbs.workspace
)}</a></li>`
).appendTo(this.$breadcrumbs);
},
set_workspace(breadcrumbs) {
@ -117,6 +125,19 @@ frappe.breadcrumbs = {
breadcrumbs.module = this.preferred[breadcrumbs.doctype];
}
// guess from last route
if (this.last_route?.[0] == "Workspaces") {
let last_workspace = this.last_route[1];
if (
breadcrumbs.module &&
frappe.boot.module_wise_workspaces[breadcrumbs.module]?.includes(last_workspace)
) {
breadcrumbs.workspace = last_workspace;
return;
}
}
if (breadcrumbs.module) {
if (this.module_map[breadcrumbs.module]) {
breadcrumbs.module = this.module_map[breadcrumbs.module];
@ -125,8 +146,11 @@ frappe.breadcrumbs = {
breadcrumbs.module_info = frappe.get_module(breadcrumbs.module);
// set workspace
if (breadcrumbs.module_info && frappe.boot.module_page_map[breadcrumbs.module]) {
breadcrumbs.workspace = frappe.boot.module_page_map[breadcrumbs.module];
if (
breadcrumbs.module_info &&
frappe.boot.module_wise_workspaces[breadcrumbs.module]
) {
breadcrumbs.workspace = frappe.boot.module_wise_workspaces[breadcrumbs.module][0];
}
}
},

View file

@ -2,6 +2,9 @@
// MIT License. See license.txt
import DataTable from "frappe-datatable";
// Expose DataTable globally to allow customizations.
window.DataTable = DataTable;
frappe.provide("frappe.widget.utils");
frappe.provide("frappe.views");
frappe.provide("frappe.query_reports");
@ -539,7 +542,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (this.prepared_report) {
this.reset_report_view();
} else if (!this._no_refresh) {
this.refresh();
this.refresh(true);
}
}
};
@ -595,10 +598,25 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.page.clear_fields();
}
refresh() {
refresh(have_filters_changed) {
this.toggle_message(true);
this.toggle_report(false);
let filters = this.get_filter_values(true);
// for custom reports,
// are_default_filters is true if the filters haven't been modified and for all filters,
// the filter value is the default value or there's no default value for the filter and the current value is empty.
// are_default_filters is false otherwise.
let are_default_filters = this.filters
.map((filter) => {
return (
!have_filters_changed &&
(filter.default === filter.value || (!filter.default && !filter.value))
);
})
.every((res) => res === true);
this.show_loading_screen();
// only one refresh at a time
@ -621,6 +639,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
filters: filters,
is_tree: this.report_settings.tree,
parent_field: this.report_settings.parent_field,
are_default_filters: are_default_filters,
},
callback: resolve,
always: () => this.page.btn_secondary.prop("disabled", false),
@ -633,6 +652,11 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.execution_time = data.execution_time || 0.1;
if (data.custom_filters) {
this.set_filters(data.custom_filters);
this.previous_filters = data.custom_filters;
}
if (data.prepared_report) {
this.prepared_report = true;
this.prepared_report_document = data.doc;
@ -933,7 +957,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (this.report_settings.get_datatable_options) {
datatable_options = this.report_settings.get_datatable_options(datatable_options);
}
this.datatable = new DataTable(this.$report[0], datatable_options);
this.datatable = new window.DataTable(this.$report[0], datatable_options);
}
if (typeof this.report_settings.initial_depth == "number") {
@ -1712,6 +1736,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
reference_report: this.report_name,
report_name: values.report_name,
columns: this.get_visible_columns(),
filters: this.get_filter_values(),
},
callback: function (r) {
this.show_save = false;

View file

@ -243,6 +243,8 @@ $input-height: 28px !default;
--highlight-color: var(--gray-50);
--yellow-highlight-color: var(--yellow-50);
--btn-group-border-color: var(--gray-300);
--field-placeholder-color: var(--gray-50);
--highlight-shadow: 1px 1px 10px var(--blue-50), 0px 0px 4px var(--blue-600);

View file

@ -321,7 +321,7 @@
overflow: hidden;
height: 0;
opacity: 0;
z-index: 1051;
z-index: 1021;
border-radius: var(--border-radius-md);
@include base-grid();
@ -362,6 +362,10 @@
}
}
#freeze.grid-form {
z-index: 1020;
}
.recorder-form-in-grid {
z-index: 0;
@include base-grid();

View file

@ -100,6 +100,8 @@
--highlight-color: var(--gray-700);
--yellow-highlight-color: var(--yellow-700);
--btn-group-border-color: var(--gray-800);
--field-placeholder-color: var(--gray-700);
--highlight-shadow: 1px 1px 10px var(--blue-900), 0px 0px 4px var(--blue-500);

View file

@ -234,6 +234,21 @@ h2 {
font-size: var(--text-md);
}
.btn-group {
.btn {
box-shadow: none;
outline: 1px solid var(--btn-group-border-color);
&:not(:first-child) {
margin-left: 1px;
}
&:focus {
outline: 2px solid var(--dark-border-color);
}
}
}
.btn-xs {
@extend .btn-sm;
line-height: 1.2;
@ -418,7 +433,7 @@ kbd {
// freeze backdrop text
#freeze {
z-index: 1050;
z-index: 1055;
bottom: 0;
opacity: 0;
background-color: var(--bg-color);

View file

@ -189,29 +189,12 @@ $level-margin-right: 8px;
.list-paging-area, .footnote-area {
border-top: 1px solid var(--border-color);
.btn-group {
box-shadow: var(--drop-shadow);
border-radius: var(--border-radius-md);
&> .btn:nth-child(2) {
border-left: none;
border-right: none;
}
.btn-paging {
box-shadow: none;
margin-left: 0px !important;
border: 1px solid var(--dark-border-color);
&.btn-info {
background-color: var(--gray-600);
border-color: var(--gray-600);
color: var(--white);
font-weight: var(--text-bold);
}
}
.btn-group .btn-paging.btn-info {
background-color: var(--gray-600);
border-color: var(--gray-600);
color: var(--white);
font-weight: var(--text-bold);
}
}
.frappe-card {

View file

@ -8,7 +8,6 @@ from unittest.mock import patch
import frappe
import frappe.translate
from frappe import _
from frappe.core.doctype.translation.test_translation import clear_translation_cache
from frappe.tests.utils import FrappeTestCase
from frappe.translate import (
extract_javascript,
@ -39,15 +38,11 @@ class TestTranslate(FrappeTestCase):
if self._testMethodName in self.guest_sessions_required:
frappe.set_user("Guest")
clear_translation_cache()
def tearDown(self):
frappe.form_dict.pop("_lang", None)
if self._testMethodName in self.guest_sessions_required:
frappe.set_user("Administrator")
clear_translation_cache()
def test_extract_message_from_file(self):
data = frappe.translate.get_messages_from_file(translation_string_file)
exp_filename = "apps/frappe/frappe/tests/translation_test_file.txt"

View file

@ -611,12 +611,12 @@ class TestDateUtils(FrappeTestCase):
now = get_datetime()
test_cases = {
now: _("just now"),
now: _("1 second ago"),
add_to_date(now, minutes=-1): _("1 minute ago"),
add_to_date(now, minutes=-3): _("3 minutes ago"),
add_to_date(now, hours=-1): _("1 hour ago"),
add_to_date(now, hours=-2): _("2 hours ago"),
add_to_date(now, days=-1): _("Yesterday"),
add_to_date(now, days=-1): _("1 day ago"),
add_to_date(now, days=-5): _("5 days ago"),
add_to_date(now, days=-8): _("1 week ago"),
add_to_date(now, days=-14): _("2 weeks ago"),

View file

@ -236,6 +236,7 @@ class TestWebsite(FrappeTestCase):
def test_printview_page(self):
frappe.db.value_cache[("DocType", "Language", "name")] = (("Language",),)
frappe.set_user("Administrator")
content = get_response_content("/Language/ru")
self.assertIn('<div class="print-format">', content)
self.assertIn("<div>Language</div>", content)

View file

@ -56,7 +56,6 @@ CSV_STRIP_WHITESPACE_PATTERN = re.compile(r"{\s?([0-9]+)\s?}")
# Cache keys
MERGED_TRANSLATION_KEY = "merged_translations"
APP_TRANSLATION_KEY = "translations_from_apps"
USER_TRANSLATION_KEY = "lang_user_translations"
@ -171,7 +170,7 @@ def get_dict(fortype: str, name: str | None = None) -> dict[str, str]:
fortype = fortype.lower()
cache = frappe.cache()
asset_key = fortype + ":" + (name or "-")
translation_assets = cache.hget("translation_assets", frappe.local.lang, shared=True) or {}
translation_assets = cache.hget("translation_assets", frappe.local.lang) or {}
if asset_key not in translation_assets:
messages = []
@ -211,7 +210,7 @@ def get_dict(fortype: str, name: str | None = None) -> dict[str, str]:
# remove untranslated
message_dict = {k: v for k, v in message_dict.items() if k != v}
translation_assets[asset_key] = message_dict
cache.hset("translation_assets", frappe.local.lang, translation_assets, shared=True)
cache.hset("translation_assets", frappe.local.lang, translation_assets)
translation_map: dict = translation_assets[asset_key]
@ -308,20 +307,17 @@ def get_translations_from_apps(lang, apps=None):
if lang == "en":
return {}
def _get_from_disk():
translations = {}
for app in apps or frappe.get_all_apps(True):
path = os.path.join(frappe.get_pymodule_path(app), "translations", lang + ".csv")
translations.update(get_translation_dict_from_file(path, lang, app) or {})
if "-" in lang:
parent = lang.split("-", 1)[0]
parent_translations = get_translations_from_apps(parent)
parent_translations.update(translations)
return parent_translations
translations = {}
for app in apps or frappe.get_installed_apps(_ensure_on_bench=True):
path = os.path.join(frappe.get_pymodule_path(app), "translations", lang + ".csv")
translations.update(get_translation_dict_from_file(path, lang, app) or {})
if "-" in lang:
parent = lang.split("-", 1)[0]
parent_translations = get_translations_from_apps(parent)
parent_translations.update(translations)
return parent_translations
return translations
return frappe.cache().hget(APP_TRANSLATION_KEY, lang, shared=True, generator=_get_from_disk)
return translations
def get_translation_dict_from_file(path, lang, app, throw=False) -> dict[str, str]:
@ -375,8 +371,7 @@ def clear_cache():
# clear translations saved in boot cache
cache.delete_key("bootinfo")
cache.delete_key("translation_assets", shared=True)
cache.delete_key(APP_TRANSLATION_KEY, shared=True)
cache.delete_key("translation_assets")
cache.delete_key(USER_TRANSLATION_KEY)
cache.delete_key(MERGED_TRANSLATION_KEY)
@ -687,7 +682,7 @@ def get_messages_from_include_files(app_name=None):
def get_all_messages_from_js_files(app_name=None):
"""Extracts all translatable strings from app `.js` files"""
messages = []
for app in [app_name] if app_name else frappe.get_installed_apps():
for app in [app_name] if app_name else frappe.get_installed_apps(_ensure_on_bench=True):
if os.path.exists(frappe.get_app_path(app, "public")):
for basepath, folders, files in os.walk(frappe.get_app_path(app, "public")):
if "frappe/public/js/lib" in basepath:

View file

@ -1514,55 +1514,20 @@ def escape_html(text: str) -> str:
def pretty_date(iso_datetime: datetime.datetime | str) -> str:
"""
Takes an ISO time and returns a string representing how
long ago the date represents.
Ported from PrettyDate by John Resig
"""
from frappe import _
Return a localized string representation of the delta to the current system time.
For example, "1 hour ago", "2 days ago", "in 5 seconds", etc.
"""
if not iso_datetime:
return ""
import math
from babel.dates import format_timedelta
if isinstance(iso_datetime, str):
iso_datetime = datetime.datetime.strptime(iso_datetime, DATETIME_FORMAT)
now_dt = datetime.datetime.strptime(now(), DATETIME_FORMAT)
dt_diff = now_dt - iso_datetime
# available only in python 2.7+
# dt_diff_seconds = dt_diff.total_seconds()
dt_diff_seconds = dt_diff.days * 86400.0 + dt_diff.seconds
dt_diff_days = math.floor(dt_diff_seconds / 86400.0)
# differnt cases
if dt_diff_seconds < 60.0:
return _("just now")
elif dt_diff_seconds < 120.0:
return _("1 minute ago")
elif dt_diff_seconds < 3600.0:
return _("{0} minutes ago").format(cint(math.floor(dt_diff_seconds / 60.0)))
elif dt_diff_seconds < 7200.0:
return _("1 hour ago")
elif dt_diff_seconds < 86400.0:
return _("{0} hours ago").format(cint(math.floor(dt_diff_seconds / 3600.0)))
elif dt_diff_days == 1.0:
return _("Yesterday")
elif dt_diff_days < 7.0:
return _("{0} days ago").format(cint(dt_diff_days))
elif dt_diff_days < 14:
return _("1 week ago")
elif dt_diff_days < 31.0:
return _("{0} weeks ago").format(dt_diff_days // 7)
elif dt_diff_days < 61.0:
return _("1 month ago")
elif dt_diff_days < 365.0:
return _("{0} months ago").format(dt_diff_days // 30)
elif dt_diff_days < 730.0:
return _("1 year ago")
else:
return _("{0} years ago").format(dt_diff_days // 365)
locale = frappe.local.lang.replace("-", "_") if frappe.local.lang else None
return format_timedelta(iso_datetime - now_dt, add_direction=True, locale=locale)
def comma_or(some_list, add_quotes=True):

View file

@ -137,11 +137,8 @@
{% if success_url %}
<div class="success_url_message">
<p>
<span>Click on this </span>
<a href="{{ success_url }}">{{_("URL")}}</a>
<span> if you are not redirected within </span>
<span class="time">5</span>
<span> seconds.</span>
{% set success_link = "<a href='{0}'>link</a>".format(success_url) %}
<span>{{ _("Click on this {0} if you are not redirected within 5 seconds").format(success_link) }} </span>
</p>
</div>
{% else %}

View file

@ -40,17 +40,21 @@ frappe.ui.form.on("Workflow", {
},
update_field_options: function (frm) {
var doc = frm.doc;
if (doc.document_type) {
const get_field_method =
"frappe.workflow.doctype.workflow.workflow.get_fieldnames_for";
frappe.xcall(get_field_method, { doctype: doc.document_type }).then((resp) => {
frm.fields_dict.states.grid.update_docfield_property(
"update_field",
"options",
[""].concat(resp)
);
});
if (!doc.document_type) {
return;
}
frappe.model.with_doctype(doc.document_type, () => {
const fieldnames = frappe
.get_meta(doc.document_type)
.fields.filter((field) => !frappe.model.no_value_type.includes(field.fieldtype))
.map((field) => field.fieldname);
frm.fields_dict.states.grid.update_docfield_property(
"update_field",
"options",
[""].concat(fieldnames)
);
});
},
create_warning_dialog: function (frm) {
const warning_html = `<p class="bold">

View file

@ -124,17 +124,9 @@ class Workflow(Document):
)
@frappe.whitelist()
def get_fieldnames_for(doctype):
frappe.has_permission(doctype=doctype, ptype='read', throw=True)
return [
f.fieldname for f in frappe.get_meta(doctype).fields if f.fieldname not in no_value_fields
]
@frappe.whitelist()
def get_workflow_state_count(doctype, workflow_state_field, states):
frappe.has_permission(doctype=doctype, ptype='read', throw=True)
frappe.has_permission(doctype=doctype, ptype="read", throw=True)
states = frappe.parse_json(states)
result = frappe.get_all(
doctype,

View file

@ -1,6 +1,7 @@
{
"actions": [],
"autoname": "field:workflow_action_name",
"allow_rename": 1,
"creation": "2012-12-28 10:49:56",
"description": "Workflow Action Master",
"doctype": "DocType",
@ -21,7 +22,7 @@
"icon": "fa fa-flag",
"idx": 1,
"links": [],
"modified": "2022-08-03 12:20:52.449982",
"modified": "2023-04-14 12:20:52.449982",
"modified_by": "Administrator",
"module": "Workflow",
"name": "Workflow Action Master",
@ -43,4 +44,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -12,6 +12,7 @@ dependencies = [
"Babel~=2.12.1",
"Click~=8.1.3",
"filelock~=3.8.0",
"filetype~=1.2.0",
"GitPython~=3.1.30",
"Jinja2~=3.1.2",
"Pillow~=9.3.0",