fix: delete newsletter related files
This commit is contained in:
parent
8fbe452b4d
commit
32a87f53d6
33 changed files with 0 additions and 1978 deletions
|
|
@ -1,16 +0,0 @@
|
|||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See LICENSE
|
||||
|
||||
from frappe.exceptions import ValidationError
|
||||
|
||||
|
||||
class NewsletterAlreadySentError(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class NoRecipientFoundError(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class NewsletterNotSavedError(ValidationError):
|
||||
pass
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.ui.form.on("Newsletter", {
|
||||
refresh(frm) {
|
||||
let doc = frm.doc;
|
||||
let can_write = frappe.boot.user.can_write.includes(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);
|
||||
}
|
||||
});
|
||||
},
|
||||
__("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.events.send_emails(frm);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
frappe.confirm(
|
||||
__("Are you sure you want to send this newsletter now?"),
|
||||
() => {
|
||||
frm.events.send_emails(frm);
|
||||
}
|
||||
);
|
||||
},
|
||||
__("Send")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Schedule sending"),
|
||||
() => {
|
||||
frm.events.schedule_send_dialog(frm);
|
||||
},
|
||||
__("Send")
|
||||
);
|
||||
}
|
||||
|
||||
frm.events.update_sending_status(frm);
|
||||
|
||||
if (frm.is_new() && !doc.sender_email) {
|
||||
let { fullname, email } = frappe.user_info(doc.owner);
|
||||
frm.set_value("sender_email", email);
|
||||
frm.set_value("sender_name", fullname);
|
||||
}
|
||||
|
||||
frm.trigger("update_schedule_message");
|
||||
},
|
||||
|
||||
send_emails(frm) {
|
||||
frappe.dom.freeze(__("Queuing emails..."));
|
||||
frm.call("send_emails").then(() => {
|
||||
frm.refresh();
|
||||
frappe.dom.unfreeze();
|
||||
frappe.show_alert(
|
||||
__("Queued {0} emails", [frappe.utils.shorten_number(frm.doc.total_recipients)])
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
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(),
|
||||
},
|
||||
reqd: true,
|
||||
},
|
||||
{
|
||||
label: __("Time"),
|
||||
fieldname: "time",
|
||||
fieldtype: "Select",
|
||||
options: time_slots,
|
||||
reqd: true,
|
||||
},
|
||||
],
|
||||
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();
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
async update_sending_status(frm) {
|
||||
if (frm.doc.email_sent && frm.$wrapper.is(":visible") && !frm.waiting_for_request) {
|
||||
frm.waiting_for_request = true;
|
||||
let res = await frm.call("get_sending_status");
|
||||
frm.waiting_for_request = false;
|
||||
let stats = res.message;
|
||||
stats && frm.events.update_sending_progress(frm, stats);
|
||||
if (
|
||||
stats.sent + stats.error >= frm.doc.total_recipients ||
|
||||
(!stats.total && !stats.emails_queued)
|
||||
) {
|
||||
frm.sending_status && clearInterval(frm.sending_status);
|
||||
frm.sending_status = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.sending_status) return;
|
||||
frm.sending_status = setInterval(() => frm.events.update_sending_status(frm), 5000);
|
||||
},
|
||||
|
||||
update_sending_progress(frm, stats) {
|
||||
if (stats.sent + stats.error >= frm.doc.total_recipients || !frm.doc.email_sent) {
|
||||
frm.doc.email_sent && frm.page.set_indicator(__("Sent"), "green");
|
||||
frm.dashboard.hide_progress();
|
||||
return;
|
||||
}
|
||||
if (stats.total) {
|
||||
frm.page.set_indicator(__("Sending"), "blue");
|
||||
frm.dashboard.show_progress(
|
||||
__("Sending emails"),
|
||||
(stats.sent * 100) / frm.doc.total_recipients,
|
||||
__("{0} of {1} sent", [stats.sent, frm.doc.total_recipients])
|
||||
);
|
||||
} else if (stats.emails_queued) {
|
||||
frm.page.set_indicator(__("Queued"), "blue");
|
||||
}
|
||||
},
|
||||
|
||||
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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -1,282 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_guest_to_view": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2013-01-10 16:34:31",
|
||||
"description": "Create and send emails to a specific group of subscribers periodically.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Other",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"status_section",
|
||||
"email_sent_at",
|
||||
"column_break_3",
|
||||
"total_recipients",
|
||||
"column_break_12",
|
||||
"total_views",
|
||||
"email_sent",
|
||||
"from_section",
|
||||
"sender_name",
|
||||
"column_break_5",
|
||||
"sender_email",
|
||||
"column_break_7",
|
||||
"send_from",
|
||||
"recipients",
|
||||
"email_group",
|
||||
"subject_section",
|
||||
"subject",
|
||||
"newsletter_content",
|
||||
"content_type",
|
||||
"message",
|
||||
"message_md",
|
||||
"message_html",
|
||||
"campaign",
|
||||
"attachments",
|
||||
"send_unsubscribe_link",
|
||||
"send_webview_link",
|
||||
"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": "Audience",
|
||||
"options": "Newsletter Email Group",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "send_from",
|
||||
"fieldtype": "Data",
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Sender",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "email_sent",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Email Sent",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "newsletter_content",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Content"
|
||||
},
|
||||
{
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Small Text",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Subject",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.content_type === 'Rich Text'",
|
||||
"fieldname": "message",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Message",
|
||||
"mandatory_depends_on": "eval: doc.content_type === 'Rich Text'"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "send_unsubscribe_link",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Unsubscribe Link"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "published",
|
||||
"fieldtype": "Check",
|
||||
"label": "Published"
|
||||
},
|
||||
{
|
||||
"depends_on": "published",
|
||||
"fieldname": "route",
|
||||
"fieldtype": "Data",
|
||||
"label": "Route",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "scheduled_to_send",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"label": "Scheduled To Send"
|
||||
},
|
||||
{
|
||||
"fieldname": "recipients",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "To"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.schedule_sending",
|
||||
"fieldname": "schedule_send",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Send Email At",
|
||||
"read_only": 1,
|
||||
"read_only_depends_on": "eval: doc.email_sent"
|
||||
},
|
||||
{
|
||||
"fieldname": "content_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Type",
|
||||
"options": "Rich Text\nMarkdown\nHTML"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.content_type === 'Markdown'",
|
||||
"fieldname": "message_md",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"label": "Message (Markdown)",
|
||||
"mandatory_depends_on": "eval:doc.content_type === 'Markdown'"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.content_type === 'HTML'",
|
||||
"fieldname": "message_html",
|
||||
"fieldtype": "HTML Editor",
|
||||
"label": "Message (HTML)",
|
||||
"mandatory_depends_on": "eval:doc.content_type === 'HTML'"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "schedule_sending",
|
||||
"fieldtype": "Check",
|
||||
"label": "Schedule sending at a later time",
|
||||
"read_only_depends_on": "eval: doc.email_sent"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "send_webview_link",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Web View Link"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "total_views",
|
||||
"fieldtype": "Int",
|
||||
"label": "Total Views",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "campaign",
|
||||
"fieldtype": "Link",
|
||||
"label": "Campaign",
|
||||
"options": "UTM Campaign"
|
||||
}
|
||||
],
|
||||
"has_web_view": 1,
|
||||
"icon": "fa fa-envelope",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_published_field": "published",
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2024-11-12 12:41:02.569631",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Newsletter",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Newsletter Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"route": "newsletters",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"title_field": "subject",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,457 +0,0 @@
|
|||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See LICENSE
|
||||
|
||||
|
||||
import frappe
|
||||
import frappe.utils
|
||||
from frappe import _
|
||||
from frappe.email.doctype.email_group.email_group import add_subscribers
|
||||
from frappe.rate_limiter import rate_limit
|
||||
from frappe.utils.safe_exec import is_job_queued
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
from frappe.website.website_generator import WebsiteGenerator
|
||||
|
||||
from .exceptions import NewsletterAlreadySentError, NewsletterNotSavedError, NoRecipientFoundError
|
||||
|
||||
|
||||
class Newsletter(WebsiteGenerator):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.email.doctype.newsletter_attachment.newsletter_attachment import NewsletterAttachment
|
||||
from frappe.email.doctype.newsletter_email_group.newsletter_email_group import NewsletterEmailGroup
|
||||
from frappe.types import DF
|
||||
|
||||
attachments: DF.Table[NewsletterAttachment]
|
||||
campaign: DF.Link | None
|
||||
content_type: DF.Literal["Rich Text", "Markdown", "HTML"]
|
||||
email_group: DF.Table[NewsletterEmailGroup]
|
||||
email_sent: DF.Check
|
||||
email_sent_at: DF.Datetime | None
|
||||
message: DF.TextEditor | None
|
||||
message_html: DF.HTMLEditor | None
|
||||
message_md: DF.MarkdownEditor | None
|
||||
published: DF.Check
|
||||
route: DF.Data | None
|
||||
schedule_send: DF.Datetime | None
|
||||
schedule_sending: DF.Check
|
||||
scheduled_to_send: DF.Int
|
||||
send_from: DF.Data | None
|
||||
send_unsubscribe_link: DF.Check
|
||||
send_webview_link: DF.Check
|
||||
sender_email: DF.Data
|
||||
sender_name: DF.Data | None
|
||||
subject: DF.SmallText
|
||||
total_recipients: DF.Int
|
||||
total_views: DF.Int
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.route = f"newsletters/{self.name}"
|
||||
self.validate_sender_address()
|
||||
self.validate_publishing()
|
||||
self.validate_scheduling_date()
|
||||
|
||||
@property
|
||||
def newsletter_recipients(self) -> list[str]:
|
||||
if getattr(self, "_recipients", None) is None:
|
||||
self._recipients = self.get_recipients()
|
||||
return self._recipients
|
||||
|
||||
@frappe.whitelist()
|
||||
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
|
||||
error = 0
|
||||
total = 0
|
||||
for row in count_by_status:
|
||||
if row.status == "Sent":
|
||||
sent = row.count
|
||||
elif row.status == "Error":
|
||||
error = row.count
|
||||
total += row.count
|
||||
emails_queued = is_job_queued(
|
||||
job_name=frappe.utils.get_job_name("send_bulk_emails_for", self.doctype, self.name),
|
||||
queue="long",
|
||||
)
|
||||
return {"sent": sent, "error": error, "total": total, "emails_queued": emails_queued}
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_test_email(self, email):
|
||||
test_emails = frappe.utils.validate_email_address(email, throw=True)
|
||||
self.send_newsletter(emails=test_emails, test_email=True)
|
||||
frappe.msgprint(_("Test email sent to {0}").format(email), alert=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def find_broken_links(self):
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
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 Exception:
|
||||
broken_links.append(url)
|
||||
return broken_links
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_emails(self):
|
||||
"""queue sending emails to recipients"""
|
||||
self.schedule_sending = False
|
||||
self.schedule_send = None
|
||||
self.queue_all()
|
||||
|
||||
def validate_send(self):
|
||||
"""Validate if Newsletter can be sent."""
|
||||
self.validate_newsletter_status()
|
||||
self.validate_newsletter_recipients()
|
||||
|
||||
def validate_newsletter_status(self):
|
||||
if self.email_sent:
|
||||
frappe.throw(_("Newsletter has already been sent"), exc=NewsletterAlreadySentError)
|
||||
|
||||
if self.get("__islocal"):
|
||||
frappe.throw(_("Please save the Newsletter before sending"), exc=NewsletterNotSavedError)
|
||||
|
||||
def validate_newsletter_recipients(self):
|
||||
if not self.newsletter_recipients:
|
||||
frappe.throw(_("Newsletter should have atleast one recipient"), exc=NoRecipientFoundError)
|
||||
|
||||
def validate_sender_address(self):
|
||||
"""Validate self.send_from is a valid email address or not."""
|
||||
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_publishing(self):
|
||||
if self.send_webview_link and not self.published:
|
||||
frappe.throw(_("Newsletter must be published to send webview link in email"))
|
||||
|
||||
def validate_scheduling_date(self):
|
||||
if getattr(frappe.flags, "is_scheduler_running", False):
|
||||
return
|
||||
|
||||
if (
|
||||
self.schedule_sending
|
||||
and frappe.utils.get_datetime(self.schedule_send) < frappe.utils.now_datetime()
|
||||
):
|
||||
frappe.throw(_("Past dates are not allowed for Scheduling."))
|
||||
|
||||
def get_linked_email_queue(self) -> list[str]:
|
||||
"""Get list of email queue linked to this newsletter."""
|
||||
return frappe.get_all(
|
||||
"Email Queue",
|
||||
filters={
|
||||
"reference_doctype": self.doctype,
|
||||
"reference_name": self.name,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
def get_queued_recipients(self) -> list[str]:
|
||||
"""Recipients who have already been queued for receiving the newsletter."""
|
||||
return frappe.get_all(
|
||||
"Email Queue Recipient",
|
||||
filters={
|
||||
"parent": ("in", self.get_linked_email_queue()),
|
||||
},
|
||||
pluck="recipient",
|
||||
)
|
||||
|
||||
def get_pending_recipients(self) -> list[str]:
|
||||
"""Get list of pending recipients of the newsletter. These
|
||||
recipients may not have receive the newsletter in the previous iteration.
|
||||
"""
|
||||
|
||||
queued_recipients = set(self.get_queued_recipients())
|
||||
return [x for x in self.newsletter_recipients if x not in queued_recipients]
|
||||
|
||||
def queue_all(self):
|
||||
"""Queue Newsletter to all the recipients generated from the `Email Group` table"""
|
||||
self.validate()
|
||||
self.validate_send()
|
||||
|
||||
recipients = self.get_pending_recipients()
|
||||
self.send_newsletter(emails=recipients)
|
||||
|
||||
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"""
|
||||
return [{"file_url": row.attachment} for row in self.attachments]
|
||||
|
||||
def send_newsletter(self, emails: list[str], test_email: bool = False):
|
||||
"""Trigger email generation for `emails` and add it in Email Queue."""
|
||||
attachments = self.get_newsletter_attachments()
|
||||
sender = self.send_from or frappe.utils.get_formatted_email(self.owner)
|
||||
args = self.as_dict()
|
||||
args["message"] = self.get_message(medium="email")
|
||||
|
||||
is_auto_commit_set = bool(frappe.db.auto_commit_on_many_writes)
|
||||
frappe.db.auto_commit_on_many_writes = not frappe.in_test
|
||||
|
||||
frappe.sendmail(
|
||||
subject=self.subject,
|
||||
sender=sender,
|
||||
recipients=emails,
|
||||
attachments=attachments,
|
||||
template="newsletter",
|
||||
add_unsubscribe_link=self.send_unsubscribe_link,
|
||||
unsubscribe_method="/unsubscribe",
|
||||
reference_doctype=self.doctype,
|
||||
reference_name=self.name,
|
||||
queue_separately=True,
|
||||
send_priority=0,
|
||||
args=args,
|
||||
email_read_tracker_url=None
|
||||
if test_email
|
||||
else "/api/method/frappe.email.doctype.newsletter.newsletter.newsletter_email_read",
|
||||
)
|
||||
|
||||
frappe.db.auto_commit_on_many_writes = is_auto_commit_set
|
||||
|
||||
def get_message(self, medium=None) -> str:
|
||||
message = self.message
|
||||
if self.content_type == "Markdown":
|
||||
message = frappe.utils.md_to_html(self.message_md)
|
||||
if self.content_type == "HTML":
|
||||
message = self.message_html
|
||||
|
||||
html = frappe.render_template(message, {"doc": self.as_dict()})
|
||||
|
||||
return self.add_source(html, medium=medium)
|
||||
|
||||
def add_source(self, html: str, medium="None") -> str:
|
||||
"""Add source to the site links in the newsletter content."""
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
|
||||
links = soup.find_all("a")
|
||||
for link in links:
|
||||
href = link.get("href")
|
||||
if href and not href.startswith("#"):
|
||||
if not frappe.utils.is_site_link(href):
|
||||
continue
|
||||
new_href = frappe.utils.add_trackers_to_url(
|
||||
href, source="Newsletter", campaign=self.campaign, medium=medium
|
||||
)
|
||||
link["href"] = new_href
|
||||
|
||||
return str(soup)
|
||||
|
||||
def get_recipients(self) -> list[str]:
|
||||
"""Get recipients from Email Group"""
|
||||
emails = frappe.get_all(
|
||||
"Email Group Member",
|
||||
filters={"unsubscribed": 0, "email_group": ("in", self.get_email_groups())},
|
||||
pluck="email",
|
||||
)
|
||||
return list(set(emails))
|
||||
|
||||
def get_email_groups(self) -> list[str]:
|
||||
# wondering why the 'or'? i can't figure out why both aren't equivalent - @gavin
|
||||
return [x.email_group for x in self.email_group] or frappe.get_all(
|
||||
"Newsletter Email Group",
|
||||
filters={"parent": self.name, "parenttype": "Newsletter"},
|
||||
pluck="email_group",
|
||||
)
|
||||
|
||||
def get_attachments(self) -> list[dict[str, str]]:
|
||||
return frappe.get_all(
|
||||
"File",
|
||||
fields=["name", "file_name", "file_url", "is_private"],
|
||||
filters={
|
||||
"attached_to_name": self.name,
|
||||
"attached_to_doctype": "Newsletter",
|
||||
"is_private": 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def confirmed_unsubscribe(email, group):
|
||||
"""unsubscribe the email(user) from the mailing list(email_group)"""
|
||||
frappe.flags.ignore_permissions = True
|
||||
doc = frappe.get_doc("Email Group Member", {"email": email, "email_group": group})
|
||||
if not doc.unsubscribed:
|
||||
doc.unsubscribed = 1
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=10, seconds=60 * 60)
|
||||
def subscribe(email, email_group=None):
|
||||
"""API endpoint to subscribe an email to a particular email group. Triggers a confirmation email."""
|
||||
|
||||
if email_group is None:
|
||||
email_group = get_default_email_group()
|
||||
|
||||
# build subscription confirmation URL
|
||||
api_endpoint = frappe.utils.get_url(
|
||||
"/api/method/frappe.email.doctype.newsletter.newsletter.confirm_subscription"
|
||||
)
|
||||
signed_params = get_signed_params({"email": email, "email_group": email_group})
|
||||
confirm_subscription_url = f"{api_endpoint}?{signed_params}"
|
||||
|
||||
# fetch custom template if available
|
||||
email_confirmation_template = frappe.db.get_value(
|
||||
"Email Group", email_group, "confirmation_email_template"
|
||||
)
|
||||
|
||||
# build email and send
|
||||
if email_confirmation_template:
|
||||
args = {"email": email, "confirmation_url": confirm_subscription_url, "email_group": email_group}
|
||||
email_template = frappe.get_doc("Email Template", email_confirmation_template)
|
||||
email_subject = email_template.subject
|
||||
content = frappe.render_template(email_template.response, args)
|
||||
else:
|
||||
email_subject = _("Confirm Your Email")
|
||||
translatable_content = (
|
||||
_("Thank you for your interest in subscribing to our updates"),
|
||||
_("Please verify your Email Address"),
|
||||
confirm_subscription_url,
|
||||
_("Click here to verify"),
|
||||
)
|
||||
content = """
|
||||
<p>{}. {}.</p>
|
||||
<p><a href="{}">{}</a></p>
|
||||
""".format(*translatable_content)
|
||||
|
||||
frappe.sendmail(
|
||||
email,
|
||||
subject=email_subject,
|
||||
content=content,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def confirm_subscription(email, email_group=None):
|
||||
"""API endpoint to confirm email subscription.
|
||||
This endpoint is called when user clicks on the link sent to their mail.
|
||||
"""
|
||||
if not verify_request():
|
||||
return
|
||||
|
||||
if email_group is None:
|
||||
email_group = get_default_email_group()
|
||||
|
||||
try:
|
||||
group = frappe.get_doc("Email Group", email_group)
|
||||
except frappe.DoesNotExistError:
|
||||
group = frappe.get_doc({"doctype": "Email Group", "title": email_group}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
|
||||
frappe.flags.ignore_permissions = True
|
||||
|
||||
add_subscribers(email_group, email)
|
||||
frappe.db.commit()
|
||||
|
||||
welcome_url = group.get_welcome_url(email)
|
||||
|
||||
if welcome_url:
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = welcome_url
|
||||
else:
|
||||
frappe.respond_as_web_page(
|
||||
_("Confirmed"),
|
||||
_("{0} has been successfully added to the Email Group.").format(email),
|
||||
indicator_color="green",
|
||||
)
|
||||
|
||||
|
||||
def get_list_context(context=None):
|
||||
context.update(
|
||||
{
|
||||
"show_search": True,
|
||||
"no_breadcrumbs": True,
|
||||
"title": _("Newsletters"),
|
||||
"filters": {"published": 1},
|
||||
"row_template": "email/doctype/newsletter/templates/newsletter_row.html",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def send_scheduled_email():
|
||||
"""Send scheduled newsletter to the recipients."""
|
||||
frappe.flags.is_scheduler_running = True
|
||||
|
||||
scheduled_newsletter = frappe.get_all(
|
||||
"Newsletter",
|
||||
filters={
|
||||
"schedule_send": ("<=", frappe.utils.now_datetime()),
|
||||
"email_sent": False,
|
||||
"schedule_sending": True,
|
||||
},
|
||||
ignore_ifnull=True,
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for newsletter_name in scheduled_newsletter:
|
||||
try:
|
||||
newsletter = frappe.get_doc("Newsletter", newsletter_name)
|
||||
newsletter.queue_all()
|
||||
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
|
||||
# wasn't able to send emails :(
|
||||
frappe.db.set_value("Newsletter", newsletter_name, "email_sent", 0)
|
||||
newsletter.log_error("Failed to send newsletter")
|
||||
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
frappe.flags.is_scheduler_running = False
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def newsletter_email_read(recipient_email=None, reference_doctype=None, reference_name=None):
|
||||
if not (recipient_email and reference_name):
|
||||
return
|
||||
verify_request()
|
||||
try:
|
||||
doc = frappe.get_cached_doc("Newsletter", reference_name)
|
||||
if doc.add_viewed(recipient_email, force=True, unique_views=True):
|
||||
newsletter = frappe.qb.DocType("Newsletter")
|
||||
(
|
||||
frappe.qb.update(newsletter)
|
||||
.set(newsletter.total_views, newsletter.total_views + 1)
|
||||
.where(newsletter.name == doc.name)
|
||||
).run()
|
||||
|
||||
except Exception:
|
||||
frappe.log_error(
|
||||
title=f"Unable to mark as viewed for {recipient_email}",
|
||||
reference_doctype="Newsletter",
|
||||
reference_name=reference_name,
|
||||
)
|
||||
|
||||
finally:
|
||||
frappe.response.update(frappe.utils.get_imaginary_pixel_response())
|
||||
|
||||
|
||||
def get_default_email_group():
|
||||
return _("Website", lang=frappe.db.get_default("language"))
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
frappe.listview_settings["Newsletter"] = {
|
||||
add_fields: ["subject", "email_sent", "schedule_sending"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.email_sent) {
|
||||
return [__("Sent"), "green", "email_sent,=,1"];
|
||||
} else if (doc.schedule_sending) {
|
||||
return [__("Scheduled"), "purple", "email_sent,=,0|schedule_sending,=,1"];
|
||||
} else {
|
||||
return [__("Not Sent"), "gray", "email_sent,=,0"];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block title %} {{ doc.subject }} {% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<style>
|
||||
.blog-container {
|
||||
max-width: 720px;
|
||||
margin: auto;
|
||||
}
|
||||
.blog-header {
|
||||
font-weight: 700;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.blog-info {
|
||||
text-align:center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.blog-text {
|
||||
padding-top: 50px;
|
||||
padding-bottom: 50px;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.blog-text p {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="blog-container">
|
||||
<article class="blog-content" itemscope>
|
||||
<div class="blog-info">
|
||||
<h1 itemprop="headline" class="blog-header">{{ doc.subject }}</h1>
|
||||
<p class="post-by text-muted">
|
||||
{{ frappe.format_date(doc.modified) }}
|
||||
</p>
|
||||
</div>
|
||||
<div itemprop="articleBody" class="longform blog-text">
|
||||
{{ doc.get_message(medium="web_page") }}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{% if doc.attachments %}
|
||||
<div>
|
||||
<div class="row text-muted">
|
||||
<div class="col-sm-12 h6 text-uppercase">
|
||||
{{ _("Attachments") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% for attachment in doc.attachments %}
|
||||
<p class="small">
|
||||
<a href="{{ attachment.attachment }}" target="_blank">
|
||||
{{ attachment.attachment }}
|
||||
</a>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<div class="web-list-item transaction-list-item">
|
||||
<a href = "{{ route }}/">
|
||||
<div class="row">
|
||||
<div class="col-sm-8 text-left bold">
|
||||
{{ doc.subject }}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="text-muted text-right"
|
||||
title="{{ frappe.utils.format_datetime(doc.modified, "medium") }}">
|
||||
{{ frappe.utils.pretty_date(doc.modified) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See LICENSE
|
||||
|
||||
from random import choice
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
import frappe
|
||||
from frappe.email.doctype.newsletter.exceptions import (
|
||||
NewsletterAlreadySentError,
|
||||
NoRecipientFoundError,
|
||||
)
|
||||
from frappe.email.doctype.newsletter.newsletter import (
|
||||
Newsletter,
|
||||
confirmed_unsubscribe,
|
||||
send_scheduled_email,
|
||||
)
|
||||
from frappe.email.queue import flush
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_days, getdate
|
||||
|
||||
emails = [
|
||||
"test_subscriber1@example.com",
|
||||
"test_subscriber2@example.com",
|
||||
"test_subscriber3@example.com",
|
||||
"test1@example.com",
|
||||
]
|
||||
newsletters = []
|
||||
|
||||
|
||||
def get_dotted_path(obj: type) -> str:
|
||||
klass = obj.__class__
|
||||
module = klass.__module__
|
||||
if module == "builtins":
|
||||
return klass.__qualname__ # avoid outputs like 'builtins.str'
|
||||
return f"{module}.{klass.__qualname__}"
|
||||
|
||||
|
||||
class TestNewsletterMixin:
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
self.setup_email_group()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
for newsletter in newsletters:
|
||||
frappe.db.delete(
|
||||
"Email Queue",
|
||||
{
|
||||
"reference_doctype": "Newsletter",
|
||||
"reference_name": newsletter,
|
||||
},
|
||||
)
|
||||
frappe.delete_doc("Newsletter", newsletter)
|
||||
frappe.db.delete("Newsletter Email Group", {"parent": newsletter})
|
||||
newsletters.remove(newsletter)
|
||||
|
||||
def setup_email_group(self):
|
||||
if not frappe.db.exists("Email Group", "_Test Email Group"):
|
||||
frappe.get_doc({"doctype": "Email Group", "title": "_Test Email Group"}).insert()
|
||||
|
||||
for email in emails:
|
||||
doctype = "Email Group Member"
|
||||
email_filters = {"email": email, "email_group": "_Test Email Group"}
|
||||
|
||||
savepoint = "setup_email_group"
|
||||
frappe.db.savepoint(savepoint)
|
||||
|
||||
try:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": doctype,
|
||||
**email_filters,
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
except Exception:
|
||||
frappe.db.rollback(save_point=savepoint)
|
||||
frappe.db.set_value(doctype, email_filters, "unsubscribed", 0)
|
||||
|
||||
frappe.db.release_savepoint(savepoint)
|
||||
|
||||
def send_newsletter(self, published=0, schedule_send=None) -> str | None:
|
||||
frappe.db.delete("Email Queue")
|
||||
frappe.db.delete("Email Queue Recipient")
|
||||
frappe.db.delete("Newsletter")
|
||||
|
||||
newsletter_options = {
|
||||
"published": published,
|
||||
"schedule_sending": bool(schedule_send),
|
||||
"schedule_send": schedule_send,
|
||||
}
|
||||
newsletter = self.get_newsletter(**newsletter_options)
|
||||
|
||||
if schedule_send:
|
||||
send_scheduled_email()
|
||||
else:
|
||||
newsletter.send_emails()
|
||||
return newsletter.name
|
||||
|
||||
return newsletter
|
||||
|
||||
@staticmethod
|
||||
def get_newsletter(**kwargs) -> "Newsletter":
|
||||
"""Generate and return Newsletter object"""
|
||||
doctype = "Newsletter"
|
||||
newsletter_content = {
|
||||
"subject": "_Test Newsletter",
|
||||
"sender_name": "Test Sender",
|
||||
"sender_email": "test_sender@example.com",
|
||||
"content_type": "Rich Text",
|
||||
"message": "Testing my news.",
|
||||
}
|
||||
similar_newsletters = frappe.get_all(doctype, newsletter_content, pluck="name")
|
||||
|
||||
for similar_newsletter in similar_newsletters:
|
||||
frappe.delete_doc(doctype, similar_newsletter)
|
||||
|
||||
newsletter = frappe.get_doc({"doctype": doctype, **newsletter_content, **kwargs})
|
||||
newsletter.append("email_group", {"email_group": "_Test Email Group"})
|
||||
newsletter.save(ignore_permissions=True)
|
||||
newsletter.reload()
|
||||
newsletters.append(newsletter.name)
|
||||
|
||||
attached_files = frappe.get_all(
|
||||
"File",
|
||||
{
|
||||
"attached_to_doctype": newsletter.doctype,
|
||||
"attached_to_name": newsletter.name,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
for file in attached_files:
|
||||
frappe.delete_doc("File", file)
|
||||
|
||||
return newsletter
|
||||
|
||||
|
||||
class TestNewsletter(TestNewsletterMixin, IntegrationTestCase):
|
||||
def test_send(self):
|
||||
self.send_newsletter()
|
||||
|
||||
email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")]
|
||||
self.assertEqual(len(email_queue_list), 4)
|
||||
|
||||
recipients = {e.recipients[0].recipient for e in email_queue_list}
|
||||
self.assertTrue(set(emails).issubset(recipients))
|
||||
|
||||
def test_unsubscribe(self):
|
||||
name = self.send_newsletter()
|
||||
to_unsubscribe = choice(emails)
|
||||
group = frappe.get_all("Newsletter Email Group", filters={"parent": name}, fields=["email_group"])
|
||||
|
||||
flush()
|
||||
confirmed_unsubscribe(to_unsubscribe, group[0].email_group)
|
||||
|
||||
name = self.send_newsletter()
|
||||
email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")]
|
||||
self.assertEqual(len(email_queue_list), 3)
|
||||
recipients = [e.recipients[0].recipient for e in email_queue_list]
|
||||
|
||||
for email in emails:
|
||||
if email != to_unsubscribe:
|
||||
self.assertTrue(email in recipients)
|
||||
|
||||
def test_schedule_send(self):
|
||||
newsletter = self.send_newsletter(schedule_send=add_days(getdate(), 1))
|
||||
newsletter.db_set("schedule_send", add_days(getdate(), -1)) # Set date in past
|
||||
send_scheduled_email()
|
||||
|
||||
email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")]
|
||||
self.assertEqual(len(email_queue_list), 4)
|
||||
recipients = [e.recipients[0].recipient for e in email_queue_list]
|
||||
for email in emails:
|
||||
self.assertTrue(email in recipients)
|
||||
|
||||
def test_newsletter_send_test_email(self):
|
||||
"""Test "Send Test Email" functionality of Newsletter"""
|
||||
newsletter = self.get_newsletter()
|
||||
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.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
|
||||
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()
|
||||
newsletter.send_emails()
|
||||
|
||||
with self.assertRaises(NewsletterAlreadySentError):
|
||||
newsletter.send_emails()
|
||||
|
||||
def test_newsletter_with_no_recipient(self):
|
||||
newsletter = self.get_newsletter()
|
||||
property_path = f"{get_dotted_path(newsletter)}.newsletter_recipients"
|
||||
|
||||
with patch(property_path, new_callable=PropertyMock) as mock_newsletter_recipients:
|
||||
mock_newsletter_recipients.return_value = []
|
||||
with self.assertRaises(NoRecipientFoundError):
|
||||
newsletter.send_emails()
|
||||
|
||||
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"
|
||||
m = MagicMock(side_effect=frappe.OutgoingEmailError)
|
||||
|
||||
with self.assertRaises(frappe.OutgoingEmailError):
|
||||
with patch(job_path, new_callable=m):
|
||||
send_scheduled_email()
|
||||
|
||||
newsletter.reload()
|
||||
self.assertEqual(newsletter.email_sent, 0)
|
||||
|
||||
def test_retry_partially_sent_newsletter(self):
|
||||
frappe.db.delete("Email Queue")
|
||||
frappe.db.delete("Email Queue Recipient")
|
||||
frappe.db.delete("Newsletter")
|
||||
|
||||
newsletter = self.get_newsletter()
|
||||
newsletter.send_emails()
|
||||
email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")]
|
||||
self.assertEqual(len(email_queue_list), 4)
|
||||
|
||||
# delete a queue document to emulate partial send
|
||||
queue_recipient_name = email_queue_list[0].recipients[0].recipient
|
||||
email_queue_list[0].delete()
|
||||
newsletter.email_sent = False
|
||||
|
||||
# make sure the pending recipient is only the one which has been deleted
|
||||
self.assertEqual(newsletter.get_pending_recipients(), [queue_recipient_name])
|
||||
|
||||
# retry
|
||||
newsletter.send_emails()
|
||||
self.assertEqual(frappe.db.count("Email Queue"), 4)
|
||||
self.assertTrue(newsletter.email_sent)
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
{
|
||||
"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": "2024-03-23 16:03:31.101104",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Newsletter Attachment",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# 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):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
attachment: DF.Attach
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2017-02-26 16:20:52.654136",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"email_group",
|
||||
"total_subscribers"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 7,
|
||||
"fieldname": "email_group",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Email Group",
|
||||
"options": "Email Group",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"fetch_from": "email_group.total_subscribers",
|
||||
"fieldname": "total_subscribers",
|
||||
"fieldtype": "Read Only",
|
||||
"in_list_view": 1,
|
||||
"label": "Total Subscribers"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:03:31.190219",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Newsletter Email Group",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class NewsletterEmailGroup(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
email_group: DF.Link
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
total_subscribers: DF.ReadOnly | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<div>
|
||||
<div style="width: 600px; margin: 10px auto;">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if published and send_webview_link %}
|
||||
<div style="font-size: 12px; line-height: 20px;">
|
||||
<div>
|
||||
<a style="color: #687178; text-decoration: underline;" href="/newsletters/{{ name }}" target="_blank">View this email on the web</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# Copyright (c) 2024, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestUTMCampaign(IntegrationTestCase):
|
||||
pass
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (c) 2024, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("UTM Campaign", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "prompt",
|
||||
"creation": "2023-03-20 22:36:45.058045",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"slug",
|
||||
"campaign_description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "campaign_description",
|
||||
"fieldtype": "Small Text",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Campaign Description (Optional)"
|
||||
},
|
||||
{
|
||||
"fieldname": "slug",
|
||||
"fieldtype": "Data",
|
||||
"label": "Slug",
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-28 15:05:14.714600",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "UTM Campaign",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Newsletter Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Desk User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Marketing Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Copyright (c) 2023, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class UTMCampaign(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
campaign_description: DF.SmallText | None
|
||||
slug: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def before_save(self):
|
||||
if self.slug:
|
||||
self.slug = frappe.utils.slug(self.slug)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# Copyright (c) 2024, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestUTMMedium(IntegrationTestCase):
|
||||
pass
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (c) 2024, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("UTM Medium", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "prompt",
|
||||
"creation": "2024-06-28 09:46:10.102141",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"slug",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "slug",
|
||||
"fieldtype": "Data",
|
||||
"label": "Slug",
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-28 15:04:51.679189",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "UTM Medium",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Marketing Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"report": 1,
|
||||
"role": "Desk User",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Copyright (c) 2024, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class UTMMedium(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
description: DF.SmallText | None
|
||||
slug: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def before_save(self):
|
||||
if self.slug:
|
||||
self.slug = frappe.utils.slug(self.slug)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# Copyright (c) 2024, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestUTMSource(IntegrationTestCase):
|
||||
pass
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (c) 2024, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("UTM Source", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "prompt",
|
||||
"creation": "2024-06-28 09:42:04.478212",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"slug",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "slug",
|
||||
"fieldtype": "Data",
|
||||
"label": "Slug",
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-28 15:04:59.643513",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "UTM Source",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Marketing Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"report": 1,
|
||||
"role": "Desk User",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Copyright (c) 2024, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class UTMSource(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
description: DF.SmallText | None
|
||||
slug: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def before_save(self):
|
||||
if self.slug:
|
||||
self.slug = frappe.utils.slug(self.slug)
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block title %} Unsubscribe from Newsletter {% endblock %}
|
||||
|
||||
{% block navbar %}{% endblock %}
|
||||
{% block footer %}{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--subtle-accent);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
frappe.ready(function() {
|
||||
$("#select-all-btn").click(function() {
|
||||
$(".group").prop('checked', true);
|
||||
|
||||
});
|
||||
$("#unselect-all-btn").click(function() {
|
||||
$(".group").prop('checked', false);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% if status == "waiting_for_confirmation" %}
|
||||
<!-- Confirmation page to select the group to unsubscribe -->
|
||||
<div class="portal-container ">
|
||||
<div class='portal-section head d-block'>
|
||||
<div class="title">{{_("Unsubscribe")}}</div>
|
||||
<div class="text-muted">Select groups you wish to unsubscribe from ({{ email }})</div>
|
||||
<!-- Show 'Select All' or 'Unselect All' buttons only if there are more than 5 groups -->
|
||||
{% if email_groups|length > 5 %}
|
||||
<button id="select-all-btn"class="small-btn">Select All</button>
|
||||
<button id="unselect-all-btn"class="small-btn">Unselect All</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if email_groups %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="user_email" value="{{ email }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ frappe.session.csrf_token }}">
|
||||
<!-- Break into columns if there are more than 20 groups -->
|
||||
<div class="portal-items">
|
||||
{% for group in email_groups %}
|
||||
<div class="checkbox portal-section d-block">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
{% if current_group[0] and current_group[0].email_group == group.email_group %} checked {% endif %}
|
||||
class="group"
|
||||
name='{{ group.email_group }}'>
|
||||
<span style="padding-left: 5px">{{ group.email_group }}</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="portal-section mt-3">
|
||||
<button
|
||||
type="submit"
|
||||
id="unsubscribe"
|
||||
class="btn btn-primary">
|
||||
Unsubscribe
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
You are not registered to any mailing list.
|
||||
<span class="text-muted">{{ email }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif status == "unsubscribed" %}
|
||||
<!-- Unsubscribed page comes after submission -->
|
||||
<div class="portal-container">
|
||||
<div class='portal-section head'>
|
||||
<div class="title">Unsubscribed</div>
|
||||
</div>
|
||||
<div class="portal-section">
|
||||
You have been unsubscribed from selected mailing list.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- For invalid and unsigned request -->
|
||||
<div class="portal-container">
|
||||
<div class='portal-section head'>
|
||||
<div class="title">Unsubscribe</div>
|
||||
</div>
|
||||
<div class="portal-section">
|
||||
Invalid request
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<style>
|
||||
.small-btn {
|
||||
padding: 1px 5px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
border-radius: 3px;
|
||||
color: inherit;
|
||||
background-color: #f0f4f7;
|
||||
border-color: transparent;
|
||||
margin: 15px 5px 0 0;
|
||||
}
|
||||
.main-div {
|
||||
width: 500px;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import frappe
|
||||
from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe
|
||||
from frappe.utils.verified_command import verify_request
|
||||
|
||||
no_cache = True
|
||||
|
||||
|
||||
def get_context(context):
|
||||
frappe.flags.ignore_permissions = True
|
||||
# Called for confirmation.
|
||||
if "email" in frappe.form_dict and frappe.request.method == "GET":
|
||||
if verify_request():
|
||||
user_email = frappe.form_dict["email"]
|
||||
context.email = user_email
|
||||
title = frappe.form_dict.get("name")
|
||||
context.email_groups = get_email_groups(user_email)
|
||||
context.current_group = get_current_groups(title)
|
||||
context.status = "waiting_for_confirmation"
|
||||
print(context)
|
||||
|
||||
# Called when form is submitted.
|
||||
elif "user_email" in frappe.form_dict and frappe.request.method == "POST":
|
||||
context.status = "unsubscribed"
|
||||
email = frappe.form_dict["user_email"]
|
||||
email_group = get_email_groups(email)
|
||||
for group in email_group:
|
||||
if group.email_group in frappe.form_dict:
|
||||
confirmed_unsubscribe(email, group.email_group)
|
||||
|
||||
# Called on Invalid or unsigned request.
|
||||
else:
|
||||
context.status = "invalid"
|
||||
|
||||
|
||||
def get_email_groups(user_email):
|
||||
# Return the all email_groups in which the email has been registered.
|
||||
return frappe.get_all(
|
||||
"Email Group Member", fields=["email_group"], filters={"email": user_email, "unsubscribed": 0}
|
||||
)
|
||||
|
||||
|
||||
def get_current_groups(name):
|
||||
# Return current group by which the mail has been sent.
|
||||
return frappe.get_all(
|
||||
"Newsletter Email Group",
|
||||
fields=["email_group"],
|
||||
filters={"parent": name, "parenttype": "Newsletter"},
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue