Merge remote-tracking branch 'upstream/develop' into fix-note-2
This commit is contained in:
commit
150d677a28
54 changed files with 365 additions and 263 deletions
|
|
@ -7,8 +7,8 @@ context("Folder Navigation", () => {
|
|||
|
||||
it("Adding Folders", () => {
|
||||
//Adding filter to go into the home folder
|
||||
cy.get(".filter-selector > .btn").findByText("1 filter").click();
|
||||
cy.findByRole("button", { name: "Clear Filters" }).click();
|
||||
cy.get(".filter-x-button").click();
|
||||
cy.click_filter_button();
|
||||
cy.get(".filter-action-buttons > .text-muted").findByText("+ Add a Filter").click();
|
||||
cy.get(".fieldname-select-area > .awesomplete > .form-control:last").type("Fol{enter}");
|
||||
cy.get(
|
||||
|
|
@ -47,9 +47,13 @@ context("Folder Navigation", () => {
|
|||
//Adding a file inside the Test Folder
|
||||
cy.findByRole("button", { name: "Add File" }).eq(0).click({ force: true });
|
||||
cy.get(".file-uploader").findByText("Link").click();
|
||||
cy.get(".input-group > .form-control").type(
|
||||
"https://wallpaperplay.com/walls/full/8/2/b/72402.jpg"
|
||||
);
|
||||
cy.get(".input-group > input.form-control:visible").as("upload_input");
|
||||
cy.get("@upload_input").type("https://wallpaperplay.com/walls/full/8/2/b/72402.jpg", {
|
||||
waitForAnimations: false,
|
||||
parseSpecialCharSequences: false,
|
||||
force: true,
|
||||
delay: 100,
|
||||
});
|
||||
cy.click_modal_primary_button("Upload");
|
||||
|
||||
//To check if the added file is present in the Test Folder
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ context("List Paging", () => {
|
|||
|
||||
it("test load more with count selection buttons", () => {
|
||||
cy.visit("/app/todo/view/report");
|
||||
cy.clear_filters();
|
||||
cy.get(".filter-x-button").click();
|
||||
|
||||
cy.get(".list-paging-area .list-count").should("contain.text", "20 of");
|
||||
cy.get(".list-paging-area .btn-more").click();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ context("List View", () => {
|
|||
});
|
||||
|
||||
it("List view check rows on drag", () => {
|
||||
cy.get(".filter-x-button").click();
|
||||
cy.get(".list-row-checkbox").then(($checkbox) => {
|
||||
cy.wrap($checkbox).first().trigger("mousedown");
|
||||
cy.get(".level.list-row").each(($ele) => {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ context("Sidebar", () => {
|
|||
);
|
||||
|
||||
//To check if there is no filter added to the listview
|
||||
cy.get(".filter-selector > .btn").should("contain", "Filter");
|
||||
cy.get(".filter-button").should("contain", "Filter");
|
||||
|
||||
//To add a filter to display data into the listview
|
||||
cy.get(".group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item").click();
|
||||
|
|
|
|||
|
|
@ -479,7 +479,7 @@ Cypress.Commands.add("click_listview_row_item_with_text", (text) => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add("click_filter_button", () => {
|
||||
cy.get(".filter-selector > .btn").click();
|
||||
cy.get(".filter-button").click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add("click_listview_primary_button", (btn_name) => {
|
||||
|
|
|
|||
|
|
@ -149,18 +149,26 @@ def get_permitted_and_not_permitted_links(doctype):
|
|||
return {"permitted_links": permitted_links, "not_permitted_links": not_permitted_links}
|
||||
|
||||
|
||||
def delete_contact_and_address(doctype, docname):
|
||||
def delete_contact_and_address(doctype: str, docname: str) -> None:
|
||||
for parenttype in ("Contact", "Address"):
|
||||
items = frappe.db.sql_list(
|
||||
"""select parent from `tabDynamic Link`
|
||||
where parenttype=%s and link_doctype=%s and link_name=%s""",
|
||||
(parenttype, doctype, docname),
|
||||
)
|
||||
|
||||
for name in items:
|
||||
for name in frappe.get_all(
|
||||
"Dynamic Link",
|
||||
filters={
|
||||
"parenttype": parenttype,
|
||||
"link_doctype": doctype,
|
||||
"link_name": docname,
|
||||
},
|
||||
pluck="parent",
|
||||
):
|
||||
doc = frappe.get_doc(parenttype, name)
|
||||
if len(doc.links) == 1:
|
||||
doc.delete()
|
||||
else:
|
||||
for link in doc.links:
|
||||
if link.link_doctype == doctype and link.link_name == docname:
|
||||
doc.remove(link)
|
||||
doc.save()
|
||||
break
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -510,7 +510,7 @@
|
|||
"label": "Disable Username/Password Login"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"default": "1",
|
||||
"description": "Allow users to log in without a password, using a login link sent to their email",
|
||||
"fieldname": "login_with_email_link",
|
||||
"fieldtype": "Check",
|
||||
|
|
|
|||
|
|
@ -114,9 +114,9 @@ frappe.ui.form.on("User", {
|
|||
return;
|
||||
}
|
||||
|
||||
function hasChanged(doc_attr, boot_attr) {
|
||||
return (doc_attr || boot_attr) && doc_attr !== boot_attr;
|
||||
}
|
||||
const hasChanged = (doc_attr, boot_attr) => {
|
||||
return doc_attr && boot_attr && doc_attr !== boot_attr;
|
||||
};
|
||||
|
||||
if (
|
||||
doc.name === frappe.session.user &&
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ class PropertySetter(Document):
|
|||
delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name)
|
||||
frappe.clear_cache(doctype=self.doc_type)
|
||||
|
||||
def on_trash(self):
|
||||
frappe.clear_cache(doctype=self.doc_type)
|
||||
|
||||
def validate_fieldtype_change(self):
|
||||
if self.property == "fieldtype" and self.field_name in not_allowed_fieldtype_change:
|
||||
frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name))
|
||||
|
|
|
|||
|
|
@ -153,6 +153,8 @@ class Workspace:
|
|||
return True
|
||||
if item_type == "dashboard":
|
||||
return True
|
||||
if item_type == "url":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ class Workspace(Document):
|
|||
def validate(self):
|
||||
if self.public and not is_workspace_manager() and not disable_saving_as_public():
|
||||
frappe.throw(_("You need to be Workspace Manager to edit this document"))
|
||||
validate_route_conflict(self.doctype, self.name)
|
||||
if self.has_value_changed("title"):
|
||||
validate_route_conflict(self.doctype, self.title)
|
||||
else:
|
||||
validate_route_conflict(self.doctype, self.name)
|
||||
|
||||
try:
|
||||
if not isinstance(loads(self.content), list):
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"field_order": [
|
||||
"type",
|
||||
"link_to",
|
||||
"url",
|
||||
"doc_view",
|
||||
"column_break_4",
|
||||
"label",
|
||||
|
|
@ -24,16 +25,16 @@
|
|||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "DocType\nReport\nPage\nDashboard",
|
||||
"options": "DocType\nReport\nPage\nDashboard\nURL",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type != \"URL\"",
|
||||
"fieldname": "link_to",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Link To",
|
||||
"options": "type",
|
||||
"reqd": 1
|
||||
"options": "type"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type == \"DocType\"",
|
||||
|
|
@ -94,12 +95,20 @@
|
|||
"fieldname": "format",
|
||||
"fieldtype": "Data",
|
||||
"label": "Format"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type == \"URL\"",
|
||||
"fieldname": "url",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "URL",
|
||||
"options": "URL"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-12 13:13:17.571324",
|
||||
"modified": "2023-04-19 13:32:31.005443",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Workspace Shortcut",
|
||||
|
|
|
|||
|
|
@ -147,35 +147,36 @@ def add_multiple(args=None):
|
|||
def close_all_assignments(doctype, name):
|
||||
assignments = frappe.get_all(
|
||||
"ToDo",
|
||||
fields=["allocated_to"],
|
||||
fields=["allocated_to", "name"],
|
||||
filters=dict(reference_type=doctype, reference_name=name, status=("!=", "Cancelled")),
|
||||
)
|
||||
if not assignments:
|
||||
return False
|
||||
|
||||
for assign_to in assignments:
|
||||
set_status(doctype, name, assign_to.allocated_to, status="Closed")
|
||||
set_status(doctype, name, todo=assign_to.name, assign_to=assign_to.allocated_to, status="Closed")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove(doctype, name, assign_to):
|
||||
return set_status(doctype, name, assign_to, status="Cancelled")
|
||||
return set_status(doctype, name, "", assign_to, status="Cancelled")
|
||||
|
||||
|
||||
def set_status(doctype, name, assign_to, status="Cancelled"):
|
||||
def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled"):
|
||||
"""remove from todo"""
|
||||
try:
|
||||
todo = frappe.db.get_value(
|
||||
"ToDo",
|
||||
{
|
||||
"reference_type": doctype,
|
||||
"reference_name": name,
|
||||
"allocated_to": assign_to,
|
||||
"status": ("!=", status),
|
||||
},
|
||||
)
|
||||
if not todo:
|
||||
todo = frappe.db.get_value(
|
||||
"ToDo",
|
||||
{
|
||||
"reference_type": doctype,
|
||||
"reference_name": name,
|
||||
"allocated_to": assign_to,
|
||||
"status": ("!=", status),
|
||||
},
|
||||
)
|
||||
if todo:
|
||||
todo = frappe.get_doc("ToDo", todo)
|
||||
todo.status = status
|
||||
|
|
@ -197,13 +198,17 @@ def clear(doctype, name):
|
|||
Clears assignments, return False if not assigned.
|
||||
"""
|
||||
assignments = frappe.get_all(
|
||||
"ToDo", fields=["allocated_to"], filters=dict(reference_type=doctype, reference_name=name)
|
||||
"ToDo",
|
||||
fields=["allocated_to", "name"],
|
||||
filters=dict(reference_type=doctype, reference_name=name),
|
||||
)
|
||||
if not assignments:
|
||||
return False
|
||||
|
||||
for assign_to in assignments:
|
||||
set_status(doctype, name, assign_to.allocated_to, "Cancelled")
|
||||
set_status(
|
||||
doctype, name, todo=assign_to.name, assign_to=assign_to.allocated_to, status="Cancelled"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ class SendMailContext:
|
|||
# Note: smtp session will have to be manually closed
|
||||
self.retain_smtp_session = bool(smtp_server_instance)
|
||||
|
||||
self.sent_to = [rec.recipient for rec in self.queue_doc.recipients if rec.is_main_sent()]
|
||||
self.sent_to = [rec.recipient for rec in self.queue_doc.recipients if rec.is_mail_sent()]
|
||||
|
||||
def __enter__(self):
|
||||
self.queue_doc.update_status(status="Sending", commit=True)
|
||||
|
|
@ -213,7 +213,6 @@ class SendMailContext:
|
|||
exceptions = [
|
||||
smtplib.SMTPServerDisconnected,
|
||||
smtplib.SMTPAuthenticationError,
|
||||
smtplib.SMTPRecipientsRefused,
|
||||
smtplib.SMTPConnectError,
|
||||
smtplib.SMTPHeloError,
|
||||
JobTimeoutException,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class EmailQueueRecipient(Document):
|
|||
def is_mail_to_be_sent(self):
|
||||
return self.status == "Not Sent"
|
||||
|
||||
def is_main_sent(self):
|
||||
def is_mail_sent(self):
|
||||
return self.status == "Sent"
|
||||
|
||||
def update_db(self, commit=False, **kwargs):
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import json
|
|||
import poplib
|
||||
import re
|
||||
import time
|
||||
from contextlib import suppress
|
||||
from email.header import decode_header
|
||||
|
||||
import _socket
|
||||
|
|
@ -37,7 +38,7 @@ from frappe.utils.html_utils import clean_email_html
|
|||
from frappe.utils.user import is_system_user
|
||||
|
||||
# fix due to a python bug in poplib that limits it to 2048
|
||||
poplib._MAXLINE = 20480
|
||||
poplib._MAXLINE = 1_00_000
|
||||
|
||||
THREAD_ID_PATTERN = re.compile(r"(?<=\[)[\w/-]+")
|
||||
WORDS_PATTERN = re.compile(r"\w+")
|
||||
|
|
@ -51,10 +52,6 @@ class EmailTimeoutError(frappe.ValidationError):
|
|||
pass
|
||||
|
||||
|
||||
class TotalSizeExceededError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class LoginLimitExceeded(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
|
@ -67,26 +64,11 @@ class EmailServer:
|
|||
"""Wrapper for POP server to pull emails."""
|
||||
|
||||
def __init__(self, args=None):
|
||||
self.setup(args)
|
||||
|
||||
def setup(self, args=None):
|
||||
# overrride
|
||||
self.settings = args or frappe._dict()
|
||||
|
||||
def check_mails(self):
|
||||
# overrride
|
||||
return True
|
||||
|
||||
def process_message(self, mail):
|
||||
# overrride
|
||||
pass
|
||||
|
||||
def connect(self):
|
||||
"""Connect to **Email Account**."""
|
||||
if cint(self.settings.use_imap):
|
||||
return self.connect_imap()
|
||||
else:
|
||||
return self.connect_pop()
|
||||
return self.connect_imap() if cint(self.settings.use_imap) else self.connect_pop()
|
||||
|
||||
def connect_imap(self):
|
||||
"""Connect to IMAP"""
|
||||
|
|
@ -150,7 +132,6 @@ class EmailServer:
|
|||
return True
|
||||
|
||||
except _socket.error:
|
||||
# log performs rollback and logs error in Error Log
|
||||
frappe.log_error("POP: Unable to connect")
|
||||
|
||||
# Invalid mail server -- due to refusing connection
|
||||
|
|
@ -177,66 +158,33 @@ class EmailServer:
|
|||
return
|
||||
|
||||
def get_messages(self, folder="INBOX"):
|
||||
"""Returns new email messages in a list."""
|
||||
if not (self.check_mails() or self.connect()):
|
||||
return []
|
||||
"""Returns new email messages."""
|
||||
|
||||
frappe.db.commit()
|
||||
self.latest_messages = []
|
||||
self.seen_status = {}
|
||||
self.uid_reindexed = False
|
||||
|
||||
uid_list = []
|
||||
email_list = self.get_new_mails(folder)
|
||||
|
||||
try:
|
||||
# track if errors arised
|
||||
self.errors = False
|
||||
self.latest_messages = []
|
||||
self.seen_status = {}
|
||||
self.uid_reindexed = False
|
||||
|
||||
uid_list = email_list = self.get_new_mails(folder)
|
||||
|
||||
if not email_list:
|
||||
return
|
||||
|
||||
num = num_copy = len(email_list)
|
||||
|
||||
# WARNING: Hard coded max no. of messages to be popped
|
||||
if num > 50:
|
||||
num = 50
|
||||
|
||||
# size limits
|
||||
self.total_size = 0
|
||||
self.max_email_size = cint(frappe.local.conf.get("max_email_size"))
|
||||
self.max_total_size = 5 * self.max_email_size
|
||||
|
||||
for i, message_meta in enumerate(email_list[:num]):
|
||||
try:
|
||||
self.retrieve_message(message_meta, i + 1)
|
||||
except (TotalSizeExceededError, EmailTimeoutError, LoginLimitExceeded):
|
||||
break
|
||||
# WARNING: Mark as read - message number 101 onwards from the pop list
|
||||
# This is to avoid having too many messages entering the system
|
||||
num = num_copy
|
||||
if not cint(self.settings.use_imap):
|
||||
if num > 100 and not self.errors:
|
||||
for m in range(101, num + 1):
|
||||
self.pop.dele(m)
|
||||
|
||||
except Exception as e:
|
||||
if not self.has_login_limit_exceeded(e):
|
||||
raise
|
||||
for i, uid in enumerate(email_list[:100]):
|
||||
try:
|
||||
self.retrieve_message(uid, i + 1)
|
||||
except (EmailTimeoutError, LoginLimitExceeded):
|
||||
# get whatever messages were retrieved
|
||||
break
|
||||
|
||||
out = {"latest_messages": self.latest_messages}
|
||||
if self.settings.use_imap:
|
||||
out.update(
|
||||
{"uid_list": uid_list, "seen_status": self.seen_status, "uid_reindexed": self.uid_reindexed}
|
||||
{"uid_list": email_list, "seen_status": self.seen_status, "uid_reindexed": self.uid_reindexed}
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
def get_new_mails(self, folder):
|
||||
"""Return list of new mails"""
|
||||
email_list = []
|
||||
if cint(self.settings.use_imap):
|
||||
email_list = []
|
||||
self.check_imap_uidvalidity(folder)
|
||||
|
||||
readonly = False if self.settings.email_sync_rule == "UNSEEN" else True
|
||||
|
|
@ -294,9 +242,6 @@ class EmailServer:
|
|||
self.settings.email_sync_rule = f"UID {from_uid}:{uidnext}"
|
||||
self.uid_reindexed = True
|
||||
|
||||
elif uid_validity == current_uid_validity:
|
||||
return
|
||||
|
||||
def parse_imap_response(self, cmd, response):
|
||||
pattern = rf"(?<={cmd} )[0-9]*"
|
||||
match = re.search(pattern, response.decode("utf-8"), re.U | re.I)
|
||||
|
|
@ -306,49 +251,28 @@ class EmailServer:
|
|||
else:
|
||||
return None
|
||||
|
||||
def retrieve_message(self, message_meta, msg_num=None):
|
||||
incoming_mail = None
|
||||
def retrieve_message(self, uid, msg_num):
|
||||
try:
|
||||
self.validate_message_limits(message_meta)
|
||||
|
||||
if cint(self.settings.use_imap):
|
||||
status, message = self.imap.uid("fetch", message_meta, "(BODY.PEEK[] BODY.PEEK[HEADER] FLAGS)")
|
||||
status, message = self.imap.uid("fetch", uid, "(BODY.PEEK[] BODY.PEEK[HEADER] FLAGS)")
|
||||
raw = message[0]
|
||||
|
||||
self.get_email_seen_status(message_meta, raw[0])
|
||||
self.get_email_seen_status(uid, raw[0])
|
||||
self.latest_messages.append(raw[1])
|
||||
else:
|
||||
msg = self.pop.retr(msg_num)
|
||||
self.latest_messages.append(b"\n".join(msg[1]))
|
||||
except (TotalSizeExceededError, EmailTimeoutError):
|
||||
except EmailTimeoutError:
|
||||
# propagate this error to break the loop
|
||||
self.errors = True
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
if self.has_login_limit_exceeded(e):
|
||||
self.errors = True
|
||||
raise LoginLimitExceeded(e)
|
||||
|
||||
else:
|
||||
# log performs rollback and logs error in Error Log
|
||||
frappe.log_error("Unable to fetch email", self.make_error_msg(msg_num, incoming_mail))
|
||||
self.errors = True
|
||||
frappe.db.rollback()
|
||||
frappe.log_error("Unable to fetch email", self.make_error_msg(uid, msg_num))
|
||||
|
||||
if not cint(self.settings.use_imap):
|
||||
self.pop.dele(msg_num)
|
||||
else:
|
||||
# mark as seen if email sync rule is UNSEEN (syncing only unseen mails)
|
||||
if self.settings.email_sync_rule == "UNSEEN":
|
||||
self.imap.uid("STORE", message_meta, "+FLAGS", "(\\SEEN)")
|
||||
else:
|
||||
if not cint(self.settings.use_imap):
|
||||
self.pop.dele(msg_num)
|
||||
else:
|
||||
# mark as seen if email sync rule is UNSEEN (syncing only unseen mails)
|
||||
if self.settings.email_sync_rule == "UNSEEN":
|
||||
self.imap.uid("STORE", message_meta, "+FLAGS", "(\\SEEN)")
|
||||
self._post_retrieve_cleanup(uid, msg_num)
|
||||
|
||||
def get_email_seen_status(self, uid, flag_string):
|
||||
"""parse the email FLAGS response"""
|
||||
|
|
@ -368,6 +292,15 @@ class EmailServer:
|
|||
def has_login_limit_exceeded(self, e):
|
||||
return "-ERR Exceeded the login limit" in strip(cstr(e))
|
||||
|
||||
def _post_retrieve_cleanup(self, uid, msg_num):
|
||||
with suppress(Exception):
|
||||
if not cint(self.settings.use_imap):
|
||||
self.pop.dele(msg_num)
|
||||
else:
|
||||
# mark as seen if email sync rule is UNSEEN (syncing only unseen mails)
|
||||
if self.settings.email_sync_rule == "UNSEEN":
|
||||
self.imap.uid("STORE", uid, "+FLAGS", "(\\SEEN)")
|
||||
|
||||
def is_temporary_system_problem(self, e):
|
||||
messages = (
|
||||
"-ERR [SYS/TEMP] Temporary system problem. Please try again later.",
|
||||
|
|
@ -378,36 +311,28 @@ class EmailServer:
|
|||
return True
|
||||
return False
|
||||
|
||||
def validate_message_limits(self, message_meta):
|
||||
# throttle based on email size
|
||||
if not self.max_email_size:
|
||||
return
|
||||
def make_error_msg(self, uid, msg_num):
|
||||
partial_mail = None
|
||||
traceback = frappe.get_traceback(with_context=True)
|
||||
with suppress(Exception):
|
||||
# retrieve headers
|
||||
if not cint(self.settings.use_imap):
|
||||
headers = b"\n".join(self.pop.top(msg_num, 5)[1])
|
||||
else:
|
||||
headers = self.imap.uid("fetch", uid, "(BODY.PEEK[HEADER])")[1][0][1]
|
||||
|
||||
m, size = message_meta.split()
|
||||
size = cint(size)
|
||||
partial_mail = Email(headers)
|
||||
|
||||
if size < self.max_email_size:
|
||||
self.total_size += size
|
||||
if self.total_size > self.max_total_size:
|
||||
raise TotalSizeExceededError
|
||||
else:
|
||||
raise EmailSizeExceededError
|
||||
|
||||
def make_error_msg(self, msg_num, incoming_mail):
|
||||
error_msg = "Error in retrieving email."
|
||||
if not incoming_mail:
|
||||
try:
|
||||
# retrieve headers
|
||||
incoming_mail = Email(b"\n".join(self.pop.top(msg_num, 5)[1]))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if incoming_mail:
|
||||
error_msg += "\nDate: {date}\nFrom: {from_email}\nSubject: {subject}\n".format(
|
||||
date=incoming_mail.date, from_email=incoming_mail.from_email, subject=incoming_mail.subject
|
||||
if partial_mail:
|
||||
return (
|
||||
"\nDate: {date}\nFrom: {from_email}\nSubject: {subject}\n\n\nTraceback: \n{traceback}".format(
|
||||
date=partial_mail.date,
|
||||
from_email=partial_mail.from_email,
|
||||
subject=partial_mail.subject,
|
||||
traceback=traceback,
|
||||
)
|
||||
)
|
||||
|
||||
return error_msg
|
||||
return traceback
|
||||
|
||||
def update_flag(self, folder, uid_list=None):
|
||||
"""set all uids mails the flag as seen"""
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@ on_logout = (
|
|||
"frappe.core.doctype.session_default_settings.session_default_settings.clear_session_defaults"
|
||||
)
|
||||
|
||||
# PDF
|
||||
pdf_header_html = "frappe.utils.pdf.pdf_header_html"
|
||||
pdf_body_html = "frappe.utils.pdf.pdf_body_html"
|
||||
pdf_footer_html = "frappe.utils.pdf.pdf_footer_html"
|
||||
|
||||
# permissions
|
||||
|
||||
permission_query_conditions = {
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ def authorize_access(g_calendar, reauthorize=None):
|
|||
"""
|
||||
google_settings = frappe.get_doc("Google Settings")
|
||||
google_calendar = frappe.get_doc("Google Calendar", g_calendar)
|
||||
google_calendar.check_permission("write")
|
||||
|
||||
redirect_uri = (
|
||||
get_request_site_address(True)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "format:GC-{email_id}",
|
||||
"creation": "2019-06-14 00:09:39.441961",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -97,10 +98,12 @@
|
|||
"label": "Push to Google Contacts"
|
||||
}
|
||||
],
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"links": [],
|
||||
"modified": "2023-03-30 11:25:48.832384",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Google Contacts",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
@ -116,17 +119,14 @@
|
|||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"if_owner": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -36,10 +36,10 @@ def authorize_access(g_contact, reauthorize=False, code=None):
|
|||
If no Authorization code get it from Google and then request for Refresh Token.
|
||||
Google Contact Name is set to flags to set_value after Authorization Code is obtained.
|
||||
"""
|
||||
contact = frappe.get_doc("Google Contacts", g_contact)
|
||||
contact.check_permission("write")
|
||||
|
||||
oauth_code = (
|
||||
frappe.db.get_value("Google Contacts", g_contact, "authorization_code") if not code else code
|
||||
)
|
||||
oauth_code = code or contact.get_password("authorization_code")
|
||||
oauth_obj = GoogleOAuth("contacts")
|
||||
|
||||
if not oauth_code or reauthorize:
|
||||
|
|
@ -51,11 +51,9 @@ def authorize_access(g_contact, reauthorize=False, code=None):
|
|||
)
|
||||
|
||||
r = oauth_obj.authorize(oauth_code)
|
||||
frappe.db.set_value(
|
||||
"Google Contacts",
|
||||
g_contact,
|
||||
{"authorization_code": oauth_code, "refresh_token": r.get("refresh_token")},
|
||||
)
|
||||
contact.authorization_code = oauth_code
|
||||
contact.refresh_token = r.get("refresh_token")
|
||||
contact.save()
|
||||
|
||||
|
||||
def get_google_contacts_object(g_contact):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# Copyright (c) 2023, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestGoogleContacts(FrappeTestCase):
|
||||
pass
|
||||
|
|
@ -160,7 +160,7 @@ def sync_customizations_for_doctype(data: dict, folder: str, filename: str = "")
|
|||
|
||||
if not frappe.db.exists("DocType", doctype):
|
||||
print(_("DocType {0} does not exist.").format(doctype))
|
||||
print(_("Skipping fixture syncing for doctyoe {0} from file {1} ").format(doctype, filename))
|
||||
print(_("Skipping fixture syncing for doctype {0} from file {1}").format(doctype, filename))
|
||||
return
|
||||
|
||||
if data["custom_fields"]:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ from frappe.desk.doctype.notification_log.notification_log import make_notificat
|
|||
|
||||
|
||||
def execute():
|
||||
if not frappe.get_value("Email Account", {"auth_method": "OAuth"}):
|
||||
if frappe.get_all(
|
||||
"Email Account", {"auth_method": "OAuth", "connected_user": ["is", "set"]}, limit=1
|
||||
):
|
||||
return
|
||||
|
||||
# Setting awaiting password to 1 for email accounts where Oauth is enabled.
|
||||
|
|
|
|||
|
|
@ -237,8 +237,14 @@
|
|||
<path d="M2.5 3.5h2m7 9h2m-10-6h6m-3 3h6" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-filter">
|
||||
<path d="M2 4h12M4 8h8m-5.5 4h3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-filter">
|
||||
<path stroke-width="1.2" d="M3.68016 3L15.4502 3C16.1 3 16.4787 3.73367 16.1023 4.26337L11.6585 10.5177C11.5383 10.6869 11.4737 10.8893 11.4737 11.0969L11.4737 16.4053C11.4737 16.6516 11.1934 16.7929 10.9954 16.6466L8.72152 14.9665C8.46635 14.7779 8.31579 14.4795 8.31579 14.1622L8.31579 11.1327C8.31579 10.9031 8.2368 10.6805 8.09208 10.5023L3.05913 4.3043C2.63456 3.78145 3.00664 3 3.68016 3Z" stroke="var(--icon-stroke)" stroke-linecap="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-filter-x">
|
||||
<path stroke-width="1.2" d="M8.5 3L3.66449 3.00002C2.99369 3.00002 2.62075 3.77596 3.0398 4.29977L8.15768 10.6971C8.29953 10.8744 8.37681 11.0947 8.37681 11.3218L8.37681 14.4565C8.37681 14.7713 8.525 15.0677 8.77681 15.2565L11.0852 16.9878C11.283 17.1362 11.5652 16.9951 11.5652 16.7478L11.5652 11.3742C11.5652 11.1155 11.6654 10.8669 11.8448 10.6806L12.5 10" stroke="var(--icon-stroke)" stroke-linecap="round"/>
|
||||
<path stroke-width="1.2" d="M11 3L16 8" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path stroke-width="1.2" d="M16 3L11 8" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-list">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 112 KiB |
|
|
@ -24,8 +24,8 @@
|
|||
</div>
|
||||
|
||||
<div class="flex config-area">
|
||||
<label v-if="is_optimizable" class="frappe-checkbox"><input type="checkbox" :checked="optimize" @change="emit('toggle_optimize')">Optimize</label>
|
||||
<label class="frappe-checkbox"><input type="checkbox" :checked="file.private" @change="emit('toggle_private')">Private</label>
|
||||
<label v-if="is_optimizable" class="frappe-checkbox"><input type="checkbox" :checked="optimize" @change="emit('toggle_optimize')">{{ __('Optimize') }}</label>
|
||||
<label class="frappe-checkbox"><input type="checkbox" :checked="file.private" @change="emit('toggle_private')">{{ __('Private') }}</label>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="file.error_message" class="file-error text-danger">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlI
|
|||
else {
|
||||
let value = this.get_input_value();
|
||||
this.parse_validate_and_set_in_model(value, e);
|
||||
this.refresh();
|
||||
}
|
||||
};
|
||||
// convert to number format on focusout since focus converts it to flt.
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ frappe.form.formatters = {
|
|||
},
|
||||
Data: function (value, df) {
|
||||
if (df && df.options == "URL") {
|
||||
if (!value) return;
|
||||
return `<a href="${value}" title="Open Link" target="_blank">${value}</a>`;
|
||||
}
|
||||
value = value == null ? "" : value;
|
||||
|
|
|
|||
|
|
@ -831,22 +831,31 @@ class FilterArea {
|
|||
|
||||
make_filter_list() {
|
||||
$(`<div class="filter-selector">
|
||||
<button class="btn btn-default btn-sm filter-button">
|
||||
<span class="filter-icon">
|
||||
${frappe.utils.icon("filter")}
|
||||
</span>
|
||||
<span class="button-label hidden-xs">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default btn-sm filter-button">
|
||||
<span class="filter-icon">
|
||||
${frappe.utils.icon("filter")}
|
||||
</span>
|
||||
<span class="button-label hidden-xs">
|
||||
${__("Filter")}
|
||||
<span>
|
||||
</button>
|
||||
<span>
|
||||
</button>
|
||||
<button class="btn btn-default btn-sm filter-x-button" title="${__("Clear all filters")}">
|
||||
<span class="filter-icon">
|
||||
${frappe.utils.icon("filter-x")}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>`).appendTo(this.$filter_list_wrapper);
|
||||
|
||||
this.filter_button = this.$filter_list_wrapper.find(".filter-button");
|
||||
this.filter_x_button = this.$filter_list_wrapper.find(".filter-x-button");
|
||||
this.filter_list = new frappe.ui.FilterGroup({
|
||||
base_list: this.list_view,
|
||||
parent: this.$filter_list_wrapper,
|
||||
doctype: this.list_view.doctype,
|
||||
filter_button: this.filter_button,
|
||||
filter_x_button: this.filter_x_button,
|
||||
default_filters: [],
|
||||
on_change: () => this.refresh_list_view(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -256,19 +256,16 @@ frappe.views.ListSidebar = class ListSidebar {
|
|||
this.insights_banner.remove();
|
||||
}
|
||||
|
||||
const message = "Get more insights from your data with Frappe Insights.";
|
||||
const message = "Get more insights with";
|
||||
const link = "https://frappe.io/s/insights";
|
||||
const cta = "Get Frappe Insights";
|
||||
const cta = "Frappe Insights";
|
||||
|
||||
this.insights_banner = $(`
|
||||
<div style="position: relative;">
|
||||
<div class="">
|
||||
${message}
|
||||
<div class="pr-3">
|
||||
${message} <a href="${link}" target="_blank" style="color: var(--primary-color)">${cta} → </a>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<a href="${link}" target="_blank" style="color: var(--primary-color)">${cta} -> </a>
|
||||
</div>
|
||||
<div style="position: absolute; top: 0px; right: 0px; cursor: pointer;" title="Dismiss"
|
||||
<div style="position: absolute; top: -1px; right: -4px; cursor: pointer;" title="Dismiss"
|
||||
onclick="localStorage.setItem('show_insights_banner', 'false') || this.parentElement.remove()">
|
||||
<svg class="icon icon-sm" style="">
|
||||
<use class="" href="#icon-close"></use>
|
||||
|
|
|
|||
|
|
@ -348,11 +348,9 @@ $.extend(frappe.model, {
|
|||
},
|
||||
|
||||
unscrub: function (txt) {
|
||||
return __(txt || "")
|
||||
.replace(/-|_/g, " ")
|
||||
.replace(/\w*/g, function (keywords) {
|
||||
return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase();
|
||||
});
|
||||
return (txt || "").replace(/-|_/g, " ").replace(/\w*/g, function (keywords) {
|
||||
return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase();
|
||||
});
|
||||
},
|
||||
|
||||
can_create: function (doctype) {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
<div class="static-area ellipsis">{{ __("Query") }}</div>
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
<div class="static-area ellipsis text-right">{{ __("Duration (ms)") }}"</div>
|
||||
<div class="static-area ellipsis text-right">{{ __("Duration (ms)") }}</div>
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
<div class="static-area ellipsis text-right">{{ __("Exact Copies") }}</div>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ frappe.RoleEditor = class {
|
|||
const $body = $(this.perm_dialog.body);
|
||||
if (!permissions.length) {
|
||||
$body.append(`<div class="text-muted text-center padding">
|
||||
${__("{0} role does not have permission on any doctype", [role])}
|
||||
${__("{0} role does not have permission on any doctype", [__(role)])}
|
||||
</div>`);
|
||||
} else {
|
||||
$body.append(`
|
||||
|
|
@ -68,7 +68,7 @@ frappe.RoleEditor = class {
|
|||
<tr>
|
||||
<th> ${__("Document Type")} </th>
|
||||
<th> ${__("Level")} </th>
|
||||
${frappe.perm.rights.map((p) => `<th> ${frappe.unscrub(p)}</th>`).join("")}
|
||||
${frappe.perm.rights.map((p) => `<th> ${__(frappe.unscrub(p))}</th>`).join("")}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
|
|
@ -77,7 +77,7 @@ frappe.RoleEditor = class {
|
|||
permissions.forEach((perm) => {
|
||||
$body.find("tbody").append(`
|
||||
<tr>
|
||||
<td>${perm.parent}</td>
|
||||
<td>${__(perm.parent)}</td>
|
||||
<td>${perm.permlevel}</td>
|
||||
${frappe.perm.rights
|
||||
.map(
|
||||
|
|
@ -91,7 +91,7 @@ frappe.RoleEditor = class {
|
|||
`);
|
||||
});
|
||||
}
|
||||
this.perm_dialog.set_title(role);
|
||||
this.perm_dialog.set_title(__(role));
|
||||
this.perm_dialog.show();
|
||||
});
|
||||
}
|
||||
|
|
@ -102,8 +102,10 @@ frappe.RoleEditor = class {
|
|||
|
||||
this.perm_dialog.$wrapper
|
||||
.find(".modal-dialog")
|
||||
.css("width", "1200px")
|
||||
.css("max-width", "80vw");
|
||||
.css("width", "auto")
|
||||
.css("max-width", "1200px");
|
||||
|
||||
this.perm_dialog.$wrapper.find(".modal-body").css("overflow", "overlay");
|
||||
}
|
||||
show() {
|
||||
this.reset();
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ frappe.router = {
|
|||
? meta.default_view
|
||||
: null
|
||||
);
|
||||
} else if (route[1] && route[1] !== "view" && !route[2]) {
|
||||
} else if (route[1] && route[1] !== "view") {
|
||||
let docname = route[1];
|
||||
if (route.length > 2) {
|
||||
docname = route.slice(1).join("/");
|
||||
|
|
|
|||
|
|
@ -14,9 +14,31 @@ frappe.ui.FilterGroup = class {
|
|||
|
||||
make_popover() {
|
||||
this.init_filter_popover();
|
||||
this.set_clear_all_filters_event();
|
||||
this.set_popover_events();
|
||||
}
|
||||
|
||||
set_clear_all_filters_event() {
|
||||
if (!this.filter_x_button) return;
|
||||
|
||||
this.filter_x_button.on("click", () => {
|
||||
this.toggle_empty_filters(true);
|
||||
if (typeof this.base_list !== "undefined") {
|
||||
// It's a list view. Clear all the filters, also the ones in the
|
||||
// FilterArea outside this FilterGroup
|
||||
this.base_list.filter_area.clear();
|
||||
} else {
|
||||
// Not a list view, just clear the filters in this FilterGroup
|
||||
this.clear_filters();
|
||||
}
|
||||
this.update_filter_button();
|
||||
});
|
||||
}
|
||||
|
||||
hide_popover() {
|
||||
this.filter_button.popover("hide");
|
||||
}
|
||||
|
||||
init_filter_popover() {
|
||||
this.filter_button.popover({
|
||||
content: this.get_filter_area_template(),
|
||||
|
|
@ -54,7 +76,7 @@ frappe.ui.FilterGroup = class {
|
|||
!$(e.target).is(this.filter_button) &&
|
||||
!in_datepicker
|
||||
) {
|
||||
this.wrapper && this.filter_button.popover("hide");
|
||||
this.wrapper && this.hide_popover();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -85,7 +107,7 @@ frappe.ui.FilterGroup = class {
|
|||
// REDESIGN-TODO: (Temporary) Review and find best solution for this
|
||||
frappe.router.on("change", () => {
|
||||
if (this.wrapper && this.wrapper.is(":visible")) {
|
||||
this.filter_button.popover("hide");
|
||||
this.hide_popover();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -130,11 +152,10 @@ frappe.ui.FilterGroup = class {
|
|||
this.toggle_empty_filters(true);
|
||||
this.clear_filters();
|
||||
this.on_change();
|
||||
this.hide_popover();
|
||||
});
|
||||
|
||||
this.wrapper.find(".apply-filters").on("click", () => {
|
||||
this.filter_button.popover("hide");
|
||||
});
|
||||
this.wrapper.find(".apply-filters").on("click", () => this.hide_popover());
|
||||
}
|
||||
|
||||
add_filters(filters) {
|
||||
|
|
|
|||
|
|
@ -137,11 +137,13 @@ frappe.ui.toolbar.Toolbar = class {
|
|||
__("Generate Tracking URL")
|
||||
);
|
||||
|
||||
if (frappe.perm.has_perm("RQ Job")) {
|
||||
frappe.search.utils.make_function_searchable(function () {
|
||||
frappe.set_route("List", "RQ Job");
|
||||
}, __("Background Jobs"));
|
||||
}
|
||||
frappe.model.with_doctype("RQ Job").then(() => {
|
||||
if (frappe.perm.has_perm("RQ Job", 0, "read")) {
|
||||
frappe.search.utils.make_function_searchable(function () {
|
||||
frappe.set_route("List", "RQ Job");
|
||||
}, __("Background Jobs"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@ frappe.views.CommunicationComposer = class {
|
|||
await this.dialog.set_value(fieldname, this[fieldname] || "");
|
||||
}
|
||||
|
||||
const subject = frappe.utils.html2text(this.subject) || "";
|
||||
const subject = this.subject ? frappe.utils.html2text(this.subject) : "";
|
||||
await this.dialog.set_value("subject", subject);
|
||||
|
||||
await this.set_values_from_last_edited_communication();
|
||||
|
|
|
|||
|
|
@ -1424,9 +1424,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
const applied_filters = this.get_filter_values();
|
||||
return Object.keys(applied_filters)
|
||||
.map((fieldname) => {
|
||||
const label = frappe.query_report.get_filter(fieldname).df.label;
|
||||
const docfield = frappe.query_report.get_filter(fieldname).df;
|
||||
const value = applied_filters[fieldname];
|
||||
return `<h6>${__(label)}: ${value}</h6>`;
|
||||
return `<h6>${__(docfield.label)}: ${frappe.format(value, docfield)}</h6>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1349,9 +1349,8 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
.map((f) => {
|
||||
const [doctype, fieldname, condition, value] = f;
|
||||
if (condition !== "=") return "";
|
||||
|
||||
const label = frappe.meta.get_label(doctype, fieldname);
|
||||
return `<h6>${__(label)}: ${value}</h6>`;
|
||||
const docfield = frappe.meta.get_docfield(doctype, fieldname);
|
||||
return `<h6>${__(docfield.label)}: ${frappe.format(value, docfield)}</h6>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ frappe.views.Workspace = class Workspace {
|
|||
}
|
||||
|
||||
if (
|
||||
sidebar_section.find("sidebar-item-container").length &&
|
||||
sidebar_section.find(".sidebar-item-container").length &&
|
||||
sidebar_section.find("> [item-is-hidden='0']").length == 0
|
||||
) {
|
||||
sidebar_section.addClass("hidden show-in-edit-mode");
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export default class ShortcutWidget extends Widget {
|
|||
restrict_to_domain: this.restrict_to_domain,
|
||||
stats_filter: this.stats_filter,
|
||||
type: this.type,
|
||||
url: this.url,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +46,16 @@ export default class ShortcutWidget extends Widget {
|
|||
frappe.open_in_new_tab = true;
|
||||
}
|
||||
|
||||
if (this.type == "URL") {
|
||||
if (frappe.open_in_new_tab) {
|
||||
window.open(this.url, "_blank");
|
||||
frappe.open_in_new_tab = false;
|
||||
} else {
|
||||
window.location.href = this.url;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.set_route(route);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,7 +350,7 @@ class ShortcutDialog extends WidgetDialog {
|
|||
fieldname: "type",
|
||||
label: "Type",
|
||||
reqd: 1,
|
||||
options: "DocType\nReport\nPage\nDashboard",
|
||||
options: "DocType\nReport\nPage\nDashboard\nURL",
|
||||
onchange: () => {
|
||||
if (this.dialog.get_value("type") == "DocType") {
|
||||
this.dialog.fields_dict.link_to.get_query = () => {
|
||||
|
|
@ -379,7 +379,6 @@ class ShortcutDialog extends WidgetDialog {
|
|||
fieldtype: "Dynamic Link",
|
||||
fieldname: "link_to",
|
||||
label: "Link To",
|
||||
reqd: 1,
|
||||
options: "type",
|
||||
onchange: () => {
|
||||
const doctype = this.dialog.get_value("link_to");
|
||||
|
|
@ -404,6 +403,17 @@ class ShortcutDialog extends WidgetDialog {
|
|||
this.hide_filters();
|
||||
}
|
||||
},
|
||||
depends_on: (s) => s.type != "URL",
|
||||
mandatory_depends_on: (s) => s.type != "URL",
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "url",
|
||||
label: "URL",
|
||||
options: "URL",
|
||||
default: "",
|
||||
depends_on: (s) => s.type == "URL",
|
||||
mandatory_depends_on: (s) => s.type == "URL",
|
||||
},
|
||||
{
|
||||
fieldtype: "Select",
|
||||
|
|
@ -500,6 +510,19 @@ class ShortcutDialog extends WidgetDialog {
|
|||
|
||||
data.label = data.label ? data.label : frappe.model.unscrub(data.link_to);
|
||||
|
||||
if (data.url) {
|
||||
!validate_url(data.url) &&
|
||||
frappe.throw({
|
||||
message: __("<b>{0}</b> is not a valid URL", [data.url]),
|
||||
title: __("Invalid URL"),
|
||||
indicator: "red",
|
||||
});
|
||||
|
||||
if (!data.label) {
|
||||
data.label = "No Label (URL)";
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ onMounted(() => {
|
|||
margin-top: auto;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.preview-control >>> .form-control {
|
||||
.preview-control :deep(.form-control) {
|
||||
background: var(--control-bg-on-gray);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ watch(print_format, () => (store.dirty.value = true), { deep: true });
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.control-font >>> .frappe-control[data-fieldname="font"] label {
|
||||
.control-font :deep(.frappe-control[data-fieldname="font"] label) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
.filter-icon.active {
|
||||
use {
|
||||
stroke: var(--text-on-blue);
|
||||
}
|
||||
--icon-stroke: var(--text-on-blue);
|
||||
}
|
||||
|
||||
.filter-popover {
|
||||
|
|
|
|||
|
|
@ -378,13 +378,14 @@ input.list-check-all {
|
|||
padding: 0 var(--padding-xs);
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
margin: 5px;
|
||||
// padding: 4px 8px;
|
||||
.filter-selector .btn-group {
|
||||
margin: var(--margin-xs);
|
||||
}
|
||||
|
||||
.filter-button.btn-primary-light {
|
||||
color: var(--text-on-blue);
|
||||
outline: 1px solid var(--bg-dark-blue);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sort-selector {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ def can_subscribe_doctype(doctype: str) -> bool:
|
|||
def get_user_info():
|
||||
return {
|
||||
"user": frappe.session.user,
|
||||
"user_type": frappe.session.user_type,
|
||||
"user_type": frappe.session.data.user_type,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2290,8 +2290,8 @@ Setup Auto Email,Einstellungen Auto E-Mail,
|
|||
Setup Complete,Einrichtung abgeschlossen,
|
||||
Setup Notifications based on various criteria.,Setup Benachrichtigungen basierend auf verschiedenen Kriterien.,
|
||||
Setup Reports to be emailed at regular intervals,Berichte regelmäßig per E-Mail senden,
|
||||
"Setup of top navigation bar, footer and logo.","Einrichten der oberen Navigationsleiste, der Fußzeile und des Logos",
|
||||
Share,Aktie,
|
||||
"Setup of top navigation bar, footer and logo.","Einrichten der oberen Navigationsleiste, der Fußzeile und des Logos",
|
||||
Share,Freigeben,
|
||||
Share URL,URL teilen,
|
||||
Share With,Freigeben für,
|
||||
Share this document with,Dieses Dokument teilen mit,
|
||||
|
|
@ -4840,3 +4840,4 @@ Non-numeric,Nicht-numerische,
|
|||
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,
|
||||
|
|
|
|||
|
|
|
@ -23,6 +23,31 @@ PDF_CONTENT_ERRORS = [
|
|||
]
|
||||
|
||||
|
||||
def pdf_header_html(soup, head, content, styles, html_id, css):
|
||||
return frappe.render_template(
|
||||
"templates/print_formats/pdf_header_footer.html",
|
||||
{
|
||||
"head": head,
|
||||
"content": content,
|
||||
"styles": styles,
|
||||
"html_id": html_id,
|
||||
"css": css,
|
||||
"lang": frappe.local.lang,
|
||||
"layout_direction": "rtl" if is_rtl() else "ltr",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def pdf_body_html(template, args, **kwargs):
|
||||
return template.render(args, filters={"len": len})
|
||||
|
||||
|
||||
def pdf_footer_html(soup, head, content, styles, html_id, css):
|
||||
return pdf_header_html(
|
||||
soup=soup, head=head, content=content, styles=styles, html_id=html_id, css=css
|
||||
)
|
||||
|
||||
|
||||
def get_pdf(html, options=None, output: PdfWriter | None = None):
|
||||
html = scrub_urls(html)
|
||||
html, options = prepare_options(html, options)
|
||||
|
|
@ -196,17 +221,15 @@ def prepare_header_footer(soup):
|
|||
tag.extract()
|
||||
|
||||
toggle_visible_pdf(content)
|
||||
html = frappe.render_template(
|
||||
"templates/print_formats/pdf_header_footer.html",
|
||||
{
|
||||
"head": head,
|
||||
"content": content,
|
||||
"styles": styles,
|
||||
"html_id": html_id,
|
||||
"css": css,
|
||||
"lang": frappe.local.lang,
|
||||
"layout_direction": "rtl" if is_rtl() else "ltr",
|
||||
},
|
||||
id_map = {"header-html": "pdf_header_html", "footer-html": "pdf_footer_html"}
|
||||
hook_func = frappe.get_hooks(id_map.get(html_id))
|
||||
html = frappe.get_attr(hook_func[-1])(
|
||||
soup=soup,
|
||||
head=head,
|
||||
content=content,
|
||||
styles=styles,
|
||||
html_id=html_id,
|
||||
css=css,
|
||||
)
|
||||
|
||||
# create temp file
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ from frappe.www.printview import validate_print_permission
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=None):
|
||||
def download_multi_pdf(
|
||||
doctype, name, format=None, no_letterhead=False, letterhead=None, options=None
|
||||
):
|
||||
"""
|
||||
Concatenate multiple docs as PDF .
|
||||
|
||||
|
|
@ -76,6 +78,7 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=
|
|||
as_pdf=True,
|
||||
output=output,
|
||||
no_letterhead=no_letterhead,
|
||||
letterhead=letterhead,
|
||||
pdf_options=options,
|
||||
)
|
||||
frappe.local.response.filename = "{doctype}.pdf".format(
|
||||
|
|
@ -92,6 +95,7 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=
|
|||
as_pdf=True,
|
||||
output=output,
|
||||
no_letterhead=no_letterhead,
|
||||
letterhead=letterhead,
|
||||
pdf_options=options,
|
||||
)
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
<h1 class="ellipsis">{{ _(title) }}</h1>
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="indicator-pill orange hide">Not Saved</span>
|
||||
<span class="indicator-pill orange hide">{{ _("Not Saved") }}</span>
|
||||
<div class="web-form-actions">
|
||||
{{ header_buttons() }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@ frappe.ui.form.on("Web Form", {
|
|||
render_list_settings_message(frm);
|
||||
},
|
||||
|
||||
anonymous: function (frm) {
|
||||
if (frm.doc.anonymous) {
|
||||
frm.set_value("login_required", 0);
|
||||
}
|
||||
},
|
||||
|
||||
validate: function (frm) {
|
||||
if (!frm.doc.login_required) {
|
||||
frm.set_value("allow_multiple", 0);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"title",
|
||||
"route",
|
||||
"published",
|
||||
"column_break_1",
|
||||
"column_break_vdhm",
|
||||
"doc_type",
|
||||
"module",
|
||||
"is_standard",
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
"allow_multiple",
|
||||
"allow_edit",
|
||||
"allow_delete",
|
||||
"anonymous",
|
||||
"column_break_2",
|
||||
"apply_document_permissions",
|
||||
"allow_print",
|
||||
|
|
@ -96,10 +97,12 @@
|
|||
"default": "0",
|
||||
"fieldname": "published",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Published"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.anonymous",
|
||||
"fieldname": "login_required",
|
||||
"fieldtype": "Check",
|
||||
"label": "Login Required"
|
||||
|
|
@ -301,6 +304,7 @@
|
|||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "show_list",
|
||||
"depends_on": "eval:!doc.anonymous",
|
||||
"fieldname": "section_break_3",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "List Settings"
|
||||
|
|
@ -308,6 +312,7 @@
|
|||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "show_sidebar",
|
||||
"depends_on": "eval:!doc.anonymous",
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Sidebar Settings"
|
||||
|
|
@ -358,13 +363,24 @@
|
|||
"fieldname": "meta_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Meta Image"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vdhm",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Receive anonymous response",
|
||||
"fieldname": "anonymous",
|
||||
"fieldtype": "Check",
|
||||
"label": "Anonymous"
|
||||
}
|
||||
],
|
||||
"has_web_view": 1,
|
||||
"icon": "icon-edit",
|
||||
"is_published_field": "published",
|
||||
"links": [],
|
||||
"modified": "2023-01-02 10:19:15.680960",
|
||||
"modified": "2023-04-20 17:24:42.657731",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Web Form",
|
||||
|
|
|
|||
|
|
@ -387,6 +387,10 @@ def accept(web_form, data):
|
|||
|
||||
web_form = frappe.get_doc("Web Form", web_form)
|
||||
doctype = web_form.doc_type
|
||||
user = frappe.session.user
|
||||
|
||||
if web_form.anonymous and frappe.session.user != "Guest":
|
||||
frappe.session.user = "Guest"
|
||||
|
||||
if data.name and not web_form.allow_edit:
|
||||
frappe.throw(_("You are not allowed to update this Web Form Document"))
|
||||
|
|
@ -468,6 +472,9 @@ def accept(web_form, data):
|
|||
if f:
|
||||
remove_file_by_url(f, doctype=doctype, name=doc.name)
|
||||
|
||||
if web_form.anonymous and frappe.session.user == "Guest" and user:
|
||||
frappe.session.user = user
|
||||
|
||||
frappe.flags.web_form_doc = doc
|
||||
return doc
|
||||
|
||||
|
|
|
|||
|
|
@ -208,8 +208,10 @@ def get_rendered_template(
|
|||
"print_settings": print_settings,
|
||||
}
|
||||
)
|
||||
|
||||
html = template.render(args, filters={"len": len})
|
||||
hook_func = frappe.get_hooks("pdf_body_html")
|
||||
html = frappe.get_attr(hook_func[-1])(
|
||||
jenv=jenv, template=template, print_format=print_format, args=args
|
||||
)
|
||||
|
||||
if cint(trigger_print):
|
||||
html += trigger_print_script
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue