浏览代码

[email] fixed test cases for bulk emails

version-14
Rushabh Mehta 10 年前
父节点
当前提交
99ca7d5f8a
共有 15 个文件被更改,包括 180 次插入109 次删除
  1. +6
    -6
      frappe/__init__.py
  2. +2
    -2
      frappe/core/doctype/communication/communication.py
  3. +88
    -54
      frappe/email/bulk.py
  4. +3
    -3
      frappe/email/doctype/bulk_email/bulk_email.json
  5. +12
    -0
      frappe/email/doctype/bulk_email/test_bulk_email.py
  6. +1
    -1
      frappe/email/doctype/email_alert/email_alert.py
  7. +22
    -22
      frappe/email/doctype/email_alert/test_email_alert.py
  8. +1
    -1
      frappe/model/base_document.py
  9. +2
    -2
      frappe/model/create_new.py
  10. +1
    -0
      frappe/patches.txt
  11. +10
    -0
      frappe/patches/v5_0/rename_ref_type_fieldnames.py
  12. +4
    -0
      frappe/patches/v5_0/v4_to_v5.py
  13. +1
    -3
      frappe/templates/includes/comments/comments.py
  14. +8
    -11
      frappe/tests/test_email.py
  15. +19
    -4
      frappe/utils/verified_command.py

+ 6
- 6
frappe/__init__.py 查看文件

@@ -269,7 +269,7 @@ def get_request_header(key, default=None):
return request.headers.get(key, default)

def sendmail(recipients=(), sender="", subject="No Subject", message="No Message",
as_markdown=False, bulk=False, ref_doctype=None, ref_docname=None,
as_markdown=False, bulk=False, reference_doctype=None, reference_name=None,
unsubscribe_url=False, attachments=None, content=None, doctype=None, name=None, reply_to=None):
"""Send email using user's default **Email Account** or global default **Email Account**.

@@ -280,8 +280,8 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message
:param message: (or `content`) Email Content.
:param as_markdown: Convert content markdown to HTML.
:param bulk: Send via scheduled email sender **Bulk Email**. Don't send immediately.
:param ref_doctype: (or `doctype`) Append as communication to this DocType.
:param ref_docname: (or `name`) Append as communication to this document name.
:param reference_doctype: (or `doctype`) Append as communication to this DocType.
:param reference_name: (or `name`) Append as communication to this document name.
:param unsubscribe_url: Unsubscribe url with options email, doctype, name. e.g. `/api/method/unsubscribe?email={email}&name={name}`
:param attachments: List of attachments.
:param reply_to: Reply-To email id.
@@ -290,8 +290,8 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message
if bulk:
import frappe.email.bulk
frappe.email.bulk.send(recipients=recipients, sender=sender,
subject=subject, message=content or message, ref_doctype = doctype or ref_doctype,
ref_docname = name or ref_docname, unsubscribe_url=unsubscribe_url, attachments=attachments,
subject=subject, message=content or message, reference_doctype = doctype or reference_doctype,
reference_name = name or reference_name, unsubscribe_url=unsubscribe_url, attachments=attachments,
reply_to=reply_to)

else:
@@ -861,7 +861,7 @@ def add_version(doc):
A **Version** is a JSON dump of the current document state."""
get_doc({
"doctype": "Version",
"ref_doctype": doc.doctype,
"reference_doctype": doc.doctype,
"docname": doc.name,
"doclist_json": as_json(doc.as_dict())
}).insert(ignore_permissions=True)


+ 2
- 2
frappe/core/doctype/communication/communication.py 查看文件

@@ -90,8 +90,8 @@ class Communication(Document):
"sender": mail.sender,
"recipient": mail.recipients[0],
"message": mail.as_string(),
"ref_doctype": self.reference_doctype,
"ref_docname": self.reference_name
"reference_doctype": self.reference_doctype,
"reference_name": self.reference_name
}).insert(ignore_permissions=True)

def notify(self, mail, except_sender=False):


+ 88
- 54
frappe/email/bulk.py 查看文件

@@ -4,75 +4,107 @@
from __future__ import unicode_literals
import frappe
import HTMLParser
from urllib import quote_plus
from frappe import msgprint, throw, _
from frappe.email.smtp import SMTPServer, get_outgoing_email_account
from frappe.email.email_body import get_email, get_formatted_html
from frappe.utils.verified_command import get_signed_params, verify_request
from html2text import html2text
from frappe.utils import get_url, nowdate

class BulkLimitCrossedError(frappe.ValidationError): pass

def send(recipients=None, sender=None, doctype='User', email_field='email',
subject='[No Subject]', message='[No Content]', ref_doctype=None,
ref_docname=None, unsubscribe_url=True, attachments=None, reply_to=None):
def send(recipients=None, sender=None, subject=None, message=None, reference_doctype=None,
reference_name=None, unsubscribe_method=None, unsubscribe_params=None,
attachments=None, reply_to=None, footer_message=None):
"""Add email to sending queue (Bulk Email)

if not unsubscribe_url:
unsubscribe_url = "/api/method/frappe.email.bulk.unsubscribe?doctype={doctype}&name={name}&email={email}"
:param recipients: List of recipients.
:param sender: Email sender.
:param subject: Email subject.
:param message: Email message.
:param reference_doctype: Reference DocType of caller document.
:param reference_name: Reference name of caller document.
:param unsubscribe_method: URL method for unsubscribe. Default is `/api/method/frappe.email.bulk.unsubscribe`.
:param unsubscribe_params: additional params for unsubscribed links. default are name, doctype, email
:param attachments: Attachments to be sent.
:param reply_to: Reply to be captured here (default inbox)"""

def check_bulk_limit(new_mails):
this_month = frappe.db.sql("""select count(*) from `tabBulk Email` where
month(creation)=month(%s)""" % nowdate())[0][0]

# No limit for own email settings
smtp_server = SMTPServer()
if smtp_server.email_account and not getattr(smtp_server.email_account,
"from_site_config", False) or frappe.flags.in_test:
monthly_bulk_mail_limit = frappe.conf.get('monthly_bulk_mail_limit') or 500

if (this_month + len(recipients)) > monthly_bulk_mail_limit:
throw(_("Bulk email limit {0} crossed").format(monthly_bulk_mail_limit),
BulkLimitCrossedError)

def update_message(formatted, unsubscribe_url, email):
updated = formatted
my_unsubscribe_url = unsubscribe_url.format(email=quote_plus(email), doctype=quote_plus(ref_doctype),
name=quote_plus(ref_docname))

unsubscribe_link = """<div style="padding: 7px; border-top: 1px solid #aaa; margin-top: 17px;">
<small><a href="{base_url}/{url}">{message}</a></small></div>""".format(base_url = get_url(),
url = my_unsubscribe_url, message = _("Unsubscribe from this list"))

updated = updated.replace("<!--unsubscribe link here-->", unsubscribe_link)

return updated
if not unsubscribe_method:
unsubscribe_method = "/api/method/frappe.email.bulk.unsubscribe"

if not recipients:
recipients = []
return

if not sender or sender == "Administrator":
email_account = get_outgoing_email_account()
sender = email_account.get("sender") or email_account.email_id

check_bulk_limit(len(recipients))
check_bulk_limit(recipients)

formatted = get_formatted_html(subject, message)

try:
text_content = html2text(formatted)
except HTMLParser.HTMLParseError:
text_content = "See html attachment"

unsubscribed = [d.email for d in frappe.db.get_all("Email Unsubscribe", "email",
{"reference_doctype": ref_doctype, "reference_name": ref_docname})]
{"reference_doctype": reference_doctype, "reference_name": reference_name})]

for email in filter(None, list(set(recipients))):
if email not in unsubscribed:
unsubscribe_url = get_unsubcribed_url(reference_doctype, reference_name, email,
unsubscribe_method, unsubscribe_params)

for r in filter(None, list(set(recipients))):
if r not in unsubscribed:
# add to queue
updated = update_message(formatted, unsubscribe_url)
try:
text_content = html2text(updated)
except HTMLParser.HTMLParseError:
text_content = "[See html attachment]"
updated = add_unsubscribe_link(formatted, email, reference_doctype, reference_name,
unsubscribe_url, footer_message)

text_content += "\n" + _("Unsubscribe link: {0}").format(unsubscribe_url)

add(email, sender, subject, updated, text_content, reference_doctype, reference_name, attachments, reply_to)

add(r, sender, subject, updated, text_content, ref_doctype, ref_docname, attachments, reply_to)
def check_bulk_limit(recipients):
this_month = frappe.db.sql("""select count(*) from `tabBulk Email` where
month(creation)=month(%s)""" % nowdate())[0][0]

# No limit for own email settings
smtp_server = SMTPServer()
if smtp_server.email_account and not getattr(smtp_server.email_account,
"from_site_config", False) or frappe.flags.in_test:
monthly_bulk_mail_limit = frappe.conf.get('monthly_bulk_mail_limit') or 500

if (this_month + len(recipients)) > monthly_bulk_mail_limit:
throw(_("Bulk email limit {0} crossed").format(monthly_bulk_mail_limit),
BulkLimitCrossedError)

def add_unsubscribe_link(message, email, reference_doctype, reference_name, unsubscribe_url, footer_message):
unsubscribe_link = """<div style="padding: 7px; border-top: 1px solid #aaa; margin-top: 17px;">
<small>{footer_message}
<a href="{unsubscribe_url}">{unsubscribe_message}</a></small></div>""".format(unsubscribe_url = unsubscribe_url,
unsubscribe_message = _("Unsubscribe from this list"), footer_message= footer_message or "")

message = message.replace("<!--unsubscribe link here-->", unsubscribe_link)

return message

def get_unsubcribed_url(reference_doctype, reference_name, email, unsubscribe_method, unsubscribe_params):
params = {"email": email.encode("utf-8"),
"doctype": reference_doctype.encode("utf-8"),
"name": reference_name.encode("utf-8")}
if unsubscribe_params:
params.update(unsubscribe_params)

query_string = get_signed_params(params)

# for test
frappe.local.flags.signed_query_string = query_string

return get_url(unsubscribe_method + "?" + get_signed_params(params))

def add(email, sender, subject, formatted, text_content=None,
ref_doctype=None, ref_docname=None, attachments=None, reply_to=None):
reference_doctype=None, reference_name=None, attachments=None, reply_to=None):
"""add to bulk mail queue"""
e = frappe.new_doc('Bulk Email')
e.sender = sender
@@ -85,27 +117,29 @@ def add(email, sender, subject, formatted, text_content=None,
# bad email id - don't add to queue
return

e.ref_doctype = ref_doctype
e.ref_docname = ref_docname
e.reference_doctype = reference_doctype
e.reference_name = reference_name
e.insert(ignore_permissions=True)

@frappe.whitelist(allow_guest=True)
def unsubscribe(doctype, name, email):
# unsubsribe from comments and communications
frappe.g
if not verify_request():
return

frappe.get_doc({
"doctype": "Email Unsubscribe",
"email": email,
"reference_doctype": doctype,
"reference_name": name
}).insert(ignore_permissions=True)

if not frappe.form_dict.get("from_test"):
frappe.db.commit()
frappe.db.commit()

return_unsubscribed_page(email)

def return_unsubscribed_page(email):
frappe.local.message_title = _("Unsubscribed")
frappe.local.message = "<h3>" + _("Unsubscribed") + "</h3><p>" \
+ _("{0} has been successfully unsubscribed").fomrat(email) + "</p>"

frappe.response['type'] = 'page'
frappe.response['page_name'] = 'message.html'
frappe.respond_as_web_page(_("Unsubscribed"), _("{0} has been successfully unsubscribed").format(email))

def flush(from_test=False):
"""flush email queue, every time: called from scheduler"""


+ 3
- 3
frappe/email/doctype/bulk_email/bulk_email.json 查看文件

@@ -44,7 +44,7 @@
"permlevel": 0
},
{
"fieldname": "ref_doctype",
"fieldname": "reference_doctype",
"fieldtype": "Link",
"label": "Reference DocType",
"options": "DocType",
@@ -53,7 +53,7 @@
"reqd": 0
},
{
"fieldname": "ref_docname",
"fieldname": "reference_name",
"fieldtype": "Data",
"label": "Reference DocName",
"permlevel": 0,
@@ -64,7 +64,7 @@
"icon": "icon-envelope",
"idx": 1,
"in_create": 1,
"modified": "2015-01-23 04:32:39.175147",
"modified": "2015-03-19 05:36:16.813340",
"modified_by": "Administrator",
"module": "Email",
"name": "Bulk Email",


+ 12
- 0
frappe/email/doctype/bulk_email/test_bulk_email.py 查看文件

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals

import frappe
import unittest

# test_records = frappe.get_test_records('Bulk Email')

class TestBulkEmail(unittest.TestCase):
pass

+ 1
- 1
frappe/email/doctype/email_alert/email_alert.py 查看文件

@@ -91,5 +91,5 @@ def evaluate_alert(doc, alert, event):

frappe.sendmail(recipients=recipients, subject=alert.subject,
message= frappe.render_template(alert.message, {"doc": doc, "alert":alert}),
bulk=True, ref_doctype = doc.doctype, ref_docname = doc.name,
bulk=True, reference_doctype = doc.doctype, reference_name = doc.name,
attachments = [frappe.attach_print(doc.doctype, doc.name)] if alert.attach_print else None)

+ 22
- 22
frappe/email/doctype/email_alert/test_email_alert.py 查看文件

@@ -20,16 +20,16 @@ class TestEmailAlert(unittest.TestCase):
comment.comment = "test"
comment.insert(ignore_permissions=True)

self.assertTrue(frappe.db.get_value("Bulk Email", {"ref_doctype": "Comment",
"ref_docname": comment.name, "status":"Not Sent"}))
self.assertTrue(frappe.db.get_value("Bulk Email", {"reference_doctype": "Comment",
"reference_name": comment.name, "status":"Not Sent"}))

frappe.db.sql("""delete from `tabBulk Email`""")

comment.description = "test"
comment.save()

self.assertTrue(frappe.db.get_value("Bulk Email", {"ref_doctype": "Comment",
"ref_docname": comment.name, "status":"Not Sent"}))
self.assertTrue(frappe.db.get_value("Bulk Email", {"reference_doctype": "Comment",
"reference_name": comment.name, "status":"Not Sent"}))

def test_condition(self):
event = frappe.new_doc("Event")
@@ -38,14 +38,14 @@ class TestEmailAlert(unittest.TestCase):
event.starts_on = "2014-06-06 12:00:00"
event.insert()

self.assertFalse(frappe.db.get_value("Bulk Email", {"ref_doctype": "Event",
"ref_docname": event.name, "status":"Not Sent"}))
self.assertFalse(frappe.db.get_value("Bulk Email", {"reference_doctype": "Event",
"reference_name": event.name, "status":"Not Sent"}))

event.event_type = "Public"
event.save()

self.assertTrue(frappe.db.get_value("Bulk Email", {"ref_doctype": "Event",
"ref_docname": event.name, "status":"Not Sent"}))
self.assertTrue(frappe.db.get_value("Bulk Email", {"reference_doctype": "Event",
"reference_name": event.name, "status":"Not Sent"}))

def test_value_changed(self):
event = frappe.new_doc("Event")
@@ -54,20 +54,20 @@ class TestEmailAlert(unittest.TestCase):
event.starts_on = "2014-06-06 12:00:00"
event.insert()

self.assertFalse(frappe.db.get_value("Bulk Email", {"ref_doctype": "Event",
"ref_docname": event.name, "status":"Not Sent"}))
self.assertFalse(frappe.db.get_value("Bulk Email", {"reference_doctype": "Event",
"reference_name": event.name, "status":"Not Sent"}))

event.subject = "test 1"
event.save()

self.assertFalse(frappe.db.get_value("Bulk Email", {"ref_doctype": "Event",
"ref_docname": event.name, "status":"Not Sent"}))
self.assertFalse(frappe.db.get_value("Bulk Email", {"reference_doctype": "Event",
"reference_name": event.name, "status":"Not Sent"}))

event.description = "test"
event.save()

self.assertTrue(frappe.db.get_value("Bulk Email", {"ref_doctype": "Event",
"ref_docname": event.name, "status":"Not Sent"}))
self.assertTrue(frappe.db.get_value("Bulk Email", {"reference_doctype": "Event",
"reference_name": event.name, "status":"Not Sent"}))

def test_date_changed(self):
event = frappe.new_doc("Event")
@@ -76,23 +76,23 @@ class TestEmailAlert(unittest.TestCase):
event.starts_on = "2014-01-01 12:00:00"
event.insert()

self.assertFalse(frappe.db.get_value("Bulk Email", {"ref_doctype": "Event",
"ref_docname": event.name, "status":"Not Sent"}))
self.assertFalse(frappe.db.get_value("Bulk Email", {"reference_doctype": "Event",
"reference_name": event.name, "status":"Not Sent"}))

frappe.utils.scheduler.trigger(frappe.local.site, "daily", now=True)

# not today, so no alert
self.assertFalse(frappe.db.get_value("Bulk Email", {"ref_doctype": "Event",
"ref_docname": event.name, "status":"Not Sent"}))
self.assertFalse(frappe.db.get_value("Bulk Email", {"reference_doctype": "Event",
"reference_name": event.name, "status":"Not Sent"}))

event.starts_on = frappe.utils.add_days(frappe.utils.nowdate(), 2) + " 12:00:00"
event.save()

self.assertFalse(frappe.db.get_value("Bulk Email", {"ref_doctype": "Event",
"ref_docname": event.name, "status":"Not Sent"}))
self.assertFalse(frappe.db.get_value("Bulk Email", {"reference_doctype": "Event",
"reference_name": event.name, "status":"Not Sent"}))

frappe.utils.scheduler.trigger(frappe.local.site, "daily", now=True)

# today so show alert
self.assertTrue(frappe.db.get_value("Bulk Email", {"ref_doctype": "Event",
"ref_docname": event.name, "status":"Not Sent"}))
self.assertTrue(frappe.db.get_value("Bulk Email", {"reference_doctype": "Event",
"reference_name": event.name, "status":"Not Sent"}))

+ 1
- 1
frappe/model/base_document.py 查看文件

@@ -241,7 +241,7 @@ class BaseDocument(object):
values = ", ".join(["%s"] * len(columns))
), d.values())
except Exception, e:
if e.args[0]==1062 and "PRIMARY" in e.message:
if e.args[0]==1062 and "PRIMARY" in e.args[1]:
if self.meta.autoname=="hash":
self.name = None
self.db_insert()


+ 2
- 2
frappe/model/create_new.py 查看文件

@@ -74,9 +74,9 @@ def get_default_value(df, defaults, user_permissions, parent_doc):
# default value based on another document
ref_doctype = df.default[1:]
ref_fieldname = ref_doctype.lower().replace(" ", "_")
ref_docname = parent_doc.get(ref_fieldname) if parent_doc else frappe.db.get_default(ref_fieldname)
reference_name = parent_doc.get(ref_fieldname) if parent_doc else frappe.db.get_default(ref_fieldname)

default_value = frappe.db.get_value(ref_doctype, ref_docname, df.fieldname)
default_value = frappe.db.get_value(ref_doctype, reference_name, df.fieldname)
is_allowed_default_value = (not user_permissions_exist or
(default_value in user_permissions.get(df.options, [])))



+ 1
- 0
frappe/patches.txt 查看文件

@@ -65,3 +65,4 @@ frappe.patches.v5_0.fix_feed
frappe.patches.v5_0.update_shared
frappe.patches.v5_0.bookmarks_to_stars
frappe.patches.v5_0.style_settings_to_website_theme
frappe.patches.v5_0.rename_ref_type_fieldnames

+ 10
- 0
frappe/patches/v5_0/rename_ref_type_fieldnames.py 查看文件

@@ -0,0 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt

from __future__ import unicode_literals
import frappe

def execute():
frappe.db.sql("alter table `tabBulk Email` change `ref_docname` `reference_name` varchar(255)")
frappe.db.sql("alter table `tabBulk Email` change `reference_doctype` `reference_doctype` varchar(255)")
frappe.reload_doctype("Bulk Email")

+ 4
- 0
frappe/patches/v5_0/v4_to_v5.py 查看文件

@@ -1,3 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals
import frappe

def execute():


+ 1
- 3
frappe/templates/includes/comments/comments.py 查看文件

@@ -53,11 +53,9 @@ def add_comment(args=None):
from frappe.email.bulk import send

send(recipients=recipients,
doctype='Comment',
email_field='comment_by',
subject = _("New comment on {0} {1}").format(comment.comment_doctype, comment.comment_docname),
message = message,
ref_doctype=comment.comment_doctype, ref_docname=comment.comment_docname)
reference_doctype=comment.comment_doctype, reference_name=comment.comment_docname)

template = frappe.get_template("templates/includes/comments/comment.html")



+ 8
- 11
frappe/tests/test_email.py 查看文件

@@ -11,7 +11,7 @@ make_test_records("Email Account")

class TestEmail(unittest.TestCase):
def setUp(self):
frappe.db.sql("""update tabUser set unsubscribed=0""")
frappe.db.sql("""delete from `tabEmail Unsubscribe`""")
frappe.db.sql("""delete from `tabBulk Email`""")

def test_send(self):
@@ -22,7 +22,7 @@ class TestEmail(unittest.TestCase):
from frappe.email.bulk import send
send(recipients = ['test@example.com', 'test1@example.com'],
sender="admin@example.com",
doctype='User', email_field='email',
reference_doctype='User', reference_name='Administrator',
subject='Testing Bulk', message='This is a bulk mail!')

bulk = frappe.db.sql("""select * from `tabBulk Email` where status='Not Sent'""", as_dict=1)
@@ -42,17 +42,14 @@ class TestEmail(unittest.TestCase):

def test_unsubscribe(self):
from frappe.email.bulk import unsubscribe, send
frappe.local.form_dict = frappe._dict({
'email':'test@example.com',
'type':'User',
'email_field':'email',
"from_test": True
})
unsubscribe()
unsubscribe(doctype="User", name="Administrator", email="test@example.com")

self.assertTrue(frappe.db.get_value("Email Unsubscribe",
{"reference_doctype": "User", "reference_name": "Administrator", "email": "test@example.com"}))

send(recipients = ['test@example.com', 'test1@example.com'],
sender="admin@example.com",
doctype='User', email_field='email',
reference_doctype='User', reference_name= "Administrator",
subject='Testing Bulk', message='This is a bulk mail!')

bulk = frappe.db.sql("""select * from `tabBulk Email` where status='Not Sent'""",
@@ -67,7 +64,7 @@ class TestEmail(unittest.TestCase):
self.assertRaises(BulkLimitCrossedError, send,
recipients=['test@example.com']*1000,
sender="admin@example.com",
doctype='User', email_field='email',
reference_doctype = "User", reference_name="Administrator",
subject='Testing Bulk', message='This is a bulk mail!')




+ 19
- 4
frappe/utils/verified_command.py 查看文件

@@ -4,25 +4,40 @@
from __future__ import unicode_literals
import hmac
import urllib
from frappe import _

import frappe
import frappe.utils

def get_signed_params(params):
"""Sign a url by appending `&_signature=xxxxx` to given params (string or dict).

:param params: String or dict of parameters."""
if not isinstance(params, basestring):
params = urllib.urlencode(params)

signature = hmac.new(params)
signature.update(get_secret())
return params + "&_signature=" + signature.hexdigest()

def get_secret():
return frappe.local.conf.get("secret") or frappe.db.get_value("User", "Administrator", "creation")
return frappe.local.conf.get("secret") or str(frappe.db.get_value("User", "Administrator", "creation"))

def verify_request():
params, signature = frappe.request.query_string.split("&_signature=")
given_signature = hmac.new(params)
"""Verify if the incoming signed request if it is correct."""
query_string = frappe.request.query_string if hasattr(frappe.request, "query_string") \
else frappe.local.flags.signed_query_string

params, signature = query_string.split("&_signature=")

given_signature = hmac.new(params.encode("utf-8"))
given_signature.update(get_secret())
return signature == given_signature
valid = signature == given_signature.hexdigest()

if not valid:
frappe.respond_as_web_page(_("Invalid Link"),
_("This link is invalid or expired. Please make sure you have pasted correctly."))
return valid

def get_url(cmd, params, nonce=None, secret=None):
if not nonce:


正在加载...
取消
保存