Django Project Blueprints
上QQ阅读APP看书,第一时间看更新

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.

Link submission

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:

Link submission

Now that link submission is done, let's look at implementing the comments feature.