diff --git a/photo/migrations/0005_collection_created_at_collection_updated_at_and_more.py b/photo/migrations/0005_collection_created_at_collection_updated_at_and_more.py new file mode 100644 index 0000000..7886fb5 --- /dev/null +++ b/photo/migrations/0005_collection_created_at_collection_updated_at_and_more.py @@ -0,0 +1,67 @@ +# Generated by Django 4.2.8 on 2025-02-03 10:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("photo", "0004_alter_contest_prize"), + ] + + operations = [ + migrations.AddField( + model_name="collection", + name="created_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="collection", + name="updated_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="contest", + name="created_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="contest", + name="updated_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="contestsubmission", + name="created_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="contestsubmission", + name="updated_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="picture", + name="created_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="picture", + name="updated_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="picturecomment", + name="updated_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="user", + name="created_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="user", + name="updated_at", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/photo/models.py b/photo/models.py index 85d4a52..c5bfc42 100644 --- a/photo/models.py +++ b/photo/models.py @@ -48,6 +48,8 @@ def create_superuser(self, email, password=None, **kwargs): class SoftDeleteModel(models.Model): is_deleted = models.BooleanField(default=False) + created_at = models.DateTimeField(null=True, blank=True) + updated_at = models.DateTimeField(null=True, blank=True) objects = SoftDeleteManager() all_objects = models.Manager() @@ -60,6 +62,12 @@ def restore(self): self.is_deleted = False self.save() + def save(self, *args, **kwargs): + if not self.created_at: + self.created_at = timezone.now() + self.updated_at = timezone.now() + super().save(*args, **kwargs) + class Meta: abstract = True @@ -106,6 +114,9 @@ def validate_profile_picture(self): def save(self, *args, **kwargs): self.validate_profile_picture() + if not self.created_at: + self.created_at = timezone.now() + self.updated_at = timezone.now() super(User, self).save(*args, **kwargs) diff --git a/photo/tests/test_database/test_timestamps.py b/photo/tests/test_database/test_timestamps.py new file mode 100644 index 0000000..17a6c3b --- /dev/null +++ b/photo/tests/test_database/test_timestamps.py @@ -0,0 +1,153 @@ +from django.test import TransactionTestCase +from django.utils import timezone +from datetime import timedelta +import time + +from photo.models import Collection, Contest, ContestSubmission, Picture, PictureComment, User +from photo.tests.factories import ( + CollectionFactory, + ContestFactory, + ContestSubmissionFactory, + PictureFactory, + PictureCommentFactory, + UserFactory, +) + + +class TimestampFieldsTest(TransactionTestCase): + def setUp(self): + self.user = UserFactory() + self.picture = PictureFactory(user=self.user) + self.collection = CollectionFactory(user=self.user) + self.contest = ContestFactory(created_by=self.user) + self.submission = ContestSubmissionFactory( + contest=self.contest, + picture=self.picture + ) + self.comment = PictureCommentFactory( + user=self.user, + picture=self.picture + ) + + def test_created_at_on_creation(self): + """Test that created_at is set on object creation""" + now = timezone.now() + + # Test for each model + self.assertIsNotNone(self.user.created_at) + self.assertLess(self.user.created_at, now) + + self.assertIsNotNone(self.picture.created_at) + self.assertLess(self.picture.created_at, now) + + self.assertIsNotNone(self.collection.created_at) + self.assertLess(self.collection.created_at, now) + + self.assertIsNotNone(self.contest.created_at) + self.assertLess(self.contest.created_at, now) + + self.assertIsNotNone(self.submission.created_at) + self.assertLess(self.submission.created_at, now) + + self.assertIsNotNone(self.comment.created_at) + self.assertLess(self.comment.created_at, now) + + def test_updated_at_on_creation(self): + """Test that updated_at is set on object creation""" + now = timezone.now() + + # Test for each model + self.assertIsNotNone(self.user.updated_at) + self.assertLess(self.user.updated_at, now) + + self.assertIsNotNone(self.picture.updated_at) + self.assertLess(self.picture.updated_at, now) + + self.assertIsNotNone(self.collection.updated_at) + self.assertLess(self.collection.updated_at, now) + + self.assertIsNotNone(self.contest.updated_at) + self.assertLess(self.contest.updated_at, now) + + self.assertIsNotNone(self.submission.updated_at) + self.assertLess(self.submission.updated_at, now) + + self.assertIsNotNone(self.comment.updated_at) + self.assertLess(self.comment.updated_at, now) + + def test_updated_at_on_update(self): + """Test that updated_at is updated when object is modified""" + # Store initial timestamps + user_updated = self.user.updated_at + picture_updated = self.picture.updated_at + collection_updated = self.collection.updated_at + contest_updated = self.contest.updated_at + submission_updated = self.submission.updated_at + comment_updated = self.comment.updated_at + + # Wait a bit to ensure timestamps will be different + time.sleep(0.01) # 10 milliseconds + + # Update each object + self.user.name_first = "Updated" + self.user.save() + + self.picture.name = "Updated Picture" + self.picture.save() + + self.collection.name = "Updated Collection" + self.collection.save() + + self.contest.title = "Updated Contest" + self.contest.save() + + self.submission.submission_date = timezone.now() + self.submission.save() + + self.comment.text = "Updated Comment" + self.comment.save() + + # Verify updated_at was changed + self.assertGreater(self.user.updated_at, user_updated) + self.assertGreater(self.picture.updated_at, picture_updated) + self.assertGreater(self.collection.updated_at, collection_updated) + self.assertGreater(self.contest.updated_at, contest_updated) + self.assertGreater(self.submission.updated_at, submission_updated) + self.assertGreater(self.comment.updated_at, comment_updated) + + def test_created_at_unchanged_on_update(self): + """Test that created_at remains unchanged when object is modified""" + # Store initial timestamps + user_created = self.user.created_at + picture_created = self.picture.created_at + collection_created = self.collection.created_at + contest_created = self.contest.created_at + submission_created = self.submission.created_at + comment_created = self.comment.created_at + + # Update each object + self.user.name_first = "Updated" + self.user.save() + + self.picture.name = "Updated Picture" + self.picture.save() + + self.collection.name = "Updated Collection" + self.collection.save() + + self.contest.title = "Updated Contest" + self.contest.save() + + self.submission.submission_date = timezone.now() + self.submission.save() + + self.comment.text = "Updated Comment" + self.comment.save() + + # Verify created_at was not changed + self.assertEqual(self.user.created_at, user_created) + self.assertEqual(self.picture.created_at, picture_created) + self.assertEqual(self.collection.created_at, collection_created) + self.assertEqual(self.contest.created_at, contest_created) + self.assertEqual(self.submission.created_at, submission_created) + self.assertEqual(self.comment.created_at, comment_created) diff --git a/photo/tests/test_queries/graphql_queries.py b/photo/tests/test_queries/graphql_queries.py index 3a1875f..5b8e05d 100644 --- a/photo/tests/test_queries/graphql_queries.py +++ b/photo/tests/test_queries/graphql_queries.py @@ -11,6 +11,8 @@ pictures { id } + created_at + updated_at } } """ @@ -55,6 +57,8 @@ id } status + created_at + updated_at } } """ @@ -131,6 +135,8 @@ votes { email } + created_at + updated_at } } """ @@ -169,6 +175,8 @@ likes { email } + created_at + updated_at } } """ @@ -200,6 +208,7 @@ } text created_at + updated_at } } """ diff --git a/photo/tests/test_queries/test_picture_comment.py b/photo/tests/test_queries/test_picture_comment.py index 12ed20c..9336f3e 100644 --- a/photo/tests/test_queries/test_picture_comment.py +++ b/photo/tests/test_queries/test_picture_comment.py @@ -22,13 +22,9 @@ def test_query_success(self): self.assertEqual(result.errors, None) self.assertEqual(len(result.data["picture_comments"]), self.batch_size) - self.assertEqual( - sorted( - [key for key in result.data["picture_comments"][0].keys()] - + ["is_deleted"] - ), - sorted([field.name for field in PictureComment._meta.fields]), - ) + expected_fields = sorted([field.name for field in PictureComment._meta.fields]) + actual_fields = sorted([key for key in result.data["picture_comments"][0].keys()] + ["is_deleted"]) + self.assertEqual(actual_fields, expected_fields) def test_filter_by_id(self): picture_comment = PictureCommentFactory.create() diff --git a/photo/types.py b/photo/types.py index e63affc..9f83605 100644 --- a/photo/types.py +++ b/photo/types.py @@ -23,6 +23,8 @@ class UserType: profile_picture: "PictureType" profile_picture_updated_at: strawberry.auto user_handle: str + created_at: strawberry.auto + updated_at: strawberry.auto @strawberry.django.type(Picture) @@ -32,6 +34,8 @@ class PictureType: name: str file: str likes: List[UserType] + created_at: strawberry.auto + updated_at: strawberry.auto @strawberry.django.type(PictureComment) @@ -41,6 +45,7 @@ class PictureCommentType: picture: "PictureType" text: str created_at: strawberry.auto + updated_at: strawberry.auto @strawberry.django.type(Collection) @@ -49,6 +54,8 @@ class CollectionType: name: str user: "UserType" pictures: List[PictureType] + created_at: strawberry.auto + updated_at: strawberry.auto @strawberry.django.type(Contest) @@ -67,6 +74,8 @@ class ContestType: winners: List[UserType] created_by: "UserType" status: str + created_at: strawberry.auto + updated_at: strawberry.auto @strawberry.field def status(self) -> str: @@ -92,6 +101,8 @@ class ContestSubmissionType: picture: PictureType submission_date: strawberry.auto votes: List[UserType] + created_at: strawberry.auto + updated_at: strawberry.auto @strawberry.type