Django

A Pyladies Montreal Workshop

Eleyine Zarour / @eleyine

Today, we will...

  1. Build a simplified Google Keep clone
  2. Describe a feature we need
  3. Implement a feature together
  4. Review our example solution
  5. Use best practices and nifty tools
  6. Ask a bunch of questions!

Today, we will not...

  • Worry about the app's general appearance
    • CSS, Javascript UI
    • (I'll take care of that)
  • Learn basic Python
    • control flow, namespaces, classes
    • (but ask me to clarify anything as we go)

By the end of this workshop...

Agenda

  1. The Basics
  2. Setup
  3. Django Overview
  4. Models
  5. Views
  6. Templates
  1. Advanced Topics
  2. Generic Views
  3. Signals
  4. Forms
  5. Django REST Framework
  6. Special Mentions

Detailed Instructions

github.com/eleyine/pyladies-django-workshop

git: Distributed Revision Control

  • git
  • pip
  • virtualenvwrapper
  • npm
  • bower

http://git-scm.com

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 Manager

  • git
  • pip
  • virtualenvwrapper
  • npm
  • bower

Installs and manages software packages written in Python

https://pip.pypa.io

										
# 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 Manager

  • git
  • pip
  • virtualenvwrapper
  • npm
  • bower

Wrappers for creating and deleting virtualenvs 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 manager

  • git
  • pip
  • virtualenvwrapper
  • npm
  • bower

npmis a package manager for Javascript

angular, less, grunt, gulp, coffee-script and many more!

https://www.npmjs.com/

We will need it for the following installation step.

										
# in pyladies-django-workshop
$ npm install
										
									

bower: Package Manager for the web

  • git
  • pip
  • virtualenvwrapper
  • npm
  • bower

bower finds, downloads, installs and saves frameworks, libraries, assets and utilities for front-end development using a flat dependency tree.

http://bower.io/

										
# in pyladies-django-workshop
$ sudo npm install -g bower
# install packages in manifest file 'bower.json'
$ bower install
$ bower list
										
									

Test the boilerplate

							
# in pyladies-django-workshop
(pyladies-django) $ python manage.py migrate
(pyladies-django) $ python manage.py runserver
							
						

Visit http://localhost:8000/, what do you see?

Agenda

  1. The Basics
  2. Setup
  3. Django Overview
  4. Models
  5. Views
  6. Templates
  1. Advanced Topics
  2. Generic Views
  3. Signals
  4. Forms
  5. Django REST Framework
  6. Special Mentions

What do all of these have in common?

The Zen of Python

							
>>> 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.
							
						

Django at a Glance

  • Rapid development and clean, pragmatic design.
  • Focus on automation and Don't Repeat Yourself (DRY)
  • Thriving ecosystem
  • Object-Relational Mapping (ORM)
  • Production ready administrative interface
  • Elegant URL scheme

The MTV Model

The Model

  • The data behind the app
  • No presentation logic
  • No layout

The MTV Model

The Template

  • What the user sees
  • Graphical representation of the model

The MTV Model

The View

  • Logic to translate user interactions
  • Gets a request passed to it
  • Returns a response (e.g. creates/updates Model instances)
  • Also known as "controllers"

Project structure


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.

Let's Create our Notes App


$ python manage.py startapp notes
					

Edit pyladies_django_workshop/settings.py


INSTALLED_APPS = (
    # previous apps
    'notes',
)
					

App folder structure


notes/
    __init__.py
    admin.py
    migrations/
        __init__.py
    models.py
    tests.py
    views.py
						
					

What models do we need for our app?

Note

Tag

NoteCollection

Model Fields

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

Activating Models


$ 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

Playing with the API
Open new console, 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

Let's fix that

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)
						
					

More Fields and Field Attributes

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/

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

Shell Time

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

Shell Time


(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!

Django Admin

Creating an Admin User


(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

Viewing the Admin Interface


(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.
Exposing our app to the Admin Interface

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/

Views

Views

In Django, web pages and other content are delivered by views. Generally speaking, a View must serve a specific function and has a specific Template.

Take a guess at which views we need!

  • Note index page displays all notes
  • Note detail page displays single note
  • Note archiveIndex page displays archived notes
  • Note pinnedIndex page displays pinned notes
  • Tag-based search page displays all notes for a given tag
  • noteAction handles note creation and update
Writing our First View

Edit 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'),
)
					
Instantiates an HttpResponse object with the given page content and the default content type (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.

Writing our First View (cont'd)

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/

Let's wire more views

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)

Let's wire more views (cont'd)

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.

Can we write a view that actually does something already?

Oh, right.

Writing a view that actually does something

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

HTML? CSS?

Got you covered.

Templates

What Templates look like

Open templates/base.html


<!DOCTYPE html>
<html>
  <head>
    <title>Not Google Keep</title>
    {% include 'stylesheets.html' %}
  </head>

  <body>
    {% include 'navbar.html' %}

      {% block content %}
        <div class="container main-content">
          <p> There is nothing here yet. Let's start building! </p>
        </div>
      {% endblock %}

    {% include 'javascripts.html' %}
    {% block scripts %}{% endblock %}
  </body>
</html>

					

In templates/notes/index.html (git reset --hard simpleIndex)


{% extends "base.html" %}

{% block content %}
<div class="container main-content {% if search %}search{% endif %}">
    <div class="row">
        <div class="col-lg-12" id="container">
            {% for note in notes %}
              <p>{{ note.title}}</p>
            {% empty %}
              <div class="no-notes-container">
              <div class="no-notes text-center">
                  {% if search %}
                    <h3>No matching notes.</h3>
                  {% else %}
                    <h3>Your {% if archive %}archived{% endif %} notes will appear here.</h3>
                  {% endif %}
              </div>
              </div>
          {% endfor %}
        </div><!-- /#container -->
    </div>
</div><!-- /main content -->
{% endblock %}

					

Writing a view that actually does something (for real)

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))
					
Even Better

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)
					

Add some of that CSS+JS <3

git reset --hard prettyIndex

(yep, nothing more)

$ python manage.py generatenotes <number>
to generate notes

Forms

Let's make some notes

Let's add a Form Template

Update templates/notes/index.html to include the form (this is a markup-simplified version)

git reset --hard noteForm


<div class="container main-content {% if search %}search{% endif %}">
    <div id="container-wrapper">
        {% if search %}
            <!-- search stuff -->
        {% else %}
            {% include "notes/note-form.html" %}
        {% endif %}

        <div id="container">
            {% for note in notes %}
                {% include "notes/note-container.html" %}
            {% endfor %}
        </div>
    </div> <!-- /#container-wrapper -->
</div><!-- /main content -->

					
Form Template

Edit templates/notes/note-form.html (this is a markup-simplified version)

git reset --hard noteForm


<div class="form-wrapper">
    <form action="{% url 'create_note' %}" method="post">
        {% csrf_token %}
        <fieldset>
        <input name="title" placeholder="Title"/>
        <textarea name="content" placeholder="Add note" ></textarea>
        {% for color in colors %}
                <input type="radio" name="color" value="{{ color.0 }}" 
                {% if forloop.first %} checked {% endif %}/>
        {% endfor %}
        <button type="submit" class="done-button" value="submit">Done</button>
        </fieldset>
    </form>
</div>

					
Create Note View

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 HttpResponseRedirectafter 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

http://localhost:8000/

All good?

Ideally, we want tags to be created/updated and saved at the same time as Note content.

Use signals!

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

http://localhost:8000/

Generic Views

Motivation

The short answer:

Less is Better

There is a plethora of common web development patterns. Generic views abstract common patterns to the point where you don’t even need to write Python code to write an app.
e.g. The code for 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

DRY

Generic Views

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.")
Generic Views

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).
Generic Views

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/

Django Forms

Generic Forms

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.

Edit notes/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
Edit notes/urls.py

urlpatterns = patterns('',
    # ...
    url(r'^(?P<note_id>\d+)/edit$', views.EditFormView.as_view(), name='edit_note'),
)
Behind the scenes, in static/javascripts/forms.js

$(buttonElem).click( function () {
    BootstrapDialog.show({
        message: function(dialog) {
            return $('
').load(dialog.getData('pageToLoad')); }, data: { 'pageToLoad': '/' + noteID + '/edit' } }); });
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')
Edit 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
Edit 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'))
Edit 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'))
Just like the IndexViews, we can now use that for our mini 'Archive' and 'Pin' Forms. (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.
Then add urls to 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/

Search Notes by Tags

Any ideas?

Let's start by implementing our search index

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'),
)
The, change 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 []
To autocomplete the searchbar, we're send an ajax request to populate tag keywords. The request expects a JsonResponse.

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/

Special Mentions

  • Django REST Framework: To build a RESTful API
  • Django Celery: Task Scheduling
  • Django Debug Toolbar
  • Django Extensions
  • Angular-Material / Polymer if you like Material Design

Credits

  • https://www.djangoproject.com/
  • https://thinkster.io/django-angularjs-tutorial/ Awesome tutorial on how to integrate Django with AngularJS (heavy use of Django REST framework), highly recommend it.
  • Django tutorial: https://speakerdeck.com/mpirnat/web-development-with-python-and-django-2014

Thanks! Google, Francoise and Pyladies

@eleyine