Merge branch 'master' of git.eleves.ens.fr:cof-geek/GestionEvenementiel into Aufinal/permissions

This commit is contained in:
Ludovic Stephan 2017-08-17 14:23:13 +02:00
commit 64a979a4ac
30 changed files with 1970 additions and 252 deletions

0
api/__init__.py Normal file
View file

View file

@ -0,0 +1,7 @@
from equipment.models import Equipment
from api.event.serializers import EventSpecificSerializer
# TODO : le faire
class EquipmentSerializer(EventSpecificSerializer):
pass

0
api/event/__init__.py Normal file
View file

33
api/event/fields.py Normal file
View file

@ -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

92
api/event/serializers.py Normal file
View file

@ -0,0 +1,92 @@
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.ModelSerializer):
# TODO: Change this to a nested serializer ~(url, full_name) of User
created_by = serializers.ReadOnlyField(source='created_by.get_full_name')
class Meta:
model = Event
fields = (
'url', 'id', 'title', 'slug', 'created_by', 'created_at',
'description', 'beginning_date', 'ending_date',
)
# Serializers
# TODO rajouter des permissions
class PlaceSerializer(serializers.ModelSerializer):
serializer_url_field = EventHyperlinkedIdentityField
class Meta:
model = Place
fields = ('url', 'id', 'name', 'description', 'event')
# TODO rajouter des permissions
class ActivityTagSerializer(serializers.ModelSerializer):
serializer_url_field = EventHyperlinkedIdentityField
class Meta:
model = ActivityTag
fields = ('url', 'id', 'name', 'is_public', 'color', 'event')
# TODO rajouter des permissions
class ActivityTemplateSerializer(serializers.ModelSerializer):
tags = ActivityTagSerializer(many=True)
serializer_url_field = EventHyperlinkedIdentityField
class Meta:
model = ActivityTemplate
fields = (
'url', 'id', 'title', 'event', 'is_public', 'has_perm', 'min_perm',
'max_perm', 'description', 'remarks', 'tags',
)
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, _ = ActivityTag.objects.get_or_create(**tag_data, defaults={
'event': instance.event,
})
tags.append(tag)
instance.tags.add(*tags)
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')
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
class ActivitySerializer(serializers.ModelSerializer):
pass

556
api/event/tests.py Normal file
View file

@ -0,0 +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.test.testcases import ModelAPITestCaseMixin
from api.test.utils import json_format
User = get_user_model()
class EventAPITests(ModelAPITestCaseMixin, APITestCase):
model = Event
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 ActivityTemplateAPITests(ModelAPITestCaseMixin, APITestCase):
model = ActivityTemplate
url_label = 'activitytemplate'
list_ordering = 'title'
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
@property
def update_expected(self):
return {
**self.update_data,
'event': None,
}
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 BasePlaceAPITests:
model = 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,
}

121
api/event/views.py Normal file
View file

@ -0,0 +1,121 @@
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 event.models import Activity, ActivityTag, ActivityTemplate, Event, Place
from .serializers import (
ActivitySerializer, ActivityTagSerializer, ActivityTemplateSerializer,
EventSerializer, PlaceSerializer,
)
User = get_user_model()
# classes utilitaires
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
* 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 extends EventSpecificMixin
"""
def get_queryset(self):
"""
Warning : You may want to override this method
and not call with super
"""
queryset = super().get_queryset()
filters = Q(event=None)
if self.event:
filters |= Q(event=self.event)
return queryset.filter(filters)
# ViewSets
class EventViewSet(ModelViewSet):
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(EventSpecificModelViewSet):
queryset = Place.objects.all()
serializer_class = PlaceSerializer
filter_backends = (OrderingFilter,)
ordering_fields = ('name', )
ordering = ('name', )
class ActivityTagViewSet(EventSpecificModelViewSet):
queryset = ActivityTag.objects.all()
serializer_class = ActivityTagSerializer
filter_backends = (OrderingFilter,)
ordering_fields = ('is_public', 'name', )
ordering = ('is_public', 'name', )
class ActivityTemplateViewSet(EventModelViewSet):
queryset = ActivityTemplate.objects.all()
serializer_class = ActivityTemplateSerializer
filter_backends = (OrderingFilter,)
ordering_fields = ('title', )
ordering = ('title', )
class ActivityViewSet(EventModelViewSet):
queryset = Activity.objects.all()
serializer_class = ActivitySerializer
filter_backends = (OrderingFilter,)
ordering_fields = ('title', )
ordering = ('title', )

694
api/test/testcases.py Normal file
View file

@ -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

19
api/test/utils.py Normal file
View file

@ -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)

26
api/urls.py Normal file
View file

@ -0,0 +1,26 @@
from django.conf.urls import url, include
from rest_framework_nested.routers import SimpleRouter, NestedSimpleRouter
from api.event import views
router = SimpleRouter()
router.register(r'event', views.EventViewSet)
router.register(r'place', views.PlaceViewSet)
router.register(r'tag', views.ActivityTagViewSet)
# Views behind /event/<event_pk>/...
event_router = NestedSimpleRouter(router, r'event', lookup='event')
event_router.register(r'place', views.PlaceViewSet)
event_router.register(r'tag', views.ActivityTagViewSet)
event_router.register(r'template', views.ActivityTemplateViewSet)
# API URLconf: routers + auth for browsable API.
urlpatterns = [
url(r'^', include(router.urls)),
url(r'^', include(event_router.urls)),
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
]

0
api/users/__init__.py Normal file
View file

38
api/users/serializers.py Normal file
View file

@ -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',)

0
api/users/views.py Normal file
View file

View file

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-21 14:20 # Generated by Django 1.11.3 on 2017-08-17 12:21
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -13,8 +12,6 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('contenttypes', '0002_remove_content_type_name'), ('contenttypes', '0002_remove_content_type_name'),
('auth', '0008_alter_user_username_max_length'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
@ -23,8 +20,6 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()), ('object_id', models.PositiveIntegerField()),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
], ],
options={ options={
'verbose_name': 'souscription en groupe', 'verbose_name': 'souscription en groupe',
@ -37,18 +32,9 @@ class Migration(migrations.Migration):
('object_id', models.PositiveIntegerField()), ('object_id', models.PositiveIntegerField()),
('is_unsub', models.BooleanField(default=False, verbose_name='désinscription')), ('is_unsub', models.BooleanField(default=False, verbose_name='désinscription')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
options={ options={
'verbose_name': 'souscription utilisateur', 'verbose_name': 'souscription utilisateur',
}, },
), ),
migrations.AlterUniqueTogether(
name='usersubscription',
unique_together=set([('user', 'content_type', 'object_id')]),
),
migrations.AlterUniqueTogether(
name='groupsubscription',
unique_together=set([('group', 'content_type', 'object_id')]),
),
] ]

View file

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-08-17 12:21
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('communication', '0001_initial'),
('auth', '0008_alter_user_username_max_length'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='usersubscription',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='groupsubscription',
name='content_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
),
migrations.AddField(
model_name='groupsubscription',
name='group',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group'),
),
migrations.AlterUniqueTogether(
name='usersubscription',
unique_together=set([('user', 'content_type', 'object_id')]),
),
migrations.AlterUniqueTogether(
name='groupsubscription',
unique_together=set([('group', 'content_type', 'object_id')]),
),
]

View file

@ -26,7 +26,7 @@ class SubscriptionTest(TestCase):
title='TestEvent', title='TestEvent',
slug='test', slug='test',
created_by=cls.root, created_by=cls.root,
creation_date=timezone.now(), created_at=timezone.now(),
description="Ceci est un test", description="Ceci est un test",
beginning_date=timezone.now() beginning_date=timezone.now()
+ timedelta(days=30), + timedelta(days=30),

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-21 14:20 # Generated by Django 1.11.3 on 2017-08-17 12:21
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@ -11,7 +11,6 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('event', '0001_initial'),
] ]
operations = [ operations = [
@ -34,8 +33,6 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.PositiveSmallIntegerField(verbose_name='quantité attribuée')), ('amount', models.PositiveSmallIntegerField(verbose_name='quantité attribuée')),
('remarks', models.TextField(verbose_name="remarques concernant l'attribution")), ('remarks', models.TextField(verbose_name="remarques concernant l'attribution")),
('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Activity')),
('equipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.Equipment')),
], ],
options={ options={
'verbose_name': 'attribution de matériel', 'verbose_name': 'attribution de matériel',
@ -57,14 +54,4 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'remarques sur le matériel', 'verbose_name_plural': 'remarques sur le matériel',
}, },
), ),
migrations.AddField(
model_name='equipment',
name='activities',
field=models.ManyToManyField(related_name='equipment', through='equipment.EquipmentAttribution', to='event.Activity'),
),
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'),
),
] ]

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-08-17 12:21
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('equipment', '0001_initial'),
('event', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='equipmentattribution',
name='activity',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Activity'),
),
migrations.AddField(
model_name='equipmentattribution',
name='equipment',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.Equipment'),
),
migrations.AddField(
model_name='equipment',
name='activities',
field=models.ManyToManyField(related_name='equipment', through='equipment.EquipmentAttribution', to='event.Activity'),
),
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'),
),
]

View file

@ -56,9 +56,11 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'channels', 'channels',
'rest_framework',
'bootstrapform', 'bootstrapform',
'widget_tweaks', 'widget_tweaks',
'guardian', 'guardian',
'api',
] ]
MIDDLEWARE_CLASSES = [ MIDDLEWARE_CLASSES = [
@ -72,6 +74,14 @@ MIDDLEWARE_CLASSES = [
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
'PAGE_SIZE': 10,
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
}
ROOT_URLCONF = 'evenementiel.urls' ROOT_URLCONF = 'evenementiel.urls'
STATIC_URL = "/static/" STATIC_URL = "/static/"
@ -80,6 +90,8 @@ MEDIA_URL = "/media/"
LOGIN_REDIRECT_URL = 'shared:home' LOGIN_REDIRECT_URL = 'shared:home'
LOGOUT_REDIRECT_URL = 'shared:home' LOGOUT_REDIRECT_URL = 'shared:home'
AUTH_USER_MODEL = "users.User"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',

View file

@ -9,6 +9,7 @@ urlpatterns = [
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^event/', include('event.urls')), url(r'^event/', include('event.urls')),
url(r'^user/', include('users.urls')), url(r'^user/', include('users.urls')),
url(r'^api/', include('api.urls')),
url(r'^', include('shared.urls')), url(r'^', include('shared.urls')),
] ]

View file

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-21 14:20 # Generated by Django 1.11.3 on 2017-08-17 12:21
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -13,7 +12,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('auth', '0008_alter_user_username_max_length'),
] ]
operations = [ operations = [
@ -22,12 +21,12 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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é")), ('title', models.CharField(blank=True, max_length=200, null=True, verbose_name="nom de l'activité")),
('is_public', models.NullBooleanField()), ('is_public', models.NullBooleanField(verbose_name='est public')),
('has_perm', models.NullBooleanField()), ('has_perm', models.NullBooleanField(verbose_name='inscription de permanents')),
('min_perm', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='nombre minimum 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')), ('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')), ('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')), ('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')), ('beginning', models.DateTimeField(verbose_name='heure de début')),
('end', models.DateTimeField(verbose_name='heure de fin')), ('end', models.DateTimeField(verbose_name='heure de fin')),
], ],
@ -41,8 +40,8 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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')), ('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")), ('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', 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')), ('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={ options={
'verbose_name': 'tag', 'verbose_name': 'tag',
@ -54,12 +53,12 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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é")), ('title', models.CharField(blank=True, max_length=200, null=True, verbose_name="nom de l'activité")),
('is_public', models.NullBooleanField()), ('is_public', models.NullBooleanField(verbose_name='est public')),
('has_perm', models.NullBooleanField()), ('has_perm', models.NullBooleanField(verbose_name='inscription de permanents')),
('min_perm', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='nombre minimum 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')), ('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')), ('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')), ('remarks', models.TextField(blank=True, help_text='Visible uniquement par les organisateurs.', null=True, verbose_name='remarques')),
], ],
options={ options={
'verbose_name': 'template activité', 'verbose_name': 'template activité',
@ -71,74 +70,39 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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")), ('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')), ('slug', models.SlugField(help_text="Seulement des lettres, des chiffres ou les caractères '_' ou '-'.", unique=True, verbose_name='identificateur')),
('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='date de création')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='date de création')),
('description', models.TextField(verbose_name='description')), ('description', models.TextField(verbose_name='description')),
('beginning_date', models.DateTimeField(verbose_name='date de début')), ('beginning_date', models.DateTimeField(help_text="date publique de l'évènement", verbose_name='date de début')),
('ending_date', models.DateTimeField(verbose_name='date de fin')), ('ending_date', models.DateTimeField(help_text="date publique de l'évènement", 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)),
], ],
options={ options={
'verbose_name': 'évènement', 'verbose_name': 'évènement',
'verbose_name_plural': 'évènements', 'verbose_name_plural': 'évènements',
}, },
), ),
migrations.CreateModel(
name='EventGroup',
fields=[
('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.Group')),
('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={
'abstract': False,
},
bases=('auth.group', models.Model),
),
migrations.CreateModel( migrations.CreateModel(
name='Place', name='Place',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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')), ('name', models.CharField(max_length=200, verbose_name='nom du lieu')),
('description', models.TextField(blank=True)), ('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={ options={
'verbose_name': 'lieu', 'verbose_name': 'lieu',
'verbose_name_plural': 'lieux', 'verbose_name_plural': 'lieux',
}, },
), ),
migrations.AddField(
model_name='activitytemplate',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Event'),
),
migrations.AddField(
model_name='activitytemplate',
name='place',
field=models.ManyToManyField(blank=True, to='event.Place'),
),
migrations.AddField(
model_name='activitytemplate',
name='tags',
field=models.ManyToManyField(blank=True, to='event.ActivityTag'),
),
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'),
),
migrations.AddField(
model_name='activity',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Event'),
),
migrations.AddField(
model_name='activity',
name='parent',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='children', to='event.ActivityTemplate'),
),
migrations.AddField(
model_name='activity',
name='place',
field=models.ManyToManyField(blank=True, to='event.Place'),
),
migrations.AddField(
model_name='activity',
name='staff',
field=models.ManyToManyField(blank=True, related_name='in_perm_activities', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='activity',
name='tags',
field=models.ManyToManyField(blank=True, to='event.ActivityTag'),
),
] ]

View file

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-08-17 12:21
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('event', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
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.AddField(
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.AddField(
model_name='activitytemplate',
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', 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'),
),
migrations.AddField(
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.AddField(
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.AddField(
model_name='activity',
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, verbose_name='permanents'),
),
migrations.AddField(
model_name='activity',
name='tags',
field=models.ManyToManyField(blank=True, to='event.ActivityTag', verbose_name='tags'),
),
]

View file

@ -1,34 +1,48 @@
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.contrib.auth.models import Group
from django.core.validators import RegexValidator from django.core.exceptions import FieldDoesNotExist, FieldError
from django.core.exceptions import FieldError
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _
from communication.models import SubscriptionMixin from communication.models import SubscriptionMixin
from .validators import ColorValidator
User = get_user_model()
class Event(SubscriptionMixin, models.Model): class Event(SubscriptionMixin, models.Model):
title = models.CharField( title = models.CharField(
_("nom de l'évènement"), _("nom de l'évènement"),
max_length=200, max_length=200,
) )
slug = models.SlugField( slug = models.SlugField(
_('identificateur'), _("identificateur"),
unique=True, unique=True,
help_text=_("Seulement des lettres, des chiffres ou" help_text=_(
"les caractères '_' ou '-'."), "Seulement des lettres, des chiffres ou les caractères '_' ou '-'."
) ),
)
created_by = models.ForeignKey( created_by = models.ForeignKey(
User, User,
related_name="created_events", verbose_name=_("créé par"),
editable=False, on_delete=models.SET_NULL,
) related_name="created_events",
creation_date = models.DateTimeField( editable=False, null=True,
)
created_at = models.DateTimeField(
_('date de création'), _('date de création'),
auto_now_add=True, auto_now_add=True,
) )
description = models.TextField(_('description')) description = models.TextField(_('description'))
beginning_date = models.DateTimeField(_('date de début')) beginning_date = models.DateTimeField(
ending_date = models.DateTimeField(_('date de fin')) _('date de début'),
help_text=_("date publique de l'évènement"),
)
ending_date = models.DateTimeField(
_('date de fin'),
help_text=_("date publique de l'évènement"),
)
class Meta: class Meta:
verbose_name = _("évènement") verbose_name = _("évènement")
@ -43,12 +57,14 @@ class EventSpecificMixin(models.Model):
or not (depending on whether the event field is null)""" or not (depending on whether the event field is null)"""
event = models.ForeignKey( event = models.ForeignKey(
'event.Event', Event,
verbose_name=_("évènement"), verbose_name=_("évènement"),
help_text=_("Si spécifié, l'instance du modèle " help_text=_(
"est spécifique à l'évènement en question"), "Si spécifié, l'instance du modèle est spécifique à l'évènement "
blank=True, "en question."
null=True ),
on_delete=models.CASCADE,
blank=True, null=True,
) )
needs_event_permissions = True needs_event_permissions = True
@ -56,11 +72,15 @@ class EventSpecificMixin(models.Model):
abstract = True abstract = True
class EventGroup(EventSpecificMixin, Group):
pass
class Place(EventSpecificMixin, models.Model): class Place(EventSpecificMixin, models.Model):
name = models.CharField( name = models.CharField(
_("nom du lieu"), _("nom du lieu"),
max_length=200, max_length=200,
) )
description = models.TextField(blank=True) description = models.TextField(blank=True)
class Meta: class Meta:
@ -73,26 +93,22 @@ class Place(EventSpecificMixin, models.Model):
class ActivityTag(EventSpecificMixin, models.Model): class ActivityTag(EventSpecificMixin, models.Model):
name = models.CharField( name = models.CharField(
_("nom du tag"), _("nom du tag"),
max_length=200, max_length=200,
) )
is_public = models.BooleanField( is_public = models.BooleanField(
help_text=_("Sert à faire une distinction dans" _("est public"),
" l'affichage selon que cela soit" help_text=_(
" destiné au public ou à l'équipe" "Sert à faire une distinction dans l'affichage selon que le tag "
" organisatrice"), "soit destiné au public ou à l'organisation."
) ),
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."),
)
color = models.CharField( color = models.CharField(
_('couleur'), _('couleur'),
max_length=7, max_length=7,
validators=[color_regex], validators=[ColorValidator],
help_text=_("Rentrer une couleur en hexadécimal"), help_text=_("Rentrer une couleur en hexadécimal (#XXX ou #XXXXXX)."),
) )
class Meta: class Meta:
verbose_name = _("tag") verbose_name = _("tag")
@ -104,49 +120,53 @@ class ActivityTag(EventSpecificMixin, models.Model):
class AbstractActivityTemplate(SubscriptionMixin, models.Model): class AbstractActivityTemplate(SubscriptionMixin, models.Model):
title = models.CharField( title = models.CharField(
_("nom de l'activité"), _("nom de l'activité"),
max_length=200, max_length=200,
blank=True, blank=True, null=True,
null=True, )
)
# FIXME: voir comment on traite l'héritage de `event` # 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( is_public = models.NullBooleanField(
blank=True, _("est public"),
) blank=True,
)
has_perm = models.NullBooleanField( has_perm = models.NullBooleanField(
blank=True, _("inscription de permanents"),
) blank=True,
)
min_perm = models.PositiveSmallIntegerField( min_perm = models.PositiveSmallIntegerField(
_('nombre minimum de permanents'), _('nombre minimum de permanents'),
blank=True, blank=True, null=True,
null=True, )
)
max_perm = models.PositiveSmallIntegerField( max_perm = models.PositiveSmallIntegerField(
_('nombre maximum de permanents'), _('nombre maximum de permanents'),
blank=True, blank=True, null=True,
null=True, )
)
description = models.TextField( description = models.TextField(
_('description'), _('description'),
help_text=_("Public, Visible par tout le monde."), help_text=_("Visible par tout le monde si l'événément est public."),
blank=True, blank=True, null=True,
null=True, )
)
remarks = models.TextField( remarks = models.TextField(
_('remarques'), _('remarques'),
help_text=_("Visible uniquement par les organisateurs"), help_text=_("Visible uniquement par les organisateurs."),
blank=True, blank=True, null=True,
null=True, )
)
tags = models.ManyToManyField( tags = models.ManyToManyField(
ActivityTag, ActivityTag,
blank=True, verbose_name=_('tags'),
) blank=True,
place = models.ManyToManyField( )
Place, places = models.ManyToManyField(
blank=True, Place,
) verbose_name=_('lieux'),
blank=True,
)
needs_event_permissions = True needs_event_permissions = True
class Meta: class Meta:
@ -165,37 +185,42 @@ class ActivityTemplate(AbstractActivityTemplate):
class Activity(AbstractActivityTemplate): class Activity(AbstractActivityTemplate):
parent = models.ForeignKey( parent = models.ForeignKey(
ActivityTemplate, ActivityTemplate,
related_name="children", verbose_name=_("template"),
) on_delete=models.PROTECT,
related_name="children",
blank=True, null=True,
)
staff = models.ManyToManyField( staff = models.ManyToManyField(
User, User,
related_name="in_perm_activities", verbose_name=_("permanents"),
blank=True, related_name="in_perm_activities",
) blank=True,
)
beginning = models.DateTimeField(_("heure de début")) beginning = models.DateTimeField(_("heure de début"))
end = models.DateTimeField(_("heure de fin")) end = models.DateTimeField(_("heure de fin"))
def get_herited(self, attrname): def get_herited(self, attrname):
inherited_fields = [f.name for f in ActivityTemplate._meta.get_fields()] try:
m2m_fields = [f.name for f in ActivityTemplate._meta.get_fields() tpl_field = ActivityTemplate._meta.get_field(attrname)
if f.many_to_many] except FieldDoesNotExist:
attr = getattr(self, attrname)
if attrname not in inherited_fields:
raise FieldError( raise FieldError(
_("%(attrname)s n'est pas un champ héritable"), "%(attrname)s field can't be herited.",
params={'attrname': attrname}, params={'attrname': attrname},
) )
elif attrname in m2m_fields:
if attr.exists(): value = getattr(self, attrname)
return attr
if tpl_field.many_to_many:
if value.exists():
return value
else: else:
return getattr(self.parent, attrname) return getattr(self.parent, attrname)
elif attr is None: elif value is None:
return getattr(self.parent, attrname) return getattr(self.parent, attrname)
else: else:
return attr return value
class Meta: class Meta:
verbose_name = _("activité") verbose_name = _("activité")

View file

@ -3,8 +3,7 @@ from django.db.models.signals import post_save, post_migrate
from django.apps import apps from django.apps import apps
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from event.models import Event from event.models import Event, EventGroup
from users.models import GEGroup
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
@ -12,7 +11,7 @@ from guardian.shortcuts import assign_perm
def create_groups_for_event(sender, **kwargs): def create_groups_for_event(sender, **kwargs):
event, created = kwargs["instance"], kwargs["created"] event, created = kwargs["instance"], kwargs["created"]
if created: if created:
orgas = GEGroup.objects.create( orgas = EventGroup.objects.create(
name="orga", name="orga",
event=event event=event
) )
@ -22,7 +21,7 @@ def create_groups_for_event(sender, **kwargs):
codename__contains="event_"): codename__contains="event_"):
assign_perm(perm.codename, orgas, event) assign_perm(perm.codename, orgas, event)
GEGroup.objects.create( EventGroup.objects.create(
name="participants", name="participants",
event=event, event=event,
) )

View file

@ -22,7 +22,7 @@ class ActivityInheritanceTest(TestCase):
title='La Nuit 2042', title='La Nuit 2042',
slug='nuit42', slug='nuit42',
created_by=cls.erkan, created_by=cls.erkan,
creation_date=timezone.now(), created_at=timezone.now(),
description="La nuit c'est lol", description="La nuit c'est lol",
beginning_date=timezone.now() beginning_date=timezone.now()
+ timedelta(days=30), + timedelta(days=30),
@ -97,14 +97,14 @@ class ActivityInheritanceTest(TestCase):
self.assertEqual(self.real_act.get_herited('max_perm'), 1) self.assertEqual(self.real_act.get_herited('max_perm'), 1)
def test_inherits_place(self): def test_inherits_place(self):
self.template_act.place.add(self.loge) self.template_act.places.add(self.loge)
self.assertEqual( self.assertEqual(
self.real_act.get_herited('place').get(), self.real_act.get_herited('places').get(),
self.loge self.loge
) )
self.real_act.place.add(self.aqua) self.real_act.places.add(self.aqua)
self.assertEqual( self.assertEqual(
self.real_act.get_herited('place').get(), self.real_act.get_herited('places').get(),
self.aqua self.aqua
) )

10
event/validators.py Normal file
View file

@ -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."
),
)

View file

@ -5,6 +5,8 @@ Pillow
channels channels
django-bootstrap-form==3.2.1 django-bootstrap-form==3.2.1
django-widget-tweaks django-widget-tweaks
djangorestframework==3.6.3
drf-nested-routers==0.90.0
django-notifications django-notifications
django-contrib-comments django-contrib-comments
django-guardian django-guardian

View file

@ -1,9 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-21 18:29 # Generated by Django 1.11.3 on 2017-08-17 12:21
from __future__ import unicode_literals from __future__ import unicode_literals
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -11,20 +13,34 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('event', '0001_initial'),
('auth', '0008_alter_user_username_max_length'), ('auth', '0008_alter_user_username_max_length'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='GEGroup', name='User',
fields=[ fields=[
('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.Group')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')), ('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={ options={
'abstract': False, 'verbose_name': 'utilisateur',
'verbose_name_plural': 'utilisateurs',
}, },
bases=('auth.group', models.Model), managers=[
('objects', django.contrib.auth.models.UserManager()),
],
), ),
] ]

View file

@ -1,10 +1,20 @@
from django.db import models from django.db import models
from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.models import Group from django.utils.translation import ugettext_lazy as _
from event.models import EventSpecificMixin
User = get_user_model() 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")
class GEGroup(EventSpecificMixin, Group):
pass

View file

@ -1,34 +0,0 @@
from django.test import TestCase, Client
from django.conf import settings
from django.contrib.auth.models import User
class TestUserCreation(TestCase):
def test_create_view(self):
"""Create a user using the user creation form"""
user_data = {
"username": "MrsFoobar",
"first_name": "Baz",
"last_name": "Foobar",
"email": "baz@foobar.net",
}
data = user_data.copy()
data["password1"] = "4zwY5jdI"
data["password2"] = "4zwY5jdI"
data["key"] = settings.CREATE_USER_KEY
client = Client()
resp = client.post("/user/create/", data)
# The user redirection means successful form validation
self.assertRedirects(resp, "/")
# The user should now exist
user = (
User.objects
.filter(username=data["username"])
.values(*user_data.keys())
.get()
)
self.assertEqual(user_data, user)