Django for Designers/CRUD
Part 4: CRUD
This section deals with "CRUD" functionality, which are key to all web applications.
So that we're all on the same page, even if you didn't get all the way through the previous part, you should do these steps right now:
# in django-for-designers/myproject
$ git branch my-branch-4 origin/pre-part-4
$ git checkout my-branch-4
Django forms
For this application, let's use ModelForms to make a form for our Bookmark model.
Making a basic, working ModelForm
Inside our bookmarks app folder, let's make a file named forms.py. Edit your new bookmarks/forms.py file to import ModelForm and our models, then create a BookmarkForm class:
from django import forms
from bookmarks.models import Bookmark
class BookmarkForm(forms.ModelForm):
class Meta: # Django convention for namespaces
model = Bookmark # form fields from Bookmark model fields
model = Bookmark ensures that generated BookmarkForm class will have a form field for every Bookmark model field--the field type being based on some defaults.
Let's add our BookmarkForm to our views and templates!
Edit bookmarks/views.py:
from django.shortcuts import render, get_object_or_404
from bookmarks.models import Bookmark, Tag
from bookmarks.forms import BookmarkForm
def index(request):
bookmarks = Bookmark.objects.all().order_by('-timestamp')[:10]
form = BookmarkForm()
context = {
'bookmarks': bookmarks,
'form': form,
}
return render(request, 'index.html', context)
Then edit index.html to take advantage of the new form object in our context. Leave most of the template intact; just change the bookmark_widget block.
{% block bookmark_widget %}
{% if user %}
<div id="new-bookmark-widget">
<form method="post">
<h3>Bookmark</h3>
{{ form.as_p }}
<p><button id="new-bookmark-submit">Submit</button>Submit</button>
</form>
</div>
{% endif %}
{% endblock %}
The other thing we need to do here is add a CSRF token to our form, since that isn't included by default.
{% block bookmark_widget %}
{% if user %}
<div id="new-bookmark-widget">
<form method="post">
{% csrf_token %}
<h3>Bookmark</h3>
{{ form.as_p }}
<p><button id="new-bookmark-submit">Submit</button>Submit</button>
</form>
</div>
{% endif %}
{% endblock %}
Just add the csrf_token tag inside your form, and it's good to go.
Spin up http://localhost:8000/, login if you're not logged in already, take a look at your beautiful new form!
Right now, if you try to submit bookmarks with this form, it'll just reload the page. That's because we haven't told the view to do anything with any form data that gets sent to it!
Let's edit views.py. First, let's add a redirect import to the top (keeping the existing imports there):
from django.shortcuts import render, get_object_or_404, redirect
Then, let's change just the index function to make it do something with this form's data:
def index(request):
if request.method == 'POST':
form = BookmarkForm(request.POST)
if form.is_valid():
form.save()
return redirect(index)
bookmarks = Bookmark.objects.all().order_by('-timestamp')[:10]
form = BookmarkForm()
context = {
'bookmarks': bookmarks,
'form': form,
}
return render(request, 'index.html', context)
Save your work, then try making some new bookmarks via your new form. The page should reload, with your new bookmark at the top of the list! High five!
Save and commit your wonderful bookmark-creating Django form.
Customize your form fields
We'll need to customize our BookmarkForm if we want it to look the way we want.
First, let's make the default author always be the currently logged-in user. Edit just the index function in views.py:
def index(request):
if request.method == "POST":
form = BookmarkForm(request.POST)
if form.is_valid():
form.save()
return redirect(index)
bookmarks = Bookmark.objects.all().order_by('-timestamp')[:10]
current_user = request.user
form = BookmarkForm(initial={'author': current_user})
context = {
'bookmarks': bookmarks,
'form': form,
}
return render(request, 'index.html', context)
Then let's hide the author field from the user. We'll do this by editing our ModelForm specification in forms.py:
from django import forms
from bookmarks.models import Bookmark
class BookmarkForm(forms.ModelForm):
class Meta:
model = Bookmark
widgets = {
'author': forms.HiddenInput(),
}
Now if you restart your server, you'll see the author field appears to be gone!
Since titles aren't even required, the URL is the most important piece of data for a bookmark. Let's change the order of the fields, so the URL comes first. Again, update theBookmarkForm in forms.py:
class BookmarkForm(forms.ModelForm):
class Meta:
model = Bookmark
fields = ('author', 'url', 'title')
widgets = {
'author': forms.HiddenInput(),
}
Save and commit your changes.
Save tags with our form
Finally, let's add a tags field. Edit forms.py again and continue to change the BookmarkForm:
class BookmarkForm(forms.ModelForm):
class Meta:
model = Bookmark
fields = ('author', 'url', 'title', 'tags')
widgets = {
'author': forms.HiddenInput(),
}
tags = forms.CharField(max_length=100, required=False)
Note that since tags isn't a field in the Bookmark model, we're going to need to make sure it gets saved separately. For that, let's go back to views.py. First, toward the top,
from django.shortcuts import render, get_object_or_404, redirect
from bookmarks.models import Bookmark, Tag
from bookmarks.forms import BookmarkForm
import urllib
def index(request):
if request.method == "POST":
form = BookmarkForm(request.POST)
if form.is_valid():
new_bookmark = form.save()
raw_tags = form.cleaned_data['tags'].split(',')
if raw_tags:
for raw_tag in raw_tags:
raw_tag = raw_tag.strip().replace(' ', '-').lower()
tag_slug = urllib.quote(raw_tag)
tag, created = Tag.objects.get_or_create(slug=tag_slug)
tag.save()
tag.bookmarks.add(new_bookmark)
return redirect(index)
bookmarks = Bookmark.objects.all().order_by('-timestamp')[:10]
current_user = request.user
form = BookmarkForm(initial={'author': current_user})
context = {
'bookmarks': bookmarks,
'form': form,
}
return render(request, 'index.html', context)
Finally, we save our tag, then add our new bookmark to its bookmarks Many-to-Many attribute to link them together.
Phew! Save your work, let your development server automatically restart, reload the page, and try adding a bookmark with some tags. It should work!
Make sure to commit your work.
CRUD with asynchronous Javascript
Let's modify our application to send our new bookmarks asynchronously to the server, and make the new bookmark appear on the page without reloading!
(Warning: semi-obviously, we'll working with Javascript in this section. Don't worry, you can copy and paste.)
First, we need to tell our templates to import some Javascript files. Edit base.html:
<!doctype html>
<html>
<head>
<title>My bookmarking app</title>
<link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,400,700,900' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="/static/css/style.css" type="text/css" media="screen" charset="utf-8">
<script src="/static/js/jquery-1.9.1.min.js"></script>
<script src="/static/js/script.js"></script>
</head>
Right now, our static/js/script.js file is blank. Let's write a quick and dirty JS function that gets triggered when someone submits the bookmarks form:
$(
function(){
$('#new-bookmark-widget form').on('submit', function(e){
e.preventDefault();
var inputs = $('#new-bookmark-widget input');
var data = {};
$.each(inputs, function(index){
var input = inputs[index];
data[input.name] = input.value;
});
$.ajax({
type: "POST",
url: '/',
data: data,
success: function(data){
$('.bookmarks').prepend('<li>WHEEE!!!</li>');
}
});
});
}
);
If you try this out, you'll see that our JS-y form makes real bookmarks, just like the old version of our form did! However, it's putting junk in our bookmarks list on the page; we have to reload to actually see the bookmark our ajax call created.
There's several ways we could fix this. We could make our views.py send back a JSON serialization of our bookmark data, and have our JS file turn that into a list element somehow (using either a JS template or a giant string). Or we could have Django do the template rendering for us, and send our JS script the raw HTML we want it to use. This is called AHAH (Asychronous HTML and HTTP), or sometimes PJAX.
For the sake of speed and simplicity, we'll go with the latter. First, though, we need to refactor our templates a little.
Create a new template called bookmark.html, and paste in the bookmark list element from index.html in there.
<li>
<a class="bookmark-link" href="{{ bookmark.url }}">{% if bookmark.title %}{{ bookmark.title }}{% else %}{{ bookmark.url }}{% endif %}</a>
<div class="metadata"><span class="author">Posted by {{ bookmark.author }}</span> | <span class="timestamp">{{ bookmark.timestamp|date:"Y-m-d" }}</span>
{% if bookmark.tag_set.all %}| <span class="tags">
{% for tag in bookmark.tag_set.all %}
<a href="{% url 'bookmarks.views.tag' tag.slug %}">{{ tag.slug }}</a></span>
{% endfor %}
{% endif %}
</div>
</li>
Note that unlike the other templates, this template doesn't inherit anything -- it's just a block of HTML with some template markup.
Then, open index.htmland edit just the content block so it looks like this:
{% block content %}
<ul class="bookmarks">
{% for bookmark in bookmarks %}
{% include 'bookmark.html' %}
{% endfor %}
</ul>
{% endblock %}
Reload the page. Nothing should have changed, in terms of how the page looks. We've just changed the structure of the templates.
Now we're ready to teach our view to send back a partial bit of HTML. Open views.py and edit just the index function to be as follows.
def index(request):
if request.method == "POST":
form = BookmarkForm(request.POST)
if form.is_valid():
new_bookmark = form.save()
raw_tags = form.cleaned_data['tags'].split(',')
if raw_tags:
for raw_tag in raw_tags:
raw_tag = raw_tag.strip()
raw_tag = raw_tag.replace(' ', '-')
raw_tag = urllib.quote(raw_tag)
tag_slug = raw_tag.lower()
tag, created = Tag.objects.get_or_create(slug=tag_slug)
tag.save()
tag.bookmarks.add(new_bookmark)
if request.is_ajax():
return render(request, 'bookmark.html', {'bookmark': new_bookmark})
return redirect(index)
else:
bookmarks = Bookmark.objects.all().order_by('-timestamp')[:10]
current_user = request.user
form = BookmarkForm(initial={'author': current_user})
context = {
'bookmarks': bookmarks,
'form': form,
}
return render(request, 'index.html', context)
Finally, let's edit the success function in our script.js file to use this rendered data instead of printing nonsense:
success: function(data){
$('.error').hide();
$('.bookmarks').prepend(data);
}
Reload your development server and your web page, and try creating a bookmark. It should appear right away on the page now!
Try submitting the form with a tag but no URL.
We see the browser try to render the entire page inside the page--because we're redirecting back to the normal index view!
Our index view doesn't properly handle the case where the request is a POST, but the form doesn't validate, and Django noticed this and returned an error. Let's fix this by editing the index function in views.py again:
def index(request):
if request.method == "POST":
form = BookmarkForm(request.POST)
if form.is_valid():
new_bookmark = form.save()
raw_tags = form.cleaned_data['tags'].split(',')
if raw_tags:
for raw_tag in raw_tags:
raw_tag = raw_tag.strip()
raw_tag = raw_tag.replace(' ', '-')
raw_tag = urllib.quote(raw_tag)
tag_slug = raw_tag.lower()
tag, created = Tag.objects.get_or_create(slug=tag_slug)
tag.save()
tag.bookmarks.add(new_bookmark)
if request.is_ajax():
return render(request, 'bookmark.html', {'bookmark': new_bookmark})
return redirect(index)
else:
response = 'Errors: '
for key in form.errors.keys():
value = form.errors[key]
errors = ''
for error in value:
errors = errors + error + ' '
response = response + ' ' + key + ': ' + errors
return HttpResponse('<li class="error">' + response + '</li>')
else:
bookmarks = Bookmark.objects.all().order_by('-timestamp')[:10]
current_user = request.user
form = BookmarkForm(initial={'author': current_user})
context = {
'bookmarks': bookmarks,
'form': form,
}
return render(request, 'index.html', context)
Now in the error case, we cheekily send back some HTML containing the errors that Django found, so there's no circumstance in which this view fails to provide some sort of response.
Save and commit your JS-ification work!