mirror of
https://github.com/Sosokker/ku-polls.git
synced 2025-12-18 13:04:05 +01:00
Merge pull request #33 from Sosokker/iteration3
Iteration3 Merge model domain approach + Authetication System
This commit is contained in:
commit
0aa1d7d213
@ -75,6 +75,15 @@ python -m virtualenv .venv
|
||||
|:--:|:--:|
|
||||
|admin|ineedmorebullets|
|
||||
|
||||
## Demo User
|
||||
|
||||
|Username|Password|
|
||||
|:--:|:--:|
|
||||
|tester1|aa12345678aa|
|
||||
|tester2|aa12345678aa|
|
||||
|tester3|aa12345678aa|
|
||||
|tester4|aa12345678aa|
|
||||
|
||||
## Project Documents
|
||||
|
||||
All project documents are in the [Project Wiki](https://github.com/Sosokker/ku-polls/wiki).
|
||||
|
||||
242
data/polls.json
Normal file
242
data/polls.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -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
92
data/users.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -126,3 +126,9 @@ STATICFILES_DIRS = [BASE_DIR]
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
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"
|
||||
@ -7,4 +7,5 @@ urlpatterns = [
|
||||
path('', RedirectView.as_view(pattern_name='polls:index'), name='home_redirect'),
|
||||
path("polls/", include("polls.urls")),
|
||||
path('admin/', admin.site.urls),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
]
|
||||
|
||||
@ -13,12 +13,11 @@ 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"]}),
|
||||
("Participant count", {"fields": ["participant_count"]}),
|
||||
("Sentiment Vote count", {"fields": ["up_vote_count", "down_vote_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"]
|
||||
|
||||
|
||||
|
||||
27
polls/forms.py
Normal file
27
polls/forms.py
Normal 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.",
|
||||
}
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -14,6 +14,17 @@ from django.utils import timezone
|
||||
from django.contrib import admin
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
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):
|
||||
@ -32,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):
|
||||
"""
|
||||
@ -150,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):
|
||||
"""
|
||||
@ -163,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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,9 +16,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Vote Page Content -->
|
||||
<div class="container mx-auto p-4">
|
||||
{% comment %} <div class="container mx-auto p-4">
|
||||
<!-- Participant + UP DOWN zone -->
|
||||
<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">
|
||||
@ -33,10 +32,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
|
||||
<!-- 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="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">
|
||||
@ -47,19 +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 error_message %}
|
||||
<div class="bg-red p-4 rounded-lg shadow-md mb-4">
|
||||
<p class="error-message text-red-500"><strong>{{ error_message }}</strong></p>
|
||||
</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"
|
||||
@ -67,11 +76,12 @@
|
||||
onclick="toggleChoice(this, '{{ choice.id }}')">
|
||||
{{ choice.choice_text }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Submit -->
|
||||
<div class="flex flex-row-reverse">
|
||||
<a
|
||||
@ -81,12 +91,9 @@
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-orange-400 text-white px-4 py-2 rounded-lg hover:bg-orange-600 transition-colors duration-300 hidden"
|
||||
id="vote-button">
|
||||
Go Back
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-500 text-white px-4 py-2 mx-5 rounded-lg hover:bg-blue-600 transition-colors duration-300">
|
||||
name="vote-button"
|
||||
class="bg-blue-500 text-white px-4 py-2 mx-5 rounded-lg hover:bg-blue-600 transition-colors duration-300"
|
||||
id="vote-button" disabled>
|
||||
Vote
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -26,15 +26,22 @@
|
||||
</button>
|
||||
</form>
|
||||
<!--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>
|
||||
</header>
|
||||
<a href=""
|
||||
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">
|
||||
{% if user.is_authenticated %}
|
||||
<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>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -75,7 +82,7 @@
|
||||
<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">
|
||||
<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" />
|
||||
<p class="mb-2 text-gray-600">{{ question.short_description }}</p>
|
||||
<div class="mb-2 flex items-center text-gray-600">
|
||||
@ -88,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
|
||||
@ -121,7 +128,7 @@
|
||||
<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">
|
||||
<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" />
|
||||
<p class="mb-2 text-gray-600">{{ question.short_description }}</p>
|
||||
<div class="mb-2 flex items-center text-gray-600">
|
||||
@ -134,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
|
||||
|
||||
@ -25,20 +25,32 @@
|
||||
</nav>
|
||||
|
||||
<!-- 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 -->
|
||||
<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>
|
||||
@ -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">
|
||||
<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>
|
||||
@ -105,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)',
|
||||
|
||||
37
polls/templates/registration/login.html
Normal file
37
polls/templates/registration/login.html
Normal 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>
|
||||
@ -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>
|
||||
14
polls/templates/registration/password_reset_confirm.html
Normal file
14
polls/templates/registration/password_reset_confirm.html
Normal 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 %}
|
||||
2
polls/templates/registration/password_reset_done.html
Normal file
2
polls/templates/registration/password_reset_done.html
Normal 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>
|
||||
8
polls/templates/registration/password_reset_form.html
Normal file
8
polls/templates/registration/password_reset_form.html
Normal 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>
|
||||
46
polls/templates/registration/signup.html
Normal file
46
polls/templates/registration/signup.html
Normal 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 can’t be too similar to your other personal information. , must contain at least 8 characters, can’t 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>
|
||||
@ -3,6 +3,7 @@ import datetime
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from .models import Question
|
||||
|
||||
@ -199,6 +200,9 @@ class QuestionDetailViewTests(TestCase):
|
||||
future_question.pub_date = timezone.now() + timezone.timedelta(days=5)
|
||||
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,))
|
||||
response = self.client.get(url)
|
||||
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.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,))
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, past_question.question_text)
|
||||
|
||||
@ -8,4 +8,5 @@ urlpatterns = [
|
||||
path("<int:pk>/", views.DetailView.as_view(), name="detail"),
|
||||
path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
|
||||
path("<int:question_id>/vote/", views.vote, name="vote"),
|
||||
path("signup/", views.SignUpView.as_view(), name="signup"),
|
||||
]
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
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.views import generic
|
||||
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):
|
||||
@ -15,13 +20,16 @@ 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(generic.DetailView):
|
||||
class DetailView(LoginRequiredMixin, generic.DetailView):
|
||||
"""
|
||||
Provide a view for detail page, a detail for each poll contain poll question
|
||||
and poll choices.
|
||||
@ -48,43 +56,68 @@ class DetailView(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
|
||||
|
||||
|
||||
class ResultsView(generic.DetailView):
|
||||
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)
|
||||
|
||||
|
||||
class SignUpView(generic.CreateView):
|
||||
form_class = SignUpForm
|
||||
success_url = reverse_lazy('login')
|
||||
template_name = 'registration/signup.html'
|
||||
|
||||
@login_required
|
||||
def vote(request, question_id):
|
||||
"""
|
||||
A function that update the database. Add vote count to choice that user vote
|
||||
in specific question_id.
|
||||
A function that updates the database. Adds a vote count to the choice that the user votes for
|
||||
in a specific question_id.
|
||||
"""
|
||||
|
||||
question = get_object_or_404(Question, pk=question_id)
|
||||
try:
|
||||
selected_choice = question.choice_set.get(pk=request.POST["choice"])
|
||||
except (KeyError, Choice.DoesNotExist):
|
||||
return render(
|
||||
request,
|
||||
"polls/detail.html",
|
||||
{
|
||||
"question": question,
|
||||
"error_message": "You didn't select a choice.",
|
||||
},
|
||||
)
|
||||
messages.error(request, "You didn't select a choice.")
|
||||
return render(request, "polls/detail.html", {"question": question})
|
||||
|
||||
else:
|
||||
selected_choice.votes += 1
|
||||
selected_choice.save()
|
||||
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:
|
||||
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"))
|
||||
Loading…
Reference in New Issue
Block a user