Let's Take Notes With Django - Part 6

Let's Take Notes With Django - Part 6

part of the Startproject To Deployment - Django Tutorial series

show all in series

back to index

Our DjangoNote application is looking better by the minute. Well, eh, that's not quite right. It's working better by the minute - it's still pretty ugly. But remember, we're not painting the Sistine Chapel. We're taking notes with DjangoNote!

Now that we've got the ability to add Tags to our database, it'd be nice to be able to search for notes with a specific tag. There are a couple of ways we could do this:

Set up a search form that sends data to our server via a POST request. Process the request on the server, collect the data and send it back to the client.

If you're making complex queries and / or need to process requests that may be phrased in unexpected ways, this is probably the way to go. However, that's not the case here. We're not querying a vast and overwhelming database like Google's, and we don't need to worry about queries like "how is babby formed" - we have a small number of user-defined tags, and we want to query our database for notes associated with tags from that small pool. So let's check out option #2.

Implement a REST-ful search pattern and return results in response to a single GET request

The benefits here are pretty obvious: GET-request >> Response is a quicker and easier workflow for both our user and our server than GET-request ( for the page with the form) >> POST form data >> process form data >> Response. The way this works is simple - we'll make a GET request to a specially crafted URL, like 127.0.0.1/notes/tags/, replacing with the name of the tag we want to explore. We'll configure our view to look for this tag label in the URL and respond by rendering a template that contains all notes associated with that tag as a queryset in its context.

However, we've got a small problem. Our tag labels are CharFields, and those can hold characters (like spaces and slashes) that can give URLs a lot of trouble. What we really need is a SlugField, which contains only letters, numbers, underscores and hyphens. We're still early in our development cycle, so we're going to be a little lazy - delete your database (db.sqlite3 in your main django project directory). We'll just make a new one with the stuff we want!

Open up your notes/models.py file and add a new field to your Tag model:

class Tag(models.Model):
label = models.CharField(max_length=200)
slug = models.SlugField(max_length=200)

def __unicode__(self):
return self.label

Make sure the max_length of your SlugField is at least as long as the max_length of your CharField. Once you've done that, hop into a terminal window and give it the ol' database migration triple whammy: manage.py makemigrations, manage.py migrate, and manage.py createsuperuser. Now that we've got a sparkling clean database, let's head to 127.0.0.1 and log in. Once you're in, use the navbar to head to your "add tag" page.

Note: thanks to Djagno 1.7 and migrations, you don't actually need to delete your database here. You can simply run the two migration steps (makemigrations and migrate) and you'll be good to go. You'll need to supply a default value for your new database column, but that's no big deal.

I personally find it easier to keep everything "mentally straight" at this phase of development if I just start over with the DB after a change like this. It's possible that I'm a lunatic; your mileage may vary.

We've added a SlugField to our Tag model, but our ModelForm doesn't have a field for it. Instead of troubling ourselves with automatically adding a slug every time we create a new tag, let's do it automatically while we validate and save the ModelForm itself.

Head over to our notes/views.py file and find the add_tag function. Specifically, we're interested in this bit:

form = TagForm(request.POST, instance=tag)
if form.is_valid():
form.save()
messages.add_message(request, messages.INFO, 'Tag Added!')
return HttpResponseRedirect(reverse('notes:index'))

This checks to make sure our form data is valid (whether we're creating a new tag or editing an existing instance) and, if it is, saves that form into the database as a new Tag. It then packages up a helpful success message for us and sends us on our way. Let's add a step and tell Django to automatically "slugify" our label and save it as our slug using the built-in django.utils.text slugify() functionality. We'll edit this bit of the view like so:


from django.utils.text import slugify
...
form = TagForm(request.POST, instance=tag)
if form.is_valid():
t = form.save(commit=false)
t.slug = slugify(t.label)
t.save()
messages.add_message(request, messages.INFO, 'Tag Added!')
return HttpResponseRedirect(reverse('notes:index'))

Now our label is automatically converted into a slug, and that value is saved in the SlugField for our Tag. Phew! You may remember that we started doing this so that we could search for notes related to specific tags with REST-ful URLs. We're finally ready to get started on that.

First, we want to write a new URL pattern so that we're sure what we'll be parsing. Add the following pattern to our notes/urls.py (don't forget to add the new view to our import statement as well):

url(r'^tags/(?P<slug>[-\w]+)/$', tag_search, name='tagsearch'),

This regex will capture slug-like stuff in the proper position in the URL and assign its value to a variable named "slug" in the kwargs dictionary. We'll be able to access that dictionary within the view, which means we'll be able to search for a tag by its slug.

Now let's create the view in notes/views.py:

def tag_search(request, **kwargs):
slug = kwargs['slug']
tag = get_object_or_404(Tag, slug=slug)
notes = tag.notes.all()

return render(request, 'notes/tagsearch.html', {'notes':notes, 'tag':tag})

That was remarkably painless. Now we'll just write up a notes/tagsearch.html template to display our results. Remember, we have access to two custom context variables from our view: notes representing all notes associated with a given tag, and tag representing that tag.

{% extends 'base.html' %}
{% load static %}
{% block body %}

<div class="jumbotron">
<h1 align='center'>Notes tagged with {{tag}}</h1>
</div>

<div class='col-md-12'>
{% if notes %}
{% for note in notes %}
<h3 class="page-header">{{note.label}}</h3>
{{note.body|linebreaks}}
{% for tag in note.tags.all %}
{{tag}}{% if not forloop.last %} | {% endif %}
{% endfor %}
{% endfor %}
{% else %}
<div class='alert alert-warning' align='center'>
<h3>No Notes With This Tag!</h3>
</div>
{% endif %}
</div>

{% endblock body %}

That sure looks a lot like the main index page for our notes app, huh?

Speaking of that index page, let's make it a little more useable. We display all of our tags on the right-hand side of the page - let's make those list items link to tagsearch pages for each tag. We'll use named urls and keyword arguments in our tag sidebar:

...
{% if tags %}
{% for tag in tags %}
<li><a href="{% url 'notes:tagsearch' slug=tag.slug %}">{{tag}}</a></li>
{% endfor %}
{% endif %}
...

Nice! Let's also add an edit link to our notes. We'll put this on both the index.html page as well as the tagsearch page, replacing the content in the <h3> tags with a link.

...
{% for note in notes %}
<h3 class='page-header'>
<a href="{% url 'notes:addnote' %}?id={{note.id}}">{{note.label}}</a>
</he>
...

The syntax is different here because the URL needs to be structured differently. Now we can get to our tagsearch page for any tag from the index page, and we can edit any note from either the index page or a search result page.

Hey, remember our checklist from earlier in the tutorial?


  1. See all of our existing notes done!

  2. Add a new note done!

  3. Edit and delete existing notes done!

  4. See all of our existing tags

  5. Create new tags

  6. Edit and delete existing tags

  7. Some basic search capability - find notes with a given tag, for instance

We're done! Our tag application has all of the basic functionality we set out to include. Just one thing - if we deployed right now, the whole world could see our private notes. We want to lock down the index_view, add_note, add_tag and tag_search views so that only a logged-in superuser can see them. We'll do this with Django's user_passes_test decorator. First, we need to devise a "test" for a user that returns True if they're allowed to see our private stuff, and False if they aren't.

from django.contrib.auth.decorators import user_passes_test
def superuser_only(user):
return (user.is_authenticated() and user.is_superuser)

The syntax here is important. Enclosing the and statement in parentheses means this only returns true if both conditions are true. It's also crucial to include the parentheses on user.is_authenticated() when testing to see if a user requesting content is logged in to an authenticated session or not. If you omit the parentheses, it'll always return True (because it thinks you're asking if it has access to that method or not, which it always does).

Now that we've defined our test, we'll decorate our four views:

def superuser_only(user):
return (user.is_authenticated() and user.is_superuser)

@user_passes_test(superuser_only, login_url="/")
def index_view(request):
...

@user_passes_test(superuser_only, login_url="/")
def add_note(request):
...

@user_passes_test(superuser_only, login_url="/")
def add_tag(request):
...

@user_passes_test(superuser_only, login_url="/")
def tag_search(request):
...

Perfect! Our views are now protected from prying eyes. Let's add a way to log out so we can see this in action. Django makes this very easy - all you've got to do is add a URL pattern for the logout view, and the built-ins can take care of this rest. Add this pattern to our core urls.py:

url(r'^logout/$', 'django.contrib.auth.views.logout', {'next_page': 'home'}, name='logout')

(Yes yes, we're calling a view with a string - it's just too easy to pass up.)

Now if we visit 127.0.0.1/logout, we're logged out and taken to our home page. Try accessing a note or one of the other protected views - it's all locked down! Sweet!

Add a logout link in your nav bar, and try using {% if user.is_authenticated %} to only display it to logged-in users. You won't need it if you're not logged in, right?

Phew, okay. We've done a lot here. Let's take a deep breath before getting to step seven - and getting this thing online!

Continue to Part 7


<< back to blog index