Skip to content

Commit

Permalink
added basic "auto" command, can be ran on cron. Next going to add Pub…
Browse files Browse the repository at this point in the history
…lic API version

Signed-off-by: Trey <[email protected]>
  • Loading branch information
TreyWW committed Oct 2, 2024
1 parent d10d9ce commit 478fe05
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 33 deletions.
13 changes: 13 additions & 0 deletions backend/management/commands/auto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import uuid
from django.core.management.base import BaseCommand
from backend.service.maintenance.expire.run import expire_and_cleanup_objects


class Command(BaseCommand):
"""
Runs automation scripts to make sure objects are up to date, expired objects are deleted, etc.
"""

def handle(self, *args, **kwargs):
self.stdout.write("Running expire + cleanup script...")
self.stdout.write(expire_and_cleanup_objects())
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generated by Django 5.1.1 on 2024-10-02 20:00

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("backend", "0064_remove_invoice_payment_status_invoice_status"),
]

operations = [
migrations.RemoveField(
model_name="invoiceurl",
name="never_expire",
),
migrations.AddField(
model_name="passwordsecret",
name="active",
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name="invoice",
name="status",
field=models.CharField(choices=[("draft", "Draft"), ("pending", "Pending"), ("paid", "Paid")], default="draft", max_length=10),
),
migrations.AlterField(
model_name="invoiceurl",
name="expires",
field=models.DateTimeField(blank=True, help_text="When the item will expire", null=True, verbose_name="Expires"),
),
migrations.AlterField(
model_name="passwordsecret",
name="expires",
field=models.DateTimeField(blank=True, help_text="When the item will expire", null=True, verbose_name="Expires"),
),
migrations.AlterField(
model_name="teaminvitation",
name="expires",
field=models.DateTimeField(blank=True, help_text="When the item will expire", null=True, verbose_name="Expires"),
),
]
94 changes: 61 additions & 33 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,59 @@ def add_3hrs_from_now():
return timezone.now() + timezone.timedelta(hours=3)


class ActiveManager(models.Manager):
"""Manager to return only active objects."""

def get_queryset(self):
return super().get_queryset().filter(active=True)


class ExpiredManager(models.Manager):
"""Manager to return only expired (inactive) objects."""

def get_queryset(self):
now = timezone.now()
return super().get_queryset().filter(expires__isnull=False, expires__lte=now)


class ExpiresBase(models.Model):
"""Base model for handling expiration logic."""

expires = models.DateTimeField("Expires", null=True, blank=True, help_text="When the item will expire")
active = models.BooleanField(default=True)

# Default manager that returns only active items
objects = ActiveManager()

# Custom manager to get expired/inactive objects
expired_objects = ExpiredManager()

# Fallback All objects
all_objects = models.Manager()

def deactivate(self) -> None:
"""Manually deactivate the object."""
self.active = False
self.save()

def delete_if_expired_for(self, days: int = 14) -> bool:
"""Delete the object if it has been expired for a certain number of days."""
if self.expires and self.expires <= timezone.now() - timedelta(days=days):
self.delete()
return True
return False

@property
def remaining_active_time(self):
"""Return the remaining time until expiration, or None if already expired or no expiration set."""
if self.expires and self.expires > timezone.now():
return self.expires - timezone.now()
return None

class Meta:
abstract = True


class VerificationCodes(models.Model):
class ServiceTypes(models.TextChoices):
CREATE_ACCOUNT = "create_account", "Create Account"
Expand Down Expand Up @@ -226,29 +279,22 @@ class Meta:
unique_together = ("team", "user")


class TeamInvitation(models.Model):
class TeamInvitation(ExpiresBase):
code = models.CharField(max_length=10)
team = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name="team_invitations")
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="team_invitations")
invited_by = models.ForeignKey(User, on_delete=models.CASCADE)
expires = models.DateTimeField(null=True, blank=True)
active = models.BooleanField(default=True)

def is_active(self):
if not self.active:
return False
if timezone.now() > self.expires:
self.active = False
self.save()
return False
return True
return self.active

def set_expires(self):
self.expires = timezone.now() + timezone.timedelta(days=7)
self.expires = timezone.now() + timezone.timedelta(days=14)

def save(self, *args, **kwargs):
self.set_expires()
self.code = RandomCode(10)
if not self.code:
self.code = RandomCode(10)
self.set_expires()
super().save()

def __str__(self):
Expand Down Expand Up @@ -733,15 +779,12 @@ def next_invoice_due_date(self, account_defaults: "DefaultValues", from_date: da
return from_date + timedelta(days=7)


class InvoiceURL(models.Model):
class InvoiceURL(ExpiresBase):
uuid = ShortUUIDField(length=8, primary_key=True)
invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name="invoice_urls")
created_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
system_created = models.BooleanField(default=False)
created_on = models.DateTimeField(auto_now_add=True)
expires = models.DateTimeField(null=True, blank=True)
never_expire = models.BooleanField(default=False)
active = models.BooleanField(default=True)

@property
def get_created_by(self):
Expand All @@ -750,23 +793,9 @@ def get_created_by(self):
else:
return "SYSTEM"

def is_active(self):
if not self.active:
return False
if timezone.now() > self.expires:
self.active = False
self.save()
return False
return True

def set_expires(self):
self.expires = timezone.now() + timezone.timedelta(days=7)

def save(self, *args, **kwargs):
if not self.never_expire:
self.set_expires()
super().save()

def __str__(self):
return str(self.invoice.id)

Expand Down Expand Up @@ -857,10 +886,9 @@ def hash(self):
self.save()


class PasswordSecret(models.Model):
class PasswordSecret(ExpiresBase):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="password_secrets")
secret = models.TextField(max_length=300)
expires = models.DateTimeField(null=True, blank=True)


class Notification(models.Model):
Expand Down
39 changes: 39 additions & 0 deletions backend/service/maintenance/expire/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from datetime import timedelta
from typing import Type

from django.db import models
from django.db.models import QuerySet

from backend.models import TeamInvitation, InvoiceURL, PasswordSecret

from django.utils import timezone

"""
Every model MUST have the field "expires" as:
expires = models.DateTimeField(null=True, blank=True)
"""


def expire_and_cleanup_objects() -> str:
deactivated_items: int = 0
deleted_items: int = 0

model_list: list[Type[models.Model]] = [TeamInvitation, InvoiceURL, PasswordSecret]

now = timezone.now()

for model in model_list:
# Delete objects that have been inactive and expired for more than 14 days
over_14_days_expired: QuerySet[models.Model] = model.all_objects.filter(expires__lte=now - timedelta(days=14))
deleted_items += over_14_days_expired.count()
over_14_days_expired.delete()

# Deactivate expired items that got missed
to_deactivate: QuerySet[models.Model] = model.all_objects.filter(expires__lte=now, active=True)

deactivated_items += to_deactivate.count()
to_deactivate.update(active=False)

return f"Deactivated {deactivated_items} objects and deleted {deleted_items} objects."

0 comments on commit 478fe05

Please sign in to comment.