Skip to content

Commit

Permalink
updating user management
Browse files Browse the repository at this point in the history
- added templates
- updated templates
- updated forms
- updated View
  • Loading branch information
daniel-gray-tangent committed Jul 30, 2024
1 parent 323d164 commit 66dbfa0
Show file tree
Hide file tree
Showing 20 changed files with 544 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
SECRET_KEY=''
DEBUG=True
DB_HOST=db
DB_PORT=5432
Expand All @@ -19,3 +18,4 @@ EMAIL_HOST_PASSWORD=''
EMAIL_BACKEND_CONSOLE='True/False'
EMAIL_USE_TLS=True
SECRET_KEY=''
DEFAULT_FROM_EMAIL='[email protected]'
42 changes: 41 additions & 1 deletion app/accounts/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
from django import forms
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import (
AuthenticationForm,
PasswordResetForm,
UserCreationForm,
)
from django.utils.translation import gettext_lazy as _

from users.models import CustomUser
Expand Down Expand Up @@ -40,3 +45,38 @@ def __init__(self, *args, **kwargs):
super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})


class CustomPasswordResetForm(PasswordResetForm):
def clean_email(self):
"""
Cleaning an Email
This method is used to clean an email address provided by the user. It performs the following operations:
1. Retrieves the user model using the "get_user_model()" function.
2. Fetches the email from the "cleaned_data" dictionary.
3. Queries the user model to find all users with the same email address.
4. Checks if any users exist with the given email address. If not, it raises a "forms.ValidationError" with a specific error message.
5. Iterates through each user found with the given email address.
6. Checks if the user is both active and a staff member. If not, it raises a "forms.ValidationError" with a specific error message.
7. Finally, the cleaned email address is returned.
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.
Reason for the Override:
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.
and prevent users getting an email to reset their password if they are not active or staff members.
"""
User = get_user_model()
email = self.cleaned_data["email"]
users = User.objects.get(email=email)
if not users.exists():
raise forms.ValidationError("There is a error please contact the administrator.")

for user in users:
if not user.is_active or not user.is_staff:
raise forms.ValidationError("There is a error please contact the administrator.")

return email
34 changes: 34 additions & 0 deletions app/accounts/service/active_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os

from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode

from accounts.tokens import account_activation_token


class SendActiveEmailService:
@staticmethod
def send_activation_email(request, user):
if user and request:
current_site = get_current_site(request)
mail_subject = "Activate your account."
message = render_to_string(
"accounts/email/activation_email.html",
{
"user": user,
"domain": current_site.domain,
"uid": urlsafe_base64_encode(force_bytes(user.pk)),
"token": account_activation_token.make_token(user),
},
)
text_content = (
"Please activate your account by clicking the link provided in the email."
)
email = EmailMultiAlternatives(
mail_subject, text_content, os.environ.get("DEFAULT_FROM_EMAIL"), [user.email]
)
email.attach_alternative(message, "text/html")
email.send()
54 changes: 54 additions & 0 deletions app/accounts/tests/test_active_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from unittest.mock import MagicMock, patch

from django.contrib.sites.shortcuts import get_current_site
from django.core import mail
from django.test import RequestFactory, TestCase
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode

from accounts.service.active_email import ( # Adjust the import path as necessary
SendActiveEmailService,
)
from accounts.tokens import account_activation_token
from users.models import CustomUser # Import your custom user model


class SendActiveEmailServiceTest(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.user = CustomUser.objects.create_user(
username="testuser", email="[email protected]", password="password123"
)
self.request = self.factory.get("/fake-path")
self.service = SendActiveEmailService()

def test_send_activation_email(self):
with patch("accounts.service.active_email.render_to_string") as mock_render:
# Set up the mocks
mock_render.return_value = "<html>mocked template</html>"

# # Call the method
self.service.send_activation_email(self.request, self.user)

# Check that render_to_string was called with the correct parameters
mock_render.assert_called_once_with(
"accounts/email/activation_email.html",
{
"user": self.user,
"domain": get_current_site(self.request).domain,
"uid": urlsafe_base64_encode(force_bytes(self.user.pk)),
"token": account_activation_token.make_token(self.user),
},
)

# Check that an email was sent
self.assertEqual(len(mail.outbox), 1)
sent_email = mail.outbox[0]
self.assertEqual(sent_email.subject, "Activate your account.")
self.assertEqual(sent_email.to, [self.user.email])
self.assertIn("mocked template", sent_email.alternatives[0][0])
self.assertEqual(sent_email.alternatives[0][1], "text/html")
self.assertIn(
"Please activate your account by clicking the link provided in the email.",
sent_email.body,
)
40 changes: 40 additions & 0 deletions app/accounts/tests/test_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import unittest
from datetime import datetime

import six
from django.contrib.auth.tokens import PasswordResetTokenGenerator

from accounts.tokens import AccountActivationTokenGenerator


# Speculate user model class for test abstraction
class User:
def __init__(self, id, is_active):
self.pk = id
self.is_active = is_active


class TestAccountActivationTokenGenerator(unittest.TestCase):
def setUp(self):
self.generator = AccountActivationTokenGenerator()
self.timestamp = datetime.now()

def test_make_hash_value_active_user(self):
user = User(1, True)
hash_val = self.generator._make_hash_value(user, self.timestamp)
expected_val = (
six.text_type(user.pk) + six.text_type(self.timestamp) + six.text_type(user.is_active)
)
self.assertEqual(hash_val, expected_val)

def test_make_hash_value_inactive_user(self):
user = User(1, False)
hash_val = self.generator._make_hash_value(user, self.timestamp)
expected_val = (
six.text_type(user.pk) + six.text_type(self.timestamp) + six.text_type(user.is_active)
)
self.assertEqual(hash_val, expected_val)


if __name__ == "__main__":
unittest.main()
24 changes: 24 additions & 0 deletions app/accounts/tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.contrib.auth.tokens import PasswordResetTokenGenerator


class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
"""
AccountActivationTokenGenerator class is a subclass of PasswordResetTokenGenerator.
Methods:
_make_hash_value(self, user, timestamp):
This method takes in a user object and a timestamp and returns a hash value
that is used to generate an account activation token. The hash value is
calculated by concatenating the user's primary key, timestamp, and active
status.
DO NOT MODIFY THIS METHOD. UNLESS YOU GET PERMISSION FROM THE PROJECT OWNER.
"""

def _make_hash_value(self, user, timestamp):
return f"{user.pk}{timestamp}{user.is_active}"


account_activation_token = AccountActivationTokenGenerator()
14 changes: 8 additions & 6 deletions app/accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
from django.urls import path

from . import views
from .views import CustomPasswordResetView

app_name = "accounts"
urlpatterns = [
path("register/", views.register, name="accounts_register"),
path("login/", auth_views.LoginView.as_view(template_name="accounts/login.html"), name="login"),
path(
"password_reset/",
auth_views.PasswordResetView.as_view(template_name="accounts/password_reset_form.html"),
name="password_reset",
),
path("password_reset/", CustomPasswordResetView.as_view(), name="password_reset"),
path(
"password_reset/done/",
auth_views.PasswordResetDoneView.as_view(template_name="accounts/password_reset_done.html"),
Expand All @@ -19,7 +17,8 @@
path(
"reset/<uidb64>/<token>/",
auth_views.PasswordResetConfirmView.as_view(
template_name="accounts/password_reset_confirm.html"
template_name="accounts/password_reset_confirm.html",
success_url="/accounts/reset/done/",
),
name="password_reset_confirm",
),
Expand All @@ -30,4 +29,7 @@
),
name="password_reset_complete",
),
path("activate/<uidb64>/<token>/", views.activate, name="activate"),
path("activation_sent/", views.activation_sent, name="activation_sent"),
path("resend_activation/", views.resend_activation, name="resend_activation"),
]
85 changes: 80 additions & 5 deletions app/accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
from django.contrib.auth import authenticate
from django.contrib.auth import get_user_model
from django.contrib.auth import login as auth_login
from django.contrib.auth.views import PasswordResetView
from django.shortcuts import redirect, render
from django.urls import reverse_lazy
from django.utils.encoding import force_str
from django.utils.http import urlsafe_base64_decode

from .forms import CustomAuthenticationForm, CustomUserCreationForm
from accounts.service.active_email import SendActiveEmailService

from .forms import (
CustomAuthenticationForm,
CustomPasswordResetForm,
CustomUserCreationForm,
)
from .tokens import account_activation_token


def register(request):
if request.method == "POST":
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save(commit=False)
user.is_staff = True
user.is_staff = False
user.is_active = False
user.save()
auth_login(request, user)
return redirect("home")

SendActiveEmailService.send_activation_email(request, user)

return redirect("accounts:activation_sent")

else:
form = CustomUserCreationForm()
return render(request, "accounts/register.html", {"form": form})
Expand All @@ -30,3 +45,63 @@ def user_login(request):
form = CustomAuthenticationForm()

return render(request, "accounts/login.html", {"form": form})


def activate(request, uidb64, token):
User = get_user_model() # Get the custom user model
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
user = None

if user is not None and account_activation_token.check_token(user, token):
user.is_staff = True
user.is_active = True
user.save()
auth_login(request, user)
return render(request, "accounts/activate.html")
else:
return render(request, "accounts/activation_invalid.html")


def activation_sent(request):
return render(request, "accounts/activation_sent.html")


def resend_activation(request):
User = get_user_model() #
if request.method == "POST":
user_email = request.POST["email"]

user = User.objects.get(email=user_email)
if not user.is_active:
SendActiveEmailService.send_activation_email(request, user)

return redirect("accounts:activation_sent")

else:
return render(
request,
"accounts/resend_activation.html",
{"error": "There is a error please contact the administrator."},
)


class CustomPasswordResetView(PasswordResetView):
"""
This class represents a custom password reset view for user accounts.
Attributes:
form_class (CustomPasswordResetForm): The form class for the password reset form.
template_name (str): The name of the template for rendering the password reset form.
success_url (reverse_lazy): The URL to redirect to after a successful password reset.
html_email_template_name (str): The name of the template for sending the password reset email.
"""

form_class = CustomPasswordResetForm
template_name = "accounts/password_reset_form.html"
success_url = reverse_lazy("accounts:password_reset_done")
html_email_template_name = "registration/password_reset_email.html"
4 changes: 1 addition & 3 deletions app/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path
from django.utils.translation import gettext_lazy as _

Expand All @@ -41,8 +40,7 @@
path("subjects/", views.subjects, name="subjects"),
path("search/", views.search, name="search"),
path("i18n/", include("django.conf.urls.i18n")),
path("accounts/", include("accounts.urls")),
path("accounts/", include("django.contrib.auth.urls")),
path("accounts/", include("accounts.urls"), name="accounts"),
]

if settings.DEBUG:
Expand Down
13 changes: 13 additions & 0 deletions app/templates/accounts/activate.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}

{% block title %}{% trans "Activated" %}{% endblock %}

{% block content %}
<div class="container">
<h1>Account Activated</h1>
<p>Your account has been successfully activated. You can now log in using your credentials.</p>
<a href="{% url 'accounts:login' %}">Login</a>
</div>
{% endblock %}
Loading

0 comments on commit 66dbfa0

Please sign in to comment.