Merge branch 'develop' of https://github.com/frappe/frappe into refactor-website
This commit is contained in:
commit
3cbc7dfb92
20 changed files with 968 additions and 1023 deletions
|
|
@ -18,6 +18,7 @@ context('Form', () => {
|
|||
cy.get('.primary-action').click();
|
||||
cy.wait('@form_save').its('response.statusCode').should('eq', 200);
|
||||
cy.visit('/app/todo');
|
||||
cy.wait(300);
|
||||
cy.get('.title-text').should('be.visible').and('contain', 'To Do');
|
||||
cy.get('.list-row').should('contain', 'this is a test todo');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ import frappe
|
|||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_email_address, strip_html, cstr, time_diff_in_seconds
|
||||
from frappe.core.doctype.communication.email import validate_email, notify, _notify
|
||||
from frappe.core.doctype.communication.email import validate_email
|
||||
from frappe.core.doctype.communication.mixins import CommunicationEmailMixin
|
||||
from frappe.core.utils import get_parent_doc
|
||||
from frappe.utils.bot import BotReply
|
||||
from frappe.utils import parse_addr
|
||||
from frappe.utils import parse_addr, split_emails
|
||||
from frappe.core.doctype.comment.comment import update_comment_in_doc
|
||||
from email.utils import parseaddr
|
||||
from urllib.parse import unquote
|
||||
|
|
@ -19,7 +20,7 @@ from frappe.automation.doctype.assignment_rule.assignment_rule import apply as a
|
|||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
class Communication(Document):
|
||||
class Communication(Document, CommunicationEmailMixin):
|
||||
"""Communication represents an external communication like Email.
|
||||
"""
|
||||
no_feed_on_delete = True
|
||||
|
|
@ -125,6 +126,45 @@ class Communication(Document):
|
|||
if self.communication_type == "Communication":
|
||||
self.notify_change('delete')
|
||||
|
||||
@property
|
||||
def sender_mailid(self):
|
||||
return parse_addr(self.sender)[1] if self.sender else ""
|
||||
|
||||
@staticmethod
|
||||
def _get_emails_list(emails=None, exclude_displayname = False):
|
||||
"""Returns list of emails from given email string.
|
||||
|
||||
* Removes duplicate mailids
|
||||
* Removes display name from email address if exclude_displayname is True
|
||||
"""
|
||||
emails = split_emails(emails) if isinstance(emails, str) else (emails or [])
|
||||
if exclude_displayname:
|
||||
return [email.lower() for email in set([parse_addr(email)[1] for email in emails]) if email]
|
||||
return [email.lower() for email in set(emails) if email]
|
||||
|
||||
def to_list(self, exclude_displayname = True):
|
||||
"""Returns to list.
|
||||
"""
|
||||
return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname)
|
||||
|
||||
def cc_list(self, exclude_displayname = True):
|
||||
"""Returns cc list.
|
||||
"""
|
||||
return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname)
|
||||
|
||||
def bcc_list(self, exclude_displayname = True):
|
||||
"""Returns bcc list.
|
||||
"""
|
||||
return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname)
|
||||
|
||||
def get_attachments(self):
|
||||
attachments = frappe.get_all(
|
||||
"File",
|
||||
fields=["name", "file_name", "file_url", "is_private"],
|
||||
filters = {"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE}
|
||||
)
|
||||
return attachments
|
||||
|
||||
def notify_change(self, action):
|
||||
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), {
|
||||
'doc': self.as_dict(),
|
||||
|
|
@ -198,36 +238,6 @@ class Communication(Document):
|
|||
if not self.sender_full_name:
|
||||
self.sender_full_name = sender_email
|
||||
|
||||
def send(self, print_html=None, print_format=None, attachments=None,
|
||||
send_me_a_copy=False, recipients=None):
|
||||
"""Send communication via Email.
|
||||
|
||||
:param print_html: Send given value as HTML attachment.
|
||||
:param print_format: Attach print format of parent document."""
|
||||
|
||||
self.send_me_a_copy = send_me_a_copy
|
||||
self.notify(print_html, print_format, attachments, recipients)
|
||||
|
||||
def notify(self, print_html=None, print_format=None, attachments=None,
|
||||
recipients=None, cc=None, bcc=None,fetched_from_email_account=False):
|
||||
"""Calls a delayed task 'sendmail' that enqueus email in Email Queue queue
|
||||
|
||||
:param print_html: Send given value as HTML attachment
|
||||
:param print_format: Attach print format of parent document
|
||||
:param attachments: A list of filenames that should be attached when sending this email
|
||||
:param recipients: Email recipients
|
||||
:param cc: Send email as CC to
|
||||
:param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient
|
||||
|
||||
"""
|
||||
notify(self, print_html, print_format, attachments, recipients, cc, bcc,
|
||||
fetched_from_email_account)
|
||||
|
||||
def _notify(self, print_html=None, print_format=None, attachments=None,
|
||||
recipients=None, cc=None, bcc=None):
|
||||
|
||||
_notify(self, print_html, print_format, attachments, recipients, cc, bcc)
|
||||
|
||||
def bot_reply(self):
|
||||
if self.comment_type == 'Bot' and self.communication_type == 'Chat':
|
||||
reply = BotReply().get_reply(self.content)
|
||||
|
|
@ -504,3 +514,4 @@ def set_avg_response_time(parent, communication):
|
|||
if response_times:
|
||||
avg_response_time = sum(response_times) / len(response_times)
|
||||
parent.db_set("avg_response_time", avg_response_time)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ import time
|
|||
from frappe import _
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
OUTGOING_EMAIL_ACCOUNT_MISSING = _("""
|
||||
Unable to send mail because of a missing email account.
|
||||
Please setup default Email Account from Setup > Email > Email Account
|
||||
""")
|
||||
|
||||
@frappe.whitelist()
|
||||
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
|
||||
sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False,
|
||||
|
|
@ -36,7 +41,6 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
|
|||
:param send_me_a_copy: Send a copy to the sender (default **False**).
|
||||
:param email_template: Template which is used to compose mail .
|
||||
"""
|
||||
|
||||
is_error_report = (doctype=="User" and name==frappe.session.user and subject=="Error Report")
|
||||
send_me_a_copy = cint(send_me_a_copy)
|
||||
|
||||
|
|
@ -84,12 +88,16 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
|
|||
frappe.db.commit()
|
||||
|
||||
if cint(send_email):
|
||||
frappe.flags.print_letterhead = cint(print_letterhead)
|
||||
comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy)
|
||||
if not comm.get_outgoing_email_account():
|
||||
frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError)
|
||||
|
||||
comm.send_email(print_html=print_html, print_format=print_format,
|
||||
send_me_a_copy=send_me_a_copy, print_letterhead=print_letterhead)
|
||||
|
||||
emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy)
|
||||
return {
|
||||
"name": comm.name,
|
||||
"emails_not_sent_to": ", ".join(comm.emails_not_sent_to) if hasattr(comm, "emails_not_sent_to") else None
|
||||
"emails_not_sent_to": ", ".join(emails_not_sent_to or [])
|
||||
}
|
||||
|
||||
def validate_email(doc):
|
||||
|
|
@ -110,164 +118,6 @@ def validate_email(doc):
|
|||
|
||||
# validate sender
|
||||
|
||||
def notify(doc, print_html=None, print_format=None, attachments=None,
|
||||
recipients=None, cc=None, bcc=None, fetched_from_email_account=False):
|
||||
"""Calls a delayed task 'sendmail' that enqueus email in Email Queue queue
|
||||
|
||||
:param print_html: Send given value as HTML attachment
|
||||
:param print_format: Attach print format of parent document
|
||||
:param attachments: A list of filenames that should be attached when sending this email
|
||||
:param recipients: Email recipients
|
||||
:param cc: Send email as CC to
|
||||
:param bcc: Send email as BCC to
|
||||
:param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient
|
||||
|
||||
"""
|
||||
recipients, cc, bcc = get_recipients_cc_and_bcc(doc, recipients, cc, bcc,
|
||||
fetched_from_email_account=fetched_from_email_account)
|
||||
|
||||
if not recipients and not cc:
|
||||
return
|
||||
|
||||
doc.emails_not_sent_to = set(doc.all_email_addresses) - set(doc.sent_email_addresses)
|
||||
|
||||
if frappe.flags.in_test:
|
||||
# for test cases, run synchronously
|
||||
doc._notify(print_html=print_html, print_format=print_format, attachments=attachments,
|
||||
recipients=recipients, cc=cc, bcc=None)
|
||||
else:
|
||||
enqueue(sendmail, queue="default", timeout=300, event="sendmail",
|
||||
communication_name=doc.name,
|
||||
print_html=print_html, print_format=print_format, attachments=attachments,
|
||||
recipients=recipients, cc=cc, bcc=bcc, lang=frappe.local.lang,
|
||||
session=frappe.local.session, print_letterhead=frappe.flags.print_letterhead)
|
||||
|
||||
def _notify(doc, print_html=None, print_format=None, attachments=None,
|
||||
recipients=None, cc=None, bcc=None):
|
||||
|
||||
prepare_to_notify(doc, print_html, print_format, attachments)
|
||||
|
||||
if doc.outgoing_email_account.send_unsubscribe_message:
|
||||
unsubscribe_message = _("Leave this conversation")
|
||||
else:
|
||||
unsubscribe_message = ""
|
||||
|
||||
frappe.sendmail(
|
||||
recipients=(recipients or []),
|
||||
cc=(cc or []),
|
||||
bcc=(bcc or []),
|
||||
expose_recipients="header",
|
||||
sender=doc.sender,
|
||||
reply_to=doc.incoming_email_account,
|
||||
subject=doc.subject,
|
||||
content=doc.content,
|
||||
reference_doctype=doc.reference_doctype,
|
||||
reference_name=doc.reference_name,
|
||||
attachments=doc.attachments,
|
||||
message_id=doc.message_id,
|
||||
unsubscribe_message=unsubscribe_message,
|
||||
delayed=True,
|
||||
communication=doc.name,
|
||||
read_receipt=doc.read_receipt,
|
||||
is_notification=True if doc.sent_or_received =="Received" else False,
|
||||
print_letterhead=frappe.flags.print_letterhead
|
||||
)
|
||||
|
||||
def get_recipients_cc_and_bcc(doc, recipients, cc, bcc, fetched_from_email_account=False):
|
||||
doc.all_email_addresses = []
|
||||
doc.sent_email_addresses = []
|
||||
doc.previous_email_sender = None
|
||||
|
||||
if not recipients:
|
||||
recipients = get_recipients(doc, fetched_from_email_account=fetched_from_email_account)
|
||||
|
||||
if not cc:
|
||||
cc = get_cc(doc, recipients, fetched_from_email_account=fetched_from_email_account)
|
||||
|
||||
if not bcc:
|
||||
bcc = get_bcc(doc, recipients, fetched_from_email_account=fetched_from_email_account)
|
||||
|
||||
if fetched_from_email_account:
|
||||
# email was already sent to the original recipient by the sender's email service
|
||||
original_recipients, recipients = recipients, []
|
||||
|
||||
# send email to the sender of the previous email in the thread which this email is a reply to
|
||||
#provides erratic results and can send external
|
||||
#if doc.previous_email_sender:
|
||||
# recipients.append(doc.previous_email_sender)
|
||||
|
||||
# cc that was received in the email
|
||||
original_cc = split_emails(doc.cc)
|
||||
|
||||
# don't cc to people who already received the mail from sender's email service
|
||||
cc = list(set(cc) - set(original_cc) - set(original_recipients))
|
||||
remove_administrator_from_email_list(cc)
|
||||
|
||||
original_bcc = split_emails(doc.bcc)
|
||||
bcc = list(set(bcc) - set(original_bcc) - set(original_recipients))
|
||||
remove_administrator_from_email_list(bcc)
|
||||
|
||||
remove_administrator_from_email_list(recipients)
|
||||
|
||||
return recipients, cc, bcc
|
||||
|
||||
def remove_administrator_from_email_list(email_list):
|
||||
administrator_email = list(filter(lambda emails: "Administrator" in emails, email_list))
|
||||
if administrator_email:
|
||||
email_list.remove(administrator_email[0])
|
||||
|
||||
def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None):
|
||||
"""Prepare to make multipart MIME Email
|
||||
|
||||
:param print_html: Send given value as HTML attachment.
|
||||
:param print_format: Attach print format of parent document."""
|
||||
|
||||
view_link = frappe.utils.cint(frappe.db.get_value("System Settings", "System Settings", "attach_view_link"))
|
||||
|
||||
if print_format and view_link:
|
||||
doc.content += get_attach_link(doc, print_format)
|
||||
|
||||
set_incoming_outgoing_accounts(doc)
|
||||
|
||||
if not doc.sender:
|
||||
doc.sender = doc.outgoing_email_account.email_id
|
||||
|
||||
if not doc.sender_full_name:
|
||||
doc.sender_full_name = doc.outgoing_email_account.name or _("Notification")
|
||||
|
||||
if doc.sender:
|
||||
# combine for sending to get the format 'Jane <jane@example.com>'
|
||||
doc.sender = get_formatted_email(doc.sender_full_name, mail=doc.sender)
|
||||
|
||||
doc.attachments = []
|
||||
|
||||
if print_html or print_format:
|
||||
doc.attachments.append({"print_format_attachment":1, "doctype":doc.reference_doctype,
|
||||
"name":doc.reference_name, "print_format":print_format, "html":print_html})
|
||||
|
||||
if attachments:
|
||||
if isinstance(attachments, str):
|
||||
attachments = json.loads(attachments)
|
||||
|
||||
for a in attachments:
|
||||
if isinstance(a, str):
|
||||
# is it a filename?
|
||||
try:
|
||||
# check for both filename and file id
|
||||
file_id = frappe.db.get_list('File', or_filters={'file_name': a, 'name': a}, limit=1)
|
||||
if not file_id:
|
||||
frappe.throw(_("Unable to find attachment {0}").format(a))
|
||||
file_id = file_id[0]['name']
|
||||
_file = frappe.get_doc("File", file_id)
|
||||
_file.get_content()
|
||||
# these attachments will be attached on-demand
|
||||
# and won't be stored in the message
|
||||
doc.attachments.append({"fid": file_id})
|
||||
except IOError:
|
||||
frappe.throw(_("Unable to find attachment {0}").format(a))
|
||||
else:
|
||||
doc.attachments.append(a)
|
||||
|
||||
def set_incoming_outgoing_accounts(doc):
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
incoming_email_account = EmailAccount.find_incoming(
|
||||
|
|
@ -280,74 +130,6 @@ def set_incoming_outgoing_accounts(doc):
|
|||
if doc.sent_or_received == "Sent":
|
||||
doc.db_set("email_account", doc.outgoing_email_account.name)
|
||||
|
||||
def get_recipients(doc, fetched_from_email_account=False):
|
||||
"""Build a list of email addresses for To"""
|
||||
# [EDGE CASE] doc.recipients can be None when an email is sent as BCC
|
||||
recipients = split_emails(doc.recipients)
|
||||
|
||||
#if fetched_from_email_account and doc.in_reply_to:
|
||||
# add sender of previous reply
|
||||
#doc.previous_email_sender = frappe.db.get_value("Communication", doc.in_reply_to, "sender")
|
||||
#recipients.append(doc.previous_email_sender)
|
||||
|
||||
if recipients:
|
||||
recipients = filter_email_list(doc, recipients, [])
|
||||
|
||||
return recipients
|
||||
|
||||
def get_cc(doc, recipients=None, fetched_from_email_account=False):
|
||||
"""Build a list of email addresses for CC"""
|
||||
# get a copy of CC list
|
||||
cc = split_emails(doc.cc)
|
||||
|
||||
if doc.reference_doctype and doc.reference_name:
|
||||
if fetched_from_email_account:
|
||||
# if it is a fetched email, add follows to CC
|
||||
cc.append(get_owner_email(doc))
|
||||
cc += get_assignees(doc)
|
||||
|
||||
if getattr(doc, "send_me_a_copy", False) and doc.sender not in cc:
|
||||
cc.append(doc.sender)
|
||||
|
||||
if cc:
|
||||
# exclude unfollows, recipients and unsubscribes
|
||||
exclude = [] #added to remove account check
|
||||
exclude += [d[0] for d in frappe.db.get_all("User", ["email"], {"thread_notify": 0}, as_list=True)]
|
||||
exclude += [(parse_addr(email)[1] or "").lower() for email in recipients]
|
||||
|
||||
if fetched_from_email_account:
|
||||
# exclude sender when pulling email
|
||||
exclude += [parse_addr(doc.sender)[1]]
|
||||
|
||||
if doc.reference_doctype and doc.reference_name:
|
||||
exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"],
|
||||
{"reference_doctype": doc.reference_doctype, "reference_name": doc.reference_name}, as_list=True)]
|
||||
|
||||
cc = filter_email_list(doc, cc, exclude, is_cc=True)
|
||||
|
||||
return cc
|
||||
|
||||
def get_bcc(doc, recipients=None, fetched_from_email_account=False):
|
||||
"""Build a list of email addresses for BCC"""
|
||||
bcc = split_emails(doc.bcc)
|
||||
|
||||
if bcc:
|
||||
exclude = []
|
||||
exclude += [d[0] for d in frappe.db.get_all("User", ["email"], {"thread_notify": 0}, as_list=True)]
|
||||
exclude += [(parse_addr(email)[1] or "").lower() for email in recipients]
|
||||
|
||||
if fetched_from_email_account:
|
||||
# exclude sender when pulling email
|
||||
exclude += [parse_addr(doc.sender)[1]]
|
||||
|
||||
if doc.reference_doctype and doc.reference_name:
|
||||
exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"],
|
||||
{"reference_doctype": doc.reference_doctype, "reference_name": doc.reference_name}, as_list=True)]
|
||||
|
||||
bcc = filter_email_list(doc, bcc, exclude, is_bcc=True)
|
||||
|
||||
return bcc
|
||||
|
||||
def add_attachments(name, attachments):
|
||||
'''Add attachments to the given Communication'''
|
||||
# loop through attachments
|
||||
|
|
@ -355,7 +137,6 @@ def add_attachments(name, attachments):
|
|||
if isinstance(a, str):
|
||||
attach = frappe.db.get_value("File", {"name":a},
|
||||
["file_name", "file_url", "is_private"], as_dict=1)
|
||||
|
||||
# save attachments to new doc
|
||||
_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
|
|
@ -367,103 +148,6 @@ def add_attachments(name, attachments):
|
|||
})
|
||||
_file.save(ignore_permissions=True)
|
||||
|
||||
def filter_email_list(doc, email_list, exclude, is_cc=False, is_bcc=False):
|
||||
# temp variables
|
||||
filtered = []
|
||||
email_address_list = []
|
||||
|
||||
for email in list(set(email_list)):
|
||||
email_address = (parse_addr(email)[1] or "").lower()
|
||||
if not email_address:
|
||||
continue
|
||||
|
||||
# this will be used to eventually find email addresses that aren't sent to
|
||||
doc.all_email_addresses.append(email_address)
|
||||
|
||||
if (email in exclude) or (email_address in exclude):
|
||||
continue
|
||||
|
||||
if is_cc:
|
||||
is_user_enabled = frappe.db.get_value("User", email_address, "enabled")
|
||||
if is_user_enabled==0:
|
||||
# don't send to disabled users
|
||||
continue
|
||||
|
||||
if is_bcc:
|
||||
is_user_enabled = frappe.db.get_value("User", email_address, "enabled")
|
||||
if is_user_enabled==0:
|
||||
continue
|
||||
|
||||
# make sure of case-insensitive uniqueness of email address
|
||||
if email_address not in email_address_list:
|
||||
# append the full email i.e. "Human <human@example.com>"
|
||||
filtered.append(email)
|
||||
email_address_list.append(email_address)
|
||||
|
||||
doc.sent_email_addresses.extend(email_address_list)
|
||||
|
||||
return filtered
|
||||
|
||||
def get_owner_email(doc):
|
||||
owner = get_parent_doc(doc).owner
|
||||
return get_formatted_email(owner) or owner
|
||||
|
||||
def get_assignees(doc):
|
||||
return [( get_formatted_email(d.owner) or d.owner ) for d in
|
||||
frappe.db.get_all("ToDo", filters={
|
||||
"reference_type": doc.reference_doctype,
|
||||
"reference_name": doc.reference_name,
|
||||
"status": "Open"
|
||||
}, fields=["owner"])
|
||||
]
|
||||
|
||||
def get_attach_link(doc, print_format):
|
||||
"""Returns public link for the attachment via `templates/emails/print_link.html`."""
|
||||
return frappe.get_template("templates/emails/print_link.html").render({
|
||||
"url": get_url(),
|
||||
"doctype": doc.reference_doctype,
|
||||
"name": doc.reference_name,
|
||||
"print_format": print_format,
|
||||
"key": get_parent_doc(doc).get_signature()
|
||||
})
|
||||
|
||||
def sendmail(communication_name, print_html=None, print_format=None, attachments=None,
|
||||
recipients=None, cc=None, bcc=None, lang=None, session=None, print_letterhead=None):
|
||||
try:
|
||||
|
||||
if lang:
|
||||
frappe.local.lang = lang
|
||||
|
||||
if session:
|
||||
# hack to enable access to private files in PDF
|
||||
session['data'] = frappe._dict(session['data'])
|
||||
frappe.local.session.update(session)
|
||||
|
||||
if print_letterhead:
|
||||
frappe.flags.print_letterhead = print_letterhead
|
||||
|
||||
# upto 3 retries
|
||||
for i in range(3):
|
||||
try:
|
||||
communication = frappe.get_doc("Communication", communication_name)
|
||||
communication._notify(print_html=print_html, print_format=print_format, attachments=attachments,
|
||||
recipients=recipients, cc=cc, bcc=bcc)
|
||||
|
||||
except frappe.db.InternalError as e:
|
||||
# deadlock, try again
|
||||
if frappe.db.is_deadlocked(e):
|
||||
frappe.db.rollback()
|
||||
time.sleep(1)
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
break
|
||||
|
||||
except:
|
||||
traceback = frappe.log_error("frappe.core.doctype.communication.email.sendmail")
|
||||
raise
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def mark_email_as_seen(name=None):
|
||||
try:
|
||||
|
|
|
|||
297
frappe/core/doctype/communication/mixins.py
Normal file
297
frappe/core/doctype/communication/mixins.py
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.utils import get_parent_doc
|
||||
from frappe.utils import parse_addr, get_formatted_email, get_url
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
|
||||
class CommunicationEmailMixin:
|
||||
"""Mixin class to handle communication mails.
|
||||
"""
|
||||
def is_email_communication(self):
|
||||
return self.communication_type=="Communication" and self.communication_medium == "Email"
|
||||
|
||||
def get_owner(self):
|
||||
"""Get owner of the communication docs parent.
|
||||
"""
|
||||
parent_doc = get_parent_doc(self)
|
||||
return parent_doc.owner if parent_doc else None
|
||||
|
||||
def get_all_email_addresses(self, exclude_displayname=False):
|
||||
"""Get all Email addresses mentioned in the doc along with display name.
|
||||
"""
|
||||
return self.to_list(exclude_displayname=exclude_displayname) + \
|
||||
self.cc_list(exclude_displayname=exclude_displayname) + \
|
||||
self.bcc_list(exclude_displayname=exclude_displayname)
|
||||
|
||||
def get_email_with_displayname(self, email_address):
|
||||
"""Returns email address after adding displayname.
|
||||
"""
|
||||
display_name, email = parse_addr(email_address)
|
||||
if display_name and display_name != email:
|
||||
return email_address
|
||||
|
||||
# emailid to emailid with display name map.
|
||||
email_map = {parse_addr(email)[1]: email for email in self.get_all_email_addresses()}
|
||||
return email_map.get(email, email)
|
||||
|
||||
def mail_recipients(self, is_inbound_mail_communcation=False):
|
||||
"""Build to(recipient) list to send an email.
|
||||
"""
|
||||
# Incase of inbound mail, recipients already received the mail, no need to send again.
|
||||
if is_inbound_mail_communcation:
|
||||
return []
|
||||
|
||||
if hasattr(self, '_final_recipients'):
|
||||
return self._final_recipients
|
||||
|
||||
to = self.to_list()
|
||||
self._final_recipients = list(filter(lambda id: id != 'Administrator', to))
|
||||
return self._final_recipients
|
||||
|
||||
def get_mail_recipients_with_displayname(self, is_inbound_mail_communcation=False):
|
||||
"""Build to(recipient) list to send an email including displayname in email.
|
||||
"""
|
||||
to_list = self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)
|
||||
return [self.get_email_with_displayname(email) for email in to_list]
|
||||
|
||||
def mail_cc(self, is_inbound_mail_communcation=False, include_sender = False):
|
||||
"""Build cc list to send an email.
|
||||
|
||||
* if email copy is requested by sender, then add sender to CC.
|
||||
* If this doc is created through inbound mail, then add doc owner to cc list
|
||||
* remove all the thread_notify disabled users.
|
||||
* Make sure that all users enabled in the system
|
||||
* Remove admin from email list
|
||||
|
||||
* FixMe: Removed adding TODO owners to cc list. Check if that is needed.
|
||||
"""
|
||||
if hasattr(self, '_final_cc'):
|
||||
return self._final_cc
|
||||
|
||||
cc = self.cc_list()
|
||||
|
||||
# Need to inform parent document owner incase communication is created through inbound mail
|
||||
if include_sender:
|
||||
cc.append(self.sender_mailid)
|
||||
if is_inbound_mail_communcation:
|
||||
cc.append(self.get_owner())
|
||||
cc = set(cc) - {self.sender_mailid}
|
||||
|
||||
cc = set(cc) - set(self.filter_thread_notification_disbled_users(cc))
|
||||
cc = cc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation))
|
||||
cc = cc - set(self.filter_disabled_users(cc))
|
||||
|
||||
# # Incase of inbound mail, to and cc already received the mail, no need to send again.
|
||||
if is_inbound_mail_communcation:
|
||||
cc = cc - set(self.cc_list() + self.to_list())
|
||||
|
||||
self._final_cc = list(filter(lambda id: id != 'Administrator', cc))
|
||||
return self._final_cc
|
||||
|
||||
def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender = False):
|
||||
cc_list = self.mail_cc(is_inbound_mail_communcation=False, include_sender = False)
|
||||
return [self.get_email_with_displayname(email) for email in cc_list]
|
||||
|
||||
def mail_bcc(self, is_inbound_mail_communcation=False):
|
||||
"""
|
||||
* Thread_notify check
|
||||
* Email unsubscribe list
|
||||
* User must be enabled in the system
|
||||
* remove_administrator_from_email_list
|
||||
"""
|
||||
if hasattr(self, '_final_bcc'):
|
||||
return self._final_bcc
|
||||
|
||||
bcc = set(self.bcc_list())
|
||||
if is_inbound_mail_communcation:
|
||||
bcc = bcc - {self.sender_mailid}
|
||||
bcc = bcc - set(self.filter_thread_notification_disbled_users(bcc))
|
||||
bcc = bcc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation))
|
||||
bcc = bcc - set(self.filter_disabled_users(bcc))
|
||||
|
||||
# Incase of inbound mail, to and cc & bcc already received the mail, no need to send again.
|
||||
if is_inbound_mail_communcation:
|
||||
bcc = bcc - set(self.bcc_list() + self.to_list())
|
||||
|
||||
self._final_bcc = list(filter(lambda id: id != 'Administrator', bcc))
|
||||
return self._final_bcc
|
||||
|
||||
def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False):
|
||||
bcc_list = self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation)
|
||||
return [self.get_email_with_displayname(email) for email in bcc_list]
|
||||
|
||||
def mail_sender(self):
|
||||
email_account = self.get_outgoing_email_account()
|
||||
if not self.sender_mailid and email_account:
|
||||
return email_account.email_id
|
||||
return self.sender_mailid
|
||||
|
||||
def mail_sender_fullname(self):
|
||||
email_account = self.get_outgoing_email_account()
|
||||
if not self.sender_full_name:
|
||||
return (email_account and email_account.name) or _("Notification")
|
||||
return self.sender_full_name
|
||||
|
||||
def get_mail_sender_with_displayname(self):
|
||||
return get_formatted_email(self.mail_sender_fullname(), mail=self.mail_sender())
|
||||
|
||||
def get_content(self, print_format=None):
|
||||
if print_format:
|
||||
return self.content + self.get_attach_link(print_format)
|
||||
return self.content
|
||||
|
||||
def get_attach_link(self, print_format):
|
||||
"""Returns public link for the attachment via `templates/emails/print_link.html`."""
|
||||
return frappe.get_template("templates/emails/print_link.html").render({
|
||||
"url": get_url(),
|
||||
"doctype": self.reference_doctype,
|
||||
"name": self.reference_name,
|
||||
"print_format": print_format,
|
||||
"key": get_parent_doc(self).get_signature()
|
||||
})
|
||||
|
||||
def get_outgoing_email_account(self):
|
||||
if not hasattr(self, '_outgoing_email_account'):
|
||||
if self.email_account:
|
||||
self._outgoing_email_account = EmailAccount.find(self.email_account)
|
||||
else:
|
||||
self._outgoing_email_account = EmailAccount.find_outgoing(
|
||||
match_by_email=self.sender_mailid,
|
||||
match_by_doctype=self.reference_doctype
|
||||
)
|
||||
|
||||
if self.sent_or_received == "Sent" and self._outgoing_email_account:
|
||||
self.db_set("email_account", self._outgoing_email_account.name)
|
||||
|
||||
return self._outgoing_email_account
|
||||
|
||||
def get_incoming_email_account(self):
|
||||
if not hasattr(self, '_incoming_email_account'):
|
||||
self._incoming_email_account = EmailAccount.find_incoming(
|
||||
match_by_email=self.sender_mailid,
|
||||
match_by_doctype=self.reference_doctype
|
||||
)
|
||||
return self._incoming_email_account
|
||||
|
||||
def mail_attachments(self, print_format=None, print_html=None):
|
||||
final_attachments = []
|
||||
|
||||
if print_format and print_html:
|
||||
d = {'print_format': print_format, 'print_html': print_html, 'print_format_attachment': 1,
|
||||
'doctype': self.reference_doctype, 'name': self.reference_name}
|
||||
final_attachments.append(d)
|
||||
|
||||
for a in self.get_attachments() or []:
|
||||
final_attachments.append({"fid": a['name']})
|
||||
|
||||
return final_attachments
|
||||
|
||||
def get_unsubscribe_message(self):
|
||||
email_account = self.get_outgoing_email_account()
|
||||
if email_account and email_account.send_unsubscribe_message:
|
||||
return _("Leave this conversation")
|
||||
return ''
|
||||
|
||||
def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False):
|
||||
"""List of mail id's excluded while sending mail.
|
||||
"""
|
||||
all_ids = self.get_all_email_addresses(exclude_displayname=True)
|
||||
final_ids = self.mail_recipients(is_inbound_mail_communcation = is_inbound_mail_communcation) + \
|
||||
self.mail_bcc(is_inbound_mail_communcation = is_inbound_mail_communcation) + \
|
||||
self.mail_cc(is_inbound_mail_communcation = is_inbound_mail_communcation, include_sender=include_sender)
|
||||
return set(all_ids) - set(final_ids)
|
||||
|
||||
@staticmethod
|
||||
def filter_thread_notification_disbled_users(emails):
|
||||
"""Filter users based on notifications for email threads setting is disabled.
|
||||
"""
|
||||
if not emails:
|
||||
return []
|
||||
|
||||
disabled_users = frappe.db.sql_list("""
|
||||
SELECT
|
||||
email
|
||||
FROM
|
||||
`tabUser`
|
||||
where
|
||||
email in %(emails)s
|
||||
and
|
||||
thread_notify=0
|
||||
""", {'emails': tuple(emails)})
|
||||
return disabled_users
|
||||
|
||||
@staticmethod
|
||||
def filter_disabled_users(emails):
|
||||
"""
|
||||
"""
|
||||
if not emails:
|
||||
return []
|
||||
|
||||
disabled_users = frappe.db.sql_list("""
|
||||
SELECT
|
||||
email
|
||||
FROM
|
||||
`tabUser`
|
||||
where
|
||||
email in %(emails)s
|
||||
and
|
||||
enabled=0
|
||||
""", {'emails': tuple(emails)})
|
||||
return disabled_users
|
||||
|
||||
def sendmail_input_dict(self, print_html=None, print_format=None,
|
||||
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None):
|
||||
|
||||
outgoing_email_account = self.get_outgoing_email_account()
|
||||
if not outgoing_email_account:
|
||||
return {}
|
||||
|
||||
recipients = self.get_mail_recipients_with_displayname(
|
||||
is_inbound_mail_communcation=is_inbound_mail_communcation
|
||||
)
|
||||
cc = self.get_mail_cc_with_displayname(
|
||||
is_inbound_mail_communcation=is_inbound_mail_communcation,
|
||||
include_sender = send_me_a_copy
|
||||
)
|
||||
bcc = self.get_mail_bcc_with_displayname(
|
||||
is_inbound_mail_communcation=is_inbound_mail_communcation
|
||||
)
|
||||
|
||||
if not (recipients or cc):
|
||||
return {}
|
||||
|
||||
final_attachments = self.mail_attachments(print_format=print_format, print_html=print_html)
|
||||
incoming_email_account = self.get_incoming_email_account()
|
||||
return {
|
||||
"recipients": recipients,
|
||||
"cc": cc,
|
||||
"bcc": bcc,
|
||||
"expose_recipients": "header",
|
||||
"sender": self.get_mail_sender_with_displayname(),
|
||||
"reply_to": incoming_email_account and incoming_email_account.email_id,
|
||||
"subject": self.subject,
|
||||
"content": self.get_content(print_format=print_format),
|
||||
"reference_doctype": self.reference_doctype,
|
||||
"reference_name": self.reference_name,
|
||||
"attachments": final_attachments,
|
||||
"message_id": self.message_id,
|
||||
"unsubscribe_message": self.get_unsubscribe_message(),
|
||||
"delayed": True,
|
||||
"communication": self.name,
|
||||
"read_receipt": self.read_receipt,
|
||||
"is_notification": (self.sent_or_received =="Received" and True) or False,
|
||||
"print_letterhead": print_letterhead
|
||||
}
|
||||
|
||||
def send_email(self, print_html=None, print_format=None,
|
||||
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None):
|
||||
input_dict = self.sendmail_input_dict(
|
||||
print_html=print_html,
|
||||
print_format=print_format,
|
||||
send_me_a_copy=send_me_a_copy,
|
||||
print_letterhead=print_letterhead,
|
||||
is_inbound_mail_communcation=is_inbound_mail_communcation
|
||||
)
|
||||
|
||||
if input_dict:
|
||||
frappe.sendmail(**input_dict)
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import frappe
|
||||
import unittest
|
||||
from urllib.parse import quote
|
||||
test_records = frappe.get_test_records('Communication')
|
||||
|
||||
import frappe
|
||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue
|
||||
|
||||
test_records = frappe.get_test_records('Communication')
|
||||
|
||||
class TestCommunication(unittest.TestCase):
|
||||
|
||||
|
|
@ -199,6 +201,70 @@ class TestCommunication(unittest.TestCase):
|
|||
|
||||
self.assertIn(("Note", note.name), doc_links)
|
||||
|
||||
class TestCommunicationEmailMixin(unittest.TestCase):
|
||||
def new_communication(self, recipients=None, cc=None, bcc=None):
|
||||
recipients = ', '.join(recipients or [])
|
||||
cc = ', '.join(cc or [])
|
||||
bcc = ', '.join(bcc or [])
|
||||
|
||||
comm = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"communication_medium": "Email",
|
||||
"content": "Test content",
|
||||
"recipients": recipients,
|
||||
"cc": cc,
|
||||
"bcc": bcc
|
||||
}).insert(ignore_permissions=True)
|
||||
return comm
|
||||
|
||||
def new_user(self, email, **user_data):
|
||||
user_data.setdefault('first_name', 'first_name')
|
||||
user = frappe.new_doc('User')
|
||||
user.email = email
|
||||
user.update(user_data)
|
||||
user.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
return user
|
||||
|
||||
def test_recipients(self):
|
||||
to_list = ['to@test.com', 'receiver <to+1@test.com>', 'to@test.com']
|
||||
comm = self.new_communication(recipients = to_list)
|
||||
res = comm.get_mail_recipients_with_displayname()
|
||||
self.assertCountEqual(res, ['to@test.com', 'receiver <to+1@test.com>'])
|
||||
comm.delete()
|
||||
|
||||
def test_cc(self):
|
||||
to_list = ['to@test.com']
|
||||
cc_list = ['cc+1@test.com', 'cc <cc+2@test.com>', 'to@test.com']
|
||||
user = self.new_user(email='cc+1@test.com', thread_notify=0)
|
||||
comm = self.new_communication(recipients=to_list, cc=cc_list)
|
||||
res = comm.get_mail_cc_with_displayname()
|
||||
self.assertCountEqual(res, ['cc <cc+2@test.com>'])
|
||||
user.delete()
|
||||
comm.delete()
|
||||
|
||||
def test_bcc(self):
|
||||
bcc_list = ['bcc+1@test.com', 'cc <bcc+2@test.com>', ]
|
||||
user = self.new_user(email='bcc+2@test.com', enabled=0)
|
||||
comm = self.new_communication(bcc=bcc_list)
|
||||
res = comm.get_mail_bcc_with_displayname()
|
||||
self.assertCountEqual(res, ['bcc+1@test.com'])
|
||||
user.delete()
|
||||
comm.delete()
|
||||
|
||||
def test_sendmail(self):
|
||||
to_list = ['to <to@test.com>']
|
||||
cc_list = ['cc <cc+1@test.com>', 'cc <cc+2@test.com>']
|
||||
|
||||
comm = self.new_communication(recipients=to_list, cc=cc_list)
|
||||
comm.send_email()
|
||||
doc = EmailQueue.find_one_by_filters(communication=comm.name)
|
||||
mail_receivers = [each.recipient for each in doc.recipients]
|
||||
self.assertIsNotNone(doc)
|
||||
self.assertCountEqual(to_list+cc_list, mail_receivers)
|
||||
doc.delete()
|
||||
comm.delete()
|
||||
|
||||
def create_email_account():
|
||||
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1")
|
||||
|
||||
|
|
@ -229,4 +295,4 @@ def create_email_account():
|
|||
"enable_automatic_linking": 1
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
return email_account
|
||||
return email_account
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import functools
|
|||
|
||||
import email.utils
|
||||
|
||||
from frappe import _, are_emails_muted
|
||||
from frappe import _, are_emails_muted, safe_encode
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (validate_email_address, cint, cstr, get_datetime,
|
||||
DATE_FORMAT, strip, comma_or, sanitize_html, add_days, parse_addr)
|
||||
|
|
@ -441,10 +441,7 @@ class EmailAccount(Document):
|
|||
if self.enable_auto_reply:
|
||||
self.send_auto_reply(communication, mail)
|
||||
|
||||
attachments = []
|
||||
if hasattr(communication, '_attachments'):
|
||||
attachments = [d.file_name for d in communication._attachments]
|
||||
communication.notify(attachments=attachments, fetched_from_email_account=True)
|
||||
communication.send_email(is_inbound_mail_communcation=True)
|
||||
except SentEmailInInboxError:
|
||||
frappe.db.rollback()
|
||||
except Exception:
|
||||
|
|
@ -453,6 +450,8 @@ class EmailAccount(Document):
|
|||
if self.use_imap:
|
||||
self.handle_bad_emails(mail.uid, mail.raw_message, frappe.get_traceback())
|
||||
exceptions.append(frappe.get_traceback())
|
||||
else:
|
||||
frappe.db.commit()
|
||||
|
||||
#notify if user is linked to account
|
||||
if len(inbound_mails)>0 and not frappe.local.flags.in_test:
|
||||
|
|
@ -609,7 +608,6 @@ class EmailAccount(Document):
|
|||
|
||||
|
||||
def append_email_to_sent_folder(self, message):
|
||||
|
||||
email_server = None
|
||||
try:
|
||||
email_server = self.get_incoming_server(in_receive=True)
|
||||
|
|
@ -623,7 +621,8 @@ class EmailAccount(Document):
|
|||
|
||||
if email_server.imap:
|
||||
try:
|
||||
email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message.encode())
|
||||
message = safe_encode(message)
|
||||
email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
|
||||
except Exception:
|
||||
frappe.log_error()
|
||||
|
||||
|
|
|
|||
|
|
@ -179,7 +179,14 @@ class SendMailContext:
|
|||
else:
|
||||
email_status = self.is_mail_sent_to_all() and 'Sent'
|
||||
email_status = email_status or (self.sent_to and 'Partially Sent') or 'Not Sent'
|
||||
self.queue_doc.update_status(status = email_status, commit = True)
|
||||
|
||||
update_fields = {'status': email_status}
|
||||
if self.email_account_doc.is_exists_in_db():
|
||||
update_fields['email_account'] = self.email_account_doc.name
|
||||
else:
|
||||
update_fields['email_account'] = None
|
||||
|
||||
self.queue_doc.update_status(**update_fields, commit = True)
|
||||
|
||||
def log_exception(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type:
|
||||
|
|
|
|||
|
|
@ -6,15 +6,61 @@ from frappe import msgprint, _
|
|||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
from frappe.utils import get_url, now_datetime, cint
|
||||
|
||||
def get_emails_sent_this_month():
|
||||
return frappe.db.sql("""
|
||||
SELECT COUNT(*) FROM `tabEmail Queue`
|
||||
WHERE `status`='Sent' AND EXTRACT(YEAR_MONTH FROM `creation`) = EXTRACT(YEAR_MONTH FROM NOW())
|
||||
""")[0][0]
|
||||
def get_emails_sent_this_month(email_account=None):
|
||||
"""Get count of emails sent from a specific email account.
|
||||
|
||||
def get_emails_sent_today():
|
||||
return frappe.db.sql("""SELECT COUNT(`name`) FROM `tabEmail Queue` WHERE
|
||||
`status` in ('Sent', 'Not Sent', 'Sending') AND `creation` > (NOW() - INTERVAL '24' HOUR)""")[0][0]
|
||||
:param email_account: name of the email account used to send mail
|
||||
|
||||
if email_account=None, email account filter is not applied while counting
|
||||
"""
|
||||
q = """
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
`tabEmail Queue`
|
||||
WHERE
|
||||
`status`='Sent'
|
||||
AND
|
||||
EXTRACT(YEAR_MONTH FROM `creation`) = EXTRACT(YEAR_MONTH FROM NOW())
|
||||
"""
|
||||
|
||||
q_args = {}
|
||||
if email_account is not None:
|
||||
if email_account:
|
||||
q += " AND email_account = %(email_account)s"
|
||||
q_args['email_account'] = email_account
|
||||
else:
|
||||
q += " AND (email_account is null OR email_account='')"
|
||||
|
||||
return frappe.db.sql(q, q_args)[0][0]
|
||||
|
||||
def get_emails_sent_today(email_account=None):
|
||||
"""Get count of emails sent from a specific email account.
|
||||
|
||||
:param email_account: name of the email account used to send mail
|
||||
|
||||
if email_account=None, email account filter is not applied while counting
|
||||
"""
|
||||
q = """
|
||||
SELECT
|
||||
COUNT(`name`)
|
||||
FROM
|
||||
`tabEmail Queue`
|
||||
WHERE
|
||||
`status` in ('Sent', 'Not Sent', 'Sending')
|
||||
AND
|
||||
`creation` > (NOW() - INTERVAL '24' HOUR)
|
||||
"""
|
||||
|
||||
q_args = {}
|
||||
if email_account is not None:
|
||||
if email_account:
|
||||
q += " AND email_account = %(email_account)s"
|
||||
q_args['email_account'] = email_account
|
||||
else:
|
||||
q += " AND (email_account is null OR email_account='')"
|
||||
|
||||
return frappe.db.sql(q, q_args)[0][0]
|
||||
|
||||
def get_unsubscribe_message(unsubscribe_message, expose_recipients):
|
||||
if unsubscribe_message:
|
||||
|
|
|
|||
|
|
@ -730,7 +730,7 @@ class DatabaseQuery(object):
|
|||
args.order_by = "`tab{0}`.`{1}` {2}".format(self.doctype, sort_field or "modified", sort_order or "desc")
|
||||
|
||||
# draft docs always on top
|
||||
if meta.is_submittable:
|
||||
if hasattr(meta, 'is_submittable') and meta.is_submittable:
|
||||
args.order_by = "`tab{0}`.docstatus asc, {1}".format(self.doctype, args.order_by)
|
||||
|
||||
def validate_order_by_and_group_by(self, parameters):
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ from frappe.model.workflow import set_workflow_state_on_action
|
|||
from frappe.utils.global_search import update_global_search
|
||||
from frappe.integrations.doctype.webhook import run_webhooks
|
||||
from frappe.desk.form.document_follow import follow_document
|
||||
from frappe.desk.utils import slug
|
||||
from frappe.core.doctype.server_script.server_script_utils import run_server_script_for_doc_event
|
||||
from frappe.utils.data import get_absolute_url
|
||||
|
||||
# once_only validation
|
||||
# methods
|
||||
|
|
@ -1200,8 +1200,8 @@ class Document(BaseDocument):
|
|||
doc.set(fieldname, flt(doc.get(fieldname), self.precision(fieldname, doc.parentfield)))
|
||||
|
||||
def get_url(self):
|
||||
"""Returns Desk URL for this document. `/app/{doctype}/{name}`"""
|
||||
return f"/app/{slug(self.doctype)}/{self.name}"
|
||||
"""Returns Desk URL for this document."""
|
||||
return get_absolute_url(self.doctype, self.name)
|
||||
|
||||
def add_comment(self, comment_type='Comment', text=None, comment_email=None, link_doctype=None, link_name=None, comment_by=None):
|
||||
"""Add a comment to this document.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import Quill from 'quill';
|
||||
import ImageResize from 'quill-image-resize';
|
||||
import MagicUrl from 'quill-magic-url';
|
||||
|
||||
|
||||
Quill.register('modules/imageResize', ImageResize);
|
||||
Quill.register('modules/magicUrl', MagicUrl);
|
||||
const CodeBlockContainer = Quill.import('formats/code-block-container');
|
||||
CodeBlockContainer.tagName = 'PRE';
|
||||
Quill.register(CodeBlockContainer, true);
|
||||
|
|
@ -148,7 +151,8 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for
|
|||
modules: {
|
||||
toolbar: this.get_toolbar_options(),
|
||||
table: true,
|
||||
imageResize: {}
|
||||
imageResize: {},
|
||||
magicUrl: true
|
||||
},
|
||||
theme: 'snow'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -210,9 +210,9 @@ export default class Grid {
|
|||
|
||||
delete_all_rows() {
|
||||
frappe.confirm(__("Are you sure you want to delete all rows?"), () => {
|
||||
this.frm.doc[this.df.fieldname] = [];
|
||||
$(this.parent).find('.rows').empty();
|
||||
this.grid_rows = [];
|
||||
this.grid_rows.forEach(row => {
|
||||
row.remove();
|
||||
});
|
||||
this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
|
||||
|
||||
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').prop('checked', 0);
|
||||
|
|
@ -236,6 +236,10 @@ export default class Grid {
|
|||
}
|
||||
|
||||
refresh_remove_rows_button() {
|
||||
if (this.df.cannot_delete_rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.remove_rows_button.toggleClass('hidden',
|
||||
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true);
|
||||
this.remove_all_rows_button.toggleClass('hidden',
|
||||
|
|
|
|||
|
|
@ -569,6 +569,9 @@ export default class GridRow {
|
|||
.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row, .grid-append-row')
|
||||
.toggle(!cannot_add_rows);
|
||||
|
||||
this.wrapper.find('.grid-delete-row')
|
||||
.toggle(!(this.grid.df && this.grid.df.cannot_delete_rows));
|
||||
|
||||
frappe.dom.freeze("", "dark");
|
||||
if (cur_frm) cur_frm.cur_grid = this;
|
||||
this.wrapper.addClass("grid-row-open");
|
||||
|
|
|
|||
|
|
@ -323,9 +323,12 @@ frappe.ui.FilterGroup = class {
|
|||
}
|
||||
|
||||
add_filters_to_filter_group(filters) {
|
||||
filters.forEach((filter) => {
|
||||
this.add_filter(filter[0], filter[1], filter[2], filter[3]);
|
||||
});
|
||||
if (filters.length) {
|
||||
this.toggle_empty_filters(false);
|
||||
filters.forEach((filter) => {
|
||||
this.add_filter(filter[0], filter[1], filter[2], filter[3]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
add(filters, refresh = true) {
|
||||
|
|
|
|||
|
|
@ -26,13 +26,13 @@ frappe.throw = function(msg) {
|
|||
|
||||
frappe.confirm = function(message, confirm_action, reject_action) {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __("Confirm"),
|
||||
primary_action_label: __("Yes"),
|
||||
title: __("Confirm", null, "Title of confirmation dialog"),
|
||||
primary_action_label: __("Yes", null, "Approve confirmation dialog"),
|
||||
primary_action: () => {
|
||||
confirm_action && confirm_action();
|
||||
d.hide();
|
||||
},
|
||||
secondary_action_label: __("No"),
|
||||
secondary_action_label: __("No", null, "Dismiss confirmation dialog"),
|
||||
secondary_action: () => d.hide(),
|
||||
});
|
||||
|
||||
|
|
@ -88,9 +88,9 @@ frappe.prompt = function(fields, callback, title, primary_label) {
|
|||
if(!$.isArray(fields)) fields = [fields];
|
||||
var d = new frappe.ui.Dialog({
|
||||
fields: fields,
|
||||
title: title || __("Enter Value"),
|
||||
title: title || __("Enter Value", null, "Title of prompt dialog"),
|
||||
});
|
||||
d.set_primary_action(primary_label || __("Submit"), function() {
|
||||
d.set_primary_action(primary_label || __("Submit", null, "Primary action of prompt dialog"), function() {
|
||||
var values = d.get_values();
|
||||
if(!values) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -377,11 +377,12 @@ frappe.ui.Page = class Page {
|
|||
});
|
||||
}
|
||||
|
||||
add_actions_menu_item(label, click, standard) {
|
||||
add_actions_menu_item(label, click, standard, shortcut) {
|
||||
return this.add_dropdown_item({
|
||||
label,
|
||||
click,
|
||||
standard,
|
||||
shortcut,
|
||||
parent: this.actions,
|
||||
show_parent: false
|
||||
});
|
||||
|
|
@ -409,6 +410,9 @@ frappe.ui.Page = class Page {
|
|||
parent.parent().removeClass("hide");
|
||||
}
|
||||
|
||||
let $link = this.is_in_group_button_dropdown(parent, 'li > a.grey-link', label);
|
||||
if ($link) return $link;
|
||||
|
||||
let $li;
|
||||
let $icon = ``;
|
||||
|
||||
|
|
@ -440,9 +444,8 @@ frappe.ui.Page = class Page {
|
|||
</li>
|
||||
`);
|
||||
}
|
||||
var $link = $li.find("a").on("click", click);
|
||||
|
||||
if (this.is_in_group_button_dropdown(parent, 'li > a.grey-link', label)) return;
|
||||
$link = $li.find("a").on("click", click);
|
||||
|
||||
if (standard) {
|
||||
$li.appendTo(parent);
|
||||
|
|
@ -508,7 +511,7 @@ frappe.ui.Page = class Page {
|
|||
let item = $(this).html();
|
||||
return $(item).attr('data-label') === label;
|
||||
});
|
||||
return result.length > 0;
|
||||
return result.length > 0 && result;
|
||||
}
|
||||
|
||||
clear_btn_group(parent) {
|
||||
|
|
|
|||
|
|
@ -271,18 +271,19 @@ class ShortcutDialog extends WidgetDialog {
|
|||
}
|
||||
|
||||
process_data(data) {
|
||||
let stats_filter = {};
|
||||
|
||||
if (this.dialog.get_value("type") == "DocType" && this.filter_group) {
|
||||
let filters = this.filter_group.get_filters();
|
||||
let stats_filter = null;
|
||||
|
||||
if (filters.length) {
|
||||
stats_filter = {};
|
||||
filters.forEach((arr) => {
|
||||
stats_filter[arr[1]] = [arr[2], arr[3]];
|
||||
});
|
||||
|
||||
data.stats_filter = JSON.stringify(stats_filter);
|
||||
stats_filter = JSON.stringify(stats_filter);
|
||||
}
|
||||
data.stats_filter = stats_filter;
|
||||
}
|
||||
|
||||
data.label = data.label
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ def make_boilerplate(dest, app_name):
|
|||
f.write(frappe.as_unicode(setup_template.format(**hooks)))
|
||||
|
||||
with open(os.path.join(dest, hooks.app_name, "requirements.txt"), "w") as f:
|
||||
f.write("frappe")
|
||||
f.write("# frappe -- https://github.com/frappe/frappe is installed via 'bench init'")
|
||||
|
||||
with open(os.path.join(dest, hooks.app_name, "README.md"), "w") as f:
|
||||
f.write(frappe.as_unicode("## {0}\n\n{1}\n\n#### License\n\n{2}".format(hooks.app_title,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"bootstrap": "4.5.0",
|
||||
"cliui": "^7.0.4",
|
||||
"cookie": "^0.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"cssnano": "^5.0.0",
|
||||
"driver.js": "^0.9.8",
|
||||
"express": "^4.17.1",
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
"frappe-gantt": "^0.5.0",
|
||||
"fuse.js": "^3.4.6",
|
||||
"highlight.js": "^10.4.1",
|
||||
"jquery": "2.2.4",
|
||||
"jquery": "3.5.0",
|
||||
"js-sha256": "^0.9.0",
|
||||
"jsbarcode": "^3.9.0",
|
||||
"localforage": "^1.9.0",
|
||||
|
|
@ -48,6 +48,7 @@
|
|||
"quagga": "^0.12.1",
|
||||
"quill": "2.0.0-dev.4",
|
||||
"quill-image-resize": "^3.0.9",
|
||||
"quill-magic-url": "^3.0.0",
|
||||
"qz-tray": "^2.0.8",
|
||||
"redis": "^3.1.1",
|
||||
"showdown": "^1.9.1",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue