Arsalan Shahid

Adding a User Voting Feature for Movies – MovieStore Part 5

My dear reader, how are you? السلام عليكم

Motivation is what gets you started. Commitment is what keeps you going — Jim Rohn

In this post, we will continue to extend our MovieStore application tutorial by allowing users to vote (like or dislike) on a movie. A like would mean a positive vote and a dislike would show a negative vote on a movie. 


In order to practically follow this tutorial, I suggest the following before you start:

MovieStore GitHub Repository – (DirectMe)

  1. View all the tutorials in this series at DirectMe
  2. Use the following command to clone the directory.
    $ git clone https://github.com/ArsalanShahid116/Django-MovieStore.git
  3. Set your GitHub repo to MovieStore Part 4 using the following command:
Django-MovieStore$ git fetch origin 2d2c054675514c8dc36d040224fd848309d6b0ae

Creating a model for Vote

To start, we will have to first create a Vote model as shown below

# open moviesApp/models.py and add the following program 

class Vote(models.Model):
    UP = 1
    DOWN = -1
    VALUE_CHOICES = (
            (UP, "LIKE",),
            (DOWN, "DISLIKE",),
            )

    value = models.SmallIntegerField(
            choices=VALUE_CHOICES,
            )
    user = models.ForeignKey(
            settings.AUTH_USER_MODEL,
            on_delete=models.CASCADE
            )
    movie = models.ForeignKey(
            myMovie,
            on_delete=models.CASCADE,
            )
    voted_on = models.DateTimeField(
            auto_now=True
            )

    class Meta:
        unique_together = ('user', 'movie')

Our Vote model has 4 fields which are explained below.

  1. value –  It will hold the value of the vote from given choices, i.e., 1 or -1 for UP and Down, respectively.
  2. user – It is a foreignkey that refers to user model using settings.AUTH_USER_MODEL and on_delete option is used to specify the SQL cascade operation that means if a user is deleted than delete the votes associated to that user as well.
  3. movie – It refers to myMovie model using a foreignkey.
  4. voted_on –  It is a DateTime field. Using auto_now helps to store time when the model is saved.

Using unique_togather in the Meta class helps to have the functionality of one vote per user on a particular movie.


Creating a Form for Vote to add in detail_view

# create a new file form.py in moviesApp and add the following program

from django import forms
from django.contrib.auth import get_user_model

from moviesApp.models import Vote, myMovie

class VoteForm(forms.ModelForm):

    user = forms.ModelChoiceField(
            widget=forms.HiddenInput,
            queryset=get_user_model().
            objects.all(),
            disabled=True,
            )
    movie = forms.ModelChoiceField(
            widget=forms.HiddenInput,
            queryset=myMovie.objects.all(),
            disabled=True
            )
    value = forms.ChoiceField(
            label='Vote',
            widget=forms.RadioSelect,
            choices=Vote.VALUE_CHOICES,
            )

    class Meta:
        model = Vote
        fields = (
            'value', 'user', 'movie',)

We could have created a simple form with myMovies and user and vote value field but letting users vote on behalf of other users would create a problem and would be a vulnerability, so we customized the user, movie and value fields as shown above. Let us now explain the form in detail.

We will create the following two types of vote views: 1) CreateVote, which will be a CreateView and 2) UpdateVote, which will be a UpdateView.

We will start by first creating VoteManager as shown below:

# open moviesApp/models.py and add 

class VoteManager(models.Manager):
    def get_vote_or_unsaved_blank_vote(self, movie, user):
        try:
            return Vote.objects.get(
                movie=movie,
                user=user)
        except Vote.DoesNotExist:
            return Vote(
                movie=movie,
                user=user)

class Vote(models.Model):
    ...
    ...
    objects = VoteManager() # add this
    ...
    ...

We will now update our MovieDetail view as shown below

class MovieDetail(DetailView):
    queryset = myMovie.objects.all_with_related_persons_and_score()

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        if self.request.user.is_authenticated:
            vote = Vote.objects.get_vote_or_unsaved_blank_vote(
                movie=self.object,
                user=self.request.user
            )
            if vote.id:
                vote_form_url = reverse(
                    'moviesApp:UpdateVote',
                    kwargs={
                        'movie_id': vote.movie.id,
                        'pk': vote.id})
            else:
                vote_form_url = (
                    reverse(
                        'moviesApp:CreateVote',
                        kwargs={
                            'movie_id': self.object.id}
                    )
                )
            vote_form = VoteForm(instance=vote)
            ctx['vote_form'] = vote_form
            ctx['vote_form_url'] = \
                vote_form_url
        return ctx

We now update our detail view template.

# open moviesApp/templates/moviesApp/mymovie_detail.html and update the following

{% block sidebar %}
<div>
This is a side bar and can be used for movie rattings or other features
</div>
<div>
        {% if vote_form %}
        <form method="post"
       action = "{{ vote_form_url }}" >
       {% csrf_token %}
       {{ vote_form.as_p }}
       <button class="btn btn-primary">
               Vote
       </button>
        </form>
        {% else %}
        <p> log in to vote for Movie </p>
        {% endif %}
</div>
{% endblock %}

Let us add CreateVote and UpdateVote view as follows

# open views.py and add the following

from django.contrib.auth.mixins import (
LoginRequiredMixin)
from django.core.exceptions import (
PermissionDenied)
from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic import (
ListView, DetailView, UpdateView,
CreateView
)

class CreateVote(LoginRequiredMixin, CreateView):
    form_class = VoteForm

    def get_initial(self):
        initial = super().get_initial()
        initial['user'] = self.request.user.id
        initial['movie'] = self.kwargs[
            'movie_id']
        return initial

    def get_success_url(self):
        movie_id = self.object.movie.id
        return reverse(
            'moviesApp:MovieDetail',
            kwargs={
                'pk': movie_id})

    def render_to_response(self, context, **response_kwargs):
        movie_id = context['object'].id
        movie_detail_url = reverse(
            'moviesApp:MovieDetail',
            kwargs={'pk': movie_id})
        return redirect(
            to=movie_detail_url)

class UpdateVote(LoginRequiredMixin, UpdateView):
    form_class = VoteForm
    queryset = Vote.objects.all()

    def get_object(self, queryset=None):
        vote = super().get_object(
            queryset)
        user = self.request.user
        if vote.user != user:
            raise PermissionDenied(
                'cannot change another '
                'users vote')
        return vote

    def get_success_url(self):
        movie_id = self.object.movie.id
        return reverse(
            'moviesApp:MovieDetail',
            kwargs={'pk': movie_id})

    def render_to_response(self, context, **response_kwargs):
        movie_id = context['object'].id
        movie_detail_url = reverse(
            'moviesApp:MovieDetail',
            kwargs={'pk': movie_id})
        return redirect(
            to=movie_detail_url)

we also need to add a login redirection in our project settings.py. Add the following line

LOGIN_REDIRECT_URL = ‘user:login’

Finally, we need to update our moviesApp/urls.py to link to the newly created views as shown below

# Extend with the program shown in bold

from django.urls import path
from . import views

app_name = 'moviesApp'
urlpatterns = [
        path('myMovies',
            views.MovieList.as_view(),
            name='MovieList'),
        path('myMovies/<int:pk>',
            views.MovieDetail.as_view(),
            name='MovieDetail'),
        path('movie/<int:movie_id>/vote',
            views.CreateVote.as_view(),
            name='CreateVote'),
        path('movie/<int:movie_id>/vote/<int:pk>',
            views.UpdateVote.as_view(),
            name='UpdateVote'),
        ]

I hope you find this tutorial useful. If you find any errors or feel any need for improvement, let me know in your comments below.

Signing off for today. Stay tuned and I will see you next week! Happy learning.

Exit mobile version