Django Authentication Part 2: Object Permissions with Django Guardian

By Adam McQuistan in Python  06/08/2019 Comment

Introduction

This is the second article in a multipart series on implementing the Django authentication system. To aid in the demonstration of the many features of Django authentication I will continue building a survey application which I've been calling Django Survey throughout these tutorials.

Each article of this series focusses on a subset of the authentication features that can be implemented with Django. This second article focusses on how to assign object level permissions to users and groups of users using the Django Guardian package. These permissions will be used to control who can view and take a survey as well as who can view survey results by restricting access to class views to only users meeting these specific object level permissions.

Series Contents:

The code for this series can be found on GitHub

Installing and Settings Up Django Guardian

To integrate Django Guardian into the project I start by installing with pip like so.

pip install django-guardian

After that I need to make the project aware of guardian's existence by adding it to the list of INSTALLED_APPS.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'survey.apps.SurveyConfig',
    'widget_tweaks',
    'guardian',
]

Following that I add guardian.backends.ObjectPermissionBackend to the AUTHENTICATION_BACKENDS list so it looks like this.

AUTHENTICATION_BACKENDS = [
  'django.contrib.auth.backends.ModelBackend',
  'guardian.backends.ObjectPermissionBackend',
]

After those two setitngs updates the last thing to do is run the migrations.

(venv) python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, guardian, sessions, survey
Running migrations:
  Applying guardian.0001_initial... OK

Assigning Django Permissions to Take a Survey

With Django Guardian integrated into the project I can now start protecting the SurveyAssignment model with specific permissions. By default Django gives all models add, change, view, and delete permission which are saved in the database within the auth_permission table accessible via the django.contrib.auth.models.Permission model. An important piece of information in the default permissions is the Permission.codename which indicates the name of the permission and loosely describes what action can be performed with each Model. For example, the complete default codename set for SurveyAssignment are listed below.

  • add_surveyassignment
  • change_surveyassginment
  • view_surveyassignment
  • delete_surveyassignment

In my case I will be working with the view_surveyassignment permission by applying it to the user's who have been assigned to take a particular survey. However, my desired scope of permission resides at the object instance level rather than the broad Model (aka class) level which Django does not immediately support out of the box. This is where Django Guardian comes in.

To accomplish this fine grained object level permission handling I need to update the SurveyCreateView.post method to give the view_surveyassignment permission to the assigned users via the assign_perm(...) function from the guardian.shortcuts module.

# survey/views.py

import json

from django.contrib.auth import authenticate, login
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User, Permission
from django.core.exceptions import ValidationError
from django.shortcuts import render, redirect, reverse

from django.views import View

# new assign_perm guardian function import
from guardian.shortcuts import assign_perm

from .models import Survey, Question, Choice, SurveyAssignment


... skipping down to SurveyCreateView

class SurveyCreateView(LoginRequiredMixin, View):
    def get(self, request):
        users = User.objects.all()
        return render(request, 'survey/create_survey.html', {'users': users})
    
    def post(self, request):
        data = request.POST
        
        title = data.get('title')
        questions_json = data.getlist('questions')
        assignees = data.getlist('assignees')
        valid = True
        context = {}
        if not title:
            valid = False
            context['title_error'] = 'title is required'

        if not questions_json:
            valid = False
            context['questions_error'] = 'questions are required'
            
        if not assignees:
            valid = False
            context['assignees_error'] = 'assignees are required'
        
        if not valid:
            context['users'] = User.objects.all()
            return render(request, 'survey/create_survey.html', context)
            
        survey = Survey.objects.create(title=title, created_by=request.user)
        for question_json in questions_json:
            question_data = json.loads(question_json)
            question = Question.objects.create(
            		text=question_data['text'],
            		survey=survey
            )
            for choice_data in question_data['choices']:
                Choice.objects.create(
                		text=choice_data['text'],
                		question=question
                )
        perm = Permission.objects.get(codename='view_surveyassignment')
        for assignee in assignees:
            assigned_to = User.objects.get(pk=int(assignee))
            assigned_survey = SurveyAssignment.objects.create(
                survey=survey,
                assigned_by=request.user,
                assigned_to=assigned_to
            )
            assign_perm(perm, assigned_to, assigned_survey)

        return redirect(reverse('profile'))

The real emphasis in this change resides in these two lines of code,

perm = Permission.objects.get(codename='view_surveyassignment')

which fetches the permission object matching the 'view_surveyassignment' permission codename and,

assign_perm(perm, assigned_to, assigned_survey)

which assigns the permission to each user for the instance of the survey assginment being created. I should state that the assign_perm(...) function also accepts a string for the first argument of the permission codename but, I feel its better to look up the object instance first and pass it to the assign_perm(...) function to reduce the number of queries hitting the database while looping over the assignees (under the hood assign_perm must hit the database if its give the string name).

It is important to recognize that this will only protect the newly created surveys. To update any existing survey's that have been previously created I will need to make a django migration to assign this object instance level permission to the existing survey assignments and their users.

To accomplish this I create an empty migration script like so.

(venv) python manage.py makemigrations --empty survey
Migrations for 'survey':
  survey/migrations/0004_auto_20190602_1835.py

I open survey/migrations/0004_auto_20190602_1835.py in my editor and go on to populate the migration with the following code.

# Generated by Django 2.2.1 on 2019-06-02 18:35

from django.conf import settings
from django.db import migrations

from guardian.shortcuts import assign_perm
from guardian.compat import get_user_model

def add_view_surveyassginemnt_perms(apps, schema_editor):
    SurveyAssignment = apps.get_model('survey', 'SurveyAssignment')
    survey_assignments = SurveyAssignment.objects.all()
    User = get_user_model()
    for assigned_survey in survey_assignments:
        assignee = assigned_survey.assigned_to
        user = User.objects.get(pk=assignee.id)
        if not user.has_perm('view_surveyassignment', assigned_survey):
            assign_perm('view_surveyassignment', user, assigned_survey)

class Migration(migrations.Migration):

    dependencies = [
        ('survey', '0003_auto_20190601_0447'),
    ]

    operations = [
        migrations.RunPython(add_view_surveyassginemnt_perms)
    ]

To provide the ability for asigned survey takers to provide their responses I'll add the SurveyAssignmentView class view complete with a get method that serves a survey_assignment.html template displaying the user's assigned survey. Complementing the get method is the post method that will accept posted survey response data and save it to the database.

Given that I've already established the requirement that a user must be assigned a survey in order to view and, thus make a survey response, I should also guard the SurveyAssignmentView view class by checking to make sure that the requested survey is capable of being viewed by the authenticated user requesting it. To guard the SurveyAssignmentView I update it to inherit from guardian.mixins.PermissionRequiredMixin which checks that the requesting user is logged in and then verifies the user has been assigned a the permission specified by a class field named permission_required. If either of these checks fail then the user is redirected to the login view. Below is the new SurveyAssignmentView.

# views.py
# survey/views.py

import json

from django.contrib.auth import authenticate, login
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.models import User, Group, Permission
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Q
from django.shortcuts import render, redirect, reverse, get_object_or_404

from django.views import View

from guardian.mixins import PermissionRequiredMixin
from guardian.shortcuts import assign_perm

... skipping down to SurveyAssignmentView

class SurveyAssignmentView(PermissionRequiredMixin, View):
    permission_required = 'survey.view_surveyassignment'

    def get_object(self):
        self.obj = get_object_or_404(SurveyAssignment, pk=self.kwargs['assignment_id'])
        return self.obj

    def get(self, request, assignment_id):
        # survey = Survey.objects.get(pk=survey_id)
        return render(request, 'survey/survey_assignment.html', {'survey_assignment': self.obj})

    def post(self, request, assignment_id):
        context = {'validation_error': ''}
        save_id = transaction.savepoint()
        try: 
            for question in self.obj.survey.questions.all():
                question_field = f"question_{question.id}"
                if question_field not in request.POST:
                    context['validation_error'] = 'All questions require an answer'
                    break
                
                choice_id = int(request.POST[question_field])
                choice = get_object_or_404(Choice, pk=choice_id)
                SurveyResponse.objects.create(
                    survey_assigned=self.obj,
                    question=question,
                    choice=choice
                )

            if context['validation_error']:
                transaction.savepoint_rollback(save_id)
                return render(request, 'survey/survey_assignment.html', context)

            transaction.savepoint_commit(save_id)
        except:
            transaction.savepoint_rollback(save_id)

        return redirect(reverse('profile'))

In order to properly configure the SurveyAssignmentView to utilize the PermissionRequiredMixin class I must set permission_required = 'survey.view_surveyassignment' and provide an implementation of the get_object() method. Under the hood the PermissionRequiredMixin calls its check_permissions() method which, suprise suprise, checks the permission specified against the SurveyAssigment object fetched by get_object() and either returns True and continues onto the request handler or returns False and redirects to the settings.LOGIN_URL specified earlier.

For completeness I have included the survey_assignment.html template below. It simply displays a form of questions with choice radio buttons and posts those selections back to SurveyAssignmentView.

<!-- survey_assignment.html -->
{% extends 'survey/base.html' %}

{% block content %}

<section class="section">
  <div class="container">
    <h1 class="title has-text-centered">
      Django Survey
    </h1>

    <div class="columns">
      <div class="column is-offset-2 is-8">
        <h2 class="subtitle is-size-4">
          Survey: {{ survey_assignment.survey.title }}
        </h2>
        {% if validation_error %}
          <p>{{ validation_error }}</p>
        {% endif %}
        <form action="{% url 'survey_assignment' survey_assignment.id %}" method="POST">
          {% csrf_token %}
          <ol>
            {% for question in survey_assignment.survey.questions.all %}
            <li style="margin-bottom: 20px;">
              {{ question.text }}
              
              {% for choice in question.choices.all %}
              <div class="control">
                <label for="choice_{{choice.id}}" class="radio">
                  <input id="choice_{{choice.id}}" type="radio" name="question_{{question.id}}" value="{{choice.id}}" required>
                  {{ choice.text }}
                </label>
              </div>
              {% endfor %}
              
            </li>
            {% endfor %}
          </ol>
          {% if survey_assignment.survey_responses.count == 0 %}
          <div class="field">
            <div class="control">
              <button class="button is-link">Submit</button>
            </div>
          </div>
          {% else %}
          <p>You've already completed this survey</p>
          {% endif %}
        </form>

      </div>
    </div>

  </div>

</section>

{% endblock %}

Below is the IU for completing a survey assignment.

Using Django Guardian's Custom Permissions and Groups to View Survey Results

Next on the aggenda is to provide a way to assign people to view survey results by updating the survey creation process to include an additional multi-select of users for a survey creator to choose from. I also update the SurveyCreateView.post method to create a group for each survey being created which I name survey_ID_result_viewers where ID is the actual Survey.id value. I then add the survey creator to that group as well as the selected reviewers. Afterwards I can assign a custom permission to the group / survey pair which will signify that users in this group can view the survey's results.

To start I generate an empty migration script to create this custom permission type, create groups for the existing surveys and, add the survey creator to the group.

(venv) python manage.py makemigrations --empty survey
Migrations for 'survey':
  survey/migrations/0005_auto_20190603_1325.py

Here is the migration code.

# Generated by Django 2.2.1 on 2019-06-03 13:25

from django.db import migrations

from guardian.shortcuts import assign_perm
from guardian.compat import get_user_model, Group

PERMISSION_NAME = 'can_view_results'
PERMISSION_FULL_NAME = f'survey.{PERMISSION_NAME}'

def make_can_view_survey_results_perm(apps, schema_editor):
    Survey = apps.get_model('survey', 'Survey')
    ContentType = apps.get_model('contenttypes', 'ContentType')
    Permission = apps.get_model('auth', 'Permission')
    content_type = ContentType.objects.get_for_model(Survey)
    permission = Permission.objects.create(
      codename=PERMISSION_NAME,
      name='Can view survey results',
      content_type=content_type
    )

def assign_can_view_survey_results_perms(apps, schema_editor):
    User = get_user_model()
    Survey = apps.get_model('survey', 'Survey')
    ContentType = apps.get_model('contenttypes', 'ContentType')
    Permission = apps.get_model('auth', 'Permission')
    surveys = Survey.objects.all()
    for survey in surveys:
        group_name = f"survey_{survey.id}_result_viewers"
        group = Group.objects.create(name=group_name)
        assign_perm(PERMISSION_FULL_NAME, group, survey)
        creator = User.objects.get(pk=survey.created_by.id)
        creator.groups.add(group)
        creator.save()

class Migration(migrations.Migration):

    dependencies = [
        ('survey', '0004_auto_20190602_1835'),
    ]

    operations = [
      migrations.RunPython(make_can_view_survey_results_perm),
      migrations.RunPython(assign_can_view_survey_results_perms)
    ]

The make_can_view_survey_results_perm(...) function in this migration demonstrates how to make your own permission based off an existing Model's content type. The process is to retrieve a Model's content type then use it to create a permission. When creating a permission you can basically use any string you want for codename as long as it hasn't been used before but, its customary to phrase it in a 'can_do_something' way. Similarly, you are able to put anyting you find useful for the name of the permission.

Then run the migration.

(venv) python manage.py migrate

Moving on to updating the existing survey creation process. I start with updating the create_survey.html template to include the reviewers multi-select as shown below.

<!-- create_survey.html -->
{% extends 'survey/base.html' %}

{% block content %}
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
<section class="section">
  <div class="container">
    <h1 class="title has-text-centered">
      Django Survey
    </h1>

    <div class="columns">
      <div class="column is-offset-2 is-8">
        <h2 class="subtitle">
          Create Survey
        </h2>

        <form action="{% url 'survey_create' %}" id="survey-form" method="POST">
          {% csrf_token %}
          <div class="field">
            <label for="title" class="label">
              Title
            </label>
            <div class="control">
              <input type="text" class="input" name="title" id="title">
            </div>
            <p class="help is-danger">{{ title_error }}</p>
          </div>

          <div class="field">
            <label for="" class="label">Assignees</label>
            <div class="control">
              <div class="select is-multiple">
                <select multiple size="4" name="assignees">
                  {% for user in users %}
                  <option value="{{ user.id }}">{{ user.username }}</option>
                  {% endfor %}
                </select>
              </div>
              <p class="help is-danger">{{ assignee_error }}</p>
            </div>
          </div>

          <div class="field">
            <label for="" class="label">Reviewers</label>
            <div class="control">
              <div class="select is-multiple">
                <select multiple size="4" name="reviewers">
                  {% for user in users %}
                  <option value="{{ user.id }}">{{ user.username }}</option>
                  {% endfor %}
                </select>
              </div>
              <p class="help is-danger">{{ reviewer_error }}</p>
            </div>
          </div>

          <div class="field">
            <label for="" class="label">Questions</label>
            <div class="control">
              <a @click.stop="addQuestion" class="button is-info is-small">
                <span class="icon">
                  <i class="fas fa-plus"></i>
                </span>
                <span>Add Question</span>
              </a>
            </div>
            <p class="help is-danger">{{ questions_error }}</p>
          </div>
          <ol>
            <li style="padding-bottom: 25px;" v-for="question in questions" :key="'question_' + question.id">
              <div class="field is-grouped">
                <label :for="'question_' + question.id" class="label">
                </label>
                <div class="control is-expanded">
                  <input type="text" class="input" v-model="question.text">
                </div>
                <div class="control">
                  <a @click.stop="removeQuestion(question)" class="button is-danger">
                    <span class="icon is-small">
                      <i class="fas fa-times"></i>
                    </span>
                  </a>
                </div>
              </div>
              <div style="margin-left: 30px;">
                <div class="field">
                  <label for="" class="label">Choices</label>
                  <div class="control">
                    <a @click.stop="addChoice(question)" class="button is-success is-small">
                      <span class="icon is-small">
                        <i class="fas fa-plus"></i>
                      </span>
                      <span>Add Choice</span>
                    </a>
                  </div>
                </div>

                <ol>
                  <li v-for="choice in question.choices" :key="'choice_' + choice.id">
                    <div class="field is-grouped">
                      <label :for="'choice_' + choice.id" class="label">
                      </label>
                      <div class="control is-expanded">
                        <input type="text" class="input" v-model="choice.text">
                      </div>
                      <div class="control">
                        <a @click.stop="removeChoice(question, choice)" class="button is-danger">
                          <span class="icon is-small">
                            <i class="fas fa-times"></i>
                          </span>
                        </a>
                      </div>
                    </div>
                  </li>
                </ol>

              </div>
              <input v-if="validQuestion(question)" type="hidden" name="questions" :value="serializeQuestion(question)">
            </li>
          </ol>
          <div class="field">
            <div class="control">
              <button class="button is-success">Submit</button>
            </div>
          </div>
        </form>
      </div>
    </div>

  </div>

</section>

<script>
 ... Omitting the Vue.js code for brevity
</script>

{% endblock %}

Afterwards I modify the SurveyCreateView.post method as described earlier to now create the survey result viewers group, assign it the can_view_results permission for the specific survey object, and then associate the creator and the reviewers with this group.

# views.py

... skipping down to SurveyCreateView for brevity

class SurveyCreateView(LoginRequiredMixin, View):
    def get(self, request):
        users = User.objects.all()
        return render(request, 'survey/create_survey.html', {'users': users})
    
    def post(self, request):
        data = request.POST
        
        title = data.get('title')
        questions_json = data.getlist('questions')
        assignees = data.getlist('assignees')
        reviewers = data.getlist('reviewers')
        valid = True
        context = {}
        if not title:
            valid = False
            context['title_error'] = 'title is required'

        if not questions_json:
            valid = False
            context['questions_error'] = 'questions are required'
            
        if not assignees:
            valid = False
            context['assignees_error'] = 'assignees are required'
        
        if not valid:
            context['users'] = User.objects.all()
            return render(request, 'survey/create_survey.html', context)
            
        survey = Survey.objects.create(title=title, created_by=request.user)
        for question_json in questions_json:
            question_data = json.loads(question_json)
            question = Question.objects.create(text=question_data['text'], survey=survey)
            for choice_data in question_data['choices']:
                Choice.objects.create(text=choice_data['text'], question=question)
        perm = Permission.objects.get(codename='view_surveyassignment')
        for assignee in assignees:
            assigned_to = User.objects.get(pk=int(assignee))
            assigned_survey = SurveyAssignment.objects.create(
                survey=survey,
                assigned_by=request.user,
                assigned_to=assigned_to
            )
            assign_perm(perm, assigned_to, assigned_survey)

        group = Group.objects.create(name=f"survey_{survey.id}_result_viewers")
        assign_perm('can_view_results', group, survey)
        request.user.groups.add(group)
        request.user.save()

        for reviewer_id in reviewers:
            reviewer = User.objects.get(pk=int(reviewer_id))
            reviewer.groups.add(group)
            reviewer.save()

        return redirect(reverse('profile'))

The updated UI is shown below.

For completeness I also add a manage_survey.html template and view class named SurveyManagerView which has a get method to serve a template displaying the survey's title and provides two multi-select lists. One multi-select is for adding additional survey assignments and another is for adding survey result viewers. SurveyManagerView also contains a post method that handles updating the list of who can take a survey and who can view survey results.

It seems like a reasonable rule that only the survey creator should be able to manage its assignment and reviewers so, I will protect SurveyManagerView so that only the creator can use it via the UserPassesTestMixin from the django.contrib.auth.mixins module.

Below is the updated views.py module which shows the aforementioned SurveyManagerView.get method as well as the post method.

# survey/views.py

import json

from django.contrib.auth import authenticate, login
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.models import User, Group, Permission
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Q
from django.shortcuts import render, redirect, reverse, get_object_or_404

from django.views import View

from guardian.conf import settings as guardian_settings
from guardian.mixins import PermissionRequiredMixin
from guardian.shortcuts import assign_perm

from .models import Survey, Question, Choice, SurveyAssignment, SurveyResponse

... omittings all other views except SurveyManagerView for brevity

class SurveyManagerView(UserPassesTestMixin, View):
  
    def test_func(self):
        self.obj = Survey.objects.get(pk=self.kwargs['survey_id'])
        return self.obj.created_by.id == self.request.user.id

    def get(self, request, survey_id):

        users = User.objects.exclude(Q(pk=request.user.id) | Q(username=guardian_settings.ANONYMOUS_USER_NAME))
        assigned_users = {
            sa.assigned_to.id 
            for sa in SurveyAssignment.objects.filter(survey=self.obj)
        }

        context = {
          'survey': self.obj,
          'available_assignees': [u for u in users if u.id not in assigned_users],
          'available_reviewers': [u for u in users if not u.has_perm('can_view_results', self.obj)]
        }
        return render(request, 'survey/manage_survey.html', context)

    def post(self, request, survey_id):
        assignees = request.POST.getlist('assignees')
        reviewers = request.POST.getlist('reviewers')
        
        perm = Permission.objects.get(codename='view_surveyassignment')
        for assignee_id in assignees:
            assigned_to = User.objects.get(pk=int(assignee_id))
            assigned_survey = SurveyAssignment.objects.create(
                survey=self.obj,
                assigned_by=request.user,
                assigned_to=assigned_to
            )
            assign_perm(perm, assigned_to, assigned_survey)
      
        group = Group.objects.get(name=f"survey_{self.obj.id}_result_viewers")
        for reviewer_id in reviewers:
            reviewer = User.objects.get(pk=int(reviewer_id))
            reviewer.groups.add(group)
            reviewer.save()

        return redirect(reverse('profile'))

The template code for manage_survey.html is shown below.

<!-- manage_survey.html -->
{% extends 'survey/base.html' %}

{% block content %}
<section class="section">
  <div class="container">
    <h1 class="title has-text-centered">
      Django Survey
    </h1>

    <div class="columns">
      <div class="column is-offset-2 is-8">
        <h2 class="subtitle">
          Manage Survey: {{ survey.title }}
        </h2>

        <form action="{% url 'survey_management' survey.id %}" method="POST">
          {% csrf_token %}

          <div class="field">
            <label for="" class="label">Available Assignees</label>
            <div class="control">
              <div class="select is-multiple">
                <select multiple size="4" name="assignees">
                  {% for user in available_assignees %}
                  <option value="{{ user.id }}">{{ user.username }}</option>
                  {% endfor %}
                </select>
              </div>
            </div>
          </div>

          <div class="field">
            <label for="" class="label">Available Reviewers</label>
            <div class="control">
              <div class="select is-multiple">
                <select multiple size="4" name="reviewers">
                  {% for user in available_reviewers %}
                  <option value="{{ user.id }}">{{ user.username }}</option>
                  {% endfor %}
                </select>
              </div>
            </div>
          </div>

          <div class="field">
            <div class="control">
              <button class="button is-success">Submit</button>
            </div>
          </div>
        </form>
      </div>
    </div>

  </div>

</section>

{% endblock %}

To get to this manage survey view I update the links in profile.html under the "Surveys You've Created" section to point to this new SurveyManagerView view class like so.

<!-- profile.html -->
{% extends 'survey/base.html' %}

{% block content %}

<section class="section">

  <div class="container">
    <h1 class="title has-text-centered">
      Django Survey
    </h1>

    <div class="columns">
      <div class="column is-offset-2 is-8">
        <h2 class="subtitle is-size-3">
          Welcome {{ request.user.username }}
        </h2>

        <h3 class="subtitle">Surveys You've Created</h3>
        <div class="content">
          <ul>
            {% for survey in surveys %}
            <li><a href="{% url 'survey_management' survey.id %}">{{ survey.title }}</a></li>
            {% endfor %}
          </ul>
        </div>

        <h3 class="subtitle">Surveys You've Been Assigned</h3>
        <div class="content">
          <ul>
            {% for assigned_survey in assgined_surveys %}
            <li>
              <a href="{% url 'survey_assignment' assigned_survey.id %}">{{ assigned_survey.survey.title }}</a>
            </li>
            {% endfor %}
          </ul>
        </div>

        <!-- 
        <h3 class="subtitle">Survey Results Shared With You</h3>
        -->

      </div>
    </div>

  </div>

</section>

{% endblock %}

Now that there exists the ability to assign result viewers I should add the ability to actually see the survey results. To accomplish this I add a SurveyResultsView class to display survey results. This view class simply retrieves the questions for a survey along with the available choices and, counts how many SurveyResponse objects match each choice then passes that data to a template named survey_results.html. SurveyResultsView is also guarded to only be viewable by people in the survey's group containing the can_view_results permission.

# survey/views.py

... skipping down to new SurveyResultsView the view models its uses

class QuestionViewModel:
    def __init__(self, text, choices=[]):
        self.text = text
        self.choices = choices

    def add_survey_response(self, survey_response):
        for choice in self.choices:
            if choice.id == survey_response.choice.id:
                choice.responses += 1
                break


class ChoiceResultViewModel:
    def __init__(self, id, text, responses=0):
        self.id = id
        self.text = text
        self.responses = responses
        

class SurveyResultsView(PermissionRequiredMixin, View):
    permission_required = 'survey.can_view_results'

    def get_object(self):
        self.obj = get_object_or_404(Survey, pk=self.kwargs['survey_id'])
        return self.obj

    def get(self, request, survey_id):
        questions = []
        for question in self.obj.questions.all():
            question_vm = QuestionViewModel(question.text)
            for choice in question.choices.all():
                question_vm.choices.append(ChoiceResultViewModel(choice.id, choice.text))
            
            for survey_response in SurveyResponse.objects.filter(question=question):
                question_vm.add_survey_response(survey_response)
            
            questions.append(question_vm)

        context = {'survey': self.obj, 'questions': questions}
        
        return render(request, 'survey/survey_results.html', context)

After this I map SurveyResultsView to a url path in survey/urls.py like so.

# survey/urls.py

from django.contrib.auth import views as auth_views
from django.urls import path

from . import views

urlpatterns = [
  path('register/', views.RegisterView.as_view(), name='register'),
  path('login/', auth_views.LoginView.as_view(template_name='survey/login.html'), name='login'),
  path('profile/', views.ProfileView.as_view(), name='profile'),
  path('logout/', auth_views.LogoutView.as_view(), name='logout'),
  path('surveys/create/', views.SurveyCreateView.as_view(), name='survey_create'),
  path('survey-assginment/<int:assignment_id>/', views.SurveyAssignmentView.as_view(), name='survey_assignment'),
  path('survey-management/<int:survey_id>/', views.SurveyManagerView.as_view(), name='survey_management'),
  path('survey-results/<int:survey_id>/', views.SurveyResultsView.as_view(), name='survey_results')
]

Then finish with the creation of the survey_results.html template which shows each question in a survey along with a table of available choices and the response count for each choice as shown below.

<!-- survey_results.html -->
{% extends 'survey/base.html' %}

{% block content %}
<section class="section">
  <div class="container">
    <h1 class="title has-text-centered">
      Django Survey
    </h1>

    <div class="columns">
      <div class="column is-offset-2 is-8">
        <h2 class="subtitle">
          Results for Survey: {{ survey.title }}
        </h2>

          <ol>
            {% for question in questions %}
            <li style="padding-bottom: 25px;">
              <p>{{ question.text }}</p>
              <div style="margin-left: 30px;">
                <table class='table'>
                  <thead>
                    <tr>
                      <td>Choice</td>
                      <td>Responses</td>
                    </tr>
                  </thead>
                  <tbody>
                    {% for choice in question.choices %}
                    <tr>
                      <td>{{ choice.text }}</td>
                      <td>{{ choice.responses }}</td>
                    </tr>
                    {% endfor %}
                  </tbody>
                </table>
              </div>
            </li>
            {% endfor %}
          </ol>
      </div>
    </div>
  </div>

</section>

{% endblock %}

Below is the view results UI.

All that is left to do now is add a third list to the profile.html template that shows the survey results that are available for a user. To accomplish this I update the ProfileView.get method to collect all survey's that the logged in user has a permission of 'can_view_results'. To do this I utilize a function provided in the guardian.shortcuts module named get_objects_for_user(...).

The get_objects_for_user(...) method accepts several parameters but, I will only be giving it three, with the first one being the user instance, the second param is the permission I want to match on which is 'can_view_results' and, the Model type that I want returned which of course is Survey. Below is the updated Profile.get method.

# survey/views.py

import json

from django.contrib.auth import authenticate, login
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.models import User, Group, Permission
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Q
from django.shortcuts import render, redirect, reverse, get_object_or_404

from django.views import View

from guardian.conf import settings as guardian_settings
from guardian.mixins import PermissionRequiredMixin
from guardian.shortcuts import assign_perm, get_objects_for_user

from .models import Survey, Question, Choice, SurveyAssignment, SurveyResponse

... skipping down to ProfileView for brevity

class ProfileView(LoginRequiredMixin, View):
    def get(self, request):
        surveys = Survey.objects.filter(created_by=request.user).all()
        assigned_surveys = SurveyAssignment.objects.filter(assigned_to=request.user).all()
        survey_results = get_objects_for_user(request.user, 'can_view_results', klass=Survey)

        context = {
          'surveys': surveys,
          'assgined_surveys': assigned_surveys,
          'survey_results': survey_results
        }

        return render(request, 'survey/profile.html', context)

And below is the updated profile.html template complete with the availabel survey results list of links pointing to the SurveyResultsView view class just defined.

<!-- profile.html -->
{% extends 'survey/base.html' %}

{% block content %}

<section class="section">

  <div class="container">
    <h1 class="title has-text-centered">
      Django Survey
    </h1>

    <div class="columns">
      <div class="column is-offset-2 is-8">
        <h2 class="subtitle is-size-3">
          Welcome {{ request.user.username }}
        </h2>

        <h3 class="subtitle">Surveys You've Created</h3>
        <div class="content">
          <ul>
            {% for survey in surveys %}
            <li><a href="{% url 'survey_management' survey.id %}">{{ survey.title }}</a></li>
            {% endfor %}
          </ul>
        </div>

        <h3 class="subtitle">Surveys You've Been Assigned</h3>
        <div class="content">
          <ul>
            {% for assigned_survey in assgined_surveys %}
            <li>
              <a href="{% url 'survey_assignment' assigned_survey.id %}">{{ assigned_survey.survey.title }}</a>
            </li>
            {% endfor %}
          </ul>
        </div>

        
        <h3 class="subtitle">Survey Results Shared With You</h3>
        <div class="content">
          <ul>
            {% for survey in survey_results %}
            <li>
              <a href="{% url 'survey_results' survey.id %}">{{ survey.title }}</a>
            </li>
            {% endfor %}
          </ul>
        </div>

      </div>
    </div>

  </div>

</section>

{% endblock %}

Want to Learn More About Python and Django?

Conclusion

In this second tutorial I have demonstrated how to create and assign permissions, specifically at the object level, to individual users and groups of users using the awesome Django Guardian package. In doing so I have also shown how to check to see if a user has been assigned a permission specific to an object as well as how to guard class views so that only users meeting a specific criteria using Mixins from Django Guardian as well as the base Django framework. In the next article I will be showing how to integrate Google authetication via Python Social Auth.

Thanks for joining along on this tour of some of the awesome authentication features that can be implemented in the Django web framework using Python.  As always, don't be shy about commenting or critiquing below.

Share with friends and colleagues

[[ likes ]] likes

Community favorites for Python

theCodingInterface