feat: add inactive user cleanup and refine UI

Introduce a management command to delete inactive users older than 7 days. Adjust `CustomUser` model to include `date_joined` with new default settings. Enhance UI across multiple templates to improve consistency and user experience.

- Inactive user accounts cleanup enhances database hygiene.
- Updates to `CustomUser` default settings ensure consistent account states.
- Refined UI for identifier and suffix templates for better accessibility and UX.
This commit is contained in:
Kumi 2024-06-23 08:56:22 +02:00
parent 31567f7bb1
commit c70b190c23
Signed by: kumi
GPG key ID: ECBCC9082395383F
16 changed files with 248 additions and 104 deletions

View file

View file

@ -0,0 +1,14 @@
import datetime
from django.core.management.base import BaseCommand
from django.utils import timezone
from accounts.models import CustomUser
class Command(BaseCommand):
help = 'Deletes inactive user accounts that were created more than 7 days ago'
def handle(self, *args, **kwargs):
expiration_date = timezone.now() - datetime.timedelta(days=7)
inactive_users = CustomUser.objects.filter(is_active=False, date_joined__lt=expiration_date)
count = inactive_users.count()
inactive_users.delete()
self.stdout.write(f'Deleted {count} inactive user accounts.')

View file

@ -0,0 +1,24 @@
# Generated by Django 5.0.6 on 2024-06-23 05:39
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0002_customuser_maximum_suffixes"),
]
operations = [
migrations.AddField(
model_name="customuser",
name="date_joined",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AlterField(
model_name="customuser",
name="is_active",
field=models.BooleanField(default=False),
),
]

View file

@ -1,39 +1,50 @@
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
PermissionsMixin,
)
from django.db import models from django.db import models
from django.utils import timezone
class CustomUserManager(BaseUserManager): class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields): def create_user(self, email, password=None, **extra_fields):
if not email: if not email:
raise ValueError('The Email field must be set') raise ValueError("The Email field must be set")
email = self.normalize_email(email) email = self.normalize_email(email)
extra_fields.setdefault("is_active", False)
extra_fields.setdefault("date_joined", timezone.now())
user = self.model(email=email, **extra_fields) user = self.model(email=email, **extra_fields)
user.set_password(password) user.set_password(password)
user.save(using=self._db) user.save(using=self._db)
return user return user
def create_superuser(self, email, password=None, **extra_fields): def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True) extra_fields.setdefault("is_staff", True)
extra_fields.setdefault('is_superuser', True) extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get('is_staff') is not True: if extra_fields.get("is_staff") is not True:
raise ValueError('Superuser must have is_staff=True.') raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get('is_superuser') is not True: if extra_fields.get("is_superuser") is not True:
raise ValueError('Superuser must have is_superuser=True.') raise ValueError("Superuser must have is_superuser=True.")
return self.create_user(email, password, **extra_fields) return self.create_user(email, password, **extra_fields)
class CustomUser(AbstractBaseUser, PermissionsMixin): class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True) email = models.EmailField(unique=True)
first_name = models.CharField(max_length=30, blank=True) first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=30, blank=True) last_name = models.CharField(max_length=30, blank=True)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=False)
is_staff = models.BooleanField(default=False) is_staff = models.BooleanField(default=False)
maximum_suffixes = models.IntegerField(default=5) maximum_suffixes = models.IntegerField(default=5)
date_joined = models.DateTimeField(default=timezone.now)
objects = CustomUserManager() objects = CustomUserManager()
USERNAME_FIELD = 'email' USERNAME_FIELD = "email"
REQUIRED_FIELDS = [] REQUIRED_FIELDS = []
def __str__(self): def __str__(self):
return self.email return self.email

View file

@ -4,11 +4,10 @@ from django.views.generic import TemplateView
from .views import SendLoginEmailView, LoginView from .views import SendLoginEmailView, LoginView
urlpatterns = [ urlpatterns = [
path("send-login-email/", SendLoginEmailView.as_view(), name="send_login_email"),
path("login/<uidb64>/<token>/", LoginView.as_view(), name="login"), path("login/<uidb64>/<token>/", LoginView.as_view(), name="login"),
path( path(
"login/", "login/",
auth_views.LoginView.as_view(template_name="accounts/login.html"), SendLoginEmailView.as_view(),
name="login", name="login",
), ),
path("logout/", auth_views.LogoutView.as_view(), name="logout"), path("logout/", auth_views.LogoutView.as_view(), name="logout"),

View file

@ -11,25 +11,27 @@ from .forms import EmailForm
User = get_user_model() User = get_user_model()
class SendLoginEmailView(FormView): class SendLoginEmailView(FormView):
template_name = 'accounts/send_login_email.html' template_name = "accounts/send_login_email.html"
form_class = EmailForm form_class = EmailForm
success_url = '/accounts/email-sent/' success_url = "/accounts/email-sent/"
def form_valid(self, form): def form_valid(self, form):
email = form.cleaned_data['email'] email = form.cleaned_data["email"]
user = User.objects.get(email=email) user = User.objects.get(email=email)
token = default_token_generator.make_token(user) token = default_token_generator.make_token(user)
uid = urlsafe_base64_encode(force_bytes(user.pk)) uid = urlsafe_base64_encode(force_bytes(user.pk))
login_url = self.request.build_absolute_uri(f'/accounts/login/{uid}/{token}/') login_url = self.request.build_absolute_uri(f"/accounts/login/{uid}/{token}/")
send_mail( send_mail(
'Your login link', "Your login link",
f'Click here to log in: {login_url}', f"Click here to log in: {login_url}",
'from@example.com', None,
[email], [email],
) )
return super().form_valid(form) return super().form_valid(form)
class LoginView(View): class LoginView(View):
def get(self, request, uidb64, token): def get(self, request, uidb64, token):
try: try:
@ -39,8 +41,10 @@ class LoginView(View):
user = None user = None
if user is not None and default_token_generator.check_token(user, token): if user is not None and default_token_generator.check_token(user, token):
user.backend = 'django.contrib.auth.backends.ModelBackend' if not user.is_active:
user.is_active = True
user.save()
auth_login(request, user) auth_login(request, user)
return redirect('home') return redirect("home")
else: else:
return HttpResponse('Login link is invalid') return HttpResponse("Login link is invalid")

View file

@ -0,0 +1,34 @@
# Generated by Django 5.0.6 on 2024-06-23 05:39
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("resolver", "0005_alter_prefix_options_alter_suffix_options"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name="suffix",
options={"ordering": ["prefix", "suffix"]},
),
migrations.AlterField(
model_name="prefix",
name="owner",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
),
),
migrations.AlterField(
model_name="suffix",
name="prefix",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="resolver.prefix"
),
),
]

View file

@ -1,9 +1,16 @@
{% extends "base_generic.html" %} {% block content %} {% extends "base_generic.html" %} {% block title %}Delete Identifier
<h1>Delete Identifier</h1> {% endblock %} {% block content %}
<p>Are you sure you want to delete "{{ object.identifier }}"?</p> <div class="container mt-5">
<form method="post"> <h1>Delete Identifier</h1>
{% csrf_token %} <p>Are you sure you want to delete "{{ object.identifier }}"?</p>
<input type="submit" value="Delete" /> <form method="post">
</form> {% csrf_token %}
<a href="{% url 'identifier_list' %}">Back to list</a> <button type="submit" class="btn btn-danger">Delete</button>
</form>
<a
class="btn btn-link mt-3"
href="{% url 'identifier_list' suffix_pk=object.suffix.pk %}"
>Back to identifiers</a
>
</div>
{% endblock %} {% endblock %}

View file

@ -1,11 +1,26 @@
{% extends "base_generic.html" %} {% block content %} {% extends "base_generic.html" %} {% block title %}Identifier Detail
<h1>{{ object.identifier }}</h1> {% endblock %} {% block content %}
<p>Suffix: {{ object.suffix }}</p> <div class="container mt-5">
<p>Target URL: {{ object.target_url }}</p> <h1>{{ object.identifier }}</h1>
<a href="{% url 'identifier_update' object.pk %}">Edit</a> <p><strong>Suffix:</strong> {{ object.suffix }}</p>
<form method="post" action="{% url 'identifier_delete' object.pk %}"> <p><strong>Target URL:</strong> {{ object.target_url }}</p>
{% csrf_token %} <a
<input type="submit" value="Delete" /> class="btn btn-secondary"
</form> href="{% url 'identifier_update' suffix_pk=object.suffix.pk pk=object.pk %}"
<a href="{% url 'identifier_list' %}">Back to list</a> >Edit</a
>
<form
method="post"
action="{% url 'identifier_delete' suffix_pk=object.suffix.pk pk=object.pk %}"
style="display: inline"
>
{% csrf_token %}
<input class="btn btn-danger" type="submit" value="Delete" />
</form>
<a
class="btn btn-link"
href="{% url 'identifier_list' suffix_pk=object.suffix.pk %}"
>Back to identifiers</a
>
</div>
{% endblock %} {% endblock %}

View file

@ -1,8 +1,16 @@
{% extends "base_generic.html" %} {% block content %} {% extends "base_generic.html" %} {% block title %}
<h1>{% if form.instance.pk %}Edit{% else %}Create{% endif %} Identifier</h1> {% if form.instance.pk %}Edit{% else %}Create{% endif %} Identifier
<form method="post"> {% endblock %} {% block content %}
{% csrf_token %} {{ form.as_p }} <div class="container mt-5">
<input type="submit" value="Save" /> <h1>{% if form.instance.pk %}Edit{% else %}Create{% endif %} Identifier</h1>
</form> <form method="post">
<a href="{% url 'identifier_list' %}">Back to list</a> {% csrf_token %} {{ form.as_p }}
<button type="submit" class="btn btn-primary">Save</button>
</form>
<a
class="btn btn-link mt-3"
href="{% url 'identifier_list' suffix_pk=form.instance.suffix.pk %}"
>Back to identifiers</a
>
</div>
{% endblock %} {% endblock %}

View file

@ -1,14 +1,24 @@
{% extends "base_generic.html" %} {% block content %} {% extends "base_generic.html" %} {% block title %}Identifiers{% endblock %}
<h1>Identifiers</h1> {% block content %}
<ul> <div class="container mt-5">
{% for identifier in object_list %} <h1>Identifiers for {{ suffix.name }}</h1>
<li> <ul class="list-group">
<a href="{% url 'identifier_detail' identifier.pk %}" {% for identifier in object_list %}
>{{ identifier.suffix.prefix.prefix }}.{{ identifier.suffix.suffix }}/ <li class="list-group-item">
{{ identifier.identifier }} ({{ identifier.target_url }})</a <a
> href="{% url 'identifier_detail' suffix_pk=suffix.pk pk=identifier.pk %}"
</li> >{{ identifier.identifier }}</a
{% endfor %} >
</ul> </li>
<a href="{% url 'identifier_create' %}">Create new identifier</a> {% endfor %}
</ul>
<a
class="btn btn-primary mt-3"
href="{% url 'identifier_create' suffix_pk=suffix.pk %}"
>Create new identifier</a
>
<a class="btn btn-link mt-3" href="{% url 'suffix_list' %}"
>Back to suffixes</a
>
</div>
{% endblock %} {% endblock %}

View file

@ -1,9 +1,12 @@
{% extends "base_generic.html" %} {% block content %} {% extends "base_generic.html" %} {% block title %}Delete Suffix{% endblock %}
<h1>Delete Suffix</h1> {% block content %}
<p>Are you sure you want to delete "{{ object.name }}"?</p> <div class="container mt-5">
<form method="post"> <h1>Delete Suffix</h1>
{% csrf_token %} <p>Are you sure you want to delete "{{ object.name }}"?</p>
<input type="submit" value="Delete" /> <form method="post">
</form> {% csrf_token %}
<a href="{% url 'suffix_list' %}">Back to list</a> <button type="submit" class="btn btn-danger">Delete</button>
</form>
<a class="btn btn-link mt-3" href="{% url 'suffix_list' %}">Back to list</a>
</div>
{% endblock %} {% endblock %}

View file

@ -1,23 +1,27 @@
{% extends "base_generic.html" %} {% block title %}Suffix Detail{% endblock %} {% extends "base_generic.html" %} {% block title %}Suffix Detail{% endblock %}
{% block content %} {% block content %}
<h1>{{ object.name }}</h1> <div class="container mt-5">
<p><strong>Suffix:</strong> {{ object.suffix }}</p> <h1>{{ object.name }}</h1>
<p><strong>Prefix:</strong> {{ object.prefix }}</p> <p><strong>Suffix:</strong> {{ object.suffix }}</p>
<p><strong>Type:</strong> {{ object.get_type_display }}</p> <p><strong>Prefix:</strong> {{ object.prefix }}</p>
<p><strong>Remote Resolver:</strong> {{ object.remote_resolver }}</p> <p><strong>Type:</strong> {{ object.get_type_display }}</p>
<a class="btn btn-secondary" href="{% url 'suffix_update' object.pk %}">Edit</a> <p><strong>Remote Resolver:</strong> {{ object.remote_resolver }}</p>
<form <a class="btn btn-secondary" href="{% url 'suffix_update' object.pk %}"
method="post" >Edit</a
action="{% url 'suffix_delete' object.pk %}" >
style="display: inline" <form
> method="post"
{% csrf_token %} action="{% url 'suffix_delete' object.pk %}"
<input class="btn btn-danger" type="submit" value="Delete" /> style="display: inline"
</form> >
<a {% csrf_token %}
class="btn btn-primary" <input class="btn btn-danger" type="submit" value="Delete" />
href="{% url 'identifier_list' suffix_pk=object.pk %}" </form>
>View Identifiers</a <a
> class="btn btn-primary"
<a class="btn btn-link" href="{% url 'suffix_list' %}">Back to list</a> href="{% url 'identifier_list' suffix_pk=object.pk %}"
>View Identifiers</a
>
<a class="btn btn-link" href="{% url 'suffix_list' %}">Back to list</a>
</div>
{% endblock %} {% endblock %}

View file

@ -1,8 +1,12 @@
{% extends "base_generic.html" %} {% block content %} {% extends "base_generic.html" %} {% block title %}
<h1>{% if form.instance.pk %}Edit{% else %}Create{% endif %} Suffix</h1> {% if form.instance.pk %}Edit{% else %}Create{% endif %} Suffix{% endblock %}
<form method="post"> {% block content %}
{% csrf_token %} {{ form.as_p }} <div class="container mt-5">
<input type="submit" value="Save" /> <h1>{% if form.instance.pk %}Edit{% else %}Create{% endif %} Suffix</h1>
</form> <form method="post">
<a href="{% url 'suffix_list' %}">Back to list</a> {% csrf_token %} {{ form.as_p }}
<button type="submit" class="btn btn-primary">Save</button>
</form>
<a class="btn btn-link mt-3" href="{% url 'suffix_list' %}">Back to list</a>
</div>
{% endblock %} {% endblock %}

View file

@ -1,9 +1,16 @@
{% extends "base_generic.html" %} {% block content %} {% extends "base_generic.html" %} {% block title %}Suffixes{% endblock %}
<h1>Suffixes</h1> {% block content %}
<ul> <div class="container mt-5">
{% for suffix in object_list %} <h1>Suffixes</h1>
<li><a href="{% url 'suffix_detail' suffix.pk %}">{{ suffix.prefix.prefix }}.{{ suffix.suffix }} &dash; {{ suffix.name }}</a></li> <ul class="list-group">
{% endfor %} {% for suffix in object_list %}
</ul> <li class="list-group-item">
<a href="{% url 'suffix_create' %}">Create new suffix</a> <a href="{% url 'suffix_detail' suffix.pk %}">{{ suffix.name }}</a>
</li>
{% endfor %}
</ul>
<a class="btn btn-primary mt-3" href="{% url 'suffix_create' %}"
>Create new suffix</a
>
</div>
{% endblock %} {% endblock %}