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.utils import timezone
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('The Email field must be set')
raise ValueError("The Email field must be set")
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.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self.create_user(email, password, **extra_fields)
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
first_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)
maximum_suffixes = models.IntegerField(default=5)
date_joined = models.DateTimeField(default=timezone.now)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
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
urlpatterns = [
path("send-login-email/", SendLoginEmailView.as_view(), name="send_login_email"),
path("login/<uidb64>/<token>/", LoginView.as_view(), name="login"),
path(
"login/",
auth_views.LoginView.as_view(template_name="accounts/login.html"),
SendLoginEmailView.as_view(),
name="login",
),
path("logout/", auth_views.LogoutView.as_view(), name="logout"),

View file

@ -11,25 +11,27 @@ from .forms import EmailForm
User = get_user_model()
class SendLoginEmailView(FormView):
template_name = 'accounts/send_login_email.html'
template_name = "accounts/send_login_email.html"
form_class = EmailForm
success_url = '/accounts/email-sent/'
success_url = "/accounts/email-sent/"
def form_valid(self, form):
email = form.cleaned_data['email']
email = form.cleaned_data["email"]
user = User.objects.get(email=email)
token = default_token_generator.make_token(user)
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(
'Your login link',
f'Click here to log in: {login_url}',
'from@example.com',
"Your login link",
f"Click here to log in: {login_url}",
None,
[email],
)
return super().form_valid(form)
class LoginView(View):
def get(self, request, uidb64, token):
try:
@ -39,8 +41,10 @@ class LoginView(View):
user = None
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)
return redirect('home')
return redirect("home")
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 %}
<h1>Delete Identifier</h1>
<p>Are you sure you want to delete "{{ object.identifier }}"?</p>
<form method="post">
{% csrf_token %}
<input type="submit" value="Delete" />
</form>
<a href="{% url 'identifier_list' %}">Back to list</a>
{% extends "base_generic.html" %} {% block title %}Delete Identifier
{% endblock %} {% block content %}
<div class="container mt-5">
<h1>Delete Identifier</h1>
<p>Are you sure you want to delete "{{ object.identifier }}"?</p>
<form method="post">
{% csrf_token %}
<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 %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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