Skip to content

Commit

Permalink
updating user management
Browse files Browse the repository at this point in the history
updated code

updated code
  • Loading branch information
daniel-gray-tangent committed Jul 19, 2024
1 parent 3e42042 commit a6a20d6
Show file tree
Hide file tree
Showing 19 changed files with 501 additions and 41 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ EMAIL_HOST_USER=''
EMAIL_HOST_PASSWORD=''
EMAIL_BACKEND_CONSOLE='True/False'
EMAIL_USE_TLS=True
DEFAULT_FROM_EMAIL='[email protected]'
22 changes: 21 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,18 @@ 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):
User = get_user_model()
email = self.cleaned_data["email"]
users = User.objects.filter(email=email)
if not users.exists():
raise forms.ValidationError("No user is associated with this email address.")

for user in users:
if not user.is_active or not user.is_staff:
raise forms.ValidationError("This account is inactive or not a staff account.")

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()
10 changes: 10 additions & 0 deletions app/accounts/tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import six
from django.contrib.auth.tokens import PasswordResetTokenGenerator


class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
def _make_hash_value(self, user, timestamp):
return six.text_type(user.pk) + six.text_type(timestamp) + six.text_type(user.is_active)


account_activation_token = AccountActivationTokenGenerator()
10 changes: 8 additions & 2 deletions app/accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
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"),
CustomPasswordResetView.as_view(),
name="password_reset",
),
path(
Expand All @@ -19,7 +21,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 +33,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"),
]
77 changes: 72 additions & 5 deletions app/accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
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.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 +44,56 @@ 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"]
try:
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": "Email address active."}
)

except User.DoesNotExist:
# Handle the case where the email does not exist
return render(
request, "accounts/resend_activation.html", {"error": "Email address not found."}
)
return render(request, "accounts/resend_activation.html")


class CustomPasswordResetView(PasswordResetView):
form_class = CustomPasswordResetForm
template_name = "accounts/password_reset_form.html"
success_url = "/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("subject/<int:pk>/", views.subject_detail, name="subject_detail"),
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 %}
20 changes: 20 additions & 0 deletions app/templates/accounts/activation_invalid.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}

{% block title %}{% trans "Sign Up" %}{% endblock %}

{% block content %}
<div class="container">
<div class="section mt-3 mb-3">
<div class="card body-card">
<div class="user-account-body">
<h2>Activation Invalid</h2>
<p>Sorry, but the activation link you used is invalid or has expired.</p>
<p>Please request a new activation link or contact support for further assistance.</p>
<a href="{% url 'accounts:accounts_register' %}">Register</a> | <a href="{% url 'accounts:login' %}">Login</a>
</div>
</div>
</div>
</div>
{% endblock %}
22 changes: 22 additions & 0 deletions app/templates/accounts/activation_sent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}

{% block title %}{% trans "Activation Email Sent" %}{% endblock %}

{% block content %}
<div class="container">
<div class="section mt-3 mb-3">
<div class="card body-card">
<div class="user-account-body">
<h2>Activation Email Sent</h2>
<p>Thank you for registering. An activation email has been sent to your email address.</p>
<p>Please check your email and click on the activation link to activate your account.</p>
<p>If you did not receive the email, please check your spam folder or
<a href="{% url 'accounts:resend_activation' %}">resend the activation email</a>.</p>
<a href="{% url 'accounts:login' %}">Login</a>
</div>
</div>
</div>
</div>
{% endblock %}
Loading

0 comments on commit a6a20d6

Please sign in to comment.