After implementing our Twitter-clone, Ribbit, in plain PHP and Rails, it's time to introduce the next walk-through: Python! In this tutorial, we'll rebuild Ribbit using Django. Without further delay, let's get started!
Step 0 - Bootstrapping
As of the time of this writing, Django 1.4 supports Python 2.5 to 2.7.3. Before proceeding, make sure that you have the apt version by executing python -v
in the terminal. Note that Python 2.7.3 is preferred. All throughout this tutorial, we'll use pip as our package manager and virtualenv to set up the Virtual Environments. To install these, fire up the terminal and execute the following commands as root
curl http://python-distribute.org/distribute_setup.py | sudo python curl https://raw.github.com/pypa/pip/master/contrib/get-pip.py | sudo python sudo pip install virtualenv
To set up our Django development evironment, we'll start off by creating a Virtual Environment. Execute the following commands in the terminal (preferrably inside your development directory)
virtualenv --no-site-packages ribbit_env source ribbit_env/bin/activate
With our Virtual Environment, set up and activated (your command prompt should be changed to reflect the environmen's name), let's move on to installing the dependencies for the project. Apart from Django, we'll be using South to handle the database migrations. We'll use pip
to install both of them by executing. Do note that from here on, we'll be doing everything inside the virtualenv. As such, ensure that it's activated before proceeding.
pip install Django South
With all of the dependencies set up, we're ready to move on to creating a new Django Project.
Step 1 - Creating the Project and the Ribbit App
We'll begin by creating a new Django project and our app. cd
into your preferred directory and run:
django-admin.py startproject ribbit cd ribbit django-admin.py startapp ribbit_app
Next, we'll initialize our git repository and create a .gitignore
file inside the Ribbit project that we just created. To do so, run:
git init echo "*.pyc" >> .gitignore git add . git commit -m 'Initial Commit'
Let's move on to editing ribbit/settings.py
and configure our project. First, we'll define some constants. Add the following to the top of the file:
import os PROJECT_PATH = os.path.dirname(os.path.abspath(__file__)) LOGIN_URL = '/'
PROJECT_PATH
will store the location of the directory in which settings.py is stored. This will allow us to use relative paths for future constants. LOGIN_URL
, as the name suggests, designates that the root of our site will be the URL to Login.
Moving on, let's configure the database. For the development evironment, sqlite3 is an ideal choice. To do so, edit the DATABASES
constant with the following values:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 'NAME': os.path.join(PROJECT_PATH, 'database.db'), # Or path to database file if using sqlite3. 'USER': '', # Not used with sqlite3. 'PASSWORD': '', # Not used with sqlite3. 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 'PORT': '', # Set to empty string for default. Not used with sqlite3. } }
Django finds the static files from the directory mentioned in the STATIC_ROOT
constant and routes requests to the files to the path specified in STATIC_URL
. Configure them so that they reflect the following:
# Absolute path to the directory static files should be collected to. # Don't put anything in this directory yourself; store your static files # in apps' "static/" subdirectories and in STATICFILES_DIRS. # Example: "/home/media/media.lawrence.com/static/" STATIC_ROOT = os.path.join(PROJECT_PATH, 'static') # URL prefix for static files. # Example: "http://media.lawrence.com/static/" STATIC_URL = '/static/'
Do note the use of os.path.join()
. The function allows us to relatively specify the path using the PROJECT_PATH
constant we defined before.
Next, we need to specify the location that Django needs to look to find the template files. We'll edit the TEMPLATE_DIRS
constant to specify the path.
TEMPLATE_DIRS = ( # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. os.path.join(PROJECT_PATH, 'templates') )
Finally, let's add South
and ribbit_app
to the list of INSTALLED_APPS
.
INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'south', 'ribbit_app', # Uncomment the next line to enable the admin: # 'django.contrib.admin', # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', )
Next, let's create the directories we defined in the settings and create the database:
mkdir ribbit/static ribbit/templates ribbit_app/static python manage.py syncdb python manage.py schemamigration ribbit_app --initial python manage.py migrate ribbit_app
The syncdb
command will create the required tables and create the superuser account. Since we're using South for migrations, we make the initial migration for the app using the syntax schemamigration <app_name> --initial
and apply it with python manage.py migrate ribbit_app
Let's start our development server to ensure everything is working correctly.
python manage.py runserver
If everything's fine indeed, you should be greeted with the following page when you visit http://localhost:8000
Further, the project tree should look like:
ribbit |-- manage.py |-- ribbit | |-- database.db | |-- __init__.py | |-- __init__.pyc | |-- settings.py | |-- settings.pyc | |-- static | |-- templates | |-- urls.py | |-- urls.pyc | |-- wsgi.py | `-- wsgi.pyc `-- ribbit_app |-- __init__.py |-- __init__.pyc |-- migrations | |-- 0001_initial.py | |-- 0001_initial.pyc | |-- __init__.py | `-- __init__.pyc |-- models.py |-- models.pyc |-- static |-- tests.py `-- views.py
Before moving on to the next step, let's commit our changes to the repo.
git add . git commit -m 'Created app and configured settings'
Step 2 - Base Template and Static Files
Following on from the interface tutorial, download the assets and place them within the ribbit_app/static
directory. We need to make some edits to style.less
for this tutorial. Let's begin with adding some styles for the flash.
.flash { padding: 10px; margin: 20px 0; &.error { background: #ffefef; color: #4c1717; border: 1px solid #4c1717; } &.warning { background: #ffe4c1; color: #79420d; border: 1px solid #79420d; } &.notice { background: #efffd7; color: #8ba015; border: 1px solid #8ba015; } }
Next, let's update the width of the input elements and add the error class for the same. Note that the code below contains only the styles that are required to be added or updated. The remaining code remains untouched.
input { width: 179px; &.error { background: #ffefef; color: #4c1717; border: 1px solid #4c1717; } }
We also need to increase the height of #content.wrapper.panel.right
.
height: 433px;
Finally, let's add a right margin to the footer images.
footer { div.wrapper { img { margin-right: 5px; } } }
Before moving on, let's commit the changes:
git add . git commit -m 'Added static files'
Next, let's create the base template, which will be inherited by all the other templates. Django uses it's own templating engine (like ERB or Jade). Define the template in ribbit/templates/base.html
with the following content:
<!DOCTYPE html> <html> <head> <link rel="stylesheet/less" href="{{ STATIC_URL }}style.less"> <script src="{{ STATIC_URL }}less.js"></script> </head> <body> <header> <div class="wrapper"> <img src="{{ STATIC_URL }}gfx/logo.png"> <span>Twitter Clone</span> {% block login %} <a href="/">Home</a> <a href="/users/">Public Profiles</a> <a href="/users/{{ username }}">My Profile</a> <a href="/ribbits">Public Ribbits</a> <form action="/logout"> <input type="submit" id="btnLogOut" value="Log Out"> </form> {% endblock %} </div> </header> <div id="content"> <div class="wrapper"> {% block flash %} {% if auth_form.non_field_errors or user_form.non_field_errors or ribbit_form.errors %} <div class="flash error"> {{ auth_form.non_field_errors }} {{ user_form.non_field_errors }} {{ ribbit_form.content.errors }} </div> {% endif %} {% if notice %} <div class="flash notice"> {{ notice }} </div> {% endif %} {% endblock %} {% block content %} {% endblock %} </div> </div> <footer> <div class="wrapper"> Ribbit - A Twitter Clone Tutorial <a href="http://net.tutsplus.com"> <img src="{{ STATIC_URL }}gfx/logo-nettuts.png"> </a> <a href="http://www.djangoproject.com/"> <img src="https://www.djangoproject.com/m/img/badges/djangomade124x25.gif" border="0" alt="Made with Django." title="Made with Django." /> </a> </div> </footer> </body> </html>
In the above markup, {{ STATIC_URL }}
prints the path for the static url defined in settings.py. Another feature is the use of blocks. All the content of the blocks is inherited to the sub-classes of the base template, and will not be overwritten unless the block is explicitly redefined in them. This provides us with some flexibility to place the navigation links at the header and replace it with the login form if the user isn't signed in. We're also using a block with an if
condition to check if any of the flash variables are not empty and render the messages appropriately.
Alright! Time to make another commit:
git add . git commit -m 'Created base template'
Step 3 - Creating the Models
One of the best things about Django is that it includes quite a few models and forms, which can be overridden to suite many purposes. For our application, we'll use the User
model and add a few properties to it by creating a UserProfile
Model. Further, to manage the ribbits, we'll create a Ribbit
Model as well. The User
model provided by Django includes fields to store the username, password, first and last names and email address (with validation) along with many others. I suggest you to have a look at the API to know about the all fields supported by default. Add the following code for models in ribbit_app/models.py
.
from django.db import models from django.contrib.auth.models import User import hashlib class Ribbit(models.Model): content = models.CharField(max_length=140) user = models.ForeignKey(User) creation_date = models.DateTimeField(auto_now=True, blank=True) class UserProfile(models.Model): user = models.OneToOneField(User) follows = models.ManyToManyField('self', related_name='followed_by', symmetrical=False) def gravatar_url(self): return "http://www.gravatar.com/avatar/%s?s=50" % hashlib.md5(self.user.email).hexdigest() User.profile = property(lambda u: UserProfile.objects.get_or_create(user=u)[0])
Let's start with the Ribbit
Model. The attributes include a CharField
with maximum length of 140 characters to store the content, a ForeignKey
to the User model (so that we have a relation between the two models) and a DateTimeField
which is automatically populated with the time when the instance of the model is saved.
Moving on to the UserProfile
Model, we've a OneToOne
field that defines a One to One relation with the User
Model and a ManyToMany
field to implement the follows/followed_by relation. The related_name
parameter allows us to use the relation backwards using a name of our choice. We've also set symmetrical
to False to ensure that if User A follows B then User B doesn't automatically follow A. We've also defined a function to get the link to the gravatar image based upon the user's url and a property to get (if the UserProfile exists for the user) or create one when we use the syntax <user_object>.profile
. This allows us to fetch the properties of UserProfile
quite easily. Here's an example of how you might use the ORM to get the users that a given User follows and is followed by:
superUser = User.object.get(id=1) superUser.profile.follows.all() # Will return an iterator of UserProfile instances of all users that superUser follows superUse.profile.followed_by.all() # Will return an iterator of UserProfile instances of all users that follow superUser
Now that our models are defined, let's generate the migrations and apply them:
python manage.py schemamigration ribbit_app --auto python manage.py migrate ribbit_app
Before moving on, let's commit the changes
git add . git commit -m 'Created Models'
Step 4 - Creating Forms
Django allows us to create forms so that we can easily validate the data accepted by the user for irregularities. We'll create a custom form for the Ribbit
Model andcreate a form that inherits UserCreationForm
provided by default to manage the registration. For managing the authentication, we'll extend the AuthenticationForm
provided by default in Django. Let's create a new file ribbit_app/forms.py
and add the imports.
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.contrib.auth.models import User from django import forms from django.utils.html import strip_tags from ribbit_app.models import Ribbit
Let's begin with creating the registration form. We'll name it UserCreateForm
and it's code is given below:
class UserCreateForm(UserCreationForm): email = forms.EmailField(required=True, widget=forms.widgets.TextInput(attrs={'placeholder': 'Email'})) first_name = forms.CharField(required=True, widget=forms.widgets.TextInput(attrs={'placeholder': 'First Name'})) last_name = forms.CharField(required=True, widget=forms.widgets.TextInput(attrs={'placeholder': 'Last Name'})) username = forms.CharField(widget=forms.widgets.TextInput(attrs={'placeholder': 'Username'})) password1 = forms.CharField(widget=forms.widgets.PasswordInput(attrs={'placeholder': 'Password'})) password2 = forms.CharField(widget=forms.widgets.PasswordInput(attrs={'placeholder': 'Password Confirmation'})) def is_valid(self): form = super(UserCreateForm, self).is_valid() for f, error in self.errors.iteritems(): if f != '__all_': self.fields[f].widget.attrs.update({'class': 'error', 'value': strip_tags(error)}) return form class Meta: fields = ['email', 'username', 'first_name', 'last_name', 'password1', 'password2'] model = User
In the form above, we've explicitly set some of the fields as mandatory by passing in required=True
. Further, I've added the placeholder attribute to the different widgets used by the forms. A class named error
is also added to the fields that contain errors. This is done in the is_valid()
function. Finally, in the Meta
class, we can specify the order in which we want our form fields to render and set the model against which the form should be validated.
Next, let's write the form for the authentication:
class AuthenticateForm(AuthenticationForm): username = forms.CharField(widget=forms.widgets.TextInput(attrs={'placeholder': 'Username'})) password = forms.CharField(widget=forms.widgets.PasswordInput(attrs={'placeholder': 'Password'})) def is_valid(self): form = super(AuthenticateForm, self).is_valid() for f, error in self.errors.iteritems(): if f != '__all__': self.fields[f].widget.attrs.update({'class': 'error', 'value': strip_tags(error)}) return form
As in the UserCreateForm
, the AuthenticateForm
adds some placeholders and error classes.
Finally, let's finish up form, to accept a new Ribbit.
class RibbitForm(forms.ModelForm): content = forms.CharField(required=True, widget=forms.widgets.Textarea(attrs={'class': 'ribbitText'})) def is_valid(self): form = super(RibbitForm, self).is_valid() for f in self.errors.iterkeys(): if f != '__all__': self.fields[f].widget.attrs.update({'class': 'error ribbitText'}) return form class Meta: model = Ribbit exclude = ('user',)
The exclude option in the Meta
class above prevents the user field from being rendered. We don't need it since the Ribbit's user will be decided by using sessions.
Let's commit the changes we've made so far
git add . git commit -m 'Created Forms'
Step 5 - Implementing Sign Up and Login
Django offers great flexibility when it comes to routing. Let's begin by defining some routes in ribbit/urls.py
.
urlpatterns = patterns('', # Examples: url(r'^$', 'ribbit_app.views.index'), # root url(r'^login$', 'ribbit_app.views.login_view'), # login url(r'^logout$', 'ribbit_app.views.logout_view'), # logout url(r'^signup$', 'ribbit_app.views.signup'), # signup )
Next, let's make use of the models and forms we've just made and write the corresponding views for each route we've just defined.. Let's start by adding the imports in ribbit_app/views.py
.
from django.shortcuts import render, redirect from django.contrib.auth import login, authenticate, logout from django.contrib.auth.models import User from ribbit_app.forms import AuthenticateForm, UserCreateForm, RibbitForm from ribbit_app.models import Ribbit
Followed by the index view:
def index(request, auth_form=None, user_form=None): # User is logged in if request.user.is_authenticated(): ribbit_form = RibbitForm() user = request.user ribbits_self = Ribbit.objects.filter(user=user.id) ribbits_buddies = Ribbit.objects.filter(user__userprofile__in=user.profile.follows.all) ribbits = ribbits_self | ribbits_buddies return render(request, 'buddies.html', {'ribbit_form': ribbit_form, 'user': user, 'ribbits': ribbits, 'next_url': '/', }) else: # User is not logged in auth_form = auth_form or AuthenticateForm() user_form = user_form or UserCreateForm() return render(request, 'home.html', {'auth_form': auth_form, 'user_form': user_form, })
For the index view, we first check if the user is logged in or not and render the templates, accordingly. The querysets ribbits_self
and ribbits_buddies
are merged with the |
operator in the above code. Also, we check if an instance of a form has been passed to the method (in the function definition) and if not we create a new one. This allows us to pass around form instances to the appropriate templates and render the errors.
Let's proceed with editing the 'home.html' template which will be used to show the index page for anonymous users. In the ribbit/templates/home.html
file, add the following code.
{% extends "base.html" %} {% block login %} <form action="/login" method="post">{% csrf_token %} {% for field in auth_form %} {{ field }} {% endfor %} <input type="submit" id="btnLogIn" value="Log In"> </form> {% endblock %} {% block content %} {% if auth_form.non_field_errors or user_form.non_field_errors %} <div class="flash error"> {{ auth_form.non_field_errors }} {{ user_form.non_field_errors }} </div> {% endif %} <img src="{{ STATIC_URL}}gfx/frog.jpg"> <div class="panel right"> <h1>New to Ribbit?</h1> <p> <form action="/signup" method="post">{% csrf_token %} {% for field in user_form %} {{ field }} {% endfor %} <input type="submit" value="Create Account"> </form> </p> </div> {% endblock %}
In the template, we inherit the base template defined before, render the authentication and sign up forms by overriding the login block. A neat thing about Django is that it makes CSRF Protection quite easy! All you need to do is add a csrf_token
in every form you use in the template.
Let's move on to the buddies.html
template, which will show the Buddies' Ribbit page. Edit ribbit/templates/buddies.html
and add the following code:
{% extends "base.html" %} {% block login %} {% with user.username as username %} {{ block.super }} {% endwith %} {% endblock %} {% block content %} <div class="panel right"> <h1>Create a Ribbit</h1> <p> <form action="/submit" method="post"> {% for field in ribbit_form %}{% csrf_token %} {{ field }} {% endfor %} <input type="hidden" value="{{ next_url }}" name="next_url"> <input type="submit" value="Ribbit!"> </form> </p> </div> <div class="panel left"> <h1>Buddies' Ribbits</h1> {% for ribbit in ribbits %} <div class="ribbitWrapper"> <a href="/users/{{ ribbit.user.username }}"> <img class="avatar" src="{{ ribbit.user.profile.gravatar_url }}"> <span class="name">{{ ribbit.user.first_name }}</span> </a> @{{ ribbit.user.username }} <p> {{ ribbit.content }} </p> </div> {% endfor %} </div> {% endblock %}
In this template, we provide the parent template i.e. base.html
with the value of username so that the navigation link for the logged in User's profile renders correctly. We're also using an instance of RibbitForm
to accept new Ribbits and looping over and showing the current ribbits by our buddies.
With the templates finished, let's move on and write the code to log in/out the user. Add the following code to ribbit_app/views.py
def login_view(request): if request.method == 'POST': form = AuthenticateForm(data=request.POST) if form.is_valid(): login(request, form.get_user()) # Success return redirect('/') else: # Failure return index(request, auth_form=form) return redirect('/') def logout_view(request): logout(request) return redirect('/')
The views for login expect a HTTP POST request for the login (since the form's method is POST). It validates the form and, if successful, logins the user using the login()
method which starts the session and then redirects to the root url. If the validation fails, we pass the instance of the auth_form
received from the user to the index function and list the errors, whereas, if the request isn't POST then the user is redirected to the root url.
The logout view is relatively simpler. It utilizes the logout()
function in Django which deletes the session and logs the user out followed by redirecting to the root url.
We now need to write the view to sign up and register a user. Append the following code to ribbit_app/views.py.
def signup(request): user_form = UserCreateForm(data=request.POST) if request.method == 'POST': if user_form.is_valid(): username = user_form.clean_username() password = user_form.clean_password2() user_form.save() user = authenticate(username=username, password=password) login(request, user) return redirect('/') else: return index(request, user_form=user_form) return redirect('/')
Similar to the login view, the signup view expects a POST request as well and redirects to the root url if the check fails. If the Sign Up form is valid, the user is saved to the database, authenticated, logged in and then redirected to the home page. Otherwise, we call the index function and pass in the instance of user_form
submitted by the user to list out the errors.
Let's check our progress by starting the server and testing the views we've written so far manually.
python manage.py runserver
Let's visit our Development Server and try registering a new user. If all goes well, you should be presented with the Buddies' Ribbits page. We can logout and reauthenticate the newly created user to check if the sign in functions work as expected.
Time to commit the changes!
git add . git commit -m 'Implemented User Login and Sign Up'
Step 6 - Accepting new Ribbits and Listing Public Ribbits
In the buddies.html
template we created before, the ribbit_form was submitted to /submit
. Let's edit ribbit/urls.py
and add the route for the form and the page to list all the public ribbits.
urlpatterns = patterns('', # Examples: url(r'^$', 'ribbit_app.views.index'), # root url(r'^login$', 'ribbit_app.views.login_view'), # login url(r'^logout$', 'ribbit_app.views.logout_view'), # logout url(r'^ribbits$', 'ribbit_app.views.public'), # public ribbits url(r'^submit$', 'ribbit_app.views.submit'), # submit new ribbit )
Let's write a view to validate and store the ribbits submitted. Open up ribbit_app/views.py
and append the following code:
from django.contrib.auth.decorators import login_required @login_required def submit(request): if request.method == "POST": ribbit_form = RibbitForm(data=request.POST) next_url = request.POST.get("next_url", "/") if ribbit_form.is_valid(): ribbit = ribbit_form.save(commit=False) ribbit.user = request.user ribbit.save() return redirect(next_url) else: return public(request, ribbit_form) return redirect('/')
The view uses the @login_required
decorator, which executes the function only if the user is authenticated; else, the user is redirected to the path specified in LOGIN_URL
constant in the settings. If the form validation is successful, we manually set the user to the one contained in the session and then save the records. After the database commit, the user is redirected to the path specified in next_url
field which is a hidden form field we manually entered in the template for this purpose. The value of next_url
is passed along in the views that render the Ribbit Form.
Moving on, let's write the view to list the last 10 Public Ribbits. Append the following in ribbit_app/views.py
@login_required def public(request, ribbit_form=None): ribbit_form = ribbit_form or RibbitForm() ribbits = Ribbit.objects.reverse()[:10] return render(request, 'public.html', {'ribbit_form': ribbit_form, 'next_url': '/ribbits', 'ribbits': ribbits, 'username': request.user.username})
In the public view, we query the database for the last 10 ribbits by slicing the queryset to the last 10 elements. The form along with the ribbits are then rendered to the template. Let's create ribbit/templates/public.html
for this view
{% extends "base.html" %} {% block content %} <div class="panel right"> <h1>Create a Ribbit</h1> <p> <form action="/submit" method="post"> {% for field in ribbit_form %}{% csrf_token %} {{ field }} {% endfor %} <input type="hidden" value="{{ next_url }}" name="next_url"> <input type="submit" value="Ribbit!"> </form> </p> </div> <div class="panel left"> <h1>Public Ribbits</h1> {% for ribbit in ribbits %} <div class="ribbitWrapper"> <img class="avatar" src="{{ ribbit.user.profile.gravatar_url }}"> <span class="name">{{ ribbit.user.first_name }}</span>@{{ ribbit.user.username }} <span class="time">{{ ribbit.creation_date|timesince }}</span> <p>{{ ribbit.content }}</p> </div> {% endfor %} </div> {% endblock %}
While looping over the ribbit objects, the template uses the timesince
template tag to automatically determine the time difference between the ribbit creation date and current time, and prints it in a Twitter-like way.
Ensuring that the development server is running, create a new ribbit and have a look at the Public Ribbits page to ensure everything is fine.
Let's commit the changes before proceeding:
git add . git commit -m 'Implemented Ribbit Submission and Public Ribbits Views'
Step 7 - User Profiles and Following Users
A twitter clone, User Profiles and Following Users go hand in hand. Let's write the routes for implementing this functionality. Update ribbit/urls.py
with the following code:
urlpatterns = patterns('', # Examples: url(r'^$', 'ribbit_app.views.index'), # root url(r'^login$', 'ribbit_app.views.login_view'), # login url(r'^logout$', 'ribbit_app.views.logout_view'), # logout url(r'^ribbits$', 'ribbit_app.views.public'), # public ribbits url(r'^submit$', 'ribbit_app.views.submit'), # submit new ribbit url(r'^users/$', 'ribbit_app.views.users'), url(r'^users/(?P<username>\w{0,30})/$', 'ribbit_app.views.users'), url(r'^follow$', 'ribbit_app.views.follow'), )
An interesting route above is the one to direct to a specific user's profile. <?P<username>
captures the username queried via GET, and \w{0,30}
asserts that the maximum length for a username is 30 characters:
Next, we'll proceed with writing the view to render User Profiles. Append the following in `ribbit_app/views.py'.
from django.db.models import Count from django.http import Http404 def get_latest(user): try: return user.ribbit_set.order_by('-id')[0] except IndexError: return "" @login_required def users(request, username="", ribbit_form=None): if username: # Show a profile try: user = User.objects.get(username=username) except User.DoesNotExist: raise Http404 ribbits = Ribbit.objects.filter(user=user.id) if username == request.user.username or request.user.profile.follows.filter(user__username=username): # Self Profile or buddies' profile return render(request, 'user.html', {'user': user, 'ribbits': ribbits, }) return render(request, 'user.html', {'user': user, 'ribbits': ribbits, 'follow': True, }) users = User.objects.all().annotate(ribbit_count=Count('ribbit')) ribbits = map(get_latest, users) obj = zip(users, ribbits) ribbit_form = ribbit_form or RibbitForm() return render(request, 'profiles.html', {'obj': obj, 'next_url': '/users/', 'ribbit_form': ribbit_form, 'username': request.user.username, })
This view is perhaps the most interesting of all that we've covered so far. We start by ensuring that only logged in users are able to view profiles. In the routes we defined in ribbit/urls.py
, we wrote one to capture the username. This captured parameter is automatically called along with the request object in the users view. We come across two options for this view:
- A username is passed to the url to render a specific user's profile
- No username is passed which implies the user wants to view all profiles
We begin by checking if a username is passed and is not empty. Then, we try to fetch a User object for the corresponding username. Note the use of a try catch block in the code above. We simply raise a Http404 exception provided by Django to redirect to the default 404 template. Next, we need to check if the profile requested is of the logged in user or one of his buddies. If so, we don't need to render a follow link in the profile since the relation is already established. Otherwise, we pass along the follow
parameter in the view to print the Follow link.
Moving on to the second point, if no username is given in the url, we fetch a list of all the users and use the annotate()
function to add a ribbit_count
attribute to all objects, which stores the number of Ribbits made by each user in the queryset. This allows us to use something along the lines of <user_object>.ribbit_count
to fetch the Ribbit Count of the user.
Getting the latest Ribbit by each user is a bit tricky. We use Python's built in map()
function for this and call get_latest()
to all the elements of users queryset. The get_latest()
function is defined in the code above and makes use of a backward relation on the User<-->Ribbit relation. For instance user.ribbit_set.all()
would return all the ribbits by the user. We order the ribbits by id in descending order and slice the first element. The code is enclosed in a try catch block to catch the exception if no ribbits are created by the user. We're then making use of Python's zip()
function to link up each element of both iterators (users and ribbits) so that we have a tuple with User Object and Latest Ribbit pair. We then pass along this zipped object along with the forms to the template. Let's write our last view that will accept the request to follow a user
from django.core.exceptions import ObjectDoesNotExist @login_required def follow(request): if request.method == "POST": follow_id = request.POST.get('follow', False) if follow_id: try: user = User.objects.get(id=follow_id) request.user.profile.follows.add(user.profile) except ObjectDoesNotExist: return redirect('/users/') return redirect('/users/')
In the view above, we get the value of the follow
parameter, passed by POST. If the id is set we check if a user exists and add the relation, else, we catch an ObjectDoesNotExist
exception and redirect the user to the page that lists all User Profiles.
We're done with the views here. Let's write the remaining two templates required to render the user profiles. Open your text editor and edit ribbit/templates/profiles.html
.
{% extends "base.html" %} {% block content %} <div class="panel right"> <h1>Create a Ribbit</h1> <p> <form action="/submit" method="post"> {% for field in ribbit_form %}{% csrf_token %} {{ field }} {% endfor %} <input type="hidden" value="{{ next_url }}" name="next_url"> <input type="submit" value="Ribbit!"> </form> </p> </div> <div class="panel left"> <h1>Public Profiles</h1> {% for user, ribbit in obj %} <div class="ribbitWrapper"> <a href="/users/{{ user.username }}"> <img class="avatar" src="{{ user.profile.gravatar_url }}"> <span class="name">{{ user.first_name }}</span> </a> @{{ user.username }} <p> {{ user.ribbit_count}} Ribbits <span class="spacing">{{ user.profile.followed_by.count }} Followers</span> <span class="spacing">{{ user.profile.follows.count }} Following</span> </p> <p>{{ ribbit.content }}</p> </div> {% endfor %} </div> {% endblock %}
To list the user along with his last ribbit, we use a common python construct to iterate over a list of tuples: for user, ribbit in obj
. Inside the loop we print the information about the user along with his ribbit stats. Notice the use of a backward relation in {{ user.profile.followed_by.count }}
. followed_by
is the related name we set up while defining the ManyToManyField attribute for the UserProfile Model.
Finally, let's write the template to list a specific user's profile. Write the following code in ribbit/templates/user.html
.
{% extends "base.html" %} {% block login %} {% with user.username as username %} {{ block.super }} {% endwith %} {% endblock %} {% block content %} <div class="panel left"> <h1>{{ user.first_name }}'s Profile</h1> <div class="ribbitWrapper"> <a href="/users/{{ user.username }}"> <img class="avatar" src="{{ user.profile.gravatar_url }}"> <span class="name">{{ user.first_name }}</span> </a> @{{ user.username }} <p> {{ ribbits.count }} Ribbits <span class="spacing">{{ user.profile.follows.all.count }} Following</span> <span class="spacing">{{ user.profile.followed_by.all.count }} Followers</span> </p> {% if follow %} <form action="/follow" method="post"> {% csrf_token %} <input type="hidden" name="follow" value="{{ user.id }}"> <input type="submit" value="Follow"> </form> {% endif %} </div> </div> <div class="panel left"> <h1>{{ user.first_name }}'s Ribbits</h1> {% for ribbit in ribbits %} <div class="ribbitWrapper"> <a href="/users/{{ user.username }}"> <img class="avatar" src="{{ user.profile.gravatar_url }}"> <span class="name">{{ ribbit.user.first_name }}</span> </a> @{{ ribbit.user.username }} <span class="time">{{ ribbit.creation_date|timesince }}</span> <p>{{ ribbit.content }}</p> </div> {% endfor %} </div> {% endblock %}
As in the buddies.html
template, we pass the username of the logged in template to the parent template using the {% with %}
construct. Next, we display the user's profile and display the follow link only if the follow
variable is set to True
. After that, we list all the ribbits created by the user by iterating over the ribbits variable.
We're finally done with all the views and templates. Browse around all the links and try following dummy users and explore the Twitter-clone you've just created.
Let's commit our changes:
git add . git commit -m 'Implemented User Profiles and Following Relation on frontend'
Step 8 - Deployment to Heroku
I prefer to keep my development and production branches separate; git makes branching a breeze. Fire up the terminal and execute the following:
git branch -b production
This will create the production branch and switch to it immediately. Let's update our .gitignore
file and add the database to it. It's contents should be
*.pyc ribbit/database.db
Let's install some packages to our virtualenv that are required for deployment.
pip install psycopg2 dj-database-url gunicorn
We'll also create a file with all dependencies of the project so that they're picked up by Heroku.
pip freeze > requirements.txt
Next, let's edit ribbit/settings.py
. The database settings should be changed like.
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 'NAME': 'ribbit', # Or path to database file if using sqlite3. 'USER': 'username', # Not used with sqlite3. 'PASSWORD': 'password', # Not used with sqlite3. 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 'PORT': '', # Set to empty string for default. Not used with sqlite3. } }
At the end of the file, append the following so that Heroku can figure out the database settings.
import dj_database_url DATABASES['default'] = dj_database_url.config()
Let's create a Procfile to start the process on Heroku's server. Place the following in a file, named Procfile
.
web: gunicorn ribbit.wsgi
Also, if your application isn't large scale, you can skip using storage services by appending the route for static files in ribbit/urls.py
urlpatterns += patterns('django.contrib.staticfiles.views', url(r'^static/(?P<path>.*)$', 'serve'), )
We're all set! Let's commit the changes:
git commit -a -m 'Configured for Production'
Finally, we'll create a Heroku app and push our repo to the remote repository:
heroku create git push heroku production:master
Once the files are transferred, run syncdb
and apply the migrations to the database.
heroku run python manage.py syncdb heroku run python manage.py migrate ribbit_app
Finally, open the deployed app with:
heroku open
Conclusion
We're finally done with Ribbit in Django! This tutorial was quite long, indeed, and the application still has much more potential that can be implemented. I hope that, if you are new to Django, this article has encouraged you to dig more deeply.
Feel free to play along with my deployment at Heroku. If you have any questions, I'd be glad to answer and all questions.
Comments