diff --git a/polls/migrations/0015_question_trend_score.py b/polls/migrations/0015_question_trend_score.py new file mode 100644 index 0000000..6b1b3a9 --- /dev/null +++ b/polls/migrations/0015_question_trend_score.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.4 on 2023-09-15 07:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('polls', '0014_remove_question_down_vote_count_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='question', + name='trend_score', + field=models.FloatField(default=0.0), + ), + ] diff --git a/polls/models.py b/polls/models.py index 70995ff..3fd50d5 100644 --- a/polls/models.py +++ b/polls/models.py @@ -46,6 +46,7 @@ class Question(models.Model): end_date = models.DateTimeField("date ended", null=True) short_description = models.CharField(max_length=200, default="Cool kids have polls") long_description = models.TextField(max_length=2000, default="No description provide for this poll.") + trend_score = models.FloatField(default=0.0, null=False, blank=False) tags = models.ManyToManyField(Tag, blank=True) def was_published_recently(self): @@ -157,6 +158,9 @@ class Question(models.Model): # ! Most of the code from https://stackoverflow.com/a/70869267 def upvote(self, user): + """create new SentimentVote object that represent upvote (vote_types=True) + return True if user change the vote or vote for the first time else return False + """ try: self.sentimentvote_set.create(user=user, question=self, vote_types=True) self.save() @@ -170,6 +174,9 @@ class Question(models.Model): return True def downvote(self, user): + """create new SentimentVote object that represent downvote (vote_types=False) + return True if user change the vote or vote for the first time else return False + """ try: self.sentimentvote_set.create(user=user, question=self, vote_types=False) self.save() @@ -190,6 +197,38 @@ class Question(models.Model): def down_vote_count(self): return self.sentimentvote_set.filter(question=self, vote_types=False).count() + def trending_score(self, up=None, down=None): + """Return trend score base on the criteria below""" + published_date_duration = timezone.now() - self.pub_date + score = 0 + + if (published_date_duration.seconds < 259200): # Second unit + score += 100 + elif (published_date_duration.seconds < 604800): + score += 75 + elif (published_date_duration.seconds < 2592000): + score += 50 + else: + score += 25 + + if (up == None) and (down == None): + score += ((self.up_vote_count/5) - (self.down_vote_count/5)) * 100 + else: + score += ((up/5) - (down/5)) * 100 + + return score + + + def save(self, *args, **kwargs): + """Modify save method of Question object""" + # to-be-added instance # * https://github.com/django/django/blob/866122690dbe233c054d06f6afbc2f3cc6aea2f2/django/db/models/base.py#L447 + if self._state.adding: + try: + self.trend_score = self.trending_score() + except ValueError: + self.trend_score = self.trending_score(up=0, down=0) + super(Question, self).save(*args, **kwargs) + class Choice(models.Model): """ @@ -228,9 +267,25 @@ class Vote(models.Model): # ! Most of the code from https://stackoverflow.com/a/70869267 class SentimentVote(models.Model): + """ + Represents a sentiment vote for a poll question. + + Attributes: + user (User): The user who cast the sentiment vote. + question (Question): The poll question for which the sentiment vote is cast. + vote_types (bool): Indicates whether the sentiment vote is an upvote (True) or a downvote (False). + + Note: + - When 'vote_types' is True, it represents an upvote or 'Like'. + - When 'vote_types' is False, it represents a downvote or 'Dislike'. + """ user = models.ForeignKey(User, on_delete=models.CASCADE) question = models.ForeignKey(Question, on_delete=models.CASCADE) vote_types = models.BooleanField() class Meta: + """ + unique_together (list of str): Ensures that a user can only cast one sentiment vote (upvote or downvote) + for a specific question. + """ unique_together = ['user', 'question'] \ No newline at end of file diff --git a/polls/templates/polls/index.html b/polls/templates/polls/index.html index fb8fdb2..0dd54a0 100644 --- a/polls/templates/polls/index.html +++ b/polls/templates/polls/index.html @@ -83,7 +83,7 @@

Top Polls Today


- {% for question in latest_question_list %} + {% for question in latest_question_list.trend_poll %}
@@ -129,7 +129,7 @@

All Polls


- {% for question in latest_question_list %} + {% for question in latest_question_list.all_poll %}
diff --git a/polls/tests/test_index_views.py b/polls/tests/test_index_views.py index 4cf0ad2..911dac1 100644 --- a/polls/tests/test_index_views.py +++ b/polls/tests/test_index_views.py @@ -12,7 +12,7 @@ class QuestionIndexViewTests(TestCase): """ response = self.client.get(reverse("polls:index")) self.assertEqual(response.status_code, 200) - self.assertQuerySetEqual(response.context["latest_question_list"], []) + self.assertQuerySetEqual(response.context["latest_question_list"]["all_poll"], []) def test_past_question(self): """ @@ -24,7 +24,7 @@ class QuestionIndexViewTests(TestCase): question.save() response = self.client.get(reverse("polls:index")) self.assertQuerySetEqual( - response.context["latest_question_list"], + response.context["latest_question_list"]["all_poll"], [question], ) @@ -37,7 +37,7 @@ class QuestionIndexViewTests(TestCase): future_question.pub_date = timezone.now() + timezone.timedelta(days=30) future_question.save() response = self.client.get(reverse("polls:index")) - self.assertQuerySetEqual(response.context["latest_question_list"], []) + self.assertQuerySetEqual(response.context["latest_question_list"]["all_poll"], []) def test_future_question_and_past_question(self): """ @@ -54,7 +54,7 @@ class QuestionIndexViewTests(TestCase): response = self.client.get(reverse("polls:index")) self.assertQuerySetEqual( - response.context["latest_question_list"], + response.context["latest_question_list"]["all_poll"], [past_question], ) @@ -72,6 +72,6 @@ class QuestionIndexViewTests(TestCase): response = self.client.get(reverse("polls:index")) self.assertQuerySetEqual( - response.context["latest_question_list"], + response.context["latest_question_list"]["all_poll"], [question2, question1], ) diff --git a/polls/tests/test_question_model.py b/polls/tests/test_question_model.py index 6514de0..f0cdd07 100644 --- a/polls/tests/test_question_model.py +++ b/polls/tests/test_question_model.py @@ -52,7 +52,7 @@ class QuestionModelTests(TestCase): response = self.client.get(reverse("polls:index")) self.assertQuerySetEqual( - response.context["latest_question_list"], + response.context["latest_question_list"]["all_poll"], [question], ) diff --git a/polls/tests/test_sentiment_model.py b/polls/tests/test_sentiment_model.py new file mode 100644 index 0000000..c1ede3b --- /dev/null +++ b/polls/tests/test_sentiment_model.py @@ -0,0 +1,42 @@ +from django.test import TransactionTestCase, Client +from django.contrib.auth.models import User + +from .base import create_question +from ..views import up_down_vote + +# ! https://stackoverflow.com/questions/24588520/testing-several-integrityerrors-in-the-same-django-unittest-test-case +# * https://stackoverflow.com/questions/44450533/difference-between-testcase-and-transactiontestcase-classes-in-django-test +class UpDownVoteViewTest(TransactionTestCase): + @classmethod + def setUp(cls) -> None: + cls.user = User.objects.create_user(username="test_user", password="12345abc") + cls.q1 = create_question(question_text="test 1") + cls.client = Client() + + def test_vote_up_once(self): + self.client.login(username="test_user", password="12345abc") + self.q1.upvote(self.user) + self.assertFalse(self.q1.upvote(self.user)) + + def test_vote_down_once(self): + self.client.login(username="test_user", password="12345abc") + self.q1.downvote(self.user) + self.assertFalse(self.q1.downvote(self.user)) + + def test_can_change_up_to_down(self): + self.client.login(username="test_user", password="12345abc") + self.q1.upvote(self.user) + self.q1.downvote(self.user) + count_up = self.q1.sentimentvote_set.filter(vote_types=True).count() + count_down = self.q1.sentimentvote_set.filter(vote_types=False).count() + self.assertEqual(count_up, 0) + self.assertEqual(count_down, 1) + + def test_can_change_up_to_down(self): + self.client.login(username="test_user", password="12345abc") + self.q1.downvote(self.user) + self.q1.upvote(self.user) + count_up = self.q1.sentimentvote_set.filter(vote_types=True).count() + count_down = self.q1.sentimentvote_set.filter(vote_types=False).count() + self.assertEqual(count_up, 1) + self.assertEqual(count_down, 0) \ No newline at end of file diff --git a/polls/views.py b/polls/views.py index 1af2b09..b3b2907 100644 --- a/polls/views.py +++ b/polls/views.py @@ -8,6 +8,7 @@ from django.utils import timezone from django.urls import reverse_lazy, reverse from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages +from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required from django.db.models import Q @@ -29,10 +30,18 @@ class IndexView(generic.ListView): Return the last published questions that is published and haven't ended yet. """ now = timezone.now() - return Question.objects.filter( + all_poll_queryset = Question.objects.filter( Q(pub_date__lte=now) & ((Q(end_date__gte=now) | Q(end_date=None))) ).order_by("-pub_date") + trend_poll_queryset = Question.objects.filter( + Q(pub_date__lte=now) & ((Q(end_date__gte=now) | Q(end_date=None))) & Q(trend_score__gte=100) + ).order_by("trend_score")[:3] + + queryset = {'all_poll' : all_poll_queryset, + 'trend_poll' : trend_poll_queryset,} + return queryset + class DetailView(LoginRequiredMixin, generic.DetailView): """ @@ -105,6 +114,13 @@ class SignUpView(generic.CreateView): success_url = reverse_lazy('login') template_name = 'registration/signup.html' + def form_valid(self, form): + valid = super(SignUpView, self).form_valid(form) + username, password = form.cleaned_data.get("username"), form.cleaned_data.get("password1") + user = authenticate(username=username, password=password) + login(self.request, user) + return valid + @login_required def vote(request, question_id):