From 81e9a4b8d243f467abefd55e38513a9a943f20ee Mon Sep 17 00:00:00 2001 From: Qwann Date: Tue, 18 Jul 2017 17:44:43 +0200 Subject: [PATCH 01/30] allow local developement in venv --- evenementiel/settings/common.py | 11 +++++++++++ manage.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/evenementiel/settings/common.py b/evenementiel/settings/common.py index 61bf2d6..299894e 100644 --- a/evenementiel/settings/common.py +++ b/evenementiel/settings/common.py @@ -55,8 +55,10 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'channels', + 'rest_framework', 'bootstrapform', 'widget_tweaks', + 'api', ] MIDDLEWARE_CLASSES = [ @@ -70,6 +72,13 @@ MIDDLEWARE_CLASSES = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAdminUser', + ], + 'PAGE_SIZE': 10 +} + ROOT_URLCONF = 'evenementiel.urls' STATIC_URL = "/static/" @@ -78,6 +87,8 @@ MEDIA_URL = "/media/" LOGIN_REDIRECT_URL = 'shared:home' LOGOUT_REDIRECT_URL = 'shared:home' +AUTH_USER_MODEL = "users.User" + TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', diff --git a/manage.py b/manage.py index 035cb32..22640b3 100755 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import os import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "evenementiel.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "evenementiel.settings.devlocal") from django.core.management import execute_from_command_line From c8483ca7fb08b812b779c4183de2334414c44bae Mon Sep 17 00:00:00 2001 From: Qwann Date: Tue, 18 Jul 2017 17:46:00 +0200 Subject: [PATCH 02/30] replace User by get_user_model --- event/models.py | 4 +++- users/models.py | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/event/models.py b/event/models.py index aaad757..44f17bb 100644 --- a/event/models.py +++ b/event/models.py @@ -1,9 +1,11 @@ -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.utils.translation import ugettext_lazy as _ from django.core.validators import RegexValidator from django.core.exceptions import FieldError from django.db import models +User = get_user_model() + class Event(models.Model): title = models.CharField( diff --git a/users/models.py b/users/models.py index 85206e0..201983c 100644 --- a/users/models.py +++ b/users/models.py @@ -1,2 +1,17 @@ from django.db import models -from django.contrib.auth.models import User +from django.contrib.auth.models import AbstractUser, UserManager +from django.utils.translation import ugettext_lazy as _ + + +class User(AbstractUser): + phone = models.CharField( + _('numéro de téléphone'), + max_length=20, + blank=True, + ) + + objects = UserManager() + + class Meta: + verbose_name = _("utilisateur") + verbose_name_plural = _("utilisateurs") From 1924d9b81adc71dfb73f86c8ac5dbda5f3475c5b Mon Sep 17 00:00:00 2001 From: Qwann Date: Tue, 18 Jul 2017 17:48:59 +0200 Subject: [PATCH 03/30] fogot a file --- api/event/serializers.py | 12 ++++++++++ api/event/views.py | 19 ++++++++++++++++ api/urls.py | 15 ++++++++++++ api/user/serializers.py | 38 +++++++++++++++++++++++++++++++ api/user/views.py | 0 evenementiel/settings/devlocal.py | 23 +++++++++++++++++++ evenementiel/urls.py | 1 + requirements.txt | 1 + 8 files changed, 109 insertions(+) create mode 100644 api/event/serializers.py create mode 100644 api/event/views.py create mode 100644 api/urls.py create mode 100644 api/user/serializers.py create mode 100644 api/user/views.py create mode 100644 evenementiel/settings/devlocal.py diff --git a/api/event/serializers.py b/api/event/serializers.py new file mode 100644 index 0000000..18bc74d --- /dev/null +++ b/api/event/serializers.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from event.models import Event +from django.contrib.auth import get_user_model + + +class EventSerializer(serializers.HyperlinkedModelSerializer): + created_by = serializers.ReadOnlyField(source='created_by.username') + + class Meta: + model = Event + fields = ('url', 'id', 'title', 'slug', 'created_by', 'creation_date', + 'description', 'begining_date', 'ending_date') diff --git a/api/event/views.py b/api/event/views.py new file mode 100644 index 0000000..0b05eb7 --- /dev/null +++ b/api/event/views.py @@ -0,0 +1,19 @@ +from django.contrib.auth import get_user_model + +from rest_framework.viewsets import ModelViewSet + +from api.event.serializers import EventSerializer +from event.models import Event + +User = get_user_model() + + +class EventViewSet(ModelViewSet): + """ + This viewset automatically provides `list` and `detail` actions. + """ + queryset = Event.objects.all() + serializer_class = EventSerializer + + def perform_create(self, serializer): + serializer.save(created_by=self.request.user) diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..660569c --- /dev/null +++ b/api/urls.py @@ -0,0 +1,15 @@ +from django.conf.urls import url, include +from rest_framework.routers import DefaultRouter +from api.event.views import EventViewSet + +# Create a router and register our viewsets with it. +router = DefaultRouter() +router.register(r'event', EventViewSet) + +# The API URLs are now determined automatically by the router. +# Additionally, we include the login URLs for the browsable API. +urlpatterns = [ + url(r'^', include(router.urls)), + url(r'^api-auth/', include('rest_framework.urls', + namespace='rest_framework')) +] diff --git a/api/user/serializers.py b/api/user/serializers.py new file mode 100644 index 0000000..8b83167 --- /dev/null +++ b/api/user/serializers.py @@ -0,0 +1,38 @@ +from rest_framework import serializers +from django.contrib.auth import get_user_model + +User = get_user_model() + + +class UserMinimalSerializer(serializers.HyperlinkedModelSerializer): + """ + Déstiné à tout le monde (de connecté) + """ + + class Meta: + model = User + fields = ('url', 'id', 'first_name', 'last_name',) + + +class UserAdminSerializer(serializers.HyperlinkedModelSerializer): + """ + Déstiné à l'utilisat-rice-eur et aux administrat-rice-eur-s + """ + + class Meta: + model = User + fields = ('url', 'id', 'username', 'first_name', 'last_name', + 'email', 'phone', 'last_login', 'date_joined',) + + +class UserSerializer(serializers.HyperlinkedModelSerializer): + """ + Déstiné aux utilisat-rice-eur-s dont l'utilisait-rice-eur-s en question + a laissé le droit d'y accéder, par exemple les participant-e-s + au même `event` que l'utilisat-rice-eur en question + """ + + class Meta: + model = User + fields = ('url', 'id', 'first_name', 'last_name', + 'email', 'phone',) diff --git a/api/user/views.py b/api/user/views.py new file mode 100644 index 0000000..e69de29 diff --git a/evenementiel/settings/devlocal.py b/evenementiel/settings/devlocal.py new file mode 100644 index 0000000..e420dda --- /dev/null +++ b/evenementiel/settings/devlocal.py @@ -0,0 +1,23 @@ +""" +Django development settings for GestionÉvénementiel +The settings that are not listed here are imported from .common +""" + +import os +from .dev import * # NOQA + + +# SQLite +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +CHANNEL_LAYERS = { + "default": { + "BACKEND": "asgiref.inmemory.ChannelLayer", + "ROUTING": "evenementiel.routing.channel_routing", + }, +} diff --git a/evenementiel/urls.py b/evenementiel/urls.py index d83241d..3098985 100644 --- a/evenementiel/urls.py +++ b/evenementiel/urls.py @@ -9,6 +9,7 @@ urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^event/', include('event.urls')), url(r'^user/', include('users.urls')), + url(r'^api/', include('api.urls')), url(r'^', include('shared.urls')), ] diff --git a/requirements.txt b/requirements.txt index 54d9b30..9313a53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ Pillow channels django-bootstrap-form==3.2.1 django-widget-tweaks +djangorestframework==3.6.3 # Production specific daphne From faafbf0630c34fddc613be4d97837cab171ffa7e Mon Sep 17 00:00:00 2001 From: Qwann Date: Thu, 20 Jul 2017 15:08:19 +0200 Subject: [PATCH 04/30] serializers started --- api/event/serializers.py | 51 +++++++++++++++++++++++++++++++++++++--- api/event/views.py | 39 +++++++++++++++++++++++++++--- api/urls.py | 6 ++++- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/api/event/serializers.py b/api/event/serializers.py index 18bc74d..6195837 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -1,6 +1,5 @@ from rest_framework import serializers -from event.models import Event -from django.contrib.auth import get_user_model +from event.models import Event, ActivityTag, Place, ActivityTemplate class EventSerializer(serializers.HyperlinkedModelSerializer): @@ -9,4 +8,50 @@ class EventSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Event fields = ('url', 'id', 'title', 'slug', 'created_by', 'creation_date', - 'description', 'begining_date', 'ending_date') + 'description', 'beginning_date', 'ending_date') + + +# TODO rajouter des permissions +class PlaceSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = Place + fields = ('url', 'id', 'name', 'description',) + + +# TODO rajouter des permissions +class ActivityTagSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = ActivityTag + fields = ('url', 'id', 'name', 'is_public', 'color',) + + +# TODO rajouter des permissions +class ActivityTemplateSerializer(serializers.HyperlinkedModelSerializer): + event = serializers.ReadOnlyField(source='event.title') + tags = ActivityTagSerializer(many=True) + + class Meta: + model = ActivityTemplate + fields = ('url', 'id', 'title', 'event', 'is_public', 'has_perm', + 'min_perm', 'max_perm', 'description', 'remarks', 'tags',) + + def update(self, instance, validated_data): + tags_data = validated_data.pop('tags') + [setattr(instance, key, value) for key, value in validated_data.items()] + instance.save() + tags = [ActivityTag.objects.get_or_create(**tag_data)[0] + for tag_data in tags_data] + instance.tags = tags + + return instance + + def create(self, validated_data): + tags_data = validated_data.pop('tags') + activity_template = ActivityTemplate.objects.create(**validated_data) + tags = [ ActivityTag.objects.get_or_create(**tag_data)[0] + for tag_data in tags_data] + activity_template.tags = tags + + return activity_template diff --git a/api/event/views.py b/api/event/views.py index 0b05eb7..fc66a42 100644 --- a/api/event/views.py +++ b/api/event/views.py @@ -2,18 +2,51 @@ from django.contrib.auth import get_user_model from rest_framework.viewsets import ModelViewSet -from api.event.serializers import EventSerializer -from event.models import Event +from api.event.serializers import EventSerializer, PlaceSerializer,\ + ActivityTagSerializer, ActivityTemplateSerializer +from event.models import Event, Place, ActivityTag, ActivityTemplate User = get_user_model() class EventViewSet(ModelViewSet): """ - This viewset automatically provides `list` and `detail` actions. + This viewset automatically provides `list`, `create`, `retrieve`, + `update` and `destroy` actions. + """ queryset = Event.objects.all() serializer_class = EventSerializer def perform_create(self, serializer): serializer.save(created_by=self.request.user) + + +class PlaceViewSet(ModelViewSet): + """ + This viewset automatically provides `list`, `create`, `retrieve`, + `update` and `destroy` actions. + + """ + queryset = Place.objects.all() + serializer_class = PlaceSerializer + + +class ActivityTagViewSet(ModelViewSet): + """ + This viewset automatically provides `list`, `create`, `retrieve`, + `update` and `destroy` actions. + + """ + queryset = ActivityTag.objects.all() + serializer_class = ActivityTagSerializer + + +class ActivityTemplateViewSet(ModelViewSet): + """ + This viewset automatically provides `list`, `create`, `retrieve`, + `update` and `destroy` actions. + + """ + queryset = ActivityTemplate.objects.all() + serializer_class = ActivityTemplateSerializer diff --git a/api/urls.py b/api/urls.py index 660569c..337a87f 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,10 +1,14 @@ from django.conf.urls import url, include from rest_framework.routers import DefaultRouter -from api.event.views import EventViewSet +from api.event.views import EventViewSet, PlaceViewSet, ActivityTagViewSet,\ + ActivityTemplateViewSet # Create a router and register our viewsets with it. router = DefaultRouter() router.register(r'event', EventViewSet) +router.register(r'place', PlaceViewSet) +router.register(r'tag', ActivityTagViewSet) +router.register(r'activitytemplate', ActivityTemplateViewSet) # The API URLs are now determined automatically by the router. # Additionally, we include the login URLs for the browsable API. From 92e8e374fd0cf7dbe0728dc7f7165280e15d9756 Mon Sep 17 00:00:00 2001 From: Qwann Date: Fri, 21 Jul 2017 17:03:33 +0200 Subject: [PATCH 05/30] WIP --- api/event/mixins.py | 5 ++++ api/event/serializers.py | 34 ++++++++++++++++++----- api/event/views.py | 18 ++++++++++++- api/urls.py | 12 ++++++--- requirements.txt | 1 + users/migrations/0001_initial.py | 46 ++++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 api/event/mixins.py create mode 100644 users/migrations/0001_initial.py diff --git a/api/event/mixins.py b/api/event/mixins.py new file mode 100644 index 0000000..d9aeaf5 --- /dev/null +++ b/api/event/mixins.py @@ -0,0 +1,5 @@ +class EventNestedMixin(object): + pass + +class ventSpecificSerializerMixin(object): + pass diff --git a/api/event/serializers.py b/api/event/serializers.py index 6195837..52d898c 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -13,10 +13,33 @@ class EventSerializer(serializers.HyperlinkedModelSerializer): # TODO rajouter des permissions class PlaceSerializer(serializers.HyperlinkedModelSerializer): + event = EventSerializer(allow_null=True) class Meta: model = Place - fields = ('url', 'id', 'name', 'description',) + fields = ('url', 'id', 'name', 'description', 'event') + + def update(self, instance, validated_data): + try: + data = validated_data.pop('event') + event = Event.objects.get_object_or_404(**data) + except KeyError: + event = None + [setattr(instance, key, value) + for key, value in validated_data.items()] + setattr(instance, 'event', event) + instance.save() + return instance + + def create(self, validated_data): + ModelClass = self.Meta.model + try: + data = validated_data.pop('event') + event = Event.objects.get_object_or_404(**data) + except KeyError: + event = None + instance = ModelClass.objects.create(event=event, **validated_data) + return instance # TODO rajouter des permissions @@ -39,19 +62,18 @@ class ActivityTemplateSerializer(serializers.HyperlinkedModelSerializer): def update(self, instance, validated_data): tags_data = validated_data.pop('tags') - [setattr(instance, key, value) for key, value in validated_data.items()] + [setattr(instance, key, value) + for key, value in validated_data.items()] instance.save() tags = [ActivityTag.objects.get_or_create(**tag_data)[0] for tag_data in tags_data] instance.tags = tags - - return instance + return instance def create(self, validated_data): tags_data = validated_data.pop('tags') activity_template = ActivityTemplate.objects.create(**validated_data) - tags = [ ActivityTag.objects.get_or_create(**tag_data)[0] + tags = [ActivityTag.objects.get_or_create(**tag_data)[0] for tag_data in tags_data] activity_template.tags = tags - return activity_template diff --git a/api/event/views.py b/api/event/views.py index fc66a42..5e3b210 100644 --- a/api/event/views.py +++ b/api/event/views.py @@ -1,9 +1,10 @@ from django.contrib.auth import get_user_model -from rest_framework.viewsets import ModelViewSet +from rest_framework.viewsets import ModelViewSet, ViewSet from api.event.serializers import EventSerializer, PlaceSerializer,\ ActivityTagSerializer, ActivityTemplateSerializer +from api.event.mixins import EventNestedMixin from event.models import Event, Place, ActivityTag, ActivityTemplate User = get_user_model() @@ -32,6 +33,21 @@ class PlaceViewSet(ModelViewSet): serializer_class = PlaceSerializer +class PlaceEventViewSet(ModelViewSet): + queryset = Place.objects.all() + serializer_class = PlaceSerializer + + def list(self, request, event_pk=None): + queryset = self.queryset.filter(event=event_pk) + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + class ActivityTagViewSet(ModelViewSet): """ This viewset automatically provides `list`, `create`, `retrieve`, diff --git a/api/urls.py b/api/urls.py index 337a87f..b7c60cc 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,19 +1,25 @@ from django.conf.urls import url, include -from rest_framework.routers import DefaultRouter +from rest_framework_nested.routers import SimpleRouter, NestedSimpleRouter from api.event.views import EventViewSet, PlaceViewSet, ActivityTagViewSet,\ - ActivityTemplateViewSet + ActivityTemplateViewSet, PlaceEventViewSet # Create a router and register our viewsets with it. -router = DefaultRouter() +router = SimpleRouter() router.register(r'event', EventViewSet) router.register(r'place', PlaceViewSet) router.register(r'tag', ActivityTagViewSet) router.register(r'activitytemplate', ActivityTemplateViewSet) +# Register nested router and register someviewsets vith it +event_router = NestedSimpleRouter(router, r'event', lookup='event') +event_router.register(r'place', PlaceEventViewSet, base_name='event-names') + + # The API URLs are now determined automatically by the router. # Additionally, we include the login URLs for the browsable API. urlpatterns = [ url(r'^', include(router.urls)), + url(r'^', include(event_router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) ] diff --git a/requirements.txt b/requirements.txt index 7357c33..18efed2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ channels django-bootstrap-form==3.2.1 django-widget-tweaks djangorestframework==3.6.3 +drf-nested-routers==0.90.0 django-notifications django-contrib-comments diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py new file mode 100644 index 0000000..347daa4 --- /dev/null +++ b/users/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2017-07-18 14:22 +from __future__ import unicode_literals + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('phone', models.CharField(blank=True, max_length=20, verbose_name='numéro de téléphone')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'utilisateur', + 'verbose_name_plural': 'utilisateurs', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] From 48f9fd516dd0e5226db97a3be94f8fc33d33bea7 Mon Sep 17 00:00:00 2001 From: Qwann Date: Sat, 22 Jul 2017 00:56:30 +0200 Subject: [PATCH 06/30] EventSpecificSerializer and EventSpecificViewSet defined --- api/event/serializers.py | 53 ++++++++++++++++++++++------------------ api/event/views.py | 53 +++++++++++++++++++++++----------------- api/urls.py | 4 +-- 3 files changed, 61 insertions(+), 49 deletions(-) diff --git a/api/event/serializers.py b/api/event/serializers.py index 52d898c..db84ebd 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -1,7 +1,34 @@ +from django.shortcuts import get_object_or_404 from rest_framework import serializers from event.models import Event, ActivityTag, Place, ActivityTemplate +# Classes utilitaires +class EventSpecificSerializer(serializers.HyperlinkedModelSerializer): + """ + Provide `update` and `create` methods for nested view with an Event + For example for Models which exetends EventSpecificMixin + the event id has to be provided in the `save` method + Works fine with view.EventSpecificViewSet + """ + def update(self, instance, validated_data): + """ + Note : does NOT change the event value of the instance + """ + validated_data.pop('event_pk') + [setattr(instance, key, value) + for key, value in validated_data.items()] + instance.save() + return instance + + def create(self, validated_data): + ModelClass = self.Meta.model + event_pk = validated_data.pop('event_pk', None) + event = get_object_or_404(Event, id=event_pk) + instance = ModelClass.objects.create(event=event, **validated_data) + return instance + + class EventSerializer(serializers.HyperlinkedModelSerializer): created_by = serializers.ReadOnlyField(source='created_by.username') @@ -12,35 +39,13 @@ class EventSerializer(serializers.HyperlinkedModelSerializer): # TODO rajouter des permissions -class PlaceSerializer(serializers.HyperlinkedModelSerializer): - event = EventSerializer(allow_null=True) +class PlaceSerializer(EventSpecificSerializer): + event = EventSerializer(allow_null=True, read_only=True) class Meta: model = Place fields = ('url', 'id', 'name', 'description', 'event') - def update(self, instance, validated_data): - try: - data = validated_data.pop('event') - event = Event.objects.get_object_or_404(**data) - except KeyError: - event = None - [setattr(instance, key, value) - for key, value in validated_data.items()] - setattr(instance, 'event', event) - instance.save() - return instance - - def create(self, validated_data): - ModelClass = self.Meta.model - try: - data = validated_data.pop('event') - event = Event.objects.get_object_or_404(**data) - except KeyError: - event = None - instance = ModelClass.objects.create(event=event, **validated_data) - return instance - # TODO rajouter des permissions class ActivityTagSerializer(serializers.HyperlinkedModelSerializer): diff --git a/api/event/views.py b/api/event/views.py index 5e3b210..8060219 100644 --- a/api/event/views.py +++ b/api/event/views.py @@ -1,15 +1,42 @@ from django.contrib.auth import get_user_model +from django.db.models import Q -from rest_framework.viewsets import ModelViewSet, ViewSet +from rest_framework.viewsets import ModelViewSet from api.event.serializers import EventSerializer, PlaceSerializer,\ ActivityTagSerializer, ActivityTemplateSerializer -from api.event.mixins import EventNestedMixin from event.models import Event, Place, ActivityTag, ActivityTemplate User = get_user_model() +# classes utilitaires +class EventSpecificViewSet(ModelViewSet): + """ + ViewSet that returns : + * rootlevel objects if no Event is specified + * OR objects related to the specidied event + AND root level objects + if an event is specified it passes the event_pk + to the save method. Works fine with serializers.EventSpecificSerializer + Useful for models that exetends EventSpecificMixin + """ + def get_queryset(self): + """ + Warning : You may want to override this method + and not call with super + """ + event_pk = self.kwargs.get('event_pk') + queryset = super().get_queryset() + if event_pk: + return queryset.filter(Q(event=event_pk) | Q(event=None)) + return queryset.filter(event=None) + + def perform_create(self, serializer): + event_pk = self.kwargs.get('event_pk') + serializer.save(event_pk=event_pk) + + class EventViewSet(ModelViewSet): """ This viewset automatically provides `list`, `create`, `retrieve`, @@ -23,31 +50,11 @@ class EventViewSet(ModelViewSet): serializer.save(created_by=self.request.user) -class PlaceViewSet(ModelViewSet): - """ - This viewset automatically provides `list`, `create`, `retrieve`, - `update` and `destroy` actions. - - """ +class PlaceViewSet(EventSpecificViewSet): queryset = Place.objects.all() serializer_class = PlaceSerializer -class PlaceEventViewSet(ModelViewSet): - queryset = Place.objects.all() - serializer_class = PlaceSerializer - - def list(self, request, event_pk=None): - queryset = self.queryset.filter(event=event_pk) - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - return self.get_paginated_response(serializer.data) - - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) - - class ActivityTagViewSet(ModelViewSet): """ This viewset automatically provides `list`, `create`, `retrieve`, diff --git a/api/urls.py b/api/urls.py index b7c60cc..9c2b0a4 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,7 +1,7 @@ from django.conf.urls import url, include from rest_framework_nested.routers import SimpleRouter, NestedSimpleRouter from api.event.views import EventViewSet, PlaceViewSet, ActivityTagViewSet,\ - ActivityTemplateViewSet, PlaceEventViewSet + ActivityTemplateViewSet # Create a router and register our viewsets with it. router = SimpleRouter() @@ -12,7 +12,7 @@ router.register(r'activitytemplate', ActivityTemplateViewSet) # Register nested router and register someviewsets vith it event_router = NestedSimpleRouter(router, r'event', lookup='event') -event_router.register(r'place', PlaceEventViewSet, base_name='event-names') +event_router.register(r'place', PlaceViewSet, base_name='event-names') # The API URLs are now determined automatically by the router. From 36f038259b02bec3217ae76e2b72bc157ed73cbf Mon Sep 17 00:00:00 2001 From: Qwann Date: Sat, 22 Jul 2017 02:09:11 +0200 Subject: [PATCH 07/30] generic classes fixed --- api/event/serializers.py | 3 ++- api/event/views.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/api/event/serializers.py b/api/event/serializers.py index db84ebd..b7b9a08 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -24,11 +24,12 @@ class EventSpecificSerializer(serializers.HyperlinkedModelSerializer): def create(self, validated_data): ModelClass = self.Meta.model event_pk = validated_data.pop('event_pk', None) - event = get_object_or_404(Event, id=event_pk) + event = event_pk and get_object_or_404(Event, id=event_pk) or None instance = ModelClass.objects.create(event=event, **validated_data) return instance +# Serializers class EventSerializer(serializers.HyperlinkedModelSerializer): created_by = serializers.ReadOnlyField(source='created_by.username') diff --git a/api/event/views.py b/api/event/views.py index 8060219..81a85c9 100644 --- a/api/event/views.py +++ b/api/event/views.py @@ -37,6 +37,7 @@ class EventSpecificViewSet(ModelViewSet): serializer.save(event_pk=event_pk) +# ViewSets class EventViewSet(ModelViewSet): """ This viewset automatically provides `list`, `create`, `retrieve`, From cd1ed08ca6862218665588602064ee5d91ed8ac0 Mon Sep 17 00:00:00 2001 From: Qwann Date: Sat, 22 Jul 2017 02:09:31 +0200 Subject: [PATCH 08/30] tag added to API --- api/event/serializers.py | 6 ++++-- api/event/views.py | 7 +------ api/urls.py | 1 + 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/api/event/serializers.py b/api/event/serializers.py index b7b9a08..741c01e 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -48,12 +48,14 @@ class PlaceSerializer(EventSpecificSerializer): fields = ('url', 'id', 'name', 'description', 'event') + # TODO rajouter des permissions -class ActivityTagSerializer(serializers.HyperlinkedModelSerializer): +class ActivityTagSerializer(EventSpecificSerializer): + event = EventSerializer(allow_null=True, read_only=True) class Meta: model = ActivityTag - fields = ('url', 'id', 'name', 'is_public', 'color',) + fields = ('url', 'id', 'name', 'is_public', 'color', 'event') # TODO rajouter des permissions diff --git a/api/event/views.py b/api/event/views.py index 81a85c9..b4a7ff4 100644 --- a/api/event/views.py +++ b/api/event/views.py @@ -56,12 +56,7 @@ class PlaceViewSet(EventSpecificViewSet): serializer_class = PlaceSerializer -class ActivityTagViewSet(ModelViewSet): - """ - This viewset automatically provides `list`, `create`, `retrieve`, - `update` and `destroy` actions. - - """ +class ActivityTagViewSet(EventSpecificViewSet): queryset = ActivityTag.objects.all() serializer_class = ActivityTagSerializer diff --git a/api/urls.py b/api/urls.py index 9c2b0a4..60ae603 100644 --- a/api/urls.py +++ b/api/urls.py @@ -13,6 +13,7 @@ router.register(r'activitytemplate', ActivityTemplateViewSet) # Register nested router and register someviewsets vith it event_router = NestedSimpleRouter(router, r'event', lookup='event') event_router.register(r'place', PlaceViewSet, base_name='event-names') +event_router.register(r'tag', ActivityTagViewSet, base_name='event-names') # The API URLs are now determined automatically by the router. From d7ee270fbf561d2d033b32cbd750179e466d2d41 Mon Sep 17 00:00:00 2001 From: Qwann Date: Sat, 22 Jul 2017 03:12:50 +0200 Subject: [PATCH 09/30] activity template serializer added --- api/event/serializers.py | 20 ++++++++++++-------- api/event/views.py | 4 ++++ api/urls.py | 3 ++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/api/event/serializers.py b/api/event/serializers.py index 741c01e..dce4c5b 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -4,7 +4,7 @@ from event.models import Event, ActivityTag, Place, ActivityTemplate # Classes utilitaires -class EventSpecificSerializer(serializers.HyperlinkedModelSerializer): +class EventSpecificSerializer(serializers.ModelSerializer): """ Provide `update` and `create` methods for nested view with an Event For example for Models which exetends EventSpecificMixin @@ -48,7 +48,6 @@ class PlaceSerializer(EventSpecificSerializer): fields = ('url', 'id', 'name', 'description', 'event') - # TODO rajouter des permissions class ActivityTagSerializer(EventSpecificSerializer): event = EventSerializer(allow_null=True, read_only=True) @@ -59,29 +58,34 @@ class ActivityTagSerializer(EventSpecificSerializer): # TODO rajouter des permissions -class ActivityTemplateSerializer(serializers.HyperlinkedModelSerializer): - event = serializers.ReadOnlyField(source='event.title') +class ActivityTemplateSerializer(serializers.ModelSerializer): + event = EventSerializer(read_only=True) tags = ActivityTagSerializer(many=True) class Meta: model = ActivityTemplate - fields = ('url', 'id', 'title', 'event', 'is_public', 'has_perm', + fields = ('id', 'title', 'event', 'is_public', 'has_perm', 'min_perm', 'max_perm', 'description', 'remarks', 'tags',) def update(self, instance, validated_data): tags_data = validated_data.pop('tags') + validated_data.pop('event_pk') + event = instance.event [setattr(instance, key, value) for key, value in validated_data.items()] instance.save() - tags = [ActivityTag.objects.get_or_create(**tag_data)[0] + tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] for tag_data in tags_data] instance.tags = tags return instance def create(self, validated_data): tags_data = validated_data.pop('tags') - activity_template = ActivityTemplate.objects.create(**validated_data) - tags = [ActivityTag.objects.get_or_create(**tag_data)[0] + event_pk = validated_data.pop('event_pk') + event = event_pk and get_object_or_404(Event, id=event_pk) or None + activity_template = ActivityTemplate.objects.create(event=event, + **validated_data) + tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] for tag_data in tags_data] activity_template.tags = tags return activity_template diff --git a/api/event/views.py b/api/event/views.py index b4a7ff4..4844e39 100644 --- a/api/event/views.py +++ b/api/event/views.py @@ -69,3 +69,7 @@ class ActivityTemplateViewSet(ModelViewSet): """ queryset = ActivityTemplate.objects.all() serializer_class = ActivityTemplateSerializer + + def perform_create(self, serializer): + event_pk = self.kwargs.get('event_pk') + serializer.save(event_pk=event_pk) diff --git a/api/urls.py b/api/urls.py index 60ae603..5065d7e 100644 --- a/api/urls.py +++ b/api/urls.py @@ -8,12 +8,13 @@ router = SimpleRouter() router.register(r'event', EventViewSet) router.register(r'place', PlaceViewSet) router.register(r'tag', ActivityTagViewSet) -router.register(r'activitytemplate', ActivityTemplateViewSet) # Register nested router and register someviewsets vith it event_router = NestedSimpleRouter(router, r'event', lookup='event') event_router.register(r'place', PlaceViewSet, base_name='event-names') event_router.register(r'tag', ActivityTagViewSet, base_name='event-names') +event_router.register(r'activitytemplate', ActivityTemplateViewSet, + base_name='event-names') # The API URLs are now determined automatically by the router. From ac6b8058f47e85a0e038867598a36cb0bf880481 Mon Sep 17 00:00:00 2001 From: Qwann Date: Sun, 23 Jul 2017 16:06:52 +0200 Subject: [PATCH 10/30] test started --- api/test_event.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ api/urls.py | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 api/test_event.py diff --git a/api/test_event.py b/api/test_event.py new file mode 100644 index 0000000..5466af2 --- /dev/null +++ b/api/test_event.py @@ -0,0 +1,46 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APIRequestFactory, APITestCase,\ + force_authenticate, APIClient +from rest_framework import status +from event.models import Event + +User = get_user_model() + + +class EventTest(APITestCase): + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create( + username='enarmanli', + email='erkan.narmanli@ens.fr', + first_name='Erkan', + last_name='Narmanli', + ) + cls.event_data = { + "title": "test event", + "slug": "test-event", + "description": "C'est trop cool !", + "beginning_date": "2017-07-18T18:05:00Z", + "ending_date": "2017-07-19T18:05:00Z", + } + + def test_anonymous_create(self): + """ + ensure anonymous can't create a new event object using API + """ + url = reverse('event-list') + response = self.client.post(url, self.event_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_user_create(self): + """ + ensure we can create a new event object using API + """ + url = reverse('event-list') + self.client.force_authenticate(user=self.user) + response = self.client.post(url, self.event_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Event.objects.count(), 1) + self.assertEqual(Event.objects.get().title, 'test event') diff --git a/api/urls.py b/api/urls.py index 5065d7e..61d7957 100644 --- a/api/urls.py +++ b/api/urls.py @@ -5,7 +5,7 @@ from api.event.views import EventViewSet, PlaceViewSet, ActivityTagViewSet,\ # Create a router and register our viewsets with it. router = SimpleRouter() -router.register(r'event', EventViewSet) +router.register(r'event', EventViewSet, 'event') router.register(r'place', PlaceViewSet) router.register(r'tag', ActivityTagViewSet) From 7585a8246ddb01598e5dc55b1f3d516eee8eca5b Mon Sep 17 00:00:00 2001 From: Qwann Date: Sun, 23 Jul 2017 18:23:02 +0200 Subject: [PATCH 11/30] url names + API default permissions --- api/urls.py | 10 +++++----- evenementiel/settings/common.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/urls.py b/api/urls.py index 61d7957..0409143 100644 --- a/api/urls.py +++ b/api/urls.py @@ -6,15 +6,15 @@ from api.event.views import EventViewSet, PlaceViewSet, ActivityTagViewSet,\ # Create a router and register our viewsets with it. router = SimpleRouter() router.register(r'event', EventViewSet, 'event') -router.register(r'place', PlaceViewSet) -router.register(r'tag', ActivityTagViewSet) +router.register(r'place', PlaceViewSet, 'place') +router.register(r'activitytag', ActivityTagViewSet, 'activitytag') # Register nested router and register someviewsets vith it event_router = NestedSimpleRouter(router, r'event', lookup='event') -event_router.register(r'place', PlaceViewSet, base_name='event-names') -event_router.register(r'tag', ActivityTagViewSet, base_name='event-names') +event_router.register(r'place', PlaceViewSet, base_name='event-place') +event_router.register(r'tag', ActivityTagViewSet, base_name='event-activitytag') event_router.register(r'activitytemplate', ActivityTemplateViewSet, - base_name='event-names') + base_name='event-activitytemplate') # The API URLs are now determined automatically by the router. diff --git a/evenementiel/settings/common.py b/evenementiel/settings/common.py index fea0ba5..b4c68fa 100644 --- a/evenementiel/settings/common.py +++ b/evenementiel/settings/common.py @@ -75,7 +75,7 @@ MIDDLEWARE_CLASSES = [ REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAdminUser', + 'rest_framework.permissions.AllowAny', ], 'PAGE_SIZE': 10 } From a4da5d94c4479acf01b394503600e1205795cdf1 Mon Sep 17 00:00:00 2001 From: Qwann Date: Sun, 23 Jul 2017 18:23:25 +0200 Subject: [PATCH 12/30] eventspecific tests --- api/test_event.py | 94 +++++++++++++++++++++++++++++---- evenementiel/settings/common.py | 3 +- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/api/test_event.py b/api/test_event.py index 5466af2..43ddb5e 100644 --- a/api/test_event.py +++ b/api/test_event.py @@ -1,10 +1,13 @@ +from datetime import timedelta + from django.contrib.auth import get_user_model -from django.test import TestCase +from django.utils import timezone from django.urls import reverse -from rest_framework.test import APIRequestFactory, APITestCase,\ - force_authenticate, APIClient + +from rest_framework.test import APIRequestFactory, APITestCase from rest_framework import status -from event.models import Event + +from event.models import Event, Place, ActivityTag, ActivityTemplate User = get_user_model() @@ -26,13 +29,13 @@ class EventTest(APITestCase): "ending_date": "2017-07-19T18:05:00Z", } - def test_anonymous_create(self): - """ - ensure anonymous can't create a new event object using API - """ - url = reverse('event-list') - response = self.client.post(url, self.event_data, format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + # def test_anonymous_create(self): + # """ + # ensure anonymous can't create a new event object using API + # """ + # url = reverse('event-list') + # response = self.client.post(url, self.event_data, format='json') + # self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_user_create(self): """ @@ -44,3 +47,72 @@ class EventTest(APITestCase): self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Event.objects.count(), 1) self.assertEqual(Event.objects.get().title, 'test event') + + +class EventSpecificTest(APITestCase): + """ + Tests is the EventSpecifics querysets are rendered correctly + using the API + """ + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user(username='user', password='pass', ) + cls.event = Event.objects.create( + title='La Nuit 2042', + slug='nuit42', + created_by=cls.user, + creation_date=timezone.now(), + description="La nuit c'est lol", + beginning_date=timezone.now() + + timedelta(days=30), + ending_date=timezone.now() + + timedelta(days=31), + ) + cls.factory = APIRequestFactory() + + def test_place(self): + Place.objects.create(name="root-place", event=None) + Place.objects.create(name="event-place", event=self.event) + + self.client.force_authenticate(user=self.user) + + url = reverse('place-list') + response = self.client.get(url, format='json') + self.assertEqual(response.json()['count'], 1) + + event_id = self.event.id + url = reverse('event-place-list', kwargs={'event_pk': event_id}) + response = self.client.get(url, format='json') + self.assertEqual(response.json()['count'], 2) + + def test_tag(self): + ActivityTag.objects.create(name="root-tag", + is_public=True, + color="#000", + event=None) + ActivityTag.objects.create(name="event-tag", + is_public=True, + color="#FFF", + event=self.event) + + self.client.force_authenticate(user=self.user) + + url = reverse('activitytag-list') + response = self.client.get(url, format='json') + self.assertEqual(response.json()['count'], 1) + + event_id = self.event.id + url = reverse('event-activitytag-list', kwargs={'event_pk': event_id}) + response = self.client.get(url, format='json') + self.assertEqual(response.json()['count'], 2) + + def test_activitytemplate(self): + ActivityTemplate.objects.create(title="test", event=self.event) + + self.client.force_authenticate(user=self.user) + + event_id = self.event.id + url = reverse('event-activitytemplate-list', + kwargs={'event_pk': event_id}) + response = self.client.get(url, format='json') + self.assertEqual(response.json()['count'], 1) diff --git a/evenementiel/settings/common.py b/evenementiel/settings/common.py index b4c68fa..6d41864 100644 --- a/evenementiel/settings/common.py +++ b/evenementiel/settings/common.py @@ -77,7 +77,8 @@ REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.AllowAny', ], - 'PAGE_SIZE': 10 + 'PAGE_SIZE': 10, + 'TEST_REQUEST_DEFAULT_FORMAT': 'json', } ROOT_URLCONF = 'evenementiel.urls' From 3246552ebad4f537f36ad31bcd6e82e1eb8be9b4 Mon Sep 17 00:00:00 2001 From: Qwann Date: Tue, 25 Jul 2017 14:02:57 +0200 Subject: [PATCH 13/30] tests factorised --- api/equipment/serializers.py | 7 + api/event/serializers.py | 4 + api/event/views.py | 30 +++- api/test_event.py | 140 +++++------------ api/test_mixins.py | 256 ++++++++++++++++++++++++++++++++ evenementiel/settings/common.py | 1 - 6 files changed, 334 insertions(+), 104 deletions(-) create mode 100644 api/equipment/serializers.py create mode 100644 api/test_mixins.py diff --git a/api/equipment/serializers.py b/api/equipment/serializers.py new file mode 100644 index 0000000..dbecee9 --- /dev/null +++ b/api/equipment/serializers.py @@ -0,0 +1,7 @@ +from equipment.models import Equipment +from api.event.serializers import EventSpecificSerializer + + +class EquipmentSerializer(EventSpecificSerializer): + pass + diff --git a/api/event/serializers.py b/api/event/serializers.py index dce4c5b..e898d8d 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -89,3 +89,7 @@ class ActivityTemplateSerializer(serializers.ModelSerializer): for tag_data in tags_data] activity_template.tags = tags return activity_template + + +class ActivitySerializer(serializers.ModelSerializer): + pass diff --git a/api/event/views.py b/api/event/views.py index 4844e39..7add4bf 100644 --- a/api/event/views.py +++ b/api/event/views.py @@ -4,8 +4,8 @@ from django.db.models import Q from rest_framework.viewsets import ModelViewSet from api.event.serializers import EventSerializer, PlaceSerializer,\ - ActivityTagSerializer, ActivityTemplateSerializer -from event.models import Event, Place, ActivityTag, ActivityTemplate + ActivityTagSerializer, ActivityTemplateSerializer, ActivitySerializer +from event.models import Event, Place, ActivityTag, ActivityTemplate, Activity User = get_user_model() @@ -36,6 +36,10 @@ class EventSpecificViewSet(ModelViewSet): event_pk = self.kwargs.get('event_pk') serializer.save(event_pk=event_pk) + def perform_update(self, serializer): + event_pk = self.kwargs.get('event_pk') + serializer.save(event_pk=event_pk) + # ViewSets class EventViewSet(ModelViewSet): @@ -73,3 +77,25 @@ class ActivityTemplateViewSet(ModelViewSet): def perform_create(self, serializer): event_pk = self.kwargs.get('event_pk') serializer.save(event_pk=event_pk) + + def perform_update(self, serializer): + event_pk = self.kwargs.get('event_pk') + serializer.save(event_pk=event_pk) + + +class ActivityViewSet(ModelViewSet): + """ + This viewset automatically provides `list`, `create`, `retrieve`, + `update` and `destroy` actions. + + """ + queryset = Activity.objects.all() + serializer_class = ActivitySerializer + + def perform_create(self, serializer): + event_pk = self.kwargs.get('event_pk') + serializer.save(event_pk=event_pk) + + def perform_update(self, serializer): + event_pk = self.kwargs.get('event_pk') + serializer.save(event_pk=event_pk) diff --git a/api/test_event.py b/api/test_event.py index 43ddb5e..6c2b118 100644 --- a/api/test_event.py +++ b/api/test_event.py @@ -1,118 +1,56 @@ -from datetime import timedelta - from django.contrib.auth import get_user_model -from django.utils import timezone -from django.urls import reverse -from rest_framework.test import APIRequestFactory, APITestCase -from rest_framework import status +from rest_framework.test import APITestCase from event.models import Event, Place, ActivityTag, ActivityTemplate +from api.event.serializers import ActivityTemplateSerializer, EventSerializer +from api.test_mixins import EventBasedModelMixin, EventSpecificMixin,\ + ModelTestMixin + User = get_user_model() -class EventTest(APITestCase): - @classmethod - def setUpTestData(cls): - cls.user = User.objects.create( - username='enarmanli', - email='erkan.narmanli@ens.fr', - first_name='Erkan', - last_name='Narmanli', - ) - cls.event_data = { - "title": "test event", - "slug": "test-event", - "description": "C'est trop cool !", - "beginning_date": "2017-07-18T18:05:00Z", - "ending_date": "2017-07-19T18:05:00Z", - } - - # def test_anonymous_create(self): - # """ - # ensure anonymous can't create a new event object using API - # """ - # url = reverse('event-list') - # response = self.client.post(url, self.event_data, format='json') - # self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_user_create(self): - """ - ensure we can create a new event object using API - """ - url = reverse('event-list') - self.client.force_authenticate(user=self.user) - response = self.client.post(url, self.event_data, format='json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Event.objects.count(), 1) - self.assertEqual(Event.objects.get().title, 'test event') +class EventTest(ModelTestMixin, APITestCase): + model = Event + base_name = 'event' + tested_fields = {'title': "I'm a test", } + # Création + data_creation = 'event2_data' + # Update/Delete + instance_name = 'event1' + serializer = EventSerializer -class EventSpecificTest(APITestCase): - """ - Tests is the EventSpecifics querysets are rendered correctly - using the API - """ - @classmethod - def setUpTestData(cls): - cls.user = User.objects.create_user(username='user', password='pass', ) - cls.event = Event.objects.create( - title='La Nuit 2042', - slug='nuit42', - created_by=cls.user, - creation_date=timezone.now(), - description="La nuit c'est lol", - beginning_date=timezone.now() - + timedelta(days=30), - ending_date=timezone.now() - + timedelta(days=31), - ) - cls.factory = APIRequestFactory() +class ActivityTemplateTest(EventBasedModelMixin, APITestCase): + model = ActivityTemplate + base_name = 'event-activitytemplate' + initial_count = 1 + # Creation + data_creation = 'act_temp2_data' + # Update/Delete + instance_name = 'act_temp1' + field_tested = 'title' + serializer = ActivityTemplateSerializer - def test_place(self): - Place.objects.create(name="root-place", event=None) - Place.objects.create(name="event-place", event=self.event) + def test_create_extra(self): + self.assertEqual(self.model.objects.get(id=1).tags.count(), 1) - self.client.force_authenticate(user=self.user) + def pre_update_extra(self, data): + data['tags'].append(self.tag2_data) + return data - url = reverse('place-list') - response = self.client.get(url, format='json') - self.assertEqual(response.json()['count'], 1) + def post_update_extra(self, instance): + self.assertEqual(instance.tags.count(), 2) - event_id = self.event.id - url = reverse('event-place-list', kwargs={'event_pk': event_id}) - response = self.client.get(url, format='json') - self.assertEqual(response.json()['count'], 2) - def test_tag(self): - ActivityTag.objects.create(name="root-tag", - is_public=True, - color="#000", - event=None) - ActivityTag.objects.create(name="event-tag", - is_public=True, - color="#FFF", - event=self.event) +class EventSpecficTagTest(EventSpecificMixin, APITestCase): + model = ActivityTag + root_base_name = 'activitytag' + event_base_name = 'event-activitytag' - self.client.force_authenticate(user=self.user) - url = reverse('activitytag-list') - response = self.client.get(url, format='json') - self.assertEqual(response.json()['count'], 1) - - event_id = self.event.id - url = reverse('event-activitytag-list', kwargs={'event_pk': event_id}) - response = self.client.get(url, format='json') - self.assertEqual(response.json()['count'], 2) - - def test_activitytemplate(self): - ActivityTemplate.objects.create(title="test", event=self.event) - - self.client.force_authenticate(user=self.user) - - event_id = self.event.id - url = reverse('event-activitytemplate-list', - kwargs={'event_pk': event_id}) - response = self.client.get(url, format='json') - self.assertEqual(response.json()['count'], 1) +class EventSpecficPlaceTest(EventSpecificMixin, APITestCase): + model = Place + root_base_name = 'place' + event_base_name = 'event-place' diff --git a/api/test_mixins.py b/api/test_mixins.py new file mode 100644 index 0000000..094676d --- /dev/null +++ b/api/test_mixins.py @@ -0,0 +1,256 @@ +from datetime import timedelta +from django.contrib.auth import get_user_model +from django.db.models import Q +from django.utils import timezone +from django.urls import reverse + +from rest_framework.test import APIRequestFactory +from rest_framework import status + +from event.models import Event, Place, ActivityTag, ActivityTemplate + + +User = get_user_model() + + +class DataBaseMixin(object): + @classmethod + def setUpTestData(cls): + # Users + cls.user1_data = {'username': "user1", 'password': "pass1"} + cls.user1 = User.objects.create_user(**cls.user1_data) + + def setUp(self): + # Events + self.event1_data = {'title': "event1", 'slug': "slug1", + 'beginning_date': timezone.now() + + timedelta(days=30), + "description": "C'est trop cool !", + 'ending_date': timezone.now()+timedelta(days=31), + 'created_by': self.user1, } + self.event2_data = {"title": "test event", "slug": "test-event", + "description": "C'est trop cool !", + "beginning_date": "2017-07-18T18:05:00Z", + "ending_date": "2017-07-19T18:05:00Z", } + self.event1 = Event.objects.create(**self.event1_data) + + # ActivityTags + self.tag1_data = {"name": "tag1", "is_public": False, "color": "#111"} + self.tag2_data = {"name": "tag2", "is_public": False, "color": "#222"} + self.tag3_data = {"name": "tag3", "is_public": False, "color": "#333"} + self.tag1 = ActivityTag.objects.create(**self.tag1_data) + + self.act_temp1_data = {'title': "act temp1", 'is_public': True, + 'remarks': "test remark", 'event': self.event1} + self.act_temp2_data = {'title': "act temp2", 'is_public': False, + 'remarks': "test remark", + 'tags': [self.tag2_data, ]} + self.act_temp1 = ActivityTemplate.objects.create(**self.act_temp1_data) + self.act_temp1.tags.add(self.tag1) + + +class EventBasedModelMixin(DataBaseMixin): + """ + Note : need to define : `model`, `base_name`, `initial_count`, + `data_creation`, `instance_name`, `field_tested`, `serializer` + """ + def test_user_create_extra(self): + pass + + def pre_update_extra(self, data): + return data + + def post_update_extra(self): + pass + + def test_user_create(self): + """ + ensure we can create a new %model object using API + """ + data = getattr(self, self.data_creation) + + event_id = self.event1.id + url = reverse('{base_name}-list'.format(base_name=self.base_name), + kwargs={'event_pk': event_id}) + self.client.force_authenticate(user=self.user1) + response = self.client.post(url, data, + format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(self.model.objects.count(), self.initial_count + 1) + self.assertEqual(self.model.objects.get(id=self.initial_count+1).event, + self.event1) + + self.test_create_extra() + + def test_user_update(self): + """ + ensure we can update an %model object using API + """ + instance = getattr(self, self.instance_name) + factory = APIRequestFactory() + + instance_id = instance.id + event_id = self.event1.id + url = reverse('{base_name}-list'.format(base_name=self.base_name), + kwargs={'event_pk': event_id}) + url = "%s%d/" % (url, instance_id) + + request = factory.get(url) + data = self.serializer(instance, context={'request': request}).data + + newvalue = "I'm a test" + data[self.field_tested] = newvalue + + data = self.pre_update_extra(data) + + self.client.force_authenticate(user=self.user1) + response = self.client.patch(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + instance.refresh_from_db() + self.assertEqual(getattr(instance, self.field_tested), newvalue) + + self.post_update_extra(instance) + + def test_user_delete(self): + """ + ensure we can update an %model object using API + """ + instance = getattr(self, self.instance_name) + + instance_id = instance.id + event_id = self.event1.id + url = reverse('{base_name}-list'.format(base_name=self.base_name), + kwargs={'event_pk': event_id}) + url = "%s%d/" % (url, instance_id) + + self.client.force_authenticate(user=self.user1) + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(self.model.objects.count(), self.initial_count - 1) + + +# TODO rajouter la gestion des permissions dans le Mixin +# TODO rajouter un test pour s'assurer que les personnes non +# connectées ne peuvent pas create/update/delete +class EventSpecificMixin(object): + """ + Tests is the EventSpecifics querysets are rendered correctly + using the API + Note : need to define : `model`, `root_base_name` and `event_base_name` + """ + @classmethod + def setUpTestData(cls): + # Users + cls.user1_data = {'username': "user1", 'password': "pass1"} + cls.user1 = User.objects.create_user(**cls.user1_data) + # Events + cls.event1_data = {'title': "event1", 'slug': "slug1", + 'beginning_date': timezone.now() + + timedelta(days=30), + 'ending_date': timezone.now()+timedelta(days=31), + 'created_by': cls.user1, } + cls.event1 = Event.objects.create(**cls.event1_data) + + def setUp(self): + # Tag + self.tag_root_data = {"name": "tag2", "is_public": False, + "color": "#222"} + self.tag_event_data = {"name": "tag3", "is_public": False, + "color": "#333", 'event': self.event1} + self.tag_root = ActivityTag.objects.create(**self.tag_root_data) + self.tag_event = ActivityTag.objects.create(**self.tag_event_data) + + # Places + self.place_root_data = {'name': "place1", 'event': None, } + self.place_event_data = {'name': "place2", 'event': self.event1, } + self.place_root = Place.objects.create(**self.place_root_data) + self.place_event = Place.objects.create(**self.place_event_data) + + def test_lists(self): + event_id = self.event1.id + root_count = self.model.objects.filter(event=None).count() + event_count = (self.model.objects + .filter(Q(event=self.event1) | Q(event=None)).count()) + + self.client.force_authenticate(user=self.user1) + + url = reverse('{base}-list'.format(base=self.root_base_name)) + response = self.client.get(url, format='json') + self.assertEqual(response.json()['count'], root_count) + + event_id = self.event1.id + url = reverse('{base}-list'.format(base=self.event_base_name), + kwargs={'event_pk': event_id}) + response = self.client.get(url, format='json') + self.assertEqual(response.json()['count'], event_count) + + +# TODO rajouter la gestion des permissions dans le Mixin +# TODO rajouter un test pour s'assurer que les personnes non +# connectées ne peuvent pas create/update/delete +# TODO? essayer de factoriser avec EventBasedMixin ? +# FIXME not working, peut être que le problème vient +# du fait que les dates sont mal envoyées dans le data ? A voir. +class ModelTestMixin(DataBaseMixin): + """ + Note : need to define : `model`, `base_name`, + `instance_name`, `field_tested`, `serializer` + """ + def test_user_create(self): + """ + ensure we can create a new %model object using API + """ + data = getattr(self, self.data_creation) + initial_count = self.model.objects.count() + + url = reverse('{base_name}-list'.format(base_name=self.base_name)) + self.client.force_authenticate(user=self.user1) + response = self.client.post(url, data) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(self.model.objects.count(), initial_count + 1) + + instance = self.model.objects.get(id=initial_count+1) + for field in self.tested_fields.keys(): + self.assertEqual(getattr(instance, field), data[field]) + + def test_user_update(self): + """ + ensure we can update an %model object using API + """ + instance = getattr(self, self.instance_name) + factory = APIRequestFactory() + + instance_id = instance.id + url = reverse('{base_name}-detail'.format(base_name=self.base_name), + kwargs={'pk': instance_id}) + + request = factory.get(url) + data = self.serializer(instance, context={'request': request}).data + for field, value in self.tested_fields.items(): + data[field] = value + + self.client.force_authenticate(user=self.user1) + response = self.client.put(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + instance.refresh_from_db() + for field, value in self.tested_fields.items(): + self.assertEqual(getattr(instance, field), value) + + def test_user_delete(self): + """ + ensure we can update an %model object using API + """ + instance = getattr(self, self.instance_name) + initial_count = self.model.objects.count() + + instance_id = instance.id + url = reverse('{base_name}-detail'.format(base_name=self.base_name), + kwargs={'pk': instance_id}) + + self.client.force_authenticate(user=self.user1) + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(self.model.objects.count(), initial_count - 1) diff --git a/evenementiel/settings/common.py b/evenementiel/settings/common.py index 6d41864..8baa801 100644 --- a/evenementiel/settings/common.py +++ b/evenementiel/settings/common.py @@ -78,7 +78,6 @@ REST_FRAMEWORK = { 'rest_framework.permissions.AllowAny', ], 'PAGE_SIZE': 10, - 'TEST_REQUEST_DEFAULT_FORMAT': 'json', } ROOT_URLCONF = 'evenementiel.urls' From 771fdf878e77611bcc74e0b7a91fb9c20e8d5f63 Mon Sep 17 00:00:00 2001 From: Qwann Date: Tue, 25 Jul 2017 14:40:34 +0200 Subject: [PATCH 14/30] renamed users api sub application --- api/{user => users}/serializers.py | 0 api/{user => users}/views.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename api/{user => users}/serializers.py (100%) rename api/{user => users}/views.py (100%) diff --git a/api/user/serializers.py b/api/users/serializers.py similarity index 100% rename from api/user/serializers.py rename to api/users/serializers.py diff --git a/api/user/views.py b/api/users/views.py similarity index 100% rename from api/user/views.py rename to api/users/views.py From d19e5978b6ba6e46b8f0cacc5f98c7ff26d70bc8 Mon Sep 17 00:00:00 2001 From: Qwann Date: Tue, 25 Jul 2017 18:47:50 +0200 Subject: [PATCH 15/30] EventSpecificSerializer provides event attribute --- api/event/serializers.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/api/event/serializers.py b/api/event/serializers.py index e898d8d..183c39d 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -3,14 +3,28 @@ from rest_framework import serializers from event.models import Event, ActivityTag, Place, ActivityTemplate +# Event Serializer +class EventSerializer(serializers.HyperlinkedModelSerializer): + created_by = serializers.ReadOnlyField(source='created_by.username') + + class Meta: + model = Event + fields = ('url', 'id', 'title', 'slug', 'created_by', 'creation_date', + 'description', 'beginning_date', 'ending_date') + + # Classes utilitaires class EventSpecificSerializer(serializers.ModelSerializer): """ Provide `update` and `create` methods for nested view with an Event - For example for Models which exetends EventSpecificMixin + For example for Models which extends EventSpecificMixin the event id has to be provided in the `save` method Works fine with view.EventSpecificViewSet + Also provides : + event = eventserializer(allow_null=true, read_only=true) """ + event = EventSerializer(allow_null=True, read_only=True) + def update(self, instance, validated_data): """ Note : does NOT change the event value of the instance @@ -30,19 +44,8 @@ class EventSpecificSerializer(serializers.ModelSerializer): # Serializers -class EventSerializer(serializers.HyperlinkedModelSerializer): - created_by = serializers.ReadOnlyField(source='created_by.username') - - class Meta: - model = Event - fields = ('url', 'id', 'title', 'slug', 'created_by', 'creation_date', - 'description', 'beginning_date', 'ending_date') - - # TODO rajouter des permissions class PlaceSerializer(EventSpecificSerializer): - event = EventSerializer(allow_null=True, read_only=True) - class Meta: model = Place fields = ('url', 'id', 'name', 'description', 'event') @@ -50,8 +53,6 @@ class PlaceSerializer(EventSpecificSerializer): # TODO rajouter des permissions class ActivityTagSerializer(EventSpecificSerializer): - event = EventSerializer(allow_null=True, read_only=True) - class Meta: model = ActivityTag fields = ('url', 'id', 'name', 'is_public', 'color', 'event') From be5a90cf6739092335e2ef9fc6ebbd616a0eb7c4 Mon Sep 17 00:00:00 2001 From: Qwann Date: Tue, 25 Jul 2017 19:00:12 +0200 Subject: [PATCH 16/30] EventSerializer field fixes --- api/event/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/event/serializers.py b/api/event/serializers.py index 183c39d..8cc80cf 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -5,7 +5,8 @@ from event.models import Event, ActivityTag, Place, ActivityTemplate # Event Serializer class EventSerializer(serializers.HyperlinkedModelSerializer): - created_by = serializers.ReadOnlyField(source='created_by.username') + created_by = serializers.ReadOnlyField(source='created_by.get_full_name') + creation_date = serializers.ReadOnlyField() class Meta: model = Event From fdce944820de73320c0b4a2e38fca5f3df869a86 Mon Sep 17 00:00:00 2001 From: Qwann Date: Wed, 26 Jul 2017 13:11:58 +0200 Subject: [PATCH 17/30] typos --- api/event/serializers.py | 2 +- api/event/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/event/serializers.py b/api/event/serializers.py index 8cc80cf..a18794f 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -78,7 +78,7 @@ class ActivityTemplateSerializer(serializers.ModelSerializer): instance.save() tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] for tag_data in tags_data] - instance.tags = tags + instance.tags.set(tags) return instance def create(self, validated_data): diff --git a/api/event/views.py b/api/event/views.py index 7add4bf..b360583 100644 --- a/api/event/views.py +++ b/api/event/views.py @@ -15,11 +15,11 @@ class EventSpecificViewSet(ModelViewSet): """ ViewSet that returns : * rootlevel objects if no Event is specified - * OR objects related to the specidied event + * OR objects related to the specified event AND root level objects if an event is specified it passes the event_pk to the save method. Works fine with serializers.EventSpecificSerializer - Useful for models that exetends EventSpecificMixin + Useful for models that extends EventSpecificMixin """ def get_queryset(self): """ From 814199da71d87aa3968f8d3db0c9f1bb9da97623 Mon Sep 17 00:00:00 2001 From: Qwann Date: Wed, 26 Jul 2017 14:51:29 +0200 Subject: [PATCH 18/30] docstring + rename some mixins --- api/test_event.py | 8 ++++---- api/test_mixins.py | 46 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/api/test_event.py b/api/test_event.py index 6c2b118..62d77d5 100644 --- a/api/test_event.py +++ b/api/test_event.py @@ -5,7 +5,7 @@ from rest_framework.test import APITestCase from event.models import Event, Place, ActivityTag, ActivityTemplate from api.event.serializers import ActivityTemplateSerializer, EventSerializer -from api.test_mixins import EventBasedModelMixin, EventSpecificMixin,\ +from api.test_mixins import EventBasedModelTestMixin, EventSpecificTestMixin,\ ModelTestMixin User = get_user_model() @@ -22,7 +22,7 @@ class EventTest(ModelTestMixin, APITestCase): serializer = EventSerializer -class ActivityTemplateTest(EventBasedModelMixin, APITestCase): +class ActivityTemplateTest(EventBasedModelTestMixin, APITestCase): model = ActivityTemplate base_name = 'event-activitytemplate' initial_count = 1 @@ -44,13 +44,13 @@ class ActivityTemplateTest(EventBasedModelMixin, APITestCase): self.assertEqual(instance.tags.count(), 2) -class EventSpecficTagTest(EventSpecificMixin, APITestCase): +class EventSpecficTagTest(EventSpecificTestMixin, APITestCase): model = ActivityTag root_base_name = 'activitytag' event_base_name = 'event-activitytag' -class EventSpecficPlaceTest(EventSpecificMixin, APITestCase): +class EventSpecficPlaceTest(EventSpecificTestMixin, APITestCase): model = Place root_base_name = 'place' event_base_name = 'event-place' diff --git a/api/test_mixins.py b/api/test_mixins.py index 094676d..9f524d7 100644 --- a/api/test_mixins.py +++ b/api/test_mixins.py @@ -14,6 +14,9 @@ User = get_user_model() class DataBaseMixin(object): + """ + provides a datatabse for API tests + """ @classmethod def setUpTestData(cls): # Users @@ -49,23 +52,35 @@ class DataBaseMixin(object): self.act_temp1.tags.add(self.tag1) -class EventBasedModelMixin(DataBaseMixin): +class EventBasedModelTestMixin(DataBaseMixin): """ Note : need to define : `model`, `base_name`, `initial_count`, `data_creation`, `instance_name`, `field_tested`, `serializer` + + tests for models served by the API that are related to an event + and whose API urls are nested under ../event//%model """ - def test_user_create_extra(self): + def user_create_extra(self): + """ + extra test in creation by a permited user + """ pass def pre_update_extra(self, data): + """ + extra modification for the data sent for update + """ return data def post_update_extra(self): + """ + extra test for updated model + """ pass def test_user_create(self): """ - ensure we can create a new %model object using API + ensure a permited user can create a new %model object using API """ data = getattr(self, self.data_creation) @@ -80,11 +95,11 @@ class EventBasedModelMixin(DataBaseMixin): self.assertEqual(self.model.objects.get(id=self.initial_count+1).event, self.event1) - self.test_create_extra() + self.user_create_extra() def test_user_update(self): """ - ensure we can update an %model object using API + ensure a permited user can update a new %model object using API """ instance = getattr(self, self.instance_name) factory = APIRequestFactory() @@ -114,7 +129,7 @@ class EventBasedModelMixin(DataBaseMixin): def test_user_delete(self): """ - ensure we can update an %model object using API + ensure a permited user can delete a new %model object using API """ instance = getattr(self, self.instance_name) @@ -133,11 +148,13 @@ class EventBasedModelMixin(DataBaseMixin): # TODO rajouter la gestion des permissions dans le Mixin # TODO rajouter un test pour s'assurer que les personnes non # connectées ne peuvent pas create/update/delete -class EventSpecificMixin(object): +class EventSpecificTestMixin(object): """ Tests is the EventSpecifics querysets are rendered correctly using the API Note : need to define : `model`, `root_base_name` and `event_base_name` + + tests for models served by the API that inherit EventSpecificMixin """ @classmethod def setUpTestData(cls): @@ -168,6 +185,12 @@ class EventSpecificMixin(object): self.place_event = Place.objects.create(**self.place_event_data) def test_lists(self): + """ + ensure that only root-level models are served under + api/%root_base_name/ + and that root-level and event-level models are served under + api/event//%event_base_name/ + """ event_id = self.event1.id root_count = self.model.objects.filter(event=None).count() event_count = (self.model.objects @@ -196,10 +219,13 @@ class ModelTestMixin(DataBaseMixin): """ Note : need to define : `model`, `base_name`, `instance_name`, `field_tested`, `serializer` + + generic mixin for testing creation/update/delete + of models served by the API """ def test_user_create(self): """ - ensure we can create a new %model object using API + ensure a permited user can create a new %model object using API """ data = getattr(self, self.data_creation) initial_count = self.model.objects.count() @@ -217,7 +243,7 @@ class ModelTestMixin(DataBaseMixin): def test_user_update(self): """ - ensure we can update an %model object using API + ensure a permited user can update a new %model object using API """ instance = getattr(self, self.instance_name) factory = APIRequestFactory() @@ -241,7 +267,7 @@ class ModelTestMixin(DataBaseMixin): def test_user_delete(self): """ - ensure we can update an %model object using API + ensure a permited user can delete a new %model object using API """ instance = getattr(self, self.instance_name) initial_count = self.model.objects.count() From 9860a19f2d9d5027a4a1448f44687a03e8224ae7 Mon Sep 17 00:00:00 2001 From: Qwann Date: Wed, 26 Jul 2017 15:41:20 +0200 Subject: [PATCH 19/30] doc for future comportement --- api/event/serializers.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/api/event/serializers.py b/api/event/serializers.py index a18794f..cd7ba7a 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -70,23 +70,41 @@ class ActivityTemplateSerializer(serializers.ModelSerializer): 'min_perm', 'max_perm', 'description', 'remarks', 'tags',) def update(self, instance, validated_data): + """ + @tags comportement attendu : si l'id existe déjà on ne change pas + les autres champs et si l'id n'existe pas on le créé + """ tags_data = validated_data.pop('tags') validated_data.pop('event_pk') event = instance.event [setattr(instance, key, value) for key, value in validated_data.items()] instance.save() + # TODO: en fonction de si backbone envoie un `id` ou non lorsque le tag + # n'existe pas encore il faudra faire un premier passage sur `tags` i + # pour s'assurer que le get ne foire pas le get si, par exemple, le tag + # été modifié entre temps dans la base de donnée (mais pas sur la + # classe backbone tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] for tag_data in tags_data] instance.tags.set(tags) return instance def create(self, validated_data): + """ + @tags comportement attendu : si l'id existe déjà on ne change pas + les autres champs et si l'id n'existe pas on le créé + """ tags_data = validated_data.pop('tags') event_pk = validated_data.pop('event_pk') event = event_pk and get_object_or_404(Event, id=event_pk) or None activity_template = ActivityTemplate.objects.create(event=event, **validated_data) + # TODO: en fonction de si backbone envoie un `id` ou non lorsque le tag + # n'existe pas encore il faudra faire un premier passage sur `tags` i + # pour s'assurer que le get ne foire pas le get si, par exemple, le tag + # été modifié entre temps dans la base de donnée (mais pas sur la + # classe backbone tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] for tag_data in tags_data] activity_template.tags = tags From 9362d4c1f06bcbbf44518237fc361ef423ea13fd Mon Sep 17 00:00:00 2001 From: Qwann Date: Thu, 27 Jul 2017 11:50:47 +0200 Subject: [PATCH 20/30] EventSpecificSerializer is now a mixin --- api/event/serializers.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/api/event/serializers.py b/api/event/serializers.py index cd7ba7a..7eb1319 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -15,7 +15,7 @@ class EventSerializer(serializers.HyperlinkedModelSerializer): # Classes utilitaires -class EventSpecificSerializer(serializers.ModelSerializer): +class EventSpecificSerializerMixin(): """ Provide `update` and `create` methods for nested view with an Event For example for Models which extends EventSpecificMixin @@ -46,14 +46,16 @@ class EventSpecificSerializer(serializers.ModelSerializer): # Serializers # TODO rajouter des permissions -class PlaceSerializer(EventSpecificSerializer): +class PlaceSerializer(serializers.ModelSerializer, + EventSpecificSerializerMixin): class Meta: model = Place fields = ('url', 'id', 'name', 'description', 'event') # TODO rajouter des permissions -class ActivityTagSerializer(EventSpecificSerializer): +class ActivityTagSerializer(serializers.ModelSerializer, + EventSpecificSerializerMixin): class Meta: model = ActivityTag fields = ('url', 'id', 'name', 'is_public', 'color', 'event') @@ -85,6 +87,8 @@ class ActivityTemplateSerializer(serializers.ModelSerializer): # pour s'assurer que le get ne foire pas le get si, par exemple, le tag # été modifié entre temps dans la base de donnée (mais pas sur la # classe backbone + for tag_data in tags_data: + tag_data.pop('event', None) tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] for tag_data in tags_data] instance.tags.set(tags) @@ -105,6 +109,8 @@ class ActivityTemplateSerializer(serializers.ModelSerializer): # pour s'assurer que le get ne foire pas le get si, par exemple, le tag # été modifié entre temps dans la base de donnée (mais pas sur la # classe backbone + for tag_data in tags_data: + tag_data.pop('event', None) tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] for tag_data in tags_data] activity_template.tags = tags From 7bec90d7a453910d4260324d224d1cda378da259 Mon Sep 17 00:00:00 2001 From: Qwann Date: Thu, 27 Jul 2017 11:54:15 +0200 Subject: [PATCH 21/30] todo added --- api/equipment/serializers.py | 2 +- api/event/mixins.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 api/event/mixins.py diff --git a/api/equipment/serializers.py b/api/equipment/serializers.py index dbecee9..ef73251 100644 --- a/api/equipment/serializers.py +++ b/api/equipment/serializers.py @@ -2,6 +2,6 @@ from equipment.models import Equipment from api.event.serializers import EventSpecificSerializer +# TODO : le faire class EquipmentSerializer(EventSpecificSerializer): pass - diff --git a/api/event/mixins.py b/api/event/mixins.py deleted file mode 100644 index d9aeaf5..0000000 --- a/api/event/mixins.py +++ /dev/null @@ -1,5 +0,0 @@ -class EventNestedMixin(object): - pass - -class ventSpecificSerializerMixin(object): - pass From c9cdc67b4f23ab1a88a317a314dffb8a54797481 Mon Sep 17 00:00:00 2001 From: Qwann Date: Thu, 27 Jul 2017 12:04:30 +0200 Subject: [PATCH 22/30] creation_date field updated --- api/event/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/event/serializers.py b/api/event/serializers.py index 7eb1319..5e01499 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -6,11 +6,11 @@ from event.models import Event, ActivityTag, Place, ActivityTemplate # Event Serializer class EventSerializer(serializers.HyperlinkedModelSerializer): created_by = serializers.ReadOnlyField(source='created_by.get_full_name') - creation_date = serializers.ReadOnlyField() + created_at = serializers.ReadOnlyField() class Meta: model = Event - fields = ('url', 'id', 'title', 'slug', 'created_by', 'creation_date', + fields = ('url', 'id', 'title', 'slug', 'created_by', 'created_at', 'description', 'beginning_date', 'ending_date') From 541939ea17b9ed34e173cbc76f1f23cff4e30f2d Mon Sep 17 00:00:00 2001 From: Qwann Date: Thu, 27 Jul 2017 15:01:05 +0200 Subject: [PATCH 23/30] things moved or modified --- api/event/serializers.py | 9 +++++---- api/{test_event.py => event/tests.py} | 2 +- api/{test_mixins.py => testcases.py} | 16 +++++++++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) rename api/{test_event.py => event/tests.py} (95%) rename api/{test_mixins.py => testcases.py} (94%) diff --git a/api/event/serializers.py b/api/event/serializers.py index 5e01499..44461e6 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -46,16 +46,17 @@ class EventSpecificSerializerMixin(): # Serializers # TODO rajouter des permissions -class PlaceSerializer(serializers.ModelSerializer, - EventSpecificSerializerMixin): +class PlaceSerializer(EventSpecificSerializerMixin, + serializers.ModelSerializer): + class Meta: model = Place fields = ('url', 'id', 'name', 'description', 'event') # TODO rajouter des permissions -class ActivityTagSerializer(serializers.ModelSerializer, - EventSpecificSerializerMixin): +class ActivityTagSerializer(EventSpecificSerializerMixin, + serializers.ModelSerializer): class Meta: model = ActivityTag fields = ('url', 'id', 'name', 'is_public', 'color', 'event') diff --git a/api/test_event.py b/api/event/tests.py similarity index 95% rename from api/test_event.py rename to api/event/tests.py index 62d77d5..147b185 100644 --- a/api/test_event.py +++ b/api/event/tests.py @@ -5,7 +5,7 @@ from rest_framework.test import APITestCase from event.models import Event, Place, ActivityTag, ActivityTemplate from api.event.serializers import ActivityTemplateSerializer, EventSerializer -from api.test_mixins import EventBasedModelTestMixin, EventSpecificTestMixin,\ +from api.testcases import EventBasedModelTestMixin, EventSpecificTestMixin,\ ModelTestMixin User = get_user_model() diff --git a/api/test_mixins.py b/api/testcases.py similarity index 94% rename from api/test_mixins.py rename to api/testcases.py index 9f524d7..d91f3cb 100644 --- a/api/test_mixins.py +++ b/api/testcases.py @@ -54,8 +54,15 @@ class DataBaseMixin(object): class EventBasedModelTestMixin(DataBaseMixin): """ - Note : need to define : `model`, `base_name`, `initial_count`, - `data_creation`, `instance_name`, `field_tested`, `serializer` + Note : need to define : + `model`: the model served by the API + `base_name`: the base_name used in the URL + `initial_count`: (will disappear) inital count in the db + `data_creation`: name in db used to create new instance + `instance_name`: existing instance name in the db + used for update/delete + `field_tested`: name of field tested in the update test + `serializer`: serialiser used for the API tests for models served by the API that are related to an event and whose API urls are nested under ../event//%model @@ -152,7 +159,10 @@ class EventSpecificTestMixin(object): """ Tests is the EventSpecifics querysets are rendered correctly using the API - Note : need to define : `model`, `root_base_name` and `event_base_name` + Note : need to define : + `model`: the concerned model serve by the API + `root_base_name`: the base_name used in the root-level urls + `event_base_name`: the base_name used in the event-level urls tests for models served by the API that inherit EventSpecificMixin """ From cd4695c27a182121f796723ade322ee97e6910ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 3 Aug 2017 12:01:10 +0200 Subject: [PATCH 24/30] Add some __init__ modules. They are pythonicly not necessary, but Django misses testcases and migrations if they are not present. --- api/__init__.py | 0 api/event/__init__.py | 0 api/users/__init__.py | 0 users/migrations/__init__.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 api/__init__.py create mode 100644 api/event/__init__.py create mode 100644 api/users/__init__.py create mode 100644 users/migrations/__init__.py diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/event/__init__.py b/api/event/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/users/__init__.py b/api/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 From cd78757244bf6f2e28847387622c0ce43f870d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 3 Aug 2017 12:09:03 +0200 Subject: [PATCH 25/30] Clean some models Add/fix some field names, verbose_name, help_text, on_delete (for foreign keys), etc. --- .../migrations/0002_auto_20170802_2323.py | 21 ++ event/migrations/0004_auto_20170802_2323.py | 136 +++++++++++ event/models.py | 224 ++++++++++-------- event/validators.py | 10 + 4 files changed, 287 insertions(+), 104 deletions(-) create mode 100644 equipment/migrations/0002_auto_20170802_2323.py create mode 100644 event/migrations/0004_auto_20170802_2323.py create mode 100644 event/validators.py diff --git a/equipment/migrations/0002_auto_20170802_2323.py b/equipment/migrations/0002_auto_20170802_2323.py new file mode 100644 index 0000000..55a2402 --- /dev/null +++ b/equipment/migrations/0002_auto_20170802_2323.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-02 23:23 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('equipment', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='equipment', + name='event', + field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), + ), + ] diff --git a/event/migrations/0004_auto_20170802_2323.py b/event/migrations/0004_auto_20170802_2323.py new file mode 100644 index 0000000..22c4349 --- /dev/null +++ b/event/migrations/0004_auto_20170802_2323.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-02 23:23 +from __future__ import unicode_literals + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0003_auto_20170726_1116'), + ] + + operations = [ + migrations.RemoveField( + model_name='activity', + name='place', + ), + migrations.RemoveField( + model_name='activitytemplate', + name='place', + ), + migrations.AddField( + model_name='activity', + name='places', + field=models.ManyToManyField(blank=True, to='event.Place', verbose_name='lieux'), + ), + migrations.AddField( + model_name='activitytemplate', + name='places', + field=models.ManyToManyField(blank=True, to='event.Place', verbose_name='lieux'), + ), + migrations.AlterField( + model_name='activity', + name='description', + field=models.TextField(blank=True, help_text="Visible par tout le monde si l'événément est public.", null=True, verbose_name='description'), + ), + migrations.AlterField( + model_name='activity', + name='event', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), + ), + migrations.AlterField( + model_name='activity', + name='has_perm', + field=models.NullBooleanField(verbose_name='inscription de permanents'), + ), + migrations.AlterField( + model_name='activity', + name='is_public', + field=models.NullBooleanField(verbose_name='est public'), + ), + migrations.AlterField( + model_name='activity', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='children', to='event.ActivityTemplate', verbose_name='template'), + ), + migrations.AlterField( + model_name='activity', + name='remarks', + field=models.TextField(blank=True, help_text='Visible uniquement par les organisateurs.', null=True, verbose_name='remarques'), + ), + migrations.AlterField( + model_name='activity', + name='staff', + field=models.ManyToManyField(blank=True, related_name='in_perm_activities', to=settings.AUTH_USER_MODEL, verbose_name='permanents'), + ), + migrations.AlterField( + model_name='activity', + name='tags', + field=models.ManyToManyField(blank=True, to='event.ActivityTag', verbose_name='tags'), + ), + migrations.AlterField( + model_name='activitytag', + name='color', + field=models.CharField(help_text='Rentrer une couleur en hexadécimal (#XXX ou #XXXXXX).', max_length=7, validators=[django.core.validators.RegexValidator(message="La chaîne de caractère rentrée n'est pas une couleur en hexadécimal.", regex='^#(?:[0-9a-fA-F]{3}){1,2}$')], verbose_name='couleur'), + ), + migrations.AlterField( + model_name='activitytag', + name='event', + field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), + ), + migrations.AlterField( + model_name='activitytag', + name='is_public', + field=models.BooleanField(help_text="Sert à faire une distinction dans l'affichage selon que le tag soit destiné au public ou à l'organisation.", verbose_name='est public'), + ), + migrations.AlterField( + model_name='activitytemplate', + name='description', + field=models.TextField(blank=True, help_text="Visible par tout le monde si l'événément est public.", null=True, verbose_name='description'), + ), + migrations.AlterField( + model_name='activitytemplate', + name='event', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), + ), + migrations.AlterField( + model_name='activitytemplate', + name='has_perm', + field=models.NullBooleanField(verbose_name='inscription de permanents'), + ), + migrations.AlterField( + model_name='activitytemplate', + name='is_public', + field=models.NullBooleanField(verbose_name='est public'), + ), + migrations.AlterField( + model_name='activitytemplate', + name='remarks', + field=models.TextField(blank=True, help_text='Visible uniquement par les organisateurs.', null=True, verbose_name='remarques'), + ), + migrations.AlterField( + model_name='activitytemplate', + name='tags', + field=models.ManyToManyField(blank=True, to='event.ActivityTag', verbose_name='tags'), + ), + migrations.AlterField( + model_name='event', + name='created_by', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_events', to=settings.AUTH_USER_MODEL, verbose_name='créé par'), + ), + migrations.AlterField( + model_name='event', + name='slug', + field=models.SlugField(help_text="Seulement des lettres, des chiffres ou les caractères '_' ou '-'.", unique=True, verbose_name='identificateur'), + ), + migrations.AlterField( + model_name='place', + name='event', + field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), + ), + ] diff --git a/event/models.py b/event/models.py index eebe9b3..cd96040 100644 --- a/event/models.py +++ b/event/models.py @@ -1,36 +1,48 @@ from django.contrib.auth import get_user_model -from django.utils.translation import ugettext_lazy as _ from django.core.validators import RegexValidator -from django.core.exceptions import FieldError +from django.core.exceptions import FieldDoesNotExist, FieldError from django.db import models +from django.utils.translation import ugettext_lazy as _ + from communication.models import SubscriptionMixin User = get_user_model() +ColorValidator = RegexValidator( + regex=r'^#(?:[0-9a-fA-F]{3}){1,2}$', + message=_( + "La chaîne de caractère rentrée n'est pas une couleur en hexadécimal." + ), +) + + class Event(SubscriptionMixin, models.Model): title = models.CharField( - _("nom de l'évènement"), - max_length=200, - ) + _("nom de l'évènement"), + max_length=200, + ) slug = models.SlugField( - _('identificateur'), - unique=True, - help_text=_("Seulement des lettres, des chiffres ou" - "les caractères '_' ou '-'."), - ) + _("identificateur"), + unique=True, + help_text=_( + "Seulement des lettres, des chiffres ou les caractères '_' ou '-'." + ), + ) created_by = models.ForeignKey( - User, - related_name="created_events", - editable=False, - ) + User, + verbose_name=_("créé par"), + on_delete=models.SET_NULL, + related_name="created_events", + editable=False, null=True, + ) created_at = models.DateTimeField( - _('date de création'), - auto_now_add=True, - ) - description = models.TextField(_('description')) - beginning_date = models.DateTimeField(_('date de début')) - ending_date = models.DateTimeField(_('date de fin')) + _("date de création"), + auto_now_add=True, + ) + description = models.TextField(_("description")) + beginning_date = models.DateTimeField(_("date de début")) + ending_date = models.DateTimeField(_("date de fin")) class Meta: verbose_name = _("évènement") @@ -45,12 +57,14 @@ class EventSpecificMixin(models.Model): or not (depending on whether the event field is null)""" event = models.ForeignKey( - 'event.Event', + Event, verbose_name=_("évènement"), - help_text=_("Si spécifié, l'instance du modèle " - "est spécifique à l'évènement en question"), - blank=True, - null=True + help_text=_( + "Si spécifié, l'instance du modèle est spécifique à l'évènement " + "en question." + ), + on_delete=models.CASCADE, + blank=True, null=True, ) class Meta: @@ -59,9 +73,9 @@ class EventSpecificMixin(models.Model): class Place(EventSpecificMixin, models.Model): name = models.CharField( - _("nom du lieu"), - max_length=200, - ) + _("nom du lieu"), + max_length=200, + ) description = models.TextField(blank=True) class Meta: @@ -74,26 +88,22 @@ class Place(EventSpecificMixin, models.Model): class ActivityTag(EventSpecificMixin, models.Model): name = models.CharField( - _("nom du tag"), - max_length=200, - ) + _("nom du tag"), + max_length=200, + ) is_public = models.BooleanField( - help_text=_("Sert à faire une distinction dans" - " l'affichage selon que cela soit" - " destiné au public ou à l'équipe" - " organisatrice"), - ) - color_regex = RegexValidator( - regex=r'^#(?:[0-9a-fA-F]{3}){1,2}$', - message=_("La chaîne de caractère rentrée n'est pas" - " une couleur en hexadécimal."), - ) + _("est public"), + help_text=_( + "Sert à faire une distinction dans l'affichage selon que le tag " + "soit destiné au public ou à l'organisation." + ), + ) color = models.CharField( - _('couleur'), - max_length=7, - validators=[color_regex], - help_text=_("Rentrer une couleur en hexadécimal"), - ) + _('couleur'), + max_length=7, + validators=[ColorValidator], + help_text=_("Rentrer une couleur en hexadécimal (#XXX ou #XXXXXX)."), + ) class Meta: verbose_name = _("tag") @@ -105,49 +115,53 @@ class ActivityTag(EventSpecificMixin, models.Model): class AbstractActivityTemplate(SubscriptionMixin, models.Model): title = models.CharField( - _("nom de l'activité"), - max_length=200, - blank=True, - null=True, - ) + _("nom de l'activité"), + max_length=200, + blank=True, null=True, + ) # FIXME: voir comment on traite l'héritage de `event` - event = models.ForeignKey(Event) + event = models.ForeignKey( + Event, + verbose_name=_("évènement"), + on_delete=models.CASCADE, + editable=False, + ) is_public = models.NullBooleanField( - blank=True, - ) + _("est public"), + blank=True, + ) has_perm = models.NullBooleanField( - blank=True, - ) + _("inscription de permanents"), + blank=True, + ) min_perm = models.PositiveSmallIntegerField( - _('nombre minimum de permanents'), - blank=True, - null=True, - ) + _('nombre minimum de permanents'), + blank=True, null=True, + ) max_perm = models.PositiveSmallIntegerField( - _('nombre maximum de permanents'), - blank=True, - null=True, - ) + _('nombre maximum de permanents'), + blank=True, null=True, + ) description = models.TextField( - _('description'), - help_text=_("Public, Visible par tout le monde."), - blank=True, - null=True, - ) + _('description'), + help_text=_("Visible par tout le monde si l'événément est public."), + blank=True, null=True, + ) remarks = models.TextField( - _('remarques'), - help_text=_("Visible uniquement par les organisateurs"), - blank=True, - null=True, - ) + _('remarques'), + help_text=_("Visible uniquement par les organisateurs."), + blank=True, null=True, + ) tags = models.ManyToManyField( - ActivityTag, - blank=True, - ) - place = models.ManyToManyField( - Place, - blank=True, - ) + ActivityTag, + verbose_name=_('tags'), + blank=True, + ) + places = models.ManyToManyField( + Place, + verbose_name=_('lieux'), + blank=True, + ) class Meta: abstract = True @@ -164,40 +178,42 @@ class ActivityTemplate(AbstractActivityTemplate): class Activity(AbstractActivityTemplate): parent = models.ForeignKey( - ActivityTemplate, - related_name="children", - blank=True, - null=True, - ) + ActivityTemplate, + verbose_name=_("template"), + on_delete=models.PROTECT, + related_name="children", + blank=True, null=True, + ) staff = models.ManyToManyField( - User, - related_name="in_perm_activities", - blank=True, - ) + User, + verbose_name=_("permanents"), + related_name="in_perm_activities", + blank=True, + ) beginning = models.DateTimeField(_("heure de début")) end = models.DateTimeField(_("heure de fin")) def get_herited(self, attrname): - inherited_fields = [f.name for f in - ActivityTemplate._meta.get_fields()] - m2m_fields = [f.name for f in ActivityTemplate._meta.get_fields() - if f.many_to_many] - attr = getattr(self, attrname) - if attrname not in inherited_fields: + try: + tpl_field = ActivityTemplate._meta.get_field(attrname) + except FieldDoesNotExist: raise FieldError( - _("%(attrname)s n'est pas un champ héritable"), - params={'attrname': attrname}, - ) - elif attrname in m2m_fields: - if attr.exists(): - return attr + "%(attrname)s field can't be herited.", + params={'attrname': attrname}, + ) + + value = getattr(self, attrname) + + if tpl_field.many_to_many: + if value.exists(): + return value else: return getattr(self.parent, attrname) - elif attr is None: + elif value is None: return getattr(self.parent, attrname) else: - return attr + return value class Meta: verbose_name = _("activité") diff --git a/event/validators.py b/event/validators.py new file mode 100644 index 0000000..46c2d19 --- /dev/null +++ b/event/validators.py @@ -0,0 +1,10 @@ +from django.core.validators import RegexValidator +from django.utils.translation import ugettext_lazy as _ + + +ColorValidator = RegexValidator( + regex=r'^#(?:[0-9a-fA-F]{3}){1,2}$', + message=_( + "La chaîne de caractère rentrée n'est pas une couleur en hexadécimal." + ), +) From 07d4c3ead1439cce2f51a8e4b42b60dfbd3e44fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 3 Aug 2017 12:14:15 +0200 Subject: [PATCH 26/30] Reflect changes on validators/fields --- event/models.py | 11 ++--------- event/tests.py | 8 ++++---- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/event/models.py b/event/models.py index cd96040..7560ae1 100644 --- a/event/models.py +++ b/event/models.py @@ -1,22 +1,15 @@ from django.contrib.auth import get_user_model -from django.core.validators import RegexValidator from django.core.exceptions import FieldDoesNotExist, FieldError from django.db import models from django.utils.translation import ugettext_lazy as _ from communication.models import SubscriptionMixin +from .validators import ColorValidator + User = get_user_model() -ColorValidator = RegexValidator( - regex=r'^#(?:[0-9a-fA-F]{3}){1,2}$', - message=_( - "La chaîne de caractère rentrée n'est pas une couleur en hexadécimal." - ), -) - - class Event(SubscriptionMixin, models.Model): title = models.CharField( _("nom de l'évènement"), diff --git a/event/tests.py b/event/tests.py index 36dee90..29c840f 100644 --- a/event/tests.py +++ b/event/tests.py @@ -97,14 +97,14 @@ class ActivityInheritanceTest(TestCase): self.assertEqual(self.real_act.get_herited('max_perm'), 1) def test_inherits_place(self): - self.template_act.place.add(self.loge) + self.template_act.places.add(self.loge) self.assertEqual( - self.real_act.get_herited('place').get(), + self.real_act.get_herited('places').get(), self.loge ) - self.real_act.place.add(self.aqua) + self.real_act.places.add(self.aqua) self.assertEqual( - self.real_act.get_herited('place').get(), + self.real_act.get_herited('places').get(), self.aqua ) From fc4930a49eb8cbf12255068b1b0207b29f0a0945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 3 Aug 2017 12:14:53 +0200 Subject: [PATCH 27/30] Clean serializers and viewsets Event-based urls - Add viewset mixin 'EventUrlViewSetMixin' to get the event from the 'event_pk' url kwarg of a view. - Add url serializer fields for object which can be accessed with a root-level and/or event-specific url ('EventHyperlinked*Field). Update viewsets and serializers to use these + clean inheritance viewsets. --- api/event/fields.py | 33 +++++++++++ api/event/serializers.py | 114 ++++++++++++++------------------------ api/event/views.py | 116 +++++++++++++++++++++++---------------- api/urls.py | 29 +++++----- 4 files changed, 157 insertions(+), 135 deletions(-) create mode 100644 api/event/fields.py diff --git a/api/event/fields.py b/api/event/fields.py new file mode 100644 index 0000000..4df025c --- /dev/null +++ b/api/event/fields.py @@ -0,0 +1,33 @@ +from rest_framework import serializers +from rest_framework.reverse import reverse + + +class EventHyperlinkedFieldMixin: + + def get_url(self, obj, view_name, request, format): + url_kwargs = {'pk': obj.pk} + if getattr(obj, 'event', None): + url_kwargs['event_pk'] = obj.event.pk + return reverse( + view_name, kwargs=url_kwargs, request=request, format=format) + + def get_object(self, view_name, view_args, view_kwargs): + lookup_kwargs = { + 'pk': view_kwargs['pk'], + 'event_id': view_kwargs.get('event_pk'), + } + return self.get_queryset().get(**lookup_kwargs) + + +class EventHyperlinkedRelatedField( + EventHyperlinkedFieldMixin, + serializers.HyperlinkedRelatedField, + ): + pass + + +class EventHyperlinkedIdentityField( + EventHyperlinkedFieldMixin, + serializers.HyperlinkedIdentityField + ): + pass diff --git a/api/event/serializers.py b/api/event/serializers.py index 44461e6..a7ff849 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -1,53 +1,30 @@ -from django.shortcuts import get_object_or_404 +from django.db import transaction +from django.utils.decorators import method_decorator + from rest_framework import serializers + from event.models import Event, ActivityTag, Place, ActivityTemplate +from .fields import EventHyperlinkedIdentityField + # Event Serializer -class EventSerializer(serializers.HyperlinkedModelSerializer): +class EventSerializer(serializers.ModelSerializer): + # TODO: Change this to a nested serializer ~(url, full_name) of User created_by = serializers.ReadOnlyField(source='created_by.get_full_name') - created_at = serializers.ReadOnlyField() class Meta: model = Event - fields = ('url', 'id', 'title', 'slug', 'created_by', 'created_at', - 'description', 'beginning_date', 'ending_date') - - -# Classes utilitaires -class EventSpecificSerializerMixin(): - """ - Provide `update` and `create` methods for nested view with an Event - For example for Models which extends EventSpecificMixin - the event id has to be provided in the `save` method - Works fine with view.EventSpecificViewSet - Also provides : - event = eventserializer(allow_null=true, read_only=true) - """ - event = EventSerializer(allow_null=True, read_only=True) - - def update(self, instance, validated_data): - """ - Note : does NOT change the event value of the instance - """ - validated_data.pop('event_pk') - [setattr(instance, key, value) - for key, value in validated_data.items()] - instance.save() - return instance - - def create(self, validated_data): - ModelClass = self.Meta.model - event_pk = validated_data.pop('event_pk', None) - event = event_pk and get_object_or_404(Event, id=event_pk) or None - instance = ModelClass.objects.create(event=event, **validated_data) - return instance + fields = ( + 'url', 'id', 'title', 'slug', 'created_by', 'created_at', + 'description', 'beginning_date', 'ending_date', + ) # Serializers # TODO rajouter des permissions -class PlaceSerializer(EventSpecificSerializerMixin, - serializers.ModelSerializer): +class PlaceSerializer(serializers.ModelSerializer): + serializer_url_field = EventHyperlinkedIdentityField class Meta: model = Place @@ -55,8 +32,9 @@ class PlaceSerializer(EventSpecificSerializerMixin, # TODO rajouter des permissions -class ActivityTagSerializer(EventSpecificSerializerMixin, - serializers.ModelSerializer): +class ActivityTagSerializer(serializers.ModelSerializer): + serializer_url_field = EventHyperlinkedIdentityField + class Meta: model = ActivityTag fields = ('url', 'id', 'name', 'is_public', 'color', 'event') @@ -64,36 +42,30 @@ class ActivityTagSerializer(EventSpecificSerializerMixin, # TODO rajouter des permissions class ActivityTemplateSerializer(serializers.ModelSerializer): - event = EventSerializer(read_only=True) tags = ActivityTagSerializer(many=True) + serializer_url_field = EventHyperlinkedIdentityField + class Meta: model = ActivityTemplate - fields = ('id', 'title', 'event', 'is_public', 'has_perm', - 'min_perm', 'max_perm', 'description', 'remarks', 'tags',) + fields = ( + 'url', 'id', 'title', 'event', 'is_public', 'has_perm', 'min_perm', + 'max_perm', 'description', 'remarks', 'tags', + ) - def update(self, instance, validated_data): - """ - @tags comportement attendu : si l'id existe déjà on ne change pas - les autres champs et si l'id n'existe pas on le créé - """ - tags_data = validated_data.pop('tags') - validated_data.pop('event_pk') - event = instance.event - [setattr(instance, key, value) - for key, value in validated_data.items()] - instance.save() + def process_tags(self, instance, tags_data): # TODO: en fonction de si backbone envoie un `id` ou non lorsque le tag # n'existe pas encore il faudra faire un premier passage sur `tags` i # pour s'assurer que le get ne foire pas le get si, par exemple, le tag # été modifié entre temps dans la base de donnée (mais pas sur la # classe backbone + tags = [] for tag_data in tags_data: - tag_data.pop('event', None) - tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] - for tag_data in tags_data] - instance.tags.set(tags) - return instance + tag, _ = ActivityTag.objects.get_or_create(**tag_data, defaults={ + 'event': instance.event, + }) + tags.append(tag) + instance.tags.add(*tags) def create(self, validated_data): """ @@ -101,20 +73,18 @@ class ActivityTemplateSerializer(serializers.ModelSerializer): les autres champs et si l'id n'existe pas on le créé """ tags_data = validated_data.pop('tags') - event_pk = validated_data.pop('event_pk') - event = event_pk and get_object_or_404(Event, id=event_pk) or None - activity_template = ActivityTemplate.objects.create(event=event, - **validated_data) - # TODO: en fonction de si backbone envoie un `id` ou non lorsque le tag - # n'existe pas encore il faudra faire un premier passage sur `tags` i - # pour s'assurer que le get ne foire pas le get si, par exemple, le tag - # été modifié entre temps dans la base de donnée (mais pas sur la - # classe backbone - for tag_data in tags_data: - tag_data.pop('event', None) - tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] - for tag_data in tags_data] - activity_template.tags = tags + activity_template = super().create(validated_data) + self.process_tags(activity_template, tags_data) + return activity_template + + def update(self, instance, validated_data): + """ + @tags comportement attendu : si l'id existe déjà on ne change pas + les autres champs et si l'id n'existe pas on le créé + """ + tags_data = validated_data.pop('tags') + activity_template = super().update(instance, validated_data) + self.process_tags(activity_template, tags_data) return activity_template diff --git a/api/event/views.py b/api/event/views.py index b360583..3497f3a 100644 --- a/api/event/views.py +++ b/api/event/views.py @@ -1,17 +1,54 @@ from django.contrib.auth import get_user_model from django.db.models import Q +from django.shortcuts import get_object_or_404 +from django.utils.functional import cached_property from rest_framework.viewsets import ModelViewSet +from rest_framework.filters import OrderingFilter -from api.event.serializers import EventSerializer, PlaceSerializer,\ - ActivityTagSerializer, ActivityTemplateSerializer, ActivitySerializer -from event.models import Event, Place, ActivityTag, ActivityTemplate, Activity +from event.models import Activity, ActivityTag, ActivityTemplate, Event, Place + +from .serializers import ( + ActivitySerializer, ActivityTagSerializer, ActivityTemplateSerializer, + EventSerializer, PlaceSerializer, +) User = get_user_model() # classes utilitaires -class EventSpecificViewSet(ModelViewSet): + +class EventUrlViewSetMixin: + """ + ViewSet mixin to handle the evenk_pk from url. + """ + + @cached_property + def event(self): + event_pk = self.kwargs.get('event_pk') + if event_pk: + return get_object_or_404(Event, pk=event_pk) + return None + + +class EventModelViewSetMixin: + + def perform_create(self, serializer): + serializer.save(event=self.event) + + def perform_update(self, serializer): + serializer.save(event=self.event) + + +class EventModelViewSet( + EventModelViewSetMixin, + EventUrlViewSetMixin, + ModelViewSet, + ): + pass + + +class EventSpecificModelViewSet(EventModelViewSet): """ ViewSet that returns : * rootlevel objects if no Event is specified @@ -21,81 +58,64 @@ class EventSpecificViewSet(ModelViewSet): to the save method. Works fine with serializers.EventSpecificSerializer Useful for models that extends EventSpecificMixin """ + def get_queryset(self): """ Warning : You may want to override this method and not call with super """ - event_pk = self.kwargs.get('event_pk') queryset = super().get_queryset() - if event_pk: - return queryset.filter(Q(event=event_pk) | Q(event=None)) - return queryset.filter(event=None) - - def perform_create(self, serializer): - event_pk = self.kwargs.get('event_pk') - serializer.save(event_pk=event_pk) - - def perform_update(self, serializer): - event_pk = self.kwargs.get('event_pk') - serializer.save(event_pk=event_pk) + filters = Q(event=None) + if self.event: + filters |= Q(event=self.event) + return queryset.filter(filters) # ViewSets class EventViewSet(ModelViewSet): - """ - This viewset automatically provides `list`, `create`, `retrieve`, - `update` and `destroy` actions. - - """ queryset = Event.objects.all() serializer_class = EventSerializer + filter_backends = (OrderingFilter,) + ordering_fields = ('title', 'creation_date', 'beginning_date', + 'ending_date', ) + ordering = ('beginning_date', ) + def perform_create(self, serializer): serializer.save(created_by=self.request.user) -class PlaceViewSet(EventSpecificViewSet): +class PlaceViewSet(EventSpecificModelViewSet): queryset = Place.objects.all() serializer_class = PlaceSerializer + filter_backends = (OrderingFilter,) + ordering_fields = ('name', ) + ordering = ('name', ) -class ActivityTagViewSet(EventSpecificViewSet): + +class ActivityTagViewSet(EventSpecificModelViewSet): queryset = ActivityTag.objects.all() serializer_class = ActivityTagSerializer + filter_backends = (OrderingFilter,) + ordering_fields = ('is_public', 'name', ) + ordering = ('is_public', 'name', ) -class ActivityTemplateViewSet(ModelViewSet): - """ - This viewset automatically provides `list`, `create`, `retrieve`, - `update` and `destroy` actions. - """ +class ActivityTemplateViewSet(EventModelViewSet): queryset = ActivityTemplate.objects.all() serializer_class = ActivityTemplateSerializer - def perform_create(self, serializer): - event_pk = self.kwargs.get('event_pk') - serializer.save(event_pk=event_pk) - - def perform_update(self, serializer): - event_pk = self.kwargs.get('event_pk') - serializer.save(event_pk=event_pk) + filter_backends = (OrderingFilter,) + ordering_fields = ('title', ) + ordering = ('title', ) -class ActivityViewSet(ModelViewSet): - """ - This viewset automatically provides `list`, `create`, `retrieve`, - `update` and `destroy` actions. - - """ +class ActivityViewSet(EventModelViewSet): queryset = Activity.objects.all() serializer_class = ActivitySerializer - def perform_create(self, serializer): - event_pk = self.kwargs.get('event_pk') - serializer.save(event_pk=event_pk) - - def perform_update(self, serializer): - event_pk = self.kwargs.get('event_pk') - serializer.save(event_pk=event_pk) + filter_backends = (OrderingFilter,) + ordering_fields = ('title', ) + ordering = ('title', ) diff --git a/api/urls.py b/api/urls.py index 0409143..1a704a0 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,27 +1,26 @@ from django.conf.urls import url, include + from rest_framework_nested.routers import SimpleRouter, NestedSimpleRouter -from api.event.views import EventViewSet, PlaceViewSet, ActivityTagViewSet,\ - ActivityTemplateViewSet -# Create a router and register our viewsets with it. +from api.event import views + + router = SimpleRouter() -router.register(r'event', EventViewSet, 'event') -router.register(r'place', PlaceViewSet, 'place') -router.register(r'activitytag', ActivityTagViewSet, 'activitytag') +router.register(r'event', views.EventViewSet) +router.register(r'place', views.PlaceViewSet) +router.register(r'tag', views.ActivityTagViewSet) -# Register nested router and register someviewsets vith it + +# Views behind /event//... event_router = NestedSimpleRouter(router, r'event', lookup='event') -event_router.register(r'place', PlaceViewSet, base_name='event-place') -event_router.register(r'tag', ActivityTagViewSet, base_name='event-activitytag') -event_router.register(r'activitytemplate', ActivityTemplateViewSet, - base_name='event-activitytemplate') +event_router.register(r'place', views.PlaceViewSet) +event_router.register(r'tag', views.ActivityTagViewSet) +event_router.register(r'template', views.ActivityTemplateViewSet) -# The API URLs are now determined automatically by the router. -# Additionally, we include the login URLs for the browsable API. +# API URLconf: routers + auth for browsable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^', include(event_router.urls)), - url(r'^api-auth/', include('rest_framework.urls', - namespace='rest_framework')) + url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')), ] From f4041d6e0258c696a2a639bd4fd9fe499fb37663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 3 Aug 2017 12:11:42 +0200 Subject: [PATCH 28/30] Prepare base structures for API' tests. --- api/test/testcases.py | 694 ++++++++++++++++++++++++++++++++ api/test/utils.py | 19 + api/testcases.py | 292 -------------- evenementiel/settings/common.py | 1 + 4 files changed, 714 insertions(+), 292 deletions(-) create mode 100644 api/test/testcases.py create mode 100644 api/test/utils.py delete mode 100644 api/testcases.py diff --git a/api/test/testcases.py b/api/test/testcases.py new file mode 100644 index 0000000..fb879e4 --- /dev/null +++ b/api/test/testcases.py @@ -0,0 +1,694 @@ +from unittest import mock + +from django.contrib.auth import get_user_model +from django.utils import timezone +from django.urls import reverse + +from rest_framework import status + +User = get_user_model() + + +class GenericAPITestCaseMixin: + """ + Base mixin for testing one or more operations on a model. + + The specifics of each operation is not implemented here. + Each operation has its own TestCase, which relies on methods/attributes + defined here (some or all). + + The "real" mixins for each operation are: + [List,Create,Retrieve,Update,Destroy]APITestCaseMixin. + + Example: + + E.g. for a creation test, your testcase should be something like: + + class MyModelCreateAPITestCase( + CreateAPITestCaseMixin, + GenericAPITestCaseMixin, + APITestCase, + ): + ... + + Attributes: + # General + model (models.Model): The model class under test. + url_label (str): Use to get the API urls of the model under test: + - The model url is:`reverse('{url_label}-list). + This is used by `[List,Create]APITestCaseMixin`. + The url can also be customized by defining `get_url_model` + method. + - The url of a model object is: `reverse('{label}-detail')`. + This is used by `[Retrieve,Update,Destroy]APITestCaseMixin`. + The url can also be customized by defining `get_url_object` + method. + + # Authentication + user_auth (User instance): If given, this user is authenticated for + each request sent to the API. Default to `None`. + See section 'Authenticate requests'. + user_auth_mapping (dict of str: User instance): Associates a user to + authenticate for each type of API request. + See section 'Authenticate requests'. + + Authenticate requests: + Each real testcase call `get_user` method with a label for the + operation under test (e.g. UpdateAPITestCaseMixin will call + `get_user('update')`. If it returns a user, this latter is + authenticated with `force_authenticate` method of the test client. + + The default behavior, which can be customized by replacing `get_user` + method, is to return `user_auth` attribute/property if not None. Else, + `user_auth_mapping[operation_type_label]` or None if key is missing. + + A basic user is created during setUp. It can be found as `user` + attribute. + + """ + model = None + url_label = '' + + user_auth = None + user_auth_mapping = {} + + def setUp(self): + """ + Prepare a test execution. + + Don't forget to call super().setUp() if you define this in subclass. + """ + # Dict comparison can be large. + self.maxDiff = 2000 + + # This will be returned by django.utils.timezone.now thanks to the mock + # in test methods. + # This can be useful to check auto_now and auto_now_add fields. + self.now = timezone.now() + + # A very basic user. + self.user = User.objects.create_user( + username='user', password='user', + first_name='first', last_name='last', + ) + + def get_user(self, action): + """ + Returns a user to authenticate for requests of type 'action'. + + Property `user_auth` has precedence over the property + `user_auth_mapping`. + + Args: + action (str): Operation label. See LCRUD TestCases for reserved + labels, before setting your own if you create a new common + operation TestCase. + + Returns: + User instance | None: + If None, no user should be authenticated for this request. + Else, the returned user should be authenticated. + """ + if self.user_auth is not None: + return self.user_auth + return self.user_auth_mapping.get(action) + + def get_url_model(self, *args, **kwargs): + """ + Returns the API url for the model. + + Default to a reverse on `{url_label}-list`. + + Used by `[List,Create]APITestCaseMixin`. + + """ + return reverse( + '{label}-list'.format(label=self.url_label), + args=args, kwargs=kwargs, + ) + + def get_url_object(self, *args, **kwargs): + """ + Returns the API url for a particular object of model. + + Default to a reverse on `{url_label}-detail`. + + Used by `[Retrieve,Update,Destroy]APITestCaseMixin`. + + """ + return reverse( + '{label}-detail'.format(label=self.url_label), + args=args, kwargs=kwargs, + ) + + def db_create(self, data): + """ + Create a model instance in DB from data given as argument. + + `data` can define M2M fields. + + Operations: + - Create object from non-M2M fields. + - Link the M2M fields with `set` on the created object. + + Returns: + The created object. + """ + fields, m2m_fields = {}, {} + for name, value in data.items(): + if self.model._meta.get_field(name).many_to_many: + m2m_fields[name] = value + else: + fields[name] = value + + instance = self.model.objects.create(**fields) + for name, value in m2m_fields.items(): + getattr(instance, name).set(value) + + return instance + + def get_expected_data(self, instance): + """ + Returns the data the API should reply for the object `instance`. + + This should be the same result of the serializer used for the sent + request. + + Must be defined by subclasses, except for DestroyAPITestCaseMixin. + + Args: + instance (a `model` object) + + Returns: + JSON-decoded data of this instance. + + """ + raise NotImplementedError( + "Subclass must implement 'get_expected_data(instance)' method." + ) + + def assertInstanceEqual(self, instance, expected): + """ + Checks if instance verifies expected. + + For each key/value pair of expected, it verifies that instance.key is + equal to value. `assertEqual` is used, except for M2M fields where + `assertQuerysetEqual(ordered=True, ...)` is used. + + Args: + instance (`model` object): E.g. obtained from DB after a create + operation. + expected (dict): Expected data of instance. + + """ + for key, exp_value in expected.items(): + field = self.model._meta.get_field(key) + value = getattr(instance, key) + if field.many_to_many: + self.assertQuerysetEqual( + value.all(), map(repr, exp_value), + ordered=False, + ) + else: + self.assertEqual(value, exp_value) + + @property + def instances_data(self): + """ + Instances data of the model which will be created if necessary. + + For example, ListAPITestCaseMixin uses these before sending the list + request. + + The first is also used, if `instance_data` is not re-defined, for RUD + operations. + + """ + raise NotImplementedError( + "Property or attribute 'instances_data' must be declared by " + "subclass." + ) + + @property + def instance_data(self): + """ + Data of a single instance of model. + + For example, this data is used to create the instance of model to apply + RUD operations. + + Default to the first item of the `instances_data` property. + + """ + return self.instances_data[0] + + +class ListAPITestCaseMixin: + """ + Mixin to test the "List" API request of a model. + + Has hooks to check the returned objects and their ordering. + + Authentication: + This operation use the label 'list'. + See `GenericAPITestCaseMixin`#`Authenticate requests` for further + informations. + + Attributes: + list_ordering (list of str | str): The ordering of objects that should + be respected by the API response. + Same format as the ordering Meta attribute of models, e.g. + '-created_at' or ['-event', 'created_at']. + Ordering on '__' fields are currently not supported. + + Todo: + * Allow '__' in ordering. + * Support pages. Currently we expect returned objects are on a single + page. + + """ + list_ordering = [] + + @property + def list_expected(self): + """ + Model instances the API should returned. + + Default to all objects of models. + Eventually sorted according to `list_ordering` attribute. + + """ + qs = self.model.objects.all() + if self.list_ordering: + qs = qs.order_by(self.list_ordering) + return qs + + @property + def list_expected_count(self): + """ + Number of objects the API should returned. + + Default to the length of expected returned objects. + """ + return len(self.list_expected) + + @property + def list_expected_ordering(self): + """ + Use this to get expected ordering, instead of relying directly on the + `list_ordering`, if you write subclasses. + """ + if isinstance(self.list_ordering, (tuple, list)): + return self.list_ordering + return [self.list_ordering] + + def assertOrdered(self, results, ordering): + """ + Check `results` are sorted according to `ordering`. + + Attributes: + results (list of model instances) + ordering (list of fields as str) + + """ + _ordering = [ + (f[1:], True) if f.startswith('-') else (f, False) + for f in ordering + ] + + it = iter(results) + + previous = next(it) + + for current in it: + self._assertOrdered(previous, current, _ordering) + previous = current + + def _assertOrdered(self, d1, d2, ordering): + if not ordering: + return True + + field, desc = ordering[0] + + v1, v2 = d1[field], d2[field] + + if v1 == v2: + self._assertOrdered(v1, v2, ordering[1:]) + elif desc: + self.assertGreater(v1, v2, msg=( + "Ordering on '%s' (DESC) is not respected for the following " + "objects.\n- First: %r.\n- Second: %r." + % (field, d1, d2) + )) + else: + self.assertLess(v1, v2, msg=( + "Ordering on '%s' (ASC) is not respected for the following " + "objects:\n- First: %r.\n- Second: %r." + % (field, d1, d2) + )) + + @mock.patch('django.utils.timezone.now') + def test_list(self, mock_now): + """ + The test. + + Preparation: + - Create instances from `instances_data` property. + - Optionally, authenticate client. + + Execution: + - HTTP GET request on model url. + + Check: + - Response status code: 200. + - The base response: count (pages not supported). + - Results (serialized instances) are formatted as expected and + their ordering. + + """ + mock_now.return_value = self.now + + # Setup database. + + for data in self.instances_data: + self.db_create(data) + + # Call API to get instances list. + user = self.get_user('list') + if user: + self.client.force_authenticate(user) + + r = self.client.get(self.get_url_model()) + + # Check API response. + self.assertEqual(r.status_code, status.HTTP_200_OK) + + base_response = { + 'count': self.list_expected_count, + 'previous': None, + 'next': None, + } + self.assertDictContainsSubset(base_response, r.data) + + results = r.data['results'] + + # Check API response ordering. + self.assertOrdered(results, self.list_expected_ordering) + + # Check API response data. + for result, instance in zip(results, self.list_expected): + self.assertDictEqual(result, self.get_expected_data(instance)) + + +class CreateAPITestCaseMixin: + """ + Mixin to test the "Create" API request of a model. + + Authentication: + This operation use the label 'create'. + See `GenericAPITestCaseMixin`#`Authenticate requests` for further + informations. + + Note: + The request is sent assuming the payload is JSON. + + """ + + @property + def create_data(self): + """ + Payload of the create request sent to the API. + """ + raise NotImplementedError( + "Subclass must define a 'create_data' attribute/property. This is " + "the payload of the POST request to the model url." + ) + + @property + def create_expected(self): + """ + Data of model instance which should be created. + """ + raise NotImplementedError( + "Subclass must define a create_expected attribute/property. It is " + "a dict-like object whose value should be equal to the key " + "attribute of the created instance." + ) + + @mock.patch('django.utils.timezone.now') + def test_create(self, mock_now): + """ + The test. + + Preparation: + - Optionally, authenticate client. + + Execution: + - HTTP POST request on model url. Payload from `create_data`. + + Check: + - Response status code: 201. + - Instance has been created in DB. + - Instance created is as expected (check with `create_expected`). + - Instance is correctly serialized (in response' data). + + """ + mock_now.return_value = self.now + + # Call API to create an instance of model. + user = self.get_user('create') + if user: + self.client.force_authenticate(user) + + r = self.client.post(self.get_url_model(), self.create_data) + + # Check database. + instances = self.model.objects.all() + self.assertEqual(len(instances), 1) + + instance = instances[0] + self.assertInstanceEqual(instance, self.create_expected) + + # Check API response. + self.assertEqual(r.status_code, status.HTTP_201_CREATED) + self.assertDictEqual(r.data, self.get_expected_data(instance)) + + +class RetrieveAPITestCaseMixin: + """ + Mixin to test the "Retrieve" API request of a model object. + + Authentication: + This operation use the label 'retrieve'. + See `GenericAPITestCaseMixin`#`Authenticate requests` for further + informations. + + """ + + @mock.patch('django.utils.timezone.now') + def test_retrieve(self, mock_now): + """ + The test. + + Preparation: + - Create a model instance from `instance_data` property. + - Optionally, authenticate client. + + Execution: + - Get url of the object with its pk. + - HTTP GET request on this url. + + Check: + - Response status code: 200. + - Instance is correctly serialized (in response' data). + + """ + mock_now.return_value = self.now + + # Setup database. + data = self.instance_data + instance = self.db_create(data) + + # Call API to retrieve the event data. + user = self.get_user('retrieve') + if user: + self.client.force_authenticate(user) + + r = self.client.get(self.get_url_object(pk=1)) + + # Check API response. + self.assertEqual(r.status_code, status.HTTP_200_OK) + self.assertDictEqual(r.data, self.get_expected_data(instance)) + + +class UpdateAPITestCaseMixin: + """ + Mixin to test the "Update" API request of a model. + + Authentication: + This operation use the label 'update'. + See `GenericAPITestCaseMixin`#`Authenticate requests` for further + informations. + + Notes: + * A single test using partial update / PATCH HTTP method is written. + * The request is sent assuming the payload is JSON. + + Todo: + * Add test for update / PUT HTTP method. + + """ + + @property + def update_data(self): + """ + Payload of the update request sent to the API. + """ + raise NotImplementedError( + "Subclass must define a update_data attribute/property. This is " + "the payload of the PUT request to the instance url." + ) + + @property + def update_expected(self): + """ + Data of model instance which should be updated. + """ + raise NotImplementedError( + "Subclass must define a update_expected attribute/property. It is " + "a dict-like object whose value should be equal to the key " + "attribute of the updated instance." + ) + + @mock.patch('django.utils.timezone.now') + def test_update(self, mock_now): + """ + The test. + + Preparation: + - Create a model instance from `instance_data` property. + - Optionally, authenticate client. + + Execution: + - Get url of the object with its pk. + - HTTP PATCH request on this url. Payload from `update_data`. + + Check: + - Response status code: 200. + - No instance has been created or deleted in DB. + - Updated instance is as expected (check with `update_expected`). + - Instance is correctly serialized (in response' data). + + """ + mock_now.return_value = self.now + + # Setup database. + data = self.instance_data + instance = self.db_create(data) + + # Call API to update the event. + user = self.get_user('update') + if user: + self.client.force_authenticate(user) + + r = self.client.patch(self.get_url_object(pk=1), self.update_data) + + # Check database. + instances = self.model.objects.all() + self.assertEqual(len(instances), 1) + + instance.refresh_from_db() + self.assertInstanceEqual(instance, self.update_expected) + + # Check API response. + self.assertEqual(r.status_code, status.HTTP_200_OK) + self.assertDictEqual(r.data, self.get_expected_data(instance)) + + +class DestroyAPITestCaseMixin: + """ + Mixin to test the "Destroy" API request of a model. + + Authentication: + This operation use the label 'destroy'. + See `GenericAPITestCaseMixin`#`Authenticate requests` for further + informations. + + """ + + @mock.patch('django.utils.timezone.now') + def test_destroy(self, mock_now): + """ + The test. + + Preparation: + - Create a model instance from `instance_data` property. + - Optionally, authenticate client. + + Execution: + - Get url of the object with its pk. + - HTTP DELETE request on this url. + + Check: + - Response status code: 204. + - Instance is no longer present in DB. + + """ + mock_now.return_value = self.now + + # Setup database. + data = self.instance_data + instance = self.db_create(data) + + # Call API to update the event. + user = self.get_user('destroy') + if user: + self.client.force_authenticate(user) + + r = self.client.delete(self.get_url_object(pk=1)) + + # Check database. + instances = self.model.objects.all() + self.assertEqual(len(instances), 0) + + with self.assertRaises(self.model.DoesNotExist): + instance.refresh_from_db() + + # Check API response. + self.assertEqual(r.status_code, status.HTTP_204_NO_CONTENT) + self.assertIsNone(r.data) + + +class ModelAPITestCaseMixin( + ListAPITestCaseMixin, CreateAPITestCaseMixin, RetrieveAPITestCaseMixin, + UpdateAPITestCaseMixin, DestroyAPITestCaseMixin, + GenericAPITestCaseMixin, + ): + """ + Tests all LCRUD operations on a model. + + See general docs in GenericAPITestCaseMixin. + See docs for a specific operation in [Operation]APITestCaseMixin. + + Example: + + class EventAPITestCase(ModelAPITestCaseMixin, APITestCase): + model = Event + ... + + @property + def create_data(self): + ... + + @property + def create_expected(self): + ... + + ... + + If you want to run only a few operations, consider using + specific-mixins individually. You can still have something as + `EventAPITestCaseMixin` to provide common atributes/properties/methods. + + """ + pass diff --git a/api/test/utils.py b/api/test/utils.py new file mode 100644 index 0000000..d6778a3 --- /dev/null +++ b/api/test/utils.py @@ -0,0 +1,19 @@ +import datetime + + +def _json_format(value): + if isinstance(value, datetime.datetime): + return value.isoformat().replace('+00:00', 'Z') + return value + + +def json_format(to_format): + """ + Returns value formatted like json output of the API. + + Supported type of value: + * datetime + """ + if type(to_format) == dict: + return {key: _json_format(value) for key, value in to_format.items()} + return _json_format(to_format) diff --git a/api/testcases.py b/api/testcases.py deleted file mode 100644 index d91f3cb..0000000 --- a/api/testcases.py +++ /dev/null @@ -1,292 +0,0 @@ -from datetime import timedelta -from django.contrib.auth import get_user_model -from django.db.models import Q -from django.utils import timezone -from django.urls import reverse - -from rest_framework.test import APIRequestFactory -from rest_framework import status - -from event.models import Event, Place, ActivityTag, ActivityTemplate - - -User = get_user_model() - - -class DataBaseMixin(object): - """ - provides a datatabse for API tests - """ - @classmethod - def setUpTestData(cls): - # Users - cls.user1_data = {'username': "user1", 'password': "pass1"} - cls.user1 = User.objects.create_user(**cls.user1_data) - - def setUp(self): - # Events - self.event1_data = {'title': "event1", 'slug': "slug1", - 'beginning_date': timezone.now() - + timedelta(days=30), - "description": "C'est trop cool !", - 'ending_date': timezone.now()+timedelta(days=31), - 'created_by': self.user1, } - self.event2_data = {"title": "test event", "slug": "test-event", - "description": "C'est trop cool !", - "beginning_date": "2017-07-18T18:05:00Z", - "ending_date": "2017-07-19T18:05:00Z", } - self.event1 = Event.objects.create(**self.event1_data) - - # ActivityTags - self.tag1_data = {"name": "tag1", "is_public": False, "color": "#111"} - self.tag2_data = {"name": "tag2", "is_public": False, "color": "#222"} - self.tag3_data = {"name": "tag3", "is_public": False, "color": "#333"} - self.tag1 = ActivityTag.objects.create(**self.tag1_data) - - self.act_temp1_data = {'title': "act temp1", 'is_public': True, - 'remarks': "test remark", 'event': self.event1} - self.act_temp2_data = {'title': "act temp2", 'is_public': False, - 'remarks': "test remark", - 'tags': [self.tag2_data, ]} - self.act_temp1 = ActivityTemplate.objects.create(**self.act_temp1_data) - self.act_temp1.tags.add(self.tag1) - - -class EventBasedModelTestMixin(DataBaseMixin): - """ - Note : need to define : - `model`: the model served by the API - `base_name`: the base_name used in the URL - `initial_count`: (will disappear) inital count in the db - `data_creation`: name in db used to create new instance - `instance_name`: existing instance name in the db - used for update/delete - `field_tested`: name of field tested in the update test - `serializer`: serialiser used for the API - - tests for models served by the API that are related to an event - and whose API urls are nested under ../event//%model - """ - def user_create_extra(self): - """ - extra test in creation by a permited user - """ - pass - - def pre_update_extra(self, data): - """ - extra modification for the data sent for update - """ - return data - - def post_update_extra(self): - """ - extra test for updated model - """ - pass - - def test_user_create(self): - """ - ensure a permited user can create a new %model object using API - """ - data = getattr(self, self.data_creation) - - event_id = self.event1.id - url = reverse('{base_name}-list'.format(base_name=self.base_name), - kwargs={'event_pk': event_id}) - self.client.force_authenticate(user=self.user1) - response = self.client.post(url, data, - format='json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(self.model.objects.count(), self.initial_count + 1) - self.assertEqual(self.model.objects.get(id=self.initial_count+1).event, - self.event1) - - self.user_create_extra() - - def test_user_update(self): - """ - ensure a permited user can update a new %model object using API - """ - instance = getattr(self, self.instance_name) - factory = APIRequestFactory() - - instance_id = instance.id - event_id = self.event1.id - url = reverse('{base_name}-list'.format(base_name=self.base_name), - kwargs={'event_pk': event_id}) - url = "%s%d/" % (url, instance_id) - - request = factory.get(url) - data = self.serializer(instance, context={'request': request}).data - - newvalue = "I'm a test" - data[self.field_tested] = newvalue - - data = self.pre_update_extra(data) - - self.client.force_authenticate(user=self.user1) - response = self.client.patch(url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - instance.refresh_from_db() - self.assertEqual(getattr(instance, self.field_tested), newvalue) - - self.post_update_extra(instance) - - def test_user_delete(self): - """ - ensure a permited user can delete a new %model object using API - """ - instance = getattr(self, self.instance_name) - - instance_id = instance.id - event_id = self.event1.id - url = reverse('{base_name}-list'.format(base_name=self.base_name), - kwargs={'event_pk': event_id}) - url = "%s%d/" % (url, instance_id) - - self.client.force_authenticate(user=self.user1) - response = self.client.delete(url) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(self.model.objects.count(), self.initial_count - 1) - - -# TODO rajouter la gestion des permissions dans le Mixin -# TODO rajouter un test pour s'assurer que les personnes non -# connectées ne peuvent pas create/update/delete -class EventSpecificTestMixin(object): - """ - Tests is the EventSpecifics querysets are rendered correctly - using the API - Note : need to define : - `model`: the concerned model serve by the API - `root_base_name`: the base_name used in the root-level urls - `event_base_name`: the base_name used in the event-level urls - - tests for models served by the API that inherit EventSpecificMixin - """ - @classmethod - def setUpTestData(cls): - # Users - cls.user1_data = {'username': "user1", 'password': "pass1"} - cls.user1 = User.objects.create_user(**cls.user1_data) - # Events - cls.event1_data = {'title': "event1", 'slug': "slug1", - 'beginning_date': timezone.now() - + timedelta(days=30), - 'ending_date': timezone.now()+timedelta(days=31), - 'created_by': cls.user1, } - cls.event1 = Event.objects.create(**cls.event1_data) - - def setUp(self): - # Tag - self.tag_root_data = {"name": "tag2", "is_public": False, - "color": "#222"} - self.tag_event_data = {"name": "tag3", "is_public": False, - "color": "#333", 'event': self.event1} - self.tag_root = ActivityTag.objects.create(**self.tag_root_data) - self.tag_event = ActivityTag.objects.create(**self.tag_event_data) - - # Places - self.place_root_data = {'name': "place1", 'event': None, } - self.place_event_data = {'name': "place2", 'event': self.event1, } - self.place_root = Place.objects.create(**self.place_root_data) - self.place_event = Place.objects.create(**self.place_event_data) - - def test_lists(self): - """ - ensure that only root-level models are served under - api/%root_base_name/ - and that root-level and event-level models are served under - api/event//%event_base_name/ - """ - event_id = self.event1.id - root_count = self.model.objects.filter(event=None).count() - event_count = (self.model.objects - .filter(Q(event=self.event1) | Q(event=None)).count()) - - self.client.force_authenticate(user=self.user1) - - url = reverse('{base}-list'.format(base=self.root_base_name)) - response = self.client.get(url, format='json') - self.assertEqual(response.json()['count'], root_count) - - event_id = self.event1.id - url = reverse('{base}-list'.format(base=self.event_base_name), - kwargs={'event_pk': event_id}) - response = self.client.get(url, format='json') - self.assertEqual(response.json()['count'], event_count) - - -# TODO rajouter la gestion des permissions dans le Mixin -# TODO rajouter un test pour s'assurer que les personnes non -# connectées ne peuvent pas create/update/delete -# TODO? essayer de factoriser avec EventBasedMixin ? -# FIXME not working, peut être que le problème vient -# du fait que les dates sont mal envoyées dans le data ? A voir. -class ModelTestMixin(DataBaseMixin): - """ - Note : need to define : `model`, `base_name`, - `instance_name`, `field_tested`, `serializer` - - generic mixin for testing creation/update/delete - of models served by the API - """ - def test_user_create(self): - """ - ensure a permited user can create a new %model object using API - """ - data = getattr(self, self.data_creation) - initial_count = self.model.objects.count() - - url = reverse('{base_name}-list'.format(base_name=self.base_name)) - self.client.force_authenticate(user=self.user1) - response = self.client.post(url, data) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(self.model.objects.count(), initial_count + 1) - - instance = self.model.objects.get(id=initial_count+1) - for field in self.tested_fields.keys(): - self.assertEqual(getattr(instance, field), data[field]) - - def test_user_update(self): - """ - ensure a permited user can update a new %model object using API - """ - instance = getattr(self, self.instance_name) - factory = APIRequestFactory() - - instance_id = instance.id - url = reverse('{base_name}-detail'.format(base_name=self.base_name), - kwargs={'pk': instance_id}) - - request = factory.get(url) - data = self.serializer(instance, context={'request': request}).data - for field, value in self.tested_fields.items(): - data[field] = value - - self.client.force_authenticate(user=self.user1) - response = self.client.put(url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - instance.refresh_from_db() - for field, value in self.tested_fields.items(): - self.assertEqual(getattr(instance, field), value) - - def test_user_delete(self): - """ - ensure a permited user can delete a new %model object using API - """ - instance = getattr(self, self.instance_name) - initial_count = self.model.objects.count() - - instance_id = instance.id - url = reverse('{base_name}-detail'.format(base_name=self.base_name), - kwargs={'pk': instance_id}) - - self.client.force_authenticate(user=self.user1) - response = self.client.delete(url) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(self.model.objects.count(), initial_count - 1) diff --git a/evenementiel/settings/common.py b/evenementiel/settings/common.py index 8baa801..6d41864 100644 --- a/evenementiel/settings/common.py +++ b/evenementiel/settings/common.py @@ -78,6 +78,7 @@ REST_FRAMEWORK = { 'rest_framework.permissions.AllowAny', ], 'PAGE_SIZE': 10, + 'TEST_REQUEST_DEFAULT_FORMAT': 'json', } ROOT_URLCONF = 'evenementiel.urls' From f33028892f6eea62543cf9f990676b837a40a7c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 3 Aug 2017 12:21:37 +0200 Subject: [PATCH 29/30] Add API tests for Event/Place/ActivityTag/ActivityTemplate models This tests mainly cover cases whose API user is friendly. Should be extended with illegal things. ActivityTag, ActivityTemplate and Place models got tests for root and specific-event instance. --- api/event/tests.py | 566 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 533 insertions(+), 33 deletions(-) diff --git a/api/event/tests.py b/api/event/tests.py index 147b185..ed748e1 100644 --- a/api/event/tests.py +++ b/api/event/tests.py @@ -1,56 +1,556 @@ +from datetime import datetime, timedelta + from django.contrib.auth import get_user_model +from django.utils import timezone from rest_framework.test import APITestCase from event.models import Event, Place, ActivityTag, ActivityTemplate -from api.event.serializers import ActivityTemplateSerializer, EventSerializer -from api.testcases import EventBasedModelTestMixin, EventSpecificTestMixin,\ - ModelTestMixin +from api.test.testcases import ModelAPITestCaseMixin +from api.test.utils import json_format User = get_user_model() -class EventTest(ModelTestMixin, APITestCase): +class EventAPITests(ModelAPITestCaseMixin, APITestCase): model = Event - base_name = 'event' - tested_fields = {'title': "I'm a test", } - # Création - data_creation = 'event2_data' - # Update/Delete - instance_name = 'event1' - serializer = EventSerializer + url_label = 'event' + + list_ordering = 'beginning_date' + + @property + def user_auth_mapping(self): + return { + 'create': self.user, + } + + def get_expected_data(self, instance): + return json_format({ + 'url': ( + 'http://testserver/api/event/{pk}/' + .format(pk=instance.pk) + ), + 'id': instance.id, + 'title': instance.title, + 'slug': instance.slug, + 'description': instance.description, + 'beginning_date': instance.beginning_date, + 'ending_date': instance.ending_date, + 'created_by': instance.created_by.get_full_name(), + 'created_at': self.now, + }) + + @property + def instances_data(self): + return [ + { + 'title': "A first event", + 'slug': 'first-event', + 'description': "It's the first event.", + 'beginning_date': self.now + timedelta(days=100), + 'ending_date': self.now + timedelta(days=120), + 'created_by': self.user, + }, + { + 'title': "Another event", + 'slug': 'another-event', + 'description': "It's another event.", + 'beginning_date': self.now + timedelta(days=50), + 'ending_date': self.now + timedelta(days=60), + 'created_by': self.user, + }, + ] + + @property + def create_data(self): + return { + 'title': "An Event", + 'slug': 'event', + 'description': "I am an event.", + 'beginning_date': '2017-08-10 05:30:00', + 'ending_date': '2017-10-21 17:16:20', + } + + @property + def create_expected(self): + return { + **self.create_data, + 'beginning_date': timezone.make_aware( + datetime(2017, 8, 10, 5, 30, 0)), + 'ending_date': timezone.make_aware( + datetime(2017, 10, 21, 17, 16, 20)), + 'id': 1, + 'created_by': self.user, + 'created_at': self.now, + } + + @property + def update_data(self): + return { + 'title': "An updated event.", + 'slug': 'first-event', + 'description': "It's the first updated event.", + 'beginning_date': '2017-08-10 05:30:00', + 'ending_date': '2017-10-21 17:16:20', + 'created_by': 1, + } + + @property + def update_expected(self): + return { + **self.update_data, + 'beginning_date': timezone.make_aware( + datetime(2017, 8, 10, 5, 30, 0)), + 'ending_date': timezone.make_aware( + datetime(2017, 10, 21, 17, 16, 20)), + 'id': 1, + 'created_by': self.user, + 'created_at': self.now, + } -class ActivityTemplateTest(EventBasedModelTestMixin, APITestCase): +class ActivityTemplateAPITests(ModelAPITestCaseMixin, APITestCase): model = ActivityTemplate - base_name = 'event-activitytemplate' - initial_count = 1 - # Creation - data_creation = 'act_temp2_data' - # Update/Delete - instance_name = 'act_temp1' - field_tested = 'title' - serializer = ActivityTemplateSerializer + url_label = 'activitytemplate' - def test_create_extra(self): - self.assertEqual(self.model.objects.get(id=1).tags.count(), 1) + list_ordering = 'title' - def pre_update_extra(self, data): - data['tags'].append(self.tag2_data) + def setUp(self): + super().setUp() + + self.event = Event.objects.create(**{ + 'title': "event1", + 'slug': "slug1", + 'beginning_date': self.now + timedelta(days=30), + 'description': "C'est trop cool !", + 'ending_date': self.now + timedelta(days=31), + 'created_by': self.user, + }) + + self.activity_tag = ActivityTag.objects.create(**{ + 'name': "tag2", + 'is_public': False, + 'color': "#222", + }) + + def get_url_model(self, *args, **kwargs): + kwargs['event_pk'] = 1 + return super().get_url_model(*args, **kwargs) + + def get_url_object(self, *args, **kwargs): + kwargs['event_pk'] = 1 + return super().get_url_object(*args, **kwargs) + + def get_expected_data(self, instance): + return json_format({ + 'url': ( + 'http://testserver/api/event/{event_pk}/template/{pk}/' + .format(event_pk=instance.event.pk, pk=instance.pk) + ), + 'id': instance.id, + 'title': instance.title, + 'is_public': instance.is_public, + 'remarks': instance.remarks, + 'event': instance.event.pk, + 'tags': [ + { + **(tag.event and { + 'url': ( + 'http://testserver/api/event/{event_pk}/' + 'tag/{tag_pk}/' + .format(event_pk=tag.event.pk, tag_pk=tag.pk) + ), + 'event': tag.event.pk, + } or { + 'url': ( + 'http://testserver/api/tag/{tag_pk}/' + .format(tag_pk=tag.pk) + ), + 'event': None, + }), + 'id': tag.id, + 'name': tag.name, + 'is_public': tag.is_public, + 'color': tag.color, + } + for tag in instance.tags.all() + ], + 'has_perm': instance.has_perm, + 'min_perm': instance.min_perm, + 'max_perm': instance.max_perm, + 'description': instance.description, + }) + + @property + def instances_data(self): + return [ + { + 'title': "act temp1", + 'is_public': True, + 'remarks': "test remark", + 'event': self.event, + }, + { + 'title': "act temp2", + 'is_public': False, + 'remarks': "test remark", + 'event': self.event, + 'tags': [self.activity_tag] + }, + ] + + @property + def create_data(self): + return { + 'title': "act temp2", + 'is_public': False, + 'remarks': "test remark", + 'tags': [ + { + 'name': "tag2", + 'is_public': False, + 'color': '#222', + }, + ], + } + + @property + def create_expected(self): + return { + **self.create_data, + 'tags': [ActivityTag.objects.get(name='tag2')], + } + + @property + def update_data(self): + return { + 'title': "act temp3", + 'is_public': False, + 'remarks': "another test remark", + 'tags': [ + { + 'name': "tag2", + 'is_public': False, + 'color': '#222', + }, + { + 'name': "body", + 'is_public': True, + 'color': '#555', + }, + ], + } + + @property + def update_expected(self): + tag_root = ActivityTag.objects.get(name='tag2') + self.assertIsNone(tag_root.event) + + tag_bound = ActivityTag.objects.get(name='body') + self.assertEqual(tag_bound.event, self.event) + + return { + 'title': "act temp3", + 'is_public': False, + 'remarks': "another test remark", + 'tags': [tag_root, tag_bound] + } + + +class BaseActivityTagAPITests: + model = ActivityTag + url_label = 'activitytag' + + list_ordering = ('is_public', 'name') + + def setUp(self): + super().setUp() + + self.event = Event.objects.create(**{ + 'title': "event1", + 'slug': "slug1", + 'beginning_date': self.now + timedelta(days=30), + 'description': "C'est trop cool !", + 'ending_date': self.now + timedelta(days=31), + 'created_by': self.user, + }) + + def get_expected_data(self, instance): + return { + **(instance.event and { + 'url': ( + 'http://testserver/api/event/{event_pk}/tag/{pk}/' + .format(event_pk=instance.event.pk, pk=instance.pk) + ), + 'event': instance.event.pk, + } or { + 'url': ( + 'http://testserver/api/tag/{pk}/' + .format(pk=instance.pk) + ), + 'event': None, + }), + 'id': instance.id, + 'name': instance.name, + 'is_public': instance.is_public, + 'color': instance.color, + } + + @property + def instances_data(self): + return [ + { + 'name': 'a tag', + 'is_public': False, + 'color': '#222', + 'event': None, + }, + { + 'name': 'another tag', + 'is_public': True, + 'color': '#555', + 'event': self.event, + } + ] + + @property + def create_data(self): + return { + 'name': 'plop tag', + 'is_public': True, + 'color': '#888999', + } + + @property + def update_data(self): + return { + 'name': 'this is the tag', + 'is_public': True, + 'color': '#333', + } + + +class RootActivityTagAPITests( + BaseActivityTagAPITests, + ModelAPITestCaseMixin, + APITestCase + ): + + @property + def list_expected(self): + return [ActivityTag.objects.get(name='a tag')] + + @property + def create_expected(self): + return { + **self.create_data, + 'event': None, + } + + @property + def instance_data(self): + data = self.instances_data[0] + self.assertIsNone( + data['event'], + msg="This test should use a tag unbound to any event.", + ) return data - def post_update_extra(self, instance): - self.assertEqual(instance.tags.count(), 2) + @property + def update_expected(self): + return { + **self.update_data, + 'event': None, + } -class EventSpecficTagTest(EventSpecificTestMixin, APITestCase): - model = ActivityTag - root_base_name = 'activitytag' - event_base_name = 'event-activitytag' +class EventActivityTagAPITests( + BaseActivityTagAPITests, + ModelAPITestCaseMixin, + APITestCase + ): + + def get_url_model(self, *args, **kwargs): + kwargs['event_pk'] = 1 + return super().get_url_model(*args, **kwargs) + + def get_url_object(self, *args, **kwargs): + kwargs['event_pk'] = 1 + return super().get_url_object(*args, **kwargs) + + @property + def list_expected(self): + return [ + ActivityTag.objects.get(name='a tag'), + ActivityTag.objects.get(name='another tag'), + ] + + @property + def create_expected(self): + return { + **self.create_data, + 'event': self.event, + } + + @property + def instance_data(self): + data = self.instances_data[1] + self.assertIsNotNone( + data['event'], + msg="This test should use an event-bound tag.", + ) + return data + + @property + def update_expected(self): + return { + **self.update_data, + 'event': self.event, + } -class EventSpecficPlaceTest(EventSpecificTestMixin, APITestCase): +class BasePlaceAPITests: model = Place - root_base_name = 'place' - event_base_name = 'event-place' + url_label = 'place' + + list_ordering = 'name' + + def setUp(self): + super().setUp() + + self.event = Event.objects.create(**{ + 'title': "event1", + 'slug': "slug1", + 'beginning_date': self.now + timedelta(days=30), + 'description': "C'est trop cool !", + 'ending_date': self.now + timedelta(days=31), + 'created_by': self.user, + }) + + def get_expected_data(self, instance): + return { + **(instance.event and { + 'url': ( + 'http://testserver/api/event/{event_pk}/place/{pk}/' + .format(event_pk=instance.event.pk, pk=instance.pk) + ), + 'event': instance.event.pk, + } or { + 'url': ( + 'http://testserver/api/place/{pk}/' + .format(pk=instance.pk) + ), + 'event': None, + }), + 'id': instance.id, + 'name': instance.name, + 'description': instance.description, + } + + @property + def instances_data(self): + return [ + { + 'name': 'a place', + 'event': None, + 'description': 'a description', + }, + { + 'name': 'another place', + 'event': self.event, + } + ] + + @property + def create_data(self): + return { + 'name': 'plop place', + 'description': 'the couro is a chill place', + } + + @property + def update_data(self): + return { + 'name': 'this is the place', + 'description': 'to be', + } + + +class RootPlaceAPITests( + BasePlaceAPITests, + ModelAPITestCaseMixin, + APITestCase + ): + + @property + def list_expected(self): + return [Place.objects.get(name='a place')] + + @property + def create_expected(self): + return { + **self.create_data, + 'event': None, + } + + @property + def instance_data(self): + data = self.instances_data[0] + self.assertIsNone( + data['event'], + msg="This test should use a place unbound to any event.", + ) + return data + + @property + def update_expected(self): + return { + **self.update_data, + 'event': None, + } + + +class EventPlaceTagAPITests( + BasePlaceAPITests, + ModelAPITestCaseMixin, + APITestCase + ): + + def get_url_model(self, *args, **kwargs): + kwargs['event_pk'] = 1 + return super().get_url_model(*args, **kwargs) + + def get_url_object(self, *args, **kwargs): + kwargs['event_pk'] = 1 + return super().get_url_object(*args, **kwargs) + + @property + def list_expected(self): + return [ + Place.objects.get(name='a place'), + Place.objects.get(name='another place'), + ] + + @property + def create_expected(self): + return { + **self.create_data, + 'event': self.event, + } + + @property + def instance_data(self): + data = self.instances_data[1] + self.assertIsNotNone( + data['event'], + msg="This test should use an event-bound place.", + ) + return data + + @property + def update_expected(self): + return { + **self.update_data, + 'event': self.event, + } From 18037246e14ba0977dba9ea2add9147ea0af08f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 12 Aug 2017 14:54:45 +0200 Subject: [PATCH 30/30] Remake migrations --- equipment/migrations/0001_initial.py | 4 +- .../migrations/0002_auto_20170802_2323.py | 21 --- event/migrations/0001_initial.py | 52 +++---- event/migrations/0002_auto_20170723_1419.py | 21 --- event/migrations/0003_auto_20170726_1116.py | 26 ---- event/migrations/0004_auto_20170802_2323.py | 136 ------------------ 6 files changed, 28 insertions(+), 232 deletions(-) delete mode 100644 equipment/migrations/0002_auto_20170802_2323.py delete mode 100644 event/migrations/0002_auto_20170723_1419.py delete mode 100644 event/migrations/0003_auto_20170726_1116.py delete mode 100644 event/migrations/0004_auto_20170802_2323.py diff --git a/equipment/migrations/0001_initial.py b/equipment/migrations/0001_initial.py index 00e5083..5e042ff 100644 --- a/equipment/migrations/0001_initial.py +++ b/equipment/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-07-21 14:20 +# Generated by Django 1.11.4 on 2017-08-12 12:47 from __future__ import unicode_literals from django.db import migrations, models @@ -65,6 +65,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='equipment', name='event', - field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), + field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), ), ] diff --git a/equipment/migrations/0002_auto_20170802_2323.py b/equipment/migrations/0002_auto_20170802_2323.py deleted file mode 100644 index 55a2402..0000000 --- a/equipment/migrations/0002_auto_20170802_2323.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.4 on 2017-08-02 23:23 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('equipment', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='equipment', - name='event', - field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), - ), - ] diff --git a/event/migrations/0001_initial.py b/event/migrations/0001_initial.py index 79dfb71..2c8ea07 100644 --- a/event/migrations/0001_initial.py +++ b/event/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-07-21 14:20 +# Generated by Django 1.11.4 on 2017-08-12 12:47 from __future__ import unicode_literals from django.conf import settings @@ -22,12 +22,12 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(blank=True, max_length=200, null=True, verbose_name="nom de l'activité")), - ('is_public', models.NullBooleanField()), - ('has_perm', models.NullBooleanField()), + ('is_public', models.NullBooleanField(verbose_name='est public')), + ('has_perm', models.NullBooleanField(verbose_name='inscription de permanents')), ('min_perm', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='nombre minimum de permanents')), ('max_perm', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='nombre maximum de permanents')), - ('description', models.TextField(blank=True, help_text='Public, Visible par tout le monde.', null=True, verbose_name='description')), - ('remarks', models.TextField(blank=True, help_text='Visible uniquement par les organisateurs', null=True, verbose_name='remarques')), + ('description', models.TextField(blank=True, help_text="Visible par tout le monde si l'événément est public.", null=True, verbose_name='description')), + ('remarks', models.TextField(blank=True, help_text='Visible uniquement par les organisateurs.', null=True, verbose_name='remarques')), ('beginning', models.DateTimeField(verbose_name='heure de début')), ('end', models.DateTimeField(verbose_name='heure de fin')), ], @@ -41,8 +41,8 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=200, verbose_name='nom du tag')), - ('is_public', models.BooleanField(help_text="Sert à faire une distinction dans l'affichage selon que cela soit destiné au public ou à l'équipe organisatrice")), - ('color', models.CharField(help_text='Rentrer une couleur en hexadécimal', max_length=7, validators=[django.core.validators.RegexValidator(message="La chaîne de caractère rentrée n'est pas une couleur en hexadécimal.", regex='^#(?:[0-9a-fA-F]{3}){1,2}$')], verbose_name='couleur')), + ('is_public', models.BooleanField(help_text="Sert à faire une distinction dans l'affichage selon que le tag soit destiné au public ou à l'organisation.", verbose_name='est public')), + ('color', models.CharField(help_text='Rentrer une couleur en hexadécimal (#XXX ou #XXXXXX).', max_length=7, validators=[django.core.validators.RegexValidator(message="La chaîne de caractère rentrée n'est pas une couleur en hexadécimal.", regex='^#(?:[0-9a-fA-F]{3}){1,2}$')], verbose_name='couleur')), ], options={ 'verbose_name': 'tag', @@ -54,12 +54,12 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(blank=True, max_length=200, null=True, verbose_name="nom de l'activité")), - ('is_public', models.NullBooleanField()), - ('has_perm', models.NullBooleanField()), + ('is_public', models.NullBooleanField(verbose_name='est public')), + ('has_perm', models.NullBooleanField(verbose_name='inscription de permanents')), ('min_perm', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='nombre minimum de permanents')), ('max_perm', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='nombre maximum de permanents')), - ('description', models.TextField(blank=True, help_text='Public, Visible par tout le monde.', null=True, verbose_name='description')), - ('remarks', models.TextField(blank=True, help_text='Visible uniquement par les organisateurs', null=True, verbose_name='remarques')), + ('description', models.TextField(blank=True, help_text="Visible par tout le monde si l'événément est public.", null=True, verbose_name='description')), + ('remarks', models.TextField(blank=True, help_text='Visible uniquement par les organisateurs.', null=True, verbose_name='remarques')), ], options={ 'verbose_name': 'template activité', @@ -71,12 +71,12 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=200, verbose_name="nom de l'évènement")), - ('slug', models.SlugField(help_text="Seulement des lettres, des chiffres oules caractères '_' ou '-'.", unique=True, verbose_name='identificateur')), - ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='date de création')), + ('slug', models.SlugField(help_text="Seulement des lettres, des chiffres ou les caractères '_' ou '-'.", unique=True, verbose_name='identificateur')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='date de création')), ('description', models.TextField(verbose_name='description')), ('beginning_date', models.DateTimeField(verbose_name='date de début')), ('ending_date', models.DateTimeField(verbose_name='date de fin')), - ('created_by', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='created_events', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_events', to=settings.AUTH_USER_MODEL, verbose_name='créé par')), ], options={ 'verbose_name': 'évènement', @@ -89,7 +89,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=200, verbose_name='nom du lieu')), ('description', models.TextField(blank=True)), - ('event', models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement')), + ('event', models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement')), ], options={ 'verbose_name': 'lieu', @@ -99,46 +99,46 @@ class Migration(migrations.Migration): migrations.AddField( model_name='activitytemplate', name='event', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Event'), + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), ), migrations.AddField( model_name='activitytemplate', - name='place', - field=models.ManyToManyField(blank=True, to='event.Place'), + name='places', + field=models.ManyToManyField(blank=True, to='event.Place', verbose_name='lieux'), ), migrations.AddField( model_name='activitytemplate', name='tags', - field=models.ManyToManyField(blank=True, to='event.ActivityTag'), + field=models.ManyToManyField(blank=True, to='event.ActivityTag', verbose_name='tags'), ), migrations.AddField( model_name='activitytag', name='event', - field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), + field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), ), migrations.AddField( model_name='activity', name='event', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Event'), + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), ), migrations.AddField( model_name='activity', name='parent', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='children', to='event.ActivityTemplate'), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='children', to='event.ActivityTemplate', verbose_name='template'), ), migrations.AddField( model_name='activity', - name='place', - field=models.ManyToManyField(blank=True, to='event.Place'), + name='places', + field=models.ManyToManyField(blank=True, to='event.Place', verbose_name='lieux'), ), migrations.AddField( model_name='activity', name='staff', - field=models.ManyToManyField(blank=True, related_name='in_perm_activities', to=settings.AUTH_USER_MODEL), + field=models.ManyToManyField(blank=True, related_name='in_perm_activities', to=settings.AUTH_USER_MODEL, verbose_name='permanents'), ), migrations.AddField( model_name='activity', name='tags', - field=models.ManyToManyField(blank=True, to='event.ActivityTag'), + field=models.ManyToManyField(blank=True, to='event.ActivityTag', verbose_name='tags'), ), ] diff --git a/event/migrations/0002_auto_20170723_1419.py b/event/migrations/0002_auto_20170723_1419.py deleted file mode 100644 index 954dc74..0000000 --- a/event/migrations/0002_auto_20170723_1419.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-07-23 14:19 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('event', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='activity', - name='parent', - field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='event.ActivityTemplate'), - ), - ] diff --git a/event/migrations/0003_auto_20170726_1116.py b/event/migrations/0003_auto_20170726_1116.py deleted file mode 100644 index e776702..0000000 --- a/event/migrations/0003_auto_20170726_1116.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-07-26 11:16 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('event', '0002_auto_20170723_1419'), - ] - - operations = [ - migrations.RenameField( - model_name='event', - old_name='creation_date', - new_name='created_at', - ), - migrations.AlterField( - model_name='activity', - name='parent', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='event.ActivityTemplate'), - ), - ] diff --git a/event/migrations/0004_auto_20170802_2323.py b/event/migrations/0004_auto_20170802_2323.py deleted file mode 100644 index 22c4349..0000000 --- a/event/migrations/0004_auto_20170802_2323.py +++ /dev/null @@ -1,136 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.4 on 2017-08-02 23:23 -from __future__ import unicode_literals - -from django.conf import settings -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('event', '0003_auto_20170726_1116'), - ] - - operations = [ - migrations.RemoveField( - model_name='activity', - name='place', - ), - migrations.RemoveField( - model_name='activitytemplate', - name='place', - ), - migrations.AddField( - model_name='activity', - name='places', - field=models.ManyToManyField(blank=True, to='event.Place', verbose_name='lieux'), - ), - migrations.AddField( - model_name='activitytemplate', - name='places', - field=models.ManyToManyField(blank=True, to='event.Place', verbose_name='lieux'), - ), - migrations.AlterField( - model_name='activity', - name='description', - field=models.TextField(blank=True, help_text="Visible par tout le monde si l'événément est public.", null=True, verbose_name='description'), - ), - migrations.AlterField( - model_name='activity', - name='event', - field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), - ), - migrations.AlterField( - model_name='activity', - name='has_perm', - field=models.NullBooleanField(verbose_name='inscription de permanents'), - ), - migrations.AlterField( - model_name='activity', - name='is_public', - field=models.NullBooleanField(verbose_name='est public'), - ), - migrations.AlterField( - model_name='activity', - name='parent', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='children', to='event.ActivityTemplate', verbose_name='template'), - ), - migrations.AlterField( - model_name='activity', - name='remarks', - field=models.TextField(blank=True, help_text='Visible uniquement par les organisateurs.', null=True, verbose_name='remarques'), - ), - migrations.AlterField( - model_name='activity', - name='staff', - field=models.ManyToManyField(blank=True, related_name='in_perm_activities', to=settings.AUTH_USER_MODEL, verbose_name='permanents'), - ), - migrations.AlterField( - model_name='activity', - name='tags', - field=models.ManyToManyField(blank=True, to='event.ActivityTag', verbose_name='tags'), - ), - migrations.AlterField( - model_name='activitytag', - name='color', - field=models.CharField(help_text='Rentrer une couleur en hexadécimal (#XXX ou #XXXXXX).', max_length=7, validators=[django.core.validators.RegexValidator(message="La chaîne de caractère rentrée n'est pas une couleur en hexadécimal.", regex='^#(?:[0-9a-fA-F]{3}){1,2}$')], verbose_name='couleur'), - ), - migrations.AlterField( - model_name='activitytag', - name='event', - field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), - ), - migrations.AlterField( - model_name='activitytag', - name='is_public', - field=models.BooleanField(help_text="Sert à faire une distinction dans l'affichage selon que le tag soit destiné au public ou à l'organisation.", verbose_name='est public'), - ), - migrations.AlterField( - model_name='activitytemplate', - name='description', - field=models.TextField(blank=True, help_text="Visible par tout le monde si l'événément est public.", null=True, verbose_name='description'), - ), - migrations.AlterField( - model_name='activitytemplate', - name='event', - field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), - ), - migrations.AlterField( - model_name='activitytemplate', - name='has_perm', - field=models.NullBooleanField(verbose_name='inscription de permanents'), - ), - migrations.AlterField( - model_name='activitytemplate', - name='is_public', - field=models.NullBooleanField(verbose_name='est public'), - ), - migrations.AlterField( - model_name='activitytemplate', - name='remarks', - field=models.TextField(blank=True, help_text='Visible uniquement par les organisateurs.', null=True, verbose_name='remarques'), - ), - migrations.AlterField( - model_name='activitytemplate', - name='tags', - field=models.ManyToManyField(blank=True, to='event.ActivityTag', verbose_name='tags'), - ), - migrations.AlterField( - model_name='event', - name='created_by', - field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_events', to=settings.AUTH_USER_MODEL, verbose_name='créé par'), - ), - migrations.AlterField( - model_name='event', - name='slug', - field=models.SlugField(help_text="Seulement des lettres, des chiffres ou les caractères '_' ou '-'.", unique=True, verbose_name='identificateur'), - ), - migrations.AlterField( - model_name='place', - name='event', - field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), - ), - ]