Merge branch 'develop' into virtual-doc-for-frappe-recorder

This commit is contained in:
Ankush Menat 2023-08-10 12:57:16 +05:30
commit 5a7348668a
79 changed files with 1082 additions and 408 deletions

View file

@ -63,7 +63,7 @@ Full-stack web application framework that uses Python and MariaDB on the server
### Development
* [Easy install script using Docker images](https://github.com/frappe/bench/tree/develop#easy-install-script)
* [Development installlation on bare metal](https://frappeframework.com/docs/user/en/installation)
* [Development installation on bare metal](https://frappeframework.com/docs/user/en/installation)
## Contributing

View file

@ -24,9 +24,9 @@ context("Discussions", () => {
.should("have.value", "Discussion from tests");
// Enter comment
cy.get(".modal .comment-field")
.type("This is a discussion from the cypress ui tests.")
.should("have.value", "This is a discussion from the cypress ui tests.");
cy.get(".modal .discussions-comment").type(
"This is a discussion from the cypress ui tests."
);
// Submit
cy.get(".modal .submit-discussion").click();
@ -38,21 +38,16 @@ context("Discussions", () => {
"Discussion from tests"
);
cy.get(".discussion-on-page:visible").should("have.class", "show");
cy.get(".discussion-on-page:visible .reply-card .reply-text").should(
cy.get(".discussion-on-page:visible .reply-card .reply-text .ql-editor p").should(
"have.text",
"This is a discussion from the cypress ui tests.\n"
"This is a discussion from the cypress ui tests."
);
};
const reply_through_comment_box = () => {
cy.get(".discussion-form:visible .comment-field")
.type(
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page."
)
.should(
"have.value",
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page."
);
cy.get(".discussion-form:visible .discussions-comment").type(
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page."
);
cy.get(".discussion-form:visible .submit-discussion").click();
cy.wait(3000);
@ -63,28 +58,18 @@ context("Discussions", () => {
.find(".reply-text")
.should(
"have.text",
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.\n"
"This is a discussion from the cypress ui tests. This comment was entered through the commentbox on the page.\n"
);
};
const cancel_and_clear_comment_box = () => {
cy.get(".discussion-form:visible .comment-field")
.type("This is a discussion from the cypress ui tests.")
.should("have.value", "This is a discussion from the cypress ui tests.");
cy.get(".discussion-form:visible .cancel-comment").click();
cy.get(".discussion-form:visible .comment-field").should("have.value", "");
};
const single_thread_discussion = () => {
cy.visit("/test-single-thread");
cy.get(".discussions-sidebar").should("have.length", 0);
cy.get(".reply").should("have.length", 0);
cy.get(".discussion-form:visible .comment-field")
.type("This comment is being made on a single thread discussion.")
.should("have.value", "This comment is being made on a single thread discussion.");
cy.get(".discussion-form:visible .discussions-comment").type(
"This comment is being made on a single thread discussion."
);
cy.get(".discussion-form:visible .submit-discussion").click();
cy.wait(3000);
cy.get(".discussion-on-page")
@ -96,6 +81,5 @@ context("Discussions", () => {
it("reply through modal", reply_through_modal);
it("reply through comment box", reply_through_comment_box);
it("cancel and clear comment box", cancel_and_clear_comment_box);
it("single thread discussion", single_thread_discussion);
});

View file

@ -33,7 +33,6 @@
{
"fieldname": "subject",
"fieldtype": "Small Text",
"in_global_search": 1,
"in_list_view": 1,
"label": "Subject",
"reqd": 1

View file

@ -10,15 +10,6 @@ from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
class TestComment(FrappeTestCase):
def tearDown(self):
frappe.form_dict.comment = None
frappe.form_dict.comment_email = None
frappe.form_dict.comment_by = None
frappe.form_dict.reference_doctype = None
frappe.form_dict.reference_name = None
frappe.form_dict.route = None
frappe.local.request_ip = None
def test_comment_creation(self):
test_doc = frappe.get_doc(dict(doctype="ToDo", description="test"))
test_doc.insert()
@ -45,16 +36,15 @@ class TestComment(FrappeTestCase):
test_blog = make_test_blog()
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
frappe.form_dict.comment = "Good comment with 10 chars"
frappe.form_dict.comment_email = "test@test.com"
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"
add_comment()
add_comment_args = {
"comment": "Good comment with 10 chars",
"comment_email": "test@test.com",
"comment_by": "Good Tester",
"reference_doctype": test_blog.doctype,
"reference_name": test_blog.name,
"route": test_blog.route,
}
add_comment(**add_comment_args)
self.assertEqual(
frappe.get_all(
@ -67,10 +57,10 @@ class TestComment(FrappeTestCase):
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
frappe.form_dict.comment = "pleez vizits my site http://mysite.com"
frappe.form_dict.comment_by = "bad commentor"
add_comment()
add_comment_args.update(
comment="pleez vizits my site http://mysite.com", comment_by="bad commentor"
)
add_comment(**add_comment_args)
self.assertEqual(
len(
@ -86,11 +76,8 @@ class TestComment(FrappeTestCase):
# test for filtering html and css injection elements
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
frappe.form_dict.comment = "<script>alert(1)</script>Comment"
frappe.form_dict.comment_by = "hacker"
add_comment()
add_comment_args.update(comment="<script>alert(1)</script>Comment", comment_by="hacker")
add_comment(**add_comment_args)
self.assertEqual(
frappe.get_all(
"Comment",
@ -106,27 +93,30 @@ class TestComment(FrappeTestCase):
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)
self.assertEqual(
add_comment(
comment="Good comment with 10 chars",
comment_email="mail@example.org",
comment_by="Good Tester",
reference_doctype="Blog Post",
reference_name=test_blog.name,
route=test_blog.route,
),
None,
)
def test_user_not_logged_in(self):
some_system_user = frappe.db.get_value("User", {})
some_system_user = frappe.db.get_value("User", {"name": ("not in", frappe.STANDARD_USERS)})
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)
self.assertRaises(
frappe.ValidationError,
add_comment,
comment="Good comment with 10 chars",
comment_email=some_system_user,
comment_by="Good Tester",
reference_doctype="Blog Post",
reference_name=test_blog.name,
route=test_blog.route,
)

View file

@ -33,7 +33,7 @@ from frappe.model.meta import Meta
from frappe.modules import get_doc_path, make_boilerplate
from frappe.modules.import_file import get_file_path
from frappe.query_builder.functions import Concat
from frappe.utils import cint, flt, random_string
from frappe.utils import cint, flt, get_table_name, random_string
from frappe.website.utils import clear_cache
if TYPE_CHECKING:
@ -198,6 +198,7 @@ class DocType(Document):
self.set("can_change_name_type", validate_autoincrement_autoname(self))
self.validate_document_type()
validate_fields(self)
self.check_indexing_for_dashboard_links()
if not self.istable:
validate_permissions(self)
@ -298,6 +299,23 @@ class DocType(Document):
if d.translatable and not supports_translation(d.fieldtype):
d.translatable = 0
def check_indexing_for_dashboard_links(self):
"""Enable indexing for outgoing links used in dashboard"""
for d in self.fields:
if d.fieldtype == "Link" and not (d.unique or d.search_index):
referred_as_link = frappe.db.exists(
"DocType Link",
{"parent": d.options, "link_doctype": self.name, "link_fieldname": d.fieldname},
)
if not referred_as_link:
continue
frappe.msgprint(
_("{0} should be indexed because it's referred in dashboard connections").format(_(d.label)),
alert=True,
indicator="orange",
)
def check_developer_mode(self):
"""Throw exception if not developer mode or via patch"""
if frappe.flags.in_patch or frappe.flags.in_test:

View file

@ -767,6 +767,7 @@ def new_doctype(
unique: bool = False,
depends_on: str = "",
fields: list[dict] | None = None,
custom: bool = True,
**kwargs,
):
if not name:
@ -777,7 +778,7 @@ def new_doctype(
{
"doctype": "DocType",
"module": "Core",
"custom": 1,
"custom": custom,
"fields": [
{
"label": "Some Field",

View file

@ -20,6 +20,9 @@ frappe.ui.form.on("File", {
if (frm.doc.file_name && frm.doc.file_name.split(".").splice(-1)[0] === "zip") {
frm.add_custom_button(__("Unzip"), () => frm.trigger("unzip"));
}
if (frm.doc.file_url) {
frm.add_web_link(frm.doc.file_url, __("View file"));
}
},
preview_file: function (frm) {

View file

@ -80,6 +80,7 @@
"in_list_view": 1,
"label": "File Size",
"length": 20,
"options": "File Size",
"read_only": 1
},
{
@ -174,7 +175,7 @@
"icon": "fa fa-file",
"idx": 1,
"links": [],
"modified": "2023-05-02 15:42:14.274901",
"modified": "2023-08-02 09:43:51.178011",
"modified_by": "Administrator",
"module": "Core",
"name": "File",

View file

@ -734,6 +734,8 @@ class File(Document):
continue
if _file.is_folder:
continue
if not has_permission(_file, "read"):
continue
zf.writestr(_file.file_name, _file.get_content())
zf.close()
return zip_file.getvalue()

View file

@ -6,6 +6,12 @@ import os
import frappe
from frappe.model.document import Document
LICENSES = (
"GNU Affero General Public License",
"GNU General Public License",
"MIT License",
)
class Package(Document):
# begin: auto-generated types
@ -29,6 +35,7 @@ class Package(Document):
@frappe.whitelist()
def get_license_text(license_type):
with open(os.path.join(os.path.dirname(__file__), "licenses", license_type + ".md")) as textfile:
return textfile.read()
def get_license_text(license_type: str) -> str | None:
if license_type in LICENSES:
with open(os.path.join(os.path.dirname(__file__), "licenses", license_type + ".md")) as textfile:
return textfile.read()

View file

@ -211,9 +211,9 @@ def expire_stalled_report():
def delete_prepared_reports(reports):
reports = frappe.parse_json(reports)
for report in reports:
frappe.delete_doc(
"Prepared Report", report["name"], ignore_permissions=True, delete_permanently=True
)
prepared_report = frappe.get_doc("Prepared Report", report["name"])
if prepared_report.has_permission():
prepared_report.delete(ignore_permissions=True, delete_permanently=True)
def create_json_gz_file(data, dt, dn):

View file

@ -68,7 +68,7 @@ else:
<pre><code>
# generate dynamic conditions and set it in the conditions variable
tenant_id = frappe.db.get_value(...)
conditions = 'tenant_id = {}'.format(tenant_id)
conditions = f'tenant_id = {tenant_id}'
# resulting select query
select name from \`tabPerson\`

View file

@ -367,6 +367,9 @@ class TestUser(FrappeTestCase):
set_request(path="/random")
frappe.local.cookie_manager = CookieManager()
frappe.local.login_manager = LoginManager()
# used by rate limiter when calling reset_password
frappe.local.request_ip = "127.0.0.69"
frappe.db.set_single_value("System Settings", "password_reset_limit", 6)
frappe.set_user("testpassword@example.com")
test_user = frappe.get_doc("User", "testpassword@example.com")

View file

@ -1010,7 +1010,7 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]:
@frappe.whitelist(allow_guest=True)
@rate_limit(limit=get_password_reset_limit, seconds=24 * 60 * 60, methods=["POST"])
@rate_limit(limit=get_password_reset_limit, seconds=24 * 60 * 60)
def reset_password(user: str) -> str:
if user == "Administrator":
return "not allowed"
@ -1042,7 +1042,7 @@ def user_query(doctype, txt, searchfield, start, page_len, filters):
conditions = []
user_type_condition = "and user_type != 'Website User'"
if filters and filters.get("ignore_user_type"):
if filters and filters.get("ignore_user_type") and frappe.session.data.user_type == "System User":
user_type_condition = ""
filters.pop("ignore_user_type")

View file

@ -2,6 +2,11 @@
// For license information, please see license.txt
frappe.ui.form.on("Console Log", {
// refresh: function(frm) {
// }
refresh: function (frm) {
frm.add_custom_button(__("Re-Run in Console"), () => {
localStorage.setItem("system_console_code", frm.doc.script);
localStorage.setItem("system_console_type", frm.doc.type);
frappe.set_route("Form", "System Console");
});
},
});

View file

@ -6,7 +6,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"script"
"script",
"type"
],
"fields": [
{
@ -15,11 +16,18 @@
"in_list_view": 1,
"label": "Script",
"read_only": 1
},
{
"fieldname": "type",
"fieldtype": "Data",
"hidden": 1,
"label": "Type",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-07-05 22:16:02.823955",
"modified": "2023-07-27 22:52:37.239039",
"modified_by": "Administrator",
"module": "Desk",
"name": "Console Log",

View file

@ -15,5 +15,6 @@ class ConsoleLog(Document):
from frappe.types import DF
script: DF.Code | None
type: DF.Data | None
# end: auto-generated types
pass

View file

@ -30,7 +30,7 @@ def get_custom_blocks_for_user(doctype, txt, searchfield, start, page_len, filte
# return logged in users private blocks and all public blocks
customHTMLBlock = DocType("Custom HTML Block")
condition_query = frappe.qb.get_query(customHTMLBlock)
condition_query = frappe.qb.from_(customHTMLBlock)
return (
condition_query.select(customHTMLBlock.name).where(

View file

@ -107,6 +107,8 @@ frappe.ui.form.on("Dashboard Chart", {
// set timeseries based on chart type
if (["Count", "Average", "Sum"].includes(frm.doc.chart_type)) {
frm.set_value("timeseries", 1);
} else if (frm.doc.chart_type == "Custom") {
return;
} else {
frm.set_value("timeseries", 0);
}

View file

@ -10,7 +10,15 @@ frappe.ui.form.on("System Console", {
description: __("Execute Console script"),
ignore_inputs: true,
});
frm.set_value("type", "Python");
if (
localStorage.getItem("system_console_code") &&
localStorage.getItem("system_console_type")
) {
frm.set_value("type", localStorage.getItem("system_console_type"));
frm.set_value("console", localStorage.getItem("system_console_code"));
localStorage.removeItem("system_console_code");
localStorage.removeItem("system_console_type");
}
},
refresh: function (frm) {

View file

@ -40,8 +40,7 @@ class SystemConsole(Document):
frappe.db.commit()
else:
frappe.db.rollback()
frappe.get_doc(dict(doctype="Console Log", script=self.console)).insert()
frappe.get_doc(dict(doctype="Console Log", script=self.console, type=self.type)).insert()
frappe.db.commit()

View file

@ -64,6 +64,13 @@ class Workspace(Document):
except Exception:
frappe.throw(_("Content data shoud be a list"))
def clear_cache(self):
super().clear_cache()
if self.for_user:
frappe.cache.hdel("bootinfo", self.for_user)
else:
frappe.cache.delete_key("bootinfo")
def on_update(self):
if disable_saving_as_public():
return

View file

@ -7,7 +7,6 @@ from urllib.parse import quote
import frappe
import frappe.defaults
import frappe.desk.form.meta
import frappe.share
import frappe.utils
from frappe import _, _dict
from frappe.desk.form.document_follow import is_document_followed
@ -86,6 +85,8 @@ def get_meta_bundle(doctype):
@frappe.whitelist()
def get_docinfo(doc=None, doctype=None, name=None):
from frappe.share import _get_users as get_docshares
if not doc:
doc = frappe.get_doc(doctype, name)
if not doc.has_permission("read"):
@ -113,7 +114,7 @@ def get_docinfo(doc=None, doctype=None, name=None):
"versions": get_versions(doc),
"assignments": get_assignments(doc.doctype, doc.name),
"permissions": get_doc_permissions(doc),
"shared": frappe.share.get_users(doc.doctype, doc.name),
"shared": get_docshares(doc),
"views": get_view_logs(doc.doctype, doc.name),
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
@ -351,18 +352,6 @@ def get_assignments(dt, dn):
)
@frappe.whitelist()
def get_badge_info(doctypes, filters):
filters = json.loads(filters)
doctypes = json.loads(doctypes)
filters["docstatus"] = ["!=", 2]
out = {}
for doctype in doctypes:
out[doctype] = frappe.db.get_value(doctype, filters, "count(*)")
return out
def run_onload(doc):
doc.set("__onload", frappe._dict())
doc.run_method("onload")

View file

@ -2,6 +2,7 @@
# License: MIT. See LICENSE
import frappe
from frappe.model import is_default_field
from frappe.query_builder import Order
from frappe.query_builder.functions import Count
from frappe.query_builder.terms import SubQuery
@ -59,6 +60,9 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d
.run(as_dict=True)
)
if not frappe.get_meta(doctype).has_field(field) and not is_default_field(field):
raise ValueError("Field does not belong to doctype")
return frappe.get_list(
doctype,
filters=current_filters,

View file

@ -243,12 +243,11 @@ def get_filters_for(doctype):
@frappe.whitelist()
@frappe.read_only()
def get_open_count(doctype, name, items=None):
"""Get open count for given transactions and filters
"""Get count for internal and external links for given transactions
:param doctype: Reference DocType
:param name: Reference Name
:param transactions: List of transactions (json/dict)
:param filters: optional filters (json/list)"""
:param items: Optional list of transactions (json/dict)"""
if frappe.flags.in_migrate or frappe.flags.in_install:
return {"count": []}
@ -267,30 +266,26 @@ def get_open_count(doctype, name, items=None):
if not isinstance(items, list):
items = json.loads(items)
out = []
out = {
"external_links_found": [],
"internal_links_found": [],
}
for d in items:
if d in links.get("internal_links", {}):
continue
filters = get_filters_for(d)
fieldname = links.get("non_standard_fieldnames", {}).get(d, links.get("fieldname"))
data = {"name": d}
if filters:
# get the fieldname for the current document
# we only need open documents related to the current document
filters[fieldname] = name
total = len(
frappe.get_all(d, fields="name", filters=filters, limit=100, distinct=True, ignore_ifnull=True)
)
data["open_count"] = total
total = len(
frappe.get_all(
d, fields="name", filters={fieldname: name}, limit=100, distinct=True, ignore_ifnull=True
)
)
data["count"] = total
out.append(data)
internal_link_for_doctype = links.get("internal_links", {}).get(d)
if internal_link_for_doctype:
internal_links_data_for_d = get_internal_links(doc, internal_link_for_doctype, d)
if internal_links_data_for_d["count"]:
out["internal_links_found"].append(internal_links_data_for_d)
else:
try:
external_links_data_for_d = get_external_links(d, name, links)
out["external_links_found"].append(external_links_data_for_d)
except Exception as e:
out["external_links_found"].append({"doctype": d, "open_count": 0, "count": 0})
else:
external_links_data_for_d = get_external_links(d, name, links)
out["external_links_found"].append(external_links_data_for_d)
out = {
"count": out,
@ -304,6 +299,58 @@ def get_open_count(doctype, name, items=None):
return out
def get_internal_links(doc, link, link_doctype):
names = []
data = {"doctype": link_doctype}
if isinstance(link, str):
# get internal links in parent document
value = doc.get(link)
if value and value not in names:
names.append(value)
elif isinstance(link, list):
# get internal links in child documents
table_fieldname, link_fieldname = link
for row in doc.get(table_fieldname):
value = row.get(link_fieldname)
if value and value not in names:
names.append(value)
data["open_count"] = 0
data["count"] = len(names)
data["names"] = names
return data
def get_external_links(doctype, name, links):
filters = get_filters_for(doctype)
fieldname = links.get("non_standard_fieldnames", {}).get(doctype, links.get("fieldname"))
data = {"doctype": doctype}
if filters:
# get the fieldname for the current document
# we only need open documents related to the current document
filters[fieldname] = name
total = len(
frappe.get_all(
doctype, fields="name", filters=filters, limit=100, distinct=True, ignore_ifnull=True
)
)
data["open_count"] = total
else:
data["open_count"] = 0
total = len(
frappe.get_all(
doctype, fields="name", filters={fieldname: name}, limit=100, distinct=True, ignore_ifnull=True
)
)
data["count"] = total
return data
def notify_mentions(ref_doctype, ref_name, content):
if ref_doctype and ref_name and content:
mentions = extract_mentions(content)

View file

@ -82,6 +82,8 @@ def delete_downloadable_backups():
def schedule_files_backup(user_email):
from frappe.utils.background_jobs import enqueue, get_jobs
frappe.only_for("System Manager")
queued_jobs = get_jobs(site=frappe.local.site, queue="long")
method = "frappe.desk.page.backups.backups.backup_files_and_notify_user"

View file

@ -88,11 +88,6 @@ def get_unsubcribed_url(
if unsubscribe_params:
params.update(unsubscribe_params)
query_string = get_signed_params(params)
# for test
frappe.local.flags.signed_query_string = query_string
return get_url(unsubscribe_method + "?" + get_signed_params(params))

View file

@ -252,6 +252,10 @@ class SessionBootFailed(ValidationError):
http_status_code = 500
class PrintFormatError(ValidationError):
pass
class TooManyWritesError(Exception):
pass

View file

@ -225,3 +225,7 @@ def get_permitted_fields(
return meta_fields + permitted_fields + optional_meta_fields
return []
def is_default_field(fieldname: str) -> bool:
return fieldname in default_fields

View file

@ -399,6 +399,7 @@ class Document(BaseDocument):
"attached_to_name": self.name,
"attached_to_doctype": self.doctype,
"folder": "Home/Attachments",
"is_private": attach_item.is_private,
}
)
_file.save()
@ -1038,7 +1039,7 @@ class Document(BaseDocument):
"""Rename the document to `name`. This transforms the current object."""
return self._rename(name=name, merge=merge, force=force, validate_rename=validate_rename)
def delete(self, ignore_permissions=False, force=False):
def delete(self, ignore_permissions=False, force=False, *, delete_permanently=False):
"""Delete document."""
return frappe.delete_doc(
self.doctype,
@ -1046,6 +1047,7 @@ class Document(BaseDocument):
ignore_permissions=ignore_permissions,
flags=self.flags,
force=force,
delete_permanently=delete_permanently,
)
def run_before_save_methods(self):

View file

@ -3,6 +3,7 @@
import datetime
import re
from collections import defaultdict
from collections.abc import Callable
from typing import TYPE_CHECKING, Optional
@ -19,7 +20,8 @@ if TYPE_CHECKING:
# NOTE: This is used to keep track of status of sites
# whether `log_types` have autoincremented naming set for the site or not.
autoincremented_site_status_map = {}
# Structure: {"sitename": {"doctype": 1}}
autoincremented_site_status_map = defaultdict(dict)
NAMING_SERIES_PATTERN = re.compile(r"^[\w\- \/.#{}]+$", re.UNICODE)
BRACED_PARAMS_PATTERN = re.compile(r"(\{[\w | #]+\})")
@ -180,22 +182,16 @@ def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool:
"""Checks if the doctype has autoincrement autoname set"""
if doctype in log_types:
if autoincremented_site_status_map.get(frappe.local.site) is None:
if (
frappe.db.sql(
f"""select data_type FROM information_schema.columns
where column_name = 'name' and table_name = 'tab{doctype}'"""
)[0][0]
== "bigint"
):
autoincremented_site_status_map[frappe.local.site] = 1
return True
else:
autoincremented_site_status_map[frappe.local.site] = 0
elif autoincremented_site_status_map[frappe.local.site]:
return True
site_map = autoincremented_site_status_map[frappe.local.site]
if site_map.get(doctype) is None:
query = f"""select data_type FROM information_schema.columns where column_name = 'name' and table_name = 'tab{doctype}'"""
values = ()
if frappe.db.db_type == "mariadb":
query += " and table_schema = %s"
values = (frappe.db.db_name,)
site_map[doctype] = frappe.db.sql(query, values)[0][0] == "bigint"
return bool(site_map[doctype])
else:
if not meta:
meta = frappe.get_meta(doctype)

View file

@ -65,6 +65,8 @@ def update_document_title(
)
name_updated = updated_name and (updated_name != doc.name)
queue = kwargs.get("queue") or "default"
if name_updated:
if action_enqueued:
current_name = doc.name
@ -86,7 +88,7 @@ def update_document_title(
save_point=True,
)
doc.queue_action("rename", name=transformed_name, merge=merge)
doc.queue_action("rename", name=transformed_name, merge=merge, queue=queue)
else:
doc.rename(updated_name, merge=merge)

View file

@ -24,3 +24,4 @@ import "./bootstrap-4-web.bundle";
import "../../website/js/website.js";
import "./frappe/socketio_client.js";
import "./frappe/form/controls/control.js";

View file

@ -70,7 +70,7 @@
<div class="mt-1">{{ __('Google Drive') }}</div>
</button>
</div>
<div class="text-muted text-medium">
<div class="text-muted text-medium text-center">
{{ upload_notes }}
</div>
</div>

View file

@ -198,14 +198,18 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for
get_quill_options() {
return {
modules: {
toolbar: this.get_toolbar_options(),
toolbar: Object.keys(this.df).includes("get_toolbar_options")
? this.df.get_toolbar_options()
: this.get_toolbar_options(),
table: true,
imageResize: {},
magicUrl: true,
mention: this.get_mention_options(),
},
theme: "snow",
theme: this.df.theme || "snow",
readOnly: this.disabled,
bounds: this.quill_container[0],
placeholder: this.df.placeholder || "",
};
}

View file

@ -369,7 +369,10 @@ frappe.ui.form.Dashboard = class FormDashboard {
let doctype = $link.attr("data-doctype"),
names = $link.attr("data-names") || [];
if (this.data.internal_links[doctype]) {
if (
this.internal_links_found &&
this.internal_links_found.find((d) => d.doctype === doctype)
) {
if (names.length) {
frappe.route_options = { name: ["in", names] };
} else {
@ -437,32 +440,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
me.update_heatmap(r.message.timeline_data);
}
// update badges
$.each(r.message.count, function (i, d) {
me.frm.dashboard.set_badge_count(d.name, cint(d.open_count), cint(d.count));
});
// update from internal links
$.each(me.data.internal_links, (doctype, link) => {
let names = [];
if (typeof link === "string" || link instanceof String) {
// get internal links in parent document
let value = me.frm.doc[link];
if (value && !names.includes(value)) {
names.push(value);
}
} else if (Array.isArray(link)) {
// get internal links in child documents
let [table_fieldname, link_fieldname] = link;
(me.frm.doc[table_fieldname] || []).forEach((d) => {
let value = d[link_fieldname];
if (value && !names.includes(value)) {
names.push(value);
}
});
}
me.frm.dashboard.set_badge_count(doctype, 0, names.length, names);
});
me.update_badges(r.message.count);
me.frm.dashboard_data = r.message;
me._fetched_counts = true;
@ -471,11 +449,52 @@ frappe.ui.form.Dashboard = class FormDashboard {
});
}
set_badge_count(doctype, open_count, count, names) {
update_badges(count) {
let me = this;
this.internal_links_found = count.internal_links_found;
$.each(count.internal_links_found, function (i, d) {
me.frm.dashboard.set_badge_count_for_internal_link(
d.doctype,
cint(d.open_count),
cint(d.count),
d.names
);
});
$.each(count.external_links_found, function (i, d) {
me.frm.dashboard.set_badge_count_for_external_link(
d.doctype,
cint(d.open_count),
cint(d.count)
);
});
}
set_badge_count_for_external_link(doctype, open_count, count) {
let $link = $(this.transactions_area).find(
'.document-link[data-doctype="' + doctype + '"]'
);
this.set_badge_count_common(open_count, count, $link);
}
set_badge_count_for_internal_link(doctype, open_count, count, names) {
let $link = $(this.transactions_area).find(
'.document-link[data-doctype="' + doctype + '"]'
);
this.set_badge_count_common(open_count, count, $link);
if (names && names.length) {
$link.attr("data-names", names ? names.join(",") : "");
} else {
$link.find("a").attr("disabled", true);
}
}
set_badge_count_common(open_count, count, $link) {
if (open_count) {
$link
.find(".open-notification")
@ -489,14 +508,6 @@ frappe.ui.form.Dashboard = class FormDashboard {
.removeClass("hidden")
.text(count > 99 ? "99+" : count);
}
if (this.data.internal_links[doctype]) {
if (names && names.length) {
$link.attr("data-names", names ? names.join(",") : "");
} else {
$link.find("a").attr("disabled", true);
}
}
}
update_heatmap(data) {

View file

@ -41,6 +41,7 @@ frappe.ui.form.Form = class FrappeForm {
this.doctype_layout = frappe.get_doc("DocType Layout", doctype_layout_name);
this.undo_manager = new UndoManager({ frm: this });
this.setup_meta(doctype);
this.debounced_reload_doc = frappe.utils.debounce(this.reload_doc.bind(this), 1000);
this.beforeUnloadListener = (event) => {
event.preventDefault();
@ -543,7 +544,7 @@ frappe.ui.form.Form = class FrappeForm {
this.doc.__last_sync_on &&
new Date() - this.doc.__last_sync_on > this.refresh_if_stale_for * 1000
) {
this.reload_doc();
this.debounced_reload_doc();
return true;
}
}
@ -1132,7 +1133,7 @@ frappe.ui.form.Form = class FrappeForm {
"alert-warning"
);
} else {
this.reload_doc();
this.debounced_reload_doc();
}
}
}

View file

@ -73,6 +73,9 @@ frappe.form.formatters = {
}
},
Int: function (value, docfield, options) {
if (cstr(docfield.options).trim() === "File Size") {
return frappe.form.formatters.FileSize(value);
}
return frappe.form.formatters._right(value == null ? "" : cint(value), options);
},
Percent: function (value, docfield, options) {
@ -339,10 +342,11 @@ frappe.form.formatters = {
return $("<div></div>").text(value).html();
},
FileSize: function (value) {
value = cint(value);
if (value > 1048576) {
value = flt(flt(value) / 1048576, 1) + "M";
return (value / 1048576).toFixed(2) + "M";
} else if (value > 1024) {
value = flt(flt(value) / 1024, 1) + "K";
return (value / 1024).toFixed(2) + "K";
}
return value;
},

View file

@ -130,7 +130,7 @@ frappe.ui.form.Sidebar = class {
callback: function (res) {
me.sidebar
.find(".auto-repeat-status")
.html(__("Repeats {0}", [res.message.frequency]));
.html(__("Repeats {0}", [__(res.message.frequency)]));
me.sidebar.find(".auto-repeat-status").on("click", function () {
frappe.set_route("Form", "Auto Repeat", me.frm.doc.auto_repeat);
});

View file

@ -98,6 +98,10 @@ frappe.ui.form.Toolbar = class Toolbar {
const docname = this.frm.doc.name;
const title_field = this.frm.meta.title_field || "";
const doctype = this.frm.doctype;
let queue;
if (this.frm.__rename_queue) {
queue = this.frm.__rename_queue;
}
if (input_name) {
const warning = __("This cannot be undone");
@ -120,6 +124,7 @@ frappe.ui.form.Toolbar = class Toolbar {
merge,
freeze: true,
freeze_message: __("Updating related fields..."),
queue,
})
.then((new_docname) => {
const reload_form = (input_name) => {

View file

@ -36,8 +36,9 @@ frappe.ui.form.States = class FormStates {
).join(", ") || __("None: End of Workflow").bold();
const document_editable_by = frappe.workflow
.get_document_state(me.frm.doctype, state)
.allow_edit.bold();
.get_document_state_roles(me.frm.doctype, state)
.map((role) => role.bold())
.join(", ");
$(d.body)
.html(

View file

@ -151,7 +151,7 @@ $.extend(frappe.model, {
) {
if (data.modified !== cur_frm.doc.modified && !frappe.ui.form.is_saving) {
if (!cur_frm.is_dirty()) {
cur_frm.reload_doc();
cur_frm.debounced_reload_doc();
} else {
doc.__needs_refresh = true;
cur_frm.show_conflict_message();

View file

@ -94,6 +94,6 @@ frappe.ui.Scanner = class Scanner {
}
load_lib() {
return frappe.require("/assets/frappe/node_modules/html5-qrcode/dist/html5-qrcode.min.js");
return frappe.require("/assets/frappe/node_modules/html5-qrcode/html5-qrcode.min.js");
}
};

View file

@ -52,6 +52,7 @@ frappe.ui.Filter = class {
"Markdown Editor": ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"],
Password: ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"],
Rating: ["like", "not like", "Between", "in", "not in", "Timespan"],
Float: ["like", "not like", "Between", "in", "not in", "Timespan"],
};
}

View file

@ -51,7 +51,7 @@ frappe.search.SearchDialog = class {
no_results_status: () => __("No Results found"),
get_results: (keywords, callback) => {
let start = 0,
limit = 1000;
limit = 100;
let results = frappe.search.utils.get_nav_results(keywords);
frappe.search.utils.get_global_results(keywords, start, limit).then(
(global_results) => {

View file

@ -1345,15 +1345,81 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
get_filters_html_for_print() {
const filters = this.filter_area.get();
return filters
.map((f) => {
const [doctype, fieldname, condition, value] = f;
if (condition !== "=") return "";
const label = frappe.meta.get_label(doctype, fieldname);
const docfield = frappe.meta.get_docfield(doctype, fieldname);
return `<h6>${__(label)}: ${frappe.format(value, docfield)}</h6>`;
})
.join("");
return (
`<h5>${__("Filters:")}</h5>` +
filters
.map((f) => {
const [doctype, fieldname, condition, value] = f;
const docfield = frappe.meta.get_docfield(doctype, fieldname);
const label = `<b>${__(frappe.meta.get_label(doctype, fieldname))}</b>`;
switch (condition) {
case "=":
return __("{0} is equal to {1}", [
label,
frappe.format(value, docfield),
]);
case "!=":
return __("{0} is not equal to {1}", [
__(label),
frappe.format(value, docfield),
]);
case ">":
return __("{0} is greater than {1}", [
__(label),
frappe.format(value, docfield),
]);
case "<":
return __("{0} is less than {1}", [
__(label),
frappe.format(value, docfield),
]);
case ">=":
return __("{0} is greater than or equal to {1}", [
__(label),
frappe.format(value, docfield),
]);
case "<=":
return __("{0} is less than or equal to {1}", [
__(label),
frappe.format(value, docfield),
]);
case "Between":
return __("{0} is between {1} and {2}", [
__(label),
frappe.format(value[0], docfield),
frappe.format(value[1], docfield),
]);
case "Timespan":
return __("{0} is within {1}", [__(label), __(value)]);
case "in":
return __("{0} is one of {1}", [
__(label),
frappe.utils.comma_or(
value.map((v) => frappe.format(v, docfield))
),
]);
case "not in":
return __("{0} is not one of {1}", [
__(label),
frappe.utils.comma_or(
value.map((v) => frappe.format(v, docfield))
),
]);
case "like":
return __("{0} is like {1}", [__(label), value]);
case "not like":
return __("{0} is not like {1}", [__(label), value]);
case "is":
return value === "set"
? __("{0} is set", [__(label)])
: __("{0} is not set", [__(label)]);
default:
return null;
}
})
.filter(Boolean)
.join("<br>")
);
}
get_columns_totals(data) {

View file

@ -103,7 +103,7 @@ export default class ChartWidget extends Widget {
this.action_area.empty();
this.prepare_chart_actions();
if (this.chart_doc.timeseries && this.chart_doc.chart_type !== "Custom") {
if (this.chart_doc.timeseries) {
this.render_time_series_filters();
}
}

View file

@ -107,17 +107,14 @@ def rate_limit(
:returns: a decorator function that limit the number of requests per endpoint
"""
def ratelimit_decorator(fun):
@wraps(fun)
def ratelimit_decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
# Do not apply rate limits if method is not opted to check
if (
methods != "ALL"
and frappe.request
and frappe.request.method
and frappe.request.method.upper() not in methods
if not frappe.request or (
methods != "ALL" and frappe.request.method and frappe.request.method.upper() not in methods
):
return frappe.call(fun, **frappe.form_dict or kwargs)
return fn(*args, **kwargs)
_limit = limit() if callable(limit) else limit
@ -147,7 +144,7 @@ def rate_limit(
_("You hit the rate limit because of too many requests. Please try after sometime.")
)
return frappe.call(fun, **frappe.form_dict or kwargs)
return fn(*args, **kwargs)
return wrapper

View file

@ -45,7 +45,7 @@ def get_current_stack_frames():
current = inspect.currentframe()
frames = inspect.getouterframes(current, context=10)
for frame, filename, lineno, function, context, index in list(reversed(frames))[:-2]:
if "/apps/" in filename:
if "/apps/" in filename or "<serverscript>" in filename:
yield {
"filename": TRACEBACK_PATH_PATTERN.sub("", filename),
"lineno": lineno,

View file

@ -1,6 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
from typing import TYPE_CHECKING
import frappe
from frappe import _
from frappe.desk.doctype.notification_log.notification_log import (
@ -11,6 +13,9 @@ from frappe.desk.doctype.notification_log.notification_log import (
from frappe.desk.form.document_follow import follow_document
from frappe.utils import cint
if TYPE_CHECKING:
from frappe.model.document import Document
@frappe.whitelist()
def add(doctype, name, user=None, read=1, write=0, submit=0, share=0, everyone=0, notify=0):
@ -122,8 +127,18 @@ def set_docshare_permission(doctype, name, user, permission_to, value=1, everyon
@frappe.whitelist()
def get_users(doctype, name):
def get_users(doctype: str, name: str) -> list:
"""Get list of users with which this document is shared"""
doc = frappe.get_doc(doctype, name)
return _get_users(doc)
def _get_users(doc: "Document") -> list:
from frappe.permissions import has_permission
if not has_permission(doc.doctype, "read", doc, raise_exception=False):
return []
return frappe.get_all(
"DocShare",
fields=[
@ -137,7 +152,7 @@ def get_users(doctype, name):
"owner",
"creation",
],
filters=dict(share_doctype=doctype, share_name=name),
filters=dict(share_doctype=doc.doctype, share_name=doc.name),
)

View file

@ -1,6 +1,6 @@
{% if frappe.session.user != "Guest" and
(condition is not defined or (condition is defined and condition )) %}
<span class="btn btn-md btn-secondary-dark reply">
<span class="btn btn-sm btn-secondary reply">
{{ _(cta_title) }}
</span>
{% endif %}

View file

@ -15,19 +15,16 @@
<div class="form-group">
<div class="control-input-wrapper">
<div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control comment-field"
data-fieldtype="Text" data-fieldname="feedback_comments"
placeholder="{{ _('Type here. Use markdown to format.') }}" spellcheck="false"></textarea>
<div class="discussions-comment"></div>
</div>
</div>
</div>
<div class="comment-footer">
<div class="small flex-grow-1">
{{ _("Press Cmd+Enter to post your comment") }}
{{ _("Cmd+Enter to add comment") }}
</div>
<a class="dark-links cancel-comment hide"> {{ _("Cancel") }} </a>
<div class="btn btn-sm btn-default submit-discussion pull-right mb-1">
{{ _("Post") }}
</div>

View file

@ -1,8 +1,12 @@
frappe.ready(() => {
setup_socket_io();
add_color_to_avatars();
this.single_thread = $(".is-single-thread").length;
if (this.single_thread) {
make_comment_editor($(".discussion-form .discussions-comment"));
}
$(".search-field").keyup((e) => {
search_topic(e);
});
@ -15,13 +19,21 @@ frappe.ready(() => {
login_from_discussion(e);
});
$(".sidebar-parent").click((e) => {
$("#discussion-modal .close").click((e) => {
$("#discussion-modal .discussions-comment").html("");
});
$(document).on("click", ".sidebar-parent", (e) => {
if ($(e.currentTarget).attr("aria-expanded") == "true") {
e.stopPropagation();
}
setTimeout(() => {
let element = $(".discussion-form:visible .discussions-comment");
if (!element.find(".ql-editor").length) make_comment_editor(element);
}, 0);
});
$(document).on("keydown", ".comment-field", (e) => {
$(document).on("keydown", ".discussions-comment", (e) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.keyCode == 13 || e.which == 13) &&
@ -36,10 +48,6 @@ frappe.ready(() => {
submit_discussion(e);
});
$(document).on("click", ".cancel-comment", () => {
clear_comment_box();
});
$(document).on("click", ".sidebar-parent", () => {
hide_sidebar();
});
@ -55,59 +63,69 @@ frappe.ready(() => {
$(document).on("click", ".reply-card .dropdown-menu", (e) => {
perform_action(e);
});
$(document).on("input", ".discussion-on-page .comment-field", (e) => {
adjust_comment_box(e);
});
});
const show_new_topic_modal = (e) => {
e.preventDefault();
$("#discussion-modal").modal("show");
make_comment_editor($("#discussion-modal .discussions-comment"));
let topic = $(e.currentTarget).attr("data-topic");
$("#submit-discussion").attr("data-topic", topic ? topic : "");
};
const setup_socket_io = () => {
frappe.realtime.init(window.socketio_port || "9000");
frappe.realtime.on("publish_message", (data) => {
publish_message(data);
});
frappe.realtime.on("update_message", (data) => {
update_message(data);
});
frappe.realtime.socket.on("delete_message", (data) => {
delete_message(data);
});
};
const publish_message = (data) => {
const doctype = decodeURIComponent($(".discussions-parent").attr("data-doctype"));
const docname = decodeURIComponent($(".discussions-parent").attr("data-docname"));
const topic = data.topic_info;
const single_thread = $(".is-single-thread").length;
const first_topic = !$(".reply-card").length;
const document_match_found =
doctype == topic.reference_doctype && docname == topic.reference_docname;
post_message_cleanup();
data = enhance_template(data);
insert_message(data);
};
const enhance_template = (data) => {
data.template = hide_actions_on_conditions(data.template, data.reply_owner);
data.template = style_avatar_frame(data.template);
data.sidebar = style_avatar_frame(data.sidebar);
data.new_topic_template = style_avatar_frame(data.new_topic_template);
return data;
};
const insert_message = (data) => {
const topic = data.topic_info;
const first_topic = !$(".reply-card").length;
const doctype = decodeURIComponent($(".discussions-parent").attr("data-doctype"));
const docname = decodeURIComponent($(".discussions-parent").attr("data-docname"));
const document_match_found =
doctype == topic.reference_doctype && docname == topic.reference_docname;
if ($(`.discussion-on-page[data-topic=${topic.name}]`).length) {
$(data.template).insertBefore(
`.discussion-on-page[data-topic=${topic.name}] .discussion-form`
);
} else if (!first_topic && !single_thread && document_match_found) {
} else if (!first_topic && !this.single_thread && document_match_found) {
$(data.sidebar).insertBefore($(`.discussions-sidebar .sidebar-parent`).first());
$(`#discussion-group`).prepend(data.new_topic_template);
if (topic.owner == frappe.session.user) {
$(".discussion-on-page") && $(".discussion-on-page").collapse();
$(".sidebar-parent").first().click();
setTimeout(() => {
make_comment_editor($(".discussion-form:visible .discussions-comment"));
}, 1000);
}
} else if (single_thread && document_match_found) {
} else if (this.single_thread && document_match_found) {
$(data.template).insertBefore(`.discussion-form`);
$(".discussion-on-page").attr("data-topic", topic.name);
} else if (topic.owner == frappe.session.user && document_match_found) {
@ -122,15 +140,18 @@ const update_message = (data) => {
reply_card.find(".reply-body").removeClass("hide");
reply_card.find(".reply-edit-card").addClass("hide");
reply_card.find(".reply-text").html(data.reply);
reply_card.find(".comment-content").html(data.reply);
reply_card.find(".reply-actions").addClass("hide");
reply_card.find(".dropdown").removeClass("hide");
reply_card.find(".discussions-comment").html("");
};
const post_message_cleanup = () => {
$(".topic-title").val("");
$(".discussion-form .comment-field").val("");
$("#discussion-modal .discussions-comment").html("");
$("#discussion-modal").modal("hide");
$("#no-discussions").addClass("hide");
$(".cancel-comment").addClass("hide");
this.comment_editor && this.comment_editor.set_value("comment_editor", "");
};
const update_reply_count = (topic) => {
@ -193,10 +214,9 @@ const submit_discussion = (e) => {
const target = $(e.currentTarget);
const reply_name = target.closest(".reply-card").data("reply");
const title = $(".topic-title:visible").length ? $(".topic-title:visible").val().trim() : "";
let reply = reply_name ? target.closest(".reply-card") : target.closest(".discussion-form");
reply = reply.find(".comment-field").val().trim();
let reply = this.comment_editor.get_value("comment_editor");
if (reply) {
if (strip_html(reply).trim() != "" || reply.includes("img")) {
let doctype = target.closest(".discussions-parent").attr("data-doctype");
doctype = doctype ? decodeURIComponent(doctype) : doctype;
@ -246,11 +266,6 @@ const style_avatar_frame = (template) => {
return $template.prop("outerHTML");
};
const clear_comment_box = () => {
$(".discussion-form .comment-field").val("");
$(".cancel-comment").removeClass("show").addClass("hide");
};
const hide_sidebar = () => {
$(".discussions-sidebar").addClass("hide");
$("#discussion-group").removeClass("hide");
@ -268,43 +283,78 @@ const back_to_sidebar = () => {
const perform_action = (e) => {
const action = $(e.target).data().action;
const reply_card = $(e.target).closest(".reply-card");
if (action === "edit") {
reply_card.find(".reply-edit-card").removeClass("hide");
reply_card.find(".reply-body").addClass("hide");
reply_card.find(".reply-actions").removeClass("hide");
edit_reply(e);
} else if (action === "delete") {
frappe.call({
method: "frappe.website.doctype.discussion_reply.discussion_reply.delete_message",
args: {
reply_name: $(e.target).closest(".reply-card").data("reply"),
},
});
delete_reply(e);
}
};
const edit_reply = (e) => {
const reply_card = $(e.target).closest(".reply-card");
reply_card.find(".reply-edit-card").removeClass("hide");
reply_card.find(".reply-body").addClass("hide ");
reply_card.find(".reply-actions").removeClass("hide");
reply_card.find(".dropdown").addClass("hide");
make_comment_editor(reply_card.find(".discussions-comment"));
};
const delete_reply = (e) => {
frappe.call({
method: "frappe.website.doctype.discussion_reply.discussion_reply.delete_message",
args: {
reply_name: $(e.target).closest(".reply-card").data("reply"),
},
});
};
const dismiss_reply = (e) => {
const reply_card = $(e.currentTarget).closest(".reply-card");
reply_card.find(".reply-edit-card").addClass("hide");
reply_card.find(".reply-body").removeClass("hide");
reply_card.find(".reply-actions").addClass("hide");
};
const adjust_comment_box = (e) => {
if ($(e.currentTarget).val()) {
$(".cancel-comment").removeClass("hide").addClass("show");
} else {
$(".cancel-comment").removeClass("show").addClass("hide");
}
reply_card.find(".dropdown").removeClass("hide");
reply_card.find(".discussions-comment").html("");
};
const hide_actions_on_conditions = (template, owner) => {
let $template = $(template);
frappe.session.user != owner && $template.find(".dropdown").addClass("hide");
frappe.session.user != owner && $template.find(".dropdown").remove();
return $template.prop("outerHTML");
};
const delete_message = (data) => {
$(`[data-reply=${data.reply_name}]`).addClass("hide");
};
const make_comment_editor = (element) => {
this.comment_editor = new frappe.ui.FieldGroup({
fields: [
{
fieldname: "comment_editor",
fieldtype: "Text Editor",
enable_mentions: true,
theme: "bubble",
placeholder: __("Type your reply here..."),
default: element.siblings(".comment-content").html(),
get_toolbar_options() {
return [
["bold", "italic", "underline", "strike"],
["blockquote", "code-block"],
[{ direction: "rtl" }],
["link", "image"],
[{ list: "ordered" }, { list: "bullet" }],
[{ align: [] }],
["clean"],
];
},
},
],
body: element,
});
this.comment_editor.make();
element.find(".form-section:last").removeClass("empty-section");
element.find(".frappe-control").removeClass("hide-control");
element.find(".form-column").addClass("p-0");
};

View file

@ -5,13 +5,16 @@
<div class="discussions-parent {% if single_thread %} is-single-thread {% endif %}"
data-doctype="{{ doctype | urlencode }}" data-docname="{{ docname | urlencode }}">
{% if not single_thread %}
{% include "frappe/templates/discussions/topic_modal.html" %}
{% endif %}
<div class="discussions-header">
<span class="discussion-heading">{{ _(title) }}</span>
<span class="discussions-section-title">{{ _(title) }}</span>
{% if topics | length and not single_thread %}
{% include "frappe/templates/discussions/search.html" %}
{% endif %}
{% if topics and not single_thread %}
{% include "frappe/templates/discussions/button.html" %}
{% endif %}
@ -20,7 +23,7 @@
<div class="">
{% if topics and not single_thread %}
<div class="discussions-sidebar card-style">
<div class="discussions-sidebar">
{% for topic in topics %}
{% set replies = frappe.get_all("Discussion Reply", {"topic": topic.name})%}

View file

@ -4,11 +4,13 @@
<div class="reply-header">
{{ avatar(reply.owner) }}
<a class="button-links topic-author ml-4"
<a class="button-links topic-author ml-3"
{% if get_profile_url %} href="{{ get_profile_url(member.username) }}" {% endif %}>
{{ member.full_name }}
</a>
<div class="ml-2 frappe-timestamp small" data-timestamp="{{ reply.creation }}"> {{ frappe.utils.pretty_date(reply.creation) }} </div>
<div class="ml-2 frappe-timestamp" data-timestamp="{{ reply.creation }}">
{{ frappe.utils.pretty_date(reply.creation) }}
</div>
<div class="reply-actions hide">
<div class="submit-discussion mr-2"> {{ _("Post") }} </div>
<div class="dismiss-reply"> {{ _("Dismiss") }} </div>
@ -17,31 +19,40 @@
<div class="reply-body">
{% if frappe.session.user == reply.owner %}
<div class="dropdown">
<div class="dropdown ml-auto">
<svg class="icon icon-sm dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<use href="#icon-dot-horizontal"></use>
</svg>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<li>
<a class="dropdown-item small" data-action="edit"> {{ _("Edit") }} </a>
<a class="dropdown-item small" data-action="edit">
{{ _("Edit") }}
</a>
</li>
{% if index != 1 %}
<li>
<a class="dropdown-item small" data-action="delete"> {{ _("Delete") }} </a>
<a class="dropdown-item small" data-action="delete">
{{ _("Delete") }}
</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
<div class="reply-text">{{ frappe.utils.md_to_html(reply.reply) }}</div>
</div>
<div class="reply-body">
<div class="reply-text">{{ reply.reply }}</div>
</div>
<div class="reply-edit-card hide">
<div class="form-group">
<div class="control-input-wrapper">
<div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control comment-field"
data-fieldtype="Text" data-fieldname="feedback_comments" spellcheck="false">{{ reply.reply }}</textarea>
<div class="discussions-comment"></div>
<div class="comment-content hide">
{{ reply.reply }}
</div>
</div>
</div>
</div>

View file

@ -1,26 +1,28 @@
{% if topic %}
{% set replies = frappe.get_all("Discussion Reply", {"topic": topic.name},
["reply", "owner", "creation", "name"], order_by="creation")%}
["reply", "owner", "creation", "name"], order_by="creation")%}
{% endif %}
<div class=" {% if not single_thread %} collapse {% endif %} discussion-on-page card-style" data-parent="#discussion-group"
<div class=" {% if not single_thread %} collapse {% endif %} discussion-on-page" data-parent="#discussion-group"
{% if topic %} id="t{{ topic.name }}" data-topic="{{ topic.name }}" {% endif %}>
{% if not single_thread %}
<div class="reply-section-header">
{% if not single_thread %}
<div class="back-button">
<svg class="icon icon-md mr-0">
<svg class="icon icon-sm mr-0">
<use class="" href="#icon-left"></use>
</svg>
</div>
{% endif %}
{% if topic and topic.title %}
<div class="discussion-heading p-0">{{ topic.title }}</div>
<div class="discussion-heading p-0">
{{ topic.title }}
</div>
{% endif %}
</div>
{% endif %}
{% for reply in replies %}
{% set index = loop.index %}

View file

@ -8,7 +8,7 @@
<div class="flex-grow-1">
<div class="discussion-topic-title">{{ topic.title }}</div>
<div class="sidebar-topic">
<svg class="icon icon-md m-0 mr-2">
<svg class="icon icon-sm m-0 mr-2">
<use class="" href="#icon-reply"></use>
</svg>
<div class="topic-author">{{ creator.full_name }}</div>

View file

@ -1,28 +1,8 @@
.modal .comment-field {
height: 300px;
resize: none;
}
.discussion-on-page .comment-field {
padding: 1rem;
}
.modal .cancel-comment {
display: none;
}
.modal .comment-footer div:first-child {
display: none;
}
.cancel-comment {
font-size: var(--text-sm);
margin-right: 0.5rem;
cursor: pointer;
}
.discussions-header {
margin: 2.5rem 0 1.25rem;
display: flex;
align-items: center;
}
@ -42,9 +22,9 @@
background-image: url(/assets/frappe/icons/timeless/search.svg);
background-repeat: no-repeat;
text-indent: 1.5rem;
background-position: 1rem 0.65rem;
background-position: 1rem 0.45rem;
font-size: var(--text-md);
padding: 0.5rem 1rem;
padding: 0.3rem 1rem;
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius-md);
margin-right: 0.5rem;
@ -68,7 +48,13 @@
}
.reply-card {
margin-bottom: 1.5rem;
padding: 1.25rem 0;
border-bottom: 1px solid var(--gray-200);
}
.reply-card:last-of-type {
border-bottom: none;
padding-bottom: 0;
}
.reply-card .dropdown {
@ -79,7 +65,6 @@
color: var(--text-color);
font-size: var(--text-lg);
font-weight: 600;
margin-bottom: 0.5rem;
}
.discussion-on-page .topic-title {
@ -88,21 +73,12 @@
.discussion-on-page {
flex-direction: column;
padding: 1.5rem;
}
.submit-discussion {
cursor: pointer;
}
.reply-body {
background: var(--bg-color);
padding: 1rem;
border-radius: var(--border-radius);
font-size: var(--text-md);
color: var(--text-color);
}
.reply-actions {
display: flex;
align-items: center;
@ -137,9 +113,16 @@
align-items: center;
}
.discussions-section-title {
font-size: var(--text-2xl);
font-weight: 600;
color: var(--text-color);
flex-grow: 1;
}
.discussion-heading {
font-weight: 600;
font-size: var(--text-3xl);
font-size: var(--text-lg);
line-height: 146%;
letter-spacing: -0.0175em;
color: var(--text-color);
@ -179,6 +162,7 @@
}
.back-button {
display: flex;
margin-right: 1rem;
cursor: pointer;
}
@ -197,7 +181,7 @@
}
.empty-state {
background: var(--control-bg);
border: 1px solid var(--gray-300);
border-radius: var(--border-radius-lg);
padding: 2rem;
display: flex;
@ -219,7 +203,7 @@
.sidebar-parent {
display: flex;
align-items: center;
padding: 1.25rem;
padding: 1.25rem 0;
cursor: pointer;
}
@ -243,7 +227,7 @@
.reply-section-header {
display: flex;
align-items: center;
margin-bottom: 2.5rem;
margin-top: 1.5rem;
}
.reply-header {
@ -265,6 +249,44 @@
margin-bottom: 0;
}
.reply-body .dropdown-menu {
.reply-header .dropdown-menu {
min-width: 7rem;
}
.discussions-parent .ql-editor {
border-radius: var(--border-radius-md);
}
.mention {
display: inline-block;
height: auto;
width: auto;
border-radius: var(--border-radius-lg);
border: 1px solid var(--border-color);
padding: 2px 5px;
font-size: var(--text-sm);
background-color: var(--fg-color);
}
.mention a {
text-decoration: none;
color: inherit;
}
.ql-editor.read-mode .mention {
background-color: var(--control-bg);
}
.ql-editor.read-mode .mention a {
color: inherit;
background-color: inherit;
}
.discussion-form .form-group {
margin-bottom: 0;
}
.discussions-parent .ql-editor.ql-blank::before {
color: var(--gray-600);
font-style: normal;
}

View file

@ -0,0 +1,270 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import os
from unittest.mock import patch
import frappe
import frappe.utils
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.desk.notifications import get_open_count
from frappe.tests.utils import FrappeTestCase
class TestDashboardConnections(FrappeTestCase):
@patch.dict(frappe.conf, {"developer_mode": 1})
def setUp(self):
delete_test_data()
create_test_data()
@patch.dict(frappe.conf, {"developer_mode": 1})
def tearDown(self):
delete_test_data()
def test_internal_link_count(self):
earth = frappe.get_doc(
{
"doctype": "Test Doctype B With Child Table With Link To Doctype A",
"title": "Earth",
}
)
earth.append(
"child_table",
{
"title": "Earth",
},
)
earth.insert()
mars = frappe.get_doc(
{
"doctype": "Test Doctype A With Child Table With Link To Doctype B",
"title": "Mars",
}
)
mars.append(
"child_table",
{"title": "Mars", "test_doctype_b_with_test_child_table_with_link_to_doctype_a": "Earth"},
)
mars.insert()
expected_open_count = {
"count": {
"external_links_found": [],
"internal_links_found": [
{
"count": 1,
"doctype": "Test Doctype B With Child Table With Link To Doctype A",
"names": ["Earth"],
"open_count": 0,
}
],
}
}
with patch.object(
mars.meta,
"get_dashboard_data",
return_value=get_dashboard_for_test_doctype_a_with_test_child_table_with_link_to_doctype_b(),
):
self.assertEqual(
get_open_count("Test Doctype A With Child Table With Link To Doctype B", "Mars"),
expected_open_count,
)
def test_external_link_count(self):
saturn = frappe.get_doc(
{
"doctype": "Test Doctype A With Child Table With Link To Doctype B",
"title": "Saturn",
}
)
saturn.append(
"child_table",
{
"title": "Saturn",
},
)
saturn.insert()
pluto = frappe.get_doc(
{
"doctype": "Test Doctype B With Child Table With Link To Doctype A",
"title": "Pluto",
}
)
pluto.append(
"child_table",
{"title": "Pluto", "test_doctype_a_with_test_child_table_with_link_to_doctype_b": "Saturn"},
)
pluto.insert()
expected_open_count = {
"count": {
"external_links_found": [
{
"doctype": "Test Doctype B With Child Table With Link To Doctype A",
"open_count": 0,
"count": 1,
}
],
"internal_links_found": [],
}
}
with patch.object(
saturn.meta,
"get_dashboard_data",
return_value=get_dashboard_for_test_doctype_a_with_test_child_table_with_link_to_doctype_b(),
):
self.assertEqual(
get_open_count("Test Doctype A With Child Table With Link To Doctype B", "Saturn"),
expected_open_count,
)
def create_test_data():
create_test_child_table_with_link_to_doctype_a()
create_test_child_table_with_link_to_doctype_b()
create_test_doctype_a_with_test_child_table_with_link_to_doctype_b()
create_test_doctype_b_with_test_child_table_with_link_to_doctype_a()
add_links_in_child_tables()
def delete_test_data():
doctypes = [
"Test Child Table With Link To Doctype A",
"Test Child Table With Link To Doctype B",
"Test Doctype A With Child Table With Link To Doctype B",
"Test Doctype B With Child Table With Link To Doctype A",
]
for doctype in doctypes:
if frappe.db.table_exists(doctype):
frappe.db.delete(doctype)
frappe.delete_doc("DocType", doctype, force=True)
def create_test_child_table_with_link_to_doctype_a():
new_doctype(
"Test Child Table With Link To Doctype A",
istable=1,
fields=[{"fieldname": "title", "fieldtype": "Data", "label": "Title", "reqd": 1, "unique": 1}],
custom=False,
autoname="field:title",
naming_rule="By fieldname",
).insert(ignore_if_duplicate=True)
def create_test_child_table_with_link_to_doctype_b():
new_doctype(
"Test Child Table With Link To Doctype B",
istable=1,
fields=[{"fieldname": "title", "fieldtype": "Data", "label": "Title", "reqd": 1, "unique": 1}],
custom=False,
autoname="field:title",
naming_rule="By fieldname",
).insert(ignore_if_duplicate=True)
def add_links_in_child_tables():
test_child_table_with_link_to_doctype_a = frappe.get_doc(
"DocType", "Test Child Table With Link To Doctype A"
)
if len(test_child_table_with_link_to_doctype_a.fields) == 1:
test_child_table_with_link_to_doctype_a.append(
"fields",
{
"fieldname": "test_doctype_a_with_test_child_table_with_link_to_doctype_b",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Test Doctype A With Child Table With Link To Doctype B" or "Doctype to Link",
"options": "Test Doctype A With Child Table With Link To Doctype B" or "Doctype to Link",
},
)
test_child_table_with_link_to_doctype_a.save()
test_child_table_with_link_to_doctype_b = frappe.get_doc(
"DocType", "Test Child Table With Link To Doctype B"
)
if len(test_child_table_with_link_to_doctype_b.fields) == 1:
test_child_table_with_link_to_doctype_b.append(
"fields",
{
"fieldname": "test_doctype_b_with_test_child_table_with_link_to_doctype_a",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Test Doctype B With Child Table With Link To Doctype A" or "Doctype to Link",
"options": "Test Doctype B With Child Table With Link To Doctype A" or "Doctype to Link",
},
)
test_child_table_with_link_to_doctype_b.save()
def create_test_doctype_a_with_test_child_table_with_link_to_doctype_b():
new_doctype(
"Test Doctype A With Child Table With Link To Doctype B",
fields=[
{"fieldname": "title", "fieldtype": "Data", "label": "Title", "unique": 1},
{
"fieldname": "child_table",
"fieldtype": "Table",
"label": "Child Table",
"options": "Test Child Table With Link To Doctype B",
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1,
},
],
custom=False,
autoname="field:title",
naming_rule="By fieldname",
).insert(ignore_if_duplicate=True)
def create_test_doctype_b_with_test_child_table_with_link_to_doctype_a():
new_doctype(
"Test Doctype B With Child Table With Link To Doctype A",
fields=[
{"fieldname": "title", "fieldtype": "Data", "label": "Title", "unique": 1},
{
"fieldname": "child_table",
"fieldtype": "Table",
"label": "Child Table",
"options": "Test Child Table With Link To Doctype A",
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1,
},
],
custom=False,
autoname="field:title",
naming_rule="By fieldname",
).insert(ignore_if_duplicate=True)
def get_dashboard_for_test_doctype_a_with_test_child_table_with_link_to_doctype_b():
dashboard = frappe._dict()
data = {
"fieldname": "test_doctype_a_with_test_child_table_with_link_to_doctype_b",
"internal_links": {
"Test Doctype B With Child Table With Link To Doctype A": [
"child_table",
"test_doctype_b_with_test_child_table_with_link_to_doctype_a",
],
},
"transactions": [
{"label": "Reference", "items": ["Test Doctype B With Child Table With Link To Doctype A"]},
],
}
dashboard.fieldname = data["fieldname"]
dashboard.internal_links = data["internal_links"]
dashboard.transactions = data["transactions"]
return dashboard

View file

@ -156,7 +156,7 @@ class TestEmail(FrappeTestCase):
frappe.conf.use_ssl = False
def test_expose(self):
from frappe.utils import set_request
from frappe.utils.verified_command import verify_request
frappe.sendmail(
@ -199,9 +199,11 @@ class TestEmail(FrappeTestCase):
if content:
eol = "\r\n"
frappe.local.flags.signed_query_string = re.search(
query_string = re.search(
r"(?<=/api/method/frappe.email.queue.unsubscribe\?).*(?=" + eol + ")", content.decode()
).group(0)
set_request(method="GET", query_string=query_string)
self.assertTrue(verify_request())
break
@ -320,6 +322,6 @@ class TestVerifiedRequests(FrappeTestCase):
for params in test_cases:
signed_url = get_signed_params(params)
set_request(method="GET", path="?" + signed_url)
set_request(method="GET", query_string=signed_url)
self.assertTrue(verify_request())
frappe.local.request = None

View file

@ -71,6 +71,15 @@ class TestListView(FrappeTestCase):
}
self.assertEqual(data["Administrator"], 1)
def test_get_group_by_invalid_field(self):
self.assertRaises(
ValueError,
get_group_by_count,
"Note",
'[["Note Seen By","user","=","Administrator"]]',
"invalid_field",
)
def test_list_view_comment_count(self):
frappe.form_dict.doctype = "DocType"
frappe.form_dict.limit = "1"

View file

@ -2,7 +2,7 @@ from contextlib import contextmanager
from random import choice
import frappe
from frappe.model import core_doctypes_list, get_permitted_fields
from frappe.model import core_doctypes_list, get_permitted_fields, is_default_field
from frappe.model.utils import get_fetch_values
from frappe.tests.utils import FrappeTestCase
@ -66,6 +66,16 @@ class TestModelUtils(FrappeTestCase):
get_permitted_fields("Installed Application", parenttype="Installed Applications"), []
)
def test_is_default_field(self):
self.assertTrue(is_default_field("doctype"))
self.assertTrue(is_default_field("name"))
self.assertTrue(is_default_field("owner"))
self.assertFalse(is_default_field({}))
self.assertFalse(is_default_field("qwerty1234"))
self.assertFalse(is_default_field(True))
self.assertFalse(is_default_field(42))
@contextmanager
def set_user(user: str):

View file

@ -149,6 +149,9 @@ def _restore_thread_locals(flags):
frappe.local.lang = "en"
frappe.local.preload_assets = {"style": [], "script": []}
if hasattr(frappe.local, "request"):
delattr(frappe.local, "request")
@contextmanager
def change_settings(doctype, settings_dict):

View file

@ -3041,6 +3041,7 @@ zoom-out,verkleinern,
{0} Calendar,{0} Kalender,
{0} Chart,{0} Diagramm,
{0} Dashboard,{0}-Dashboard,
{0} if you are not redirected within 5 seconds,"{0}, falls Sie nicht innerhalb von 5 Sekunden weitergeleitet werden",
{0} List,{0} Liste,
{0} Modules,{0} Module,
{0} Report,{0} Bericht(e),
@ -3240,6 +3241,7 @@ Check the Error Log for more information: {0},Überprüfen Sie das Fehlerprotoko
Clear Cache and Reload,Cache leeren und neu laden,
Clear Filters,Filter löschen,
Clear all filters,Alle Filter löschen,
Click here,Klicken Sie hier,
Click on <b>Authorize Google Drive Access</b> to authorize Google Drive Access.,"Klicken Sie auf <b>Google Drive Access autorisieren, um Google Drive Access</b> zu autorisieren.",
Click on a file to select it.,"Klicken Sie auf eine Datei, um sie auszuwählen.",
Click on the link below to approve the request,"Klicken Sie auf den folgenden Link, um die Anfrage zu genehmigen",
@ -4637,6 +4639,7 @@ Published on,Veröffentlicht auf,
Enable developer mode to create a standard Web Template,"Aktivieren Sie den Entwicklermodus, um eine Standard-Webvorlage zu erstellen",
Was this article helpful?,War dieser Artikel hilfreich?,
Thank you for your feedback!,Vielen Dank für dein Feedback!,
Thank you for spending your valuable time to fill this form,"Vielen Dank, dass Sie sich die Zeit genommen haben, dieses Formular auszufüllen",
New Mention on {0},Neue Erwähnung zu {0},
Assignment Update on {0},Zuweisungsaktualisierung auf {0},
New Document Shared {0},Neues Dokument freigegeben {0},
@ -4853,3 +4856,18 @@ Minimal,Minimal,
This value is fetched from {0}'s {1} field,Dieser Wert ergibt sich aus dem Feld {1} von {0},
This form is not editable due to a Workflow.,Dieses Formular kann in diesem Workflow-Status nicht bearbeitet werden.,
{0} role does not have permission on any doctype,Die Rolle {0} hat auf keinen DocType Zugriff,
Filters:,Filter:,
{0} is equal to {1},{0} ist gleich {1},
{0} is not equal to {1},{0} ist ungleich {1},
{0} is greater than {1},{0} ist größer als {1},
{0} is less than {1},{0} ist kleiner als {1},
{0} is greater than or equal to {1},{0} ist größer oder gleich {1},
{0} is less than or equal to {1},{0} ist kleiner oder gleich {1},
{0} is between {1} and {2},{0} ist zwischen {1} und {2},
{0} is within {1},{0} ist innerhalb von {1},
{0} is one of {1},{0} ist eine von {1},
{0} is not one of {1},{0} ist keine von {1},
{0} is like {1},{0} ist wie {1},
{0} is not like {1},{0} ist nicht wie {1},
{0} is set,{0} ist eingetragen,
{0} is not set,{0} ist nicht eingetragen,

1 A4 A4
3041 {0} Calendar {0} Kalender
3042 {0} Chart {0} Diagramm
3043 {0} Dashboard {0}-Dashboard
3044 {0} if you are not redirected within 5 seconds {0}, falls Sie nicht innerhalb von 5 Sekunden weitergeleitet werden
3045 {0} List {0} Liste
3046 {0} Modules {0} Module
3047 {0} Report {0} Bericht(e)
3241 Clear Cache and Reload Cache leeren und neu laden
3242 Clear Filters Filter löschen
3243 Clear all filters Alle Filter löschen
3244 Click here Klicken Sie hier
3245 Click on <b>Authorize Google Drive Access</b> to authorize Google Drive Access. Klicken Sie auf <b>Google Drive Access autorisieren, um Google Drive Access</b> zu autorisieren.
3246 Click on a file to select it. Klicken Sie auf eine Datei, um sie auszuwählen.
3247 Click on the link below to approve the request Klicken Sie auf den folgenden Link, um die Anfrage zu genehmigen
4639 Enable developer mode to create a standard Web Template Aktivieren Sie den Entwicklermodus, um eine Standard-Webvorlage zu erstellen
4640 Was this article helpful? War dieser Artikel hilfreich?
4641 Thank you for your feedback! Vielen Dank für dein Feedback!
4642 Thank you for spending your valuable time to fill this form Vielen Dank, dass Sie sich die Zeit genommen haben, dieses Formular auszufüllen
4643 New Mention on {0} Neue Erwähnung zu {0}
4644 Assignment Update on {0} Zuweisungsaktualisierung auf {0}
4645 New Document Shared {0} Neues Dokument freigegeben {0}
4856 This value is fetched from {0}'s {1} field Dieser Wert ergibt sich aus dem Feld {1} von {0}
4857 This form is not editable due to a Workflow. Dieses Formular kann in diesem Workflow-Status nicht bearbeitet werden.
4858 {0} role does not have permission on any doctype Die Rolle {0} hat auf keinen DocType Zugriff
4859 Filters: Filter:
4860 {0} is equal to {1} {0} ist gleich {1}
4861 {0} is not equal to {1} {0} ist ungleich {1}
4862 {0} is greater than {1} {0} ist größer als {1}
4863 {0} is less than {1} {0} ist kleiner als {1}
4864 {0} is greater than or equal to {1} {0} ist größer oder gleich {1}
4865 {0} is less than or equal to {1} {0} ist kleiner oder gleich {1}
4866 {0} is between {1} and {2} {0} ist zwischen {1} und {2}
4867 {0} is within {1} {0} ist innerhalb von {1}
4868 {0} is one of {1} {0} ist eine von {1}
4869 {0} is not one of {1} {0} ist keine von {1}
4870 {0} is like {1} {0} ist wie {1}
4871 {0} is not like {1} {0} ist nicht wie {1}
4872 {0} is set {0} ist eingetragen
4873 {0} is not set {0} ist nicht eingetragen

View file

@ -680,6 +680,7 @@ Clear Error Logs,Limpiar Registros de Errores,
Clear User Permissions,Borrar permisos de usuario,
Clear all roles,Limpiar todos los roles,
"Clearing end date, as it cannot be in the past for published pages.","Borrando la fecha de finalización, ya que no puede ser en el pasado para las páginas publicadas.",
Click here,Click aquí,
Click here to post bugs and suggestions,Haga clic aquí para publicar errores y sugerencias,
Click here to verify,Haga clic aquí para verificar,
Click on the link below to complete your registration and set a new password,Haga clic en el enlace de abajo para completar su registro y establecer una nueva contraseña,
@ -2460,6 +2461,7 @@ Text Color,Color de texto,
Text Content,Contenido del texto,
Text Editor,Editor de texto,
Text to be displayed for Link to Web Page if this form has a web page. Link route will be automatically generated based on `page_name` and `parent_website_route`,"El texto que se mostrará para enlazar a la página web, en caso que este formulario sea una pagina web. El enlace se generará automaticamente basado en 'nombre de pagina' y ruta del sitio principal' (`page_name` and `parent_website_route`)",
Thank you for spending your valuable time to fill this form,Gracias por tomarse el tiempo para llenar este formulario,
Thank you for your email,Gracias por su Email,
Thank you for your interest in subscribing to our updates,Gracias por su interés en suscribirse a nuestras actualizaciones,
Thank you for your message,¡Gracias por tu mensaje!,
@ -3027,6 +3029,7 @@ zoom-out,Alejar,
{0} Calendar,{0} Calendario,
{0} Chart,{0} Gráfico,
{0} Dashboard,{0} Panel de control,
{0} if you are not redirected within 5 seconds,{0} si no es redirigido en 5 segundos,
{0} List,Lista {0},
{0} Modules,{0} Módulos,
{0} Report,{0} Informe,

Can't render this file because it has a wrong number of fields in line 1708.

View file

@ -3014,6 +3014,7 @@ zoom-out,Réduire,
{0} Calendar,{0} Calendrier,
{0} Chart,Graphique {0},
{0} Dashboard,{0} Tableau de bord,
{0} if you are not redirected within 5 seconds,{0} si vous n'êtes pas redirigé dans les 5 secondes,
{0} List,Liste {0},
{0} Modules,{0} Modules,
{0} Report,Rapport {0},
@ -3212,6 +3213,7 @@ Change User,Changer d&#39;utilisateur,
Check the Error Log for more information: {0},Consultez le journal des erreurs pour plus d&#39;informations: {0},
Clear Cache and Reload,Vider le cache et recharger,
Clear Filters,Effacer les filtres,
Click here,Cliquez ici,
Click on <b>Authorize Google Drive Access</b> to authorize Google Drive Access.,Cliquez sur <b>Autoriser l&#39;accès</b> à Google Drive pour autoriser l&#39; <b>accès</b> à Google Drive.,
Click on a file to select it.,Cliquez sur un fichier pour le sélectionner.,
Click on the link below to approve the request,Cliquez sur le lien ci-dessous pour approuver la demande.,
@ -4605,6 +4607,7 @@ Published on,Publié le,
Enable developer mode to create a standard Web Template,Activer le mode développeur pour créer un modèle Web standard,
Was this article helpful?,Cet article a-t-il été utile?,
Thank you for your feedback!,Merci pour votre avis!,
Thank you for spending your valuable time to fill this form,"Merci d'avoir consacré votre temps précieux à remplir ce formulaire",
New Mention on {0},Nouvelle mention sur {0},
Assignment Update on {0},Mise à jour du devoir le {0},
New Document Shared {0},Nouveau document partagé {0},

1 A4 A4
3014 {0} Calendar {0} Calendrier
3015 {0} Chart Graphique {0}
3016 {0} Dashboard {0} Tableau de bord
3017 {0} if you are not redirected within 5 seconds {0} si vous n'êtes pas redirigé dans les 5 secondes
3018 {0} List Liste {0}
3019 {0} Modules {0} Modules
3020 {0} Report Rapport {0}
3213 Check the Error Log for more information: {0} Consultez le journal des erreurs pour plus d&#39;informations: {0}
3214 Clear Cache and Reload Vider le cache et recharger
3215 Clear Filters Effacer les filtres
3216 Click here Cliquez ici
3217 Click on <b>Authorize Google Drive Access</b> to authorize Google Drive Access. Cliquez sur <b>Autoriser l&#39;accès</b> à Google Drive pour autoriser l&#39; <b>accès</b> à Google Drive.
3218 Click on a file to select it. Cliquez sur un fichier pour le sélectionner.
3219 Click on the link below to approve the request Cliquez sur le lien ci-dessous pour approuver la demande.
4607 Enable developer mode to create a standard Web Template Activer le mode développeur pour créer un modèle Web standard
4608 Was this article helpful? Cet article a-t-il été utile?
4609 Thank you for your feedback! Merci pour votre avis!
4610 Thank you for spending your valuable time to fill this form Merci d'avoir consacré votre temps précieux à remplir ce formulaire
4611 New Mention on {0} Nouvelle mention sur {0}
4612 Assignment Update on {0} Mise à jour du devoir le {0}
4613 New Document Shared {0} Nouveau document partagé {0}

View file

@ -666,6 +666,7 @@ Clear Error Logs,Registri evidente errore,
Clear User Permissions,Cancella autorizzazioni utente,
Clear all roles,Cancellare tutti i ruoli,
"Clearing end date, as it cannot be in the past for published pages.","Cancellare la data di fine, in quanto non può essere nel passato per le pagine pubblicate.",
Click here,Clicca qui,
Click here to post bugs and suggestions,Clicca qui per inserire bug e suggerimenti,
Click here to verify,Clicca qui per verificare,
Click on the link below to complete your registration and set a new password,Clicca sul link qui sotto per completare la registrazione e impostare una nuova password,
@ -3012,6 +3013,7 @@ zoom-out,Riduci,
{0} Calendar,{0} Calendario,
{0} Chart,{0} Grafico,
{0} Dashboard,{0} Dashboard,
{0} if you are not redirected within 5 seconds,{0} se non vieni reindirizzato entro 5 secondi,
{0} List,{0} Lista,
{0} Modules,{0} Moduli,
{0} Report,{0} Report,
@ -4604,6 +4606,7 @@ Published on,pubblicato su,
Enable developer mode to create a standard Web Template,Abilita la modalità sviluppatore per creare un modello web standard,
Was this article helpful?,questo articolo è stato utile?,
Thank you for your feedback!,Grazie per il tuo feedback!,
Thank you for spending your valuable time to fill this form,Grazie per aver dedicato il tuo prezioso tempo a compilare questo modulo,
New Mention on {0},Nuova menzione su {0},
Assignment Update on {0},Aggiornamento del compito su {0},
New Document Shared {0},Nuovo documento condiviso {0},

1 A4 A4
666 Clear User Permissions Cancella autorizzazioni utente
667 Clear all roles Cancellare tutti i ruoli
668 Clearing end date, as it cannot be in the past for published pages. Cancellare la data di fine, in quanto non può essere nel passato per le pagine pubblicate.
669 Click here Clicca qui
670 Click here to post bugs and suggestions Clicca qui per inserire bug e suggerimenti
671 Click here to verify Clicca qui per verificare
672 Click on the link below to complete your registration and set a new password Clicca sul link qui sotto per completare la registrazione e impostare una nuova password
3013 {0} Calendar {0} Calendario
3014 {0} Chart {0} Grafico
3015 {0} Dashboard {0} Dashboard
3016 {0} if you are not redirected within 5 seconds {0} se non vieni reindirizzato entro 5 secondi
3017 {0} List {0} Lista
3018 {0} Modules {0} Moduli
3019 {0} Report {0} Report
4606 Enable developer mode to create a standard Web Template Abilita la modalità sviluppatore per creare un modello web standard
4607 Was this article helpful? questo articolo è stato utile?
4608 Thank you for your feedback! Grazie per il tuo feedback!
4609 Thank you for spending your valuable time to fill this form Grazie per aver dedicato il tuo prezioso tempo a compilare questo modulo
4610 New Mention on {0} Nuova menzione su {0}
4611 Assignment Update on {0} Aggiornamento del compito su {0}
4612 New Document Shared {0} Nuovo documento condiviso {0}

View file

@ -157,5 +157,5 @@ def guess_exception_source(exception: str) -> str | None:
app_name = matches.group("app_name")
apps[app_name] += app_priority.get(app_name, 0)
if probably_source := apps.most_common(1):
if (probably_source := apps.most_common(1)) and probably_source[0][0] != "frappe":
return f"{probably_source[0][0]} (app)"

View file

@ -46,12 +46,15 @@ def strip_exif_data(content, content_type):
def optimize_image(
content, content_type, max_width=1920, max_height=1080, optimize=True, quality=85
content, content_type, max_width=1024, max_height=768, optimize=True, quality=85
):
if content_type == "image/svg+xml":
return content
image = Image.open(io.BytesIO(content))
width, height = image.size
max_height = max(min(max_height, height * 0.8), 200)
max_width = max(min(max_width, width * 0.8), 200)
image_format = content_type.split("/")[1]
size = max_width, max_height
image.thumbnail(size, Image.Resampling.LANCZOS)

View file

@ -1,5 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import contextlib
import io
import os
import re
@ -43,7 +44,30 @@ def pdf_header_html(soup, head, content, styles, html_id, css):
def pdf_body_html(template, args, **kwargs):
return template.render(args, filters={"len": len})
try:
return template.render(args, filters={"len": len})
except Exception as e:
# Guess line number ?
frappe.throw(
_("Error in print format on line {0}: {1}").format(
_guess_template_error_line_number(template), e
),
exc=frappe.PrintFormatError,
title=_("Print Format Error"),
)
def _guess_template_error_line_number(template) -> int | None:
"""Guess line on which exception occured from current traceback."""
with contextlib.suppress(Exception):
import sys
import traceback
_, _, tb = sys.exc_info()
for frame in reversed(traceback.extract_tb(tb)):
if template.filename in frame.filename:
return frame.lineno
def pdf_footer_html(soup, head, content, styles, html_id, css):

View file

@ -247,7 +247,7 @@ def safe_enqueue(function, **kwargs):
Accepts frappe.enqueue params like job_name, queue, timeout, etc.
in addition to params to be passed to function
:param function: whitelised function or API Method set in Server Script
:param function: whitelisted function or API Method set in Server Script
"""
return enqueue("frappe.utils.safe_exec.call_whitelisted_function", function=function, **kwargs)

View file

@ -20,6 +20,10 @@ class TestBlogPost(FrappeTestCase):
def setUp(self):
reset_customization("Blog Post")
def tearDown(self):
if hasattr(frappe.local, "request"):
delattr(frappe.local, "request")
def test_generator_view(self):
pages = frappe.get_all(
"Blog Post", fields=["name", "route"], filters={"published": 1, "route": ("!=", "")}, limit=1
@ -159,17 +163,10 @@ class TestBlogPost(FrappeTestCase):
from frappe.templates.includes.likes.likes import like
frappe.form_dict.reference_doctype = "Blog Post"
frappe.form_dict.reference_name = test_blog.name
frappe.form_dict.like = True
frappe.local.request_ip = "127.0.0.1"
liked = like()
liked = like("Blog Post", test_blog.name, True)
self.assertEqual(liked, True)
frappe.form_dict.like = False
disliked = like()
disliked = like("Blog Post", test_blog.name, False)
self.assertEqual(disliked, False)
frappe.db.delete("Comment", {"comment_type": "Like", "reference_doctype": "Blog Post"})

View file

@ -12,7 +12,7 @@
"fields": [
{
"fieldname": "reply",
"fieldtype": "Long Text",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Reply"
},
@ -27,7 +27,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-28 12:09:10.875927",
"modified": "2021-09-28 12:09:10.875929",
"modified_by": "Administrator",
"module": "Website",
"name": "Discussion Reply",
@ -49,5 +49,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -15,7 +15,7 @@ class DiscussionReply(Document):
if TYPE_CHECKING:
from frappe.types import DF
reply: DF.LongText | None
reply: DF.TextEditor | None
topic: DF.Link | None
# end: auto-generated types
def on_update(self):

View file

@ -3,6 +3,7 @@
import frappe
from frappe import _
from frappe.rate_limiter import rate_limit
from frappe.utils import cint, is_markdown, markdown
from frappe.website.utils import get_comment_list
from frappe.website.website_generator import WebsiteGenerator
@ -129,10 +130,9 @@ def clear_website_cache(path=None):
@frappe.whitelist(allow_guest=True)
def add_feedback(article, helpful):
field = "helpful"
if helpful == "No":
field = "not_helpful"
@rate_limit(key="article", limit=5, seconds=60 * 60)
def add_feedback(article: str, helpful: str):
field = "not_helpful" if helpful == "No" else "helpful"
value = cint(frappe.db.get_value("Help Article", article, field))
frappe.db.set_value("Help Article", article, field, value + 1, update_modified=False)

View file

@ -141,8 +141,8 @@
{% if success_url %}
<div class="success_url_message">
<p>
{% 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>
{% set success_link = '<a href="{0}">{1}</a>'.format(success_url, _("Click here")) %}
{{ _("{0} if you are not redirected within 5 seconds").format(success_link) }}
</p>
</div>
{% else %}

View file

@ -14,15 +14,9 @@ test_dependencies = ["Web Form"]
class TestWebForm(FrappeTestCase):
def setUp(self):
frappe.conf.disable_website_cache = True
frappe.local.path = None
def tearDown(self):
frappe.conf.disable_website_cache = False
frappe.local.path = None
frappe.local.request_ip = None
frappe.form_dict.web_form = None
frappe.form_dict.data = None
frappe.form_dict.docname = None
def test_accept(self):
frappe.set_user("Administrator")
@ -34,10 +28,6 @@ class TestWebForm(FrappeTestCase):
"starts_on": "2014-09-09",
}
frappe.form_dict.web_form = "manage-events"
frappe.form_dict.data = json.dumps(doc)
frappe.local.request_ip = "127.0.0.1"
accept(web_form="manage-events", data=json.dumps(doc))
self.event_name = frappe.db.get_value("Event", {"subject": "_Test Event Web Form"})
@ -58,11 +48,7 @@ class TestWebForm(FrappeTestCase):
frappe.db.get_value("Event", self.event_name, "description"), doc.get("description")
)
frappe.form_dict.web_form = "manage-events"
frappe.form_dict.docname = self.event_name
frappe.form_dict.data = json.dumps(doc)
accept(web_form="manage-events", docname=self.event_name, data=json.dumps(doc))
accept("manage-events", json.dumps(doc))
self.assertEqual(
frappe.db.get_value("Event", self.event_name, "description"), doc.get("description")

View file

@ -429,7 +429,7 @@ def get_web_form_module(doc):
@frappe.whitelist(allow_guest=True)
@rate_limit(key="web_form", limit=5, seconds=60, methods=["POST"])
@rate_limit(key="web_form", limit=5, seconds=60)
def accept(web_form, data):
"""Save the web form"""
data = frappe._dict(json.loads(data))

View file

@ -23,10 +23,10 @@
<input id="confirm_password" type="password"
class="form-control" placeholder="{{ _('Confirm Password') }}" autocomplete="new-password">
<p class="password-mismatch-message text-muted small hidden mt-2"></p>
</div>
<p class='password-strength-message text-muted small hidden'></p>
<button type="submit" id="update"
<p class="password-mismatch-message text-muted small hidden mt-2"></p>
<p class='password-strength-message text-muted small mt-2 hidden'></p>
<button type="submit" id="update" disabled = true style="cursor: not-allowed;"
class="btn btn-primary btn-block btn-update">{{_("Confirm")}}</button>
</form>
{%- if not disable_signup -%}
@ -43,11 +43,27 @@
<script>
frappe.ready(function() {
if(frappe.utils.get_url_arg("key")) {
$("#old_password").parent().toggle();
// URL args
const key = frappe.utils.get_url_arg('key');
const password_expired = frappe.utils.get_url_arg('password_expired');
// inputs, paragraphs and button elements
const old_password = $('#old_password');
const new_password = $('#new_password');
const confirm_password = $('#confirm_password');
const update_button = $('#update');
const password_strength_indicator = $('.password-strength-indicator');
const password_strength_message =$('.password-strength-message');
const password_mismatch_message = $('.password-mismatch-message');
// Info text
const password_not_same_as_old_password = "{{ _('New password cannot be same as old password') }}";
const password_mismatch = "{{ _('Passwords do not match') }}";
const password_strength_message_success = "{{ _('Success! You are good to go 👍') }}";
if(key) {
old_password.parent().toggle();
}
if(frappe.utils.get_url_arg("password_expired")) {
if(password_expired) {
$(".password-box").html("{{ _('The password of your account has expired.') }}");
}
@ -55,18 +71,18 @@ frappe.ready(function() {
return false;
});
$("#new_password").on("keypress", function(e) {
if(e.which===13) $("#update").click();
new_password.on("keypress", function(e) {
if(e.which===13) update_button.click();
})
$("#update").click(function() {
update_button.click(function() {
var args = {
key: frappe.utils.get_url_arg("key") || "",
old_password: $("#old_password").val(),
new_password: $("#new_password").val(),
key: key || "",
old_password: old_password.val(),
new_password: new_password.val(),
confirm_password: confirm_password.val(),
logout_all_sessions: 1
}
const confirm_password = $('#confirm_password').val()
if (!args.old_password && !args.key) {
frappe.msgprint({
title: "{{ _('Missing Value') }}",
@ -81,20 +97,36 @@ frappe.ready(function() {
clear: true
});
}
if (args.new_password !== confirm_password) {
$('.password-mismatch-message').text("{{ _('Passwords do not match') }}")
if (args.old_password === args.new_password) {
frappe.msgprint({
title: "{{ _('Invalid Password') }}",
message: password_not_same_as_old_password,
});
password_strength_message.addClass('hidden');
return;
}
if (args.new_password !== args.confirm_password) {
password_mismatch_message.text(password_mismatch)
.removeClass('hidden text-muted').addClass('text-danger');
return false;
password_strength_message.addClass('hidden');
return;
}
frappe.call({
type: "POST",
method: "frappe.core.doctype.user.user.update_password",
btn: $("#update"),
btn: update_button,
args: args,
statusCode: {
401: function() {
$(".page-card-head .reset-password-heading").text("{{ _('Invalid Password') }}");
frappe.msgprint({
title: "{{ _('Invalid Password') }}",
message: "{{ _('Your old password is incorrect.') }}",
// clear any server message
clear: true
});
},
410: function({ responseJSON }) {
const title = "{{ _('Invalid Link') }}";
@ -127,21 +159,55 @@ frappe.ready(function() {
return false;
});
window.strength_indicator = $('.password-strength-indicator');
window.strength_message = $('.password-strength-message');
window.strength_indicator = password_strength_indicator;
window.strength_message = password_strength_message;
$('#new_password').on('keyup', function() {
new_password.on('keyup', function() {
window.clear_timeout();
window.timout_password_strength = setTimeout(window.test_password_strength, 200);
});
$("#old_password, #new_password, #confirm_password").on("keyup", frappe.utils.debounce(function () {
let common_conditions = new_password.val() && confirm_password.val() && new_password.val() === confirm_password.val() &&
password_strength_message.text() === password_strength_message_success
if (new_password.val() && old_password.val() === new_password.val()) {
password_mismatch_message.text(password_not_same_as_old_password)
.removeClass("hidden text-muted").addClass("text-danger");
password_strength_message.addClass("hidden");
}
if ((new_password.val() || old_password.val) && old_password.val() !== new_password.val()) {
password_mismatch_message.addClass("hidden");
password_strength_message.removeClass("hidden");
password_mismatch_message.text('')
}
if (new_password.val() === confirm_password.val() && old_password.val() !== new_password.val() ) {
password_mismatch_message.addClass("hidden");
password_strength_message.removeClass("hidden");
}
if (confirm_password.val() && new_password.val() !== confirm_password.val()) {
password_mismatch_message.text(password_mismatch)
.removeClass("hidden text-muted").addClass("text-danger");
password_strength_message.addClass("hidden");
}
if ((key || (!key && old_password.val() && password_mismatch_message.text() !== password_not_same_as_old_password )) && common_conditions ) {
update_button.prop("disabled", false).css("cursor", "pointer");
}
else {
update_button.prop("disabled", true).css("cursor", "not-allowed");
}
},500)
)
window.test_password_strength = function() {
window.timout_password_strength = null;
var args = {
key: frappe.utils.get_url_arg("key") || "",
old_password: $("#old_password").val(),
new_password: $("#new_password").val()
key: key || "",
old_password: old_password.val(),
new_password: new_password.val()
}
if (!args.new_password) {
@ -195,9 +261,11 @@ frappe.ready(function() {
message.push(feedback.help_msg);
} else {
message.push("{{ _('Success! You are good to go 👍') }}");
message.push(password_strength_message_success);
}
}
password_mismatch_message.addClass('hidden');
strength_message.html(message.join(' ') || '').removeClass('hidden');
}