Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SoftDeleteModel, ActiveUndeleteManager; tests for same #130

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!--
A new scriv changelog fragment.

Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Removed

- A bullet item for the Removed category.

-->

### Added

- Adds the SoftDeleteModel
- Adds ActiveUndeleteManager to support models that use SoftDeleteModel

<!--
### Changed

- A bullet item for the Changed category.

-->
<!--
### Deprecated

- A bullet item for the Deprecated category.

-->
<!--
### Fixed

- A bullet item for the Fixed category.

-->
<!--
### Security

- A bullet item for the Security category.

-->
26 changes: 26 additions & 0 deletions src/mitol/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
from django.db import transaction
from django.db.models import (
PROTECT,
BooleanField,
DateTimeField,
ForeignKey,
JSONField,
Manager,
Model,
prefetch_related_objects,
)
Expand Down Expand Up @@ -47,6 +49,22 @@ class Meta:
abstract = True


class SoftDeleteModel(Model):
"""Model that blocks delete and instead marks the items is_active flag as false"""

is_active = BooleanField(default=True)

def delete(self):
"""Mark items inactive instead of deleting them"""
self.is_active = False
self.save(update_fields=["is_active"])

class Meta:
"""Meta options for SoftDeleteTimestampModel"""

abstract = True


class AuditModel(TimestampedModel):
"""An abstract base class for audit models"""

Expand Down Expand Up @@ -235,3 +253,11 @@ def _clone(self):
self._prefetch_generic_related_lookups
)
return c


class ActiveUndeleteManager(Manager):
"""Query manager for models with soft-delete that excludes inactive records"""

def get_queryset(self):
"""Get the active queryset for manager"""
return QuerySet(self.model, using=self._db).filter(is_active=True)
32 changes: 32 additions & 0 deletions tests/mitol/common/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
Root,
SecondLevel1,
SecondLevel2,
TestSoftDelete,
TestSoftDeleteTimestamped,
Updateable,
)

Expand Down Expand Up @@ -102,3 +104,33 @@ def test_auditable_model():
# auditable_instance.status = FinancialAidStatus.AUTO_APPROVED
auditable_instance.save_and_log(user)
assert AuditableTestModelAudit.objects.count() == 1


def test_soft_delete_model():
"""Verify that soft delete models work"""

soft_delete_record = TestSoftDelete.objects.create(test_data="Test Deletion")

assert TestSoftDelete.objects.count() == 1

soft_delete_record.delete()

assert TestSoftDelete.objects.count() == 0
assert TestSoftDelete.all_objects.count() == 1


def test_soft_delete_model_with_timestamps():
"""Verify that the updated_on timestamp gets updated too when deleting"""

soft_delete_record = TestSoftDeleteTimestamped.objects.create(
test_data="Test Deletion"
)
pk = soft_delete_record.id

assert TestSoftDeleteTimestamped.objects.count() == 1

soft_delete_record.delete()

soft_delete_record = TestSoftDeleteTimestamped.all_objects.get(pk=pk)

assert soft_delete_record.created_on != soft_delete_record.updated_on
123 changes: 123 additions & 0 deletions tests/testapp/migrations/0004_add_soft_delete_models_update_ids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Generated by Django 4.2.8 on 2024-01-08 17:19

import django.db.models.manager
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("testapp", "0003_auditabletestmodelaudit"),
]

operations = [
migrations.CreateModel(
name="TestSoftDelete",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("is_active", models.BooleanField(default=True)),
("test_data", models.CharField(max_length=100)),
],
options={
"abstract": False,
},
managers=[
("all_objects", django.db.models.manager.Manager()),
],
),
migrations.CreateModel(
name="TestSoftDeleteTimestamped",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_on", models.DateTimeField(auto_now_add=True)),
("updated_on", models.DateTimeField(auto_now=True)),
("is_active", models.BooleanField(default=True)),
("test_data", models.CharField(max_length=100)),
],
options={
"abstract": False,
},
managers=[
("all_objects", django.db.models.manager.Manager()),
],
),
migrations.AlterField(
model_name="auditabletestmodel",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="auditabletestmodelaudit",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="democourseware",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="firstlevel1",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="firstlevel2",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="root",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="secondlevel1",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="secondlevel2",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="updateable",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
]
20 changes: 20 additions & 0 deletions tests/testapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
from django.db import models

from mitol.common.models import (
ActiveUndeleteManager,
AuditableModel,
AuditModel,
PrefetchGenericQuerySet,
SoftDeleteModel,
TimestampedModel,
)
from mitol.common.utils.serializers import serialize_model_object
Expand Down Expand Up @@ -85,3 +87,21 @@ class DemoCourseware(models.Model):
description = models.TextField()

learner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)


class TestSoftDelete(SoftDeleteModel):
"""Test model for soft deletion"""

test_data = models.CharField(max_length=100)

all_objects = models.Manager()
objects = ActiveUndeleteManager()


class TestSoftDeleteTimestamped(SoftDeleteModel, TimestampedModel):
"""Test model for soft deletions with timestamps"""

test_data = models.CharField(max_length=100)

all_objects = models.Manager()
objects = ActiveUndeleteManager()