Skip to content

Commit bc6d202

Browse files
authored
Merge pull request #238 from PROCOLLAB-github/feature/admin-mass-mail
Mass mailing from admin
2 parents 7931c9e + 875153c commit bc6d202

5 files changed

Lines changed: 190 additions & 34 deletions

File tree

mailing/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ def get_default_mailing_schema() -> dict[str, dict[str, str]]:
77
"text": {"title": "Основной текст письма"},
88
"button_text": {"title": "Текст кнопки", "default": "Кнопка"},
99
}
10+
11+
12+
MAILING_USERS_BATCH_SIZE = 100

mailing/utils.py

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from typing import Dict, List, Union
2+
from .constants import MAILING_USERS_BATCH_SIZE
3+
from .models import MailingSchema
4+
from users.models import CustomUser
25

36
import django.db.models
47
from django.contrib.auth import get_user_model
@@ -9,29 +12,64 @@
912
User = get_user_model()
1013

1114

15+
def prepare_mail_data(post_data) -> dict:
16+
users = post_data.getlist("users[]")
17+
schema_id = post_data["schemas"]
18+
subject = post_data["subject"]
19+
mail_schema = MailingSchema.objects.get(pk=schema_id)
20+
context = {}
21+
for variable_name in mail_schema.schema:
22+
key_in_post = "field-" + variable_name
23+
if key_in_post in post_data:
24+
context[variable_name] = post_data[key_in_post]
25+
users_to_send = CustomUser.objects.filter(pk__in=users)
26+
data_dict = {
27+
"users_to_send": users_to_send,
28+
"subject": subject,
29+
"mail_schema_template": mail_schema.template,
30+
"context": context,
31+
}
32+
return data_dict
33+
34+
35+
def create_message_groups(messages: list) -> list[list]:
36+
grouped_messages: list[list] = [
37+
messages[message: message + MAILING_USERS_BATCH_SIZE]
38+
for message in range(0, len(messages), MAILING_USERS_BATCH_SIZE)
39+
]
40+
return grouped_messages
41+
42+
1243
def send_mail(
13-
user: User,
14-
subject: str,
15-
template_string: str,
16-
template_context: Union[
17-
Dict,
18-
List,
19-
] = None,
20-
connection=None,
44+
user: User,
45+
subject: str,
46+
template_string: str,
47+
template_context: Union[
48+
Dict,
49+
List,
50+
] = None,
51+
connection=None,
2152
):
2253
return send_mass_mail([user], subject, template_string, template_context, connection)
2354

2455

56+
def send_group_messages(messages: list) -> int:
57+
connection = mail.get_connection()
58+
num_sent = connection.send_messages(messages)
59+
connection.close()
60+
return num_sent
61+
62+
2563
def send_mass_mail(
26-
users: django.db.models.QuerySet | List[User],
27-
subject: str,
28-
template_string: str,
29-
template_context: Union[
30-
Dict,
31-
List,
32-
] = None,
33-
connection=None,
34-
) -> None:
64+
users: django.db.models.QuerySet | List[User],
65+
subject: str,
66+
template_string: str,
67+
template_context: Union[
68+
Dict,
69+
List,
70+
] = None,
71+
connection=None,
72+
) -> int:
3573
"""
3674
Begin mailing to specified users, sending rendered template with template_text arg.
3775
Throws an error if template render is unsuccessful.
@@ -45,16 +83,18 @@ def send_mass_mail(
4583
if template_context is None:
4684
template_context = {}
4785

48-
connection = connection or mail.get_connection()
4986
template = Template(template_string)
5087
messages = []
5188
for user in users:
5289
template_context["user"] = user
5390
html_msg = template.render(Context(template_context))
5491
plain_msg = template.render(Context(template_context))
55-
msg = EmailMultiAlternatives(
56-
subject, plain_msg, None, [user.email], connection=connection
57-
)
92+
msg = EmailMultiAlternatives(subject, plain_msg, None, [user.email])
5893
msg.attach_alternative(html_msg, "text/html")
5994
messages.append(msg)
60-
return connection.send_messages(messages)
95+
96+
grouped_messages = create_message_groups(messages)
97+
num_sent: int = 0
98+
for group in grouped_messages:
99+
num_sent += send_group_messages(group)
100+
return num_sent

mailing/views.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,18 @@
66
from users.models import CustomUser
77
from .utils import send_mass_mail
88
from .models import MailingSchema
9+
from .utils import prepare_mail_data
910

1011

1112
class SendMailView(APIView):
12-
def post(self, request):
13-
users = request.POST.getlist("users[]")
14-
schema_id = request.POST["schemas"]
15-
subject = request.POST["subject"]
16-
mail_schema = MailingSchema.objects.get(pk=schema_id)
17-
context = {}
18-
for variable_name in mail_schema.schema:
19-
key_in_post = "field-" + variable_name
20-
if key_in_post in request.POST:
21-
context[variable_name] = request.POST[key_in_post]
22-
users_to_send = CustomUser.objects.filter(pk__in=users)
23-
send_mass_mail(users_to_send, subject, mail_schema.template, context)
13+
def post(self, request) -> JsonResponse:
14+
mail_data: dict = prepare_mail_data(request.POST)
15+
send_mass_mail(
16+
mail_data["users_to_send"],
17+
mail_data["subject"],
18+
mail_data["mail_schema_template"],
19+
mail_data["context"],
20+
)
2421
return JsonResponse({"detail": "ok"})
2522

2623

templates/admin/change_list.html

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
{% extends "admin/base_site.html" %}
2+
{% load i18n admin_urls static admin_list %}
3+
4+
{% block extrastyle %}
5+
{{ block.super }}
6+
<link rel="stylesheet" href="{% static 'admin/css/changelists.css'%}">
7+
<link rel="stylesheet" href="{% static 'admin/css/forms.css' %}">
8+
{% if cl.formset or action_form %}
9+
<script src="{% url 'admin:jsi18n' %}"></script>
10+
{% endif %}
11+
{{ media.css }}
12+
{% if not actions_on_top and not actions_on_bottom %}
13+
<style>
14+
#changelist table thead th:first-child {width: inherit}
15+
16+
17+
</style>
18+
{% endif %}
19+
{% endblock %}
20+
21+
{% block extrahead %}
22+
{{ block.super }}
23+
{{ media.js }}
24+
<script src="{% static 'admin/js/filters.js' %}" defer></script>
25+
{% endblock %}
26+
27+
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %}
28+
29+
{% if not is_popup %}
30+
{% block breadcrumbs %}
31+
<div class="breadcrumbs">
32+
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
33+
&rsaquo; <a href="{% url 'admin:app_list' app_label=cl.opts.app_label %}">{{ cl.opts.app_config.verbose_name }}</a>
34+
&rsaquo; {{ cl.opts.verbose_name_plural|capfirst }}
35+
</div>
36+
{% endblock %}
37+
{% endif %}
38+
39+
{% block coltype %}{% endblock %}
40+
41+
{% block content %}
42+
<div id="content-main">
43+
{% block object-tools %}
44+
<ul class="object-tools">
45+
{% block object-tools-items %}
46+
{% change_list_object_tools %}
47+
{% endblock %}
48+
</ul>
49+
{% endblock %}
50+
{% if cl.formset and cl.formset.errors %}
51+
<p class="errornote">
52+
{% blocktranslate count counter=cl.formset.total_error_count %}Please correct the error below.{% plural %}Please
53+
correct the errors below.{% endblocktranslate %}
54+
</p>
55+
{{ cl.formset.non_form_errors }}
56+
{% endif %}
57+
<div class="module{% if cl.has_filters %} filtered{% endif %}" id="changelist">
58+
<div class="changelist-form-container">
59+
{% block search %}{% search_form cl %}{% endblock %}
60+
{% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %}
61+
62+
<form id="changelist-form" method="post" {% if cl.formset and cl.formset.is_multipart %}
63+
enctype="multipart/form-data" {% endif %} novalidate>{% csrf_token %}
64+
{% if cl.formset %}
65+
<div>{{ cl.formset.management_form }}</div>
66+
{% endif %}
67+
68+
{% block result_list %}
69+
{% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %}
70+
{% result_list cl %}
71+
{% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %}
72+
{% endblock %}
73+
{% block pagination %}{% pagination cl %}{% endblock %}
74+
<div class="submit-row" id="mailing_btn">
75+
<input type="button" class="mailing-btn" value="Рассылка" onclick="mailing()"/>
76+
</div>
77+
<br>
78+
<script>
79+
function mailing() {
80+
window.open("{% url 'admin:user_mass_mail' %}", '_blank').focus()
81+
}
82+
window.onload = function() {
83+
var currentURL = window.location.href;
84+
currentURL = currentURL.split('/');
85+
if (currentURL.at(-2) != "customuser")
86+
{
87+
const button = document.getElementById("mailing_btn");
88+
button.setAttribute("style", "display:none;");
89+
}
90+
};
91+
</script>
92+
</form>
93+
</div>
94+
{% block filters %}
95+
{% if cl.has_filters %}
96+
<div id="changelist-filter">
97+
<h2>{% translate 'Filter' %}</h2>
98+
{% if cl.has_active_filters %}<h3 id="changelist-filter-clear">
99+
<a href="{{ cl.clear_all_filters_qs }}">&#10006; {% translate "Clear all filters" %}</a>
100+
</h3>{% endif %}
101+
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
102+
</div>
103+
{% endif %}
104+
{% endblock %}
105+
</div>
106+
</div>
107+
{% endblock %}

users/admin.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,18 @@ def get_urls(self):
163163
self.admin_site.admin_view(self.force_verify),
164164
name="force_verify",
165165
),
166+
path(
167+
"mailing/",
168+
self.admin_site.admin_view(self.mass_mail),
169+
name="user_mass_mail",
170+
),
166171
]
167172
return custom_urls + default_urls
168173

174+
def mass_mail(self, request):
175+
users = CustomUser.objects.all()
176+
return MailingTemplateRender().render_template(request, None, users, None)
177+
169178
def mailing(self, request, user_object):
170179
user = CustomUser.objects.get(pk=user_object)
171180
users = [user]

0 commit comments

Comments
 (0)