Django for Designers/CRUD: Difference between revisions

imported>Paulproteus
(Restore this part from a previous snapshot)
 
imported>Aldeka
 
(23 intermediate revisions by 3 users not shown)
Line 1:
== Part 4: CRUD ==
 
<div class="instructor">Time: 2 hours, 25 minutes</div>
Right now, we have a nice website that displays data from our database. Unfortunately, though, while we have a mock bookmark form in the app header, currently the only way to create new bookmarks is in the Python shell. That's not fun at all.
 
<div class="instructor">Right now, we have a nice website that displays data from our database. Unfortunately, though, while we have a mock bookmark form in the app header, currently the only way to create new bookmarks is in the Python shell. That's not fun at all.</div>
This section deals with "CRUD" functionality, which are key to all web applications. CRUD stands for Create, Read, Update, and Delete. We already have Reading bookmarks covered; now we need to handle Creating bookmarks!
 
This section deals with "CRUD" functionality, which are key to all web applications.
 
This section deals with<div class="CRUDinstructor" functionality, which are key to all web applications. >CRUD stands for Create, Read, Update, and Delete. We already have Reading bookmarks covered; now we need to handle Creating bookmarks!</div>
 
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:
 
<source lang="bash">
# in django-for-designers/myproject
$ git branch my-branch-4 origin/pre-part-4
$ git checkout my-branch-4
</source>
 
=== Django forms ===
 
<div class="instructor">Django comes with some built-in classes that make it easy to create forms with built-in validation and other functionality. There are plain django.forms classes (which are useful for encapsulating forms-related processing functions all in one place) as well as ModelForm classes that create a form based on a Django model that can automagically save instances of that model.</div> 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 ityour new ''bookmarks/forms.py'' file to import ModelForm and our models, then create a BookmarkForm class:
 
<source lang="python">
Line 19 ⟶ 31:
 
class BookmarkForm(forms.ModelForm):
class Meta: # Django convention for namespaces
model = Bookmark # form fields from Bookmark model fields
</source>
 
The<tt>model = Bookmark</tt> 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'':
 
<source lang="python">
Line 40 ⟶ 52:
context = {
'bookmarks': bookmarks,
'form': form,
}
return render(request, 'index.html', context)
</source>
 
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.
 
<source lang="html4strict">
{% block bookmark_widget %}
{% if request.user %}
<div id="new-bookmark-widget">
<form method="post">
<h3>New bookmarkBookmark</h3>
{{ form.as_p }}
<p><button id="new-bookmark-submit">Submit</button>Submit</button>
Line 61 ⟶ 73:
</source>
 
<div class="instructor">The .as_p call makes the form put each field inside a paragraph tag. There are similar methods for making the form appear inside a table or inside divs.</div>
 
The other thing we need to do here is add a CSRF token to our form, since that isn't included by default. A CSRF token is a special bit of code, built into Django, that protects your site from Cross Site Request Forgeries. It tells Django that a POST from this form really came from this form, not some other malicious site. Django makes this super easy:
 
<div class="instructor">A CSRF token is a special bit of code, built into Django, that protects your site from Cross Site Request Forgeries. It tells Django that a POST from this form really came from this form, not some other malicious site. In ''index.html'', keep editing the ''bookmark_widget'' block; Django makes this super easy.</div>
 
<source lang="html4strict">
{% block bookmark_widget %}
{% if request.user %}
<div id="new-bookmark-widget">
<form method="post">
{% csrf_token %}
<h3>New bookmarkBookmark</h3>
{{ form.as_p }}
<p><button id="new-bookmark-submit">Submit</button>Submit</button>
Line 82 ⟶ 96:
Just add the csrf_token tag inside your form, and it's good to go.
 
<div class="instructor">Also note that the form call doesn't include the external form tags or the submit button -- Django leaves those for you to write yourself. This makes things more flexible if you want to add additional elements inside the form tag, or if you want to combine multiple Django Form objects into one HTML form.</div>
 
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 our index view in views.py to make it do something with this form's data:
 
Let's edit ''views.py''. First, let's add a redirect import to the top (keeping the existing imports there):
 
<source lang="python">
from django.shortcuts import render, get_object_or_404, redirect
</source>
 
Then, let's change just the ''index'' function to make it do something with this form's data:
 
<source lang="python">
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)
</source>
 
<div class="instructor">What's going on here? We first check to see if the request method was a POST (as opposed to a GET, the usual method your browser uses when you're just reading a web page). If so, we use our ModelForm class to make an instance of the ModelForm using the data that we received via the POST from the HTML form's fields. If the form's built-in validator functions come back clean, we save the ModelForm, which makes a shiny new Bookmark in our database! Once we have made the change, we do a redirect back to the home page. (This avoids a common problem; read more on [http://en.wikipedia.org/wiki/Post/Redirect/Get Wikipedia].)</div>
 
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!
Line 111 ⟶ 134:
==== Customize your form fields ====
 
<div class="instructor">So our form works, but it doesn't look the way we originally expected. For one, there's a dropdown asking us to specify the bookmark's author. For another, we're missing a field for tags.</div> 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. InEdit just the ''index'' function in ''views.py'':
 
<source lang="python">
Line 121 ⟶ 144:
if form.is_valid():
form.save()
return redirect(index)
bookmarks = Bookmark.objects.all().order_by('-timestamp')[:10]
current_user = request.user
Line 126 ⟶ 150:
context = {
'bookmarks': bookmarks,
'form': form,
}
return render(request, 'index.html', context)
</source>
 
<div class="instructor">Now if you reload, thanks to the initial data we provided our BookmarkForm, you'll see that your account is chosen by default in the author dropdown.</div>
 
Then let's hide the author field from the user. We'll do this inby editing our ModelForm specification in ''forms.py'':
 
<source lang="python">
Line 148 ⟶ 172:
</source>
 
Now if you restart your server, you'll see the author field appears to be gone! <div class="instructor">Instead of the default text input widget, we told Django to use a hidden field for the author, so we no longer see it.</div>
 
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 the''BookmarkForm'' in ''forms.py'':
 
<source lang="python">
Line 166 ⟶ 190:
==== Save tags with our form ====
 
Finally, let's add a tags field. Edit ''forms.py'' again and continue to change the ''BookmarkForm'':
 
<source lang="python">
Line 179 ⟶ 203:
</source>
 
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,
 
<source lang="python">
from django.shortcuts import render, get_object_or_404, redirect
from bookmarks.models import Bookmark, Tag
from bookmarks.forms import BookmarkForm
Line 196 ⟶ 220:
if raw_tags:
for raw_tag in raw_tags:
raw_tag = raw_tag.strip().replace(' ', '-').lower()
raw_tagtag_slug = raw_tagurllib.replacequote(' ', '-'raw_tag)
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)
return redirect(index)
bookmarks = Bookmark.objects.all().order_by('-timestamp')[:10]
current_user = request.user
Line 208 ⟶ 231:
context = {
'bookmarks': bookmarks,
'form': form,
}
return render(request, 'index.html', context)
</source>
 
<div class="instructor">What's happening here? First, we're saving our ModelForm to save our original bookmark, as is. Then we're accessing our ModelForm's cleaned_data attribute to look for our string of comma-delineated tags. (We use cleaned_data instead of data because that one was pre-sanitized when we ran form.is_valid().) We split the string on the commas, clean up the tags by removing excess whitespace, making them all lowercase, turning spaces into hyphens, and then using urllib to quote any remaining special characters. Then we use a model shortcut function called get_or_create(). What get_or_create() does is see if there's already a tag with this slug. If so, it returns us the old tag, plus a False argument since it didn't make anything new. If not, it creates a new tag with the slug, and returns that (plus True, since it did make a new tag).</div>
 
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.
Line 223 ⟶ 246:
=== CRUD with asynchronous Javascript ===
 
<div class="instructor">That's pretty cool, but what if we don't want to make our users reload the page every time we want them to see new data? Most web apps are going to involve asynchronous Javascript in one way or another.</div>
 
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 beworking writing somewith Javascript in this section. Don't worry, you can copy and paste.)
 
First, we need to tell our templates to import some Javascript files. InEdit ''base.html'':
 
<source lang="html4strict">
Line 243 ⟶ 266:
</source>
 
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:
 
<source lang="javascript">
Line 269 ⟶ 292:
</source>
 
<div class="instructor">What this does is when the user submits the bookmarks form, it prevent the page from reloading (like it would normally), serializes all the form fields, POSTs them to localhost:8000, and upon successfully receiving a response from the server displays a message at the top of our list of bookmarks.</div>
 
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.
Line 277 ⟶ 300:
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.
 
<source lang="html4strict">
Line 294 ⟶ 317:
Note that unlike the other templates, this template doesn't inherit anything -- it's just a block of HTML with some template markup.
 
Then, rewriteopen ''index.html''and soedit just the ''content'' block so it looks like this:
 
<source lang="html4strict">
Line 306 ⟶ 329:
</source>
 
Reload the page. Nothing should have changed, in terms of how the page looks. We've just changed the structure of the templates. <div class="instructor">Using the include tag, for each bookmark in the list, the index template drops in the contents of bookmarks.html rendered with the values for athe given bookmark, for each bookmark in the list.</div>
 
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.
 
<source lang="python">
Line 326 ⟶ 349:
tag.save()
tag.bookmarks.add(new_bookmark)
returnif render(request, 'bookmark.html', {'bookmark'is_ajax(): new_bookmark})
return render(request, 'bookmark.html', {'bookmark': new_bookmark})
return redirect(index)
else:
bookmarks = Bookmark.objects.all().order_by('-timestamp')[:10]
Line 333 ⟶ 358:
context = {
'bookmarks': bookmarks,
'form': form,
}
return render(request, 'index.html', context)
</source>
 
<div class="instructor">What are we doing here? First, we're turning our request.POST test into a true choice -- we no longer go on to send back the full index.html rendered template if the request is a POST. Second, if we manage to create a bookmark successfully, we send back just our bookmark.html -- not the full index.html -- rendered with the data for our new bookmark.</div>
 
Finally, let's edit the success function in our script.js file to use this rendered data instead of printing nonsense:
Reload your development server and your web page, and try creating a bookmark. It should appear right away on the page now!
 
<source lang="javascript">
Try submitting the form with a tag but no URL. Nothing happens, right? If you check the server log in your terminal window, you'll see an error message:
success: function(data){
 
tag_slug = raw_tag$('.error').lowerhide();
<source lang="bash">
$('.bookmarks').prepend(data);
ValueError: The view bookmarks.views.index didn't return an HttpResponse object.
}
</source>
 
Reload your development server and your web page, and try creating a bookmark. It should appear right away on the page now!
Eek. Our index view doesn't 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:
 
Try submitting the form with a tag but no URL. <div class="instructor">Eek.</div> We see the browser try to render the entire page inside the page--because we're redirecting back to the normal index view!
 
Eek. 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:
 
<source lang="python">
Line 366 ⟶ 396:
tag.save()
tag.bookmarks.add(new_bookmark)
returnif render(request, 'bookmark.html', {'bookmark'is_ajax(): new_bookmark})
return render(request, 'bookmark.html', {'bookmark': new_bookmark})
return redirect(index)
else:
response = 'Errors: '
Line 382 ⟶ 414:
context = {
'bookmarks': bookmarks,
'form': form,
}
return render(request, 'index.html', context)
</source>
 
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. <div class="instructor">(If we wanted to go to more effort, we could send back the errors as JSON, teach our callback function how to tell the difference between JSON and plain HTML responses, and render the former differently, instead of sticking our form error messages in the bookmarks list. But we'll run with this for now.)</div>
 
Save and commit your JS-ification work!
 
<div class="instructor">In the real world, if you were doing lots of this sort of manipulation, instead of AHAH you might want to be using a Javascript framework such as Backbone to avoid getting confused, messy code. You'd also want to use templates for the elements that your Javascript hooks are adding and modifying. Ideally, you'd want those templates to be the same ones that your Django application used, to avoid repeating yourself! There's a lot of ways Django users deal with these problems. One way to do this is to use Django as an API data engine and do all the routing, application logic, and template rendering--at least, on the JS-heavy pages--via a Javascript-based template language. [http://django-tastypie.readthedocs.org/en/latest/ Tastypie] is a popular Django application for making APIs for this sort of thing. Another approach is to teach Django to speak the language of a JS-based templating language. Projects like [https://github.com/yavorskiy/django-handlebars django-handlebars] or [https://github.com/mjumbewu/djangobars djangobars] are examples of this approach!</div>
 
[[Django_for_Designers/Styling|Next page]]
Anonymous user