Merge branch 'develop' into fix-note-2
This commit is contained in:
commit
3028918f98
65 changed files with 495 additions and 261 deletions
|
|
@ -183,7 +183,7 @@ function get_all_files_to_build(apps) {
|
|||
for (let app of apps) {
|
||||
let public_path = get_public_path(app);
|
||||
include_patterns.push(
|
||||
path.resolve(public_path, "**", "*.bundle.{js,ts,css,sass,scss,less,styl}")
|
||||
path.resolve(public_path, "**", "*.bundle.{js,ts,css,sass,scss,less,styl,jsx}")
|
||||
);
|
||||
ignore_patterns.push(
|
||||
path.resolve(public_path, "node_modules"),
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ from frappe.contacts.doctype.contact.contact import (
|
|||
get_contacts_linked_from,
|
||||
get_contacts_linking_to,
|
||||
)
|
||||
from frappe.core.doctype.communication.email import make
|
||||
from frappe.desk.form import assign_to
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
|
|
@ -365,7 +363,7 @@ class AutoRepeat(Document):
|
|||
error_string += _(
|
||||
"{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings"
|
||||
).format(frappe.bold(_("Note")), frappe.bold(_("Allow Print for Draft")))
|
||||
attachments = "[]"
|
||||
attachments = None
|
||||
|
||||
if error_string:
|
||||
message = error_string
|
||||
|
|
@ -376,14 +374,14 @@ class AutoRepeat(Document):
|
|||
|
||||
recipients = self.recipients.split("\n")
|
||||
|
||||
make(
|
||||
doctype=new_doc.doctype,
|
||||
name=new_doc.name,
|
||||
frappe.sendmail(
|
||||
reference_doctype=new_doc.doctype,
|
||||
reference_name=new_doc.name,
|
||||
recipients=recipients,
|
||||
subject=subject,
|
||||
content=message,
|
||||
attachments=attachments,
|
||||
send_email=1,
|
||||
expose_recipients="header",
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ class TestAutoRepeat(FrappeTestCase):
|
|||
docnames = frappe.get_all(doc.reference_doctype, {"auto_repeat": doc.name})
|
||||
self.assertEqual(len(docnames), months)
|
||||
|
||||
def test_notification_is_attached(self):
|
||||
def test_email_notification(self):
|
||||
todo = frappe.get_doc(
|
||||
dict(
|
||||
doctype="ToDo",
|
||||
|
|
@ -187,10 +187,10 @@ class TestAutoRepeat(FrappeTestCase):
|
|||
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name"
|
||||
)
|
||||
|
||||
linked_comm = frappe.db.exists(
|
||||
"Communication", dict(reference_doctype="ToDo", reference_name=new_todo)
|
||||
email_queue = frappe.db.exists(
|
||||
"Email Queue", dict(reference_doctype="ToDo", reference_name=new_todo)
|
||||
)
|
||||
self.assertTrue(linked_comm)
|
||||
self.assertTrue(email_queue)
|
||||
|
||||
def test_next_schedule_date(self):
|
||||
current_date = getdate(today())
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@
|
|||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.templates.includes.comments.comments import add_comment
|
||||
from frappe.tests.test_model_utils import set_user
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
|
||||
|
||||
|
||||
class TestComment(FrappeTestCase):
|
||||
|
|
@ -39,14 +42,10 @@ class TestComment(FrappeTestCase):
|
|||
|
||||
# test via blog
|
||||
def test_public_comment(self):
|
||||
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
|
||||
|
||||
test_blog = make_test_blog()
|
||||
|
||||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
|
||||
|
||||
from frappe.templates.includes.comments.comments import add_comment
|
||||
|
||||
frappe.form_dict.comment = "Good comment with 10 chars"
|
||||
frappe.form_dict.comment_email = "test@test.com"
|
||||
frappe.form_dict.comment_by = "Good Tester"
|
||||
|
|
@ -102,3 +101,32 @@ class TestComment(FrappeTestCase):
|
|||
)
|
||||
|
||||
test_blog.delete()
|
||||
|
||||
@change_settings("Blog Settings", {"allow_guest_to_comment": 0})
|
||||
def test_guest_cannot_comment(self):
|
||||
test_blog = make_test_blog()
|
||||
with set_user("Guest"):
|
||||
frappe.form_dict.comment = "Good comment with 10 chars"
|
||||
frappe.form_dict.comment_email = "mail@example.org"
|
||||
frappe.form_dict.comment_by = "Good Tester"
|
||||
frappe.form_dict.reference_doctype = "Blog Post"
|
||||
frappe.form_dict.reference_name = test_blog.name
|
||||
frappe.form_dict.route = test_blog.route
|
||||
frappe.local.request_ip = "127.0.0.1"
|
||||
|
||||
self.assertEqual(add_comment(), None)
|
||||
|
||||
def test_user_not_logged_in(self):
|
||||
some_system_user = frappe.db.get_value("User", {})
|
||||
|
||||
test_blog = make_test_blog()
|
||||
with set_user("Guest"):
|
||||
frappe.form_dict.comment = "Good comment with 10 chars"
|
||||
frappe.form_dict.comment_email = some_system_user
|
||||
frappe.form_dict.comment_by = "Good Tester"
|
||||
frappe.form_dict.reference_doctype = "Blog Post"
|
||||
frappe.form_dict.reference_name = test_blog.name
|
||||
frappe.form_dict.route = test_blog.route
|
||||
frappe.local.request_ip = "127.0.0.1"
|
||||
|
||||
self.assertRaises(frappe.ValidationError, add_comment)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -205,9 +205,11 @@ class Exporter:
|
|||
for df in self.fields:
|
||||
is_parent = not df.is_child_table_field
|
||||
if is_parent:
|
||||
label = _(df.label)
|
||||
label = _(df.label or df.fieldname)
|
||||
else:
|
||||
label = f"{_(df.label)} ({_(df.child_table_df.label)})"
|
||||
label = (
|
||||
f"{_(df.label or df.fieldname)} ({_(df.child_table_df.label or df.child_table_df.fieldname)})"
|
||||
)
|
||||
|
||||
if label in header:
|
||||
# this label is already in the header,
|
||||
|
|
|
|||
|
|
@ -345,6 +345,7 @@ class DocType(Document):
|
|||
"name",
|
||||
"parent",
|
||||
"creation",
|
||||
"owner",
|
||||
"modified",
|
||||
"modified_by",
|
||||
"parentfield",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -25,9 +25,12 @@ def get_user_default(key, user=None):
|
|||
if d and isinstance(d, (list, tuple)) and len(d) == 1:
|
||||
# Use User Permission value when only when it has a single value
|
||||
d = d[0]
|
||||
|
||||
else:
|
||||
d = user_defaults.get(frappe.scrub(key), None)
|
||||
user_permission_default = get_user_permission_default(key, user_defaults)
|
||||
if not d:
|
||||
# If no default value is found, use the User Permission value
|
||||
d = user_permission_default
|
||||
|
||||
value = isinstance(d, (list, tuple)) and d[0] or d
|
||||
if not_in_user_permission(key, value, user):
|
||||
|
|
@ -36,6 +39,24 @@ def get_user_default(key, user=None):
|
|||
return value
|
||||
|
||||
|
||||
def get_user_permission_default(key, defaults):
|
||||
permissions = get_user_permissions()
|
||||
user_default = ""
|
||||
if permissions.get(key):
|
||||
# global default in user permission
|
||||
for item in permissions.get(key):
|
||||
doc = item.get("doc")
|
||||
if defaults.get(key) == doc:
|
||||
user_default = doc
|
||||
|
||||
for item in permissions.get(key):
|
||||
if item.get("is_default"):
|
||||
user_default = item.get("doc")
|
||||
break
|
||||
|
||||
return user_default
|
||||
|
||||
|
||||
def get_user_default_as_list(key, user=None):
|
||||
user_defaults = get_defaults(user or frappe.session.user)
|
||||
d = user_defaults.get(key, None)
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@
|
|||
],
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-15 01:16:56.035205",
|
||||
"modified": "2023-04-11 14:34:24.829366",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Workspace",
|
||||
|
|
@ -220,5 +220,6 @@
|
|||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -443,7 +452,7 @@ def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None):
|
|||
def get_data_for_custom_field(doctype, field):
|
||||
|
||||
if not frappe.has_permission(doctype, "read"):
|
||||
frappe.throw(_("Not Permitted"), frappe.PermissionError)
|
||||
frappe.throw(_("Not Permitted to read {0}").format(doctype), frappe.PermissionError)
|
||||
|
||||
value_map = frappe._dict(frappe.get_all(doctype, fields=["name", field], as_list=1))
|
||||
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
">",
|
||||
"<",
|
||||
">=",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -184,11 +184,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",
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "Active\nRevoked",
|
||||
|
|
@ -74,10 +75,11 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-04-26 06:40:34.922441",
|
||||
"modified": "2023-04-07 07:08:00.249740",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "OAuth Bearer Token",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
@ -92,5 +94,6 @@
|
|||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "URIs for receiving authorization code once the user allows access, as well as failure responses. Typically a REST endpoint exposed by the Client App.\n<br>e.g. http://hostname//api/method/frappe.www.login.login_via_facebook",
|
||||
"description": "URIs for receiving authorization code once the user allows access, as well as failure responses. Typically a REST endpoint exposed by the Client App.\n<br>e.g. http://hostname/api/method/frappe.www.login.login_via_facebook",
|
||||
"fieldname": "redirect_uris",
|
||||
"fieldtype": "Text",
|
||||
"label": "Redirect URIs"
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2022-08-03 12:21:52.062755",
|
||||
"modified": "2023-04-07 07:06:35.765981",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "OAuth Client",
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -304,7 +304,9 @@ class BaseDocument:
|
|||
self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False
|
||||
) -> dict:
|
||||
d = _dict()
|
||||
permitted_fields = get_permitted_fields(doctype=self.doctype)
|
||||
permitted_fields = get_permitted_fields(
|
||||
doctype=self.doctype, parenttype=getattr(self, "parenttype", None)
|
||||
)
|
||||
|
||||
for fieldname in self.meta.get_valid_columns():
|
||||
field_value = getattr(self, fieldname, None)
|
||||
|
|
|
|||
|
|
@ -114,10 +114,10 @@ def sync_customizations(app=None):
|
|||
with open(os.path.join(folder, fname)) as f:
|
||||
data = json.loads(f.read())
|
||||
if data.get("sync_on_migrate"):
|
||||
sync_customizations_for_doctype(data, folder)
|
||||
sync_customizations_for_doctype(data, folder, fname)
|
||||
|
||||
|
||||
def sync_customizations_for_doctype(data: dict, folder: str):
|
||||
def sync_customizations_for_doctype(data: dict, folder: str, filename: str = ""):
|
||||
"""Sync doctype customzations for a particular data set"""
|
||||
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
|
||||
|
||||
|
|
@ -158,6 +158,11 @@ def sync_customizations_for_doctype(data: dict, folder: str):
|
|||
if doc_type == doctype or not os.path.exists(os.path.join(folder, scrub(doc_type) + ".json")):
|
||||
sync_single_doctype(doc_type)
|
||||
|
||||
if not frappe.db.exists("DocType", doctype):
|
||||
print(_("DocType {0} does not exist.").format(doctype))
|
||||
print(_("Skipping fixture syncing for doctyoe {0} from file {1} ").format(doctype, filename))
|
||||
return
|
||||
|
||||
if data["custom_fields"]:
|
||||
sync("custom_fields", "Custom Field", "dt")
|
||||
update_schema = True
|
||||
|
|
@ -165,10 +170,10 @@ def sync_customizations_for_doctype(data: dict, folder: str):
|
|||
if data["property_setters"]:
|
||||
sync("property_setters", "Property Setter", "doc_type")
|
||||
|
||||
print(f"Updating customizations for {doctype}")
|
||||
if data.get("custom_perms"):
|
||||
sync("custom_perms", "Custom DocPerm", "parent")
|
||||
|
||||
print(f"Updating customizations for {doctype}")
|
||||
validate_fields_for_doctype(doctype)
|
||||
|
||||
if update_schema and not frappe.db.get_value("DocType", doctype, "issingle"):
|
||||
|
|
|
|||
|
|
@ -89,8 +89,11 @@ class Monitor:
|
|||
self.data.duration = int(timediff.total_seconds() * 1000000)
|
||||
|
||||
if self.data.transaction_type == "request":
|
||||
self.data.request.status_code = response.status_code
|
||||
self.data.request.response_length = int(response.headers.get("Content-Length", 0))
|
||||
if response:
|
||||
self.data.request.status_code = response.status_code
|
||||
self.data.request.response_length = int(response.headers.get("Content-Length", 0))
|
||||
else:
|
||||
self.data.request.status_code = 500
|
||||
|
||||
if hasattr(frappe.local, "rate_limiter"):
|
||||
limiter = frappe.local.rate_limiter
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -168,7 +168,6 @@ execute:frappe.db.set_default('desktop:home_page', 'space')
|
|||
execute:frappe.delete_doc_if_exists('Page', 'workspace')
|
||||
execute:frappe.delete_doc_if_exists('Page', 'dashboard', force=1)
|
||||
frappe.core.doctype.page.patches.drop_unused_pages
|
||||
execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
|
||||
frappe.patches.v13_0.remove_chat
|
||||
frappe.patches.v13_0.rename_desk_page_to_workspace # 02.02.2021
|
||||
frappe.patches.v13_0.delete_package_publish_tool
|
||||
|
|
@ -199,6 +198,7 @@ frappe.patches.v15_0.remove_event_streaming
|
|||
frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report
|
||||
|
||||
[post_model_sync]
|
||||
execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
|
||||
frappe.core.doctype.role.patches.v13_set_default_desk_properties
|
||||
frappe.patches.v14_0.drop_data_import_legacy
|
||||
frappe.patches.v14_0.copy_mail_data #08.03.21
|
||||
|
|
@ -223,3 +223,4 @@ frappe.patches.v14_0.disable_email_accounts_with_oauth
|
|||
execute:frappe.delete_doc("Page", "translation-tool", force=1)
|
||||
frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings
|
||||
frappe.patches.v14_0.remove_manage_subscriptions_from_navbar
|
||||
frappe.patches.v15_0.remove_background_jobs_from_dropdown
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
item = frappe.db.exists("Navbar Item", {"item_label": "Background Jobs"})
|
||||
if not item:
|
||||
return
|
||||
|
||||
frappe.delete_doc("Navbar Item", item)
|
||||
|
|
@ -3,10 +3,14 @@
|
|||
|
||||
frappe.defaults = {
|
||||
get_user_default: function (key) {
|
||||
var defaults = frappe.boot.user.defaults;
|
||||
var d = defaults[key];
|
||||
if (!d && frappe.defaults.is_a_user_permission_key(key))
|
||||
let defaults = frappe.boot.user.defaults;
|
||||
let d = defaults[key];
|
||||
if (!d && frappe.defaults.is_a_user_permission_key(key)) {
|
||||
d = defaults[frappe.model.scrub(key)];
|
||||
// Check for default user permission values
|
||||
user_default = this.get_user_permission_default(key, defaults);
|
||||
if (user_default) d = user_default;
|
||||
}
|
||||
if ($.isArray(d)) d = d[0];
|
||||
|
||||
if (!frappe.defaults.in_user_permission(key, d)) {
|
||||
|
|
@ -15,6 +19,27 @@ frappe.defaults = {
|
|||
|
||||
return d;
|
||||
},
|
||||
|
||||
get_user_permission_default: function (key, defaults) {
|
||||
let permissions = this.get_user_permissions();
|
||||
let user_default = null;
|
||||
if (permissions[key]) {
|
||||
permissions[key].forEach((item) => {
|
||||
if (defaults[key] == item.doc) {
|
||||
user_default = item.doc;
|
||||
}
|
||||
});
|
||||
|
||||
permissions[key].forEach((item) => {
|
||||
if (item.is_default) {
|
||||
user_default = item.doc;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return user_default;
|
||||
},
|
||||
|
||||
get_user_defaults: function (key) {
|
||||
var defaults = frappe.boot.user.defaults;
|
||||
var d = defaults[key];
|
||||
|
|
|
|||
|
|
@ -1188,6 +1188,13 @@ export default class Grid {
|
|||
// update the parent too (for new rows)
|
||||
this.docfields.find((d) => d.fieldname === fieldname)[property] = value;
|
||||
|
||||
if (this.user_defined_columns && this.user_defined_columns.length > 0) {
|
||||
let field = this.user_defined_columns.find((d) => d.fieldname === fieldname);
|
||||
if (field && Object.keys(field).includes(property)) {
|
||||
field[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
this.debounced_refresh();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ frappe.views.BaseList = class BaseList {
|
|||
|
||||
this.fields = [];
|
||||
this.filters = [];
|
||||
this.sort_by = "modified";
|
||||
this.sort_order = "desc";
|
||||
this.sort_by = this.meta.sort_field || "modified";
|
||||
this.sort_order = this.meta.sort_order || "desc";
|
||||
|
||||
// Setup buttons
|
||||
this.primary_action = null;
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
|
||||
this.view = "List";
|
||||
// initialize with saved order by
|
||||
this.sort_by = this.view_user_settings.sort_by || "modified";
|
||||
this.sort_order = this.view_user_settings.sort_order || "desc";
|
||||
this.sort_by = this.view_user_settings.sort_by || this.sort_by || "modified";
|
||||
this.sort_order = this.view_user_settings.sort_order || this.sort_order || "desc";
|
||||
|
||||
// build menu items
|
||||
this.menu_items = this.menu_items.concat(this.get_menu_items());
|
||||
|
|
|
|||
|
|
@ -34,9 +34,7 @@ $.extend(frappe.perm, {
|
|||
|
||||
doctype_perm: {},
|
||||
|
||||
has_perm: (doctype, permlevel, ptype, doc) => {
|
||||
if (!permlevel) permlevel = 0;
|
||||
|
||||
has_perm: (doctype, permlevel = 0, ptype = "read", doc) => {
|
||||
const perms = frappe.perm.get_perm(doctype, doc);
|
||||
return !!perms?.[permlevel]?.[ptype];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ frappe.ui.toolbar.Toolbar = class {
|
|||
})
|
||||
);
|
||||
$(".dropdown-toggle").dropdown();
|
||||
$("#toolbar-user a[href]").click(function () {
|
||||
$(this).closest(".dropdown-menu").prev().dropdown("toggle");
|
||||
});
|
||||
|
||||
this.setup_awesomebar();
|
||||
this.setup_notifications();
|
||||
|
|
@ -133,6 +136,12 @@ frappe.ui.toolbar.Toolbar = class {
|
|||
frappe.utils.generate_tracking_url,
|
||||
__("Generate Tracking URL")
|
||||
);
|
||||
|
||||
if (frappe.perm.has_perm("RQ Job")) {
|
||||
frappe.search.utils.make_function_searchable(function () {
|
||||
frappe.set_route("List", "RQ Job");
|
||||
}, __("Background Jobs"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
this.filters = this.report_doc.json.filters;
|
||||
this.order_by = this.report_doc.json.order_by;
|
||||
this.add_totals_row = this.report_doc.json.add_totals_row;
|
||||
this.page_title = this.report_name;
|
||||
this.page_title = __(this.report_name);
|
||||
this.page_length = this.report_doc.json.page_length || 20;
|
||||
this.order_by = this.report_doc.json.order_by || "modified desc";
|
||||
this.chart_args = this.report_doc.json.chart_args;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -362,6 +362,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
#freeze.grid-form {
|
||||
z-index: 1020;
|
||||
}
|
||||
|
||||
.recorder-form-in-grid {
|
||||
z-index: 0;
|
||||
@include base-grid();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: 1020;
|
||||
z-index: 1055;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
background-color: var(--bg-color);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ body[data-route^="Module"] .main-menu {
|
|||
right: 0;
|
||||
opacity: 0.3;
|
||||
background: #000;
|
||||
z-index: 1041;
|
||||
z-index: 9998;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,17 @@ EMAIL_PATTERN = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
|
|||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(key="reference_name", limit=get_comment_limit, seconds=60 * 60)
|
||||
def add_comment(comment, comment_email, comment_by, reference_doctype, reference_name, route):
|
||||
doc = frappe.get_doc(reference_doctype, reference_name)
|
||||
if frappe.session.user == "Guest":
|
||||
if reference_doctype not in ("Blog Post", "Web Page"):
|
||||
return
|
||||
|
||||
if frappe.session.user == "Guest" and doc.doctype not in ["Blog Post", "Web Page"]:
|
||||
return
|
||||
if reference_doctype == "Blog Post" and not frappe.db.get_single_value(
|
||||
"Blog Settings", "allow_guest_to_comment"
|
||||
):
|
||||
return
|
||||
|
||||
if frappe.db.exists("User", comment_email):
|
||||
frappe.throw(_("Please login to post a comment."))
|
||||
|
||||
if not comment.strip():
|
||||
frappe.msgprint(_("The comment cannot be empty"))
|
||||
|
|
@ -31,6 +38,7 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference
|
|||
frappe.msgprint(_("Comments cannot have links or email addresses"))
|
||||
return False
|
||||
|
||||
doc = frappe.get_doc(reference_doctype, reference_name)
|
||||
comment = doc.add_comment(
|
||||
text=clean_html(comment), comment_email=comment_email, comment_by=comment_by
|
||||
)
|
||||
|
|
@ -50,9 +58,7 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference
|
|||
url, _("View Comment")
|
||||
)
|
||||
|
||||
if doc.doctype == "Blog Post" and not doc.enable_email_notification:
|
||||
pass
|
||||
else:
|
||||
if doc.doctype != "Blog Post" or doc.enable_email_notification:
|
||||
# notify creator
|
||||
creator_email = frappe.db.get_value("User", doc.owner, "email") or doc.owner
|
||||
subject = _("New Comment on {0}: {1}").format(doc.doctype, doc.get_title())
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class TestRedisCache(FrappeAPITestCase):
|
|||
self.assertEqual(calculate_area(10), 314)
|
||||
self.assertEqual(function_call_count, 1)
|
||||
|
||||
time.sleep(CACHE_TTL)
|
||||
time.sleep(CACHE_TTL * 1.5)
|
||||
self.assertEqual(calculate_area(10), 314)
|
||||
self.assertEqual(function_call_count, 2)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.defaults import *
|
||||
from frappe.query_builder.utils import db_type_is
|
||||
from frappe.tests.test_query_builder import run_only_if
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
|
|
@ -71,3 +74,39 @@ class TestDefaults(FrappeTestCase):
|
|||
|
||||
frappe.delete_doc("User Permission", perm_doc.name)
|
||||
frappe.set_user(old_user)
|
||||
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
def test_user_permission_defaults(self):
|
||||
# Create user permission
|
||||
create_user("user_default_test@example.com", "Blogger")
|
||||
frappe.set_user("user_default_test@example.com")
|
||||
set_global_default("Country", "")
|
||||
clear_user_default("Country")
|
||||
|
||||
perm_doc = frappe.get_doc(
|
||||
dict(
|
||||
doctype="User Permission",
|
||||
user=frappe.session.user,
|
||||
allow="Country",
|
||||
for_value="India",
|
||||
)
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value("User Permission", perm_doc.name, "is_default", 1)
|
||||
set_global_default("Country", "United States")
|
||||
self.assertEqual(get_user_default("Country"), "India")
|
||||
|
||||
frappe.db.set_value("User Permission", perm_doc.name, "is_default", 0)
|
||||
clear_user_default("Country")
|
||||
self.assertEqual(get_user_default("Country"), None)
|
||||
|
||||
perm_doc = frappe.get_doc(
|
||||
dict(
|
||||
doctype="User Permission",
|
||||
user=frappe.session.user,
|
||||
allow="Country",
|
||||
for_value="United States",
|
||||
)
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
self.assertEqual(get_user_default("Country"), "United States")
|
||||
|
|
|
|||
|
|
@ -33,6 +33,20 @@ class TestMonitor(FrappeTestCase):
|
|||
self.assertEqual(log.transaction_type, "request")
|
||||
self.assertEqual(log.request["method"], "GET")
|
||||
|
||||
def test_no_response(self):
|
||||
set_request(method="GET", path="/api/method/frappe.ping")
|
||||
|
||||
frappe.monitor.start()
|
||||
frappe.monitor.stop(response=None)
|
||||
|
||||
logs = frappe.cache().lrange(MONITOR_REDIS_KEY, 0, -1)
|
||||
self.assertEqual(len(logs), 1)
|
||||
|
||||
log = frappe.parse_json(logs[0].decode())
|
||||
self.assertEqual(log.request["status_code"], 500)
|
||||
self.assertEqual(log.transaction_type, "request")
|
||||
self.assertEqual(log.request["method"], "GET")
|
||||
|
||||
def test_job(self):
|
||||
frappe.utils.background_jobs.execute_job(
|
||||
frappe.local.site, "frappe.ping", None, None, {}, is_async=False
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ Align Labels to the Right,Etiketten rechts ausrichten,
|
|||
Align Value,Wert anordnen,
|
||||
All Images attached to Website Slideshow should be public,"Alle Bilder, die an die Website-Slideshow angehängt werden, sollten öffentlich sein.",
|
||||
All customizations will be removed. Please confirm.,Alle Anpassungen werden entfernt. Bitte bestätigen.,
|
||||
"All possible Workflow States and roles of the workflow. Docstatus Options: 0 is""Saved"", 1 is ""Submitted"" and 2 is ""Cancelled""","Alle möglichen Zustände und Rollen des Workflows. Dokumentenstatus-Optionen sind: 0 ist ""Gespeichert"", 1 ist ""Übertragen"" und 2 ist ""Abgebrochen""",
|
||||
"All possible Workflow States and roles of the workflow. Docstatus Options: 0 is""Saved"", 1 is ""Submitted"" and 2 is ""Cancelled""","Alle möglichen Stati und Rollen des Workflows. Dokumentenstatus-Optionen sind: 0 ist ""Gespeichert"", 1 ist ""Übertragen"" und 2 ist ""Abgebrochen""",
|
||||
All-uppercase is almost as easy to guess as all-lowercase.,Ausschließlich Großbuchstaben sind fast so einfach zu erraten wie ausschließlich Kleinbuchstaben.,
|
||||
Allocated To,Zugewiesen zu,
|
||||
Allow,Zulassen,
|
||||
|
|
@ -851,7 +851,7 @@ Default Value,Standardwert,
|
|||
DefaultValue,Standardwert,
|
||||
Define workflows for forms.,Workflows für Formulare definieren,
|
||||
Defines actions on states and the next step and allowed roles.,"Definiert Maßnahmen bei bestimmten Zuständen, den nächsten Schritt und erlaubte Rollen.",
|
||||
Defines workflow states and rules for a document.,Definiert Workflow-Zustände und Regeln für ein Dokument.,
|
||||
Defines workflow states and rules for a document.,Definiert Workflow-Stati und Regeln für ein Dokument.,
|
||||
Delayed,Verzögert,
|
||||
Delete Data,Daten löschen,
|
||||
Delete comment?,Kommentar löschen?,
|
||||
|
|
@ -1080,7 +1080,7 @@ Fetch attached images from document,Holen Sie angehängte Bilder aus dem Dokumen
|
|||
Field Description,Feldbeschreibung,
|
||||
Field Maps,Feldkarten,
|
||||
Field Type,Feldtyp,
|
||||
"Field that represents the Workflow State of the transaction (if field is not present, a new hidden Custom Field will be created)","Feld, das den Status des Workflows der einzelnen Transaktionen wiedergibt (wenn das Feld nicht vorhanden ist, wird ein neues verstecktes, benutzerdefiniertes Feld erstellt)",
|
||||
"Field that represents the Workflow State of the transaction (if field is not present, a new hidden Custom Field will be created)","Feldc für den Workflow-Status der einzelnen Transaktionen (wenn das Feld nicht vorhanden ist, wird ein neues verstecktes, benutzerdefiniertes Feld erstellt)",
|
||||
Field to Track,Zu verfolgendes Feld,
|
||||
Field type cannot be changed for {0},Feldtyp kann nicht für {0} geändert werden,
|
||||
Field {0} not found.,Feld {0} nicht gefunden,
|
||||
|
|
@ -2777,11 +2777,11 @@ Workflow Action Master,Stammdaten zu Workflow-Aktionen,
|
|||
Workflow Action Name,Workflow-Aktionsname,
|
||||
Workflow Document State,Workflow-Dokumentenstatus,
|
||||
Workflow Name,Workflow-Name,
|
||||
Workflow State,Workflow-Zustand,
|
||||
Workflow State Field,Workflow-Zustandsfeld,
|
||||
Workflow State,Workflow-Status,
|
||||
Workflow State Field,Workflow-Status-Feld,
|
||||
Workflow State not set,Workflow-Status nicht festgelegt,
|
||||
Workflow Transition,Workflow-Übergang,
|
||||
Workflow state represents the current state of a document.,Workflow-Zustand stellt den aktuellen Status eines Dokuments dar.,
|
||||
Workflow state represents the current state of a document.,Der Workflow-Status steht für den aktuellen Zustand eines Dokuments.,
|
||||
Write,Schreiben,
|
||||
Wrong fieldname <b>{0}</b> in add_fetch configuration of custom script,Falscher Feldname <b>{0}</b> in der add_fetch-Konfiguration des benutzerdefinierten Skripts,
|
||||
X Axis Field,X-Achsenfeld,
|
||||
|
|
@ -3070,7 +3070,7 @@ zoom-out,verkleinern,
|
|||
{0} is an invalid email address in 'Recipients',"{0} ist eine ungültige E-Mail-Adresse in ""Empfänger""",
|
||||
{0} is not a raw printing format.,{0} ist kein unformatiertes Druckformat.,
|
||||
{0} is not a valid Email Address,{0} ist keine gültige E-Mail-Adresse,
|
||||
{0} is not a valid Workflow State. Please update your Workflow and try again.,{0} ist kein gültiger Workflow-Zustand. Bitte aktualisieren Sie Ihren Workflow und versuchen Sie es erneut.,
|
||||
{0} is not a valid Workflow State. Please update your Workflow and try again.,{0} ist kein gültiger Workflow-Status. Bitte aktualisieren Sie Ihren Workflow und versuchen Sie es erneut.,
|
||||
{0} is now default print format for {1} doctype,{0} ist jetzt das Standard-Druckformat für den DocType {1},
|
||||
{0} is saved,{0} ist gespeichert,
|
||||
{0} items selected,{0} Elemente ausgewählt,
|
||||
|
|
@ -3131,7 +3131,7 @@ Force User to Reset Password,Benutzer zum Zurücksetzen des Kennworts zwingen,
|
|||
In Days,In Tagen,
|
||||
Last Password Reset Date,Datum der letzten Kennwortrücksetzung,
|
||||
The password of your account has expired.,Das Passwort Ihres Kontos ist abgelaufen.,
|
||||
Workflow State transition not allowed from {0} to {1},Workflow-Statusübergang von {0} nach {1} nicht zulässig,
|
||||
Workflow State transition not allowed from {0} to {1},Eine Veränderung des Workflow-Status von {0} nach {1} ist nicht zulässig,
|
||||
{0} must be after {1},{0} muss nach {1} liegen,
|
||||
{0}: Field '{1}' cannot be set as Unique as it has non-unique values,"{0}: Feld '{1}' kann nicht als eindeutig festgelegt werden, da es nicht eindeutige Werte enthält",
|
||||
{0}: Field {1} in row {2} cannot be hidden and mandatory without default,{0}: Das Feld {1} in Zeile {2} kann ohne Vorgabe nicht ausgeblendet und obligatorisch sein,
|
||||
|
|
@ -4645,8 +4645,8 @@ Hide Traceback,Traceback ausblenden,
|
|||
Value from this field will be set as the due date in the ToDo,Der Wert aus diesem Feld wird im Fälligkeitsdatum als Fälligkeitsdatum festgelegt,
|
||||
New module created {0},Neues Modul erstellt {0},
|
||||
"Report has no numeric fields, please change the Report Name",Der Bericht enthält keine numerischen Felder. Bitte ändern Sie den Berichtsnamen,
|
||||
There are documents which have workflow states that do not exist in this Workflow. It is recommended that you add these states to the Workflow and change their states before removing these states.,"Es gibt Dokumente mit Workflow-Status, die in diesem Workflow nicht vorhanden sind. Es wird empfohlen, diese Status zum Workflow hinzuzufügen und ihre Status zu ändern, bevor Sie diese Status entfernen.",
|
||||
Worflow States Don't Exist,Worflow-Zustände existieren nicht,
|
||||
There are documents which have workflow states that do not exist in this Workflow. It is recommended that you add these states to the Workflow and change their states before removing these states.,"Es gibt Dokumente mit Workflow-Status, die in diesem Workflow nicht vorhanden sind. Es wird empfohlen, diese Stati zum Workflow hinzuzufügen oder den Status der Dokumente ändern, bevor Sie einen Status entfernen.",
|
||||
Worflow States Don't Exist,Worflow-Stati existieren nicht,
|
||||
Save Anyway,Auf jeden Fall speichern,
|
||||
Energy Points:,Energiepunkte:,
|
||||
Review Points:,Bewertungspunkte:,
|
||||
|
|
|
|||
|
|
|
@ -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 < 12:
|
||||
return _("1 week ago")
|
||||
elif dt_diff_days < 31.0:
|
||||
return _("{0} weeks ago").format(dt_diff_days // 7)
|
||||
elif dt_diff_days < 46:
|
||||
return _("1 month ago")
|
||||
elif dt_diff_days < 365.0:
|
||||
return _("{0} months ago").format(dt_diff_days // 30)
|
||||
elif dt_diff_days < 550.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):
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -124,15 +124,9 @@ class Workflow(Document):
|
|||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_fieldnames_for(doctype):
|
||||
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)
|
||||
states = frappe.parse_json(states)
|
||||
result = frappe.get_all(
|
||||
doctype,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue