From 9667e60c0e5a9066aaa33cbe852469fff21cc462 Mon Sep 17 00:00:00 2001 From: OmerCS8 Date: Thu, 31 Mar 2022 15:22:09 +0300 Subject: [PATCH] Adding Choice model and votes - Adding Choice model that stores choice options of a poll and votes - Adding tests Adding migrations --- posts/admin.py | 6 +- posts/migrations/0004_poll.py | 27 +++ posts/migrations/0005_pollfile.py | 26 +++ posts/migrations/0006_choice.py | 25 +++ posts/models.py | 53 +++++ posts/tests.py | 308 +++++++++++++++++++++++++++++- 6 files changed, 438 insertions(+), 7 deletions(-) create mode 100644 posts/migrations/0004_poll.py create mode 100644 posts/migrations/0005_pollfile.py create mode 100644 posts/migrations/0006_choice.py diff --git a/posts/admin.py b/posts/admin.py index 07bee8f..e6a9a21 100644 --- a/posts/admin.py +++ b/posts/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin -from .models import Resume -from .models import Rating +from .models import Resume, Rating, Poll, PollFile, Choice admin.site.register(Resume) admin.site.register(Rating) +admin.site.register(Poll) +admin.site.register(PollFile) +admin.site.register(Choice) diff --git a/posts/migrations/0004_poll.py b/posts/migrations/0004_poll.py new file mode 100644 index 0000000..a8e659b --- /dev/null +++ b/posts/migrations/0004_poll.py @@ -0,0 +1,27 @@ +# Generated by Django 4.0.3 on 2022-04-05 15:30 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('posts', '0003_rating'), + ] + + operations = [ + migrations.CreateModel( + name='Poll', + fields=[ + ('post_ptr', models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to='posts.post')), + ], + bases=('posts.post',), + ), + ] diff --git a/posts/migrations/0005_pollfile.py b/posts/migrations/0005_pollfile.py new file mode 100644 index 0000000..216d9c2 --- /dev/null +++ b/posts/migrations/0005_pollfile.py @@ -0,0 +1,26 @@ +# Generated by Django 4.0.3 on 2022-03-31 11:01 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('posts', '0004_poll'), + ] + + operations = [ + migrations.CreateModel( + name='PollFile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField( + default=None, upload_to='files', + validators=[django.core.validators.FileExtensionValidator( + allowed_extensions=['pdf', 'png', 'jpg', 'jpeg'])])), + ('poll', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='posts.poll')), + ], + ), + ] diff --git a/posts/migrations/0006_choice.py b/posts/migrations/0006_choice.py new file mode 100644 index 0000000..226471e --- /dev/null +++ b/posts/migrations/0006_choice.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0.3 on 2022-04-03 09:50 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('posts', '0005_pollfile'), + ] + + operations = [ + migrations.CreateModel( + name='Choice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('choice_text', models.CharField(max_length=200)), + ('poll', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='posts.poll')), + ('voters', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/posts/models.py b/posts/models.py index a2853d8..d01189f 100644 --- a/posts/models.py +++ b/posts/models.py @@ -110,3 +110,56 @@ def __str__(self): def get_rating_average(self): return (self.design_rating + self.skill_relevance_rating + self.grammar_rating + self.conciseness_rating) / 4 + + +# -----------------------------------Poll model inherited from Post----------------------------------- + + +class Poll(Post): + + def __str__(self): + return f"Poll {self.post_id} by {self.author}" + + def get_amount_of_votes(self): + votes_amount = 0 + for choice in self.choice_set.all(): + votes_amount += choice.get_amount_of_votes() + return votes_amount + + +# -----------------------------PollFile model that saves a file of a Poll----------------------------- + + +class PollFile(models.Model): + poll = models.ForeignKey(Poll, on_delete=models.CASCADE) + file = models.FileField( + default=None, + upload_to='files', + validators=[FileExtensionValidator(allowed_extensions=['pdf', 'png', 'jpg', 'jpeg'])] + ) + + def __str__(self): + return f"File {self.pk} of {self.poll}" + + +# ------------------------Choice model that stores one choice option of a Poll------------------------ + + +class Choice(models.Model): + poll = models.ForeignKey(Poll, on_delete=models.CASCADE) + choice_text = models.CharField(max_length=200) + voters = models.ManyToManyField(User, blank=True) + + def __str__(self): + return f"'{self.choice_text}' of {self.poll}" + + def get_amount_of_votes(self): + return len(self.voters.all()) + + # get percentage of voters who voted to this choice + def get_percentage(self): + total_votes_amount = self.poll.get_amount_of_votes() + if total_votes_amount <= 0: + return 0 + else: + return (self.get_amount_of_votes() / total_votes_amount) * 100 diff --git a/posts/tests.py b/posts/tests.py index 3bb5e49..6625eb4 100644 --- a/posts/tests.py +++ b/posts/tests.py @@ -1,7 +1,5 @@ import pytest -from .models import Post -from .models import Resume -from .models import Rating +from .models import Post, Resume, Rating, Poll, PollFile, Choice from django.contrib.auth.models import User from django.core.exceptions import ValidationError @@ -10,17 +8,25 @@ LASTNAME = "User" PASSWORD = "testpass" EMAIL = "testuser@gmail.com" -DESCRIPTION = "this is a test resume" +DESCRIPTION = "this is a test post" FILE1 = "Alon_Shakaroffs_resume.pdf" - +FILE2 = "Olive.png" +CHOICETEXT1 = "First option" +CHOICETEXT2 = "Second option" # -----------------------------------------------fixtures---------------------------------------------------------- + @pytest.fixture def new_user(): return User(username=USERNAME, first_name=FIRSTNAME, last_name=LASTNAME, password=PASSWORD, email=EMAIL) +@pytest.fixture +def new_user2(): + return User(username="user2", first_name=FIRSTNAME, last_name=LASTNAME, password=PASSWORD, email="test2@gmail.com") + + @pytest.fixture def persist_user(new_user): new_user.save() @@ -59,6 +65,44 @@ def persist_rating(new_rating): return new_rating +@pytest.fixture +def new_poll(new_user): + return Poll(author=new_user, description=DESCRIPTION) + + +@pytest.fixture +def persist_poll(new_poll): + new_poll.author.save() + new_poll.save() + return new_poll + + +@pytest.fixture +def new_poll_file(new_poll): + return PollFile(poll=new_poll, file=FILE1) + + +@pytest.fixture +def persist_poll_file(new_poll_file): + new_poll_file.poll.author.save() + new_poll_file.poll.save() + new_poll_file.save() + return new_poll_file + + +@pytest.fixture +def new_choice(new_poll): + return Choice(poll=new_poll, choice_text=CHOICETEXT1) + + +@pytest.fixture +def persist_choice(new_choice): + new_choice.poll.author.save() + new_choice.poll.save() + new_choice.save() + return new_choice + + def new_user_with_name_and_email(user_name, email): user = User(username=user_name, first_name=FIRSTNAME, last_name=LASTNAME, password=PASSWORD, email=email) user.save() @@ -226,3 +270,257 @@ def test_rating_invalid_args( # Check if the Rating average function works properly def test_rating_avg_function(self, persist_rating): assert persist_rating.get_rating_average() == 5 + + +# -------------------------------------------- Poll tests -------------------------------------------- + + +@pytest.mark.django_db() +class TestPoll: + + # Check that the Poll values are the same as the Poll inputs. + def test_new_poll_input_same_as_output(self, new_poll): + assert new_poll.author.username == USERNAME + assert new_poll.description == DESCRIPTION + + # Check if the Poll is saved in the database. + def test_persist_poll(self, persist_poll): + assert persist_poll in Poll.objects.all() + + # Check if the Poll can be referred as post. + def test_inheritance_of_poll_from_post(self, persist_poll): + assert Post.objects.filter(poll=persist_poll).exists() + post = Post.objects.all().first() + assert hasattr(post, 'poll') + assert not hasattr(post, 'resume') + + # Check if Poll deletion delete only Poll from database. + def test_delete_poll(self, persist_poll): + assert persist_poll in Poll.objects.all() + assert persist_poll.author in User.objects.all() + persist_poll.delete() + assert persist_poll not in Poll.objects.all() + assert persist_poll.author in User.objects.all() + + # Check if Poll's author deletion delete both the author and the poll. + def test_delete_polls_author(self, persist_poll): + assert persist_poll in Poll.objects.all() + assert persist_poll.author in User.objects.all() + persist_poll.author.delete() + assert persist_poll not in Poll.objects.all() + assert persist_poll.author not in User.objects.all() + + # verify that it is impossible to create a Poll with invalid parameters. + @pytest.mark.parametrize("user, description, expected_error", [( + USERNAME, + DESCRIPTION, + TypeError), + (new_user, 6, TypeError), (EMAIL, DESCRIPTION, TypeError)]) + def test_poll_invalid_args(self, user, description, expected_error): + with pytest.raises(expected_error): + Poll(user=user, description=description) + + +# -------------------------------------------- PollFile tests -------------------------------------------- + + +@pytest.mark.django_db() +class TestPollFile: + + # Check that the PollFile values are the same as the PollFile inputs. + def test_new_poll_file_input_same_as_output(self, new_poll_file): + assert new_poll_file.poll.author.username == USERNAME + assert new_poll_file.poll.description == DESCRIPTION + assert new_poll_file.file == FILE1 + + # Check if the PollFile is saved in the database and accessible via its Poll. + @pytest.mark.django_db() + def test_persist_poll_file(self, persist_poll_file): + assert persist_poll_file in PollFile.objects.all() + assert persist_poll_file in persist_poll_file.poll.pollfile_set.all() + + # Check if PollFile deletion delete only PollFile from database. + @pytest.mark.django_db() + def test_delete_pollfile(self, persist_poll_file): + assert persist_poll_file.poll in Poll.objects.all() + assert persist_poll_file.poll.author in User.objects.all() + assert persist_poll_file in PollFile.objects.all() + persist_poll_file.delete() + assert persist_poll_file.poll in Poll.objects.all() + assert persist_poll_file.poll.author in User.objects.all() + assert persist_poll_file not in PollFile.objects.all() + + # Check if PollFile's poll deletion delete both PollFile and poll from database - but not the poll's author. + @pytest.mark.django_db() + def test_delete_pollfile_poll(self, persist_poll_file): + persist_poll_file.poll.delete() + assert persist_poll_file.poll not in Poll.objects.all() + assert persist_poll_file.poll.author in User.objects.all() + assert persist_poll_file not in PollFile.objects.all() + + # Check if PollFile's poll's author deletion delete also PollFile and database. + @pytest.mark.django_db() + def test_delete_pollfile_user(self, persist_poll_file): + persist_poll_file.poll.author.delete() + assert persist_poll_file.poll not in Poll.objects.all() + assert persist_poll_file.poll.author not in User.objects.all() + assert persist_poll_file not in PollFile.objects.all() + + # Check if Poll can have several PollFiles simultaneously. + @pytest.mark.django_db() + def test_several_pollfiles(self, persist_poll): + file1 = PollFile(poll=persist_poll, file=FILE1) + assert len(PollFile.objects.all()) == 0 + file1.save() + assert len(PollFile.objects.all()) == 1 + persist_poll.pollfile_set.create(file=FILE2) + assert len(persist_poll.pollfile_set.all()) == 2 + + # verify that it is impossible to create a PollFile with invalid params. + @pytest.mark.parametrize("poll, file, expected_error", [( + USERNAME, DESCRIPTION, ValueError), + (new_user, DESCRIPTION, ValueError), + (new_poll, 6, ValueError)]) + def test_pollFile_invalid_args(self, poll, file, expected_error): + with pytest.raises(expected_error): + PollFile(poll=poll, file=file) + + +# -------------------------------------------- Choice tests -------------------------------------------- + + +@pytest.mark.django_db() +class TestChoice: + + # Check that the Choice values are the same as the Choice inputs. + def test_new_choice_input_same_as_output(self, new_choice): + assert new_choice.poll.author.username == USERNAME + assert new_choice.poll.description == DESCRIPTION + assert new_choice.poll.get_amount_of_votes() == 0 + assert new_choice.choice_text == CHOICETEXT1 + + # Check if the Choice is saved in the database and accessible via its Poll. + def test_persist_choice(self, persist_choice): + assert persist_choice in Choice.objects.all() + assert persist_choice in persist_choice.poll.choice_set.all() + + # Check if Choice deletion delete only Choice from database. + def test_delete_choice(self, persist_choice): + persist_choice.delete() + assert persist_choice.poll in Poll.objects.all() + assert persist_choice.poll.author in User.objects.all() + assert persist_choice not in Choice.objects.all() + + # Check if Choice's poll deletion delete both Choice and poll from database - but not the poll's author. + def test_delete_choice_poll(self, persist_choice): + persist_choice.poll.delete() + assert persist_choice.poll not in Poll.objects.all() + assert persist_choice.poll.author in User.objects.all() + assert persist_choice not in Choice.objects.all() + + # Check if Choice's poll's author deletion delete also Choice and database. + def test_delete_choice_user(self, persist_choice): + persist_choice.poll.author.delete() + assert persist_choice not in Poll.objects.all() + assert persist_choice.poll.author not in User.objects.all() + assert persist_choice not in Choice.objects.all() + + # Check if Poll can have several Choices simultaneously. + def test_several_choices(self, persist_poll): + choice1 = Choice(poll=persist_poll, choice_text=CHOICETEXT1) + assert len(Choice.objects.all()) == 0 + choice1.save() + assert len(Choice.objects.all()) == 1 + persist_poll.poll.choice_set.create(choice_text=CHOICETEXT2) + assert len(persist_poll.choice_set.all()) == 2 + + # verify that it is impossible to create a Choice with invalid params. + @pytest.mark.parametrize("poll, choice_text, expected_error", [( + USERNAME, CHOICETEXT1, ValueError), + (new_user, DESCRIPTION, ValueError), + (new_poll, 6, ValueError)]) + def test_pollFile_invalid_args(self, poll, choice_text, expected_error): + with pytest.raises(expected_error): + PollFile(poll=poll, file=choice_text) + + +# -------------------------------------------- votes tests -------------------------------------------- + + +@pytest.mark.django_db() +class TestVote: + + # Check if a vote is saved in the database and accessible via its voted choice and voter. + def test_persist_vote(self, persist_choice, persist_user): + assert persist_user not in persist_choice.voters.all() + persist_choice.voters.add(persist_user) + assert persist_user in persist_choice.voters.all() + assert persist_choice in persist_user.choice_set.all() + + # Check if vote removal works. + def test_remove_vote(self, persist_choice, persist_user): + persist_choice.voters.add(persist_user) + persist_choice.voters.remove(persist_user) + assert persist_user not in persist_choice.voters.all() + + # Check if Choice can have several Votes simultaneously. + def test_several_votes_on_one_choice(self, persist_choice, persist_user): + persist_choice.voters.add(persist_user) + persist_choice.voters.create( + username="user2", + first_name=FIRSTNAME, + last_name=LASTNAME, + password=PASSWORD, + email="test2@gmail.com") + assert len(persist_choice.voters.all()) == 2 + + # Check if Choice can have several Votes simultaneously. + def test_several_votes_choices_of_one_user(self, persist_poll, persist_user): + choice1 = persist_poll.choice_set.create(choice_text=CHOICETEXT1) + choice2 = persist_poll.choice_set.create(choice_text=CHOICETEXT2) + choice1.voters.add(persist_user) + choice2.voters.add(persist_user) + assert len(persist_user.choice_set.all()) == 2 + + # Check if percentage function work as expected. + def test_percentage_and_get_amount_of_voters_functions(self, persist_poll): + user1 = User.objects.create( + username="user1", + first_name="uaser1", + last_name="test", + password="12345", + email="user1@gmail.com") + user2 = User.objects.create( + username="user2", + first_name="uaser2", + last_name="test", + password="12345", + email="user2@gmail.com") + user3 = User.objects.create( + username="user3", + first_name="uaser3", + last_name="test", + password="12345", + email="user3@gmail.com") + user4 = User.objects.create( + username="user4", + first_name="uaser4", + last_name="test", + password="12345", + email="user4@gmail.com") + choice1 = persist_poll.choice_set.create(choice_text=CHOICETEXT1) + choice2 = persist_poll.choice_set.create(choice_text=CHOICETEXT2) + assert choice1.get_amount_of_votes() == 0 + choice1.voters.add(user1) + assert choice1.get_amount_of_votes() == 1 + assert choice1.get_percentage() == 100.0 + assert choice2.get_percentage() == 0 + assert persist_poll.get_amount_of_votes() == 1 + choice1.voters.add(user2) + choice1.voters.add(user3) + choice2.voters.add(user4) + assert choice1.get_amount_of_votes() == 3 + assert choice2 + assert choice1.get_percentage() == 75.0 + assert choice2.get_percentage() == 25.0 + assert persist_poll.get_amount_of_votes() == 4