Skip to content

Commit 82fe663

Browse files
authored
Add json model snapshots + tests (#32)
* add json model snapshots + tests * remove python 3.11
1 parent 4a6e1b7 commit 82fe663

File tree

5 files changed

+108
-3
lines changed

5 files changed

+108
-3
lines changed

.github/workflows/pythonpackage.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ jobs:
1010
runs-on: ubuntu-latest
1111
strategy:
1212
matrix:
13-
python-version: [3.8, 3.9, 3.10.x]
13+
# TODO: add 3.11.x when it's ready! Last checked Oct 31, 2022
14+
python-version: [3.9, 3.10.x]
1415
django-version: ['<4', '>=4']
1516

1617
steps:

ckc/models.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.db import models
2+
from django.utils.timezone import now
23

34

45
class SoftDeleteQuerySet(models.QuerySet):
@@ -23,3 +24,35 @@ class Meta:
2324
def delete(self, *args, **kwargs):
2425
self.deleted = True
2526
self.save()
27+
28+
29+
class JsonSnapshotModel(models.Model):
30+
"""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.
31+
This field is used to solidify data at a given point in time.
32+
33+
The create_json_snapshot() method must be overridden in the class inheriting this mixin. Inside this method you will build
34+
a custom JSON object of your model state. Include the fields you wish to be solidified.
35+
36+
Lastly, call take_snapshot() at the point in your code you want data to be saved. The time and date this occurs will
37+
also be saved in a separate field called snapshot_date.
38+
"""
39+
snapshot = models.JSONField(null=True, blank=True, default=dict)
40+
snapshot_date = models.DateTimeField(null=True, blank=True)
41+
42+
class Meta:
43+
abstract = True
44+
45+
def _create_json_snapshot(self) -> dict:
46+
"""Override this method to take a "snapshot" of the relevant data on this model"""
47+
raise NotImplementedError
48+
49+
def take_snapshot(self, force=False):
50+
if not force:
51+
assert not self.snapshot, "Can not override an existing snapshot instance."
52+
53+
self.snapshot = self._create_json_snapshot()
54+
55+
# TODO: Do we want to test these edge cases?
56+
# assert self.snapshot is not None, ""
57+
58+
self.snapshot_date = now()

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ name = django-ckc
3737
author = Eric Carmichael
3838
author_email = [email protected]
3939
description = tools, utilities, etc. we use across projects @ ckc
40-
version = 0.0.8
40+
version = 0.0.9
4141
url = https://github.com/ckcollab/django-ckc
4242
keywords =
4343
django

testproject/testapp/models.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,29 @@
22
from django.contrib.gis.db.models import PointField
33
from django.db import models
44

5-
from ckc.models import SoftDeletableModel
5+
from ckc.models import SoftDeletableModel, JsonSnapshotModel
66

77

88
User = get_user_model()
99

1010

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

1417

18+
# ----------------------------------------------------------------------------
19+
# PrimaryKeyWriteSerializerReadField related model
20+
# ----------------------------------------------------------------------------
1521
class BModel(models.Model):
1622
a = models.ForeignKey(AModel, on_delete=models.CASCADE)
1723

1824

25+
# ----------------------------------------------------------------------------
26+
# DefaultCreatedByMixin models
27+
# ----------------------------------------------------------------------------
1928
class ModelWithACreator(models.Model):
2029
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
2130

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

2635

36+
# ----------------------------------------------------------------------------
37+
# For testing geo points in factories
38+
# ----------------------------------------------------------------------------
2739
class Location(models.Model):
2840
geo_point = PointField()
41+
42+
43+
# ----------------------------------------------------------------------------
44+
# For testing JSON snapshots
45+
# ----------------------------------------------------------------------------
46+
class SnapshottedModel(JsonSnapshotModel, models.Model):
47+
48+
def _create_json_snapshot(self) -> dict:
49+
return {
50+
"test": "snapshot"
51+
}
52+
53+
54+
class SnapshottedModelMissingOverride(JsonSnapshotModel, models.Model):
55+
# No _create_json_snapshot here! This is for testing purposes, to confirm we raise
56+
# an assertion when this method is missing
57+
pass
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import pytest
2+
from rest_framework.test import APITestCase
3+
4+
from testapp.models import SnapshottedModel, SnapshottedModelMissingOverride
5+
6+
7+
class TestJsonSnapshottedModels(APITestCase):
8+
def test_snapshot_model_asserts_method_must_be_implemented_if_it_is_missing(self):
9+
instance = SnapshottedModelMissingOverride()
10+
11+
# make sure proper error raised when we try to snapshot with a missing method
12+
with pytest.raises(NotImplementedError):
13+
instance.take_snapshot()
14+
15+
def test_snapshot_model_save_actually_saves_to_databse(self):
16+
instance = SnapshottedModel()
17+
instance.take_snapshot()
18+
19+
# Snapshot was written to model...
20+
assert instance.snapshot == {"test": "snapshot"}
21+
22+
instance.save()
23+
24+
# Snapshot was saved to database, forreal
25+
assert SnapshottedModel.objects.get(snapshot__test="snapshot")
26+
27+
def test_snapshotting_an_already_snapshotted_model_raises_exception_unless_forced(self):
28+
instance = SnapshottedModel()
29+
instance.take_snapshot()
30+
31+
# trying to snapshot already snapshotted model -> shit the bed
32+
with pytest.raises(Exception):
33+
instance.take_snapshot()
34+
35+
# Clear snapshot to see if we can re-set it..
36+
instance.snapshot = None
37+
38+
# No exception raised here! And data was properly written
39+
instance.take_snapshot(force=True)
40+
41+
# We were able to re-snapshot
42+
assert instance.snapshot == {"test": "snapshot"}

0 commit comments

Comments
 (0)