├── mysite ├── static │ ├── site.js │ └── site.css ├── mysite │ ├── __init__.py │ ├── asgi.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── polls │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── static │ │ └── polls │ │ │ └── style.css │ ├── apps.py │ ├── urls.py │ ├── templates │ │ └── polls │ │ │ ├── results.html │ │ │ ├── index.html │ │ │ └── detail.html │ ├── admin.py │ ├── models.py │ ├── views.py │ └── tests.py ├── templates │ └── base.html └── manage.py ├── .gitignore ├── requirements.in ├── setup.cfg ├── .github └── workflows │ └── ci.yml ├── requirements.txt └── README.rst /mysite/static/site.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/mysite/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | mysite/db.sqlite3 2 | *.pyc 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /mysite/static/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #fff; 3 | font-family: sans-serif; 4 | } 5 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | # Project maintenance 2 | pip-tools 3 | black 4 | isort 5 | 6 | # Project dependencies 7 | django~=3.2 8 | 9 | -------------------------------------------------------------------------------- /mysite/polls/static/polls/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: white url("images/background.gif") no-repeat; 3 | } 4 | 5 | li a { 6 | color: green; 7 | } 8 | -------------------------------------------------------------------------------- /mysite/polls/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PollsConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "polls" 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E123,E128,E203,E501,W503 3 | max-line-length = 88 4 | exclude = .tox,.git 5 | 6 | [isort] 7 | multi_line_output = 3 8 | line_length = 88 9 | known_django = django 10 | known_first_party = fastview 11 | sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 12 | include_trailing_comma = True 13 | lines_after_imports = 2 14 | skip = .tox,.git 15 | -------------------------------------------------------------------------------- /mysite/polls/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | 6 | app_name = "polls" 7 | urlpatterns = [ 8 | path("", views.IndexView.as_view(), name="index"), 9 | path("/", views.DetailView.as_view(), name="detail"), 10 | path("/results/", views.ResultsView.as_view(), name="results"), 11 | path("/vote/", views.vote, name="vote"), 12 | ] 13 | -------------------------------------------------------------------------------- /mysite/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | Django tutorial 6 | 7 | 8 | {% block extrahead %}{% endblock %} 9 | 10 | 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | -------------------------------------------------------------------------------- /mysite/polls/templates/polls/results.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

{{ question.question_text }}

6 | 7 |
    8 | {% for choice in question.choice_set.all %} 9 |
  • {{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}
  • 10 | {% endfor %} 11 |
12 | 13 | Vote again? 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /mysite/mysite/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for mysite project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 16 | 17 | application = get_asgi_application() 18 | -------------------------------------------------------------------------------- /mysite/mysite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for mysite project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /mysite/polls/templates/polls/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block extrahead %} 5 | 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 | 11 | {% if latest_question_list %} 12 | 17 | {% else %} 18 |

No polls are available.

19 | {% endif %} 20 | 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /mysite/polls/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Choice, Question 4 | 5 | 6 | class ChoiceInline(admin.TabularInline): 7 | model = Choice 8 | extra = 3 9 | 10 | 11 | class QuestionAdmin(admin.ModelAdmin): 12 | list_display = ("question_text", "pub_date", "was_published_recently") 13 | list_filter = ["pub_date"] 14 | search_fields = ["question_text"] 15 | fieldsets = [ 16 | (None, {"fields": ["question_text"]}), 17 | ("Date information", {"fields": ["pub_date"], "classes": ["collapse"]}), 18 | ] 19 | inlines = [ChoiceInline] 20 | 21 | 22 | admin.site.register(Question, QuestionAdmin) 23 | -------------------------------------------------------------------------------- /mysite/polls/templates/polls/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 | {% csrf_token %} 7 |
8 |

{{ question.question_text }}

9 | {% if error_message %}

{{ error_message }}

{% endif %} 10 | {% for choice in question.choice_set.all %} 11 | 12 |
13 | {% endfor %} 14 |
15 | 16 |
17 | 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test-sqlite: 9 | name: py-${{ matrix.python }} 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | include: 14 | - python: "3.9" 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python ${{ matrix.python }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python }} 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | - name: Test 26 | run: | 27 | cd mysite 28 | ./manage.py test 29 | -------------------------------------------------------------------------------- /mysite/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /mysite/polls/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.contrib import admin 4 | from django.db import models 5 | from django.utils import timezone 6 | 7 | 8 | class Question(models.Model): 9 | question_text = models.CharField(max_length=200) 10 | pub_date = models.DateTimeField("date published") 11 | 12 | def __str__(self): 13 | return self.question_text 14 | 15 | @admin.display(boolean=True, ordering="pub_date", description="Published recently?") 16 | def was_published_recently(self): 17 | now = timezone.now() 18 | return now - datetime.timedelta(days=1) <= self.pub_date <= now 19 | 20 | 21 | class Choice(models.Model): 22 | question = models.ForeignKey(Question, on_delete=models.CASCADE) 23 | choice_text = models.CharField(max_length=200) 24 | votes = models.IntegerField(default=0) 25 | 26 | def __str__(self): 27 | return self.choice_text 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.9 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | asgiref==3.4.1 8 | # via django 9 | black==21.12b0 10 | # via -r requirements.in 11 | click==8.0.3 12 | # via 13 | # black 14 | # pip-tools 15 | django==3.2.11 16 | # via -r requirements.in 17 | isort==5.10.1 18 | # via -r requirements.in 19 | mypy-extensions==0.4.3 20 | # via black 21 | pathspec==0.9.0 22 | # via black 23 | pep517==0.12.0 24 | # via pip-tools 25 | pip-tools==6.4.0 26 | # via -r requirements.in 27 | platformdirs==2.4.1 28 | # via black 29 | pytz==2021.3 30 | # via django 31 | sqlparse==0.4.2 32 | # via django 33 | tomli==1.2.3 34 | # via 35 | # black 36 | # pep517 37 | typing-extensions==4.0.1 38 | # via black 39 | wheel==0.37.1 40 | # via pip-tools 41 | 42 | # The following packages are considered to be unsafe in a requirements file: 43 | # pip 44 | # setuptools 45 | -------------------------------------------------------------------------------- /mysite/mysite/urls.py: -------------------------------------------------------------------------------- 1 | """mysite URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | from django.views.generic import RedirectView 19 | 20 | 21 | urlpatterns = [ 22 | path("polls/", include("polls.urls")), 23 | path("admin/", admin.site.urls), 24 | path("", RedirectView.as_view(pattern_name="polls:index", permanent=False)), 25 | ] 26 | -------------------------------------------------------------------------------- /mysite/polls/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.11 on 2022-01-15 06:40 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="Question", 16 | fields=[ 17 | ( 18 | "id", 19 | models.BigAutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("question_text", models.CharField(max_length=200)), 27 | ("pub_date", models.DateTimeField(verbose_name="date published")), 28 | ], 29 | ), 30 | migrations.CreateModel( 31 | name="Choice", 32 | fields=[ 33 | ( 34 | "id", 35 | models.BigAutoField( 36 | auto_created=True, 37 | primary_key=True, 38 | serialize=False, 39 | verbose_name="ID", 40 | ), 41 | ), 42 | ("choice_text", models.CharField(max_length=200)), 43 | ("votes", models.IntegerField(default=0)), 44 | ( 45 | "question", 46 | models.ForeignKey( 47 | on_delete=django.db.models.deletion.CASCADE, to="polls.question" 48 | ), 49 | ), 50 | ], 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /mysite/polls/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect 2 | from django.shortcuts import get_object_or_404, render 3 | from django.urls import reverse 4 | from django.utils import timezone 5 | from django.views import generic 6 | 7 | from .models import Choice, Question 8 | 9 | 10 | class IndexView(generic.ListView): 11 | template_name = "polls/index.html" 12 | context_object_name = "latest_question_list" 13 | 14 | def get_queryset(self): 15 | """Return the last five published questions.""" 16 | 17 | return Question.objects.filter(pub_date__lte=timezone.now()).order_by( 18 | "-pub_date" 19 | )[:5] 20 | 21 | 22 | class DetailView(generic.DetailView): 23 | model = Question 24 | template_name = "polls/detail.html" 25 | 26 | def get_queryset(self): 27 | """ 28 | Excludes any questions that aren't published yet. 29 | """ 30 | return Question.objects.filter(pub_date__lte=timezone.now()) 31 | 32 | 33 | class ResultsView(generic.DetailView): 34 | model = Question 35 | template_name = "polls/results.html" 36 | 37 | 38 | def vote(request, question_id): 39 | question = get_object_or_404(Question, pk=question_id) 40 | try: 41 | selected_choice = question.choice_set.get(pk=request.POST["choice"]) 42 | except (KeyError, Choice.DoesNotExist): 43 | # Redisplay the question voting form. 44 | return render( 45 | request, 46 | "polls/detail.html", 47 | {"question": question, "error_message": "You didn't select a choice."}, 48 | ) 49 | else: 50 | selected_choice.votes += 1 51 | selected_choice.save() 52 | # Always return an HttpResponseRedirect after successfully dealing 53 | # with POST data. This prevents data from being posted twice if a 54 | # user hits the Back button. 55 | return HttpResponseRedirect(reverse("polls:results", args=(question.id,))) 56 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | django-tutorial-starter 3 | ======================= 4 | 5 | This is a functional implementation of the `official Django tutorial`__. It should only 6 | be used for experimentation and demonstration purposes, it is unsuitable for use as a 7 | base for production sites. 8 | 9 | __ https://docs.djangoproject.com/en/dev/intro/tutorial01/ 10 | 11 | 12 | Running this project 13 | ==================== 14 | 15 | To get this project running:: 16 | 17 | # Create and activate a virtualenv using your favourite method 18 | python -mvenv venv 19 | . venv/bin/activate 20 | 21 | # Clone and install requirements 22 | git clone https://github.com/radiac/django-tutorial-starter.git tutorial 23 | cd tutorial 24 | pip install -r requirements.txt 25 | 26 | # Run 27 | ./manage.py migrate 28 | ./manage.py createsuperuser 29 | ./manage.py runserver 0:8000 30 | 31 | 32 | In this project 33 | =============== 34 | 35 | The project aims to follow the official tutorial as closely as practical, while 36 | tweaking things enough to be useful. 37 | 38 | There are the following changes: 39 | 40 | * Project requirements are defined in ``requirements.in`` and pinned to 41 | ``requirements.txt`` using `pip-tools `_. 42 | * All ``polls`` templates have been wrapped in ``{% block content %}``, and extend 43 | ``mysite/templates/base.html``, which includes a placeholder static ``site.css`` and 44 | ``site.js``. 45 | * No background image in ``polls/static/polls/style.css``. 46 | * No admin template override (no file at ``templates/admin/base_site.html``). 47 | * Root url ``/`` redirects to ``/polls/`` for convenience. 48 | * Minor changes may be introduced by ``black`` and ``isort``. 49 | 50 | We will aim to keep this repository up-to-date with the latest LTS release of Django. 51 | 52 | 53 | Why does this exist? 54 | ==================== 55 | 56 | This repository is intended as a starting point for tutorials of third party apps. 57 | 58 | Given its prominence, most Django developers will already be familiar with the official 59 | tutorial, or at least the features it uses. Developers can point their users at this 60 | repo to give them a tutorial foundation which is simple enough to be easy to understand, 61 | but just complex enough to be well-suited to build into more advanced concepts. 62 | 63 | It also serves as a good template for building an example project in your own repo. 64 | 65 | 66 | Used by 67 | ======= 68 | 69 | This project is currently used by: 70 | 71 | * `django-fastview `_ - 72 | `Tutorial `_, 73 | `Example project `_ 74 | 75 | 76 | Changelog 77 | ========= 78 | 79 | 2022-01-15 80 | ---------- 81 | 82 | * Initial release using Django 3.2 and associated tutorial 83 | -------------------------------------------------------------------------------- /mysite/mysite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for mysite project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.11. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = "secret_key" 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = ["*"] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | "django.contrib.admin", 36 | "django.contrib.auth", 37 | "django.contrib.contenttypes", 38 | "django.contrib.sessions", 39 | "django.contrib.messages", 40 | "django.contrib.staticfiles", 41 | "polls.apps.PollsConfig", 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | "django.middleware.security.SecurityMiddleware", 46 | "django.contrib.sessions.middleware.SessionMiddleware", 47 | "django.middleware.common.CommonMiddleware", 48 | "django.middleware.csrf.CsrfViewMiddleware", 49 | "django.contrib.auth.middleware.AuthenticationMiddleware", 50 | "django.contrib.messages.middleware.MessageMiddleware", 51 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 52 | ] 53 | 54 | ROOT_URLCONF = "mysite.urls" 55 | 56 | TEMPLATES = [ 57 | { 58 | "BACKEND": "django.template.backends.django.DjangoTemplates", 59 | "DIRS": [BASE_DIR / "templates"], 60 | "APP_DIRS": True, 61 | "OPTIONS": { 62 | "context_processors": [ 63 | "django.template.context_processors.debug", 64 | "django.template.context_processors.request", 65 | "django.contrib.auth.context_processors.auth", 66 | "django.contrib.messages.context_processors.messages", 67 | ] 68 | }, 69 | } 70 | ] 71 | 72 | WSGI_APPLICATION = "mysite.wsgi.application" 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 77 | 78 | DATABASES = { 79 | "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3"} 80 | } 81 | 82 | 83 | # Password validation 84 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 85 | 86 | AUTH_PASSWORD_VALIDATORS = [ 87 | { 88 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 89 | }, 90 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 91 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 92 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 93 | ] 94 | 95 | 96 | # Internationalization 97 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 98 | 99 | LANGUAGE_CODE = "en-us" 100 | 101 | TIME_ZONE = "UTC" 102 | 103 | USE_I18N = True 104 | 105 | USE_L10N = True 106 | 107 | USE_TZ = True 108 | 109 | 110 | # Static files (CSS, JavaScript, Images) 111 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 112 | 113 | STATIC_URL = "/static/" 114 | 115 | STATICFILES_DIRS = [ 116 | BASE_DIR / "static", 117 | ] 118 | 119 | # Default primary key field type 120 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 121 | 122 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 123 | -------------------------------------------------------------------------------- /mysite/polls/tests.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.test import TestCase 4 | from django.urls import reverse 5 | from django.utils import timezone 6 | 7 | from .models import Question 8 | 9 | 10 | def create_question(question_text, days): 11 | """ 12 | Create a question with the given `question_text` and published the 13 | given number of `days` offset to now (negative for questions published 14 | in the past, positive for questions that have yet to be published). 15 | """ 16 | time = timezone.now() + datetime.timedelta(days=days) 17 | return Question.objects.create(question_text=question_text, pub_date=time) 18 | 19 | 20 | class QuestionModelTests(TestCase): 21 | def test_was_published_recently_with_future_question(self): 22 | """ 23 | was_published_recently() returns False for questions whose pub_date 24 | is in the future. 25 | """ 26 | time = timezone.now() + datetime.timedelta(days=30) 27 | future_question = Question(pub_date=time) 28 | self.assertIs(future_question.was_published_recently(), False) 29 | 30 | def test_was_published_recently_with_old_question(self): 31 | """ 32 | was_published_recently() returns False for questions whose pub_date 33 | is older than 1 day. 34 | """ 35 | time = timezone.now() - datetime.timedelta(days=1, seconds=1) 36 | old_question = Question(pub_date=time) 37 | self.assertIs(old_question.was_published_recently(), False) 38 | 39 | def test_was_published_recently_with_recent_question(self): 40 | """ 41 | was_published_recently() returns True for questions whose pub_date 42 | is within the last day. 43 | """ 44 | time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) 45 | recent_question = Question(pub_date=time) 46 | self.assertIs(recent_question.was_published_recently(), True) 47 | 48 | 49 | class QuestionIndexViewTests(TestCase): 50 | def test_no_questions(self): 51 | """ 52 | If no questions exist, an appropriate message is displayed. 53 | """ 54 | response = self.client.get(reverse("polls:index")) 55 | self.assertEqual(response.status_code, 200) 56 | self.assertContains(response, "No polls are available.") 57 | self.assertQuerysetEqual(response.context["latest_question_list"], []) 58 | 59 | def test_past_question(self): 60 | """ 61 | Questions with a pub_date in the past are displayed on the 62 | index page. 63 | """ 64 | question = create_question(question_text="Past question.", days=-30) 65 | response = self.client.get(reverse("polls:index")) 66 | self.assertQuerysetEqual(response.context["latest_question_list"], [question]) 67 | 68 | def test_future_question(self): 69 | """ 70 | Questions with a pub_date in the future aren't displayed on 71 | the index page. 72 | """ 73 | create_question(question_text="Future question.", days=30) 74 | response = self.client.get(reverse("polls:index")) 75 | self.assertContains(response, "No polls are available.") 76 | self.assertQuerysetEqual(response.context["latest_question_list"], []) 77 | 78 | def test_future_question_and_past_question(self): 79 | """ 80 | Even if both past and future questions exist, only past questions 81 | are displayed. 82 | """ 83 | question = create_question(question_text="Past question.", days=-30) 84 | create_question(question_text="Future question.", days=30) 85 | response = self.client.get(reverse("polls:index")) 86 | self.assertQuerysetEqual(response.context["latest_question_list"], [question]) 87 | 88 | def test_two_past_questions(self): 89 | """ 90 | The questions index page may display multiple questions. 91 | """ 92 | question1 = create_question(question_text="Past question 1.", days=-30) 93 | question2 = create_question(question_text="Past question 2.", days=-5) 94 | response = self.client.get(reverse("polls:index")) 95 | self.assertQuerysetEqual( 96 | response.context["latest_question_list"], [question2, question1] 97 | ) 98 | 99 | 100 | class QuestionDetailViewTests(TestCase): 101 | def test_future_question(self): 102 | """ 103 | The detail view of a question with a pub_date in the future 104 | returns a 404 not found. 105 | """ 106 | future_question = create_question(question_text="Future question.", days=5) 107 | url = reverse("polls:detail", args=(future_question.id,)) 108 | response = self.client.get(url) 109 | self.assertEqual(response.status_code, 404) 110 | 111 | def test_past_question(self): 112 | """ 113 | The detail view of a question with a pub_date in the past 114 | displays the question's text. 115 | """ 116 | past_question = create_question(question_text="Past Question.", days=-5) 117 | url = reverse("polls:detail", args=(past_question.id,)) 118 | response = self.client.get(url) 119 | self.assertContains(response, past_question.question_text) 120 | --------------------------------------------------------------------------------