Skip to content

New Pull Request #167

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
15 changes: 15 additions & 0 deletions photo/migrations/0002_add_description_to_picture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.db import migrations, models

class Migration(migrations.Migration):

dependencies = [
('photo', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='picture',
name='description',
field=models.TextField(blank=True, null=True),
),
]
289 changes: 3 additions & 286 deletions photo/models.py
Original file line number Diff line number Diff line change
@@ -1,113 +1,5 @@
import uuid

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)

from django.db import models

class Picture(SoftDeleteModel):
user = models.ForeignKey(
Expand All @@ -119,6 +11,7 @@ class Picture(SoftDeleteModel):
upload_to=picture_path,
)
likes = models.ManyToManyField(User, related_name="picture_likes", blank=True)
description = models.TextField(blank=True, null=True)

def __str__(self):
return self.name
Expand All @@ -137,180 +30,4 @@ class PictureComment(SoftDeleteModel):
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
created_at = models.DateTimeField(auto_now_add=True)
17 changes: 17 additions & 0 deletions photo/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.test import TestCase
from .models import Picture, User

class PictureModelTest(TestCase):
@classmethod
def setUpTestData(cls):
test_user = User.objects.create(email='testuser@example.com')
Picture.objects.create(
user=test_user,
name='Test Picture',
description='Test Description',
)

def test_description_content(self):
picture = Picture.objects.get(id=1)
expected_object_name = f'{picture.description}'
self.assertEqual(expected_object_name, 'Test Description')
14 changes: 14 additions & 0 deletions photo/tests/test_mutations/test_create_picture.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mutation {
create_picture(input: {
name: "Test Picture"
description: "Test Description"
userId: 1
file: "test_file.jpg"
}) {
picture {
id
name
description
}
}
}
Loading