mirror of
https://github.com/Sosokker/ku-polls.git
synced 2025-12-18 13:04:05 +01:00
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:
parent
791d9c3b0d
commit
50874dbf40
@ -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"]
|
||||
|
||||
|
||||
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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}"
|
||||
@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)',
|
||||
|
||||
@ -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,)))
|
||||
Loading…
Reference in New Issue
Block a user