Django

From campisano.org
Jump to navigation Jump to search

Django, REST and TDD example project

Install python 3 env

  • required packages:
sudo apt-get install python3-pip virtualenv
  • optional update:
sudo pip3 install --user --upgrade setuptools pip virtualenv

Create a virtual env with Django

virtualenv -p python3 env        # go in virtual env, use deactivate to exit
# OR: virtualenv --no-site-packages --always-copy --python python3 env
source env/bin/activate
pip3 install Django==1.9.2
python -c "import django; print(django.get_version())"

Create our test project

django-admin.py startproject tdd_rest_project
cd tdd_rest_project
python manage.py migrate            # create database using sqlite initially
python manage.py createsuperuser    # create main user (e.g. 'user:linea@2014')
# run follow to test the admin server
python manage.py runserver          # open at http://localhost:8000/admin/
    ^C                              # Ctrl+C to stop
  • NOTE: A Django project is a single database divided in several mini-apps

Add addictional packages to test and implement REST api

pip3 install webtest==2.0.20 django-webtest==1.7.8
pip3 install djangorestframework==3.3.2
  • Fix requirements
pip freeze
echo -n > requirements.txt
echo Django==1.9.2 >> requirements.txt
echo WebTest==2.0.20 >> requirements.txt
echo django-webtest==1.7.8 >> requirements.txt
echo djangorestframework==3.3.2 >> requirements.txt

Create our first REST mini Application INSIDE project sub-folder

  • NOTE: we can have several mini-app in the same project

The follow is get from http://arunrocks.com/understanding-tdd-with-django/

applying the http://www.django-rest-framework.org/tutorial/quickstart/ tutorial

cd tdd_rest_project
django-admin.py startapp shorturls_app
  • Configure the app
    • Add the app in the project tdd_rest_project/settings.py file
[...]

INSTALLED_APPS = [
    [...]
    'rest_framework',
    'tdd_rest_project.shorturls_app'
]

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAdminUser',),
    'PAGE_SIZE': 10
}

[...]

Add our first test: the url received is less then the original

  • Add our first test sobtituting shorturls_app/tests.py content with follow:
from django.test import TestCase
from .models import Link


class ShortenerText(TestCase):

    def test_shortens(self):
        """
        Test that urls get shorter
        """

        url = "http://www.example.com/"
        l = Link(url=url)
        short_url = Link.shorten(l)
        self.assertLess(len(short_url), len(url))
  • Run the test, it will fail
python ../manage.py test shorturls_app
  • Add our fist model sobstituting shorturls_app/models.py with follow
from django.db import models


class Link(models.Model):

    url = models.URLField()

    @staticmethod
    def shorten(long_url):
        return ""

  • Migrate the model (this create database tables)
python ../manage.py makemigrations
python ../manage.py migrate
python ../manage.py test shorturls_app

Add our second test: obtain the original url from reduced one

  • append follow to ShortenerText class in shorturls_app/tests.py
[...]

    def test_recover_link(self):
        """
        Tests that the shortened then expanded url is the same as original
        """

        url = "http://www.example.com/"
        l = Link(url=url)
        short_url = Link.shorten(l)
        l.save()
        # Another user asks for the expansion of short_url
        exp_url = Link.expand(short_url)
        self.assertEqual(url, exp_url)
  • Run the test again, it will fail, because Link model have not expand function
python ../manage.py test shorturls_app
  • So we can create a dummy expand function, but the test will fail again

In this case, we can alter the Link methods to use the primary key as shorten url, in a way that shorten method returns the primary key, and the expand method returns the url related to the primary key that the user inform.

So, change the Link model sobstituting the shorturls_app/models.py with the follow one:

from django.db import models


class Link(models.Model):

    url = models.URLField()

    @staticmethod
    def shorten(link):
        l, _ = Link.objects.get_or_create(url=link.url)
        return str(l.pk)

    @staticmethod
    def expand(slug):
        link_id = int(slug)
        l = Link.objects.get(pk=link_id)
        return l.url

  • Run the test again, it will succeed
python ../manage.py test shorturls_app

Add our first REST interface

  • Add a View for Links in shorturls_app/views.py
from rest_framework import viewsets
from .serializers import LinkSerializer
from .models import Link


class LinkViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows links to be viewed or edited.
    """

    queryset = Link.objects.all().order_by('-url')
    serializer_class = LinkSerializer
  • Add a Link serializer in shorturls_app/serializers.py
from rest_framework import serializers
from .models import Link


class LinkSerializer(serializers.HyperlinkedModelSerializer):

    class Meta:
        model = Link
        fields = ['url']
  • Configure common project urls adding the follow in tdd_rest_project/urls.py:
[...]
from django.conf.urls import include
from rest_framework import routers
from .shorturls_app import views

router = routers.DefaultRouter()
router.register(r'links', views.LinkViewSet)

# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.

urlpatterns.append(
    url(r'^rest/', include(router.urls))
)
urlpatterns.append(
    url(r'^api-auth/', include(
        'rest_framework.urls', namespace='rest_framework'))
)
  • Run the server
python ../manage.py runserver
  • Open 'localhost:8000/rest/links' in your browser, log-in and enjoy

Add our first test to our REST interface

  • Sobstitute wit follow:

Auth details in http://www.django-rest-framework.org/api-guide/testing/#forcing-authentication

from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APIRequestFactory
from rest_framework.test import force_authenticate
import json
from .models import Link
from .views import LinkViewSet


class ShortenerText(TestCase):

    def test_shortens(self):
        """
        Test that urls get shorter
        """

        url = "http://www.example.com/"
        l = Link(url=url)
        short_url = Link.shorten(l)
        self.assertLess(len(short_url), len(url))

    def test_recover_link(self):
        """
        Tests that the shortened then expanded url is the same as original
        """

        url = "http://www.example.com/"
        l = Link(url=url)
        short_url = Link.shorten(l)
        l.save()
        # Another user asks for the expansion of short_url
        exp_url = Link.expand(short_url)
        self.assertEqual(url, exp_url)

    def test_rest_obtaining_a_saved_link_link(self):
        """
        Tests a REST request retreiving a saved url
        """
        rest_url = "/rest/links/"
        data = "http://www.example.com/"

        # save link
        l = Link(url=data)
        l.save()

        # Another user asks for the expansion of short_url

        user = User(username='user', password='user', is_staff=True)
        user.save()

        request = APIRequestFactory().post(
            rest_url,
            json.dumps({'url': data}),
            content_type='application/json'
        )

        force_authenticate(request, user=user)

        view = LinkViewSet.as_view({'post': 'create'})
        response = view(request)

        self.assertEqual(response.data, {'url': data})