Skip to content

Commit d12d2d0

Browse files
authored
chore: upgrade django-registration to latest version (#531)
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 8fe0d1b commit d12d2d0

11 files changed

Lines changed: 210 additions & 53 deletions

File tree

dejacode/urls.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,16 @@
102102
),
103103
name="login",
104104
),
105-
# Activation and password views are required for the user creation flow.
106-
# registration_activation_complete needs to be register before registration_activate
107-
# so the 'complete/' segment is not caught as the activation_key
105+
# User activation.
106+
# Activation views are required for the user creation flow, even when
107+
# self-registration (ENABLE_SELF_REGISTRATION) is turned off.
108108
path(
109109
"account/activate/complete/",
110110
TemplateView.as_view(template_name="django_registration/activation_complete.html"),
111111
name="django_registration_activation_complete",
112112
),
113113
path(
114-
"account/activate/<str:activation_key>/",
114+
"account/activate/",
115115
DejaCodeActivationView.as_view(),
116116
name="django_registration_activate",
117117
),
@@ -180,11 +180,13 @@
180180
from django_registration.backends.activation.views import RegistrationView
181181

182182
urlpatterns += [
183+
# Override the registration view to use our custom form
183184
path(
184185
"account/register/",
185186
RegistrationView.as_view(form_class=DejaCodeRegistrationForm),
186187
name="django_registration_register",
187188
),
189+
# Include the rest (complete, disallowed, etc.) from the default backend
188190
path("account/", include("django_registration.backends.activation.urls")),
189191
]
190192

dje/registration.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@
4646
class DejaCodeActivationView(ActivationView):
4747
def get_success_url(self, user=None):
4848
"""Add support for 'Sign Up' registration and User creation in admin."""
49+
# In django-registration 5.x, get_success_url is called with user=user
50+
# as a keyword argument. The default ``user=None`` keeps it safe when
51+
# called without a user (e.g. from base FormView code paths).
52+
if user is None:
53+
return self.success_url
54+
4955
if user.has_usable_password():
5056
# User created from registration process
5157
return self.success_url

dje/templates/django_registration/activation_email_body.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ Your DejaCode {{ user.dataspace.name }} account is pending activation.
44

55
Username: {{ user.username }}
66

7-
To activate the account please click on the link below:
8-
https://{{ site }}{% url 'django_registration_activate' activation_key %}
7+
To activate the account, please click on the link below and confirm the activation on the page that opens:
8+
9+
https://{{ site }}{% url 'django_registration_activate' %}?activation_key={{ activation_key }}
910

1011
If you cannot click on the link, please copy it to your browser.
1112

@@ -15,5 +16,4 @@ Please note that you have {{ expiration_days }} days to activate your account.
1516
{% endif %}
1617

1718
Thank You,
18-
1919
The DejaCode Team.

dje/templates/django_registration/activation_failed.html

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{% extends "bootstrap_base.html" %}
2+
{% load i18n %}
3+
{% block page_title %}{% trans 'Account activation' %}{% endblock %}
4+
{% block bodyclass %}bg-body-tertiary{% endblock %}
5+
{% block content %}
6+
{% if activation_error %}
7+
{% include 'includes/header_title.html' with pretitle='Account' title='Error during your account activation' %}
8+
<p>{{ activation_error.message }}</p>
9+
{% if DEJACODE_SUPPORT_EMAIL %}
10+
<p>
11+
If the problem persists, send us an email at <a href="mailto:{{ DEJACODE_SUPPORT_EMAIL }}">{{ DEJACODE_SUPPORT_EMAIL }}</a>
12+
</p>
13+
{% endif %}
14+
{% else %}
15+
{% include 'includes/header_title.html' with pretitle='Account' title='Confirm your account activation' %}
16+
{% if form.activation_key.errors %}
17+
<div class="alert alert-danger">
18+
{% for error in form.activation_key.errors %}
19+
<p class="mb-0">{{ error }}</p>
20+
{% endfor %}
21+
</div>
22+
{% else %}
23+
<p>Click the button below to activate your DejaCode account.</p>
24+
{% endif %}
25+
<form method="post">
26+
{% csrf_token %}
27+
<input type="hidden" name="activation_key" value="{{ form.activation_key.value|default:'' }}">
28+
<button type="submit" class="btn btn-warning">
29+
{% trans 'Activate my account' %}
30+
</button>
31+
</form>
32+
{% endif %}
33+
{% endblock %}

dje/templates/django_registration/registration_complete.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<p>
1111
Thank you for creating your account for DejaCode.
1212
An activation email will be sent shortly to the email address you provided.<br>
13-
In order to activate your account you must click on the activation link inside the email you receive.
13+
To activate your account, click on the link inside the email and confirm the activation on the page that opens.
1414
</p>
1515
<p>
1616
<a href="{% url 'index_dispatch' %}">Back to homepage.</a>

dje/tests/test_registration.py

Lines changed: 141 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@
66
# See https://aboutcode.org for more information about AboutCode FOSS projects.
77
#
88

9+
from datetime import timedelta
910
from unittest.mock import patch
1011

1112
from django.conf import settings
1213
from django.contrib.auth import get_user_model
1314
from django.contrib.auth.models import Group
1415
from django.core import mail
16+
from django.core import signing
1517
from django.test import TestCase
1618
from django.test import override_settings
1719
from django.urls import reverse
20+
from django.utils import timezone
1821

1922
from django_altcha import AltchaField
23+
from django_registration.backends.activation.views import REGISTRATION_SALT
2024
from django_registration.backends.activation.views import RegistrationView
2125

2226
from dje.registration import REGISTRATION_DEFAULT_GROUPS
@@ -26,6 +30,7 @@
2630
@override_settings(
2731
ENABLE_SELF_REGISTRATION=True,
2832
ADMINS=[("admin", "admin@nexb.com")],
33+
ALTCHA_HMAC_KEY="abcdef123456",
2934
)
3035
class DejaCodeUserRegistrationTestCase(TestCase):
3136
"""Tests for the dejacode.com registration workflow."""
@@ -48,7 +53,6 @@ def setUp(self):
4853
def tearDown(self):
4954
self.captcha_patch.stop()
5055

51-
@override_settings(ALTCHA_HMAC_KEY="abcdef123456")
5256
def test_user_registration_form_submit(self):
5357
url = reverse("django_registration_register")
5458
response = self.client.get(url)
@@ -67,6 +71,8 @@ def test_user_registration_form_submit(self):
6771
self.assertTrue("Your DejaCode Evaluation account is pending activation." in body)
6872
self.assertTrue("Username: {}".format(self.registration_data["username"]) in body)
6973
self.assertTrue("{} days to activate".format(settings.ACCOUNT_ACTIVATION_DAYS) in body)
74+
# Verify the activation URL uses the querystring format
75+
self.assertTrue("?activation_key=" in body)
7076

7177
new_user = get_user_model().objects.get(username=self.registration_data["username"])
7278
self.assertEqual(new_user.email, self.registration_data["email"])
@@ -84,7 +90,6 @@ def test_user_registration_form_submit(self):
8490
body = mail.outbox[0].body
8591
self.assertTrue("New registration for user username username@company.com" in body)
8692

87-
@override_settings(ALTCHA_HMAC_KEY="abcdef123456")
8893
def test_user_registration_form_validators(self):
8994
self.captcha_patch.stop()
9095

@@ -126,15 +131,19 @@ def test_user_registration_account_activation(self):
126131

127132
self.assertEqual("[DejaCode] Please activate your account", mail.outbox[1].subject)
128133
activation_key = RegistrationView().get_activation_key(user)
129-
activation_url = reverse("django_registration_activate", args=[activation_key])
134+
activation_url = reverse("django_registration_activate")
130135
# Check the validity of URL in activation email
131136
# WARNING: The key of the URL set in the email may be different since it is not
132-
# generated at the same time as the `activation_key` and the results is based on
137+
# generated at the same time as the `activation_key` and the result is based on
133138
# the timestamp.
134-
self.assertTrue(activation_url in mail.outbox[1].body)
135-
136-
# Call the url to activate the account
137-
response = self.client.get(activation_url, follow=True)
139+
expected_url_in_email = f"{activation_url}?activation_key={activation_key}"
140+
self.assertTrue(expected_url_in_email in mail.outbox[1].body)
141+
# Activate the account via POST (django-registration 5.x requires POST)
142+
response = self.client.post(
143+
activation_url,
144+
data={"activation_key": activation_key},
145+
follow=True,
146+
)
138147
self.assertRedirects(response, reverse("django_registration_activation_complete"))
139148
self.assertContains(response, "account is now active")
140149
# Now the user is active
@@ -143,16 +152,100 @@ def test_user_registration_account_activation(self):
143152
self.assertTrue(user.has_usable_password())
144153
self.assertTrue(user.is_staff)
145154
self.assertFalse(user.is_superuser)
146-
147-
# Make sure the activation link can be use multiple time within the
155+
# Make sure the activation link can be used multiple times within the
148156
# `ACCOUNT_ACTIVATION_DAYS` period.
149-
response = self.client.get(activation_url, follow=True)
157+
response = self.client.post(
158+
activation_url,
159+
data={"activation_key": activation_key},
160+
follow=True,
161+
)
150162
self.assertContains(response, "account is now active")
151163

164+
def test_user_registration_activation_form_displayed_on_get(self):
165+
url = reverse("django_registration_register")
166+
self.client.post(url, self.registration_data)
167+
user = get_user_model().objects.get(username=self.registration_data["username"])
168+
activation_key = RegistrationView().get_activation_key(user)
169+
activation_url = reverse("django_registration_activate")
170+
171+
response = self.client.get(f"{activation_url}?activation_key={activation_key}")
172+
self.assertEqual(response.status_code, 200)
173+
# The page should display the activation form, not redirect or auto-activate
174+
self.assertContains(response, "Activate my account")
175+
# User should NOT be active yet (security: GET should not activate)
176+
user.refresh_from_db()
177+
self.assertFalse(user.is_active)
178+
152179
def test_user_registration_activate_wrong_key(self):
153-
activation_url = reverse("django_registration_activate", args=["wrong_key"])
154-
response = self.client.get(activation_url)
155-
self.assertContains(response, "Error during your account activation")
180+
activation_url = reverse("django_registration_activate")
181+
response = self.client.post(activation_url, data={"activation_key": "wrong_key"})
182+
self.assertEqual(response.status_code, 200)
183+
# The form should reject the invalid activation key
184+
self.assertContains(response, "invalid")
185+
# No user should have been created or activated
186+
self.assertEqual(get_user_model().objects.count(), 0)
187+
188+
def test_user_registration_activate_empty_key(self):
189+
activation_url = reverse("django_registration_activate")
190+
response = self.client.post(activation_url, data={"activation_key": ""})
191+
self.assertEqual(response.status_code, 200)
192+
# Form should reject the empty key
193+
self.assertContains(response, "required")
194+
195+
def test_user_registration_activate_expired_key(self):
196+
url = reverse("django_registration_register")
197+
self.client.post(url, self.registration_data)
198+
user = get_user_model().objects.get(username=self.registration_data["username"])
199+
200+
# Generate a key with a timestamp older than ACCOUNT_ACTIVATION_DAYS
201+
expired_days = settings.ACCOUNT_ACTIVATION_DAYS + 1
202+
with patch("django.core.signing.time.time") as mock_time:
203+
past_timestamp = (timezone.now() - timedelta(days=expired_days)).timestamp()
204+
mock_time.return_value = past_timestamp
205+
expired_key = signing.dumps(obj=user.username, salt=REGISTRATION_SALT)
206+
207+
activation_url = reverse("django_registration_activate")
208+
response = self.client.post(activation_url, data={"activation_key": expired_key})
209+
self.assertEqual(response.status_code, 200)
210+
self.assertContains(response, "expired")
211+
# User should remain inactive
212+
user.refresh_from_db()
213+
self.assertFalse(user.is_active)
214+
215+
def test_user_registration_activate_unknown_user(self):
216+
# Generate a key for a user that doesn't exist
217+
bogus_key = signing.dumps(obj="nonexistent_user", salt=REGISTRATION_SALT)
218+
activation_url = reverse("django_registration_activate")
219+
response = self.client.post(activation_url, data={"activation_key": bogus_key}, follow=True)
220+
self.assertEqual(response.status_code, 200)
221+
# Should display the activation error template content
222+
self.assertContains(response, "The account you attempted to activate is invalid")
223+
224+
def test_user_registration_unique_email(self):
225+
url = reverse("django_registration_register")
226+
# First registration succeeds
227+
self.client.post(url, self.registration_data)
228+
# Second registration with same email but different username
229+
duplicate_data = dict(self.registration_data)
230+
duplicate_data["username"] = "different_username"
231+
response = self.client.post(url, duplicate_data)
232+
self.assertEqual(response.status_code, 200)
233+
self.assertIn("email", response.context["form"].errors)
234+
235+
def test_user_registration_unique_username(self):
236+
url = reverse("django_registration_register")
237+
self.client.post(url, self.registration_data)
238+
duplicate_data = dict(self.registration_data)
239+
duplicate_data["email"] = "different@company.com"
240+
response = self.client.post(url, duplicate_data)
241+
self.assertEqual(response.status_code, 200)
242+
self.assertIn("username", response.context["form"].errors)
243+
244+
def test_user_registration_default_dataspace_assigned(self):
245+
url = reverse("django_registration_register")
246+
self.client.post(url, self.registration_data)
247+
new_user = get_user_model().objects.get(username=self.registration_data["username"])
248+
self.assertEqual(new_user.dataspace.name, "Evaluation")
156249

157250
def test_user_registration_default_groups(self):
158251
for group_name in REGISTRATION_DEFAULT_GROUPS:
@@ -164,7 +257,41 @@ def test_user_registration_default_groups(self):
164257
new_user = get_user_model().objects.get(username=self.registration_data["username"])
165258
self.assertEqual(len(REGISTRATION_DEFAULT_GROUPS), new_user.groups.count())
166259

260+
def test_user_registration_default_groups_missing(self):
261+
# Don't create any groups
262+
url = reverse("django_registration_register")
263+
response = self.client.post(url, self.registration_data, follow=True)
264+
# Registration should succeed
265+
self.assertRedirects(response, reverse("django_registration_complete"))
266+
new_user = get_user_model().objects.get(username=self.registration_data["username"])
267+
self.assertEqual(0, new_user.groups.count())
268+
269+
def test_user_registration_password_field_only_password1(self):
270+
url = reverse("django_registration_register")
271+
response = self.client.get(url)
272+
self.assertContains(response, 'name="password1"')
273+
self.assertNotContains(response, 'name="password2"')
274+
167275
@override_settings(REGISTRATION_OPEN=False)
168276
def test_user_registration_closed(self):
169277
resp = self.client.get(reverse("django_registration_register"))
170278
self.assertRedirects(resp, reverse("django_registration_disallowed"))
279+
280+
@override_settings(REGISTRATION_OPEN=False)
281+
def test_user_registration_closed_post_blocked(self):
282+
resp = self.client.post(reverse("django_registration_register"), self.registration_data)
283+
self.assertRedirects(resp, reverse("django_registration_disallowed"))
284+
# No user should have been created
285+
self.assertEqual(get_user_model().objects.count(), 0)
286+
287+
def test_user_registration_admin_notification_email_sent(self):
288+
url = reverse("django_registration_register")
289+
self.client.post(url, self.registration_data)
290+
291+
admin_email = mail.outbox[0]
292+
self.assertEqual("[DejaCode] New User registration", admin_email.subject)
293+
# Check that admin@nexb.com is in any of the recipient tuples or strings
294+
recipients_str = str(admin_email.to)
295+
self.assertIn("admin@nexb.com", recipients_str)
296+
self.assertIn(self.registration_data["username"], admin_email.body)
297+
self.assertIn(self.registration_data["email"], admin_email.body)

0 commit comments

Comments
 (0)