Skip to content

Commit

Permalink
Re-send registration + ModelAdminFormViewMixin
Browse files Browse the repository at this point in the history
Added a view in the admin panel that allows re-sending registration emails for members that do not already have an associated user.

Allow reusing ModelAdminFormViewMixin by generalizing titles and other text.
  • Loading branch information
EricTRL committed Sep 29, 2023
1 parent b64315b commit bb25f4b
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

{% block extrahead %}{{ block.super }}
<script src="{% url 'admin:jsi18n' %}"></script>
<!-- media normally form.media + inline.media -->
{{ adminform.media }}
{% endblock %}

Expand All @@ -19,7 +18,8 @@
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; {% blocktranslate with name=opts.verbose_name %}Register new {{ name }}{% endblocktranslate %}
{% if original %}&rsaquo; <a href="{% url opts|admin_urlname:'change' object_id=original.id %}">{{ original|truncatewords:"18" }}</a>{% endif %}
&rsaquo; {{ breadcrumbs_title|default:title }}
</div>
{% endblock %}

Expand All @@ -37,35 +37,10 @@
{% for fieldset in adminform %}
{% include "admin/includes/fieldset.html" %}
{% endfor %}

{%comment%}
{% for field in form %}

<div class="fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}"
>
{% if not line.fields|length_is:'1' and not field.is_readonly %}{{ field.errors }}{% endif %}
{% if field.is_checkbox %}
check
{{ field.field }}{{ field.label_tag }}
{% else %}
{{ field.label_tag }}
{% if field.is_readonly %}
<div class="readonly">{{ field.contents }}</div>
{% else %}
{{ field.field }}
{% endif %}
{% endif %}
{% if field.field.help_text %}
<div class="help">{{ field.field.help_text|safe }}</div>
{% endif %}
</div>
{% endfor %}
<!-- {{ form.as_p }} -->
{%endcomment%}
{% endblock %}

<div class="submit-row">
<input type="submit" value="{% translate 'Register Member' %}" class="default" name="_save">
<input type="submit" value="{{ save_button_title|default:'Save' }}" class="default" name="_save">
</div>

{% block admin_change_form_document_ready %}
Expand All @@ -80,4 +55,4 @@

</div>
</form></div>
{% endblock %}
{% endblock %}
2 changes: 2 additions & 0 deletions core/tests/tests_views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from django.test import TestCase, RequestFactory
from core.views import LinkedLoginView


class LinkedLoginTestCase(TestCase):
"""Tests for LinkedLoginView"""

class DummyLinkedLoginView(LinkedLoginView):
"""Linked login with some overrides"""

image_source = "image.png"
image_alt = "Image"
link_title = "Link account"
Expand Down
1 change: 1 addition & 0 deletions core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class LinkedLoginView(LoginView):
the Squire account when logging in. This does not actually link any data itself;
subclasses should implement that sort of behaviour.
"""

template_name = "core/user_accounts/login_linked.html"

image_source = None
Expand Down
20 changes: 18 additions & 2 deletions membership_file/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from membership_file.forms import AdminMemberForm
from membership_file.export import MemberResource, MembersFinancialResource
from membership_file.models import Member, MemberLog, MemberLogField, Room, MemberYear, Membership
from membership_file.views import RegisterNewMemberAdminView
from membership_file.views import RegisterNewMemberAdminView, ResendRegistrationMailAdminView
from utils.forms import RequestUserToFormModelAdminMixin


Expand Down Expand Up @@ -71,8 +71,24 @@ def register_new_member(modeladmin, request, queryset):
view = modeladmin.admin_site.admin_view(RegisterNewMemberAdminView.as_view(model_admin=modeladmin))
return view(request)

# Note: get_urls is extended by
@object_action(label="Re-send registration email", description="Re-sends the registration email to this member.")
def resend_verification(self, request, object):
view = self.admin_site.admin_view(ResendRegistrationMailAdminView.as_view(model_admin=self))
return view(request, pk=object.pk)

Check warning on line 77 in membership_file/admin.py

View check run for this annotation

Codecov / codecov/patch

membership_file/admin.py#L76-L77

Added lines #L76 - L77 were not covered by tests

# Note: get_urls is extended
changelist_actions = ("register_new_member",)
change_actions = ("resend_verification",)

def get_change_actions(self, request, object_id, form_url):
# Action is only available if the user can add members normally
actions = super().get_change_actions(request, object_id, form_url)
if (
not request.user.has_perm("membership_file.add_member")
or Member.objects.get(id=object_id).user is not None
):
actions = [action for action in actions if action != "resend_verification"]
return actions

def get_changelist_actions(self, request):
# Action is only available if the user can add members normally
Expand Down
155 changes: 91 additions & 64 deletions membership_file/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,91 @@ def save(self):
)


class RegisterMemberForm(UpdatingUserFormMixin, FieldsetAdminFormMixin, forms.ModelForm):
class RegistrationFormBase(forms.ModelForm):
"""Base class defining email functionality for sending registration emails to (new) members"""

def __init__(self, request: HttpRequest, token_generator: LinkAccountTokenGenerator, *args, **kwargs):
self.domain = get_current_site(request).domain
self.use_https = request.is_secure()
self.token_generator = token_generator
super().__init__(*args, **kwargs)

def send_registration_email(self):
"""Generates and sends a registration email"""
# There is probably a better way to handle this than through a global preference,
# but it should do for now. This needs to be refactored anyway after #317 is merged.
global_preferences = global_preferences_registry.manager()

context = {
"member": self.instance,
"sender": {
"name": str(self.user),
"description": str(global_preferences["membership__registration_description"]),
"extra_description": str(global_preferences["membership__registration_extra_description"]),
},
"domain": self.domain,
"uid": urlsafe_base64_encode(force_bytes(self.instance.pk)),
"token": self.token_generator.make_token(user=self.instance),
"protocol": "https" if self.use_https else "http",
}
# Reply-To address
reply_to = str(global_preferences["membership__registration_reply_to_address"]) or None
if reply_to is not None:
reply_to = reply_to.split(",")

Check warning on line 145 in membership_file/forms.py

View check run for this annotation

Codecov / codecov/patch

membership_file/forms.py#L145

Added line #L145 was not covered by tests
self.send_mail(
"membership_file/registration/registration_subject.txt",
"membership_file/registration/registration_email.txt",
context,
None,
self.instance.email,
reply_to=reply_to,
)

def send_mail(
self,
subject_template_name,
email_template_name,
context,
from_email,
to_email,
html_email_template_name=None,
reply_to=None,
):
"""
Send a django.core.mail.EmailMultiAlternatives to `to_email`.
"""

subject = loader.render_to_string(subject_template_name, context)
# Email subject *must not* contain newlines
subject = "".join(subject.splitlines())
body = loader.render_to_string(email_template_name, context)

email_message = EmailMultiAlternatives(subject, body, from_email, [to_email], reply_to=reply_to)
if html_email_template_name is not None:
html_email = loader.render_to_string(html_email_template_name, context)
email_message.attach_alternative(html_email, "text/html")

email_message.send()


class ResendRegistrationForm(UpdatingUserFormMixin, FieldsetAdminFormMixin, RegistrationFormBase):
"""
Form that allows sending a registration email to a member.
There's no fields here; we basically only want a submit button.
"""

class Meta:
model = Member
fields = ()

def save(self, commit=True) -> Any:
# We don't call super().save(commit) because the object didn't actually change
# There's no need to activate signals or update auto_now fields
self.send_registration_email()
return self.instance

Check warning on line 196 in membership_file/forms.py

View check run for this annotation

Codecov / codecov/patch

membership_file/forms.py#L195-L196

Added lines #L195 - L196 were not covered by tests


class RegisterMemberForm(UpdatingUserFormMixin, FieldsetAdminFormMixin, RegistrationFormBase):
"""
Registers a member in the membership file, and optionally sends them an email to link or register a Squire account.
Is able to automatically link an active year or room access. Also contains some useful presets, like those for
Expand Down Expand Up @@ -159,7 +243,7 @@ class Meta:
{
"fields": [
"email",
"send_registration_email",
"do_send_registration_email",
"phone_number",
("street", "house_number", "house_number_addition"),
("postal_code", "city"),
Expand Down Expand Up @@ -190,16 +274,14 @@ class Meta:
),
}

send_registration_email = forms.BooleanField(
do_send_registration_email = forms.BooleanField(
label="Send registration email?",
initial=True,
required=False,
help_text="Whether to email a registration link to the new member, allowing them to link their account to this membership data.",
)

def __init__(self, request: HttpRequest, token_generator: LinkAccountTokenGenerator, *args, **kwargs):
self.domain = get_current_site(request).domain
self.use_https = request.is_secure()
self.token_generator = token_generator
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Make more fields required
Expand Down Expand Up @@ -294,63 +376,8 @@ def _save_m2m(self):
self.instance.accessible_rooms.add(*self.cleaned_data["room_access"])

# Only send out an email once the member is actually saved
if self.cleaned_data["send_registration_email"]:
# There is probably a better way to handle this than through a global preference,
# but it should do for now. This needs to be refactored anyway after #317 is merged.
global_preferences = global_preferences_registry.manager()

context = {
"member": self.instance,
"sender": {
"name": str(self.user),
"description": str(global_preferences["membership__registration_description"]),
"extra_description": str(global_preferences["membership__registration_extra_description"]),
},
"domain": self.domain,
"uid": urlsafe_base64_encode(force_bytes(self.instance.pk)),
"token": self.token_generator.make_token(user=self.instance),
"protocol": "https" if self.use_https else "http",
}
# Reply-To address
reply_to = str(global_preferences["membership__registration_reply_to_address"]) or None
if reply_to is not None:
reply_to = reply_to.split(",")
self.send_mail(
"membership_file/registration/registration_subject.txt",
"membership_file/registration/registration_email.txt",
context,
None,
self.instance.email,
reply_to=reply_to,
)

def send_mail(
self,
subject_template_name,
email_template_name,
context,
from_email,
to_email,
html_email_template_name=None,
reply_to=None,
):
"""
Send a django.core.mail.EmailMultiAlternatives to `to_email`.
"""
# registration/registration_subject.txt
# registration/registration_email.html

subject = loader.render_to_string(subject_template_name, context)
# Email subject *must not* contain newlines
subject = "".join(subject.splitlines())
body = loader.render_to_string(email_template_name, context)

email_message = EmailMultiAlternatives(subject, body, from_email, [to_email], reply_to=reply_to)
if html_email_template_name is not None:
html_email = loader.render_to_string(html_email_template_name, context)
email_message.attach_alternative(html_email, "text/html")

email_message.send()
if self.cleaned_data["do_send_registration_email"]:
self.send_registration_email()


class ConfirmLinkMembershipRegisterForm(RegisterForm):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends "core/admin_form.html" %}

{% block content_subtitle %}
{{ block.super }}
<p>
This action will re-send a registration email to {{ original.email|urlize }}.
This email allows them to link {{ original }}'s membership data to a new account or an already existing account.
</p>
<p class="help">
Members registered through the <a href="{% url "admin:membership_file_member_actions" tool="register_new_member" %}">
admin member registration page</a> will have already received this email.
</p>
{% endblock %}
2 changes: 1 addition & 1 deletion membership_file/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def test_save(self, _):
"country": "The Netherlands",
"date_of_birth": "1970-01-01",
"notes": "",
"send_registration_email": False,
"do_send_registration_email": False,
}
self.assertFormValid(data)

Expand Down
8 changes: 5 additions & 3 deletions membership_file/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,14 @@ def test_successful_get(self):
def test_messages(self):
"""Tests messages and urls they contain"""
# With registration mail
data = {**self.data, "send_registration_email": True}
data = {**self.data, "do_send_registration_email": True}
res = self.assertValidPostResponse(data=data, redirect_url=self.base_url)
member = Member.objects.filter(email=self.data["email"]).first()
self.assertIsNotNone(member, "New member should've been created.")
member.delete()

# Without registration mail
data = {**self.data, "send_registration_email": False}
data = {**self.data, "do_send_registration_email": False}
res = self.assertValidPostResponse(data=data, redirect_url=self.base_url)
member = Member.objects.filter(email=self.data["email"]).first()
self.assertIsNotNone(member, "New member should've been created.")
Expand Down Expand Up @@ -266,7 +266,9 @@ def test_login_already_member(self):
self._regenerate_token()
data = {"username": "newuser", "password": "linkedlogintest"}
user = User.objects.create_user(**data)
member = Member.objects.create(first_name="Bar", last_name="", legal_name="Bar", email="bar@example.com", user=user)
member = Member.objects.create(
first_name="Bar", last_name="", legal_name="Bar", email="bar@example.com", user=user
)
res: TemplateResponse = self.assertValidPostResponse(data, self.login_url)
self.assertTemplateNotUsed(res, LinkMembershipLoginView.fail_template_name)
self.assertIsInstance(res, TemplateResponse)
Expand Down
Loading

0 comments on commit bb25f4b

Please sign in to comment.