Refine UI/UX - Add new Feature (Model) - Refine View

1. Refine UI/UX
Apply Tailwinds
Rewrite all page

2. Add new feature to model
Add new field
- description - long - short
- use default + editable instead of auto_add_now
- track up vote
- Change timezone
- Add time left track
-Add participant
-Add up/down vote attribute

-- Choice
- Add total_vote
-Add calculate_percentage of vote

3. Refine View
-Redirect instead of home page
-Add context to result, detail view
-Apply ChartJS
-Delete home
This commit is contained in:
sosokker 2023-09-09 21:20:23 +07:00
parent f33dbdf1d5
commit 1b996629da
18 changed files with 694 additions and 330 deletions

View File

@ -4,7 +4,13 @@
"pk": 1, "pk": 1,
"fields": { "fields": {
"question_text": "Python vs C++, which one is better in your opinion?", "question_text": "Python vs C++, which one is better in your opinion?",
"pub_date": "2023-08-28T13:38:42Z" "short_description": "Cool kids have polls",
"long_description": "No description provide for this poll.",
"pub_date": "2023-08-28T13:38:42Z",
"end_date": "2025-09-24T22:56:52Z",
"up_vote_count": 0,
"down_vote_count": 0,
"participant_count": 0
} }
}, },
{ {
@ -12,7 +18,55 @@
"pk": 2, "pk": 2,
"fields": { "fields": {
"question_text": "The chicken and the egg, which came first?", "question_text": "The chicken and the egg, which came first?",
"pub_date": "2023-08-28T13:39:16Z" "short_description": "Cool kids have polls",
"long_description": "No description provide for this poll.",
"pub_date": "2023-08-28T13:39:16Z",
"end_date": "2023-11-05T06:30:16Z",
"up_vote_count": 12,
"down_vote_count": 4,
"participant_count": 11
}
},
{
"model": "polls.question",
"pk": 3,
"fields": {
"question_text": "Do you love Django?",
"short_description": "Cool kids have polls",
"long_description": "No description provide for this poll.",
"pub_date": "2023-11-07T15:40:31Z",
"end_date": "2023-09-20T13:41:33Z",
"up_vote_count": 3000,
"down_vote_count": 50,
"participant_count": 555
}
},
{
"model": "polls.question",
"pk": 4,
"fields": {
"question_text": "So far so good?",
"short_description": "Cool kids have polls",
"long_description": "No description provide for this poll.",
"pub_date": "2023-09-09T06:30:50.690Z",
"end_date": "2023-09-30T12:54:30Z",
"up_vote_count": 0,
"down_vote_count": 0,
"participant_count": 234
}
},
{
"model": "polls.question",
"pk": 5,
"fields": {
"question_text": "Do you love Django?",
"short_description": "Cool kids have polls",
"long_description": "No description provide for this poll.",
"pub_date": "2023-09-09T13:42:27.501Z",
"end_date": "2023-09-21T13:42:17Z",
"up_vote_count": 1,
"down_vote_count": 1,
"participant_count": 1
} }
}, },
{ {
@ -21,7 +75,7 @@
"fields": { "fields": {
"question": 2, "question": 2,
"choice_text": "Chicken", "choice_text": "Chicken",
"votes": 5 "votes": 55
} }
}, },
{ {
@ -30,7 +84,7 @@
"fields": { "fields": {
"question": 2, "question": 2,
"choice_text": "Egg", "choice_text": "Egg",
"votes": 2 "votes": 56
} }
}, },
{ {
@ -39,7 +93,7 @@
"fields": { "fields": {
"question": 1, "question": 1,
"choice_text": "Python!", "choice_text": "Python!",
"votes": 2 "votes": 6
} }
}, },
{ {
@ -48,6 +102,78 @@
"fields": { "fields": {
"question": 1, "question": 1,
"choice_text": "C++", "choice_text": "C++",
"votes": 3
}
},
{
"model": "polls.choice",
"pk": 8,
"fields": {
"question": 3,
"choice_text": "Yeah for sure!",
"votes": 0
}
},
{
"model": "polls.choice",
"pk": 9,
"fields": {
"question": 3,
"choice_text": "nah",
"votes": 0
}
},
{
"model": "polls.choice",
"pk": 13,
"fields": {
"question": 4,
"choice_text": "Yes!",
"votes": 5
}
},
{
"model": "polls.choice",
"pk": 14,
"fields": {
"question": 4,
"choice_text": "No!",
"votes": 4
}
},
{
"model": "polls.choice",
"pk": 15,
"fields": {
"question": 4,
"choice_text": "Okay!",
"votes": 5
}
},
{
"model": "polls.choice",
"pk": 16,
"fields": {
"question": 4,
"choice_text": "Thank you!",
"votes": 2
}
},
{
"model": "polls.choice",
"pk": 17,
"fields": {
"question": 5,
"choice_text": "Yeah for sure!",
"votes": 2
}
},
{
"model": "polls.choice",
"pk": 18,
"fields": {
"question": 5,
"choice_text": "nah",
"votes": 2 "votes": 2
} }
} }

View File

@ -26,10 +26,15 @@ SECRET_KEY = config('SECRET_KEY', default='k2pd1p)zwe0qy0k25=sli+7+n^vd-0h*&6vga
#! SECURITY WARNING: don't run with debug turned on in production! #! SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool) DEBUG = config('DEBUG', default=False, cast=bool)
LANGUAGE_CODE = 'en-us'
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv()) ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv())
TIME_ZONE = config('TIME_ZONE', default='Asia/Bangkok', cast=str) TIME_ZONE = config('TIME_ZONE', default='UTC', cast=str)
USE_I18N = True
USE_TZ = True
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [

View File

@ -1,10 +1,10 @@
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from polls.views import HomeView from django.views.generic import RedirectView
urlpatterns = [ urlpatterns = [
path('', HomeView.as_view(), name='home'), 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),
] ]

View File

@ -12,6 +12,8 @@ class QuestionAdmin(admin.ModelAdmin):
fieldsets = [ fieldsets = [
(None, {"fields": ["question_text"]}), (None, {"fields": ["question_text"]}),
("End date", {"fields": ["end_date"], "classes": ["collapse"]}), ("End date", {"fields": ["end_date"], "classes": ["collapse"]}),
("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"]
inlines = [ChoiceInline] inlines = [ChoiceInline]

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2.4 on 2023-09-09 05:27
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('polls', '0002_question_end_date_alter_question_pub_date'),
]
operations = [
migrations.AlterField(
model_name='choice',
name='votes',
field=models.PositiveIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2147483647)]),
),
migrations.AlterField(
model_name='question',
name='pub_date',
field=models.DateTimeField(auto_now_add=True, verbose_name='date published'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.4 on 2023-09-09 08:15
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('polls', '0003_alter_choice_votes_alter_question_pub_date'),
]
operations = [
migrations.AlterField(
model_name='question',
name='pub_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='date published'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 4.2.4 on 2023-09-09 08:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('polls', '0004_alter_question_pub_date'),
]
operations = [
migrations.AddField(
model_name='question',
name='long_description',
field=models.TextField(default='No description provide for this poll.', max_length=2000),
),
migrations.AddField(
model_name='question',
name='short_description',
field=models.CharField(default='Cool kids have polls', max_length=200),
),
migrations.AlterField(
model_name='question',
name='question_text',
field=models.CharField(max_length=100),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2.4 on 2023-09-09 08:33
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('polls', '0005_question_long_description_question_short_description_and_more'),
]
operations = [
migrations.AddField(
model_name='question',
name='down_vote_count',
field=models.PositiveIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2147483647)]),
),
migrations.AddField(
model_name='question',
name='up_vote_count',
field=models.PositiveIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2147483647)]),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.4 on 2023-09-09 08:36
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('polls', '0006_question_down_vote_count_question_up_vote_count'),
]
operations = [
migrations.AddField(
model_name='question',
name='participant_count',
field=models.PositiveIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2147483647)]),
),
]

View File

@ -9,12 +9,11 @@ Attributes:
None None
""" """
import datetime
from django.db import models from django.db import models
from django.utils import timezone 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
class Question(models.Model): class Question(models.Model):
@ -26,9 +25,14 @@ class Question(models.Model):
pub_date (datetime): The date and time when the question was published. pub_date (datetime): The date and time when the question was published.
""" """
question_text = models.CharField(max_length=200) question_text = models.CharField(max_length=100)
pub_date = models.DateTimeField("date published", auto_now_add=True) short_description = models.CharField(max_length=200, default="Cool kids have polls")
long_description = models.TextField(max_length=2000, default="No description provide for this poll.")
pub_date = models.DateTimeField("date published", default=timezone.now, editable=False)
end_date = models.DateTimeField("date ended", null=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)])
def was_published_recently(self): def was_published_recently(self):
""" """
@ -38,7 +42,7 @@ class Question(models.Model):
bool: True if the question was published within the last day, else False. bool: True if the question was published within the last day, else False.
""" """
now = timezone.now() now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now return now - timezone.timedelta(days=1) <= self.pub_date <= now
@admin.display( @admin.display(
boolean=True, boolean=True,
@ -47,7 +51,7 @@ class Question(models.Model):
) )
def was_published_recently(self): def was_published_recently(self):
now = timezone.now() now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now return now - timezone.timedelta(days=1) <= self.pub_date <= now
def __str__(self): def __str__(self):
""" """
@ -78,6 +82,57 @@ class Question(models.Model):
else: else:
return self.pub_date <= now <= self.end_date return self.pub_date <= now <= self.end_date
def calculate_time_left(self):
"""
Calculate the time left until the end date.
Returns:
str: A formatted string representing the time left.
"""
if self.end_date is None:
return "No end date"
now = timezone.now()
time_left = self.end_date - now
days, seconds = divmod(time_left.total_seconds(), 86400)
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
time_left_str = ""
if days > 0:
time_left_str += f"{int(days)} D "
elif hours > 0:
time_left_str += f"{int(hours)} H "
elif minutes > 0:
time_left_str += f"{int(minutes)} M "
elif seconds > 0:
time_left_str += f"{int(seconds)} S "
return time_left_str.strip()
@property
def time_left(self):
return self.calculate_time_left()
def calculate_vote_percentage(self):
total_vote = self.up_vote_count + self.down_vote_count
if total_vote == 0:
return (0, 0)
up_vote_percentage = self.up_vote_count / total_vote * 100
down_vote_percentage = self.down_vote_count / total_vote * 100
return (int(up_vote_percentage), int(down_vote_percentage))
@property
def up_vote_percentage(self):
return self.calculate_vote_percentage()[0]
@property
def down_vote_percentage(self):
return self.calculate_vote_percentage()[1]
class Choice(models.Model): class Choice(models.Model):
""" """
@ -93,6 +148,29 @@ class Choice(models.Model):
choice_text = models.CharField(max_length=200) choice_text = models.CharField(max_length=200)
votes = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)]) 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):
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.

View File

@ -1,191 +1,9 @@
/*! NAVBAR */ /*! NAVBAR */
header { @font-face {
background-color: #1C1C1C; font-family: 'Open Sans';
color: #fff; font-style: normal;
padding: 20px; font-weight: 700;
text-align: center; src: local('Open Sans Extrabold'), local('OpenSans-Extrabold'),
display: flex; url('https://fonts.googleapis.com/css2?family=Kanit&family=Open+Sans&family=Roboto:ital@1&display=swap') format('truetype');
justify-content: center; }
align-items: center;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
border-radius: 10px;
}
.nav-container {
display: flex;
justify-content: center;
align-items: center;
}
.nav-left h1 a {
text-decoration: none;
color: #fff;
font-size: 24px;
}
.nav-right ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
align-items: center;
}
.nav-right li {
margin: 0 10px;
}
.nav-right a {
text-decoration: none;
color: #fff;
font-weight: bold;
padding: 10px 20px;
border: 2px solid #fff;
border-radius: 5px;
transition: background-color 0.3s ease, color 0.3s ease;
}
.nav-right a:hover {
background-color: #fff;
color: #007bff;
}
/*! HOME AND POLL CARD */
.hero-section {
background-size: cover;
background-position: center;
text-align: center;
color: #1c1c1c;
}
.hero-content {
max-width: 800px;
margin: 0 auto;
}
h1 {
font-size: 36px;
margin-bottom: 20px;
}
.polls-section {
background-size: cover;
background-position: center;
text-align: center;
color: #1c1c1c;
}
.poll-cards {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
margin-top: 30px;
}
.poll-card {
background-color: #fff;
border: 1px solid #e0e0e0;
padding: 20px;
border-radius: 5px;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
cursor: pointer;
}
.poll-card:hover {
transform: translateY(-5px);
}
/*! DETAILED */
.poll-details {
padding: 30px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
}
.poll-form {
text-align: center;
}
.poll-question {
font-size: 24px;
margin-bottom: 20px;
}
.error-message {
color: red;
margin-bottom: 10px;
}
.choice {
display: flex;
align-items: center;
margin: 10px 0;
}
.choice input[type="radio"] {
margin-right: 10px;
}
.choice-text {
font-size: 18px;
}
.vote-button {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease, color 0.3s ease;
}
.vote-button:hover {
background-color: #0056b3;
}
/*! RESULT */
.poll-results {
text-align: center;
padding: 30px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
}
.poll-question {
font-size: 24px;
margin-bottom: 20px;
}
.choice-list {
list-style: none;
padding: 0;
margin: 20px 0;
text-align: left;
}
.choice-item {
font-size: 18px;
margin: 10px 0;
}
.vote-again {
display: inline-block;
margin-top: 20px;
text-decoration: none;
color: #007bff;
transition: color 0.3s ease;
}
.vote-again:hover {
color: #0056b3;
}

View File

@ -1,25 +1,17 @@
{% load static %} {% load static %}
<html>
<head> <!DOCTYPE html>
<title>MySite Polls</title> <html lang="en">
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400" rel="stylesheet"> <head>
<link rel='stylesheet' href='{% static 'polls/base.css' %}'> <meta charset="UTF-8">
</head> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<body> <script src="https://cdn.tailwindcss.com"></script>
<div> <script src="https://unpkg.com/htmx.org@1.7.0/dist/htmx.js"></script>
<header> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<div class='nav-left'> <title>Your Poll Website</title>
<h1><a href={% url 'home' %}>MySite Polls | </a></h1> </head>
</div> <body class="bg-gray-100">
<div class='nav-right'> {% block content %}
<ul> {% endblock content %}
<li><a href="{% url 'home' %}">Home</a></li> </body>
<li><a href="{% url 'polls:index' %}">View Polls</a></li> </html>
</ul>
</div>
</header>
{% block content %}
{% endblock content %}
</div>
</body>
</html>

View File

@ -2,23 +2,109 @@
{% block content %} {% block content %}
<main> <main>
<section class="poll-details"> <!-- Vote Page Content -->
<form action="{% url 'polls:vote' question.id %}" method="post" class="poll-form"> <div class="container mx-auto p-4">
{% csrf_token %} <h1 class="text-3xl font-semibold mb-2">{{ question_text }}</h1>
<fieldset> <hr class="h-px my-4 bg-gray-200 border-0 dark:bg-gray-700">
<legend class="poll-question">{{ question.question_text }}</legend> <p class="text-gray-600 mb-4 hover:bg-amber-100 text-black px-2 py-1 rounded-md mr-2">{{ long_description }}</p>
{% if error_message %}
<p class="error-message"><strong>{{ error_message }}</strong></p> <!-- Tags and Stats Section (tag) to be add -->
{% endif %} <div class="flex flex-wrap items-center text-gray-600 mb-4">
{% for choice in question.choice_set.all %} <div class="flex items-center bg-orange-100 text-black px-2 py-1 rounded-md mr-2">
<div class="choice"> <span class="mr-2">👤 {{ participant_count }} Participants</span>
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}"> <span class="mr-2">👍 {{ question.up_vote_percentage }}% Upvoted</span>
<label for="choice{{ forloop.counter }}" class="choice-text">{{ choice.choice_text }}</label> <span>👎 {{ question.down_vote_percentage }}% Downvoted</span>
</div>
</div>
<!-- Modern Choice Selection -->
<div class="bg-white p-4 rounded-lg shadow-md mb-4 ">
<h2 class="text-xl font-semibold mb-4">Which one would you prefer:</h2>
<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> </div>
{% endfor %} {% if error_message %}
</fieldset> <div class="bg-red p-4 rounded-lg shadow-md mb-4 ">
<button type="submit" class="vote-button">Vote</button> <p class="error-message text-red-500"><strong>{{ error_message }}</strong></p>
</form> </div
</section> {% 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>
<input type="radio" name="choice" value="{{ choice.id }}" class="hidden" />
<button
type="button"
class="choice-button bg-blue-500 hover:bg-blue-600 text-white 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>
</label>
{% endfor %}
</div>
</div>
<!-- Submit -->
<div class="flex flex-row-reverse">
<a href="{% url 'polls:index' %}" class="bg-orange-400 text-white px-4 py-2 rounded-lg hover:bg-orange-500 transition-colors duration-300">
Back to Polls
</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">Vote</button>
</div>
</form>
</div>
</div>
</div>
</main> </main>
<script>
const toggleChoice = (button, choiceId) => {
const choiceInput = document.querySelector(`input[name="choice"][value="${choiceId}"]`);
if (choiceInput) {
// already selected -> unselect it
if (choiceInput.checked) {
choiceInput.checked = false;
button.classList.remove('bg-green-500', 'border-solid', 'border-2', 'border-green-500', 'hover:bg-green-600');
button.classList.add('bg-blue-500', 'hover:bg-blue-600');
// 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;
});
// 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-green-500', 'hover:bg-green-600');
btn.classList.add('bg-blue-500', 'hover:bg-blue-600');
});
button.classList.remove('bg-blue-500', 'hover:bg-blue-600');
button.classList.add('bg-green-500', 'border-solid', 'border-2', 'border-green-500', 'hover:bg-green-600');
const choiceText = button.textContent.trim();
document.getElementById('selected-choice').textContent = `You selected: ${choiceText}`;
}
// Enable the "Vote" button -> if select
const voteButton = document.getElementById('vote-button');
voteButton.disabled = !document.querySelector('input[name="choice"]:checked');
}
};
</script>
{% endblock content %} {% endblock content %}

View File

@ -1,26 +0,0 @@
{% extends 'polls/base.html' %}
{% block content %}
<main>
<section class="hero-section">
<div class="hero-content">
<h1>Welcome to KU Polls</h1>
<p>Explore and participate in our weird poll questions.</p>
</div>
</section>
<section class="polls-section">
<h2>Recent Polls</h2>
<p class="total-polls">Total number of polls: {{ total_open_polls }}</p>
<div class="poll-cards">
{% if latest_question_list %}
{% for question in latest_question_list %}
<div class="poll-card">
<h3><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></h3>
</div>
{% endfor %}
{% else %}
<p>No polls are available.</p>
{% endif %}
</div>
</section>
{% endblock content %}

View File

@ -1,19 +1,97 @@
{% extends 'polls/base.html' %} {% extends 'polls/base.html' %}
{% block content %} {% block content %}
<section class="polls-section"> <main>
<h2>Recent Polls</h2> <!-- Navbar -->
<div class="poll-cards"> <nav class="bg-blue-500 p-4">
{% if latest_question_list %} <div class="container mx-auto flex items-center justify-between">
{% for question in latest_question_list %} <div class="text-2xl font-semibold text-white">KU POLL</div>
<div class="poll-card">
<h3><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></h3> <!-- Button -->
<p class="publication-date">Published on: {{ question.pub_date|date:"F j, Y" }}</p> <div>
</div> {% comment %} <button class="mr-4 rounded-md bg-green-500 px-4 py-2 text-white">New Poll</button> {% endcomment %}
{% endfor %} <button class="text-white">Sign In</button>
{% else %} </div>
<p>No polls are available.</p> </div>
{% endif %} </nav>
<!-- Main Content -->
<div class="container mx-auto p-4">
<header class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-semibold">Explore Polls</h1>
<button class="rounded-md bg-green-500 px-4 py-2 text-white">New Poll</button>
</header>
<!-- Filter Section -->
<div class="mb-4">
<input type="text" placeholder="Search for polls..." class="w-full rounded-md border border-gray-300 px-4 py-2" />
</div>
<!-- Trends Polls Section -->
<section class="mb-6">
<h2 class="mb-4 text-2xl font-semibold">Trending</h2>
<div class="bg-white p-4 rounded-lg shadow-md mb-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{% for question in latest_question_list %}
<a class="rounded-lg bg-white p-4 shadow-md">
<h2 class="mb-2 text-xl font-semibold">{{ 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">
<span class="mr-2">👍</span>
<span>{{ question.up_vote_percentage }}% Upvoted</span>
<span class="ml-4 mr-2">👎</span>
<span>{{ question.down_vote_percentage }}% Downvoted</span>
</div>
<!-- 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>
</div>
<div class="flex items-center text-gray-600 py-4">
<button onclick="window.location.href='{% url 'polls:detail' question.id %}'" class="mr-2 rounded-md bg-white px-2 py-1 text-black border-solid border-2 border-black hover:bg-gray-500 transform translate-y-0 hover:translate-y-1 transition-transform">VOTE</button>
<button onclick="window.location.href='{% url 'polls:results' question.id %}'" class="mr-2 rounded-md bg-white px-2 py-1 text-black border-solid border-2 border-black hover:bg-gray-500 transform translate-y-0 hover:translate-y-1 transition-transform">VIEW</button>
</div>
</a>
{% endfor %}
</div>
</div>
</section>
<!-- Poll Cards Section -->
<section>
<h2 class="mb-4 text-2xl font-semibold">Polls</h2>
<div class="bg-white p-4 rounded-lg shadow-md mb-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3">
{% for question in latest_question_list %}
<a href="{% url 'polls:detail' question.id %}" class="rounded-lg bg-white p-4 shadow-md">
<h2 class="mb-2 text-xl font-semibold">{{ question.question_text }}</h2>
<p class="mb-2 text-gray-600">{{ question.short_description }}</p>
<div class="mb-2 flex items-center text-gray-600">
<span class="mr-2">👍</span>
<span>{{ question.up_vote_percentage }}% Upvoted</span>
<span class="ml-4 mr-2">👎</span>
<span>{{ question.down_vote_percentage }}% Downvoted</span>
</div>
<!-- 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>
</div>
<div class="flex items-center text-gray-600 py-4">
<button onclick="window.location.href='{% url 'polls:detail' question.id %}'" class="mr-2 rounded-md bg-white px-2 py-1 text-black border-solid border-2 border-black hover:bg-gray-500 transform translate-y-0 hover:translate-y-1 transition-transform">VOTE</button>
<button onclick="window.location.href='{% url 'polls:results' question.id %}'" class="mr-2 rounded-md bg-white px-2 py-1 text-black border-solid border-2 border-black hover:bg-gray-500 transform translate-y-0 hover:translate-y-1 transition-transform">VIEW</button>
</div>
</a>
{% endfor %}
</div>
</div>
</section>
</div> </div>
</section> </main>
{% endblock content %} {% endblock content %}

View File

@ -2,16 +2,116 @@
{% block content %} {% block content %}
<main> <main>
<section class="poll-results"> <!-- Result Page Content -->
<div class="poll-header"> <div class="container mx-auto p-4">
<h1 class="poll-question">{{ question.question_text }}</h1> <h1 class="text-3xl font-semibold mb-4">{{ question.question_text }}</h1>
</div> <hr class="h-px my-4 bg-gray-200 border-0 dark:bg-gray-700">
<ul class="choice-list">
<!-- Result Summary -->
<div class="bg-white p-4 rounded-lg shadow-md mb-4">
<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 %}
<li class="choice-item">{{ choice.choice_text }} — {{ choice.votes }} vote{{ choice.votes|pluralize }}</li> <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>
</div>
</div>
{% endfor %} {% endfor %}
</ul> </div>
<a href="{% url 'polls:detail' question.id %}" class="vote-again">Vote again?</a>
</section> <!-- Result Page Content -->
<div class="container mx-auto grid grid-cols-3 gap-4">
<!-- Statistics -->
<div class="col-span-1 bg-white p-4 rounded-lg shadow-md mb-4">
<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 👤</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>
</div>
<!-- Pie Chart -->
<div class="col-span-1 bg-white p-4 rounded-lg shadow-md mb-4">
<h2 class="text-xl font-semibold mb-2">👋 Vote Percentage</h2>
<div class="w-full h-48 bg-gray-100 rounded-lg">
<canvas id="percentageChart"></canvas>
</div>
</div>
<!-- Bar Chart -->
<div class="col-span-1 bg-white p-4 rounded-lg shadow-md mb-4">
<h2 class="text-xl font-semibold mb-2">👏 Vote Count</h2>
<div class="w-full h-48 bg-gray-100 rounded-lg">
<canvas id="voteCountChart"></canvas>
</div>
</div>
</div>
<!-- Back to Polls Button -->
<a href="{% url 'polls:index' %}" class="bg-orange-400 text-white px-4 py-2 rounded-lg hover:bg-orange-500 transition-colors duration-300">
Back to Polls
</a>
</div>
</main> </main>
{% endblock content %}
<script>
var percentageCtx = document.getElementById('percentageChart').getContext('2d');
var percentageChart = new Chart(percentageCtx, {
type: 'pie',
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 %}],
backgroundColor: [
'rgba(75, 192, 192, 0.2)',
'rgba(255, 99, 132, 0.2)',
],
borderColor: [
'rgba(75, 192, 192, 1)',
'rgba(255, 99, 132, 1)',
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
}
});
var voteCountCtx = document.getElementById('voteCountChart').getContext('2d');
var voteCountChart = new Chart(voteCountCtx, {
type: 'bar',
data: {
labels: [{% for choice in question.choice_set.all %}"{{ choice.choice_text }}",{% endfor %}],
datasets: [{
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)',
],
borderColor: [
'rgba(75, 192, 192, 1)',
'rgba(255, 99, 132, 1)',
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
</script>
{% endblock content %}

View File

@ -124,7 +124,6 @@ class QuestionIndexViewTests(TestCase):
""" """
response = self.client.get(reverse("polls:index")) response = self.client.get(reverse("polls:index"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerySetEqual(response.context["latest_question_list"], []) self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_past_question(self): def test_past_question(self):
@ -150,7 +149,6 @@ class QuestionIndexViewTests(TestCase):
future_question.pub_date = timezone.now() + timezone.timedelta(days=30) future_question.pub_date = timezone.now() + timezone.timedelta(days=30)
future_question.save() future_question.save()
response = self.client.get(reverse("polls:index")) response = self.client.get(reverse("polls:index"))
self.assertContains(response, "No polls are available.")
self.assertQuerySetEqual(response.context["latest_question_list"], []) self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_future_question_and_past_question(self): def test_future_question_and_past_question(self):

View File

@ -8,42 +8,15 @@ from django.views.generic import TemplateView
from .models import Choice, Question from .models import Choice, Question
class HomeView(TemplateView):
"""
Provide a view for Home page(first page).
"""
template_name = 'polls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
all_questions = Question.objects.all()
#* Check if the question is published and can be voted. Then, sort by pub_date
published_questions = [q for q in all_questions if q.is_published() and q.can_vote()]
latest_published_questions = sorted(published_questions, key=lambda q: q.pub_date, reverse=True)[:5]
context['latest_question_list'] = latest_published_questions
context['total_open_polls'] = sum(1 for q in published_questions if q.end_date is None)
context['total_polls'] = all_questions.count()
return context
class IndexView(generic.ListView): class IndexView(generic.ListView):
""" """View for index.html."""
Provide a view for Index page that list all polls.
"""
template_name = "polls/index.html" template_name = "polls/index.html"
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 the last five published questions (not including those set to be return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[:5]
published in the future).
"""
return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[
:5
]
class DetailView(generic.DetailView): class DetailView(generic.DetailView):
@ -61,15 +34,35 @@ class DetailView(generic.DetailView):
""" """
return Question.objects.filter(pub_date__lte=timezone.now()) return Question.objects.filter(pub_date__lte=timezone.now())
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
question = self.object
context['question_text'] = question.question_text
context['short_description'] = question.short_description
context['long_description'] = question.long_description
context['pub_date'] = question.pub_date
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
return context
class ResultsView(generic.DetailView): class ResultsView(generic.DetailView):
"""
Provide a view for result page that show up when user submit on of the choices.
"""
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):
return render(self.request, self.template_name, context)
def vote(request, question_id): def vote(request, question_id):
""" """