Fix query in Index / Check User Vote/ Refine Model

- Use Model from domain model
- Change Vote Query because I change model
- Write some properties such as participants in Question
This commit is contained in:
sosokker 2023-09-12 02:47:21 +07:00
parent 791d9c3b0d
commit 50874dbf40
8 changed files with 227 additions and 109 deletions

View File

@ -13,12 +13,12 @@ class QuestionAdmin(admin.ModelAdmin):
(None, {"fields": ["question_text"]}),
("Published date", {"fields": ["pub_date"], "classes": ["collapse"]}),
("End date", {"fields": ["end_date"], "classes": ["collapse"]}),
("Vote count", {"fields": ["up_vote_count", "down_vote_count"]}),
("Sentiment Vote count", {"fields": ["up_vote_count", "down_vote_count"]}),
("Participant count", {"fields": ["participant_count"]}),
]
list_display = ["question_text", "pub_date", "end_date", "was_published_recently"]
list_display = ["question_text", "pub_date", "end_date", "was_published_recently", "can_vote"]
inlines = [ChoiceInline]
list_filter = ["pub_date"]
list_filter = ["pub_date", ]
search_fields = ["question_text"]

View File

@ -0,0 +1,41 @@
# Generated by Django 4.2.4 on 2023-09-11 18:23
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),
('polls', '0008_alter_question_pub_date'),
]
operations = [
migrations.CreateModel(
name='Tag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tag_text', models.CharField(max_length=50)),
],
),
migrations.RemoveField(
model_name='choice',
name='votes',
),
migrations.CreateModel(
name='Vote',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('choice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.choice')),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.question')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='question',
name='tags',
field=models.ManyToManyField(blank=True, to='polls.tag'),
),
]

View File

@ -17,6 +17,16 @@ from django.db.models import Sum
from django.contrib.auth.models import User
class Tag(models.Model):
"""
Represents a tag for a poll question.
"""
tag_text = models.CharField(max_length=50)
def __str__(self):
return self.name
class Question(models.Model):
"""
Represents a poll question.
@ -33,23 +43,15 @@ class Question(models.Model):
"""
question_text = models.CharField(max_length=100)
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."
)
pub_date = models.DateTimeField(
"date published", default=timezone.now, editable=True
)
pub_date = models.DateTimeField("date published", default=timezone.now, editable=True)
end_date = models.DateTimeField("date ended", null=True)
up_vote_count = models.PositiveIntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)]
)
down_vote_count = models.PositiveIntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)]
)
participant_count = models.PositiveIntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)]
)
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.")
tags = models.ManyToManyField(Tag, blank=True)
up_vote_count = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)])
down_vote_count = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)])
participant_count = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)])
def was_published_recently(self):
"""
@ -151,6 +153,13 @@ class Question(models.Model):
def down_vote_percentage(self):
return self.calculate_vote_percentage()[1]
@property
def participants(self):
"""
Calculate the number of participants based on the number of votes.
"""
return self.vote_set.count()
class Choice(models.Model):
"""
@ -164,40 +173,24 @@ class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.PositiveIntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)]
)
def tailwind_width_class(self):
"""
Calculate and return the Tailwind CSS width class based on the 'votes' percentage.
"""
total_votes = self.question.choice_set.aggregate(Sum("votes")).get(
"votes__sum", 0
)
#! Tailwind w-0 to w-48
if total_votes == 0:
return "w-0"
ratio = self.votes / total_votes
scaled_value = ratio * 48
return f"w-{int(round(scaled_value))}"
def calculate_percentage(self):
"""Calculate percentage of votes for all choices."""
total_votes_for_question = (
self.question.choice_set.aggregate(Sum("votes"))["votes__sum"] or 0
)
if total_votes_for_question == 0:
return 0
else:
return round((self.votes / total_votes_for_question) * 100, 2)
@property
def votes(self):
return self.vote_set.count()
def __str__(self):
"""
Returns a string representation of the choice.
"""
return self.choice_text
return f"{self.choice_text} get ({self.votes})"
class Vote(models.Model):
"""Represent Vote of User for a poll question."""
choice = models.ForeignKey(Choice, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
question = models.ForeignKey(Question, on_delete=models.CASCADE)
def __str__(self):
return f"{self.user} voted for {self.choice} in {self.question}"

View File

@ -1,38 +1,82 @@
const toggleChoice = (button, choiceId) => {
const choiceInput = document.querySelector(`input[name="choice"][value="${choiceId}"]`);
const choiceInput = document.querySelector(`input[name="choice"][value="${choiceId}"]`);
const selectedChoice2 = document.getElementById("selected-choice-1");
if (selectedChoice2 !== null) {
if (choiceInput) {
// already selected -> unselect it
if (choiceInput.checked) {
choiceInput.checked = false;
button.classList.remove('bg-green-500', 'border-solid', 'border-2', 'border-black', 'hover:bg-green-600');
button.classList.add('bg-white', 'border-solid', 'border-2', 'border-black', 'hover:bg-white');
// Clear display
document.getElementById('selected-choice').textContent = 'Please Select a Choice😊';
} else {
// Unselect all choices
document.querySelectorAll('input[name="choice"]').forEach((choice) => {
choice.checked = false;
});
// already selected -> unselect it
if (choiceInput.checked) {
choiceInput.checked = false;
button.classList.remove("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600");
button.classList.add("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white");
// Clear display
document.getElementById("selected-choice-1").textContent = "You have been voted😊 Anyway, you can change the choice anytime before end date";
} else {
// Unselect all choices
document.querySelectorAll('input[name="choice"]').forEach(choice => {
choice.checked = false;
});
// Select the clicked choice
choiceInput.checked = true;
// Select the clicked choice
choiceInput.checked = true;
// Reset the style of all choice buttons
document.querySelectorAll('.choice-button').forEach((btn) => {
btn.classList.remove('bg-green-500', 'border-solid', 'border-2', 'border-black', 'hover:bg-green-600');
btn.classList.add('bg-white', 'border-solid', 'border-2', 'border-black', 'hover:bg-white');
});
// Reset the style of all choice buttons
document.querySelectorAll(".choice-button").forEach(btn => {
btn.classList.remove("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600");
btn.classList.add("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white");
});
button.classList.remove('bg-white', 'border-solid', 'border-2', 'border-black', 'hover:bg-white');
button.classList.add('bg-green-500', 'border-solid', 'border-2', 'border-black', 'hover:bg-green-600');
button.classList.remove("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white");
button.classList.add("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600");
const choiceText = button.textContent.trim();
document.getElementById('selected-choice').textContent = `You select: ${choiceText}`;
}
// Enable the "Vote" button -> if select
const voteButton = document.getElementById('vote-button');
voteButton.disabled = !document.querySelector('input[name="choice"]:checked');
const choiceText = button.textContent.trim();
document.getElementById("selected-choice-1").textContent = `You select: ${choiceText}`;
}
}
};
// Enable the "Vote" button -> if select
const voteButton = document.getElementById("vote-button");
voteButton.disabled = !document.querySelector('input[name="choice"]:checked');
} else {
if (choiceInput) {
// already selected -> unselect it
if (choiceInput.checked) {
choiceInput.checked = false;
button.classList.remove("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600");
button.classList.add("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white");
// Clear display
document.getElementById("selected-choice-2").textContent = "Please Select a Choice😊";
} else {
// Unselect all choices
document.querySelectorAll('input[name="choice"]').forEach(choice => {
choice.checked = false;
});
// Select the clicked choice
choiceInput.checked = true;
// Reset the style of all choice buttons
document.querySelectorAll(".choice-button").forEach(btn => {
btn.classList.remove("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600");
btn.classList.add("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white");
});
button.classList.remove("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white");
button.classList.add("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600");
const choiceText = button.textContent.trim();
document.getElementById("selected-choice-2").textContent = `You select: ${choiceText}`;
}
}
// Enable the "Vote" button -> if select
const voteButton = document.getElementById("vote-button");
voteButton.disabled = !document.querySelector('input[name="choice"]:checked');
}
};
function confirmChangeVote(choiceId) {
const confirmation = confirm("Are you sure you want to change your vote?");
if (confirmation) {
window.location.href = `{% url 'polls:vote' question.id %}${choiceId}`;
}
}

View File

@ -46,14 +46,29 @@
<div class="flex flex-col space-y-4">
<form action="{% url 'polls:vote' question.id %}" method="post" class="poll-form" id="poll-form">
{% csrf_token %}
<div class="bg-white p-4 rounded-lg shadow-md mb-4">
<div id="selected-choice" class="mt-4 text-lg font-bold text-green-500">Please Select a Choice😊</div>
</div>
{% if selected_choice %}
<div class="bg-white p-4 rounded-lg shadow-md mb-4">
<div id="selected-choice-1" class="mt-4 text-lg font-bold text-orange-500">You have been voted: {{ selected_choice.choice_text }}</div>
</div>
{% else %}
<div class="bg-white p-4 rounded-lg shadow-md mb-4">
<div id="selected-choice-2" class="mt-4 text-lg font-bold text-green-500">Please Select a Choice😊</div>
</div>
{% endif %}
<div class="bg-white p-4 rounded-lg shadow-md mb-4">
<div class="grid grid-cols-3 gap-4">
<!-- Buttons as choices (hidden) -->
{% for choice in question.choice_set.all %}
<label>
{% if choice == selected_choice %}
<input type="radio" name="choice" value="{{ choice.id }}" class="hidden" />
<button
type="button"
class="choice-button selected bg-white-500 border-2 border-black hover:bg-neutral-200 text-black px-4 py-2 rounded-lg shadow-md transition-colors duration-300 w-full py-5 font-bold text-lg truncate"
onclick="toggleChoice(this, '{{ choice.id }}')">
{{ choice.choice_text }}
</button>
{% else %}
<input type="radio" name="choice" value="{{ choice.id }}" class="hidden" />
<button
type="button"
@ -61,6 +76,7 @@
onclick="toggleChoice(this, '{{ choice.id }}')">
{{ choice.choice_text }}
</button>
{% endif %}
</label>
{% endfor %}
</div>

View File

@ -95,7 +95,7 @@
<!-- Tag / Time -->
<div class="flex items-center text-gray-600">
<span class="mr-2 rounded-md bg-green-500 px-2 py-1 text-white">🕒 {{ question.time_left }}</span>
<span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">{{ question.participant_count }} Participants 👤</span>
<span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">{{ question.participants }} Participants 👤</span>
</div>
<div class="flex items-center text-gray-600 py-4">
<button
@ -141,7 +141,7 @@
<!-- Tag / Time -->
<div class="flex items-center text-gray-600">
<span class="mr-2 rounded-md bg-green-500 px-2 py-1 text-white">🕒 {{ question.time_left }}</span>
<span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">{{ question.participant_count }} Participants 👤</span>
<span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">{{ question.participants }} Participants 👤</span>
</div>
<div class="flex items-center text-gray-600 py-4">
<button

View File

@ -42,15 +42,15 @@
<!-- Result Summary -->
<div class="relative">
<div class="bg-white p-4 rounded-lg shadow-md mb-4 z-10 relative border-black border-solid border-2">
<h2 class="text-xl font-semibold mb-2">Result Summary:</h2>
<h2 class="text-xl font-semibold mb-2">Result Summary</h2>
{% for choice in question.choice_set.all %}
<div class="flex justify-between items-center mb-2">
<span>{{ choice.choice_text }}</span>
<div class="flex items-center">
<span class="mr-2">👍 {{ choice.votes }}</span>
<div class="percentage-bar">
<div class="bar bg-blue-500 h-2" style="width: {{ choice.calculate_percentage }}%;"></div>
<div class="vote-bar">
<div class="bar bg-blue-500 h-2" style="width: {{ choice.votes }}%;"></div>
</div>
</div>
</div>
@ -68,7 +68,7 @@
<div class="col-span-1 bg-white py-4 rounded-lg shadow-md mb-4 relative z-10 border-solid border-black border-2 h-full">
<h2 class="text-xl font-semibold mb-2">🕵️ Statistics</h2>
<span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">
{{ question.participant_count }} Participants 👤
{{ question.participants }} Participants 👤
</span>
<span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">👍 {{ question.up_vote_percentage }}% </span>
<span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">👎 {{ question.down_vote_percentage }}% </span>
@ -117,8 +117,8 @@
data: {
labels: [{% for choice in question.choice_set.all %}"{{ choice.choice_text }}",{% endfor %}],
datasets: [{
label: 'Percentage',
data: [{% for choice in question.choice_set.all %}{{ choice.calculate_percentage }},{% endfor %}],
label: 'Vote Count',
data: [{% for choice in question.choice_set.all %}{{ choice.votes }},{% endfor %}],
backgroundColor: [
'rgba(75, 192, 192, 0.2)',
'rgba(255, 99, 132, 0.2)',

View File

@ -7,9 +7,10 @@ from django.urls import reverse_lazy, reverse
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from .forms import SignUpForm
from .models import Choice, Question
from .models import Choice, Question, Vote
class IndexView(generic.ListView):
@ -19,10 +20,13 @@ class IndexView(generic.ListView):
context_object_name = "latest_question_list"
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.filter(pub_date__lte=timezone.now()).order_by(
"-pub_date"
)[:5]
"""
Return the last published questions that is published and haven't ended yet.
"""
now = timezone.now()
return Question.objects.filter(
Q(pub_date__lte=now) & (Q(end_date__gte=now) | Q(end_date=None))
).order_by("-pub_date")
class DetailView(LoginRequiredMixin, generic.DetailView):
@ -52,7 +56,21 @@ class DetailView(LoginRequiredMixin, generic.DetailView):
context["end_date"] = question.end_date
context["up_vote_count"] = question.up_vote_count
context["down_vote_count"] = question.down_vote_count
context["participant_count"] = question.participant_count
user = self.request.user
selected_choice = None
has_voted = False
if user.is_authenticated:
try:
vote = question.vote_set.get(user=user)
selected_choice = vote.choice
has_voted = True
except Vote.DoesNotExist:
pass
context["selected_choice"] = selected_choice
context["has_voted"] = has_voted
return context
@ -61,11 +79,6 @@ class ResultsView(LoginRequiredMixin, generic.DetailView):
model = Question
template_name = "polls/results.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["question"] = self.object
return context
def render_to_response(self, context, **response_kwargs):
return render(self.request, self.template_name, context)
@ -87,12 +100,23 @@ def vote(request, question_id):
except (KeyError, Choice.DoesNotExist):
messages.error(request, "You didn't select a choice.")
return render(request, "polls/detail.html", {"question": question})
else:
if request.method == "POST" and "vote-button" in request.POST:
selected_choice.votes += 1
selected_choice.save()
messages.success(request, "Your vote was recorded successfully🥳")
return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
if question.can_vote():
if request.method == "POST" and "vote-button" in request.POST:
if Vote.objects.filter(user=request.user, question=question).exists():
old_vote = question.vote_set.get(user=request.user)
old_vote.choice = selected_choice
old_vote.save()
messages.success(request, "You vote successfully🥳")
return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
else:
Vote.objects.create(choice=selected_choice, user=request.user, question=question).save()
else:
messages.error(request, "You cannot vote by typing the URL.")
return render(request, "polls/detail.html", {"question": question})
else:
messages.error(request, "You cannot vote by typing the URL.")
return render(request, "polls/detail.html", {"question": question})
messages.error(request, "You can not vote on this question.")
return HttpResponseRedirect(reverse("polls:index"))
return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))