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,38 +1,49 @@
|
|||
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):
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
{% 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 %}
|
||||
<input type="submit" value="Delete" />
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
<a href="{% url 'identifier_list' %}">Back to list</a>
|
||||
<a
|
||||
class="btn btn-link mt-3"
|
||||
href="{% url 'identifier_list' suffix_pk=object.suffix.pk %}"
|
||||
>Back to identifiers</a
|
||||
>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,11 +1,26 @@
|
|||
{% extends "base_generic.html" %} {% block content %}
|
||||
{% extends "base_generic.html" %} {% block title %}Identifier Detail
|
||||
{% endblock %} {% block content %}
|
||||
<div class="container mt-5">
|
||||
<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 %}">
|
||||
<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 type="submit" value="Delete" />
|
||||
<input class="btn btn-danger" type="submit" value="Delete" />
|
||||
</form>
|
||||
<a href="{% url 'identifier_list' %}">Back to list</a>
|
||||
<a
|
||||
class="btn btn-link"
|
||||
href="{% url 'identifier_list' suffix_pk=object.suffix.pk %}"
|
||||
>Back to identifiers</a
|
||||
>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
{% extends "base_generic.html" %} {% block content %}
|
||||
{% 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 }}
|
||||
<input type="submit" value="Save" />
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</form>
|
||||
<a href="{% url 'identifier_list' %}">Back to list</a>
|
||||
<a
|
||||
class="btn btn-link mt-3"
|
||||
href="{% url 'identifier_list' suffix_pk=form.instance.suffix.pk %}"
|
||||
>Back to identifiers</a
|
||||
>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
{% extends "base_generic.html" %} {% block content %}
|
||||
<h1>Identifiers</h1>
|
||||
<ul>
|
||||
{% 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>
|
||||
<a href="{% url 'identifier_detail' identifier.pk %}"
|
||||
>{{ identifier.suffix.prefix.prefix }}.{{ identifier.suffix.suffix }}/
|
||||
{{ identifier.identifier }} ({{ identifier.target_url }})</a
|
||||
<li class="list-group-item">
|
||||
<a
|
||||
href="{% url 'identifier_detail' suffix_pk=suffix.pk pk=identifier.pk %}"
|
||||
>{{ identifier.identifier }}</a
|
||||
>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{% url 'identifier_create' %}">Create new identifier</a>
|
||||
<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 %}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
{% extends "base_generic.html" %} {% block content %}
|
||||
{% 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 %}
|
||||
<input type="submit" value="Delete" />
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
<a href="{% url 'suffix_list' %}">Back to list</a>
|
||||
<a class="btn btn-link mt-3" href="{% url 'suffix_list' %}">Back to list</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
{% extends "base_generic.html" %} {% block title %}Suffix Detail{% endblock %}
|
||||
{% block content %}
|
||||
<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>
|
||||
<a class="btn btn-secondary" href="{% url 'suffix_update' object.pk %}"
|
||||
>Edit</a
|
||||
>
|
||||
<form
|
||||
method="post"
|
||||
action="{% url 'suffix_delete' object.pk %}"
|
||||
|
@ -20,4 +23,5 @@
|
|||
>View Identifiers</a
|
||||
>
|
||||
<a class="btn btn-link" href="{% url 'suffix_list' %}">Back to list</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
{% extends "base_generic.html" %} {% block content %}
|
||||
{% 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 }}
|
||||
<input type="submit" value="Save" />
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</form>
|
||||
<a href="{% url 'suffix_list' %}">Back to list</a>
|
||||
<a class="btn btn-link mt-3" href="{% url 'suffix_list' %}">Back to list</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
{% extends "base_generic.html" %} {% block content %}
|
||||
{% extends "base_generic.html" %} {% block title %}Suffixes{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<h1>Suffixes</h1>
|
||||
<ul>
|
||||
<ul class="list-group">
|
||||
{% for suffix in object_list %}
|
||||
<li><a href="{% url 'suffix_detail' suffix.pk %}">{{ suffix.prefix.prefix }}.{{ suffix.suffix }} ‐ {{ suffix.name }}</a></li>
|
||||
<li class="list-group-item">
|
||||
<a href="{% url 'suffix_detail' suffix.pk %}">{{ suffix.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{% url 'suffix_create' %}">Create new suffix</a>
|
||||
<a class="btn btn-primary mt-3" href="{% url 'suffix_create' %}"
|
||||
>Create new suffix</a
|
||||
>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue