Skip to content

Commit

Permalink
Add json model snapshots + tests (#32)
Browse files Browse the repository at this point in the history
* add json model snapshots + tests

* remove python 3.11
  • Loading branch information
ckcollab authored Oct 31, 2022
1 parent 4a6e1b7 commit 82fe663
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, 3.10.x]
# TODO: add 3.11.x when it's ready! Last checked Oct 31, 2022
python-version: [3.9, 3.10.x]
django-version: ['<4', '>=4']

steps:
Expand Down
33 changes: 33 additions & 0 deletions ckc/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.db import models
from django.utils.timezone import now


class SoftDeleteQuerySet(models.QuerySet):
Expand All @@ -23,3 +24,35 @@ class Meta:
def delete(self, *args, **kwargs):
self.deleted = True
self.save()


class JsonSnapshotModel(models.Model):
"""This mixin is meant to be inherited by a model class. It creates a snapshot field for the class that it is a part of.
This field is used to solidify data at a given point in time.
The create_json_snapshot() method must be overridden in the class inheriting this mixin. Inside this method you will build
a custom JSON object of your model state. Include the fields you wish to be solidified.
Lastly, call take_snapshot() at the point in your code you want data to be saved. The time and date this occurs will
also be saved in a separate field called snapshot_date.
"""
snapshot = models.JSONField(null=True, blank=True, default=dict)
snapshot_date = models.DateTimeField(null=True, blank=True)

class Meta:
abstract = True

def _create_json_snapshot(self) -> dict:
"""Override this method to take a "snapshot" of the relevant data on this model"""
raise NotImplementedError

def take_snapshot(self, force=False):
if not force:
assert not self.snapshot, "Can not override an existing snapshot instance."

self.snapshot = self._create_json_snapshot()

# TODO: Do we want to test these edge cases?
# assert self.snapshot is not None, ""

self.snapshot_date = now()
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ name = django-ckc
author = Eric Carmichael
author_email = [email protected]
description = tools, utilities, etc. we use across projects @ ckc
version = 0.0.8
version = 0.0.9
url = https://github.com/ckcollab/django-ckc
keywords =
django
Expand Down
31 changes: 30 additions & 1 deletion testproject/testapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,29 @@
from django.contrib.gis.db.models import PointField
from django.db import models

from ckc.models import SoftDeletableModel
from ckc.models import SoftDeletableModel, JsonSnapshotModel


User = get_user_model()


# ----------------------------------------------------------------------------
# Testing soft deletable model
# ----------------------------------------------------------------------------
class AModel(SoftDeletableModel):
title = models.CharField(max_length=255, default="I'm a test!")


# ----------------------------------------------------------------------------
# PrimaryKeyWriteSerializerReadField related model
# ----------------------------------------------------------------------------
class BModel(models.Model):
a = models.ForeignKey(AModel, on_delete=models.CASCADE)


# ----------------------------------------------------------------------------
# DefaultCreatedByMixin models
# ----------------------------------------------------------------------------
class ModelWithACreator(models.Model):
created_by = models.ForeignKey(User, on_delete=models.CASCADE)

Expand All @@ -24,5 +33,25 @@ class ModelWithADifferentNamedCreator(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE)


# ----------------------------------------------------------------------------
# For testing geo points in factories
# ----------------------------------------------------------------------------
class Location(models.Model):
geo_point = PointField()


# ----------------------------------------------------------------------------
# For testing JSON snapshots
# ----------------------------------------------------------------------------
class SnapshottedModel(JsonSnapshotModel, models.Model):

def _create_json_snapshot(self) -> dict:
return {
"test": "snapshot"
}


class SnapshottedModelMissingOverride(JsonSnapshotModel, models.Model):
# No _create_json_snapshot here! This is for testing purposes, to confirm we raise
# an assertion when this method is missing
pass
42 changes: 42 additions & 0 deletions tests/integration/test_json_model_snapshots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import pytest
from rest_framework.test import APITestCase

from testapp.models import SnapshottedModel, SnapshottedModelMissingOverride


class TestJsonSnapshottedModels(APITestCase):
def test_snapshot_model_asserts_method_must_be_implemented_if_it_is_missing(self):
instance = SnapshottedModelMissingOverride()

# make sure proper error raised when we try to snapshot with a missing method
with pytest.raises(NotImplementedError):
instance.take_snapshot()

def test_snapshot_model_save_actually_saves_to_databse(self):
instance = SnapshottedModel()
instance.take_snapshot()

# Snapshot was written to model...
assert instance.snapshot == {"test": "snapshot"}

instance.save()

# Snapshot was saved to database, forreal
assert SnapshottedModel.objects.get(snapshot__test="snapshot")

def test_snapshotting_an_already_snapshotted_model_raises_exception_unless_forced(self):
instance = SnapshottedModel()
instance.take_snapshot()

# trying to snapshot already snapshotted model -> shit the bed
with pytest.raises(Exception):
instance.take_snapshot()

# Clear snapshot to see if we can re-set it..
instance.snapshot = None

# No exception raised here! And data was properly written
instance.take_snapshot(force=True)

# We were able to re-snapshot
assert instance.snapshot == {"test": "snapshot"}

0 comments on commit 82fe663

Please sign in to comment.