Skip to content

Commit 7bc0ac2

Browse files
daniel-gray-tangent“OMosimege”
authored andcommitted
updating user management
- added templates - updated templates - updated forms - updated View add translations wrapping
1 parent 323d164 commit 7bc0ac2

22 files changed

+573
-51
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
SECRET_KEY=''
21
DEBUG=True
32
DB_HOST=db
43
DB_PORT=5432
@@ -19,3 +18,4 @@ EMAIL_HOST_PASSWORD=''
1918
EMAIL_BACKEND_CONSOLE='True/False'
2019
EMAIL_USE_TLS=True
2120
SECRET_KEY=''
21+
DEFAULT_FROM_EMAIL='[email protected]'

app/accounts/forms.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
from django import forms
2-
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
2+
from django.contrib.auth import get_user_model
3+
from django.contrib.auth.forms import (
4+
AuthenticationForm,
5+
PasswordResetForm,
6+
UserCreationForm,
7+
)
38
from django.utils.translation import gettext_lazy as _
49

510
from users.models import CustomUser
@@ -40,3 +45,38 @@ def __init__(self, *args, **kwargs):
4045
super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
4146
for field in self.fields.values():
4247
field.widget.attrs.update({"class": "form-control"})
48+
49+
50+
class CustomPasswordResetForm(PasswordResetForm):
51+
def clean_email(self):
52+
"""
53+
54+
Cleaning an Email
55+
56+
This method is used to clean an email address provided by the user. It performs the following operations:
57+
58+
1. Retrieves the user model using the "get_user_model()" function.
59+
2. Fetches the email from the "cleaned_data" dictionary.
60+
3. Queries the user model to find all users with the same email address.
61+
4. Checks if any users exist with the given email address. If not, it raises a "forms.ValidationError" with a specific error message.
62+
5. Iterates through each user found with the given email address.
63+
6. Checks if the user is both active and a staff member. If not, it raises a "forms.ValidationError" with a specific error message.
64+
7. Finally, the cleaned email address is returned.
65+
66+
Please note that this method assumes the presence of the "forms.ValidationError" class and the "get_user_model()" function. If any of these are missing, this method will not work properly.
67+
68+
Reason for the Override:
69+
The "clean_email" method is overridden to ensure that only active staff members can reset their passwords. This is done to prevent unauthorized users from resetting their passwords and gaining access to the system.
70+
and prevent users getting an email to reset their password if they are not active or staff members.
71+
"""
72+
User = get_user_model()
73+
email = self.cleaned_data["email"]
74+
users = User.objects.filter(email=email)
75+
if not users.exists():
76+
raise forms.ValidationError("There is a error please contact the administrator.")
77+
78+
for user in users:
79+
if not user.is_active or not user.is_staff:
80+
raise forms.ValidationError("There is a error please contact the administrator.")
81+
82+
return email
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import os
2+
3+
from django.contrib.sites.shortcuts import get_current_site
4+
from django.core.mail import EmailMultiAlternatives
5+
from django.template.loader import render_to_string
6+
from django.utils.encoding import force_bytes
7+
from django.utils.http import urlsafe_base64_encode
8+
9+
from accounts.tokens import account_activation_token
10+
11+
12+
class SendActiveEmailService:
13+
@staticmethod
14+
def send_activation_email(request, user):
15+
if user and request:
16+
current_site = get_current_site(request)
17+
mail_subject = "Activate your account."
18+
message = render_to_string(
19+
"accounts/email/activation_email.html",
20+
{
21+
"user": user,
22+
"domain": current_site.domain,
23+
"uid": urlsafe_base64_encode(force_bytes(user.pk)),
24+
"token": account_activation_token.make_token(user),
25+
},
26+
)
27+
text_content = (
28+
"Please activate your account by clicking the link provided in the email."
29+
)
30+
email = EmailMultiAlternatives(
31+
mail_subject, text_content, os.environ.get("DEFAULT_FROM_EMAIL"), [user.email]
32+
)
33+
email.attach_alternative(message, "text/html")
34+
email.send()
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
from django.contrib.sites.shortcuts import get_current_site
4+
from django.core import mail
5+
from django.test import RequestFactory, TestCase
6+
from django.utils.encoding import force_bytes
7+
from django.utils.http import urlsafe_base64_encode
8+
9+
from accounts.service.active_email import ( # Adjust the import path as necessary
10+
SendActiveEmailService,
11+
)
12+
from accounts.tokens import account_activation_token
13+
from users.models import CustomUser # Import your custom user model
14+
15+
16+
class SendActiveEmailServiceTest(TestCase):
17+
def setUp(self):
18+
self.factory = RequestFactory()
19+
self.user = CustomUser.objects.create_user(
20+
username="testuser", email="[email protected]", password="password123"
21+
)
22+
self.request = self.factory.get("/fake-path")
23+
self.service = SendActiveEmailService()
24+
25+
def test_send_activation_email(self):
26+
with patch("accounts.service.active_email.render_to_string") as mock_render:
27+
# Set up the mocks
28+
mock_render.return_value = "<html>mocked template</html>"
29+
30+
# # Call the method
31+
self.service.send_activation_email(self.request, self.user)
32+
33+
# Check that render_to_string was called with the correct parameters
34+
mock_render.assert_called_once_with(
35+
"accounts/email/activation_email.html",
36+
{
37+
"user": self.user,
38+
"domain": get_current_site(self.request).domain,
39+
"uid": urlsafe_base64_encode(force_bytes(self.user.pk)),
40+
"token": account_activation_token.make_token(self.user),
41+
},
42+
)
43+
44+
# Check that an email was sent
45+
self.assertEqual(len(mail.outbox), 1)
46+
sent_email = mail.outbox[0]
47+
self.assertEqual(sent_email.subject, "Activate your account.")
48+
self.assertEqual(sent_email.to, [self.user.email])
49+
self.assertIn("mocked template", sent_email.alternatives[0][0])
50+
self.assertEqual(sent_email.alternatives[0][1], "text/html")
51+
self.assertIn(
52+
"Please activate your account by clicking the link provided in the email.",
53+
sent_email.body,
54+
)

app/accounts/tests/test_token.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import unittest
2+
from datetime import datetime
3+
4+
import six
5+
from django.contrib.auth.tokens import PasswordResetTokenGenerator
6+
7+
from accounts.tokens import AccountActivationTokenGenerator
8+
9+
10+
# Speculate user model class for test abstraction
11+
class User:
12+
def __init__(self, id, is_active):
13+
self.pk = id
14+
self.is_active = is_active
15+
16+
17+
class TestAccountActivationTokenGenerator(unittest.TestCase):
18+
def setUp(self):
19+
self.generator = AccountActivationTokenGenerator()
20+
self.timestamp = datetime.now()
21+
22+
def test_make_hash_value_active_user(self):
23+
user = User(1, True)
24+
hash_val = self.generator._make_hash_value(user, self.timestamp)
25+
expected_val = (
26+
six.text_type(user.pk) + six.text_type(self.timestamp) + six.text_type(user.is_active)
27+
)
28+
self.assertEqual(hash_val, expected_val)
29+
30+
def test_make_hash_value_inactive_user(self):
31+
user = User(1, False)
32+
hash_val = self.generator._make_hash_value(user, self.timestamp)
33+
expected_val = (
34+
six.text_type(user.pk) + six.text_type(self.timestamp) + six.text_type(user.is_active)
35+
)
36+
self.assertEqual(hash_val, expected_val)
37+
38+
39+
if __name__ == "__main__":
40+
unittest.main()

app/accounts/tokens.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from django.contrib.auth.tokens import PasswordResetTokenGenerator
2+
3+
4+
class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
5+
"""
6+
AccountActivationTokenGenerator class is a subclass of PasswordResetTokenGenerator.
7+
8+
Methods:
9+
10+
_make_hash_value(self, user, timestamp):
11+
This method takes in a user object and a timestamp and returns a hash value
12+
that is used to generate an account activation token. The hash value is
13+
calculated by concatenating the user's primary key, timestamp, and active
14+
status.
15+
16+
DO NOT MODIFY THIS METHOD. UNLESS YOU GET PERMISSION FROM THE PROJECT OWNER.
17+
18+
"""
19+
20+
def _make_hash_value(self, user, timestamp):
21+
return f"{user.pk}{timestamp}{user.is_active}"
22+
23+
24+
account_activation_token = AccountActivationTokenGenerator()

app/accounts/urls.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22
from django.urls import path
33

44
from . import views
5+
from .views import CustomPasswordResetView
56

7+
app_name = "accounts"
68
urlpatterns = [
79
path("register/", views.register, name="accounts_register"),
810
path("login/", auth_views.LoginView.as_view(template_name="accounts/login.html"), name="login"),
9-
path(
10-
"password_reset/",
11-
auth_views.PasswordResetView.as_view(template_name="accounts/password_reset_form.html"),
12-
name="password_reset",
13-
),
11+
path("password_reset/", CustomPasswordResetView.as_view(), name="password_reset"),
1412
path(
1513
"password_reset/done/",
1614
auth_views.PasswordResetDoneView.as_view(template_name="accounts/password_reset_done.html"),
@@ -19,7 +17,8 @@
1917
path(
2018
"reset/<uidb64>/<token>/",
2119
auth_views.PasswordResetConfirmView.as_view(
22-
template_name="accounts/password_reset_confirm.html"
20+
template_name="accounts/password_reset_confirm.html",
21+
success_url="/accounts/reset/done/",
2322
),
2423
name="password_reset_confirm",
2524
),
@@ -30,4 +29,7 @@
3029
),
3130
name="password_reset_complete",
3231
),
32+
path("activate/<uidb64>/<token>/", views.activate, name="activate"),
33+
path("activation_sent/", views.activation_sent, name="activation_sent"),
34+
path("resend_activation/", views.resend_activation, name="resend_activation"),
3335
]

app/accounts/views.py

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
1-
from django.contrib.auth import authenticate
1+
from django.contrib.auth import get_user_model
22
from django.contrib.auth import login as auth_login
3+
from django.contrib.auth.views import PasswordResetView
34
from django.shortcuts import redirect, render
5+
from django.urls import reverse_lazy
6+
from django.utils.encoding import force_str
7+
from django.utils.http import urlsafe_base64_decode
48

5-
from .forms import CustomAuthenticationForm, CustomUserCreationForm
9+
from accounts.service.active_email import SendActiveEmailService
10+
11+
from .forms import (
12+
CustomAuthenticationForm,
13+
CustomPasswordResetForm,
14+
CustomUserCreationForm,
15+
)
16+
from .tokens import account_activation_token
617

718

819
def register(request):
920
if request.method == "POST":
1021
form = CustomUserCreationForm(request.POST)
1122
if form.is_valid():
1223
user = form.save(commit=False)
13-
user.is_staff = True
24+
user.is_staff = False
25+
user.is_active = False
1426
user.save()
15-
auth_login(request, user)
16-
return redirect("home")
27+
28+
SendActiveEmailService.send_activation_email(request, user)
29+
30+
return redirect("accounts:activation_sent")
31+
1732
else:
1833
form = CustomUserCreationForm()
1934
return render(request, "accounts/register.html", {"form": form})
@@ -30,3 +45,63 @@ def user_login(request):
3045
form = CustomAuthenticationForm()
3146

3247
return render(request, "accounts/login.html", {"form": form})
48+
49+
50+
def activate(request, uidb64, token):
51+
User = get_user_model() # Get the custom user model
52+
try:
53+
uid = force_str(urlsafe_base64_decode(uidb64))
54+
user = User.objects.get(pk=uid)
55+
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
56+
user = None
57+
58+
if user is not None and account_activation_token.check_token(user, token):
59+
user.is_staff = True
60+
user.is_active = True
61+
user.save()
62+
auth_login(request, user)
63+
return render(request, "accounts/activate.html")
64+
else:
65+
return render(request, "accounts/activation_invalid.html")
66+
67+
68+
def activation_sent(request):
69+
return render(request, "accounts/activation_sent.html")
70+
71+
72+
def resend_activation(request):
73+
User = get_user_model() #
74+
if request.method == "POST":
75+
user_email = request.POST["email"]
76+
77+
user = User.objects.get(email=user_email)
78+
if not user.is_active:
79+
SendActiveEmailService.send_activation_email(request, user)
80+
81+
return redirect("accounts:activation_sent")
82+
83+
else:
84+
return render(
85+
request,
86+
"accounts/resend_activation.html",
87+
{"error": "There is a error please contact the administrator."},
88+
)
89+
90+
91+
class CustomPasswordResetView(PasswordResetView):
92+
"""
93+
This class represents a custom password reset view for user accounts.
94+
95+
Attributes:
96+
form_class (CustomPasswordResetForm): The form class for the password reset form.
97+
template_name (str): The name of the template for rendering the password reset form.
98+
success_url (reverse_lazy): The URL to redirect to after a successful password reset.
99+
html_email_template_name (str): The name of the template for sending the password reset email.
100+
101+
102+
"""
103+
104+
form_class = CustomPasswordResetForm
105+
template_name = "accounts/password_reset_form.html"
106+
success_url = reverse_lazy("accounts:password_reset_done")
107+
html_email_template_name = "registration/password_reset_email.html"

app/app/urls.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from django.conf import settings
1919
from django.conf.urls.static import static
2020
from django.contrib import admin
21-
from django.contrib.auth import views as auth_views
2221
from django.urls import include, path
2322
from django.utils.translation import gettext_lazy as _
2423

@@ -41,8 +40,7 @@
4140
path("subjects/", views.subjects, name="subjects"),
4241
path("search/", views.search, name="search"),
4342
path("i18n/", include("django.conf.urls.i18n")),
44-
path("accounts/", include("accounts.urls")),
45-
path("accounts/", include("django.contrib.auth.urls")),
43+
path("accounts/", include("accounts.urls"), name="accounts"),
4644
]
4745

4846
if settings.DEBUG:
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{% extends "base.html" %}
2+
{% load static %}
3+
{% load i18n %}
4+
5+
{% block title %}{% trans "Activated" %}{% endblock %}
6+
7+
{% block content %}
8+
<div class="container">
9+
<div class="section mt-3 mb-3">
10+
<div class="card body-card">
11+
<div class="user-account-body">
12+
<h1>{% trans "Account Activated " %}</h1>
13+
<p>{% trans "Your account has been successfully activated. You can now log in using your credentials. "%}</p>
14+
<a href="{% url 'accounts:login' %}">{% trans "Login" %}</a>
15+
</div>
16+
</div>
17+
</div>
18+
</div>
19+
{% endblock %}

0 commit comments

Comments
 (0)