Merge branch 'develop' of https://github.com/frappe/frappe into refactor-website

This commit is contained in:
Suraj Shetty 2021-06-14 12:24:50 +05:30
commit 3cbc7dfb92
20 changed files with 968 additions and 1023 deletions

View file

@ -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');
});

View file

@ -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)

View file

@ -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:

View 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)

View file

@ -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

View file

@ -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()

View file

@ -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:

View file

@ -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:

View file

@ -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):

View file

@ -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.

View file

@ -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'
};

View file

@ -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',

View file

@ -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");

View file

@ -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) {

View file

@ -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;

View file

@ -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) {

View file

@ -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

View file

@ -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,

View file

@ -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",

1049
yarn.lock

File diff suppressed because it is too large Load diff