Merge pull request #33 from Sosokker/iteration3

Iteration3 Merge model domain approach + Authetication System
This commit is contained in:
Sirin Puenggun 2023-09-12 14:02:17 +07:00 committed by GitHub
commit 0aa1d7d213
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 775 additions and 164 deletions

View File

@ -75,6 +75,15 @@ python -m virtualenv .venv
|:--:|:--:| |:--:|:--:|
|admin|ineedmorebullets| |admin|ineedmorebullets|
## Demo User
|Username|Password|
|:--:|:--:|
|tester1|aa12345678aa|
|tester2|aa12345678aa|
|tester3|aa12345678aa|
|tester4|aa12345678aa|
## Project Documents ## Project Documents
All project documents are in the [Project Wiki](https://github.com/Sosokker/ku-polls/wiki). All project documents are in the [Project Wiki](https://github.com/Sosokker/ku-polls/wiki).

242
data/polls.json Normal file
View File

@ -0,0 +1,242 @@
[
{
"model": "polls.question",
"pk": 1,
"fields": {
"question_text": "Python vs C++, which one is better in your opinion?",
"pub_date": "2023-09-05T06:31:14Z",
"end_date": "2023-09-29T20:31:49Z",
"short_description": "Cool kids have polls",
"long_description": "No description provide for this poll.",
"up_vote_count": 5,
"down_vote_count": 0,
"participant_count": 6,
"tags": []
}
},
{
"model": "polls.question",
"pk": 2,
"fields": {
"question_text": "The chicken and the egg, which came first?",
"pub_date": "2023-09-11T02:50:04Z",
"end_date": "2023-09-19T23:50:19Z",
"short_description": "Cool kids have polls",
"long_description": "No description provide for this poll.",
"up_vote_count": 1,
"down_vote_count": 0,
"participant_count": 0,
"tags": []
}
},
{
"model": "polls.question",
"pk": 3,
"fields": {
"question_text": "So far so good?",
"pub_date": "2023-08-03T06:50:43Z",
"end_date": "2023-11-15T19:50:53Z",
"short_description": "Cool kids have polls",
"long_description": "No description provide for this poll.",
"up_vote_count": 1,
"down_vote_count": 0,
"participant_count": 0,
"tags": []
}
},
{
"model": "polls.question",
"pk": 4,
"fields": {
"question_text": "Do you love Django?",
"pub_date": "2023-09-11T19:51:12Z",
"end_date": "2023-09-13T17:51:18Z",
"short_description": "Cool kids have polls",
"long_description": "No description provide for this poll.",
"up_vote_count": 10,
"down_vote_count": 0,
"participant_count": 0,
"tags": []
}
},
{
"model": "polls.choice",
"pk": 1,
"fields": {
"question": 1,
"choice_text": "C++"
}
},
{
"model": "polls.choice",
"pk": 2,
"fields": {
"question": 1,
"choice_text": "Python"
}
},
{
"model": "polls.choice",
"pk": 3,
"fields": {
"question": 2,
"choice_text": "Egg!"
}
},
{
"model": "polls.choice",
"pk": 4,
"fields": {
"question": 2,
"choice_text": "Chicken"
}
},
{
"model": "polls.choice",
"pk": 5,
"fields": {
"question": 3,
"choice_text": "Yes sir!"
}
},
{
"model": "polls.choice",
"pk": 6,
"fields": {
"question": 3,
"choice_text": "Nah"
}
},
{
"model": "polls.choice",
"pk": 7,
"fields": {
"question": 4,
"choice_text": "Yeah for sure!"
}
},
{
"model": "polls.choice",
"pk": 8,
"fields": {
"question": 4,
"choice_text": "Hell nah!"
}
},
{
"model": "polls.choice",
"pk": 9,
"fields": {
"question": 4,
"choice_text": "No comment."
}
},
{
"model": "polls.vote",
"pk": 1,
"fields": {
"choice": 1,
"user": 1,
"question": 1
}
},
{
"model": "polls.vote",
"pk": 2,
"fields": {
"choice": 2,
"user": 3,
"question": 1
}
},
{
"model": "polls.vote",
"pk": 3,
"fields": {
"choice": 1,
"user": 2,
"question": 1
}
},
{
"model": "polls.vote",
"pk": 4,
"fields": {
"choice": 7,
"user": 2,
"question": 4
}
},
{
"model": "polls.vote",
"pk": 5,
"fields": {
"choice": 6,
"user": 2,
"question": 3
}
},
{
"model": "polls.vote",
"pk": 6,
"fields": {
"choice": 4,
"user": 2,
"question": 2
}
},
{
"model": "polls.vote",
"pk": 7,
"fields": {
"choice": 8,
"user": 4,
"question": 4
}
},
{
"model": "polls.vote",
"pk": 8,
"fields": {
"choice": 6,
"user": 4,
"question": 3
}
},
{
"model": "polls.vote",
"pk": 9,
"fields": {
"choice": 3,
"user": 4,
"question": 2
}
},
{
"model": "polls.vote",
"pk": 10,
"fields": {
"choice": 2,
"user": 4,
"question": 1
}
},
{
"model": "polls.vote",
"pk": 11,
"fields": {
"choice": 6,
"user": 3,
"question": 3
}
},
{
"model": "polls.vote",
"pk": 12,
"fields": {
"choice": 3,
"user": 3,
"question": 2
}
}
]

View File

@ -1,20 +0,0 @@
[
{
"model": "auth.user",
"pk": 1,
"fields": {
"password": "pbkdf2_sha256$600000$iGfCFe97r89Z86pdlQGUnB$WWlHxi2Q1iVSSk0kZt0C5QSwwBKwzEofhJ8CBvakemU=",
"last_login": null,
"is_superuser": true,
"username": "admin",
"first_name": "",
"last_name": "",
"email": "admin@email.com",
"is_staff": true,
"is_active": true,
"date_joined": "2023-08-31T10:14:33.235Z",
"groups": [],
"user_permissions": []
}
}
]

92
data/users.json Normal file
View File

@ -0,0 +1,92 @@
[
{
"model": "auth.user",
"pk": 1,
"fields": {
"password": "pbkdf2_sha256$600000$aDh9a1PXxcXAb8z3YIjAPX$NVH24kt/wMad+0fZcCii738dfojI4vL2ffXOwNRuLz4=",
"last_login": "2023-09-12T04:02:42.758Z",
"is_superuser": true,
"username": "admin",
"first_name": "",
"last_name": "",
"email": "admin@email.com",
"is_staff": true,
"is_active": true,
"date_joined": "2023-09-11T18:24:20.740Z",
"groups": [],
"user_permissions": []
}
},
{
"model": "auth.user",
"pk": 2,
"fields": {
"password": "pbkdf2_sha256$600000$quZKLKT8Ec3TQgpdqlCkpX$o+VOOnRDLGf64qjHb239Yvsre74tPkC8hw1qH1un/hk=",
"last_login": "2023-09-12T04:22:38.555Z",
"is_superuser": false,
"username": "tester1",
"first_name": "",
"last_name": "",
"email": "",
"is_staff": false,
"is_active": true,
"date_joined": "2023-09-11T19:41:22.592Z",
"groups": [],
"user_permissions": []
}
},
{
"model": "auth.user",
"pk": 3,
"fields": {
"password": "pbkdf2_sha256$600000$1xGp6EDCoaljdTlSdVT1Mn$UID0Woeh8hwW7LtchH+hKzqdKTDeITTxQ/0DGvfG3CY=",
"last_login": "2023-09-11T19:57:39.303Z",
"is_superuser": false,
"username": "tester3",
"first_name": "",
"last_name": "",
"email": "",
"is_staff": false,
"is_active": true,
"date_joined": "2023-09-11T19:41:41.209Z",
"groups": [],
"user_permissions": []
}
},
{
"model": "auth.user",
"pk": 4,
"fields": {
"password": "pbkdf2_sha256$600000$fJJcIwAuIESYwZDBOqBv8t$YEDVCgg/xJOqAOiAdvGvvqgi1jgn1YfYHJE9yx2JWTA=",
"last_login": "2023-09-11T19:55:41.583Z",
"is_superuser": false,
"username": "tester2",
"first_name": "",
"last_name": "",
"email": "",
"is_staff": false,
"is_active": true,
"date_joined": "2023-09-11T19:43:25.226Z",
"groups": [],
"user_permissions": []
}
},
{
"model": "auth.user",
"pk": 5,
"fields": {
"password": "pbkdf2_sha256$600000$aHyU2gjOR6Vfsh3DBMIvQy$PZwRu+rOLc+N15DDguvy29dks6GUiN5YN/4io8b390o=",
"last_login": null,
"is_superuser": false,
"username": "novote",
"first_name": "",
"last_name": "",
"email": "",
"is_staff": false,
"is_active": true,
"date_joined": "2023-09-11T19:52:38.130Z",
"groups": [],
"user_permissions": []
}
}
]

View File

@ -126,3 +126,9 @@ STATICFILES_DIRS = [BASE_DIR]
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_REDIRECT_URL = "home_redirect"
LOGOUT_REDIRECT_URL = "home_redirect"
EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
EMAIL_FILE_PATH = BASE_DIR / "sent_emails"

View File

@ -7,4 +7,5 @@ urlpatterns = [
path('', RedirectView.as_view(pattern_name='polls:index'), name='home_redirect'), path('', RedirectView.as_view(pattern_name='polls:index'), name='home_redirect'),
path("polls/", include("polls.urls")), path("polls/", include("polls.urls")),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
] ]

View File

@ -13,12 +13,11 @@ class QuestionAdmin(admin.ModelAdmin):
(None, {"fields": ["question_text"]}), (None, {"fields": ["question_text"]}),
("Published date", {"fields": ["pub_date"], "classes": ["collapse"]}), ("Published date", {"fields": ["pub_date"], "classes": ["collapse"]}),
("End date", {"fields": ["end_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] inlines = [ChoiceInline]
list_filter = ["pub_date"] list_filter = ["pub_date", ]
search_fields = ["question_text"] search_fields = ["question_text"]

27
polls/forms.py Normal file
View File

@ -0,0 +1,27 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
class SignUpForm(UserCreationForm):
tailwind_class = "w-full border-2 border-gray-300 bg-gray-100 rounded-lg focus:ring focus:border-blue-300 focus:shadow-none"
username = forms.CharField(widget=forms.TextInput(attrs={'class': tailwind_class}),
error_messages={
'unique': 'This username is already in use.',
'invalid': 'Invalid username format.',
'max_length': 'Username should not exceed 150 characters.',
}
)
password1 = forms.CharField(widget=forms.PasswordInput(attrs={'class': tailwind_class}),
error_messages={'min_length': 'Password must contain at least 8 characters.',}
)
password2 = forms.CharField(widget=forms.PasswordInput(attrs={'class': tailwind_class}),)
class Meta:
model = User
fields = ('username', 'password1', 'password2')
error_messages = {
'password_mismatch': "The two password fields didn't match.",
}

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

@ -14,6 +14,17 @@ from django.utils import timezone
from django.contrib import admin from django.contrib import admin
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db.models import Sum 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): class Question(models.Model):
@ -32,23 +43,15 @@ class Question(models.Model):
""" """
question_text = models.CharField(max_length=100) question_text = models.CharField(max_length=100)
short_description = models.CharField(max_length=200, default="Cool kids have polls") pub_date = models.DateTimeField("date published", default=timezone.now, editable=True)
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
)
end_date = models.DateTimeField("date ended", null=True) end_date = models.DateTimeField("date ended", null=True)
up_vote_count = models.PositiveIntegerField( short_description = models.CharField(max_length=200, default="Cool kids have polls")
default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)] long_description = models.TextField(max_length=2000, default="No description provide for this poll.")
) tags = models.ManyToManyField(Tag, blank=True)
down_vote_count = models.PositiveIntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)] 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( participant_count = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)])
default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)]
)
def was_published_recently(self): def was_published_recently(self):
""" """
@ -150,6 +153,13 @@ class Question(models.Model):
def down_vote_percentage(self): def down_vote_percentage(self):
return self.calculate_vote_percentage()[1] 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): class Choice(models.Model):
""" """
@ -163,40 +173,24 @@ class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE) question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200) choice_text = models.CharField(max_length=200)
votes = models.PositiveIntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)]
)
def tailwind_width_class(self): @property
""" def votes(self):
Calculate and return the Tailwind CSS width class based on the 'votes' percentage. return self.vote_set.count()
"""
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)
def __str__(self): def __str__(self):
""" """
Returns a string representation of the choice. 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 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) { if (choiceInput) {
// already selected -> unselect it // already selected -> unselect it
if (choiceInput.checked) { if (choiceInput.checked) {
choiceInput.checked = false; choiceInput.checked = false;
button.classList.remove('bg-green-500', 'border-solid', 'border-2', 'border-black', 'hover:bg-green-600'); 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'); button.classList.add("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white");
// Clear display // Clear display
document.getElementById('selected-choice').textContent = 'Please Select a Choice😊'; document.getElementById("selected-choice-1").textContent = "You have been voted😊 Anyway, you can change the choice anytime before end date";
} else { } else {
// Unselect all choices // Unselect all choices
document.querySelectorAll('input[name="choice"]').forEach((choice) => { document.querySelectorAll('input[name="choice"]').forEach(choice => {
choice.checked = false; choice.checked = false;
}); });
// Select the clicked choice // Select the clicked choice
choiceInput.checked = true; choiceInput.checked = true;
// Reset the style of all choice buttons // Reset the style of all choice buttons
document.querySelectorAll('.choice-button').forEach((btn) => { document.querySelectorAll(".choice-button").forEach(btn => {
btn.classList.remove('bg-green-500', 'border-solid', 'border-2', 'border-black', 'hover:bg-green-600'); 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'); 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.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.add("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600");
const choiceText = button.textContent.trim(); const choiceText = button.textContent.trim();
document.getElementById('selected-choice').textContent = `You select: ${choiceText}`; 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');
} }
}; // 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

@ -16,9 +16,8 @@
</div> </div>
</div> </div>
</nav> </nav>
<!-- Vote Page Content --> <!-- Vote Page Content -->
<div class="container mx-auto p-4"> {% comment %} <div class="container mx-auto p-4">
<!-- Participant + UP DOWN zone --> <!-- Participant + UP DOWN zone -->
<div class="flex flex-wrap items-center text-gray-600 mb-4 place-content-center"> <div class="flex flex-wrap items-center text-gray-600 mb-4 place-content-center">
<div class="flex items-center text-black py-1 rounded-md mr-2"> <div class="flex items-center text-black py-1 rounded-md mr-2">
@ -33,10 +32,10 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div> {% endcomment %}
<!-- Modern Choice Selection --> <!-- Modern Choice Selection -->
<div class="bg-white p-4 rounded-lg shadow-md mb-4"> <div class="bg-neutral-100 p-4 rounded-lg mb-4">
<div class="relative"> <div class="relative">
<div class="rounded-lg bg-white p-4 shadow-md border-solid border-2 border-neutral-500 relative z-10"> <div class="rounded-lg bg-white p-4 shadow-md border-solid border-2 border-neutral-500 relative z-10">
<div class="bg-white p-4 rounded-lg shadow-md mb-4"> <div class="bg-white p-4 rounded-lg shadow-md mb-4">
@ -47,19 +46,29 @@
<div class="flex flex-col space-y-4"> <div class="flex flex-col space-y-4">
<form action="{% url 'polls:vote' question.id %}" method="post" class="poll-form" id="poll-form"> <form action="{% url 'polls:vote' question.id %}" method="post" class="poll-form" id="poll-form">
{% csrf_token %} {% csrf_token %}
<div class="bg-white p-4 rounded-lg shadow-md mb-4"> {% if selected_choice %}
<div id="selected-choice" class="mt-4 text-lg font-bold text-green-500">Please Select a Choice😊</div> <div class="bg-white p-4 rounded-lg shadow-md mb-4">
</div> <div id="selected-choice-1" class="mt-4 text-lg font-bold text-orange-500">You have been voted: {{ selected_choice.choice_text }}</div>
{% if error_message %} </div>
<div class="bg-red p-4 rounded-lg shadow-md mb-4"> {% else %}
<p class="error-message text-red-500"><strong>{{ error_message }}</strong></p> <div class="bg-white p-4 rounded-lg shadow-md mb-4">
</div> <div id="selected-choice-2" class="mt-4 text-lg font-bold text-green-500">Please Select a Choice😊</div>
</div>
{% endif %} {% endif %}
<div class="bg-white p-4 rounded-lg shadow-md mb-4"> <div class="bg-white p-4 rounded-lg shadow-md mb-4">
<div class="grid grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<!-- Buttons as choices (hidden) --> <!-- Buttons as choices (hidden) -->
{% for choice in question.choice_set.all %} {% for choice in question.choice_set.all %}
<label> <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" /> <input type="radio" name="choice" value="{{ choice.id }}" class="hidden" />
<button <button
type="button" type="button"
@ -67,11 +76,12 @@
onclick="toggleChoice(this, '{{ choice.id }}')"> onclick="toggleChoice(this, '{{ choice.id }}')">
{{ choice.choice_text }} {{ choice.choice_text }}
</button> </button>
{% endif %}
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<!-- Submit --> <!-- Submit -->
<div class="flex flex-row-reverse"> <div class="flex flex-row-reverse">
<a <a
@ -81,12 +91,9 @@
</a> </a>
<button <button
type="submit" type="submit"
class="bg-orange-400 text-white px-4 py-2 rounded-lg hover:bg-orange-600 transition-colors duration-300 hidden" name="vote-button"
id="vote-button"> class="bg-blue-500 text-white px-4 py-2 mx-5 rounded-lg hover:bg-blue-600 transition-colors duration-300"
Go Back id="vote-button" disabled>
</button>
<button
class="bg-blue-500 text-white px-4 py-2 mx-5 rounded-lg hover:bg-blue-600 transition-colors duration-300">
Vote Vote
</button> </button>
</div> </div>

View File

@ -26,15 +26,22 @@
</button> </button>
</form> </form>
<!--End--> <!--End-->
<button class="flex items-center whitespace-nowrap rounded-full border border-transparent bg-green-500 px-5 py-2 text-sm font-bold text-white transition duration-150 ease-in-out hover:scale-[101%] hover:bg-green-700 focus:bg-green-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-green-900">
New Poll
</button>
</div> </div>
</header> </header>
<a href="" {% if user.is_authenticated %}
class="flex items-center whitespace-nowrap rounded-full border border-transparent bg-neutral-800 px-5 py-2 text-sm font-bold text-white transition duration-150 ease-in-out hover:scale-[101%] hover:bg-neutral-700 focus:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-neutral-900"> <button class="flex items-center whitespace-nowrap rounded-full border border-transparent bg-green-500 px-5 py-2 text-sm font-bold text-white transition duration-150 ease-in-out hover:scale-[101%] hover:bg-green-700 focus:bg-green-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-green-900">
New Poll
</button>
<a href="{% url 'logout' %}"
class="flex items-center whitespace-nowrap rounded-full border border-transparent bg-red-600 px-5 py-2 text-sm font-bold text-white transition duration-150 ease-in-out hover:scale-[101%] hover:bg-red-700 focus:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-neutral-900">
<span>Sign out <span class="hidden sm:inline-block">😭</span></span>
</a>
{% else %}
<a href="{% url 'login' %}"
class="flex items-center whitespace-nowrap rounded-full border border-transparent bg-neutral-800 px-5 py-2 text-sm font-bold text-white transition duration-150 ease-in-out hover:scale-[101%] hover:bg-neutral-700 focus:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-red-900">
<span>Sign in <span class="hidden sm:inline-block">😎</span></span> <span>Sign in <span class="hidden sm:inline-block">😎</span></span>
</a> </a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -75,7 +82,7 @@
<div class="relative"> <div class="relative">
<!-- INFO --> <!-- 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"> <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">
<h2 class="mb-2 text-xl font-bold">{{ question.question_text }}</h2> <h2 class="mb-2 text-xl font-bold truncate">{{ question.question_text }}</h2>
<hr class="h-px my-2 bg-gray-200 border-0 dark:bg-gray-400" /> <hr class="h-px my-2 bg-gray-200 border-0 dark:bg-gray-400" />
<p class="mb-2 text-gray-600">{{ question.short_description }}</p> <p class="mb-2 text-gray-600">{{ question.short_description }}</p>
<div class="mb-2 flex items-center text-gray-600"> <div class="mb-2 flex items-center text-gray-600">
@ -88,7 +95,7 @@
<!-- Tag / Time --> <!-- Tag / Time -->
<div class="flex items-center text-gray-600"> <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-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>
<div class="flex items-center text-gray-600 py-4"> <div class="flex items-center text-gray-600 py-4">
<button <button
@ -121,7 +128,7 @@
<div class="relative"> <div class="relative">
<!-- INFO --> <!-- 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"> <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">
<h2 class="mb-2 text-xl font-semibold">{{ question.question_text }}</h2> <h2 class="mb-2 text-xl font-semibold truncate">{{ question.question_text }}</h2>
<hr class="h-px my-2 bg-gray-200 border-0 dark:bg-gray-400" /> <hr class="h-px my-2 bg-gray-200 border-0 dark:bg-gray-400" />
<p class="mb-2 text-gray-600">{{ question.short_description }}</p> <p class="mb-2 text-gray-600">{{ question.short_description }}</p>
<div class="mb-2 flex items-center text-gray-600"> <div class="mb-2 flex items-center text-gray-600">
@ -134,7 +141,7 @@
<!-- Tag / Time --> <!-- Tag / Time -->
<div class="flex items-center text-gray-600"> <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-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>
<div class="flex items-center text-gray-600 py-4"> <div class="flex items-center text-gray-600 py-4">
<button <button

View File

@ -25,20 +25,32 @@
</nav> </nav>
<!-- Result Page Content --> <!-- Result Page Content -->
<div class="container mx-auto p-4">
<div class="container mx-auto p-4 text-center">
{% for message in messages %}
{% if message.tags == 'success' %}
<div class="relative">
<div class="bg-white p-4 rounded-lg shadow-md mb-4 z-10 relative border-black border-solid border-2">
<p class="text-green-500 font-bold text-xl">{{ message }}</p>
</div>
<div
class="absolute inset-0 mt-1 ml-1 w-full rounded-lg border-2 border-neutral-700 bg-gradient-to-r from-green-400 to-blue-500 h-full"></div>
</div>
{% endif %}
{% endfor %}
<!-- Result Summary --> <!-- Result Summary -->
<div class="relative"> <div class="relative">
<div class="bg-white p-4 rounded-lg shadow-md mb-4 z-10 relative border-black border-solid border-2"> <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 %} {% for choice in question.choice_set.all %}
<div class="flex justify-between items-center mb-2"> <div class="flex justify-between items-center mb-2">
<span>{{ choice.choice_text }}</span> <span>{{ choice.choice_text }}</span>
<div class="flex items-center"> <div class="flex items-center">
<span class="mr-2">👍 {{ choice.votes }}</span> <span class="mr-2">👍 {{ choice.votes }}</span>
<div class="percentage-bar"> <div class="vote-bar">
<div class="bar bg-blue-500 h-2" style="width: {{ choice.calculate_percentage }}%;"></div> <div class="bar bg-blue-500 h-2" style="width: {{ choice.votes }}%;"></div>
</div> </div>
</div> </div>
</div> </div>
@ -56,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"> <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> <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"> <span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">
{{ question.participant_count }} Participants 👤 {{ question.participants }} Participants 👤
</span> </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.up_vote_percentage }}% </span>
<span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">👎 {{ question.down_vote_percentage }}% </span> <span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">👎 {{ question.down_vote_percentage }}% </span>
@ -105,8 +117,8 @@
data: { data: {
labels: [{% for choice in question.choice_set.all %}"{{ choice.choice_text }}",{% endfor %}], labels: [{% for choice in question.choice_set.all %}"{{ choice.choice_text }}",{% endfor %}],
datasets: [{ datasets: [{
label: 'Percentage', label: 'Vote Count',
data: [{% for choice in question.choice_set.all %}{{ choice.calculate_percentage }},{% endfor %}], data: [{% for choice in question.choice_set.all %}{{ choice.votes }},{% endfor %}],
backgroundColor: [ backgroundColor: [
'rgba(75, 192, 192, 0.2)', 'rgba(75, 192, 192, 0.2)',
'rgba(255, 99, 132, 0.2)', 'rgba(255, 99, 132, 0.2)',

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sign In Page</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div class="bg-white p-8 shadow-md rounded-md w-96">
<h2 class="text-3xl font-semibold text-center mb-6">Sign In</h2>
<form method="post">
{% csrf_token %}
<div class="mb-6 flex flex-col">
<p class="block text-gray-700 font-medium mb-2">Username</p>
{{ form.username }}
</div>
<div class="mb-6 flex flex-col">
<p class="block text-gray-700 font-medium mb-2">Password</p>
{{ form.password }}
</div>
<button
type="submit"
class="w-full bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600 focus:ring-2 focus:ring-blue-300 focus:outline-none">
Log In
</button>
</form>
<p class="text-center mt-4 text-gray-600 text-sm">
Don't have an account? <a href="{% url 'polls:signup' %}" class="text-blue-500 hover:underline">Sign up here</a>
</p>
<p class="text-center mt-4 text-gray-600 text-sm">
Forget the Password? <a href="{% url 'password_reset' %}" class="text-blue-500 hover:underline">Reset here</a>
</p>
<a href="{% url 'polls:index' %}" class="mt-4 block text-center text-blue-500 hover:underline">Back to Poll</a>
</div>
</body>
</html>

View File

@ -0,0 +1,2 @@
<h1>Password reset complete</h1>
<p>Your new password has been set. You can log in now on the <a href="{% url 'login' %}">log in page</a>.</p>

View File

@ -0,0 +1,14 @@
{% if validlink %}
<h1>Set a new password!</h1>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Change my password">
</form>
{% else %}
<p>The password reset link was invalid, possibly because it has already been used. Please request a new password reset.</p>
{% endif %}

View File

@ -0,0 +1,2 @@
<h1>Check your inbox.</h1>
<p>We've emailed you instructions for setting your password. You should receive the email shortly!</p>

View File

@ -0,0 +1,8 @@
<h1>Forgot your password?</h1>
<p>Enter your email address below, and we'll email instructions for setting a new one.</p>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Send me instructions!">
</form>

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign Up Page</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
<div class="min-h-screen flex items-center justify-center">
<div class="bg-white p-8 shadow-md rounded-md w-96">
<h2 class="text-2xl font-semibold mb-6">Sign Up</h2>
<form method="post">
{% csrf_token %}
<div class="mb-4">
<p class="block text-gray-700 font-medium">Username</p>
{{ form.username }}
</div>
<div class="mb-4">
<p class="block text-gray-700 font-medium">Password</p>
{{ form.password1 }}
<div class="mb-4">
<p class="block text-gray-700 font-medium">Password Confirmation</p>
{{ form.password2 }}
</div>
<button type="submit" class="w-full bg-green-500 text-white py-2 rounded-md hover:bg-green-600 focus:ring-2 focus:ring-green-300 focus:outline-none">Sign Up</button>
<!-- text form -->
<p class="mt-2 text-gray-600 text-sm">Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.</p>
<p class="mt-2 text-gray-600 text-sm">Your password cant be too similar to your other personal information. , must contain at least 8 characters, cant be entirely numeric.</p>
</form>
<div class="mt-2 text-gray-600 text-sm">
{% if form.errors %}
{% for field in form %}
{% if field.errors %}
{% for error in field.errors %}
<p class="text-red-500">{{ error }}</p>
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
</div>
<p class="mt-4 text-gray-600 text-sm">Already have an account? <a href="{% url 'login' %}" class="text-blue-500 hover:underline">Sign in here</a></p>
<a href="{% url 'polls:index' %}" class="mt-4 block text-center text-blue-500 hover:underline">Back to Poll</a>
</div>
</div>
</body>
</html>

View File

@ -3,6 +3,7 @@ import datetime
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from django.urls import reverse from django.urls import reverse
from django.contrib.auth.models import User
from .models import Question from .models import Question
@ -199,6 +200,9 @@ class QuestionDetailViewTests(TestCase):
future_question.pub_date = timezone.now() + timezone.timedelta(days=5) future_question.pub_date = timezone.now() + timezone.timedelta(days=5)
future_question.save() future_question.save()
user = User.objects.create_user(username="testcase", password="123test123")
self.client.login(username="testcase", password="123test123")
url = reverse("polls:detail", args=(future_question.id,)) url = reverse("polls:detail", args=(future_question.id,))
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@ -212,6 +216,9 @@ class QuestionDetailViewTests(TestCase):
past_question.pub_date = timezone.now() - timezone.timedelta(days=5) past_question.pub_date = timezone.now() - timezone.timedelta(days=5)
past_question.save() past_question.save()
user = User.objects.create_user(username="testcase", password="123test123")
self.client.login(username="testcase", password="123test123")
url = reverse("polls:detail", args=(past_question.id,)) url = reverse("polls:detail", args=(past_question.id,))
response = self.client.get(url) response = self.client.get(url)
self.assertContains(response, past_question.question_text) self.assertContains(response, past_question.question_text)

View File

@ -8,4 +8,5 @@ urlpatterns = [
path("<int:pk>/", views.DetailView.as_view(), name="detail"), path("<int:pk>/", views.DetailView.as_view(), name="detail"),
path("<int:pk>/results/", views.ResultsView.as_view(), name="results"), path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
path("<int:question_id>/vote/", views.vote, name="vote"), path("<int:question_id>/vote/", views.vote, name="vote"),
path("signup/", views.SignUpView.as_view(), name="signup"),
] ]

View File

@ -1,11 +1,16 @@
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse from django.urls import reverse
from django.views import generic from django.views import generic
from django.utils import timezone from django.utils import timezone
from django.views.generic import TemplateView 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 .models import Choice, Question from .forms import SignUpForm
from .models import Choice, Question, Vote
class IndexView(generic.ListView): class IndexView(generic.ListView):
@ -15,13 +20,16 @@ class IndexView(generic.ListView):
context_object_name = "latest_question_list" context_object_name = "latest_question_list"
def get_queryset(self): def get_queryset(self):
"""Return the last five published questions.""" """
return Question.objects.filter(pub_date__lte=timezone.now()).order_by( Return the last published questions that is published and haven't ended yet.
"-pub_date" """
)[:5] 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(generic.DetailView): class DetailView(LoginRequiredMixin, generic.DetailView):
""" """
Provide a view for detail page, a detail for each poll contain poll question Provide a view for detail page, a detail for each poll contain poll question
and poll choices. and poll choices.
@ -48,43 +56,68 @@ class DetailView(generic.DetailView):
context["end_date"] = question.end_date context["end_date"] = question.end_date
context["up_vote_count"] = question.up_vote_count context["up_vote_count"] = question.up_vote_count
context["down_vote_count"] = question.down_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 return context
class ResultsView(generic.DetailView): class ResultsView(LoginRequiredMixin, generic.DetailView):
model = Question model = Question
template_name = "polls/results.html" 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): def render_to_response(self, context, **response_kwargs):
return render(self.request, self.template_name, context) return render(self.request, self.template_name, context)
class SignUpView(generic.CreateView):
form_class = SignUpForm
success_url = reverse_lazy('login')
template_name = 'registration/signup.html'
@login_required
def vote(request, question_id): def vote(request, question_id):
""" """
A function that update the database. Add vote count to choice that user vote A function that updates the database. Adds a vote count to the choice that the user votes for
in specific question_id. in a specific question_id.
""" """
question = get_object_or_404(Question, pk=question_id) question = get_object_or_404(Question, pk=question_id)
try: try:
selected_choice = question.choice_set.get(pk=request.POST["choice"]) selected_choice = question.choice_set.get(pk=request.POST["choice"])
except (KeyError, Choice.DoesNotExist): except (KeyError, Choice.DoesNotExist):
return render( messages.error(request, "You didn't select a choice.")
request, return render(request, "polls/detail.html", {"question": question})
"polls/detail.html",
{
"question": question,
"error_message": "You didn't select a choice.",
},
)
else: else:
selected_choice.votes += 1 if question.can_vote():
selected_choice.save() if request.method == "POST" and "vote-button" in request.POST:
return HttpResponseRedirect(reverse("polls:results", args=(question.id,))) 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:
messages.success(request, "You vote successfully🥳")
Vote.objects.create(choice=selected_choice, user=request.user, question=question).save()
return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
else:
messages.error(request, "You cannot vote by typing the URL.")
return render(request, "polls/detail.html", {"question": question})
else:
messages.error(request, "You can not vote on this question.")
return HttpResponseRedirect(reverse("polls:index"))