django-rest-generators

Simple tool to add a bunch of commands related to django_rest_framework, django_guardian and behave


Keywords
testing
License
MIT
Install
pip install django-rest-generators==0.1.5

Documentation

django-rest-generators

A set of tools used to make django models management faster with django-rest-framework and behave

Install

Just as most of django tools, you can install DRG quite quickly :

pip install django-rest-generators

Then just add django_rest_generators to your INSTALLED_APPS

If you want to use auto-install for newly created application (recommanded), you should also add :

SETTING_FILE = os.path.abspath(__file__)
URLS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'urls.py')

somewhere in your settings.py file (or the same file where you put INSTALLED_APPS, if you use multiple files)

Added commands

The following commands are added to your project when you use DRG :

create_app

The create_app command take one parameter (name) and create a rest-ready application with this name :

$ python manage.py create_app my_app

The created application with be added to your project's urls with the following path :

/my_app/1.0/

The 1.0 part is intended to let you use it as an evolving API and is expected to be present in file genereted by make_rest_model command (see below)

create_model

The create_model command take two parameters (app and name) and create a rest-ready model with this name inside the application :

$ python manage.py create_model my_app my_model

make_rest_model

The make_rest_model command is the main tool in DRG. It parse a model an generate a set of ready-to-use tools around it :

  • A serializer
  • A viewset
  • A gherkin test file
  • A gherkin step to populate a mock-up database

By now, the following fields are understood :

  • BooleanField
  • NullBooleanField
  • CharField
  • TextField
  • EmailField
  • SlugField
  • URLField
  • UUIDField
  • GenericIPAddressField
  • IntegerField
  • SmallIntegerField
  • PositiveIntegerField
  • PositiveSmallIntegerField
  • FloatField
  • DateTimeField
  • DateField

Other fields may be present in the model, but they will not be included in the resulting serializer. Please also note that related fields will not be included either - you must define your relations manually in your serializer.

You may also specify a given set of permissions to allow on your viewset by using-p option.

$ python manage.py my_app my_model [-p permission]

The following permissions are availables:

name description create list detail update delete
everyone Allow everyone to do everything Everyone Everyone Everyone Everyone Everyone
auth Require login for every action User User User User User
auth_or_read_only Require login for modification only User Everyone Everyone User User
model Require global permission for everything add_model view_model view_model change_model delete_model
model_or_read_only Require global permissions for modification add_model User User change_model delete_model
model_or_anon_read_only Require global permissions for modification, everyone can read add_model Everyone Everyone change_model delete_model
object Require object permissions for everything add_model view_model view_model change_model delete_model
object_or_read_only Require object permissions for modification add_model User User change_model delete_model
object_or_anon_read_only Require object permissions for modification, everyone can read add_model Everyone Everyone change_model delete_model
admin Require to be a staff member (is_staff=True) for every operations Staff Staff Staff Staff Staff

By default, the everyone permission is set.

Global and object permissions

An important distinction exist between global permission and object-level permissions. Global permissions are given to an user without object, as such :

from guardian.shortcuts import assign_perms
from myapp import models

user = models.Users.objects.get(username='MyUser')
assign_perms('view_mymodel', user)

and object-level permissions are assigned to an user with a specific object, as such :

from guardian.shortcuts import assign_perms
from myapp import models

user = models.Users.objects.get(username='MyUser')
target = models.MyObject.objects.get(pk=42)
assign_perms('view_mymodel', user, target)

When using object-level permissions, you must also give to the user the corresponding global permissions ; in other words, this will not be enough :

from guardian.shortcuts import assign_perms
from myapp import models

user = models.Users.objects.get(username='MyUser')
target = models.MyObject.objects.get(pk=42)
assign_perms('view_mymodel', user, target)
# If i try to access target with MyUser, I will face a 403 error

But this is enough and safe :

from guardian.shortcuts import assign_perms
from myapp import models

user = models.Users.objects.get(username='MyUser')
target = models.MyObject.objects.get(pk=42)
assign_perms('view_mymodel', user)
assign_perms('view_mymodel', user, target)
# Now I can view my model

For obvious reasons, you only need global add_model permission to create an object, even with object-level permissions.

In the previous table, you must replace model by the name of your model. When creating a model, the following three permissions are created by Django :

  • add_model
  • change_model
  • delete_model

DRG will assume a fourth permission exists if you use model or object permissions : view_model

Existing project

Sometimes, you may want to add DRG to an existing project, that may already contain some gherkin files. In this case, you will need to add DRGs step to your project as described below, and fix any possible conflicts, for DRG generated tests to work.

Other tools

DRG also add some shortcuts that may be usefull in some context and that can be used in your project.

Gherkin sentences

To use DRG's gherkin sentences, you must include it in your steps definition. You can do so by adding the following line in the __init__.py file in your steps directory :

from django_rest_generators.steps import authentication, database, http

Here is the list of sentences :

Users and authentication
step regexp example usage
Given i am logged in as (?P[^ ]+) i am logged in as MyUser Simulate a login over the API
Given i am not logged in Do nothing (pass) ; this is useful to make your intention clear in your test file
Given a basic set of users exists in the database Create three users in your database : administrator (with is_staff=True), allowed_user and unallowed_user
Given a super administrator exists in the database Create a single super-user (is_staff=True, is_superuser=True) in your database with username superadmin
Given user (?P[^ ]+) have permission (?P[^ ]+) user MyUser have permission my_app.add_my_model Give a global permission to an user
Given user (?P[^ ]+) have permission (?P[^ ]+) over a (?P[^ ]+) with (?P[^ ]+) (?P<key_value>.+) User MyUser have permission my_app.change_my_model over a my_model with id 2 Give a specific object-level permission to an user. Note: This will not give the corresponding global permission to this user
Database
step regexp example usage
Then a (?P<model_name>[a-zA-Z0-9_.]+) exists with (?P[a-zA-Z0-9_]+) (?P.+) a my_app.my_model exists with id 2 Check if a given model exists in the database with a given key
Then no (?P<model_name>[a-zA-Z0-9_.]+) exists with (?P[a-zA-Z0-9_]+) (?P.+) no my_app.my_model exists with id 2 Check that a given model does not exist in the database with a given key
HTTP requests and values
step regexp example usage
When i prepare a request to (?P[a-zA-Z0-9-_/.]+) i prepare a request to /my_app/1.0/my_model/ Initialize a request over an API endpoint
When i provide (?P[a-zA-Z0-9-_]+) (?P.+) i provide name This is a name Set a parameter to use for the request
When i send the request using (?PPOST GET PUT PATCH DELETE) i send the request using POST Send the request over the API with the corresponding HTTP verb
Then the return code is (?P[0-9]+) the return code is 404 Specify the return code for the request
Then the return value for (?P[a-zA-Z0-9-_]+) is (?P.+) the return value for name is Some name Specify an expected value in a returned json object
Then the returned array contain (?P[0-9]+) elements the returned array contain 2 elements Assuming that the response from the API was a json array, validate the number of returned elements
Then the returned element (?P[0-9]+) have a key named (?P[a-zA-Z0-9_]+) with value (?P.+) the returned element 2 have a key named name with value the name Assuming that the response from the API was a json array of objects, validate that at the given line, the object contain a given key with a given value
Then there is no (?P[a-zA-Z0-9-_]+) in the returned object there is no name is the returned object Assuming that the response is a json object, validate that a given key is not present in this object
Then the returned element (?P[0-9]+) have no key named (?P[a-zA-Z0-9-_]+) the returned element 2 have no key named *name Assuming that the response is a json array of object, validate that the object at the given line have no key of the given name

Initialization

To be able to use most of those sentences, you must initialize your testing environment. The sentences assume two elements :

  • You will logout after each test
  • You will have in your behave context a apiContext key with a rest_framework.test.APIClient instance

Here is a sample environment.py to meet those requirements :

import os
import django

os.environ['DJANGO_SETTINGS_MODULE'] = 'my_project.settings'
django.setup()

from django.test.runner import DiscoverRunner
from django.test.testcases import TestCase
from rest_framework.test import APIClient


def before_all(context):
    context.test_runner = DiscoverRunner()
    context.test_runner.setup_test_environment()
    context.old_db_config = context.test_runner.setup_databases()
    context.apiClient = APIClient()


def after_all(context):
    context.test_runner.teardown_databases(context.old_db_config)
    context.test_runner.teardown_test_environment()


def before_scenario(context, _):
    context.test = TestCase()
    context.test.setUpClass()


def after_scenario(context, _):
    context.test.tearDownClass()
    context.apiClient.logout()
    del context.test

Permissions

Three classes are added to RestFramework's permissions classes :

  • DjangoModelPermissionsWithRead : Work just as DjangoModelPermissions from rest_framework except that a view_model permission is expected for read actions
  • DjangoObjectPermissionsWithRead : Work just as DjangoObjectPermissions from rest_framework except that a view_model permission is expected for read actions
  • DjangoObjectPermissionsOrAnonReadOnly : Work just as DjangoObjectPermissions from rest_framework except that anonymous user is allowed to read actions as well

Dependencies

DRG is strongly dependent of DjangoRestFramework and DjangoGuardian. It also need Behave.