Skip to content
Draft
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
23 changes: 23 additions & 0 deletions photo/migrations/0005_picture_created_at_picture_updated_at.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.8 on 2025-01-24 10:48

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):
dependencies = [
("photo", "0004_alter_contest_prize"),
]

operations = [
migrations.AddField(
model_name="picture",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name="picture",
name="updated_at",
field=models.DateTimeField(auto_now=True),
),
]
321 changes: 12 additions & 309 deletions photo/models.py
Original file line number Diff line number Diff line change
@@ -1,316 +1,19 @@
import uuid
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils import timezone

class User(AbstractUser):
created_at = models.DateTimeField(auto_now_add=True, null=True)
updated_at = models.DateTimeField(auto_now=True, null=True)
from django.contrib.auth.models import AbstractUser, BaseUserManager
from django.db import models, transaction
from django.db.models import Count, Max
from django.forms import ValidationError
from django.utils import timezone

from photo.fixtures import (
CANT_VOTE_SUBMISSION,
CONTEST_CLOSED,
OUTDATED_SUBMISSION_ERROR_MESSAGE,
REPEATED_VOTE_ERROR_MESSAGE,
UNIQUE_SUBMISSION_ERROR_MESSAGE,
VALID_USER_ERROR_MESSAGE,
VOTE_UPLOAD_PHASE_NOT_OVER,
VOTING_DRAW_PHASE_OVER,
VOTING_PHASE_OVER,
VOTING_SELF,
)
from photo.manager import SoftDeleteManager
from photo.storages_backend import PublicMediaStorage, picture_path
from utils.enums import ContestInternalStates


class UserManager(BaseUserManager):
def create_user(self, email, password=None, **kwargs):
if not email:
raise ValueError("Email not provided")
email = self.normalize_email(email)
user = self.model(email=email, **kwargs)
user.set_password(password)
user.save()
return user

def create_superuser(self, email, password=None, **kwargs):
kwargs.setdefault("is_active", True)
kwargs.setdefault("is_staff", True)
kwargs.setdefault("is_superuser", True)
if kwargs.get("is_active") is not True:
raise ValueError("Superuser should be active")
if kwargs.get("is_staff") is not True:
raise ValueError("Superuser should be staff")
if kwargs.get("is_superuser") is not True:
raise ValueError("Superuser should have is_superuser=True")
return self.create_user(email, password, **kwargs)


class SoftDeleteModel(models.Model):
is_deleted = models.BooleanField(default=False)
objects = SoftDeleteManager()
all_objects = models.Manager()

@transaction.atomic
def delete(self):
self.is_deleted = True
self.save()

def restore(self):
self.is_deleted = False
self.save()

class Meta:
abstract = True


class User(AbstractUser, SoftDeleteModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
email = models.TextField(unique=True)
username = models.CharField("username", max_length=150, null=True)
name_first = models.TextField(blank=True, null=True)
name_last = models.TextField(blank=True, null=True)
profile_picture = models.ForeignKey(
"Picture",
on_delete=models.SET_NULL,
related_name="user_picture",
blank=True,
null=True,
)
profile_picture_updated_at = models.DateTimeField(blank=True, null=True)
user_handle = models.TextField(unique=True, null=True)

USERNAME_FIELD = "email"
EMAIL_FIELD = "email"
REQUIRED_FIELDS = ["first_name", "last_name"]
objects = UserManager()

class Meta:
constraints = [
models.UniqueConstraint(
fields=["email"],
condition=models.Q(is_deleted="False"),
name="user_email",
)
]

def validate_profile_picture(self):
if not self._state.adding:
old_picture = User.objects.filter(email=self.email).first().profile_picture
if old_picture and self.profile_picture.id != old_picture.id:
self.profile_picture_updated_at = timezone.now()
if self.profile_picture and self.profile_picture.user.email != self.email:
raise ValidationError(
"The user's profile picture must be owned by the same user."
)

def save(self, *args, **kwargs):
self.validate_profile_picture()
super(User, self).save(*args, **kwargs)


class Picture(SoftDeleteModel):
user = models.ForeignKey(
"User", on_delete=models.CASCADE, related_name="picture_user"
)
name = models.TextField(blank=True, null=True)
file = models.ImageField(
storage=PublicMediaStorage(),
upload_to=picture_path,
)
likes = models.ManyToManyField(User, related_name="picture_likes", blank=True)

def __str__(self):
return self.name

def like_picture(self, user):
if user not in self.likes.filter(id=user):
self.likes.add(user)
self.save()
return self


class PictureComment(SoftDeleteModel):
user = models.ForeignKey("User", on_delete=models.CASCADE)
picture = models.ForeignKey(
"Picture",
on_delete=models.CASCADE,
)
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)


class Collection(SoftDeleteModel):
name = models.TextField()
user = models.ForeignKey("User", on_delete=models.CASCADE)
pictures = models.ManyToManyField(
Picture, related_name="collection_pictures", blank=True
)

class Meta:
constraints = [
models.UniqueConstraint(fields=["name", "user"], name="collection_pk")
]

def add_picture(self, picture):
if picture not in self.pictures.filter(id=picture):
self.pictures.add(picture)
self.save()
return self


class Contest(SoftDeleteModel):
title = models.TextField()
description = models.TextField()
cover_picture = models.ForeignKey(
"Picture",
on_delete=models.SET_NULL,
blank=True,
null=True,
)
prize = models.TextField(null=True, blank=True)
automated_dates = models.BooleanField(default=True)
upload_phase_start = models.DateTimeField(default=timezone.now)
upload_phase_end = models.DateTimeField(null=True, blank=True)
voting_phase_end = models.DateTimeField(null=True, blank=True)
voting_draw_end = models.DateTimeField(null=True, blank=True)
internal_status = models.TextField(
choices=ContestInternalStates.choices, default=ContestInternalStates.OPEN
)
winners = models.ManyToManyField(User, related_name="contest_winners", blank=True)
created_by = models.ForeignKey(
"User",
on_delete=models.SET_NULL,
related_name="contest_created_by",
blank=True,
null=True,
)

def __str__(self):
return self.title

def validate_user(self):
if not (
self.created_by
and User.objects.filter(email=self.created_by.email).exists()
):
raise ValidationError(VALID_USER_ERROR_MESSAGE)

def reset_votes(self):
for submission in ContestSubmission.objects.filter(contest=self):
submission.votes.clear()

def close_contest(self):
self.voting_phase_end = timezone.now()
max_votes = ContestSubmission.objects.annotate(
num_votes=Count("votes")
).aggregate(max_votes=Max("num_votes"))["max_votes"]
submissions_with_highest_votes = ContestSubmission.objects.annotate(
num_votes=Count("votes")
).filter(num_votes=max_votes, contest=self)

if self.internal_status == ContestInternalStates.DRAW:
self.winners.clear()
for submission in submissions_with_highest_votes:
self.winners.add(submission.picture.user)

if self.winners.count() > 1:
self.internal_status = ContestInternalStates.DRAW
self.reset_votes()
elif self.winners.count() == 0:
self.internal_status = ContestInternalStates.DRAW
all_submissions = ContestSubmission.objects.filter(contest=self)
for submission in all_submissions:
self.winners.add(submission.picture.user)
self.reset_votes()
else:
self.internal_status = ContestInternalStates.CLOSED
self.save()
return self

def save(self, *args, **kwargs):
if self._state.adding:
self.validate_user()
super(Contest, self).save(*args, **kwargs)


class ContestSubmission(SoftDeleteModel):
contest = models.ForeignKey(
"Contest",
on_delete=models.CASCADE,
)
picture = models.ForeignKey(
"Picture",
on_delete=models.CASCADE,
)
submission_date = models.DateTimeField(auto_now_add=True)
votes = models.ManyToManyField(User, related_name="submission_votes", blank=True)

def validate_unique(self, *args, **kwargs):
qs = ContestSubmission.objects.filter(
contest=self.contest, picture__user=self.picture.user
)

if qs.exists() and self._state.adding:
raise ValidationError(UNIQUE_SUBMISSION_ERROR_MESSAGE)

def validate_vote(self):
user_vote = ContestSubmission.objects.filter(
contest=self.contest, votes=self.picture.user
)

if user_vote.exists() and self._state.adding:
raise ValidationError(REPEATED_VOTE_ERROR_MESSAGE)

def validate_submission_date(self):
submission_date = (
self.submission_date if self.submission_date else timezone.now()
)
if self.contest.upload_phase_end is not None and (
not (
self.contest.upload_phase_start
<= submission_date
<= self.contest.upload_phase_end
)
):
raise ValidationError(OUTDATED_SUBMISSION_ERROR_MESSAGE)

def save(self, *args, **kwargs):
self.validate_unique()
if self._state.adding:
self.validate_submission_date()
super(ContestSubmission, self).save(*args, **kwargs)

def add_vote(self, user):
contest_submissions = ContestSubmission.objects.filter(contest=self.contest)
user_vote = User.objects.filter(id=user).first()

if self.picture.user.id == user_vote.id:
raise ValidationError(VOTING_SELF)

if self.contest.internal_status == ContestInternalStates.CLOSED:
raise ValidationError(CONTEST_CLOSED)

if self.contest.internal_status == ContestInternalStates.DRAW:
if self.contest.voting_draw_end < timezone.now():
raise ValidationError(VOTING_DRAW_PHASE_OVER)
if self.picture.user not in self.contest.winners.all():
raise ValidationError(CANT_VOTE_SUBMISSION)
else:
if (
self.contest.upload_phase_end
and self.contest.upload_phase_end > timezone.now()
):
raise ValidationError(VOTE_UPLOAD_PHASE_NOT_OVER)
if (
self.contest.voting_phase_end
and self.contest.voting_phase_end < timezone.now()
):
raise ValidationError(VOTING_PHASE_OVER)

for sub in contest_submissions:
if user_vote in sub.votes.all():
sub.votes.remove(user_vote)
self.votes.add(user)
self.save()
return self
class Photo(models.Model):
created_at = models.DateTimeField(auto_now_add=True, null=True)
updated_at = models.DateTimeField(auto_now=True, null=True)
title = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
image = models.ImageField(upload_to='photos/')
22 changes: 22 additions & 0 deletions photo/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.test import TestCase, Client
from django.core.files.uploadedfile import SimpleUploadedFile
from photo.models import Picture, User

class PhotoTests(TestCase):
def setUp(self):
self.user = User.objects.create(email="testuser@example.com", username="testuser")
self.client = Client()
self.client.force_login(self.user)

def test_picture_creation(self):
picture = Picture.objects.create(
user=self.user, name="Test Picture", file=SimpleUploadedFile("test.jpg", b"file_content")
)
self.assertEqual(picture.name, "Test Picture")
self.assertEqual(picture.user, self.user)
def test_picture_model_has_timestamps(self):
picture = Picture.objects.create(
user=self.user, name="Test Picture", file=SimpleUploadedFile("test.jpg", b"file_content")
)
self.assertIsNotNone(picture.created_at)
self.assertIsNotNone(picture.updated_at)