Skip to content
Snippets Groups Projects
Commit 37cad4d6 authored by florimondmanca's avatar florimondmanca
Browse files

add action model and api, simplify tests, add migrate to TravisCI deployment stage

parent 769f8f43
No related branches found
No related tags found
No related merge requests found
Showing
with 289 additions and 263 deletions
......@@ -35,6 +35,9 @@ deploy:
# direct each branch to the corresponding app
master: oser-backend
staging: oser-backend-staging
run:
# automatically run new migrations
- "python oser_backend/manage.py migrate"
api_key:
# Encrypted API key obtained from the following command
# (requires TravisCI and Heroku CLI installed)
......
......@@ -45,6 +45,7 @@ router.register('categories', showcase_site_views.CategoryViewSet)
router.register('testimonies', showcase_site_views.TestimonyViewSet)
router.register('keyfigures', showcase_site_views.KeyFigureViewSet)
router.register('partners', showcase_site_views.PartnerViewSet)
router.register('actions', showcase_site_views.ActionViewSet)
# Core views
router.register('documents', core_views.DocumentViewSet)
......
......@@ -3,3 +3,4 @@ from django.apps import AppConfig
class CoreConfig(AppConfig):
name = 'core'
verbose_name = 'Général'
......@@ -69,3 +69,17 @@ class PartnerFactory(factory.DjangoModelFactory):
# 90% of partnerships will be active on average
active = factory.LazyFunction(
lambda: random.choices([True, False], weights=[.9, .1])[0])
class ActionFactory(factory.DjangoModelFactory):
"""Action object factory."""
class Meta: # noqa
model = models.Action
title = factory.Faker('text', max_nb_chars=30)
description = factory.Faker('text', max_nb_chars=300)
key_figure = factory.Faker('text', max_nb_chars=100)
highlight = factory.LazyFunction(
lambda: random.choices([True, False], weights=[.6, .4])[0]
)
# Generated by Django 2.0.3 on 2018-03-25 12:42
from django.db import migrations, models
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('showcase_site', '0004_auto_20180325_1338'),
]
operations = [
migrations.CreateModel(
name='Action',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=30, verbose_name='titre')),
('thumbnail', models.ImageField(blank=True, help_text="Une petite image représentant l'action. Format recommandé : 200x200", null=True, upload_to='actions/', verbose_name='illustration')),
('description', markdownx.models.MarkdownxField()),
('key_figure', models.TextField(blank=True, default='', help_text="Enoncer un chiffre clé à propos de cette action. Exemple : 'En 2018, 18 sorties ont été organisées dans des lieux tels que…'", verbose_name='chiffre clé')),
('highlight', models.BooleanField(default=True, help_text="Cochez pour afficher cette action sur la page d'accueil. Pour un affichage optimal, assurez-vous alors d'avoir renseigné une illustration.", verbose_name='mettre en avant')),
('order', models.PositiveIntegerField(default=0, verbose_name='ordre')),
],
options={
'verbose_name': 'action clé',
'verbose_name_plural': 'actions clés',
'ordering': ('order',),
},
),
]
......@@ -8,6 +8,7 @@ from dry_rest_permissions.generics import authenticated_users
from markdownx.models import MarkdownxField
# Create your models here.
......@@ -94,14 +95,45 @@ class Article(models.Model):
def has_object_read_permission(self, request):
return True
@staticmethod
@authenticated_users
def has_write_permission(request):
return True
def __str__(self):
return str(self.title)
@authenticated_users
def has_object_write_permission(self, request):
return True
class Action(models.Model):
"""Represents a key action point the association pursues."""
title = models.CharField('titre', max_length=30)
thumbnail = models.ImageField(
'illustration', null=True, blank=True, upload_to='actions/',
help_text=(
"Une petite image représentant l'action. "
"Format recommandé : 200x200"
)
)
description = MarkdownxField()
key_figure = models.TextField(
'chiffre clé',
default='', blank=True,
help_text=(
"Enoncer un chiffre clé à propos de cette action. "
"Exemple : "
"'En 2018, 18 sorties ont été organisées dans des lieux tels que…'"
)
)
highlight = models.BooleanField(
"mettre en avant", default=True,
help_text=(
"Cochez pour afficher cette action sur la page d'accueil. "
"Pour un affichage optimal, assurez-vous alors d'avoir renseigné "
"une illustration."
)
)
order = models.PositiveIntegerField('ordre', default=0)
class Meta: # noqa
verbose_name = 'action clé'
verbose_name_plural = 'actions clés'
ordering = ('order',)
def __str__(self):
return str(self.title)
......@@ -158,7 +190,7 @@ class KeyFigure(models.Model):
super().save(*args, **kwargs)
def __str__(self):
return '{} {}'.format(self.figure, self.description.lower())
return '{} {}'.format(self.figure, self.description)
class Partner(models.Model):
......
"""Showcase site API serializers."""
from rest_framework import serializers
from core.markdown import MarkdownField
from .models import Article, Category, Testimony, KeyFigure, Partner
from .models import Action, Article, Category, KeyFigure, Partner, Testimony
class CategoryField(serializers.RelatedField):
......@@ -80,3 +82,12 @@ class PartnerSerializer(serializers.ModelSerializer):
class Meta: # noqa
model = Partner
fields = ('id', 'name', 'website', 'logo', 'premium')
class ActionSerializer(serializers.ModelSerializer):
"""Serializer for action points."""
class Meta: # noqa
model = Action
fields = ('id', 'title', 'description', 'key_figure',
'thumbnail', 'highlight')
......@@ -7,24 +7,25 @@ from .serializers import CategorySerializer
from .serializers import TestimonySerializer
from .serializers import KeyFigureSerializer
from .serializers import PartnerSerializer
from .serializers import ActionSerializer
from .models import Article
from .models import Category
from .models import Testimony
from .models import KeyFigure
from .models import Partner
from .models import Action
# Create your views here.
class ArticleViewSet(viewsets.ModelViewSet):
"""API endpoint that allows articles to be viewed or edited.
class ArticleViewSet(viewsets.ReadOnlyModelViewSet):
"""API endpoint that allows articles to be viewed.
Actions: list, retrieve, create, update, partial_update, destroy
"""
serializer_class = ArticleSerializer
queryset = Article.objects.all()
permission_classes = (DRYPermissions,)
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
......@@ -67,3 +68,10 @@ class PartnerViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = PartnerSerializer
queryset = Partner.objects.filter(active=True)
class ActionViewSet(viewsets.ReadOnlyModelViewSet):
"""API endpoint to view actions."""
serializer_class = ActionSerializer
queryset = Action.objects.all()
"""API test mixins."""
from rest_framework import status
......@@ -70,3 +71,39 @@ class ProfileEndpointsTestMixin:
lambda: self.perform_delete(obj=obj),
user=user,
expected_status_code=status.HTTP_204_NO_CONTENT)
class SimpleReadOnlyResourceTestMixin:
"""Test mixin for simple read-only resources.
Must be mixed in with a SimpleAPITestCase.
Provided tests check the following:
- the list action is not authenticated, and returns code 200
- the retrieve action is not authenticated, and returns code 200
"""
list_url: str
retrieve_url_fmt: str
def perform_list(self):
response = self.client.get(self.list_url)
return response
def test_list_no_authentication_required(self):
self.assertRequestResponse(
self.perform_list,
user=None,
expected_status_code=status.HTTP_200_OK)
def perform_retrieve(self):
obj = self.factory.create()
url = self.retrieve_url_fmt.format(obj=obj)
response = self.client.get(url)
return response
def test_retrieve_no_authentication_required(self):
self.assertRequestResponse(
self.perform_retrieve,
user=None,
expected_status_code=status.HTTP_200_OK)
"""Action API tests."""
from showcase_site.factory import ActionFactory
from showcase_site.serializers import ActionSerializer
from tests.utils import SimpleAPITestCase
from .mixins import SimpleReadOnlyResourceTestMixin
class ActionEndpointsTest(SimpleReadOnlyResourceTestMixin,
SimpleAPITestCase):
"""Test access to the actions endpoints."""
factory = ActionFactory
serializer_class = ActionSerializer
list_url = '/api/actions/'
retrieve_url_fmt = '/api/actions/{obj.pk}/'
......@@ -37,74 +37,6 @@ class ArticleEndpointsTest(HyperlinkedAPITestCase):
user=None,
expected_status_code=status.HTTP_200_OK)
create_url = '/api/articles/'
def get_create_data(self):
# Use .build() instead of .create() to get a raw object
# not stored in DB.
obj = self.factory.build()
data = self.serialize(obj, 'post', self.create_url)
return data
def perform_create(self, data=None):
if data is None:
data = self.get_create_data()
response = self.client.post(self.create_url, data, format='json')
return response
def test_create_requires_to_be_authenticated(self):
self.assertRequiresAuth(
self.perform_create,
expected_status_code=status.HTTP_201_CREATED)
def test_create_with_categories(self):
# create categories
categories = CategoryFactory.create_batch(3)
data = self.get_create_data()
# add categories titles to the POST data
for cat in categories:
data.setdefault('categories', []).append(cat.title)
self.assertUserRequestResponse(
lambda: self.perform_create(data=data),
expected_status_code=status.HTTP_201_CREATED)
def perform_update(self):
obj = self.factory.create()
url = '/api/articles/{obj.pk}/'.format(obj=obj)
data = self.serialize(obj, 'put', url)
data['pinned'] = not data['pinned']
response = self.client.put(url, data, format='json')
return response
def test_update_requires_to_be_authenticated(self):
self.assertRequiresAuth(
self.perform_update,
expected_status_code=status.HTTP_200_OK)
def perform_partial_update(self):
obj = self.factory.create()
url = '/api/articles/{obj.pk}/'.format(obj=obj)
data = {'pinned': not obj.pinned}
response = self.client.patch(url, data, format='json')
return response
def test_partial_update_requires_to_be_authenticated(self):
self.assertRequiresAuth(
self.perform_partial_update,
expected_status_code=status.HTTP_200_OK)
def perform_delete(self):
obj = self.factory.create()
url = '/api/articles/{obj.pk}/'.format(obj=obj)
response = self.client.delete(url)
return response
def test_delete_requires_to_be_authenticated(self):
self.assertRequiresAuth(
self.perform_delete,
expected_status_code=status.HTTP_204_NO_CONTENT)
class TestArticleSerializer(SerializerTestCaseMixin, TestCase):
"""Test the Article serializer."""
......
"""Category API tests."""
from rest_framework import status
from showcase_site.factory import CategoryFactory
from showcase_site.serializers import CategorySerializer
from tests.utils import HyperlinkedAPITestCase
from tests.utils import SimpleAPITestCase
from .mixins import SimpleReadOnlyResourceTestMixin
class CategoryEndpointsTest(HyperlinkedAPITestCase):
class CategoryEndpointsTest(
SimpleReadOnlyResourceTestMixin, SimpleAPITestCase):
"""Test access to the categories endpoints."""
factory = CategoryFactory
serializer_class = CategorySerializer
def perform_list(self):
url = '/api/categories/'
response = self.client.get(url)
return response
def test_list_no_authentication_required(self):
self.assertRequestResponse(
self.perform_list,
user=None,
expected_status_code=status.HTTP_200_OK)
def perform_retrieve(self):
obj = self.factory.create()
url = '/api/categories/{obj.pk}/'.format(obj=obj)
response = self.client.get(url)
return response
def test_retrieve_no_authentication_required(self):
self.assertRequestResponse(
self.perform_retrieve,
user=None,
expected_status_code=status.HTTP_200_OK)
list_url = '/api/categories/'
retrieve_url_fmt = '/api/categories/{obj.pk}/'
"""Document API tests."""
from rest_framework import status
from core.factory import DocumentFactory
from core.serializers import DocumentSerializer
from tests.utils import HyperlinkedAPITestCase
from tests.utils import SimpleAPITestCase
from .mixins import SimpleReadOnlyResourceTestMixin
class ArticleEndpointsTest(HyperlinkedAPITestCase):
"""Test access to the articles endpoints."""
class DocumentEndpointsTest(
SimpleReadOnlyResourceTestMixin, SimpleAPITestCase):
"""Test access to the documents endpoints."""
factory = DocumentFactory
serializer_class = DocumentSerializer
def perform_list(self):
url = '/api/documents/'
response = self.client.get(url)
return response
def test_list_no_authentication_required(self):
self.assertRequestResponse(
self.perform_list,
user=None,
expected_status_code=status.HTTP_200_OK)
def perform_retrieve(self):
obj = self.factory.create()
url = '/api/documents/{obj.slug}/'.format(obj=obj)
response = self.client.get(url)
return response
def test_retrieve_no_authentication_required(self):
self.assertRequestResponse(
self.perform_retrieve,
user=None,
expected_status_code=status.HTTP_200_OK)
list_url = '/api/documents/'
retrieve_url_fmt = '/api/documents/{obj.slug}/'
"""KeyFigure API tests."""
from rest_framework import status
from showcase_site.factory import KeyFigureFactory
from showcase_site.serializers import KeyFigureSerializer
from tests.utils import HyperlinkedAPITestCase
from tests.utils import SimpleAPITestCase
from .mixins import SimpleReadOnlyResourceTestMixin
class KeyFigureEndpointsTest(HyperlinkedAPITestCase):
class KeyFigureEndpointsTest(
SimpleReadOnlyResourceTestMixin, SimpleAPITestCase):
"""Test access to the key figures endpoints."""
factory = KeyFigureFactory
serializer_class = KeyFigureSerializer
def perform_list(self):
url = '/api/keyfigures/'
response = self.client.get(url)
return response
def test_list_no_authentication_required(self):
self.assertRequestResponse(
self.perform_list,
user=None,
expected_status_code=status.HTTP_200_OK)
def perform_retrieve(self):
obj = self.factory.create()
url = '/api/keyfigures/{obj.pk}/'.format(obj=obj)
response = self.client.get(url)
return response
def test_retrieve_no_authentication_required(self):
self.assertRequestResponse(
self.perform_retrieve,
user=None,
expected_status_code=status.HTTP_200_OK)
list_url = '/api/keyfigures/'
retrieve_url_fmt = '/api/keyfigures/{obj.pk}/'
"""Partner API tests."""
from django.test import TestCase
from rest_framework import status
from showcase_site.factory import PartnerFactory
from showcase_site.models import Partner
from showcase_site.serializers import PartnerSerializer
from showcase_site.views import PartnerViewSet
from tests.utils import HyperlinkedAPITestCase
from tests.utils import SimpleAPITestCase
from .mixins import SimpleReadOnlyResourceTestMixin
class PartnerEndpointsTest(HyperlinkedAPITestCase):
"""Test access to the key figures endpoints."""
class PartnerEndpointsTest(
SimpleReadOnlyResourceTestMixin, SimpleAPITestCase):
"""Test access to the partners endpoints."""
factory = PartnerFactory
serializer_class = PartnerSerializer
def perform_list(self):
url = '/api/partners/'
response = self.client.get(url)
return response
def test_list_no_authentication_required(self):
self.assertRequestResponse(
self.perform_list,
user=None,
expected_status_code=status.HTTP_200_OK)
def perform_retrieve(self):
# Only active partners are exposed by API, so
# make sure to create an active one.
obj = self.factory.create(active=True)
url = '/api/partners/{obj.pk}/'.format(obj=obj)
response = self.client.get(url)
return response
def test_retrieve_no_authentication_required(self):
self.assertRequestResponse(
self.perform_retrieve,
user=None,
expected_status_code=status.HTTP_200_OK)
class PartnerViewSetTest(TestCase):
"""Test partners viewset."""
def test_queryset_is_active_partners_only(self):
self.assertQuerysetEqual(
PartnerViewSet().get_queryset(),
Partner.objects.filter(active=True))
list_url = '/api/partners/'
retrieve_url_fmt = '/api/partners/{obj.pk}/'
"""Testimony API tests."""
from rest_framework import status
from showcase_site.factory import TestimonyFactory
from showcase_site.serializers import TestimonySerializer
from tests.utils import HyperlinkedAPITestCase
from tests.utils import SimpleAPITestCase
from .mixins import SimpleReadOnlyResourceTestMixin
class TestimonyEndpointsTest(HyperlinkedAPITestCase):
class TestimonyEndpointsTest(
SimpleReadOnlyResourceTestMixin, SimpleAPITestCase):
"""Test access to the testimonies endpoints."""
factory = TestimonyFactory
serializer_class = TestimonySerializer
def perform_list(self):
url = '/api/testimonies/'
response = self.client.get(url)
return response
def test_list_no_authentication_required(self):
self.assertRequestResponse(
self.perform_list,
user=None,
expected_status_code=status.HTTP_200_OK)
def perform_retrieve(self):
obj = self.factory.create()
url = '/api/testimonies/{obj.pk}/'.format(obj=obj)
response = self.client.get(url)
return response
def test_retrieve_no_authentication_required(self):
self.assertRequestResponse(
self.perform_retrieve,
user=None,
expected_status_code=status.HTTP_200_OK)
list_url = '/api/testimonies/'
retrieve_url_fmt = '/api/testimonies/{obj.pk}/'
......@@ -3,12 +3,12 @@
from django.test import TestCase
from rest_framework import status
from tests.utils import HyperlinkedAPITestCase, SerializerTestCaseMixin
from tests.utils import SimpleAPITestCase, SerializerTestCaseMixin
from visits.factory import VisitFactory
from visits.serializers import VisitSerializer
class VisitEndpointsTest(HyperlinkedAPITestCase):
class VisitEndpointsTest(SimpleAPITestCase):
"""Test access to the visits endpoints."""
factory = VisitFactory
......
"""Action model tests."""
from showcase_site.factory import ActionFactory
from showcase_site.models import Action
from tests.utils import ModelTestCase
class ActionTest(ModelTestCase):
"""Test the Action model."""
model = Action
field_tests = {
'title': {
'max_length': 30,
'verbose_name': 'titre',
},
'description': {
},
'key_figure': {
'verbose_name': 'chiffre clé',
'default': '',
'blank': True,
},
'highlight': {
'verbose_name': 'mettre en avant',
'default': True,
},
'order': {
'verbose_name': 'ordre',
'default': 0,
}
}
model_tests = {
'verbose_name': 'action clé',
'verbose_name_plural': 'actions clés',
'ordering': ('order',),
}
@classmethod
def setUpTestData(cls):
cls.obj = ActionFactory.create()
def test_str_is_title(self):
self.assertEqual(str(self.obj), self.obj.title)
......@@ -5,43 +5,15 @@ from rest_framework.test import APIRequestFactory, APITestCase
from users.factory import UserFactory
__all__ = ('HyperlinkedAPITestCase', 'SerializerTestCaseMixin')
__all__ = (
'SimpleAPITestCase',
'HyperlinkedAPITestCase',
'SerializerTestCaseMixin'
)
class HyperlinkedAPITestCase(APITestCase):
"""API test case suited for hyperlinked serializers."""
serializer_class = None
def serialize(self, obj, method, url,
serializer_class=None):
"""Serialize an object.
Parameters
----------
obj : instance of django.db.models.Model
method : str
An HTTP method (case insensitive), e.g. 'post'.
url : str
serializer_class : subclass of rest_framework.Serializer, optional
If not given, the test case class' serializer_class attribute
will be used.
"""
if serializer_class is None:
serializer_class = self.get_serializer_class()
request_factory = getattr(APIRequestFactory(), method.lower())
request = request_factory(url, format='json')
serializer = serializer_class(
obj, context={'request': request})
data = serializer.data
return data
def get_serializer_class(self):
"""Return the serializer class."""
if self.serializer_class is None:
raise AttributeError('serializer_class attribute not defined'
'for {}'.format(self.__class__))
return self.serializer_class
class SimpleAPITestCase(APITestCase):
"""API test case that provides handy extra assert functions."""
@staticmethod
def _check_response(perform_request, response):
......@@ -118,6 +90,41 @@ class HyperlinkedAPITestCase(APITestCase):
expected_status_code=status.HTTP_403_FORBIDDEN)
class HyperlinkedAPITestCase(SimpleAPITestCase):
"""API test case suited for hyperlinked serializers."""
serializer_class = None
def serialize(self, obj, method, url, serializer_class=None):
"""Serialize an object.
Parameters
----------
obj : instance of django.db.models.Model
method : str
An HTTP method (case insensitive), e.g. 'post'.
url : str
serializer_class : subclass of rest_framework.Serializer, optional
If not given, the test case class' serializer_class attribute
will be used.
"""
if serializer_class is None:
serializer_class = self.get_serializer_class()
request_factory = getattr(APIRequestFactory(), method.lower())
request = request_factory(url, format='json')
serializer = serializer_class(
obj, context={'request': request})
data = serializer.data
return data
def get_serializer_class(self):
"""Return the serializer class."""
if self.serializer_class is None:
raise AttributeError('serializer_class attribute not defined'
'for {}'.format(self.__class__))
return self.serializer_class
class SerializerTestCaseMixin:
"""Base test case for serializers."""
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment