Let's Take Notes With Django - Part 5

Let's Take Notes With Django - Part 5

part of the Startproject To Deployment - Django Tutorial series

show all in series

back to index

DjangoNote is taking shape! We've got a way to log in through our homepage and an index view inside our Notes application that shows us all of our notes - not that we have any to look at yet. Don't worry - that's going to change soon! Let's look back to our current list of goals for this app:


  1. See all of our existing notes done!

  2. Add a new note

  3. Edit and delete existing notes

  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

Let's implement a way to add a new note. We defined a ModelForm earlier in notes/forms.py, and now we'll get to use it! Head into notes/views.py and get started defining our add_note view. We'll add some more import statements (they're all listed below for convenience), and define our new view function below our existing index_view function:

from django.shortcuts import render
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from notes.models import Note, Tag
from notes.forms import NoteForm

def add_note(request):

if request.method == 'POST':
form = NoteForm(request.POST)
if form.is_valid():
form.save()
messages.add_message(request, messages.INFO, 'Note Added!')
return HttpResponseRedirect(reverse('notes:index'))

else:
form = NoteForm()

return render(request, 'notes/addnote.html', {'form':form})

Thanks to our modelform, NoteForm, this view is short and simple. First we check to see if we're dealing with a GET or POST request. If it's a GET request, we set form equal to an empty instance of our NoteForm. Then we drop to the bottom of the function and render the notes/addnote.html template with a context dictionary including our form.

If we have a POST request, we set form equal to an instance of our modelform filled with the data from the request - NoteForm(request.POST). Then we call is_valid() on that form. This runs through the default modelform validation, saving us an amazing amount of headache on custom validation and security. Remember, we're writing this stuff to our database so the input needs to be sanitary - our modelform is handling that for us. If the form is indeed valid, we'll call its save() method to write the information into the database. Then we'll use the messaging framework to attach a success message to the request object and send it over to the index view for our notes app.

We have pulled a lazy trick by leaving our return render... statement on its own indent level, outside of the else block. If form.is_valid() evaluates to False, we'll drop down to the return render... line and re-render the same page with the form populated with our invalid input, along with error messages thanks to Django's built-in error handling. This saves us from duplicating ourself and writing code to handle both a GET request and a failed form validation - for many use cases, this will handle both just fine.

Let's create templates/notes/addnote.html to display our new form.

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

<div class="jumbotron">
<h1 align='center'>Create New Note!</h1>
</div>

<div class='col-md-12'>
<form action="" method="post">{% csrf_token %}
{{form.as_p}}
<input type='submit' value='Create New Note!' class='btn btn-primary btn-lg btn-block' />
</form>
</div>

{% endblock body %}

That was fast. We didn't have to write much code at all for the form - Django's templating engine knows how to handle form objects when you pass them directly to the template's context. You still have to supply your own form tags, CSRF token and submit button, but the modelform's fields are all displayed in <p> tags simply by using {{form.as_p}}. You can display the form as a table instead if you like with {{form.as_table}}, iterate through the fields manually with {% for field in form %}, or anything else you might like. For now, we'll keep it simple.

Now we just need to import our new view into our notes/urls.py and add a new pattern.

from django.conf.urls import patterns, include, url
from django.contrib import admin
from notes.views import index_view, add_note

urlpatterns = patterns('',
url(r'^$', index_view, name='index'),
url(r'^addnote/', add_note, name='addnote'),
)

Alright, let's go to 127.0.0.1/notes/addnote and ... okay. Well. It's there, but it's disgustingly ugly even for my tastes. We're going to add this inside the {% block body %} of our addnote.html file:

<script type='text/javascript'>
$('input[type="text"]').addClass('form-control');
$('textarea').addClass('form-control');
</script>

Right, this is slightly less appalling but still pretty gnarly. That multiple select widget is especially displeasing to the eye, but we're going to leave it alone for now. It'll get what's coming to it soon enough. We don't have any tags yet, anyway.

Let's try creating a note. If all goes well, we'll enter a note into the database then get redirected to the index view of our Notes app where our new note will be waiting for us alongside a success message. We'll enter a label of "Test Note 1" and a body that looks like this:

Test Body 1

Linebreak

We can't enter any tags because we haven't defined any yet, so that's all for now. Hit submit and - uh oh. We got an error message that tells us the Tags field is required. Hmm ... sometimes we might want to create a note without any tags. Let's make a quick change to our model definition in notes/models.py:

class Note(models.Model):
...
tags = models.ManyToManyField('Tag', related_name='notes', blank=True)
...

Adding blank=True means modelforms for this model won't treat this field as required. Let's save, refresh and try to submit that note again - success!

We've been redirected back to the index page for our Notes app, and the yellow banner telling us we have no notes to display is gone. Instead, we see our first note - huzzah! At the top of the page there's a message heralding our success. It's a good day. We can also see that the linebreaks we included in our note have been preserved and are being rendered properly. If you recall, our notes/index.html renders our notes like this:

<h3 class="page-header">{{note.label}}</h3>
{{note.body|linebreaks}}

If we remove the linebreaks filter and refresh the page, we can see that the body of our note now displays on a single line. Nothing has changed in the database, and we can bring back the linebreaks by re-adding the filter. Convenient!

Let's make it easy to add a note from anywhere by adding a link in our navigation bar. Head into our templates/base.html file and find this bit in our navigation bar:

<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li>
<a href="#">notes</a>
</li>
</ul>
</div>

Let's fix that link to our Notes app to point at its index page and add a link to the "add note" page.

<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li>
<a href="{% url 'notes:index' %}">notes</a>
</li>
<li>
<a href="{% url 'notes:addnote %}">add note</a>
</li>
</ul>
</div>

Rejoice in this convenience by creating a few more notes. Hey, the index page shows the newest notes at the bottom - it'd be nicer to have the newest notes at the top, right? Let's go back to our index_view and add an extra bit to our query:

notes = Note.objects.all().order_by('-timestamp')

Save and refresh at 127.0.0.1/notes - that order makes a little more sense.

Now let's figure out a way to edit our existing notes. We could create a brand new view to do this, but let's be lazy and re-use the addnote view we've already got in place. That way we have one less pattern to write, one less template to create, one less view to make ... this is really appealing to my lazy side.

Editing existing model instances with a modelform is simple: you just pass an "instance" argument when you instantiate and process your form. The instance should be the object you'd like to edit. So, object number one is to provide a way for our existing addnote view to grab a note from the database and work with it instead of creating something new.

We could do this by adding a querystring to the end of our URL when accessing the addnote view. In other words, let's make it our goal to add a note at :

127.0.0.1/notes/addnote

And edit a note (with id=1) at:

127.0.0.1/notes/addnote/?id=1

In order to do this, we'll want to:


  1. Modify our addnote view to look for a querysting called "id," and

  2. Serve up our existing modelform with an instance of the note object corresponding to our id.

Let's give it a go. We'll modify our add_note view function like so:

def add_note(request):
id = request.GET.get('id', None)
if id is not None:
note = Note.objects.get(id=id)
else:
note = None

if request.method == 'POST':
form = NoteForm(request.POST, instance=note)
if form.is_valid():
form.save()
messages.add_message(request, messages.INFO, 'Note Added!')
return HttpResponseRedirect(reverse('notes:index'))

else:
form = NoteForm(instance=note)

return render(request, 'notes/addnote.html', {'form':form})

Nice! If we go to 127.0.0.1/notes/addnote, everything looks just the same as it did before. Since there's no "id" querystring in the URL, note = None which means we're passing None to the form's instance argument. This is effectively the same as not supplying an instance argument at all - just as we wanted.

But if we go to 127.0.0.1/notes/addnote/?id=1, our form is automatically populated with the information from the note with id=1. We can edit this information and save it like normal. When we get back to our index view, we see that our note has been edited successfully.

One thing, though - what if we try to access a note that doesn't exist, like 127.0.0.1/notes/addnote/?id=9999999? We hit a server error, which - in a production environment with debug = False - is ugly and inelegant. Let's change this:

...
note = Note.objects.get(id=id)
...

To this (note the extra import - we'd already gotten render, and our new helpful shortcut comes from the same place):

from django.shortcuts import render, get_object_or_404
...
note = get_object_or_404(Note, id=id)
...

Now if we try to go to 127.0.0.1/notes/addnote/?id=9999999 we get a 404 page, which makes more sense.

What if we want to delete one of our notes? It'd make sense to be able to do that from the same place we can edit a note, so let's include that functionality here. We'll add a second form to the same addnote page that lets us delete a note.

This immediately brings two things to mind: first, we only want the delete button to show up when we're in "edit mode," not in "create mode." Second, our view function now only knows how to process a POST request in one way. We need to add an intermediary step to tell our view what kind of form it's getting before it starts processing.

Below the div containing our current form in addnote.html we'll add a second form:

...
{% if note %}
<div class="col-md-12" style="padding-top:15px">
<form action="" method="POST">{% csrf_token %}
<input type="hidden" name="control" value="delete" />
<input type="submit" value="Delete" class="btn btn-lg btn-block btn-danger" />
</form>
</div>
{% endif %}
...

We'll need to pass the same note instance to the template context that we passed as our instance argument to our modelform. That allows our {% if note %} block to render this content only when we've got a model to delete - it won't show up when we're creating a brand new note. We also added a hidden input named "control" with a value of "delete" which will come in handy momentarily.

We'll edit our view like so:

def add_note(request):

id = request.GET.get('id', None)
if id is not None:
note = get_object_or_404(Note, id=id)
else:
note = None

if request.method == 'POST':
if request.POST.get('control') == 'delete':
note.delete()
messages.add_message(request, messages.INFO, 'Note Deleted!')
return HttpResponseRedirect(reverse('notes:index'))

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

else:
form = NoteForm(instance=note)

return render(request, 'notes/addnote.html', {'form':form, 'note':note})

Check out how we're handling things when we receive a POST request now. First, we look to see if that POST request includes a variable called "control" named "delete" - that's the hidden input we included earlier. If it's detected, we take our existing note model and call its delete method then redirect to the index page with a success message. We also added 'note':note to our render's context dictionary. Test it out by going to 127.0.0.1/addnote/?id=1 - you should see your first test note along with a nice red delete button. Hit it, and boom - no more note!

For convenience, let's add an "edit" link to each of our various notes on our index page. Template tags make this simple - you can do it! Remember, they work inside of HTML tags, too. If a link to our addnote page looks like this: href="{% url 'notes:addnote' %}", then we can append the ID for a given note like this: href="{% url 'notes:addnote' %}?id={{note.id}}".

Let's evaluate our app's progress so far:


  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 rocking and rolling now. Let's tackle tags. Our Tag model doesn't have a lot of moving parts - in fact, the only thing we've got is a CharField called label. With that in mind, there's not a lot of sense having an index page dedicated just to our tags. If we just list all of our tags on the main index page, that'll work just fine. Let's collect all of our tags and pass the queryset to the context dictionary of our index_view:

def index_view(request):
notes = Note.objects.all().order_by('-timestamp')
tags = Tag.objects.all()
return render(request, 'notes/index.html', {'notes':notes, 'tags':tags})

Now we can access the tags queryset in our template, thanks to the magic of context. Let's modify our notes/index.html template so that our notes take up three-quarters of the page, reserving the final quarter for displaying our tags. First, let's change the div just beneath our jumbotron from this:

<div class='col-md-12'>

To this:

<div class='col-md-9'>

Now we've got a little room to play with - let's use that to display a list of our tags. We won't change anything else in this div beyond swapping the 12 for a 9, so let's skip down and create a new div underneath.

<div class='col-md-3'>
<h3 class="page-header" align='right'>Tags</h3>
<ul>
{% if tags %}
{% for tag in tags %}
<li>{{tag}}</li>
{% endfor %}
{% else %}
<div class='alert alert-warning'>No tags yet!</div>
{% endif %}
</ul>
</div>

Now we're iterating through our tags and displaying them one by one in an unordered list. Note that we could use {{tag}} instead of {{tag.label}} - that's because we set the label attribute as the unicode representation of the object. Using {{tag}} by itself therefore displays that label we want. We also reused the same {% if ... else %} logic from earlier, displaying a note alerting us to the absence of tags if we haven't added any yet.

Now let's create a view to add a new tag. Spoiler alert: we're also going to configure this view to look for an optional ID parameter in case we want to edit or delete a note. In fact, we're going to re-use the logic from our add_note view almost to the letter. The only real change is to the model and template we reference (and of course, we'll import our tag form instead of our note form):

from notes.forms import TagForm
def add_tag(request):
id = request.GET.get('id', None)
if id is not None:
tag = get_object_or_404(Tag, id=id)
else:
tag = None

if request.method == 'POST':
if request.POST.get('control') == 'delete':
tag.delete()
messages.add_message(request, messages.INFO, 'Tag Deleted!')
return HttpResponseRedirect(reverse('notes:index'))

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'))

else:
form = TagForm(instance=tag)

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

(Angry class-based view fans are frothing by now because we are violating DRY with all this repeated code in the view. That's okay. I'd rather have a few lines repeated in a function or two than have a bunch of bloated classes with methods and features I don't need littering my entire project.)

Our addtag.html file is almost an exact copy of our addnote.html - we'll only make three small alterations. Two changes we make are to the header in the Jumbotron and to the value of the first submit button - we change each from "Create New Note" to "Create New Tag." The third, and final, tweak is changing {% if note %} to {% if tag %}. You'll want to create that addtag.html file as well, inside the templates/notes directory.

Finally, we need to add a new URL pattern in our notes/urls.py. Don't forget to import the new view as well!

...
from notes.views import index_view, add_note, add_tag
...
url(r'^addtag/', add_tag, name='addtag'),
...

Navigate to 127.0.0.1/notes/addtag and you'll be able to create a tag! Create a couple, then try editing one by navigating to 127.0.0.1/notes/addtag/?id=1. Make sure the delete function is working, too. Looks like everything is in order! Let's take another look at our list of goals:


  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 getting close! In part six we'll explore REST-ful searching. Let's celebrate the end of part 5 by adding our "add tag" page to our navigation bar, after "notes" and "add note." Head back to base.html and add:

<li>
<a href="{% url 'notes:addtag' %}">add tag</a>
</li>

Continue to Part 6


<< back to blog index