Merge pull request #15182 from netchampfaris/cleanup-newsletter

fix: Newsletter Enhancements
This commit is contained in:
Faris Ansari 2021-12-07 18:38:09 +05:30 committed by GitHub
commit 19fb8cd409
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 490 additions and 352 deletions

View file

@ -283,9 +283,14 @@ class SendMailContext:
if attachment.get('fcontent'):
continue
fid = attachment.get("fid")
if fid:
_file = frappe.get_doc("File", fid)
file_filters = {}
if attachment.get('fid'):
file_filters['name'] = attachment.get('fid')
elif attachment.get('file_url'):
file_filters['file_url'] = attachment.get('file_url')
if file_filters:
_file = frappe.get_doc("File", file_filters)
fcontent = _file.get_content()
attachment.update({
'fname': _file.file_name,
@ -293,6 +298,7 @@ class SendMailContext:
'parent': message_obj
})
attachment.pop("fid", None)
attachment.pop("file_url", None)
add_attachment(**attachment)
elif attachment.get("print_format_attachment") == 1:
@ -503,7 +509,7 @@ class QueueBuilder:
if self._attachments:
# store attachments with fid or print format details, to be attached on-demand later
for att in self._attachments:
if att.get('fid'):
if att.get('fid') or att.get('file_url'):
attachments.append(att)
elif att.get("print_format_attachment") == 1:
if not att.get('lang', None):

View file

@ -4,69 +4,137 @@
frappe.ui.form.on('Newsletter', {
refresh(frm) {
let doc = frm.doc;
if (!doc.__islocal && !cint(doc.email_sent) && !doc.__unsaved
&& in_list(frappe.boot.user.can_write, doc.doctype)) {
frm.add_custom_button(__('Send Now'), function() {
frappe.confirm(__("Do you really want to send this email newsletter?"), function() {
frm.call('send_emails').then(() => {
frm.refresh();
});
let can_write = in_list(frappe.boot.user.can_write, doc.doctype);
if (!frm.is_new() && !frm.is_dirty() && !doc.email_sent && can_write) {
frm.add_custom_button(__('Send a test email'), () => {
frm.events.send_test_email(frm);
}, __('Preview'));
frm.add_custom_button(__('Check broken links'), () => {
frm.dashboard.set_headline(__('Checking broken links...'));
frm.call('find_broken_links').then(r => {
frm.dashboard.set_headline('');
let links = r.message;
if (links && links.length) {
let html = '<ul>' + links.map(link => `<li>${link}</li>`).join('') + '</ul>';
frm.dashboard.set_headline(__("Following links are broken in the email content: {0}", [html]));
} else {
frm.dashboard.set_headline(__("No broken links found in the email content"));
setTimeout(() => {
frm.dashboard.set_headline('');
}, 3000);
}
});
}, "fa fa-play", "btn-success");
}, __('Preview'));
frm.add_custom_button(__('Send now'), () => {
if (frm.doc.schedule_send) {
frappe.confirm(__("This newsletter was scheduled to send on a later date. Are you sure you want to send it now?"), function () {
frm.call('send_emails').then(() => frm.refresh());
});
return;
}
frappe.confirm(__("Are you sure you want to send this newsletter now?"), function () {
frm.call('send_emails').then(() => frm.refresh());
});
}, __('Send'));
frm.add_custom_button(__('Schedule sending'), () => {
frm.events.schedule_send_dialog(frm);
}, __('Send'));
}
frm.events.setup_dashboard(frm);
frm.events.setup_sending_status(frm);
if (doc.__islocal && !doc.send_from) {
if (frm.is_new() && !doc.sender_email) {
let { fullname, email } = frappe.user_info(doc.owner);
frm.set_value('send_from', `${fullname} <${email}>`);
frm.set_value('sender_email', email);
frm.set_value('sender_name', fullname);
}
frm.trigger('update_schedule_message');
},
onload_post_render(frm) {
frm.trigger('setup_schedule_send');
},
setup_schedule_send(frm) {
let today = new Date();
// setting datepicker options to set min date & min time
today.setHours(today.getHours() + 1 );
frm.get_field('schedule_send').$input.datepicker({
maxMinutes: 0,
minDate: today,
timeFormat: 'hh:00:00',
onSelect: function (fd, d, picker) {
if (!d) return;
var date = d.toDateString();
if (date === today.toDateString()) {
picker.update({
minHours: (today.getHours() + 1)
});
} else {
picker.update({
minHours: 0
});
}
frm.get_field('schedule_send').$input.trigger('change');
schedule_send_dialog(frm) {
let hours = frappe.utils.range(24);
let time_slots = hours.map(hour => {
return `${(hour + '').padStart(2, '0')}:00`;
});
let d = new frappe.ui.Dialog({
title: __('Schedule Newsletter'),
fields: [
{
label: __('Date'),
fieldname: 'date',
fieldtype: 'Date',
options: {
minDate: new Date()
}
},
{
label: __('Time'),
fieldname: 'time',
fieldtype: 'Select',
options: time_slots,
},
],
primary_action_label: __('Schedule'),
primary_action({ date, time }) {
frm.set_value('schedule_sending', 1);
frm.set_value('schedule_send', `${date} ${time}:00`);
d.hide();
frm.save();
},
secondary_action_label: __('Cancel Scheduling'),
secondary_action() {
frm.set_value('schedule_sending', 0);
frm.set_value('schedule_send', '');
d.hide();
frm.save();
}
});
if (frm.doc.schedule_sending) {
let parts = frm.doc.schedule_send.split(' ');
if (parts.length === 2) {
let [date, time] = parts;
d.set_value('date', date);
d.set_value('time', time.slice(0, 5));
}
}
d.show();
},
const $tp = frm.get_field('schedule_send').datepicker.timepicker;
$tp.$minutes.parent().css('display', 'none');
$tp.$minutesText.css('display', 'none');
$tp.$minutesText.prev().css('display', 'none');
$tp.$seconds.parent().css('display', 'none');
send_test_email(frm) {
let d = new frappe.ui.Dialog({
title: __('Send Test Email'),
fields: [
{
label: __('Email'),
fieldname: 'email',
fieldtype: 'Data',
options: 'Email',
}
],
primary_action_label: __('Send'),
primary_action({ email }) {
d.get_primary_btn().text(__('Sending...')).prop('disabled', true);
frm.call('send_test_email', { email })
.then(() => {
d.get_primary_btn().text(__('Send again')).prop('disabled', false);
});
}
});
d.show();
},
setup_dashboard(frm) {
if(!frm.doc.__islocal && cint(frm.doc.email_sent)
if (!frm.doc.__islocal && cint(frm.doc.email_sent)
&& frm.doc.__onload && frm.doc.__onload.status_count) {
var stat = frm.doc.__onload.status_count;
var total = frm.doc.scheduled_to_send;
if(total) {
$.each(stat, function(k, v) {
if (total) {
$.each(stat, function (k, v) {
stat[k] = flt(v * 100 / total, 2) + '%';
});
@ -94,5 +162,58 @@ frappe.ui.form.on('Newsletter', {
]);
}
}
},
setup_sending_status(frm) {
frm.call('get_sending_status').then(r => {
if (r.message) {
frm.events.update_sending_progress(frm, r.message.sent, r.message.total);
}
if (r.message.sent >= r.message.total) {
return;
}
if (frm.sending_status) return;
frm.sending_status = setInterval(() => {
if (frm.doc.email_sent && frm.$wrapper.is(':visible')) {
frm.call('get_sending_status').then(r => {
if (r.message) {
let { sent, total } = r.message;
frm.events.update_sending_progress(frm, sent, total);
if (sent >= total) {
clearInterval(frm.sending_status);
frm.sending_status = null;
return;
}
}
});
}
}, 5000);
});
},
update_sending_progress(frm, sent, total) {
if (sent >= total) {
frm.dashboard.hide_progress();
return;
}
frm.dashboard.show_progress(__('Sending emails'), sent * 100 / total, __("{0} of {1} sent", [sent, total]));
},
on_hide(frm) {
if (frm.sending_status) {
clearInterval(frm.sending_status);
frm.sending_status = null;
}
},
update_schedule_message(frm) {
if (!frm.doc.email_sent && frm.doc.schedule_send) {
let datetime = frappe.datetime.global_date_format(frm.doc.schedule_send);
frm.dashboard.set_headline_alert(__('This newsletter is scheduled to be sent on {0}', [datetime.bold()]));
} else {
frm.dashboard.clear_headline();
}
}
});

View file

@ -7,48 +7,59 @@
"document_type": "Other",
"engine": "InnoDB",
"field_order": [
"status_section",
"email_sent_at",
"column_break_3",
"total_recipients",
"column_break_12",
"email_sent",
"from_section",
"sender_name",
"column_break_5",
"sender_email",
"column_break_7",
"send_from",
"schedule_sending",
"schedule_send",
"recipients",
"email_group",
"email_sent",
"newsletter_content",
"subject_section",
"subject",
"newsletter_content",
"content_type",
"message",
"message_md",
"message_html",
"section_break_13",
"attachments",
"send_unsubscribe_link",
"send_attachments",
"column_break_9",
"published",
"send_webview_link",
"route",
"test_the_newsletter",
"test_email_id",
"test_send",
"scheduled_to_send"
"schedule_settings_section",
"scheduled_to_send",
"schedule_sending",
"schedule_send",
"publish_as_a_web_page_section",
"published",
"route"
],
"fields": [
{
"fieldname": "email_group",
"fieldtype": "Table",
"in_standard_filter": 1,
"label": "Email Group",
"options": "Newsletter Email Group"
"label": "Audience",
"options": "Newsletter Email Group",
"reqd": 1
},
{
"fieldname": "send_from",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"label": "Sender"
"label": "Sender",
"read_only": 1
},
{
"default": "0",
"fieldname": "email_sent",
"fieldtype": "Check",
"hidden": 1,
"label": "Email Sent",
"no_copy": 1,
"read_only": 1
@ -87,32 +98,12 @@
"label": "Published"
},
{
"depends_on": "published",
"fieldname": "route",
"fieldtype": "Data",
"hidden": 1,
"label": "Route",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "test_the_newsletter",
"fieldtype": "Section Break",
"label": "Testing"
},
{
"description": "A Lead with this Email Address should exist",
"fieldname": "test_email_id",
"fieldtype": "Data",
"label": "Test Email Address",
"options": "Email"
},
{
"depends_on": "eval: doc.test_email_id",
"fieldname": "test_send",
"fieldtype": "Button",
"label": "Test",
"options": "test_send"
},
{
"fieldname": "scheduled_to_send",
"fieldtype": "Int",
@ -122,21 +113,16 @@
{
"fieldname": "recipients",
"fieldtype": "Section Break",
"label": "Recipients"
"label": "To"
},
{
"depends_on": "eval: doc.schedule_sending",
"fieldname": "schedule_send",
"fieldtype": "Datetime",
"label": "Schedule Send",
"label": "Send Email At",
"read_only": 1,
"read_only_depends_on": "eval: doc.email_sent"
},
{
"default": "0",
"fieldname": "send_attachments",
"fieldtype": "Check",
"label": "Send Attachments"
},
{
"fieldname": "content_type",
"fieldtype": "Select",
@ -161,23 +147,87 @@
"default": "0",
"fieldname": "schedule_sending",
"fieldtype": "Check",
"label": "Schedule Sending",
"label": "Schedule sending at a later time",
"read_only_depends_on": "eval: doc.email_sent"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "published",
"fieldname": "send_webview_link",
"fieldtype": "Check",
"label": "Send Web View Link"
},
{
"fieldname": "section_break_13",
"fieldtype": "Section Break"
"fieldname": "from_section",
"fieldtype": "Section Break",
"label": "From"
},
{
"fieldname": "sender_name",
"fieldtype": "Data",
"label": "Sender Name"
},
{
"fieldname": "sender_email",
"fieldtype": "Data",
"label": "Sender Email",
"options": "Email",
"reqd": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"fieldname": "subject_section",
"fieldtype": "Section Break",
"label": "Subject"
},
{
"fieldname": "publish_as_a_web_page_section",
"fieldtype": "Section Break",
"label": "Publish as a web page"
},
{
"depends_on": "schedule_sending",
"fieldname": "schedule_settings_section",
"fieldtype": "Section Break",
"label": "Scheduled Sending"
},
{
"fieldname": "attachments",
"fieldtype": "Table",
"label": "Attachments",
"options": "Newsletter Attachment"
},
{
"fieldname": "email_sent_at",
"fieldtype": "Datetime",
"label": "Email Sent At",
"read_only": 1
},
{
"fieldname": "total_recipients",
"fieldtype": "Int",
"label": "Total Recipients",
"read_only": 1
},
{
"depends_on": "email_sent",
"fieldname": "status_section",
"fieldtype": "Section Break",
"label": "Status"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
}
],
"has_web_view": 1,
@ -187,7 +237,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 3,
"modified": "2021-02-22 14:33:56.095380",
"modified": "2021-12-06 20:09:37.963141",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",

View file

@ -15,13 +15,11 @@ from .exceptions import NewsletterAlreadySentError, NoRecipientFoundError, Newsl
class Newsletter(WebsiteGenerator):
def onload(self):
self.setup_newsletter_status()
def validate(self):
self.route = f"newsletters/{self.name}"
self.validate_sender_address()
self.validate_recipient_address()
self.validate_publishing()
@property
def newsletter_recipients(self) -> List[str]:
@ -30,29 +28,55 @@ class Newsletter(WebsiteGenerator):
return self._recipients
@frappe.whitelist()
def test_send(self):
test_emails = frappe.utils.split_emails(self.test_email_id)
self.queue_all(test_emails=test_emails)
frappe.msgprint(_("Test email sent to {0}").format(self.test_email_id))
def get_sending_status(self):
count_by_status = frappe.get_all("Email Queue",
filters={"reference_doctype": self.doctype, "reference_name": self.name},
fields=["status", "count(name) as count"],
group_by="status",
order_by="status"
)
sent = 0
total = 0
for row in count_by_status:
if row.status == "Sent":
sent = row.count
total += row.count
return {'sent': sent, 'total': total}
@frappe.whitelist()
def send_test_email(self, email):
test_emails = frappe.utils.validate_email_address(email, throw=True)
self.send_newsletter(emails=test_emails)
frappe.msgprint(_("Test email sent to {0}").format(email), alert=True)
@frappe.whitelist()
def find_broken_links(self):
from bs4 import BeautifulSoup
import requests
html = self.get_message()
soup = BeautifulSoup(html, "html.parser")
links = soup.find_all("a")
images = soup.find_all("img")
broken_links = []
for el in links + images:
url = el.attrs.get("href") or el.attrs.get("src")
try:
response = requests.head(url, verify=False, timeout=5)
if response.status_code >= 400:
broken_links.append(url)
except:
broken_links.append(url)
return broken_links
@frappe.whitelist()
def send_emails(self):
"""send emails to leads and customers"""
"""queue sending emails to recipients"""
self.schedule_sending = False
self.schedule_send = None
self.queue_all()
frappe.msgprint(_("Email queued to {0} recipients").format(len(self.newsletter_recipients)))
def setup_newsletter_status(self):
"""Setup analytical status for current Newsletter. Can be accessible from desk.
"""
if self.email_sent:
status_count = frappe.get_all("Email Queue",
filters={"reference_doctype": self.doctype, "reference_name": self.name},
fields=["status", "count(name)"],
group_by="status",
order_by="status",
as_list=True,
)
self.get("__onload").status_count = dict(status_count)
frappe.msgprint(_("Email queued to {0} recipients").format(self.total_recipients))
def validate_send(self):
"""Validate if Newsletter can be sent.
@ -75,8 +99,9 @@ class Newsletter(WebsiteGenerator):
def validate_sender_address(self):
"""Validate self.send_from is a valid email address or not.
"""
if self.send_from:
frappe.utils.validate_email_address(self.send_from, throw=True)
if self.sender_email:
frappe.utils.validate_email_address(self.sender_email, throw=True)
self.send_from = f"{self.sender_name} <{self.sender_email}>" if self.sender_name else self.sender_email
def validate_recipient_address(self):
"""Validate if self.newsletter_recipients are all valid email addresses or not.
@ -84,6 +109,10 @@ class Newsletter(WebsiteGenerator):
for recipient in self.newsletter_recipients:
frappe.utils.validate_email_address(recipient, throw=True)
def validate_publishing(self):
if self.send_webview_link and not self.published:
frappe.throw(_("Newsletter must be published to send webview link in email"))
def get_linked_email_queue(self) -> List[str]:
"""Get list of email queue linked to this newsletter.
"""
@ -116,45 +145,24 @@ class Newsletter(WebsiteGenerator):
x for x in self.newsletter_recipients if x not in self.get_success_recipients()
]
def queue_all(self, test_emails: List[str] = None):
"""Queue Newsletter to all the recipients generated from the `Email Group`
table
Args:
test_email (List[str], optional): Send test Newsletter to the passed set of emails.
Defaults to None.
def queue_all(self):
"""Queue Newsletter to all the recipients generated from the `Email Group` table
"""
if test_emails:
for test_email in test_emails:
frappe.utils.validate_email_address(test_email, throw=True)
else:
self.validate()
self.validate_send()
self.validate()
self.validate_send()
newsletter_recipients = test_emails or self.get_pending_recipients()
self.send_newsletter(emails=newsletter_recipients)
recipients = self.get_pending_recipients()
self.send_newsletter(emails=recipients)
if not test_emails:
self.email_sent = True
self.schedule_send = frappe.utils.now_datetime()
self.scheduled_to_send = len(newsletter_recipients)
self.save()
self.email_sent = True
self.email_sent_at = frappe.utils.now()
self.total_recipients = len(recipients)
self.save()
def get_newsletter_attachments(self) -> List[Dict[str, str]]:
"""Get list of attachments on current Newsletter
"""
attachments = []
if self.send_attachments:
files = frappe.get_all(
"File",
filters={"attached_to_doctype": "Newsletter", "attached_to_name": self.name},
order_by="creation desc",
pluck="name",
)
attachments.extend({"fid": file} for file in files)
return attachments
return [{"file_url": row.attachment} for row in self.attachments]
def send_newsletter(self, emails: List[str]):
"""Trigger email generation for `emails` and add it in Email Queue.
@ -224,21 +232,6 @@ class Newsletter(WebsiteGenerator):
},
)
def get_context(self, context):
newsletters = get_newsletter_list("Newsletter", None, None, 0)
if newsletters:
newsletter_list = [d.name for d in newsletters]
if self.name not in newsletter_list:
frappe.redirect_to_message(
_("Permission Error"), _("You are not permitted to view the newsletter.")
)
frappe.local.flags.redirect_location = frappe.local.response.location
raise frappe.Redirect
else:
context.attachments = self.get_attachments()
context.no_cache = 1
context.show_sidebar = True
@frappe.whitelist(allow_guest=True)
def confirmed_unsubscribe(email, group):
@ -321,35 +314,14 @@ def confirm_subscription(email, email_group=_("Website")):
def get_list_context(context=None):
context.update({
"show_sidebar": True,
"show_search": True,
'no_breadcrumbs': True,
"title": _("Newsletter"),
"get_list": get_newsletter_list,
"no_breadcrumbs": True,
"title": _("Newsletters"),
"filters": {"published": 1},
"row_template": "email/doctype/newsletter/templates/newsletter_row.html",
})
def get_newsletter_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"):
email_group_list = frappe.db.sql('''SELECT eg.name
FROM `tabEmail Group` eg, `tabEmail Group Member` egm
WHERE egm.unsubscribed=0
AND eg.name=egm.email_group
AND egm.email = %s''', frappe.session.user)
email_group_list = [d[0] for d in email_group_list]
if email_group_list:
return frappe.db.sql('''SELECT n.name, n.subject, n.message, n.modified
FROM `tabNewsletter` n, `tabNewsletter Email Group` neg
WHERE n.name = neg.parent
AND n.email_sent=1
AND n.published=1
AND neg.email_group in ({0})
ORDER BY n.modified DESC LIMIT {1} OFFSET {2}
'''.format(','.join(['%s'] * len(email_group_list)),
limit_page_length, limit_start), email_group_list, as_dict=1)
def send_scheduled_email():
"""Send scheduled newsletter to the recipients."""
scheduled_newsletter = frappe.get_all(

View file

@ -1,6 +1,6 @@
{% extends "templates/web.html" %}
{% block title %} {{ _("Newsletter") }} {% endblock %}
{% block title %} {{ doc.subject }} {% endblock %}
{% block page_content %}
<style>
@ -36,11 +36,11 @@
</p>
</div>
<div itemprop="articleBody" class="longform blog-text">
{{ doc.message }}
{{ doc.get_message() }}
</div>
</article>
{% if attachments %}
{% if doc.attachments %}
<div>
<div class="row text-muted">
<div class="col-sm-12 h6 text-uppercase">
@ -49,10 +49,10 @@
</div>
<div class="row">
<div class="col-sm-12">
{% for attachment in attachments %}
{% for attachment in doc.attachments %}
<p class="small">
<a href="{{ attachment.file_url }}" target="blank">
{{ attachment.file_name }}
<a href="{{ attachment.attachment }}" target="_blank">
{{ attachment.attachment }}
</a>
</p>
{% endfor %}

View file

@ -14,7 +14,6 @@ from frappe.email.doctype.newsletter.exceptions import (
from frappe.email.doctype.newsletter.newsletter import (
Newsletter,
confirmed_unsubscribe,
get_newsletter_list,
send_scheduled_email
)
from frappe.email.queue import flush
@ -101,7 +100,8 @@ class TestNewsletterMixin:
doctype = "Newsletter"
newsletter_content = {
"subject": "_Test Newsletter",
"send_from": "Test Sender <test_sender@example.com>",
"sender_name": "Test Sender",
"sender_email": "test_sender@example.com",
"content_type": "Rich Text",
"message": "Testing my news.",
}
@ -157,21 +157,6 @@ class TestNewsletter(TestNewsletterMixin, unittest.TestCase):
if email != to_unsubscribe:
self.assertTrue(email in recipients)
def test_portal(self):
self.send_newsletter(published=1)
frappe.set_user("test1@example.com")
newsletter_list = get_newsletter_list("Newsletter", None, None, 0)
self.assertEqual(len(newsletter_list), 1)
def test_newsletter_context(self):
context = frappe._dict()
newsletter_name = self.send_newsletter(published=1)
frappe.set_user("test2@example.com")
doc = frappe.get_doc("Newsletter", newsletter_name)
doc.get_context(context)
self.assertEqual(context.no_cache, 1)
self.assertTrue("attachments" not in list(context))
def test_schedule_send(self):
self.send_newsletter(schedule_send=add_days(getdate(), -1))
@ -181,26 +166,32 @@ class TestNewsletter(TestNewsletterMixin, unittest.TestCase):
for email in emails:
self.assertTrue(email in recipients)
def test_newsletter_test_send(self):
"""Test "Test Send" functionality of Newsletter
def test_newsletter_send_test_email(self):
"""Test "Send Test Email" functionality of Newsletter
"""
newsletter = self.get_newsletter()
newsletter.test_email_id = choice(emails)
newsletter.test_send()
test_email = choice(emails)
newsletter.send_test_email(test_email)
self.assertFalse(newsletter.email_sent)
newsletter.save = MagicMock()
self.assertFalse(newsletter.save.called)
# check if the test email is in the queue
email_queue = frappe.db.get_all('Email Queue', filters=[
['reference_doctype', '=', 'Newsletter'],
['reference_name', '=', newsletter.name],
['Email Queue Recipient', 'recipient', '=', test_email]
])
self.assertTrue(email_queue)
def test_newsletter_status(self):
"""Test for Newsletter's stats on onload event
"""
newsletter = self.get_newsletter()
newsletter.email_sent = True
# had to use run_onload as calling .onload directly bought weird errors
# like TestNewsletter has no attribute "_TestNewsletter__onload"
run_onload(newsletter)
self.assertIsInstance(newsletter.get("__onload").status_count, dict)
result = newsletter.get_sending_status()
self.assertTrue('total' in result)
self.assertTrue('sent' in result)
def test_already_sent_newsletter(self):
newsletter = self.get_newsletter()
@ -218,22 +209,6 @@ class TestNewsletter(TestNewsletterMixin, unittest.TestCase):
with self.assertRaises(NoRecipientFoundError):
newsletter.send_emails()
def test_send_newsletter_with_attachments(self):
newsletter = self.get_newsletter()
newsletter.reload()
file_attachment = frappe.get_doc({
"doctype": "File",
"file_name": "test1.txt",
"attached_to_doctype": newsletter.doctype,
"attached_to_name": newsletter.name,
"content": frappe.mock("paragraph")
})
file_attachment.save()
newsletter.send_attachments = True
newsletter_attachments = newsletter.get_newsletter_attachments()
self.assertEqual(len(newsletter_attachments), 1)
self.assertEqual(newsletter_attachments[0]["fid"], file_attachment.name)
def test_send_scheduled_email_error_handling(self):
newsletter = self.get_newsletter(schedule_send=add_days(getdate(), -1))
job_path = "frappe.email.doctype.newsletter.newsletter.Newsletter.queue_all"

View file

@ -0,0 +1,31 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-12-06 16:37:40.652468",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"attachment"
],
"fields": [
{
"fieldname": "attachment",
"fieldtype": "Attach",
"in_list_view": 1,
"label": "Attachment",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-12-06 16:37:47.481057",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter Attachment",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View file

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class NewsletterAttachment(Document):
pass

View file

@ -1,106 +1,42 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-02-26 16:20:52.654136",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2017-02-26 16:20:52.654136",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"email_group",
"total_subscribers"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "email_group",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Email Group",
"length": 0,
"no_copy": 0,
"options": "Email Group",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 7,
"fieldname": "email_group",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Email Group",
"options": "Email Group",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"columns": 3,
"fetch_from": "email_group.total_subscribers",
"fieldname": "total_subscribers",
"fieldtype": "Read Only",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Total Subscribers",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "total_subscribers",
"fieldtype": "Read Only",
"in_list_view": 1,
"label": "Total Subscribers"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-05-16 22:42:55.437367",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter Email Group",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
],
"istable": 1,
"links": [],
"modified": "2021-12-06 20:12:08.420240",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter Email Group",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -255,7 +255,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
response = doc.run_method(method, **args)
frappe.response.docs.append(doc)
if not response:
if response is None:
return
# build output as csv

View file

@ -1130,12 +1130,16 @@ class Document(BaseDocument):
collated in one dict and returned. Ideally, don't return values in hookable
methods, set properties in the document."""
def add_to_return_value(self, new_return_value):
if new_return_value is None:
self._return_value = self.get("_return_value")
return
if isinstance(new_return_value, dict):
if not self.get("_return_value"):
self._return_value = {}
self._return_value.update(new_return_value)
else:
self._return_value = new_return_value or self.get("_return_value")
self._return_value = new_return_value
def compose(fn, *hooks):
def runner(self, method, *args, **kwargs):

View file

@ -73,7 +73,8 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
.text(this.today_text);
this.update_datepicker_position();
}
},
...(this.get_df_options())
};
}
set_datepicker() {
@ -150,4 +151,19 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
}
return value;
}
get_df_options() {
let options = {};
let df_options = this.df.options || '';
if (typeof df_options === 'string') {
try {
options = JSON.parse(df_options);
} catch (error) {
console.warn(`Invalid JSON in options of "${this.df.fieldname}"`);
}
}
else if (typeof df_options === 'object') {
options = df_options;
}
return options;
}
};

View file

@ -2,14 +2,16 @@ frappe.ui.form.ControlMarkdownEditor = class ControlMarkdownEditor extends frapp
static editor_class = 'markdown'
make_ace_editor() {
super.make_ace_editor();
if (this.markdown_container) return;
this.ace_editor_target.wrap(`<div class="${this.editor_class}-container">`);
this.markdown_container = this.$input_wrapper.find(`.${this.constructor.editor_class}-container`);
let editor_class = this.constructor.editor_class;
this.ace_editor_target.wrap(`<div class="${editor_class}-container">`);
this.markdown_container = this.$input_wrapper.find(`.${editor_class}-container`);
this.editor.getSession().setUseWrapMode(true);
this.showing_preview = false;
this.preview_toggle_btn = $(`<button class="btn btn-default btn-xs ${this.editor_class}-toggle">${__('Preview')}</button>`)
this.preview_toggle_btn = $(`<button class="btn btn-default btn-xs ${editor_class}-toggle">${__('Preview')}</button>`)
.click(e => {
if (!this.showing_preview) {
this.update_preview();
@ -25,7 +27,7 @@ frappe.ui.form.ControlMarkdownEditor = class ControlMarkdownEditor extends frapp
});
this.markdown_container.prepend(this.preview_toggle_btn);
this.markdown_preview = $(`<div class="${this.editor_class}-preview border rounded">`).hide();
this.markdown_preview = $(`<div class="${editor_class}-preview border rounded">`).hide();
this.markdown_container.append(this.markdown_preview);
}

View file

@ -75,6 +75,10 @@ frappe.ui.form.Form = class FrappeForm {
this.page = this.wrapper.page;
this.layout_main = this.page.main.get(0);
this.$wrapper.on("hide", () => {
this.script_manager.trigger("on_hide");
});
this.toolbar = new frappe.ui.form.Toolbar({
frm: this,
page: this.page

View file

@ -1376,5 +1376,18 @@ Object.assign(frappe.utils, {
return array;
}
return undefined;
},
// simple implementation of python's range
range(start, end) {
if (!end) {
end = start;
start = 0;
}
let arr = [];
for (let i = start; i < end; i++) {
arr.push(i);
}
return arr;
}
});

View file

@ -7,7 +7,7 @@
{% if published and send_webview_link %}
<div style="font-size: 12px; line-height: 20px;">
<div>
Open in <a style="color: #687178; text-decoration: underline;" href="/newsletters/{{ name }}" target="_blank">web</a>
<a style="color: #687178; text-decoration: underline;" href="/newsletters/{{ name }}" target="_blank">View this email on the web</a>
</div>
</div>
{% endif %}