Anonymous user
Django for Designers/Adding models: Difference between revisions
m
fixed a minor typo.
imported>Strixcuriosus m (fixed a minor typo.) |
|||
(35 intermediate revisions by 3 users not shown) | |||
Line 1:
== Part 3: Models
<div class="instructor">Time: 50 minutes</div>
Remember to make a new branch for section 3 based off the official branch!
Line 9 ⟶ 11:
</source>
<div class="instructor">====Introduction to databases and the ORM, or: finally, something we couldn't've done with plain HTML/CSS/JS!====
Okay, being able to design your URLs however you want, without them having to correspond to your actual file structure, is pretty neat. But besides that, we haven't done anything yet that you couldn't do with just HTML, CSS, and JavaScript. Time to fix that.
Line 19 ⟶ 21:
The models that you define here are built on top of Django's ''object-relational mapper'', or ORM. Because you define your models as Python ''objects'', you can effectively write queries against a ''relational'' database by instead coding in Python with reference to your objects. Django then ''maps'' your Python code into relational database queries in SQL.
By configuring your data access in Python, Django makes it easy to take advantage of your data layout in other places. For example, you'll see later how the ORM makes it easy to automatically generate forms that ask for exactly the information you need. It is also generally convenient to write your entire app in one language, rather than being required to switch to SQL to do queries. The ORM also handles "escaping," which makes your SQL queries handle Unicode and other strange characters properly. These conveniences let you avoid [http://en.wikipedia.org/wiki/Mojibake mojibake]-style data corruption and [http://en.wikipedia.org/wiki/SQL_injection SQL injection] attacks effortlessly.</div>
====Creating a basic model====
Line 37:
A tag also needs to know which bookmarks it applies to.
<div class="instructor">How do we represent this information in a Django model? </div>
Open up bookmarks/models.py in your editor.
Line 51:
</source>
<div class="instructor">What's going on here?
* We've made a Bookmark class that inherits from Django's model class.
* We gave it a url, which is a URLField (a field that expects a string that parses as a valid URL).
* We gave it a timestamp, which is a DateTimeField (a field that expects a Python datetime object). auto_now_add means that the field will automatically set itself to the time when the model is created.
* Finally, we gave it a title, which is a CharField (a field that expects a string of some length or less, in this case less than 200 characters). "blank=True" means that Django will permit this field to be blank. We also set a default of the empty string, so that if someone fails to specify a title we permit that.</div>
Now let's add a tag model! In the same file, below the Bookmark model:
Line 61:
<source lang="python">
class Tag(models.Model):
slug = models.CharField(max_length=50, unique=True)
</source>
<div class="instructor">The tag slug is stored in another CharField, which we've seen before. But what's this ManyToManyField? Well, it's one type of field for denoting a relationship between two models.
In relational databases, there
With a ManyToManyField, on the other hand, the relationship isn't exclusive for either of the models involved. For instance, you could imagine having a Pizza model and a Topping model. Each Pizza can have multiple Toppings, and each Topping can be on multiple Pizzas.
Line 73:
(There are other variants of these fields, but this is the basic concept. See https://docs.djangoproject.com/en/dev/ref/models/fields/#module-django.db.models.fields.related for more details for how these relationship fields work in Django.)
In this case, each Bookmark can have multiple Tags, and each Tag can apply to multiple Bookmarks. So we use a ManyToManyField to store that information.</div>
=== Creating database tables for your app (carefully, with South) ===
<div class="instructor">When you first create your models, you might not always know exactly what fields your models will need in advance. Maybe someday your bookmarks app will have preview thumbnail images! Then you would want to add another field to the model to store that information. Maybe someday you'll decide that tracking all the timestamps is silly, and want to delete that field.
Unfortunately, Django (and most database-using software) can’t figure out how to handle model changes very well on its own. Fortunately, a Django app called South that we installed earlier can handle these database changes--called ‘migrations’--for us.</div>
Now that we’ve made our first version of our models file, let’s set up our bookmarks app to work with South so that we can make migrations with it in the future!
Line 107:
Great! Now our database file knows about bookmarks and its new models, and if we need to change our models, South is set up to handle those changes. We’ll come back to South later.
<div class="instructor">IMPORTANT: You can't migrate an app if it's already been synced in the database using
<source lang="bash">
Line 113:
</source>
. But you do need to run syncdb at least once before you use south (since south itself uses syncdb to give itself space in your database). That's why it's super important that when you run syncdb, south should be listed under INSTALLED_APPS, but none of your own apps should be, and after you add your app to INSTALLED_APPS, you must not run syncdb again until after you've already set up migrations with that app.</div>
Add and commit all your work, including the migrations folder that South generated for you!
Line 125:
$ python manage.py shell</source>
<div class="instructor">We’re using this instead of simply typing “python”, because manage.py's shell sets up the project’s environment for you. “Setting up the environment” involves two things:
* Making sure bookmarks (and any other apps you might have) are on the right path to be imported.
* Setting the DJANGO_SETTINGS_MODULE environment variable, which gives Django the path to your settings.py file.</div>
Once you’re in the shell, let's explore the database API. Let's import the model classes we just wrote:
Line 141:
</source>
How many bookmarks is this? <div class="instructor">It's an empty list, so zero!</div>
Let's add a bookmark:
Line 149:
</source>
<div class="instructor">We could specify the value for timestamp as well here, but we don't have to since when we wrote its model definition, we specified that timestamp has a default value (of when the bookmark is created). We did specify a title for the bookmark, even though we didn't have to since it's an optional field.</div>
Try getting the ID number of our new bookmark 'b' by typing:
Line 155:
<source lang="python">>>> b.id</source>
Python has given you back your prompt because the value of b.id is ''None''. <div class="instructor">That is because we have not yet saved the object; to the database, it does not exist.</div>
Save your bookmark to the database. In Django, you have to call save() explicitly.
Line 161:
<source lang="python">>>> b.save()</source>
Now, try again to get the id of the Bookmark instance. <div class="instructor">Because it’s been saved, it has an ID in the database now! Even though we didn't specify one in our models.py, every saved model instance automatically has an id field.</div>
<source lang="python">>>> b.id
Line 191:
</source>
=== Fix the hideous default model representation
<div class="instructor">Wait a minute! <Bookmark: Bookmark object> is an utterly unhelpful representation of this object. Let’s fix that by editing the Bookmark model.</div> Use your text editor to open the bookmarks/models.py file and adding a __unicode__() method to both Bookmark and Tag:
<source lang="python">
Line 272:
</source>
If you try to use filter to search for a
<source lang="python">
Line 284:
>>> Bookmark.objects.get(id=1)
<Bookmark: http://www.bringinthecats.com/>
>>> Bookmark.objects.get(id=
Traceback (most recent call last):
...
Line 292:
More information on making queries with Django's ORM can be found in the Django docs at https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-objects.
<div class="instructor">Go take a break!!!</div>
== Part 3.5: Changing our mind and adding users ==
<div class="instructor">Time: 1 hour 35 minutes</div>
<div class="instructor">D'oh! You know what every social bookmarking app has, that ours doesn't have? Users!
I don't mean like the number of people using it--I mean a way to store different users' accounts and keep track of who owns which bookmarks. So let's change our app to include this feature.
Lucky for us, Django comes with an app for user accounts and authentication from the get-go! In fact, it's already installed.</div> If you look back at your settings.py file, you'll see that in INSTALLED_APPS, there is an entry for 'django.contrib.auth'.
<source lang="python">
Line 317 ⟶ 321:
</source>
That's our built-in authentication app!
<source lang="bash">
Line 331 ⟶ 335:
</source>
<div class="instructor">Whaaaaat??</div> There's already a User here. How can that be?
<div class="instructor">You might recall making a 'superuser' account when you first set up your Django project. That superuser was, in fact, created using Django's built-in auth app. </div>
What is our user account's id number?
Line 358 ⟶ 362:
Now we need to think--what kind of relationship do users and bookmarks have?
<div class="instructor">Well, a user can have multiple bookmarks. But (right now, anyway) a bookmark should only have one user. So that means that we should use a ForeignKey field to add the user to our Bookmark model.</div>
<source lang="python">
Line 394 ⟶ 398:
$ python manage.py schemamigration bookmarks --auto</source>
<div class="instructor">Note that we're now using --auto instead of --initial (which we used back when we first wrote our models and set up our app to use South).</div>
Eep! Before it will make our migration file, South wants some information from us:
Line 407 ⟶ 411:
</source>
We could in theory modify our models to specify a default value for author, or make it optional. But neither of those sound like good options
<source lang="bash">
Line 417 ⟶ 421:
We then need to come up with a default value. Well, right now there's only one User in our system who the sample bookmarks we'd entered so far could belong to--our superuser account, which (if you don't remember) had an ID number of 1.
<div class="instructor">So let's enter 1 for our default.</div>
<source lang="bash">
Line 464 ⟶ 468:
<source lang="bash">
# in django-for-designers/myproject
$
</source>
Line 500 ⟶ 502:
</source>
<div class="instructor">Note the hidden input with the name "next". This input tells the login view what URL to send the user to after they successfully log in. We have it set to '/', so it'll just take them back to the home page.</div>
We need to add a logout view too! Let's do that. Back in urls.py:
Line 509 ⟶ 511:
</source>
<div class="instructor">The dictionary after the logout URL sends some extra arguments to the logout view. Specifically it tells the logout view where to send the user after they log out. We could make a special goodbye splash page or something, but nah, let's just send them back to the home page again.</div>
Now we need to add a login/logout link to our site so people can actually use these views! We want this link to be at the top of every page, so we'll edit our base template:
Line 531 ⟶ 533:
</source>
<div class="instructor">Our template checks to see if there's a logged-in user, and if so, shows a hello message and a logout link. If the user isn't logged in, it shows a login link instead.</div>
Check http://localhost:8000 and try logging in with the superadmin username and password you created before! It should work. :)
<div class="instructor">You might be wondering--where did the 'user' variable in the template come from? If you look at views.py, you'll notice we never added a user variable to our context dictionary. So how did this happen?
The answer is in the function we are using to render our templates, render(). render() automatically uses the request we sent it to create a Django RequestContext, which contains a bunch of extra context variables that get sent along to every view that uses a RequestContext. The Django auth app adds the current user to the RequestContext automatically. This is handy, since you don't want to have to look up the current user to every single view you ever write separately, just so the nav section on your website will work everywhere!
There are other functions that return an HTMLResponse, like render(), but don't include a RequestContext. render_to_response() is one common shortcut function that doesn't include it by default; another was the HttpResponse() function we used earlier! Just something to remember--if you're trying to send a piece of data to almost every page in your web app, 1.) you probably want to use render() in your views, and 2.) you want to find a way to add your data to your app's RequestContext. (https://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.RequestContext says more about this!)</div>
Save and commit your changes in adding login/logout functionality.
Let's edit our views and templates so they use real bookmark data from our database!
Line 553 ⟶ 555:
<source lang="bash">
# in django-for-designers/myproject
$ python
</source>
Line 561 ⟶ 563:
<source lang="python">
>>>
>>> me =
</source>
Line 570 ⟶ 572:
<source lang="python">
>>>
>>> import random
>>> tag_names = ['kids', 'read_on_plane', '
>>> # Create the Tag objects
>>> for tag_name in tag_names:
... tag =
... tag.save()
...
>>> kids_urls = ['http://pbskids.org/', 'http://www.clubpenguin.com/', 'http://www.aplusmath.com/hh/index.html', 'http://kids.nationalgeographic.com/kids/activities/recipes/lucky-smoothie/', 'http://www.handwritingforkids.com/', 'http://pinterest.com/catfrilda/origami-for-kids/', 'http://richkidsofinstagram.tumblr.com/', 'http://www.dorkly.com/picture/50768/', 'http://www.whyzz.com/what-is-paint-made-of', 'http://yahooligans.com/']
>>> for kids_url in kids_urls:
... b =
... b.save()
... how_many_tags = random.randrange(len(tag_names))
... for tag_name in random.sample(tag_names, how_many_tags):
... b.tag_set.add(
...
>>> b = Bookmark.objects.get(url='http://yahooligans.com/')
>>> b.title = "Yahoooo!!!!!ligans"
>>> b.save()
</source>
Line 616 ⟶ 620:
</source>
Wait! We don't actually want to load every bookmark in our database when we go to the front page. <div class="instructor">If we have lots of bookmarks, that will get slow and unwieldy quickly.
Instead, let's show the 10 most recent bookmarks:
Line 667 ⟶ 671:
</source>
<div class="instructor">Instead of writing out each bookmark list element individually ahead of time, we are using a Django template language for loop to create a list element for each bookmark. Thus, we only have to specify the HTML formatting of our list elements once!</div>
Start your development server and look at http://localhost:8000 now. You'll see that instead of the fake HTML we had before, each link's text is the unicode representation of each bookmark in your database. Cool, huh?
That's okay, but the programmer-friendly unicode representation isn't really what we want to go there. Let's fix that:
<source lang="html4strict">
# in index.html
<a class="bookmark-link" href="{{ bookmark.url }}">
{% if bookmark.title %}{{ bookmark.title }}{% else %}{{ bookmark.url }}{% endif %}
Line 681 ⟶ 686:
If the bookmark has a title, we'll make the title the link. Otherwise we'll show the URL. We also fill in the URL on the <a> tag, to make the link work.
We also want to fill in the other metadata, like tags and author info. To do that
<source lang="html4strict">
# in index.html
<div class="metadata"><span class="author">Posted by {{ bookmark.author }}</span> | <span class="timestamp">{{ bookmark.timestamp }}</span>
{% if bookmark.tag_set.all %}| <span class="tags">
Line 695 ⟶ 701:
We check if there are any tags for the bookmark, and if so loop over the tags to put each of them in.
Additionally, we use Django's {% url %} tag to generate the URL for our tag's link. <div class="instructor">The URL tag takes first an argument which is the path of a particular view function (bookmarks.views.tag), then additional arguments for any input variables that the function expects to glean from the URL. (As you may recall, the tag view takes an argument tag_name, which is the name of the tag in question).
Why use {% url %} instead of just writing "/tags/{⁠{ tag.slug }}"? Django principle of DRY--Don't Repeat Yourself--means that we want to avoid duplicating work as much as possible. If later down the line we decided to change our URL structure so that tag pages would appear at "/bookmarks/by_tag/<tag_name>" instead, we'd have to go in and fix all these hard-coded URL patterns by hand. Using the {% url %} tag makes Django generate our URL for us, based on our urls.py file, so any changes we make automatically get propagated outward!</div>
Run
Line 714 ⟶ 720:
</source>
<div class="instructor">We tell Django that we're using a filter via the |filter_name syntax. The arguments that come after the colon are a standard Python code for describing different ways of formatting dates. You can read more about the date filter and all the different formatting codes at https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date. For our purposes, Y outputs the full four-digit year, while m and d outputs the month and the day as two digit numbers. The hyphens we put between them are included in the formatting, too -- if we wanted the date to use slashes instead, we'd simply write |date:"Y/m/d".</div>
Spin up your dev server, if you haven't got it running already, and check out your changes! Then save and commit your precious work.
====
Let's update our tag.html template to use the same formatting as index.html. Since we're using the same formatting for the list of bookmarks, and there aren't any other content block differences between index.html and tag.html, we can simply modify our tag template to inherit from index.html.
<source lang="html4strict">
{% extends 'index.html' %}
{% block subheader %}Bookmarks tagged {{ tag.slug }}{% endblock %}
</source>
Now we can go to http://localhost:8000/tags/funny and see a nicely styled list of bookmarks tagged with that tag!
What happens if we go to http://localhost:8000/tags/asdfjkl ?
<div class="instructor">Eek, a DoesNotExist error! That's not so great.</div> Our application is erroring on one of our view lines:
<source lang="python">
tag = Tag.objects.get(slug=tag_name)</source>
Fortunately, Django has a shortcut function that can help us -- get_object_or_404(). <div class="instructor">This function will attempt to get a Django model based on the parameters you give it, and if it fails, automatically throw
<source lang="python">
Line 757 ⟶ 763:
</source>
Now http://localhost:8000/tags/asdfjkl will throw a nicer 404 error. We could even make a pretty 404.html template to handle such errors! <div class="instructor">There are also shortcuts in Django for 500 and 403 (Forbidden) errors, if you want to handle and style those as well.</div>
Save and commit your error-catching work.
<!-- ==== Let me show off ====
So far, we've all only been able to access our own Django-powered sites. In this section, you will see how to access my (the instructor's) super cool bookmarks site! (We'll talk later about how you can share your code the same way. It might require firewall configuration changes on your computer, so we save that complexity for later.)
Line 769 ⟶ 775:
http://192.168.1.1:8000/
<div class="instructor">(If you are wondering exactly who can connect to the instructor's app, it is typically only people on the same wifi/wired network as her, rather than the whole Internet. This is due to the a technique in wide use called "network address translation", and it is not considered perfect security.)</div>
When you visit the instructor's app, you are interacting with the database stored on her laptop. So all the changes made by other people in the room are reflected in what you see! So try not to be too
No demo for now.
-->
<!-- Instructor git note: git push origin HEAD:pre-part-4 00-->
[[Django_for_Designers/CRUD|Next page]]
|