Add trend score and modify view / test to apply trending

This commit is contained in:
sosokker 2023-09-15 23:52:51 +07:00
parent f6d0239ec6
commit a9eb3b6497
7 changed files with 140 additions and 9 deletions

View File

@ -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),
),
]

View File

@ -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']

View File

@ -83,7 +83,7 @@
<h2 class="text-2xl font-bold bg-gradient-to-r from-red-600 via-orange-600 to-yellow-600 bg-clip-text text-transparent lg:inline">Top Polls Today</h2>
<hr class="h-px my-4 bg-gray-200 border-0 dark:bg-gray-400" />
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{% for question in latest_question_list %}
{% for question in latest_question_list.trend_poll %}
<div class="relative">
<!-- INFO -->
<div class="rounded-lg bg-white p-4 shadow-md border-solid border-2 border-yellow-500 relative z-10 transform translate-y-0 hover:translate-y-1 transition-transform">
@ -129,7 +129,7 @@
<h2 class="mb-4 text-2xl font-bold">All Polls</h2>
<hr class="h-px my-4 bg-gray-200 border-0 dark:bg-gray-400" />
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3">
{% for question in latest_question_list %}
{% for question in latest_question_list.all_poll %}
<div class="relative">
<!-- INFO -->
<div class="rounded-lg bg-white p-4 shadow-md border-solid border-2 border-neutral-500 relative z-10 transform translate-y-0 hover:translate-y-1 transition-transform">

View File

@ -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],
)

View File

@ -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],
)

View File

@ -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)

View File

@ -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):