After you have created some records using the admin the next step is to display them also in the frontend. Therefore you have do three things:
- Define URLs
- Write views
- Create templates
First we define the URLs that are used for calling the different views. For now, we want to create two URLs. Jump to the file urls.py and add at the end of urlpatterns the following two lines:
url(r'^admin/', include(admin.site.urls)),
url(r'^recipe/(?P<slug>[-\w]+)/$', 'recipes.views.detail'),
The file urls.py then looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | from django.conf.urls import patterns, include, url
from django.conf import settings
# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
# Examples:
# url(r'^$', 'cookbook.views.home', name='home'),
# url(r'^cookbook/', include('cookbook.foo.urls')),
# Uncomment the admin/doc line below to enable admin documentation:
# url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
url(r'^admin/', include(admin.site.urls)),
url(r'^recipe/(?P<slug>[-\w]+)/$', 'recipes.views.detail'),
url(r'^$', 'recipes.views.index'),
)
if settings.DEBUG:
# for development only: serve media files
urlpatterns += patterns('',
url(r'^media/(?P<path>.*)$', 'django.views.static.serve',
{'document_root': settings.MEDIA_ROOT}),
)
|
Note
The first argument of the url() function is a raw string, which contains a regular expression.
If you encounter regular expressions for the first time, you can learn more about it in the Regular-Expression-HOWTO, on Regular Expressions.info or in the article by Doug Hellmann about the re-Modul. At RegexPlanet you can test regular expressions directly in the browser.
Now you start the development server:
$ python manage.py runserver
Validating models...
0 errors found
November 10, 2013 - 22:21:46
Django version 1.5.5, using settings 'cookbook.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Calling the URL http://127.0.0.1:8000/ shows a ViewDoesNotExist Exception. That’s right, because until now you still have no view written. But it shows that your URL works.
Before we write the first views we want to look how Django templates are rendered.
Django templates are simple Python objects whose constructor expectes a string. With the help of a context object the placeholders in the template are replaced by the desired values.
The first example shows how to use a dictionary as a data structure:
$ python manage.py shell
Note
The command shell loads the settings from settings.py for the current project, which would not happen if you had simply typed python.
>>> from django.template import Context, Template
>>> t = Template('My name is {{ person.first_name }}.')
>>> d = {'person': {'first_name': 'Alice'}}
>>> t.render(Context(d))
u'My name is Alice.'
In the second example, we use a simple Python object as a data structure:
>>> class Person: pass
...
>>> p = Person()
>>> p.first_name = 'Bob'
>>> c = Context({'person': p})
>>> t.render(c)
u'My name is Bob.'
Lists can also be used:
>>> t = Template('First article: {{ articles.0 }}')
>>> c = Context({'articles': ['bread', 'eggs', 'milk']})
>>> t.render(c)
u'First article: bread'
So now the views have to be created. You want to display the data that is retrieved from the database using the ORM. For this you open the file recipes/views.py.
Most views return a HttpResponse object. So we write a very simple view, which does this:
from django.http import HttpResponse
def index(request):
return HttpResponse('My first view.')
After you have saved the view and called http://127.0.0.1:8000/ you’ll see the string that you passed to the HttpResponse object. So a HttpResponse always expects a string as first argument.
Now we will replace the string with a Template and render it with a Context which contains a Recipe object. The HttpResponse will then return the string rendered by the Template:
from django.http import HttpResponse
from django.template import Context, loader
from .models import Recipe
def index(request):
recipes = Recipe.objects.all()
t = loader.get_template('recipes/index.html')
c = Context({'object_list': recipes})
return HttpResponse(t.render(c))
If you now call http://127.0.0.1:8000/ a TemplateDoesNotExist exception is raised. Sure - you didn’t create the template yet.
First you need a basic template for your website. Create the file base.html in the templates directory with the following content:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}Kochbuch{% endblock %}</title>
</head>
<body>
<h1>Kochbuch</h1>
{% block content %}{% endblock %}
</body>
</html>
It contains HTML and two blocks. These will be filled by the other templates which derive from this template.
Within the application, you have to create two directories for the templates, namely recipes/templates/recipes. In it you create the file index.html:
{% extends "base.html" %}
{% block title %}{{ block.super }} - List of recipes{% endblock %}
{% block content %}
<h2>List of recipes</h2>
<ul>
{% for recipe in object_list %}
<li><a href="/recipe/{{ recipe.slug }}/">{{ recipe.title }}</a></li>
{% endfor %}
</ul>
{% endblock %}
Now your directory structure should look like this:
cookbook
|-- cookbook
| |-- __init__.py
| |-- settings.py
| |-- urls.py
| `-- wsgi.py
|-- default.db
|-- manage.py
|-- media
|-- recipes
| |-- __init__.py
| |-- admin.py
| |-- fixtures
| | `-- initial_data.json
| |-- models.py
| |-- templates
| | `-- recipes
| | `-- recipe.html
| |-- tests.py
| `-- views.py
|-- static
`-- templates
`-- base.html
After you have started the development web server you should now see a list of recipes if you call http://127.0.0.1:8000/.
Thus the detail view of the recipes work, a second view must are written.
First you need an additional import at the beginning of the file views.py:
from django.http import Http404
At the end of the file comes a new function for the new view:
def detail(request, slug):
try:
recipe = Recipe.objects.get(slug=slug)
except Recipe.DoesNotExist:
raise Http404
t = loader.get_template('recipes/detail.html')
c = Context({'object': recipe})
return HttpResponse(t.render(c))
The entire file now looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | from django.http import Http404
from django.http import HttpResponse
from django.template import Context, loader
from .models import Recipe
def index(request):
recipes = Recipe.objects.all()
t = loader.get_template('recipes/index.html')
c = Context({'object_list': recipes})
return HttpResponse(t.render(c))
def detail(request, slug):
try:
recipe = Recipe.objects.get(slug=slug)
except Recipe.DoesNotExist:
raise Http404
t = loader.get_template('recipes/detail.html')
c = Context({'object': recipe})
return HttpResponse(t.render(c))
|
Now only the second template is missing: recipes/detail.html. Put it in the same directory as recipes/index.html:
{% extends "base.html" %}
{% block title %}{{ block.super }} - {{ object.title }}{% endblock %}
{% block content %}
<h2>{{ object.title }}</h2>
<p>Makes {{ object.number_of_portions }} servings.</p>
<h3>Ingredients</h3>
{{ object.ingredients|linebreaks }}
<h3>Preparation</h3>
{{ object.preparation|linebreaks }}
<p>Time for preparation: {{ object.time_for_preparation }} minutes</p>
{% endblock %}
Now you can also view all the details of the recipes by clicking on the links on the index page.
If a variable is not defined as key in the context, this is ignored by the Django template engine. This is mainly makes sense in the production environemnt, since as the site despite the absence of another variable can be rendered.
To see anyway, if a variable has not been rendered, one can define a string in the configuration settings.py which in this case appears:
TEMPLATE_STRING_IF_INVALID = 'TEMPLATE NAME ERROR'
This setting should be disabled again in a production environment.
For safety reasons the Django template engine escapes all HTML and JavaScript that is in the context. Suppose a user uses the following text in the field “preparation” of a recipe:
<script>alert('The best recipe in the world!')</script>
Heat the water in the pot to 100 °C.
Then this HTML would be generated:
<p><script>alert('The best recipe in the <world!')</script></p>
<p>Heat the water in the pot to 100 °C.</p>
The JavaScript code would therefore not be executed.
It is also possible to remove HTML tags completely. To do this you’d have to use the striptags filter in the template:
<h3>Preparation</h3>
{{ object.preparation|striptags|linebreaks }}
Now the HTML looks like this:
<p>alert('The best recipe in the world!')</p>
<p>Heat the water in the pot to 100 °C.</p>
Are you sure, however, that HTML or JavaScript should be rendered and possibly be executed, you can use the safe filter to explicitly allow this:
<h3>Preparation</h3>
{{ object.preparation|safe|linebreaks }}
Now actually the JavaScript is executed as desired by the user:
<p><script>alert('The best recipe in the world!')</script></p>
<p>Heat the water in the pot to 100 °C.</p>
Note
This allows of course XSS attacks and should therefore be used with caution.