From 55f9063206b666ef4b66378bab2ba21fa111f810 Mon Sep 17 00:00:00 2001 From: GithiomiAnn Date: Sun, 14 Sep 2025 10:09:15 +1000 Subject: [PATCH 1/9] Secure Delete Account Feature Introduced three choices for users when they want to delete their accounts. They receive an email notification upon each request. --- .../commands/delete_expired_accounts.py | 31 +++++++ home/migrations/0002_adninsesssion.py | 30 +++++++ home/migrations/0003_userdeletionrequest.py | 26 ++++++ .../0004_userdeletionrequest_executed_at.py | 18 +++++ home/models.py | 21 ++++- home/templates/accounts/confirm_delete.html | 81 +++++++++++++++++-- home/views.py | 64 +++++++++++++-- 7 files changed, 255 insertions(+), 16 deletions(-) create mode 100644 home/management/commands/delete_expired_accounts.py create mode 100644 home/migrations/0002_adninsesssion.py create mode 100644 home/migrations/0003_userdeletionrequest.py create mode 100644 home/migrations/0004_userdeletionrequest_executed_at.py diff --git a/home/management/commands/delete_expired_accounts.py b/home/management/commands/delete_expired_accounts.py new file mode 100644 index 000000000..859df59d5 --- /dev/null +++ b/home/management/commands/delete_expired_accounts.py @@ -0,0 +1,31 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from django.contrib.auth.models import User +from home.models import UserDeletionRequest + +class Command(BaseCommand): + help = "Deletes user accounts that have been scheduled for deletion after 30 days" + + def handle(self, *args, **kwargs): + now = timezone.now() + expired_requests = UserDeletionRequest.objects.filter( + scheduled_for__lte=now, + is_executed=False + ) + if not expired_requests.exists(): + self.stdout.write("No expired deletion requests to process.") + return + + for deletion_request in expired_requests: + user = deletion_request.user + self.stdout.write( + f"Deleting user {user.username} (scheduled for {deletion_request.scheduled_for})" + ) + + user.delete() #user is deleted + + deletion_request.is_executed = True + deletion_request.executed_at = now + deletion_request.save() + + self.stdout.write(self.style.SUCCESS("Expired accounts deletion task complete.")) \ No newline at end of file diff --git a/home/migrations/0002_adninsesssion.py b/home/migrations/0002_adninsesssion.py new file mode 100644 index 000000000..86cbba2c4 --- /dev/null +++ b/home/migrations/0002_adninsesssion.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.14 on 2025-09-01 22:12 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AdninSesssion', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('session_key', models.CharField(max_length=40, unique=True)), + ('ip_address', models.GenericIPAddressField()), + ('user_agent', models.TextField()), + ('login_time', models.DateTimeField(auto_now_add=True)), + ('last_activity', models.DateTimeField(auto_now=True)), + ('is_active', models.BooleanField(default=True)), + ('logout_time', models.DateTimeField(blank=True, null=True)), + ('logout_reason', models.CharField(blank=True, max_length=50, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admin_sessions', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/home/migrations/0003_userdeletionrequest.py b/home/migrations/0003_userdeletionrequest.py new file mode 100644 index 000000000..d3454ac7c --- /dev/null +++ b/home/migrations/0003_userdeletionrequest.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.14 on 2025-09-02 05:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0002_adninsesssion'), + ] + + operations = [ + migrations.CreateModel( + name='UserDeletionRequest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('requested_at', models.DateTimeField(default=django.utils.timezone.now)), + ('scheduled_for', models.DateTimeField()), + ('is_executed', models.BooleanField(default=False)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='deletion_request', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/home/migrations/0004_userdeletionrequest_executed_at.py b/home/migrations/0004_userdeletionrequest_executed_at.py new file mode 100644 index 000000000..a2d9bf8c0 --- /dev/null +++ b/home/migrations/0004_userdeletionrequest_executed_at.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.14 on 2025-09-13 23:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0003_userdeletionrequest'), + ] + + operations = [ + migrations.AddField( + model_name='userdeletionrequest', + name='executed_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/home/models.py b/home/models.py index 413e244f6..3c306d68f 100644 --- a/home/models.py +++ b/home/models.py @@ -890,8 +890,8 @@ def is_expired(self, timeout_minutes=30): return now() > expiry_time def update_activity(self): - self.last_activity = now() - self.save(update_fields=['last_activity']) + self.last_activity = now() self.save(update_fields=['last_activity']) + class Resource(models.Model): class Category(models.TextChoices): @@ -987,3 +987,20 @@ def save(self, *args, **kwargs): def __str__(self): return self.title + + +class UserDeletionRequest(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="deletion_request") + requested_at = models.DateTimeField(default=timezone.now) + scheduled_for = models.DateTimeField() + is_executed = models.BooleanField(default=False) + executed_at = models.DateTimeField(null=True, blank=True) + + def save(self, *args, **kwargs): + if not self.scheduled_for: + self.scheduled_for = self.requested_at + timedelta(days=30) + super().save(*args, **kwargs) + + def __str__(self): + status = "Executed" if self.is_executed else "Pending" + return f"Deletion request for {self.user.username} ({status}), scheduled {self.scheduled_for}" diff --git a/home/templates/accounts/confirm_delete.html b/home/templates/accounts/confirm_delete.html index 45b45ad85..3bf3e0b30 100644 --- a/home/templates/accounts/confirm_delete.html +++ b/home/templates/accounts/confirm_delete.html @@ -1,8 +1,73 @@ -

Are you sure you want to delete your account?

-

This action cannot be undone.

- -
- {% csrf_token %} - - Cancel -
+{% load static %} + +{% block content %} +
+
+

Confirm Account Action

+ + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + +
+ {% csrf_token %} + +
+ + +
+ +

Select an action:

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ Cancel + +
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/home/views.py b/home/views.py index 111e0dfbf..7266f4b0e 100644 --- a/home/views.py +++ b/home/views.py @@ -50,10 +50,11 @@ from .models import Article, Student, Project, Contact, Smishingdetection_join_us, Projects_join_us, Webpage, Profile, User, Course, Skill, Experience, Job, JobAlert, UserBlogPage, VaultDocument #Feedback - +from django.contrib.auth.hashers import check_password from django.contrib.auth import get_user_model from .models import User from django.utils import timezone +from datetime import timedelta from django.core.mail import send_mail from django.core.exceptions import ValidationError from django.utils.http import urlsafe_base64_encode @@ -173,6 +174,8 @@ def wrapped_view(request, *args, **kwargs): from .forms import PenTestingRequestForm, SecureCodeReviewRequestForm from .models import AppAttackReport +from .models import UserDeletionRequest + from home.models import TeamMember @@ -3631,11 +3634,60 @@ def secure_code_review_form_view(request): def delete_account(request): if request.method == 'POST': user = request.user - user.delete() - logout(request) - messages.success(request, "Your account has been deleted.") - return redirect('login') - return HttpResponseNotAllowed(['POST']) + choice = request.POST.get("choice") + password = request.POST.get("password") + + if not password: + messages.error(request, "Password Required") + return render(request, "accounts/confirm_delete.html") + + if not check_password(password, user.password): + messages.error(request, "Incorrect password. Please try again.") + return redirect('delete-account') + + if choice == "deactivate": + logout(request) + send_mail( + "Account Deactivated", + "Your account has been temporarily deactivated. Log in again to reactivate.", + "admin@example.com", + [user.email], + ) + messages.success(request, "Your account has been deactivated.") + return redirect('login') + + elif choice == "delete_now": + email = user.email + user.delete() + send_mail( + "Account Deleted", + "Your account has been permanently deleted.", + "admin@example.com", + [email], + ) + messages.success(request, "Your account has been permanently deleted.") + return redirect('login') + + elif choice == "delete_30days": + UserDeletionRequest.objects.update_or_create( + user=user, + defaults={"scheduled_for": timezone.now() + timedelta(days=30)} + ) + logout(request) + send_mail( + "Account Scheduled for Deletion", + "Your account is deactivated and will be permanently deleted in 30 days unless reactivated.", + "admin@example.com", + [user.email], + ) + messages.success(request, "Your account is scheduled for deletion in 30 days.") + return redirect('login') + + else: + messages.error(request, "Invalid option.") + return redirect('delete-account') + + return render(request, "accounts/confirm_delete.html") def tools_home(request): return render(request, 'pages/pt_gui/tools/index.html') From 2f9d16f49829a5f669ed1f04130ca52cbeae4525 Mon Sep 17 00:00:00 2001 From: GithiomiAnn Date: Sat, 20 Sep 2025 23:16:31 +1000 Subject: [PATCH 2/9] Include new migration in initial migration file --- home/migrations/0001_initial.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/home/migrations/0001_initial.py b/home/migrations/0001_initial.py index 1a3c7e61a..734195987 100644 --- a/home/migrations/0001_initial.py +++ b/home/migrations/0001_initial.py @@ -609,4 +609,25 @@ class Migration(migrations.Migration): 'ordering': ['-published_at'], }, ), + + migrations.CreateModel( + name='UserDeletionRequest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('requested_at', models.DateTimeField(default=django.utils.timezone.now)), + ('scheduled_for', models.DateTimeField()), + ('is_executed', models.BooleanField(default=False)), + ('executed_at', models.DateTimeField(blank=True, null=True)), + ('user', models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name='deletion_request', + to=settings.AUTH_USER_MODEL + )), + ], + ), + + + + + ] From 9baaa36797ab197fae32e8d10e6395a140eaf4dc Mon Sep 17 00:00:00 2001 From: GithiomiAnn Date: Sun, 21 Sep 2025 09:04:44 +1000 Subject: [PATCH 3/9] Fixed checks --- home/models.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/home/models.py b/home/models.py index 3c306d68f..35aa7cbe1 100644 --- a/home/models.py +++ b/home/models.py @@ -873,9 +873,9 @@ class AdminSession(models.Model): class Meta: ordering = ['-login_time'] - def __str__(self): - admin_type = "Superuser" if self.user.is_superuser else "Staff" - return f"{admin_type} session for {self.user.email} - {self.login_time}" + def __str__(self): + admin_type = "Superuser" if self.user.is_superuser else "Staff" + return f"{admin_type} session for {self.user.email} - {self.login_time}" def mark_logout(self, reason="manual"): self.is_active = False @@ -883,14 +883,15 @@ def mark_logout(self, reason="manual"): self.logout_reason = reason self.save() - def is_expired(self, timeout_minutes=30): - if not self.is_active: - return True - expiry_time = self.last_activity + timedelta(minutes=timeout_minutes) - return now() > expiry_time + def is_expired(self, timeout_minutes=30): + if not self.is_active: + return True + expiry_time = self.last_activity + timedelta(minutes=timeout_minutes) + return now() > expiry_time -def update_activity(self): - self.last_activity = now() self.save(update_fields=['last_activity']) + def update_activity(self): + self.last_activity = now() + self.save(update_fields=['last_activity']) class Resource(models.Model): @@ -988,6 +989,10 @@ def save(self, *args, **kwargs): def __str__(self): return self.title + def update_activity(self): + + self.last_activity = now() + self.save(update_fields=['last_activity']) class UserDeletionRequest(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="deletion_request") From 8a1315ee657eef7a14f3beea1ee4ade83ea5bef7 Mon Sep 17 00:00:00 2001 From: GithiomiAnn Date: Sun, 21 Sep 2025 09:22:20 +1000 Subject: [PATCH 4/9] Fixed models.py file --- home/models.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/home/models.py b/home/models.py index 35aa7cbe1..072a76fe3 100644 --- a/home/models.py +++ b/home/models.py @@ -893,6 +893,80 @@ def update_activity(self): self.last_activity = now() self.save(update_fields=['last_activity']) +class Resource(models.Model): + class Category(models.TextChoices): + WHITEPAPER = "whitepaper", "Whitepaper" + CHECKLIST = "checklist", "Checklist / Guide" + INFOGRAPH = "infographic", "Infographic" + CASESTUDY = "casestudy", "Case Study" + OTHER = "other", "Other" + + title = models.CharField(max_length=180) + slug = models.SlugField(max_length=200, unique=True, blank=True) + summary = models.TextField(max_length=600, help_text="Short 1–3 line description.") + category = models.CharField(max_length=20, choices=Category.choices, default=Category.OTHER) + file = models.FileField(upload_to="resources/files/") + cover = models.ImageField(upload_to="resources/covers/", blank=True, null=True) + is_published = models.BooleanField(default=True) + published_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-published_at"] + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.title)[:190] + return super().save(*args, **kwargs) + + def __str__(self): + return self.title + + +class Tip(models.Model): + text = models.CharField(max_length=280, unique=True) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = "Daily Security Tip" + verbose_name_plural = "Daily Security Tips" + + def __str__(self): + return self.text[:60] + +# keep this only if you implemented 24h rolling rotation +class TipRotationState(models.Model): + lock = models.CharField(max_length=16, default="default", unique=True) + last_index = models.IntegerField(default=-1) + rotated_at = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return f"{self.lock} @ {self.rotated_at or 'never'} (idx={self.last_index})" + +#Model to track known devices +class UserDevice(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="devices" + ) + # Device fingerprint (unique identifier) + device_fingerprint = models.CharField(max_length=255, null=True, blank=True) + + # User-friendly info + device_name = models.CharField(max_length=200) + + # Technical info + user_agent = models.TextField() + ip_address = models.GenericIPAddressField() + + # Timestamps + created_at = models.DateTimeField(auto_now_add=True) + last_seen = models.DateTimeField(auto_now=True) + def __str__(self): + return f"{self.user.email} - {self.device_name} ({self.ip_address})" + class Resource(models.Model): class Category(models.TextChoices): From cd76a57573f724475854cb6d99ea045b704edccb Mon Sep 17 00:00:00 2001 From: GithiomiAnn Date: Sun, 21 Sep 2025 18:22:51 +1000 Subject: [PATCH 5/9] match upstream main models.py --- home/models.py | 62 +++++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/home/models.py b/home/models.py index 072a76fe3..7e042c834 100644 --- a/home/models.py +++ b/home/models.py @@ -877,11 +877,17 @@ def __str__(self): admin_type = "Superuser" if self.user.is_superuser else "Staff" return f"{admin_type} session for {self.user.email} - {self.login_time}" - def mark_logout(self, reason="manual"): - self.is_active = False - self.logout_time = now() - self.logout_reason = reason - self.save() + def mark_logout(self, reason="manual"): + self.is_active = False + self.logout_time = now() + self.logout_reason = reason + self.save() + + def is_expired(self, timeout_minutes=30): + if not self.is_active: + return True + expiry_time = self.last_activity + timedelta(minutes=timeout_minutes) + return now() > expiry_time def is_expired(self, timeout_minutes=30): if not self.is_active: @@ -889,9 +895,9 @@ def is_expired(self, timeout_minutes=30): expiry_time = self.last_activity + timedelta(minutes=timeout_minutes) return now() > expiry_time - def update_activity(self): - self.last_activity = now() - self.save(update_fields=['last_activity']) + def update_activity(self): + self.last_activity = now() + self.save(update_fields=['last_activity']) class Resource(models.Model): class Category(models.TextChoices): @@ -927,47 +933,47 @@ class Tip(models.Model): text = models.CharField(max_length=280, unique=True) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) - + class Meta: verbose_name = "Daily Security Tip" verbose_name_plural = "Daily Security Tips" - + def __str__(self): return self.text[:60] -# keep this only if you implemented 24h rolling rotation class TipRotationState(models.Model): lock = models.CharField(max_length=16, default="default", unique=True) last_index = models.IntegerField(default=-1) rotated_at = models.DateTimeField(null=True, blank=True) - + def __str__(self): return f"{self.lock} @ {self.rotated_at or 'never'} (idx={self.last_index})" - -#Model to track known devices + class UserDevice(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="devices" ) - # Device fingerprint (unique identifier) device_fingerprint = models.CharField(max_length=255, null=True, blank=True) - - # User-friendly info device_name = models.CharField(max_length=200) - - # Technical info user_agent = models.TextField() ip_address = models.GenericIPAddressField() - - # Timestamps created_at = models.DateTimeField(auto_now_add=True) last_seen = models.DateTimeField(auto_now=True) + def __str__(self): return f"{self.user.email} - {self.device_name} ({self.ip_address})" +class Resource(models.Model): + class Category(models.TextChoices): + WHITEPAPER = "whitepaper", "Whitepaper" + CHECKLIST = "checklist", "Checklist / Guide" + INFOGRAPH = "infographic", "Infographic" + CASESTUDY = "casestudy", "Case Study" + OTHER = "other", "Other" +<<<<<<< HEAD class Resource(models.Model): class Category(models.TextChoices): WHITEPAPER = "whitepaper", "Whitepaper" @@ -976,6 +982,8 @@ class Category(models.TextChoices): CASESTUDY = "casestudy", "Case Study" OTHER = "other", "Other" +======= +>>>>>>> c1bbe61a (match upstream main models.py) title = models.CharField(max_length=180) slug = models.SlugField(max_length=200, unique=True, blank=True) summary = models.TextField(max_length=600, help_text="Short 1–3 line description.") @@ -988,6 +996,7 @@ class Category(models.TextChoices): class Meta: ordering = ["-published_at"] +<<<<<<< HEAD def save(self, *args, **kwargs): if not self.slug: @@ -1074,12 +1083,13 @@ class UserDeletionRequest(models.Model): scheduled_for = models.DateTimeField() is_executed = models.BooleanField(default=False) executed_at = models.DateTimeField(null=True, blank=True) +======= +>>>>>>> c1bbe61a (match upstream main models.py) def save(self, *args, **kwargs): - if not self.scheduled_for: - self.scheduled_for = self.requested_at + timedelta(days=30) - super().save(*args, **kwargs) + if not self.slug: + self.slug = slugify(self.title)[:190] + return super().save(*args, **kwargs) def __str__(self): - status = "Executed" if self.is_executed else "Pending" - return f"Deletion request for {self.user.username} ({status}), scheduled {self.scheduled_for}" + return self.title From 9b687bd896ffb0383cc353fa2f322ff55abbe5b1 Mon Sep 17 00:00:00 2001 From: GithiomiAnn Date: Sun, 21 Sep 2025 18:24:50 +1000 Subject: [PATCH 6/9] Added UserDeletionRequest to models.py --- home/models.py | 123 ++----------------------------------------------- 1 file changed, 5 insertions(+), 118 deletions(-) diff --git a/home/models.py b/home/models.py index 7e042c834..fe5382162 100644 --- a/home/models.py +++ b/home/models.py @@ -965,131 +965,18 @@ class UserDevice(models.Model): def __str__(self): return f"{self.user.email} - {self.device_name} ({self.ip_address})" -class Resource(models.Model): - class Category(models.TextChoices): - WHITEPAPER = "whitepaper", "Whitepaper" - CHECKLIST = "checklist", "Checklist / Guide" - INFOGRAPH = "infographic", "Infographic" - CASESTUDY = "casestudy", "Case Study" - OTHER = "other", "Other" - -<<<<<<< HEAD -class Resource(models.Model): - class Category(models.TextChoices): - WHITEPAPER = "whitepaper", "Whitepaper" - CHECKLIST = "checklist", "Checklist / Guide" - INFOGRAPH = "infographic", "Infographic" - CASESTUDY = "casestudy", "Case Study" - OTHER = "other", "Other" - -======= ->>>>>>> c1bbe61a (match upstream main models.py) - title = models.CharField(max_length=180) - slug = models.SlugField(max_length=200, unique=True, blank=True) - summary = models.TextField(max_length=600, help_text="Short 1–3 line description.") - category = models.CharField(max_length=20, choices=Category.choices, default=Category.OTHER) - file = models.FileField(upload_to="resources/files/") - cover = models.ImageField(upload_to="resources/covers/", blank=True, null=True) - is_published = models.BooleanField(default=True) - published_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - ordering = ["-published_at"] -<<<<<<< HEAD - - def save(self, *args, **kwargs): - if not self.slug: - self.slug = slugify(self.title)[:190] - return super().save(*args, **kwargs) - - def __str__(self): - return self.title - - -class Tip(models.Model): - text = models.CharField(max_length=280, unique=True) - is_active = models.BooleanField(default=True) - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - verbose_name = "Daily Security Tip" - verbose_name_plural = "Daily Security Tips" - - def __str__(self): - return self.text[:60] - -class TipRotationState(models.Model): - lock = models.CharField(max_length=16, default="default", unique=True) - last_index = models.IntegerField(default=-1) - rotated_at = models.DateTimeField(null=True, blank=True) - - def __str__(self): - return f"{self.lock} @ {self.rotated_at or 'never'} (idx={self.last_index})" - -class UserDevice(models.Model): - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name="devices" - ) - device_fingerprint = models.CharField(max_length=255, null=True, blank=True) - device_name = models.CharField(max_length=200) - user_agent = models.TextField() - ip_address = models.GenericIPAddressField() - created_at = models.DateTimeField(auto_now_add=True) - last_seen = models.DateTimeField(auto_now=True) - - def __str__(self): - return f"{self.user.email} - {self.device_name} ({self.ip_address})" - -class Resource(models.Model): - class Category(models.TextChoices): - WHITEPAPER = "whitepaper", "Whitepaper" - CHECKLIST = "checklist", "Checklist / Guide" - INFOGRAPH = "infographic", "Infographic" - CASESTUDY = "casestudy", "Case Study" - OTHER = "other", "Other" - - title = models.CharField(max_length=180) - slug = models.SlugField(max_length=200, unique=True, blank=True) - summary = models.TextField(max_length=600, help_text="Short 1–3 line description.") - category = models.CharField(max_length=20, choices=Category.choices, default=Category.OTHER) - file = models.FileField(upload_to="resources/files/") - cover = models.ImageField(upload_to="resources/covers/", blank=True, null=True) - is_published = models.BooleanField(default=True) - published_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - ordering = ["-published_at"] - - def save(self, *args, **kwargs): - if not self.slug: - self.slug = slugify(self.title)[:190] - return super().save(*args, **kwargs) - - def __str__(self): - return self.title - - def update_activity(self): - - self.last_activity = now() - self.save(update_fields=['last_activity']) - class UserDeletionRequest(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="deletion_request") requested_at = models.DateTimeField(default=timezone.now) scheduled_for = models.DateTimeField() is_executed = models.BooleanField(default=False) executed_at = models.DateTimeField(null=True, blank=True) -======= ->>>>>>> c1bbe61a (match upstream main models.py) def save(self, *args, **kwargs): - if not self.slug: - self.slug = slugify(self.title)[:190] - return super().save(*args, **kwargs) + if not self.scheduled_for: + self.scheduled_for = self.requested_at + timedelta(days=30) + super().save(*args, **kwargs) def __str__(self): - return self.title + status = "Executed" if self.is_executed else "Pending" + return f"Deletion request for {self.user.username} ({status}), scheduled {self.scheduled_for}" \ No newline at end of file From 0297ab2c4679569afb682f2d4f6a4be4d3cae9bc Mon Sep 17 00:00:00 2001 From: GithiomiAnn Date: Sun, 21 Sep 2025 21:47:13 +1000 Subject: [PATCH 7/9] Deleted multiple migrations --- home/migrations/0002_adninsesssion.py | 30 ------------------- home/migrations/0003_userdeletionrequest.py | 26 ---------------- .../0004_userdeletionrequest_executed_at.py | 18 ----------- 3 files changed, 74 deletions(-) delete mode 100644 home/migrations/0002_adninsesssion.py delete mode 100644 home/migrations/0003_userdeletionrequest.py delete mode 100644 home/migrations/0004_userdeletionrequest_executed_at.py diff --git a/home/migrations/0002_adninsesssion.py b/home/migrations/0002_adninsesssion.py deleted file mode 100644 index 86cbba2c4..000000000 --- a/home/migrations/0002_adninsesssion.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 4.2.14 on 2025-09-01 22:12 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='AdninSesssion', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('session_key', models.CharField(max_length=40, unique=True)), - ('ip_address', models.GenericIPAddressField()), - ('user_agent', models.TextField()), - ('login_time', models.DateTimeField(auto_now_add=True)), - ('last_activity', models.DateTimeField(auto_now=True)), - ('is_active', models.BooleanField(default=True)), - ('logout_time', models.DateTimeField(blank=True, null=True)), - ('logout_reason', models.CharField(blank=True, max_length=50, null=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admin_sessions', to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/home/migrations/0003_userdeletionrequest.py b/home/migrations/0003_userdeletionrequest.py deleted file mode 100644 index d3454ac7c..000000000 --- a/home/migrations/0003_userdeletionrequest.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 4.2.14 on 2025-09-02 05:19 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0002_adninsesssion'), - ] - - operations = [ - migrations.CreateModel( - name='UserDeletionRequest', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('requested_at', models.DateTimeField(default=django.utils.timezone.now)), - ('scheduled_for', models.DateTimeField()), - ('is_executed', models.BooleanField(default=False)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='deletion_request', to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/home/migrations/0004_userdeletionrequest_executed_at.py b/home/migrations/0004_userdeletionrequest_executed_at.py deleted file mode 100644 index a2d9bf8c0..000000000 --- a/home/migrations/0004_userdeletionrequest_executed_at.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.14 on 2025-09-13 23:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0003_userdeletionrequest'), - ] - - operations = [ - migrations.AddField( - model_name='userdeletionrequest', - name='executed_at', - field=models.DateTimeField(blank=True, null=True), - ), - ] From 39779b3defddf26c5b4c6be84b9503a70dffaa40 Mon Sep 17 00:00:00 2001 From: GithiomiAnn Date: Sun, 21 Sep 2025 22:02:24 +1000 Subject: [PATCH 8/9] Fixed models.py --- home/models.py | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/home/models.py b/home/models.py index fe5382162..4c2f12b6c 100644 --- a/home/models.py +++ b/home/models.py @@ -873,31 +873,26 @@ class AdminSession(models.Model): class Meta: ordering = ['-login_time'] - def __str__(self): - admin_type = "Superuser" if self.user.is_superuser else "Staff" - return f"{admin_type} session for {self.user.email} - {self.login_time}" - - def mark_logout(self, reason="manual"): - self.is_active = False - self.logout_time = now() - self.logout_reason = reason - self.save() - - def is_expired(self, timeout_minutes=30): - if not self.is_active: - return True - expiry_time = self.last_activity + timedelta(minutes=timeout_minutes) - return now() > expiry_time - - def is_expired(self, timeout_minutes=30): - if not self.is_active: - return True - expiry_time = self.last_activity + timedelta(minutes=timeout_minutes) - return now() > expiry_time - - def update_activity(self): - self.last_activity = now() - self.save(update_fields=['last_activity']) + def __str__(self): + admin_type = "Superuser" if self.user.is_superuser else "Staff" + return f"{admin_type} session for {self.user.email} - {self.login_time}" + + def mark_logout(self, reason="manual"): + self.is_active = False + self.logout_time = now() + self.logout_reason = reason + self.save() + + def is_expired(self, timeout_minutes=30): + if not self.is_active: + return True + expiry_time = self.last_activity + timedelta(minutes=timeout_minutes) + return now() > expiry_time + + +def update_activity(self): + self.last_activity = now() + self.save(update_fields=['last_activity']) class Resource(models.Model): class Category(models.TextChoices): From 2b344bd561a45c2aa63187037d930d226caf76e7 Mon Sep 17 00:00:00 2001 From: GithiomiAnn Date: Mon, 22 Sep 2025 12:30:13 +1000 Subject: [PATCH 9/9] Fixed migration errors --- home/models.py | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/home/models.py b/home/models.py index 4c2f12b6c..413e244f6 100644 --- a/home/models.py +++ b/home/models.py @@ -887,9 +887,8 @@ def is_expired(self, timeout_minutes=30): if not self.is_active: return True expiry_time = self.last_activity + timedelta(minutes=timeout_minutes) - return now() > expiry_time + return now() > expiry_time - def update_activity(self): self.last_activity = now() self.save(update_fields=['last_activity']) @@ -960,18 +959,31 @@ class UserDevice(models.Model): def __str__(self): return f"{self.user.email} - {self.device_name} ({self.ip_address})" -class UserDeletionRequest(models.Model): - user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="deletion_request") - requested_at = models.DateTimeField(default=timezone.now) - scheduled_for = models.DateTimeField() - is_executed = models.BooleanField(default=False) - executed_at = models.DateTimeField(null=True, blank=True) +class Resource(models.Model): + class Category(models.TextChoices): + WHITEPAPER = "whitepaper", "Whitepaper" + CHECKLIST = "checklist", "Checklist / Guide" + INFOGRAPH = "infographic", "Infographic" + CASESTUDY = "casestudy", "Case Study" + OTHER = "other", "Other" + + title = models.CharField(max_length=180) + slug = models.SlugField(max_length=200, unique=True, blank=True) + summary = models.TextField(max_length=600, help_text="Short 1–3 line description.") + category = models.CharField(max_length=20, choices=Category.choices, default=Category.OTHER) + file = models.FileField(upload_to="resources/files/") + cover = models.ImageField(upload_to="resources/covers/", blank=True, null=True) + is_published = models.BooleanField(default=True) + published_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-published_at"] def save(self, *args, **kwargs): - if not self.scheduled_for: - self.scheduled_for = self.requested_at + timedelta(days=30) - super().save(*args, **kwargs) + if not self.slug: + self.slug = slugify(self.title)[:190] + return super().save(*args, **kwargs) def __str__(self): - status = "Executed" if self.is_executed else "Pending" - return f"Deletion request for {self.user.username} ({status}), scheduled {self.scheduled_for}" \ No newline at end of file + return self.title