Merge Iteration 1 progress into main branch Pull request #8

Merge Iteration 1 into main branch. Finish Iteration 1
This commit is contained in:
Sirin Puenggun 2023-08-28 23:13:31 +07:00 committed by GitHub
commit 00aa53196e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 703 additions and 29 deletions

2
.gitignore vendored
View File

@ -58,7 +58,7 @@ cover/
# Django stuff:
*.log
local_settings.py
db.sqlite3
# db.sqlite3
db.sqlite3-journal
# Flask stuff:

BIN
db.sqlite3 Normal file

Binary file not shown.

View File

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from pathlib import Path
from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@ -20,7 +21,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-ij$9jv9o30j(51=l)ho^l+x&q9)n77i1vt%j()%9=(ohh(b*!^'
SECRET_KEY = config('SECRET_KEY', default='fake-secret-key')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@ -31,6 +32,7 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
"polls.apps.PollsConfig",
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@ -53,15 +55,15 @@ ROOT_URLCONF = 'mysite.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
@ -115,7 +117,8 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR]
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

View File

@ -1,22 +1,10 @@
"""
URL configuration for mysite project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import include, path
from polls.views import HomeView
urlpatterns = [
path('', HomeView.as_view(), name='home'),
path("polls/", include("polls.urls")),
path('admin/', admin.site.urls),
]

0
polls/__init__.py Normal file
View File

23
polls/admin.py Normal file
View File

@ -0,0 +1,23 @@
from django.contrib import admin
from .models import Choice, Question
class ChoiceInline(admin.TabularInline):
model = Choice
extra = 3
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ["question_text"]}),
("Date information", {"fields": ["pub_date"], "classes": ["collapse"]}),
]
list_display = ["question_text", "pub_date", "was_published_recently"]
inlines = [ChoiceInline]
list_filter = ["pub_date"]
search_fields = ["question_text"]
admin.site.register(Question, QuestionAdmin)

7
polls/apps.py Normal file
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class PollsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'polls'

View File

@ -0,0 +1,32 @@
# Generated by Django 4.2.4 on 2023-08-28 11:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Question',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question_text', models.CharField(max_length=200)),
('pub_date', models.DateTimeField(verbose_name='date published')),
],
),
migrations.CreateModel(
name='Choice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('choice_text', models.CharField(max_length=200)),
('votes', models.IntegerField(default=0)),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.question')),
],
),
]

View File

76
polls/models.py Normal file
View File

@ -0,0 +1,76 @@
"""
This module defines the models for the polls app.
It includes the Question and Choice models, which represent poll questions
and the choices associated with them. These models are used to store and
get poll data in the database.
Attributes:
None
"""
import datetime
from django.db import models
from django.utils import timezone
from django.contrib import admin
class Question(models.Model):
"""
Represents a poll question.
Attributes:
question_text (str): The text of the poll question.
pub_date (datetime): The date and time when the question was published.
"""
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")
def was_published_recently(self):
"""
Checks if the question was published recently or not.
Returns:
bool: True if the question was published within the last day, else False.
"""
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
@admin.display(
boolean=True,
ordering="pub_date",
description="Published recently?",
)
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
def __str__(self):
"""
Returns a string representation of the question.
"""
return self.question_text
class Choice(models.Model):
"""
Represents a choice for a poll question.
Attributes:
question (Question): The poll question to which the choice belongs.
choice_text (str): The text of the choice.
votes (int): The number of votes the choice has received.
"""
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
def __str__(self):
"""
Returns a string representation of the choice.
"""
return self.choice_text

191
polls/static/polls/base.css Normal file
View File

@ -0,0 +1,191 @@
/*! NAVBAR */
header {
background-color: #1C1C1C;
color: #fff;
padding: 20px;
text-align: center;
display: flex;
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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

View File

@ -0,0 +1,12 @@
{% extends "admin/base.html" %}
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block branding %}
<div id="site-name"><a href="{% url 'admin:index' %}">Polls Administration Page</a></div>
{% if user.is_anonymous %}
{% include "admin/color_theme_toggle.html" %}
{% endif %}
{% endblock %}
{% block nav-global %}{% endblock %}

View File

@ -0,0 +1,25 @@
{% load static %}
<html>
<head>
<title>MySite Polls</title>
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400" rel="stylesheet">
<link rel='stylesheet' href='{% static 'polls/base.css' %}'>
</head>
<body>
<div>
<header>
<div class='nav-left'>
<h1><a href={% url 'home' %}>MySite Polls | </a></h1>
</div>
<div class='nav-right'>
<ul>
<li><a href="{% url 'home' %}">Home</a></li>
<li><a href="{% url 'polls:index' %}">View Polls</a></li>
</ul>
</div>
</header>
{% block content %}
{% endblock content %}
</div>
</body>
</html>

View File

@ -0,0 +1,24 @@
{% extends 'polls/base.html' %}
{% block content %}
<main>
<section class="poll-details">
<form action="{% url 'polls:vote' question.id %}" method="post" class="poll-form">
{% csrf_token %}
<fieldset>
<legend class="poll-question">{{ question.question_text }}</legend>
{% if error_message %}
<p class="error-message"><strong>{{ error_message }}</strong></p>
{% endif %}
{% for choice in question.choice_set.all %}
<div class="choice">
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
<label for="choice{{ forloop.counter }}" class="choice-text">{{ choice.choice_text }}</label>
</div>
{% endfor %}
</fieldset>
<button type="submit" class="vote-button">Vote</button>
</form>
</section>
</main>
{% endblock content %}

View File

@ -0,0 +1,26 @@
{% 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_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

@ -0,0 +1,19 @@
{% extends 'polls/base.html' %}
{% block content %}
<section class="polls-section">
<h2>Recent Polls</h2>
<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>
<p class="publication-date">Published on: {{ question.pub_date|date:"F j, Y" }}</p>
</div>
{% endfor %}
{% else %}
<p>No polls are available.</p>
{% endif %}
</div>
</section>
{% endblock content %}

View File

@ -0,0 +1,17 @@
{% extends 'polls/base.html' %}
{% block content %}
<main>
<section class="poll-results">
<div class="poll-header">
<h1 class="poll-question">{{ question.question_text }}</h1>
</div>
<ul class="choice-list">
{% for choice in question.choice_set.all %}
<li class="choice-item">{{ choice.choice_text }} — {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>
<a href="{% url 'polls:detail' question.id %}" class="vote-again">Vote again?</a>
</section>
</main>
{% endblock content %}

129
polls/tests.py Normal file
View File

@ -0,0 +1,129 @@
import datetime
from django.test import TestCase
from django.utils import timezone
from django.urls import reverse
from .models import Question
class QuestionModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is in the future.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is older than 1 day.
"""
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Question(pub_date=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() returns True for questions whose pub_date
is within the last day.
"""
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
recent_question = Question(pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)
def create_question(question_text, days):
"""
Create a question with the given `question_text` and published the
given number of `days` offset to now (negative for questions published
in the past, positive for questions that have yet to be published).
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)
class QuestionIndexViewTests(TestCase):
def test_no_questions(self):
"""
If no questions exist, an appropriate message is displayed.
"""
response = self.client.get(reverse("polls:index"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_past_question(self):
"""
Questions with a pub_date in the past are displayed on the
index page.
"""
question = create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question],
)
def test_future_question(self):
"""
Questions with a pub_date in the future aren't displayed on
the index page.
"""
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse("polls:index"))
self.assertContains(response, "No polls are available.")
self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
are displayed.
"""
question = create_question(question_text="Past question.", days=-30)
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question],
)
def test_two_past_questions(self):
"""
The questions index page may display multiple questions.
"""
question1 = create_question(question_text="Past question 1.", days=-30)
question2 = create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question2, question1],
)
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
"""
The detail view of a question with a pub_date in the future
returns a 404 not found.
"""
future_question = create_question(question_text="Future question.", days=5)
url = reverse("polls:detail", args=(future_question.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_past_question(self):
"""
The detail view of a question with a pub_date in the past
displays the question's text.
"""
past_question = create_question(question_text="Past Question.", days=-5)
url = reverse("polls:detail", args=(past_question.id,))
response = self.client.get(url)
self.assertContains(response, past_question.question_text)

12
polls/urls.py Normal file
View File

@ -0,0 +1,12 @@
from django.urls import path
from . import views
app_name = "polls"
urlpatterns = [
path("", views.IndexView.as_view(), name="index"),
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"),
]

90
polls/views.py Normal file
View File

@ -0,0 +1,90 @@
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic
from django.utils import timezone
from django.views.generic import TemplateView
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)
context['latest_question_list'] = Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[:5]
context['total_polls'] = Question.objects.count()
return context
class IndexView(generic.ListView):
"""
Provide a view for Index page that list all polls.
"""
template_name = "polls/index.html"
context_object_name = "latest_question_list"
def get_queryset(self):
"""
Return the last five published questions (not including those set to be
published in the future).
"""
return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[
:5
]
class DetailView(generic.DetailView):
"""
Provide a view for detail page, a detail for each poll contain poll question
and poll choices.
"""
model = Question
template_name = "polls/detail.html"
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
class ResultsView(generic.DetailView):
"""
Provide a view for result page that show up when user submit on of the choices.
"""
model = Question
template_name = "polls/results.html"
def vote(request, question_id):
"""
A function that update the database. Add vote count to choice that user vote
in 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.",
},
)
else:
selected_choice.votes += 1
selected_choice.save()
return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))

Binary file not shown.