Link submission
Let's look at what features we want to be related with link submissions. This is just a part of the features list that we saw at the start of the chapter:
- Link submission by users
- Voting on links submitted by other users
- Commenting on the submissions and replying to comments by other users
Let's think about what models we'll need to implement this. First, we need a model to hold information about a single submission, such as the title, URL, who submitted the link, and at what time. Next, we need a way to track votes on submissions by users. This can be implemented by a ManyToMany
field from the submission model to the User
model. This way, whenever a user votes for a submission, we just add them to the set of related objects and remove them if they decide to take back their vote.
Commenting as a feature is separate from link submissions because it can be implemented as a separate model that links to the submission model with ForeignKey
. We'll look at commenting in the next section. For now, we'll concentrate on link submissions.
To start out, let's create a new application in our project for link submission-related features. Run the following command in your CLI:
> python manage.py startapp links
Then, add our newly created app to the INSTALLED_APPS
settings variable. Now we're ready to write code.
Let's start with the models. Here's the code for Link model
. This code should be in links/models.py
:
from django.contrib.auth.models import User from django.db import models class Link(models.Model): title = models.CharField(max_length=100) url = models.URLField() submitted_by = models.ForeignKey(User) upvotes = models.ManyToManyField(User, related_name='votes') submitted_on = models.DateTimeField(auto_now_add=True, editable=False)
Note that we had to set related_name
for the upvotes
field. If we hadn't done this, we would get an error from Django when we try to run our application. Django would have complained about having two relationships to the User
model from the Link
model, both trying to create a reverse relationship named link
. To fix this, we explicitly named the reverse relationship from the User
model to the Link
model via the upvotes
field. The User
model should now have an attribute called votes
, which can be used to get a list of submissions that the user has voted on.
Once you've saved this code, you'll need to make and run migrations in order for Django to create database tables for the new model. To do so, type the following commands:
> python manage.py makemigrations > python manage.py migrate
Next, let's work on the templates and views. We'll customize the generic CreateView
that we've seen in the previous chapter for the view. Put this code in links/views.py
:
from django.contrib.auth.decorators import login_required from django.core.urlresolvers import reverse from django.http.response import HttpResponseRedirect from django.utils.decorators import method_decorator from django.views.generic import CreateView from links.models import Link class NewSubmissionView(CreateView): model = Link fields = ( 'title', 'url' ) template_name = 'new_submission.html' @method_decorator(login_required) def dispatch(self, *args, **kwargs): return super(NewSubmissionView, self).dispatch(*args, **kwargs) def form_valid(self, form): new_link = form.save(commit=False) new_link.submitted_by = self.request.user new_link.save() self.object = new_link return HttpResponseRedirect(self.get_success_url()) def get_success_url(self): return reverse('home')
This should look familiar to the CreateView
subclasses that we have already created in the previous chapter. However, look closer! This time, we don't define a custom form class. Instead, we just point to the model—Link
in this case—and CreateView
automagically creates a model form for us. This is the power of built-in Django generic views. They give you multiple options to get what you want, depending on how much customization you need to do.
We define the model
and fields
attributes. The model
attribute is self-explanatory. The fields
attribute has the same meaning here as it has in a ModelForm
subclass. It tells Django which fields we want to be made editable. In our link
model, the title and submission URL are the only two fields that we want the user to control, so we put these in the fields list.
Another important thing to look at here is the form_valid
function. Note that it doesn't have any calls to super
. Unlike our previous code, where we always called the parent class method for methods that we had overridden, we do no such thing here. That's because form_valid
of CreateView
calls the save()
method of the form. This will try to save the new link object without giving us the chance to set its submitted_by
field. As the submitted_by
field is required and can't be null
, the object won't be saved and we'll have to deal with a database exception.
So instead, we chose to not call the form_valid
method on the parent class and wrote the code for it ourselves. To do so, I needed to know what the base method did. So I looked up the documentation for it at https://docs.djangoproject.com/en/1.9/ref/class-based-views/mixins-editing/#django.views.generic.edit.ModelFormMixin.form_valid:
"Saves the form instance, sets the current object for the view, and redirects to get_success_url()."
If you look at our code for the form_valid
function, you will see that we do exactly the same thing. If you're ever faced with a similar situation, the Django documentation is the best resource to clear things up. It has some of the best documentation that I have ever encountered in any of the open source projects that I have used.
Finally, we need the template and URL configuration for the link submission feature. Create a new folder called templates
in the links
directory and save this code in a file called new_submission.html
:
{% extends "base.html" %} {% block content %} <h1>New Submission</h1> <form action="" method="post">{% csrf_token %} {{ form.as_p }} <input type="submit" value="Submit" /> </form> {% endblock %}
In discuss/urls.py
, import the new view:
from links.views import NewSubmissionView
Create a new URL configuration for this view:
url(r'^new-submission/$', NewSubmissionView.as_view(), name='new-submission'),
That's it. All the code that we need to get a basic link submission process up is written. However, to be able to test it out, we'll need to give the user some way of accessing this new view. The navigation bar in our base.html
template seems like a good place to put the link in for this. Change the code for the nav
HTML tag in base.html
in the templates
directory in the project root to match the following code:
<nav> <ul> {% if request.user.is_authenticated %} <li><a href="{% url "new-submission" %}">Submit New Link</a></li> <li><a href="{% url "logout" %}">Logout</a></li> {% else %} <li><a href="{% url "login" %}">Login</a></li> <li><a href="{% url "user-registration"%}">Create New Account</a></li> {% endif %} </ul> </nav>
To test it out, run the development server and open the home page. You'll see a Submit New Link option in the navigation menu on the top. Click on it and you'll see a page similar to the following one. Fill in the data and click on submit. If the data that you've filled in doesn't have any errors, you should be redirected to the home page.
While this works, this isn't the best user experience. Redirecting the user to the home page without giving them any feedback on whether their link was submitted is not good. Let's fix this next. We'll create a details page for the submissions and if the user was successful in submitting a new link, we'll take them to the details page.
Let's start with the view. We'll use the DetailView
generic view provided by Django. In your links/views.py
file, import DetailView
:
from django.views.generic import DetailView
Subclass it for our submission detail view:
class SubmissionDetailView(DetailView): model = Link template_name = 'submission_detail.html'
Create the submission_detail.html
template in the links/templates
directory and put in the following Django template code:
{% extends "base.html" %} {% block content %} <h1><a href="{{ object.url }}" target="_blank">{{ object.title }}</a></h1> <p>submitted by: <b>{{ object.submitted_by.username }}</b></p> <p>submitted on: <b>{{ object.submitted_on }}</b></p> {% endblock %}
Configure the URL for this view in discuss/urls.py
by first importing it:
from links.views import SubmissionDetailView
Then, add a URL pattern for it to the urlpatterns
list:
url(r'^submission/(?P<pk>\d+)/$', SubmissionDetailView.as_view(), name='submission-detail'),
Finally, we'll need to edit the NewSubmissionView
get_success_url
method to redirect the user to our new detail view on successfully creating a new submission:
def get_success_url(self): return reverse('submission-detail', kwargs={'pk': self.object.pk})
That's it. Now when you create a new submission, you should see the following detail page for your new submission:
Now that link submission is done, let's look at implementing the comments feature.