There is nothing here yet. Let's start building!
git
: Distributed Revision Controlgit
pip
virtualenvwrapper
npm
bower
Normally, to start a Django project:
$ cd {WORKING_DIR}
$ django-admin.py startproject mysite
Today however, we'll use a boilerplate project
$ cd YourFavouriteDirectory
$ git clone eleyine.github.com/pyladies-django-workshop
$ cd pyladies-django-workshop
# rewind to the start of the project
$ git reset --hard boilerplate
pip
: Python Package Managergit
pip
virtualenvwrapper
npm
bower
Installs and manages software packages written in Python
# Installs package into site-packages
# directory of your 'default' Python lib
$ pip install SomePackage
[...]
Successfully installed SomePackage
..but different programs may need different versions of packages.
virtualenvwrapper
: Virtual Environment Managergit
pip
virtualenvwrapper
npm
bower
Wrappers for creating and deleting virtualenv
s i.e. isolated Python environments, without introducing conflicts in dependencies between projects.
https://virtualenvwrapper.readthedocs.org
# in pyladies-django-workshop
$ mkvirtualenv pyladies-django
$ workon pyladies-django
(pyladies-django) $ pip install -r requirements.txt
(pyladies-django) $ pip list
(pyladies-django) $ python
>>> import django
npm
: Node package managergit
pip
virtualenvwrapper
npm
bower
npm
is a package manager for Javascript
angular
, less
, grunt
, gulp
, coffee-script
and many more!
We will need it for the following installation step.
# in pyladies-django-workshop
$ npm install
bower
: Package Manager for the webgit
pip
virtualenvwrapper
npm
bower
bower
finds, downloads, installs and saves frameworks, libraries, assets and utilities for front-end development using a flat dependency tree.
# in pyladies-django-workshop
$ sudo npm install -g bower
# install packages in manifest file 'bower.json'
$ bower install
$ bower list
# in pyladies-django-workshop
(pyladies-django) $ python manage.py migrate
(pyladies-django) $ python manage.py runserver
Visit http://localhost:8000/, what do you see?
What do all of these have in common?
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
pyladies-django-project/
manage.py
pyladies_django_project/
__init__.py
settings.py
urls.py
wsgi.py
Project container, can be renamed.
Command-line utility (e.g. startproject, runserver, startapp)
Python package for your project. Use pyladies_django_project.fileName
to import anything inside.
Project configuration. Most of it is done in the boilerplate project.
"Table of contents" of your site.
Entry-point for WSGI-compatible web servers to serve your project.
$ python manage.py startapp notes
Edit pyladies_django_workshop/settings.py
INSTALLED_APPS = (
# previous apps
'notes',
)
notes/
__init__.py
admin.py
migrations/
__init__.py
models.py
tests.py
views.py
Note
Tag
NoteCollection
Edit notes/models.py
from django.db import models
class Note(models.Model):
#todo
title = models.CharField(max_length=100)
content = models.TextField()
class Tag(models.Model):
#todo
keyword = models.CharField(max_length=50)
class NoteCollection(models.Model):
name = models.CharField(max_length=50)
max_length
used not only in the database schema, but also in validation
$ python manage.py makemigrations
$ python manage.py migrate
Tell Django you've made changes to your models. Changes are stored as migrations.
Migrations live inside notes/migrations/
and are designed to be human-readable.
Tell Django to a run the migrations for you and manage your database schema.
Object Relational Mapping (ORM) -> Database Agnostic
cd
to project, workon pyladies-django
and then:
(pyladies-django) $ python manage.py shell
>>> from notes.models import Note
>>> Note.objects.all()
[]
>>> n = Note(title='My First Note', content='My first content')
>>> n.save()
>>> n.id
1
>>> n.title
"My First Note"
>>> n.title = "This is not really my first Note"
>>> n.save()
>>> Note.objects.all()
[<Note: Note object>]
Query all Note objects in database
Create a new Note object.
Save object into database.
Object now has an id (note that objects only have ids after they've been saved).
Access and change fields via Python attributes.
Ewww
Edit notes/models.py
class Note(models.Model):
# ...
def __unicode__(self):
return '{0}'.format(self.title)
class Tag(models.Model):
# ...
def __unicode__(self):
return '#{0}'.format(self.keyword)
class NoteCollection(models.Model):
# ...
def __unicode__(self):
return '{0}'.format(self.name)
Edit notes/models.py
class Note(models.Model):
title = models.CharField(max_length=100, default='')
content = models.TextField(default='')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_pinned = models.BooleanField(default=False)
is_archived = models.BooleanField(default=False)
# ...
class Note(models.Model):
# ...
COLOR_CHOICES = (
('white', 'White'),
('red', 'Red'),
('yellow', 'Yellow'),
# ...
)
color = models.CharField(max_length=9,
choices=COLOR_CHOICES,
default=COLOR_CHOICES[0][0])
class Meta:
# when ordering by boolean, false comes first (db dependant)
ordering = ('-is_pinned', 'is_archived', '-updated_at' )
Other useful Meta fields: unique_together, verbose_name, verbose_name_plural, db_table
For the complete reference: https://docs.djangoproject.com/en/1.7/ref/models/fields/
Edit notes/models.py
class Note(models.Model):
# ...
note_collection = models.ForeignKey('NoteCollection',
null=True, related_name='notes')
class Tag(models.Model):
# ...
notes = models.ManyToManyField('Note')
ForeignKey
: Many-To-One Relation Each Note is related to one NoteCollection. Each NoteCollection has many Notes. not the best example I know...
ManyToManyField
: Many-To-Many Relation Each Note can have many Tags. Each Tag can appear in many Notes.
related_name
: allows us to rename the related object manager (more on this later)
Remember to do your migrations.
(pyladies-django) $ python manage.py makemigrations
(pyladies-django) $ python manage.py migrate
or
(pyladies-django) $ python manage.py flush
(pyladies-django) $ python manage.py migrate
(pyladies-django) $ python manage.py shell
>>> from notes.models import Note, Tag
>>> Note.objects.create(title='My First Note')
<Note: My First Note>
>>> n = Note.objects.get(id=1)
>>> t = Tag.objects.create(keyword='pyladies')
>>> n.tags
<django.db.models.fields.related.ManyRelatedManager object at 0x10be21550>
>>> n.tags.all()
[]
>>> n.tags.add(t)
>>> Note.objects.first().tags.all()
[<Tag: #pyladies>]
Shortcut to create Python object and save into database.
Returns single object matching the query. Throws an error if query matches more than one object.
This is the related manager we renamed earlier. If we hadn't done so, this Note's tag manager would be n.tag_set
(default is <RelatedModel>_set
).
Using the related manager, add tag to note.
The change has been saved!
(pyladies-django) $ python manage.py createsuperuser
or execute bash script
(pyladies-django) $ git reset --hard solAdmin0
(pyladies-django) $ chmod u+x ./scripts/createsuperuser
(pyladies-django) $ ./scripts/createsuperuser
(pyladies-django) $ python manage.py runserver
Go to http://127.0.0.1:8000/admin/
If you used the script, use admin/pass for login.Edit notes/admin.py
from django.contrib import admin
from notes.models import Note, Tag, NoteCollection
admin.site.register(Note)
admin.site.register(Tag)
admin.site.register(NoteCollection)
Go to http://127.0.0.1:8000/admin/
Very customizable, read more about it here https://docs.djangoproject.com/en/1.7/intro/tutorial02/Take a guess at which views we need!
index
page displays all notesdetail
page displays single notearchiveIndex
page displays archived notespinnedIndex
page displays pinned notessearch
page displays all notes for a given tagnoteAction
handles note creation and updateEdit notes/views.py
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, world. You're at the notes index.")
Edit notes/urls.py (create it if it doesn't exist)
from django.conf.urls import patterns, url
from notes import views
urlpatterns = patterns('',
url(r'^$', views.index, name='index'),
)
text/html; charset=utf-8
).
Admittedly, this is simple. But views can also read records from a database (or not), use Django's template system (or not), generate PDF files, output XML or JSON, create ZIP files... The sky is the limit (or not?).
All Django wants is that HttpResponse. Or an exception.
This part is wiring our index
view to a url using the url()
function.
url()
is passed 4 arguments: regex
, view
(required) and name
, kwargs
(optional).
regex
matches patterns in urls. Django starts at the first regular expression and makes its way down the list.
When a regex matches, it calls the associated view
function with an HttpRequest
object as the first argument as well as any additonal captures (more about this later).
You can name
your url and refer to it elsewhere in Django. This way you can change your url conf and only edit a single file.
Edit pyladies_django_project/urls.py
from django.conf.urls import patterns, include, url
from django.contrib import admin
urlpatterns = patterns('',
url(r'^notes/', include('notes.urls')),
url(r'^', include('notes.urls')),
url(r'^admin/', include(admin.site.urls)),
)
Go to http://127.0.0.1:8000/notes/
Go to http://127.0.0.1:8000/
Add these lines to notes/urls.py
urlpatterns = patterns('',
# ...
# ex: /archive.
url(r'^archive/$', views.archive_index, name='archive'),
# ex: /pinned/
url(r'^pinned/$', views.pinned_index, name='pinned'),
# ex: /5/
url(r'^(?P<note_id>\d+)/$', views.detail, name='detail'),
# ex: /create/
url(r'^create$', views.create_note, name='create_note'),
# ex: /5/edit
url(r'^(?P<note_id>\d+)/edit$', views.edit_note, name='edit_note'),
)
Using parentheses around a pattern “captures” the text matched by that pattern and sends it as an argument to the view function
P<note_id> defines the name that will be used to identify the matched pattern; and \d+ is a regex to match a sequence of digits (i.e., a number)
Update notes/views.py to match notes/urls.py
def archive_index(request):
return HttpResponse("You're looking at the archive.")
def pinned_index(request):
return HttpResponse("You're looking at your pinned notes.")
def detail(request, note_id):
return HttpResponse("You're looking at note %s." % note_id)
def create_note(request):
return HttpResponse("You're creating a note.")
def edit_note(request, note_id):
return HttpResponse("You're editing note %s." % note_id)
Notice the note_id argument for some of the views.
Oh, right.
Update the index function in notes/views.py
from notes.models import Note
def index(request):
notes = Note.objects.all()
output = ' / '.join([note.title for note in notes])
return HttpResponse(output)
Go to http://127.0.0.1:8000/
How to avoid hard-coding the page's design in the view?
Yep, templates
Open templates/base.html
Not Google Keep
{% include 'stylesheets.html' %}
{% include 'navbar.html' %}
{% block content %}
There is nothing here yet. Let's start building!
{% endblock %}
{% include 'javascripts.html' %}
{% block scripts %}{% endblock %}
In templates/notes/index.html (git reset --hard simpleIndex
)
{% extends "base.html" %}
{% block content %}
{% for note in notes %}
{{ note.title}}
{% empty %}
{% if search %}
No matching notes.
{% else %}
Your {% if archive %}archived{% endif %} notes will appear here.
{% endif %}
{% endfor %}
{% endblock %}
Update the index function in notes/views.py
from django.template import RequestContext, loader
from notes.models import Note
def index(request):
template = loader.get_template('notes/index.html')
context = RequestContext(request, {
'notes': Note.objects.all(),
})
return HttpResponse(template.render(context))
Update the index function in notes/views.py
from django.shortcuts import render
from notes.models import Note
def index(request):
context = {
'notes': Note.objects.all(),
}
return render(request, 'notes/index.html', context)
git reset --hard prettyIndex
$ python manage.py generatenotes <number>
to generate notes
Let's make some notes
Update templates/notes/index.html to include the form (this is a markup-simplified version)
git reset --hard noteForm
{% if search %}
{% else %}
{% include "notes/note-form.html" %}
{% endif %}
{% for note in notes %}
{% include "notes/note-container.html" %}
{% endfor %}
Edit templates/notes/note-form.html (this is a markup-simplified version)
git reset --hard noteForm
from django.http import HttpResponseRedirect
from django.contrib import messages
def create_note(request):
if request.method == 'POST':
allowed_fields = ('title', 'content', 'color', )
create_options = {}
for name, value in request.POST.iteritems():
if name in allowed_fields:
create_options[name] = value
Note.objects.create(**create_options)
messages.success(request, 'Your note has been saved.')
return HttpResponseRedirect(reverse('index'))
Form data is usually sent via POST because the information is sensitive and/or not to be limited by size. Here, we enfore note creation to be done via POST.
request.POST
is a dictionary-like object that lets you access submitted data by key name. request.POST
values are always strings (so careful when dealing with integers, booleans etc.)
Nifty built-in messages API.
You should always return an HttpResponseRedirect
after successfully dealing with POST data. HttpResponseRedirect
takes a single argument: the url to redirect to.
reverse()
helps avoid having to hardcode a URL in the view function, uses the name attribute we specified in notes/urls.py instead.
Yikes! We'll see better ways to do this.
git reset --hard createNoteView
Ideally, we want tags to be created/updated and saved at the same time as Note content.
Signals allow certain senders [Note] to notify a set of receivers [updateTags] that some action has taken place. They’re especially useful when many pieces of code may be interested in the same events [Note content creation/update].
Edit notes/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from notes.models import Note, Tag
@receiver(post_save, sender=Note)
def update_created_note_tags(sender, instance, created=False, **kwargs):
note_instance = instance
if created:
tag_instances = []
for t in Note.parsed_tags(note_instance.content):
tag_instance, created = Tag.objects.get_or_create(keyword=t)
tag_instances.append(tag_instance)
note_instance.tags.add(*tag_instances)
If Note is created, create associated Tags.
By using this decorator, we register a receiver function update_created_note_tags() that gets callled each time a Note object is saved (after it is saved)
Other signals include pre_save, m2m_changed, pre_delete, pre_migrate etc.
What if note content is updated? What if some tags are no longer associated with any notes? This is left as an exercise. (solution in solSignals
, python manage.py test
in preSignals
as guide)
git reset --hard solSignals
The short answer:
index
, archive_index
and pinned_index
is essentially the same. It involves (a) getting data from a database according to a parameter, (b) loading a template and (c) returning the rendered template
We are going to refactor the following code in notes/views.py
def index(request):
context = {
'notes': Note.objects.all(),
'colors': Note.COLOR_CHOICES
}
return render(request, 'notes/index.html', context)
def archive_index(request):
return HttpResponse("You're looking at the archive.")
def pinned_index(request):
return HttpResponse("You're looking at your pinned notes.")
Edit notes/urls.py
urlpatterns = patterns(
# ...,
url(r'^$', views.IndexView.as_view(), name='index'),
# ex: /archive.
url(r'^archive/$', views.ArchiveIndexView.as_view(), name='archive'),
# ex: /pinned/
url(r'^pinned/$', views.PinnedIndexView.as_view(), name='pinned'),
# ...
)
Class-based views allow us to reuse code and harness inheritance and mixins.
Remember, url()
takes a callable as a second argument that takes a request and returns a response.
In Django, all Class-based views inherit from View
class which handles linking to the view into the URLs through as_view()
.
Arguments passed to as_view()
override attributes set on the class. Example: if you simply want to render a tempate, use: TemplateView.as_view(template_name="about.html")
Edit notes/views.py
from django.views import generic
class IndexView(generic.ListView):
template_name = 'notes/index.html'
context_object_name = 'notes'
index_type = 'index'
filter_options = {'is_archived': False}
def get_queryset(self):
"""Return all non archived notes."""
return Note.objects.filter(**self.filter_options)
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
context['colors'] = Note.COLOR_CHOICES
context['index_type'] = self.index_type
return context
ListView
is a generic view for pages representing a list of objects.
It inherits methos and attributes from MultipleObjectTemplateResponseMixin
, TemplateResponseMixin, BaseListView, MultipleObjectMixin, View
The Method flowchart for this view is well-documented, but for our purposes, we only need to override two methods.
get_queryset()
Get the list of items for this view. Must be an iterable or queryset.
get_context_data()
Returns context data for displaying the list of objects.
context_object_name
: variable name that will be used to contain the list of data that this view is manipulating (defaults to note_list
)
Attribute I specified to aid in filtering and which will be overridden in the other index views.
Attribute I specified for templating purposes (determining which tab is active in the sidebar).
Edit notes/views.py
class ArchiveIndexView(IndexView):
index_type = 'archive'
filter_options = {'is_archived': True}
class PinnedIndexView(IndexView):
index_type = 'pinned'
filter_options = {'is_pinned': True, 'is_archived': False}
See? We barely wrote any code for these additional views.
See them in action at http://localhost:8000/
First we need a DetailView to display a form that is specific to a note. This is required to pre-fill fields and pass on the note id.
Editnotes/views.py
class EditFormView(generic.DetailView):
template_name = 'notes/note-form.html'
model = Note
def get_object(self):
return get_object_or_404(Note, id=self.kwargs['note_id'])
def get_context_data(self, **kwargs):
context = super(generic.DetailView, self).get_context_data(**kwargs)
context['colors'] = Note.COLOR_CHOICES
context['is_edit_form'] = True
return context
notes/urls.py
urlpatterns = patterns('',
# ...
url(r'^(?P<note_id>\d+)/edit$', views.EditFormView.as_view(), name='edit_note'),
)
$(buttonElem).click( function () {
BootstrapDialog.show({
message: function(dialog) {
return $('').load(dialog.getData('pageToLoad'));
},
data: {
'pageToLoad': '/' + noteID + '/edit'
}
});
});
notes/forms.py
(create if necessary)
from django.forms import ModelForm
from notes.models import Note
class NoteForm(forms.ModelForm):
class Meta:
model = Note
fields = ('title', 'content', 'color')
notes/views.py
class NoteAction(generic.View):
form_class = NoteForm
template_name = 'notes/index.html'
def get(self, request, *args, **kwargs):
return HttpResponseRedirect(reverse('index'))
def post(self, request, *args, **kwargs):
# handle form data
notes/views.py
class NoteAction(generic.View):
form_class = NoteForm
template_name = 'notes/index.html'
def post(self, request, *args, **kwargs):
do_update = 'note_id' in self.kwargs
if do_update:
note_instance = get_object_or_404(Note,
id=self.kwargs['note_id'])
form = self.form_class(request.POST, instance=note_instance)
else:
form = self.form_class(request.POST)
# .. validate form and save if valid (TODO)
return HttpResponseRedirect(reverse('index'))
notes/views.py
class NoteAction(generic.View):
form_class = NoteForm
template_name = 'notes/index.html'
def post(self, request, *args, **kwargs):
# ... continuing from where we left off
if form.is_valid():
form.save()
messages.success(request, 'Your note has been saved.')
else:
for error, message in form.errors.iteritems():
messages.error(request, 'Invalid field ' + error)
return HttpResponseRedirect(reverse('index'))
notes/forms.py
)
class ArchiveNoteForm(NoteForm):
class Meta:
model = Note
fields = ('is_archived',)
class PinnedNoteForm(NoteForm):
class Meta:
model = Note
fields = ('is_pinned',)
If you specify fields or exclude when creating a form with ModelForm, then the fields that are not in the resulting form will not be set by the form's save() method.
notes/urls.py
urlpatterns = patterns('',
url(r'^(?P<note_id>\d+)/update$', views.NoteAction.as_view(), name='update_note'),
url(r'^(?P<note_id>\d+)/update/pin$', views.PinnedNoteAction.as_view(), name='update_note_pin'),
url(r'^(?P<note_id>\d+)/update/archive$', views.ArchiveNoteAction.as_view(), name='update_note_archive'),
)
Finally, change notes/views.py
class ArchiveNoteAction(NoteAction):
form_class = ArchiveNoteForm
class PinnedNoteAction(NoteAction):
form_class = PinnedNoteForm
Voila http://localhost:8000/
Any ideas?
urlpatterns = patterns('',
# ...
url(r'^tags/$',
views.TagJsonListView.as_view(), name='search_notes'),
url(r'^search/tag/(?P<tag_keyword>\w+)$',
views.SearchIndexView.as_view(), name='search_notes'),
)
notes/views.py
class SearchIndexView(IndexView):
index_type = 'search'
def get_queryset(self):
print self.kwargs
if 'tag_keyword' in self.kwargs:
return Note.objects.filter(
tags__keyword__icontains=self.kwargs['tag_keyword'])
else:
messages.error(request, 'Invalid Search')
return []
from django.http import JsonResponse
class TagJsonListView(generic.TemplateView):
def render_to_response(self, context, **response_kwargs):
keywords = [{'name': t.keyword} for t in Tag.objects.all()]
return JsonResponse(keywords, safe=False)
We're finally done! http://localhost:8000/