Merge branch 'develop' into virtual-doc-for-frappe-recorder
This commit is contained in:
commit
5a7348668a
79 changed files with 1082 additions and 408 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@
|
|||
{
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Small Text",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Subject",
|
||||
"reqd": 1
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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\`
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -252,6 +252,10 @@ class SessionBootFailed(ValidationError):
|
|||
http_status_code = 500
|
||||
|
||||
|
||||
class PrintFormatError(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class TooManyWritesError(Exception):
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 || "",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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})%}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
270
frappe/tests/test_dashboard_connections.py
Normal file
270
frappe/tests/test_dashboard_connections.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
|
@ -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.
|
|
|
@ -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'utilisateur,
|
|||
Check the Error Log for more information: {0},Consultez le journal des erreurs pour plus d'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'accès</b> à Google Drive pour autoriser l' <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},
|
||||
|
|
|
|||
|
|
|
@ -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},
|
||||
|
|
|
|||
|
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue