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:
parent
31567f7bb1
commit
c70b190c23
16 changed files with 248 additions and 104 deletions
0
freedoi/accounts/management/__init__.py
Normal file
0
freedoi/accounts/management/__init__.py
Normal file
0
freedoi/accounts/management/commands/__init__.py
Normal file
0
freedoi/accounts/management/commands/__init__.py
Normal 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.')
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 }} ‐ {{ 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 %}
|
||||||
|
|
Loading…
Reference in a new issue