mirror of
https://github.com/Sosokker/ku-polls.git
synced 2025-12-18 21:14:05 +01:00
Merge Iteration 1 progress into main branch Pull request #8
Merge Iteration 1 into main branch. Finish Iteration 1
This commit is contained in:
commit
00aa53196e
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
BIN
db.sqlite3
Normal file
Binary file not shown.
@ -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
|
||||
|
||||
@ -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
0
polls/__init__.py
Normal file
23
polls/admin.py
Normal file
23
polls/admin.py
Normal 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
7
polls/apps.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PollsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'polls'
|
||||
|
||||
32
polls/migrations/0001_initial.py
Normal file
32
polls/migrations/0001_initial.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
polls/migrations/__init__.py
Normal file
0
polls/migrations/__init__.py
Normal file
76
polls/models.py
Normal file
76
polls/models.py
Normal 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
191
polls/static/polls/base.css
Normal 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;
|
||||
}
|
||||
BIN
polls/static/polls/images/background.jpg
Normal file
BIN
polls/static/polls/images/background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
0
polls/static/polls/style.css
Normal file
0
polls/static/polls/style.css
Normal file
12
polls/templates/admin/base_site.html
Normal file
12
polls/templates/admin/base_site.html
Normal 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 %}
|
||||
25
polls/templates/polls/base.html
Normal file
25
polls/templates/polls/base.html
Normal 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>
|
||||
24
polls/templates/polls/detail.html
Normal file
24
polls/templates/polls/detail.html
Normal 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 %}
|
||||
26
polls/templates/polls/home.html
Normal file
26
polls/templates/polls/home.html
Normal 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 %}
|
||||
19
polls/templates/polls/index.html
Normal file
19
polls/templates/polls/index.html
Normal 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 %}
|
||||
17
polls/templates/polls/results.html
Normal file
17
polls/templates/polls/results.html
Normal 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
129
polls/tests.py
Normal 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
12
polls/urls.py
Normal 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
90
polls/views.py
Normal 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,)))
|
||||
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Loading…
Reference in New Issue
Block a user