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)
- View all the tutorials in this series at DirectMe
- Use the following command to clone the directory.
$ git clone https://github.com/ArsalanShahid116/Django-MovieStore.git
- 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.
- value – It will hold the value of the vote from given choices, i.e., 1 or -1 for UP and Down, respectively.
- 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.
- movie – It refers to myMovie model using a foreignkey.
- 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 used ModelForm since we want our form to represent a model, i.e., Vote.
- for user attribute, ModelChoiceField accepts the model as a value by using the queryset which holds the valid choices of the field. In our case, any user can vote. Widget field is to allow the type of UI for a particular attribute. For this case, we do not need the user to be distracted by any UI. so we used HiddenInput and disabled prevents other users to vote on a movie.
- The movie attribute is the same as user, with a change in queryset.
- The value attribute is a ChoiceField, that by default is a dropdown menu list, But, since in our case, it should be a visible option for users to vote we used a radio button widget.
Creating Views for Voting
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.