fix: delete newsletter related files

This commit is contained in:
sokumon 2025-05-13 14:36:55 +05:30
parent 8fbe452b4d
commit 32a87f53d6
33 changed files with 0 additions and 1978 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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"];
}
},
};

View file

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

View file

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

View file

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

View file

@ -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": []
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {
// },
// });

View file

@ -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": []
}

View file

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

View file

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

View file

@ -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) {
// },
// });

View file

@ -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": []
}

View file

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

View file

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

View file

@ -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) {
// },
// });

View file

@ -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": []
}

View file

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

View file

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

View file

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