This full text is the 5% of the total django documentation. It covers the beginner friendly guide to build the first django app that. This guide was broken into 7 parts. The same has been done with this readme Django manual that I created for myself.
- The first Django App again - Polls
- [Part 1] Setup
- Part 2 - Database and model setting up
- Database setup
- Setting up Models
- Add models
Question
andChoice
- Activate models
- Playing with the API
- Info: Add
__Str__()
to all models to replace Django's automatically-generated admin - add a custom method
was_published_recently
to modelQuestion
- Info: we can pass to
Model.objects.somefunct
something likepub_date__year
to access year or functions that are part of its data_type,pub_date
for ex - Info: we can access by
[Question Object]q.choice_set
, key being_set
, because Django creates a set to hold the "other side" of a ForeignKey relation, which can be access bychoice_set
- Info: Add
- Add models
- Django Admin
- [Part 3] Views
- Writing more views
- How does this requesting work?
- The Proper View
- Each view is responsible for two things:
HttpResponse
&Http404
- Range of choices offered by the view:
- Add templates to
polls
- Info: Here is the directory
DjangoTemplates
wants:polls/templates/polls/index.html
. to access just dopolls/index.html
. [important] - Info: Instead of using
django.template.render
, we can use usedjango.shortcuts.render
to render the template - Info:
render()
syntax:render(request, template_file, context)
- Each view is responsible for two things:
- Raising a 404 error
- Remove hard coded urls
- How to change url of polls
detail
view? - Namespacing URL names
- [Part 4] - Form Processing and Generic Views
- [Part 5] Setting up automated tests for the app
- [Part 6] Add support for static files: Stylesheet and Image
- [Part 7] Customizing the automatically-generated admin site
-
Info
- create aModelAmin
class and pass it toregister("here", [])
to change admin options for a model- Separate the fields to different sections where necessary
- Add the related objects
- Customize the admin change list
- Customize the admin look and feel
- Customize the project's templates
Info
-DIRS
is a list of filesystem directories to check when loading DJango templates; it's a search pathInfo
- Django source files can be located by usingpython -c "import django; print(django.__path__)"
Info
- Any of Django's default admin templates can be overrridden. To override a template, copy it to the/templates/
directory where themanage.py
is located
- Customizing the application's templates
- Customize the admin index page
- Customize the project's templates
- Advanced Tutorial: How to write reusable apps
-
Info
- In part 1, we decoupled polls from the project-level URLConf using an include
python -m venv venv
To install django in the virtual environment
python -m pip install django
python manage.py runserver
To specify port number
python manage.py runserver 8080
To change the server's IP, for ex, to listen on all available public IPs
python manage.py runserver 0.0.0.0:8000
\
in command prompt
to activate
venv\Scripts\activate
to deactivate
venv\Scripts\deactivate.bat // just deactivate in cmd
django-admin startproject mysite
There is no need to create folder, reason is below shown by folder structure
django-admin startproject mysite .
The following get created
mysite/
manage.py
mysite/
__init__.py
settings.py
urls.py
asgi.py
wsgi.py
- mysite/ root directory is a container for the project. it's name doesn't matter
- mysite/ inner directory is the actual Python package for the project. Refer to this by
mysite.urls
mysite/__init__.py
- the directory containing this file is treated as packagesmysite/asgi.py
andmysite/wsgi.py
- are entry points for ASGI-compatible and WSGI-compatible web servers.
Packages are a way of structureing Python's module namespace by using "dotted module names". More
Go to diretory containing manage.py
python manage.py startapp polls
# polls/views.py
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, world, You're at the index of poll")
to map the view, we need to map it to a URL - and for this we need a URLconf
# polls/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index')
]
import this urls file in mysite/urls.py
# mysite/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('polls/', include("polls.urls"))
path("admin/", admin.site.urls),
]
include()
function allows referencing other URLconfs
- when to use it? always use it when including other URL patterns.
- exept for
admin.site.urls
- exept for
To test visit: http://localhost:8000/polls/
The path()
function is passed four arguments, two required: route
and view
and two optional: kwargs
, name
.
- route is the string that contains a URL pattern
- starts from the first patter in the urlpatters and works down the list
- specifies the view function with an
HttpRequest
object as the first argument - and any 'captured' values from the route.
- arbitrary keyword arguments can be passed in a dictionary to the target view.
- naming lets us refer to the URL elsewhere in Django,
- especially necessary for template files
- by touching one singe file, global changes to URL patters can be made
-
python comes with SQLite support and is included in Django
-
to use another daztabse, install appropriate database bindings and change the following in
DATABASES 'default'
:ENGINE
-django.db.backends.sqlite
,django.db.backends.mysql
, ordjango.db.backends.oracle
NAME
- default isdb.sqlite3
-
if not using SQLite as the database, additional settings such as
USER
,PASSWORD
, andHOST
must be added.
IF using a database besides SQLite, create a database before moving to next steps
- Do `CREATE DATABASE database_name;` within the database's prompt
- set up
TIME_ZONE
when in settings.py/
django.contrib.admin
- the admin site. you'll use it shortlydjango.contrib.auth
- An authentication systemdjango.contrib.contenttypes
- a framework for content typesdjango.contrib.sessions
- a session frameworkdjango.contrib.messages
- A messaging frameworkdjango.contrib.staticfiles
- A framework for managing static files
- the following looks at the
installed_apps
and creates any necessary database tables.
python manage.py migrate
\dt
(PostgreSQL),SHOW_TABLES;
(MariaDB, MySQL).tables
(SQLite)SELECT TABLE_NAME FROM USER_TABLES;
(Oracle)
- models
-
- database layout
-
- additional metadata
- philosophy: in python, a model is to be a single, difinitive source of information about the data.
-
# polls/models.py
from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published') # defines human readable name, maybe for documentation purposes
class Choice (models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
Explanation:
django.db.models.Model
as a subclassField
- each field is represeneted by it- ex.
CharField
,DateTimeField
- the name of the fields are
questions_text
,pub_date
, which will also be the column name in the database - required arguments
CharField
for example requiremax_length
- optional arguments
default
to set a default value- used in
votes
ForeignKey
- in the example above, we told django thatChoice
is related to a singleQuestion
- used in
- ex.
- all database relationships are supported:
- many-to-many relationships
- many-to-one relationships
- one-to-one relationships
after the completion of the last section, we can:
- create a database schema (
CREATE TABLE
) - create a Python database-access API for accessing Question and Choice objects
Note: django apps are pluggable which means: it can be used in multiple projects and can be distributed
include the app in the project by adding to the INSTALLED_APPS
:
# mysite/settings.py
INSTALLED_APPS = [
'polls.apps.PollsConfig',
'django.contrib.admin',
# ...,
]
Now, make migrations
python manage.py makemigrations polls
do
python manage.py sqlmigrate polls 001
to view how the migration looks like in SQL query
output could look like:
BEGIN;
--
-- Create model Question
--
CREATE TABLE "polls_question" (
"id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
"question_text" varchar(200) NOT NULL,
"pub_date" timestamp with time zone NOT NULL
);
--
-- Create model Choice
--
CREATE TABLE "polls_choice" (
"id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
"choice_text" varchar(200) NOT NULL,
"votes" integer NOT NULL,
"question_id" bigint NOT NULL
);
ALTER TABLE "polls_choice"
ADD CONSTRAINT "polls_choice_question_id_c5b4b260_fk_polls_question_id"
FOREIGN KEY ("question_id")
REFERENCES "polls_question" ("id")
DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id");
COMMIT;
- table names are generated by combining
- name of the app
- lowercase name of the model -
question
andchoice
which can be modified
- primary keys are added automatically
- foreign key field name, appears with suffix
_id
, which can be modified, exquestion_id
sqlmigrate
- doesn't actually run the migration but prints how it looks like
- can be used to diagnose anything missing or errors before commiting migrations
- or when database administrators require scripts for making the changes
- doesn't actually run the migration but prints how it looks like
then run python manage.py migrate
- takes all the migrations that haven't been applied and runs them against the database to sync
- lets us change database and table, make new ones without the need to delete your database or tables
- lets live upgrade with no data loss
- because the migrations are commited to version control system and are shipped with the app
- easier
- shareable
- production ready
python manage.py shell
>>> from polls.models import Choice, Question # Import the model classes we just wrote.
# No questions are in the system yet.
>>> Question.objects.all()
<QuerySet []>
# Create a new Question.
# Support for time zones is enabled in the default settings file, so
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
# instead of datetime.datetime.now() and it will do the right thing.
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())
# Save the object into the database. You have to call save() explicitly.
>>> q.save()
# Now it has an ID.
>>> q.id
1
# Access model field values via Python attributes.
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=datetime.timezone.utc)
# Change values by changing the attributes, then calling save().
>>> q.question_text = "What's up?"
>>> q.save()
# objects.all() displays all the questions in the database.
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>
<QuerySet [<Question: Question object (1)>]>
is not very helpful, so lets add __str__
to the model classes to display another output
changes made:
# polls/models.py
from django.db import models
class Question(models.Model):
# ...
def __str__(self):
return self.question_text
class Choice(models.Model):
# ...
def __str__(self):
return self.choice_text
# polls/models.py
import datetime
from django.db import models
from django.utils import timezone
class Question(models.Model):
# ..
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
run python manage.py shell
>>> from polls.models import Choice, Question
# Make sure our __str__() addition worked.
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>
# Django provides a rich database lookup API that's entirely driven by
# keyword arguments.
>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
>>> Question.objects.filter(question_text__startswith='What')
<QuerySet [<Question: What's up?>]>
# Get the question that was published this year.
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>
# Request an ID that doesn't exist, this will raise an exception.
>>> Question.objects.get(id=2)
Traceback (most recent call last):
...
DoesNotExist: Question matching query does not exist.
# Lookup by a primary key is the most common case, so Django provides a
# shortcut for primary-key exact lookups.
# The following is identical to Question.objects.get(id=1).
>>> Question.objects.get(pk=1)
<Question: What's up?>
# Make sure our custom method worked.
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True
# Give the Question a couple of Choices. The create call constructs a new
# Choice object, does the INSERT statement, adds the choice to the set
# of available choices and returns the new Choice object. Django creates
# a set to hold the "other side" of a ForeignKey relation
# (e.g. a question's choice) which can be accessed via the API.
>>> q = Question.objects.get(pk=1)
# Display any choices from the related object set -- none so far.
>>> q.choice_set.all()
<QuerySet []>
# Create three choices.
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)
# Choice objects have API access to their related Question objects.
>>> c.question
<Question: What's up?>
# And vice versa: Question objects get access to Choice objects.
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> q.choice_set.count()
3
# The API automatically follows relationships as far as you need.
# Use double underscores to separate relationships.
# This works as many levels deep as you want; there's no limit.
# Find all Choices for any question whose pub_date is in this year
# (reusing the 'current_year' variable we created above).
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
# Let's delete one of the choices. Use delete() for that.
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> c.delete()
Info: we can pass to Model.objects.somefunct
something like pub_date__year
to access year or functions that are part of its data_type, pub_date
for ex
Info: we can access by [Question Object]q.choice_set
, key being _set
, because Django creates a set to hold the "other side" of a ForeignKey relation, which can be access by choice_set
The API automatically follow relationships as far as you need.
- Use double underscores to separate relationships
- This works as many levels deep as you want; there's no limit
Choice.objects.filter(question__pub_date__year=current_year)
Philosophy
- creation of admin interfaces for models is automated
- admin isn't intended to be used by site visitors. It's for site managers.
Create an admin user
python mange.py createsuperuers
Start development server
python manage.py runserver
After setting up the adming user, and accessing it the poll
will not have displayed.
# polls/admin.py
from django.contrib import admin
from .models import Question
admin.site.register(Question)
Info: To make Question
model modifiable in django admin add to polls/admin.py
`admin.site.register(Questions) after necessary imports
- form is automatically generatted based on Question model
- each
DateTimeField
gets free Javascript shortcuts:- Dates get a "Today" shortcut and calendar popup
- times get a "Now" shortcut and a convenient poput that lists commonly entered times
A view is a "type" of web page in your Django application that generally serves as specific functions and has a specific template.
A typical blog application could have:
-
- Blog homepage - displays the latest few entries
-
- Entry "detail" page - permalink page for a single entry
-
- Year-based archive page - displays all months with entries in the given year
-
- Month-based archive page - displays all days with entries in the given month
-
- Day-based archive page - displays all entries in a given day
-
- Comment action - handles posting comments to a given entry
Features that will be implemented in our app:
-
- Question "index" page
-
- Question "detail" page
-
- Question "results" page
-
- Vote action
A django url patter looks like /newsarchive/<year>/<month>/
URLconf
maps URL patterns to views
# polls/views.py
def detail(request, question_id):
return HttpResponse("Your're looking at question %s." % question_id)
def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("You're voting on question %s." % question_id)
connect the views
# polls/urls.py
from django.urls import path
from . import views
urlpatterns = [
# ex: /polls/
path('', views.index, name='index'),
# ex: /polls/5/
path('<int:question_id>/', views.detail, name="detail"),
# ex: /polls/5/results/
path('<int:question_id>/results/', views.results, name="results"),
# ex: /polls/5/vote/
path('<int:question_id>/vote/', views.vote, name='vote')
]
- when we visit - say,
polls/1/
, Django will load mysite.urls that is pointed by the ROOT_URLCONF setting - it finds
urlpatterns
, and traverse through it to findpolls/
, which gets stripped off- remaining
1/
is sent to thepolls.urls
URLconf for further processing
<int:question_id>/
is found, which calls detail()- it looks like:
detail(request=<HttpRequest object>, question_id=1)
- it looks like:
New github feature Ctrl+shift+k
to pull of command pallet, switch organization and search
- Return an HttpResponse object containing the content for the requested page
- Raise an exception such as Http404 These are the only two things django requires view to create.
- Abilty to access database or not
- Template system from Django itself or from a third-party or not
- Generate a PDF file, output XML, create a ZIP file on the fly or not
# polls/views.py
from django.http import HttpResponse
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
output = ', '.join([q.question_text for q in latest_question_list])
return HttpResponse(output)
- how the templates are loaded, is determined by
TEMPLATES
. DjangoTemplates
backend accesses allAPP_DIRS
to look for templates.- By convention,
DjangoTemplates
looks for atemplates
subdirectory in each of theINSTALLED_APPS
- By convention,
Info: Here is the directory DjangoTemplates
wants: polls/templates/polls/index.html
. to access just do polls/index.html
. [important]
<!--polls/templates/polls/index.html-->
{% if latest_question_list %}
<ul>
{% for question in latest_question_list %}
<li><a href="/polls/{{ question.id }}/" >
{{ question.question_text }}
</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available</p>
{% endif %}
Add the template to views.py
from django.http import HttpResponse
from django.template import loader
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
template = loader.get_template("polls/index.html")
context = {
"latest_question_list": latest_question_list,
}
return HttpResponse(template.render(context, request))
Info: Instead of using django.template.render
, we can use use django.shortcuts.render
to render the template
The shortcut exists because this is very common. Here's how:
# polls/views.py
from django.shortcuts import render
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'latest_question_list': latest_question_list}
return render (request, 'polls/index.html', context)
# polls/views.py
# import ...
from django.http import Http404
# ...
def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, "poll/detail.html", {"question": question})
temporary put the following in templates
<!--polls/templates/polls/detail.html-->
{{ question }}
So modify details
method as:
# polls/views.py
from django.shortcuts import get_object_or_404, render
from .models import Question
# ...
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
modify /polls/detail.html
<!--polls/templates/polls/detail.html-->
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
- the template system uses dot-lookup syntax to access variable attributes
- in the example
{{ question.question_text }}
- django does a dictionary lookup which fails
- so it loops up attribute
- should last fail, it would try
list-index
lookup
- in the example
Info: url should look like: {% url 'detail' question.id %}
, where is 'detail' is the name passed to path()
in polls.urls
change the following:
<li><a href="/polls/{{ question.id }}">{{ question.question_text }}</a></li>
To:
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
change
# the 'name' value as called by the {% url %} template tag
path('<int:question_id>/', views.detail, name='detail'),
to
# added the word 'specifics'
path('specifics/<int:question_id>/', views.detail, name='detail'),
Info: to avoid confusion: set app_name
(for ex. poll
) in polls/urls.py
and access by {% url 'polls:detail' question.id %}
in polls/index.html
Lets do this insertion in polls/urls.py
# polls/urls.py
# ...
app_name = 'polls'
# ...
change:
<!--polls/templates/polls/index.html-->
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
to
<!--polls/templates/polls/index.html-->
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
Add a form to the detail.html
<!--polls/templates/polls/detail.html-->
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
<legend><h1>{{ question.question_text }}</h1></legend>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
{% for choice in question.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>
value
of each button is choice's ID. Thanname
of each radio button ischoice
.- the form's
action
is set to{% url 'polls:vote' question.id %}
forloop.counter
countes how many times for tag has gone through its loop- cross Site Request Forgeries is taken care of by
{% csrf_token %}
.
# ...
def vote(request, 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):
# Redisplay the question voting form.
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
- request.POST
- is a dictionary-like object that lets us access submitted data by key name
request.POST['choice']
returns the ID of the selected choice as string- return is always a string
HttpResponseRedirect
takes a sigle argument, the url to redirect toreverse()
- this functions helps avoid having to hard-code a url
- takes two arguments
- name of the view that we want to pass control to
- variable portion of the URL pattern that points to the view
- return looks like:
/polls/2/results/
request
is a HttpRequest object
Info: always return a HttpResponseRedirect
after succesfully dealing with POST data - solution to the Back
button problem 😉
# ...
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question })
Note: this is almost the same as detail()
with the difference being the template name.
<!--polls/templates/polls/results.html-->
<h1>{{ question.question_text}}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>
<a href=" {% url 'polls:detail' question.id %} ">Vote again? </a>
Note: there is an issue with the vote()
function. If two users vote at the same time a problem might occur called race condition
steps taken here:
-
- convert the URLconf
-
- Delete some of the old, unneeded views
-
- Introduce new views based on Django's generic views
Info: use generic views where possible from the start, but understand what to do if we need more flexibility
# polls/urls.py
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')
]
Note: the name of the matched pattern in the path strings of the second and third patterns has changed from <question_id>
to <pk>
.
replace the old index
, detail
, and results
views
# polls/views
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 .models import Choice, Question
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
return Question.objects.order_by('-pub_date')[:5]
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
class ResultsView(generic.DetailView):
modal = Question
template_name = "polls/results.html"
queryset = Question.objects.all()
# ...
Here
- those with
_name
are either template urls or the names by which they will be referred to in the template files - Each generic view needs to know what model it will be acting upon.
model
=model_name
DetailView
generic view expects the primary key value to be captured form the URL to be calledpk
, soquestion_id
have been changed topk
DetailView
generic view uses a template called<app name>/<modelname>_detail.html
.template_name
attribute defines a specific name instead
ListView
uses a default template called<app name>/<model name>_list.html
template- we use
template_name
to tellListView
to use our existingpolls/index.html
template
- we use
- context
DetailView
generic view can use modelQuestion
to generatequestion
variable automaticallyListView
would generatequestion_list
automatically, but we want to override itcontext_object_name
attribute is used to given another variable insteadget_queryset()
helps generate the new list
Info: A weird problem: we need to add queryset = Question.objects.all()
to the ResultsView
to avoid error that looks like
C:\Python310\lib\site-packages\django\views\generic\detail.py", line 72, in get_queryset raise ImproperlyConfigured( django.core.exceptions.ImproperlyConfigured: ResultsView is missing a QuerySet. Define ResultsView.model, ResultsView.queryset, or override ResultsView.get_queryset().
Extra info: git diff starting-commit-sha ending-commit-sha myPatch. patch
can be used to create a git patch file
- Tests are routines that check the operation of the code
- it can be appplied to
- tiny details: like internal behavior such as
function
return - overall operation of the software: like
Views
- tiny details: like internal behavior such as
- saves time for large applications
- it identifies the problem from inside and points us to it
- code without tests is broken by design
- one programmer may not break another's codes if the tests are established beforehand
-
test-driven development
- some write tests before writing their code
- formalizes the problem in a Python test case
-
- write tests as each functionality is implemented
there is the bug in the code function: was_published_recently()
method
- it return true both when the date is within last 24 hour or in the future as well
# polls/tests.py
import datetime
from django.test import TestCase
from django.utils import timezone
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)
python manage.py test polls
manage.py test polls
looks for tests in the polls applicationdjango.test.TestCase
is found- a special database is created for the purpose of testing
- it looks for test methods - ones that begin with
test
- a
Question
instance with a futurepub_date
is created self.assertIs
checks to see if the value returned is false
modify was_published_recently
:
# polls/models.py
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
Bug is fixed, and with the test setup, we might be informed if there is any change that creates error in this function.
#..
# class
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)
now we have 3 tests that confirm that Question.was_published_recently()
returns sensible values for past, recent, and future questions.
- So far, our application still doesn't discriminate where the
pub_date
while posting to the db.
- a test
Client
can simulate a user interacting with the code at the view level
Start with shell
python manage.py shell
from django.test.utils import setup_test_environment
setup_test_environment()
from django.test import Client
client = Client()
# get a response from '/'
response = client.get('/')
# Not found: /
# we should expect a 404 from the address
>>> response.status_code
404
>>> # on the other hand we should expect to find something at '/polls/'
>>> # we'll use 'reverse()' rather than a hardcoded URL
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
b'\n <ul>\n \n <li><a href="/polls/1/">What's up?</a></li>\n \n </ul>\n\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>
if the error is: "Invalit HTTP_HOST header" error and a 400 response, setup_test_environment
call might be missing
change previous IndexView
from
# polls/views.py
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
""" Return the last five published questions. """
return Quetsion.objects.order_by('-pub_date'):[:5]
to
# polls/vies.py
from django.utils import timezone
# ...
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]
# polls/tests.py
from django.urls import reverse
# shortcut function to create questions as well as a new test class:
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)
# finally the class that tests the views
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],
)
Note: an weird error occurred:
AssertionError: False is not true : Couldn't find 'No polls are available.' in response
I had missed .
at the end. Adding it should fix it.
<!-- ... --->
{% else %}
<p>No polls are available.</p>
<!-- ... -->
-
create_question
takes some reptition out
-
- rest are pretty self explanatory
In the word frm the docs
... we are using tests to tell a story of admin input and user experience on the site, and checking that at every state and for every new change in the state of the system, the expected results are published.
lets add a constraint such that users cant access the detail view of a question with published date in the future.
# polls/views.py
class DetailView(generic.DetailView):
# ...
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
add the test cases
# polls/tests.py
# ...
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)
# polls/views.py
# class ResultsView(self):
# ...
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
# polls/tests.py
class ResultsDetailViewTests(TestCase):
def test_future_question(self):
"""
The results 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:results', 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:results', args=(past_question.id,))
response = self.client.get(url)
self.assertContains(response, past_question.question_text)
-
- Test to check if
Question
with noChoices
are published
- Test would create
Question
without anyChoice
- and test to see if it's not published
- Test that would create
Question
withChoice
s- and test if it's published
- Test to check if
-
- More advanced test:
- Admin users get to see the unpublished questions
- not ordinary visitors
Info
: 3 rules for testing: 1. a separate TestClass
for each model or view, 2. a separate method for each set of conditions, 3. test method names that describe their function
Selenium
to test the way HTML actually renders a browserLiveServerTestCase
in Django can facilitate it
- test may bet better run with every commit, a development principle known as:
continuous integration
integration with coverage.py
- to spot untested parts of the application
- helps indentify fragile or even dead code.
django.contrib.staticfiles
- collects static files form each of the applicatoin(or any specified places) into a single locatio that can easily be served in production
STATICFILES_FINDERS
setting contains a list of finders that know how to discover static filesAppDirectoriesFinder
- looks for astatic
subdirectory in each ofINSTALLED_APPS
create a file:
polls/static/polls/style.css
- this can be referred to as
polls/styles.css
, similar to how it is done for templates
add to the stylesheet
// polls/static/polls/style.css
li a{
color: green
}
Add to the top of index.html
<!-- polls/templates/polls/index.html -->
{% load static %}
<link rel ="stylesheet" href="{% static 'polls/style.css'%}">
add an image titleed 'background.png' in the directory
polls/static/polls/images/background.png
add a reference to the image in the stylesheet polls/static/polls/style.css
body {
background: white url("images/background.png") no-repeat;
}
- this way we can change
STATIC_URL
(used by static templates to generate its URLs)- without having to modify a bunch of paths in yoru static files as well
More advance tutorial on
The appearance of the fields can be reordered
# polls/admin.py
from django.contrib import admin
from .models import Question
class QuestionAdmin(admin.ModelAdmin):
fields = ['pub_date', 'question_text']
admin.site.register(Question, QuestionAdmin)
Info
- create a ModelAmin
class and pass it to register("here", [])
to change admin options for a model
# polls/admin.py
from django.contrib import admin
from .models import Question
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date']}),
]
admin.site.register(Question, QuestionAdmin)
There are two ways to add related objects
first is to just register the Choice
model in admin
like
from django.contrib import admin
from .models import Choice, Question
# ...
admin.site.register(Choice)
but the problem is Choice with appear separately from the question, which a dropbox for to select questions for each field.
Hence, there is the other method
# polls/admin.py
from django.contrib import admin
from .models import Choice, Question
class ChoiceInline(admin.StackedInline):
model = Choice
extra = 3
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
]
inlines = [ChoiceInline]
admin.site.register(Question, QuestionAdmin)
extra
adds 3 extra slots for related Choices
There is a problem with the choices taking a lot of space StackedInLin
being the culprit.
To fix it: change it to TabularInLine
# polls/admin.py
class ChoiceInLine(admin.TabularInLine):
# ...
by default only __str___
is displayed in the list of questions,
lets change the question list display page in admin
# polls/admin.py
class QuestionAdmin(admin.ModelAdmin):
# ...
list_display = ('question_text', 'pub_date')
Add another section was_published_recently()
:
class QuestionAdmin(admin.ModelAdmin):
# ...
list_display = ('question_text', 'pub_date', 'was_published_recently')
here, we should be able to click the column headers to sort by values in the columns
Note: underscores got replaced by spaces for was_published_recently
However, this can be changed by adding the following:
# polls/models.py
from django.contrib import admin
class Question(models.Model):
# ...
@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
more inforation of this decorator here
at a filter to the list display by adding
# polls/admin.py
# ...
# class QuestionAdmin():
list_filter = ['pub_date']
# ...
Depending on the type of filed provide, django knows how to give the filter
pub_date
as aDateTimeField
is given- "Any date", "Today", "Past 7days" etc
To add search capability do
# polls/admin.py
# class QuestionAdmin():
search_fields = ['question_text']
This adds a search box to the top of the change list. When somebody enters search terms, Django will serach the question_text
field.
- other fields can be added, but it is better to limit to a few since it uses
LIKE
query behind the scenes
By default, the page displays 100 items per page but that can be changed by using list_per_page
like
# polls/admin.py
# class QuestionAdmin():
list_per_page = 1
- Django admin is powered by Django itself, and its interfaces use Django's own template system
- create a
templates
directory in the project directory where themanage.py
lives - Open your settings file
mysite/settings.py
, and add aDIRS
option in theTEMPLATES
setting:
# mysite/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # this line got modified
'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',
],
},
},
]
Info
- DIRS
is a list of filesystem directories to check when loading DJango templates; it's a search path
Info
- Django source files can be located by using python -c "import django; print(django.__path__)"
Now create a directory called admin
inside templates
, and copy the template admin/base_site.html
from within the default Django admin template directory in the source code of Django itself (`django/contrib/admin/templates) into that directory.
<!-- /templates/admin/base_site.html -->
<!-- replace the default:_("content-here")-->
{% extends "admin/base.html" %}
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1>
{% endblock %}
{% block nav-global %}{% endblock %}
- here we learnt to replace the original template
- but in projects, we might use
django.contrib.admin.AdminSite.site_header
attribute to more easily make this particular customization
Info
- Any of Django's default admin templates can be overrridden. To override a template, copy it to the /templates/
directory where the manage.py
is located
IF the DIRS
was empty by default, who does did Django find the default templates?
- by setting
APP_DIRS
toTrue
, Django automatically looks fortemplates/
subdirectory within each package includingdjango.contrib.admin
which is also an application
Not exactly sure but looks important
Our poll application is not very complex and doesn’t need custom admin templates.
But if it grew more sophisticated and required modification of Django’s standard
admin templates for some of its functionality, it would be more sensible to modify
the application’s templates, rather than those in the project. That way, you could
include the polls application in any new project and be assured that it would find
the custom templates it needed.
- by default, all apps in
INSTALLED_APPS
is displayed in alphabetical order. - the template to customize for this is
admin/index.html
- it uses a template variable called
app_list
- this variable contains every installed Django app
- but we can hard-code links to object-specific admin pages in whatever way you think is best
- this variable contains every installed Django app
- it uses a template variable called
Django makes reusability of applications easy with The Python Package Index (PyPi)
Info
- In part 1, we decoupled polls from the project-level URLConf using an include
A Python package provides a way of grouping related Python code for easy resuse. A package contains one or more files of Python code aka modules.
- for a directory (like
polls/
) to form a package, it must contain__init__.py
, even if empty - A django application is also a python package
- specifically made for Django projects
- An application may use common Django conventions, such as having
models
,tests
,urls
, andviews
submodules - packaging - the process of making a Python package easy for others to install.
📦mysite
┣ 📂mysite
┃ ┣ 📜asgi.py
┃ ┣ 📜settings.py
┃ ┣ 📜urls.py
┃ ┣ 📜wsgi.py
┃ ┗ 📜__init__.py
┣ 📂polls
┃ ┣ 📂migrations
┃ ┃ ┣ 📜0001_initial.py
┃ ┃ ┗ 📜__init__.py
┃ ┣ 📂static
┃ ┃ ┗ 📂polls
┃ ┃ ┃ ┣ 📂images
┃ ┃ ┃ ┃ ┗ 📜background.png
┃ ┃ ┃ ┗ 📜style.css
┃ ┣ 📂templates
┃ ┃ ┗ 📂polls
┃ ┃ ┃ ┣ 📜detail.html
┃ ┃ ┃ ┣ 📜index.html
┃ ┃ ┃ ┗ 📜results.html
┃ ┣ 📜admin.py
┃ ┣ 📜apps.py
┃ ┣ 📜models.py
┃ ┣ 📜tests.py
┃ ┣ 📜urls.py
┃ ┣ 📜views.py
┃ ┗ 📜__init__.py
┣ 📂templates
┃ ┗ 📂admin
┃ ┃ ┗ 📜base_site.html
┣ 📜db.sqlite3
┗ 📜manage.py
- everthing that is part of
polls
is in the same directory- guess why?
- the self-contained directory can be easily packaged and shared
- guess why?
- install
setuptools
- first creat a parent directory for
polls
, outside the Django project. Call this directorydjango-polls
- while choosing a package name;
- check resources like PyPi to avoid naming conflicts
django-
added to the module name can help distinguish apps made for Django- Application labels must be unique in
INSTALLED_APPS
. Avoid using the same label as any of the Djangocontrib packages
for example auth, admin or message
- while choosing a package name;
- Move the polls directory into the
django-polls
directory - Create a file
django-polls/README.rst
with the following contents:
=====
Polls
=====
Polls is a Django app to conduct web-based polls. For each question,
visitors can choose between a fixed number of answers.
Detailed documentation is in the "docs" directory.
Quick start
-----------
1. Add "polls" to your INSTALLED_APPS setting like this::
INSTALLED_APPS = [
...
'polls',
]
2. Include the polls URLconf in your project urls.py like this::
path('polls/', include('polls.urls')),
3. Run ``python manage.py migrate`` to create the polls models.
4. Start the development server and visit http://127.0.0.1:8000/admin/
to create a poll (you'll need the Admin app enabled).
5. Visit http://127.0.0.1:8000/polls/ to participate in the poll.
- Create a
django-polls/LICENSE
file. - Create
pyproject.toml
,setup.cfg
, andsetup.py
files which detail how to build and install the app. Checkout thesetuptools
documentationpyproject.toml
[build-system]
requires = ['setuptools>=40.8.0', 'wheel']
build-backend = 'setuptools.build_meta:__legacy__'
setup.cfg
[metadata]
name = django-polls
version = 0.1
description = A Django app to conduct web-based polls.
long_description = file: README.rst
url = https://www.example.com/
author = Your Name
author_email = yourname@example.com
license = BSD-3-Clause # Example license
classifiers =
Environment :: Web Environment
Framework :: Django
Framework :: Django :: X.Y # Replace "X.Y" as appropriate
Intended Audience :: Developers
License :: OSI Approved :: BSD License
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: Dynamic Content
[options]
include_package_data = true
packages = find:
python_requires = >=3.8
install_requires =
Django >= X.Y # Replace "X.Y" as appropriate
setup.py
from setuptools import setup
setup()
- Only Python modules and packages are included in the package by default. To include additional files, we'll need to create a
MANIFEST.in
file. The setuptools docs referred to in the previous step discuss this file in more detail. To include the templates, theREADME.rst
and ourLICENSE
file, create a filedjango-polls/MANIFEST.in
with:
include LICENSE
include README.rst
recursive-include polls/static *
recursive-include polls/templates *
- [Optional] add a detailed documentation to the app, by creating a directory
django-polls/docs
recursive-include docs *
this docs directory will not be included unless some files are added to it.
- Try building the package using
python setup.py sdist
- inside
django-polls
folder - this creates a directory
dist
with new packagedjango-polls-0.1.tar.gz
- inside