Compare commits
5 commits
master
...
Qwann/Seri
Author | SHA1 | Date | |
---|---|---|---|
|
8568199de4 | ||
|
8d29049989 | ||
|
6bb176be01 | ||
|
2412c8344f | ||
|
26a62ea30b |
198 changed files with 9705 additions and 11294 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,8 +1,7 @@
|
|||
.vagrant/
|
||||
__pycache__
|
||||
venv
|
||||
poulpe/settings.py
|
||||
evenementiel/settings.py
|
||||
.*.swp
|
||||
*.pyc
|
||||
*.sqlite3
|
||||
*.scssc
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
class APIConfig(AppConfig):
|
||||
name = 'api'
|
||||
|
|
|
@ -2,6 +2,6 @@ from equipment.models import Equipment
|
|||
from api.event.serializers import EventSpecificSerializer
|
||||
|
||||
|
||||
# TODO : le faire
|
||||
class EquipmentSerializer(EventSpecificSerializer):
|
||||
pass
|
||||
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
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
|
5
api/event/mixins.py
Normal file
5
api/event/mixins.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
class EventNestedMixin(object):
|
||||
pass
|
||||
|
||||
class ventSpecificSerializerMixin(object):
|
||||
pass
|
|
@ -1,40 +1,65 @@
|
|||
from django.db import transaction
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from event.models import Event, ActivityTag, Place, ActivityTemplate
|
||||
from event.models import Event, ActivityTag, Place, ActivityTemplate, Activity
|
||||
from api.users.serializers import UserSerializer
|
||||
|
||||
from .fields import EventHyperlinkedIdentityField
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
# Event Serializer
|
||||
class EventSerializer(serializers.ModelSerializer):
|
||||
# TODO: Change this to a nested serializer ~(url, full_name) of User
|
||||
class EventSerializer(serializers.HyperlinkedModelSerializer):
|
||||
created_by = serializers.ReadOnlyField(source='created_by.get_full_name')
|
||||
creation_date = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = (
|
||||
'url', 'id', 'title', 'slug', 'created_by', 'created_at',
|
||||
'description', 'beginning_date', 'ending_date',
|
||||
)
|
||||
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 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
|
||||
|
||||
|
||||
# Serializers
|
||||
# TODO rajouter des permissions
|
||||
class PlaceSerializer(serializers.ModelSerializer):
|
||||
serializer_url_field = EventHyperlinkedIdentityField
|
||||
|
||||
class PlaceSerializer(EventSpecificSerializer):
|
||||
class Meta:
|
||||
model = Place
|
||||
fields = ('url', 'id', 'name', 'description', 'event')
|
||||
|
||||
|
||||
# TODO rajouter des permissions
|
||||
class ActivityTagSerializer(serializers.ModelSerializer):
|
||||
serializer_url_field = EventHyperlinkedIdentityField
|
||||
|
||||
class ActivityTagSerializer(EventSpecificSerializer):
|
||||
class Meta:
|
||||
model = ActivityTag
|
||||
fields = ('url', 'id', 'name', 'is_public', 'color', 'event')
|
||||
|
@ -42,40 +67,13 @@ class ActivityTagSerializer(serializers.ModelSerializer):
|
|||
|
||||
# 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 = (
|
||||
'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
|
||||
fields = ('id', 'title', 'event', 'is_public', 'has_perm',
|
||||
'min_perm', 'max_perm', 'description', 'remarks', 'tags',)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""
|
||||
|
@ -83,10 +81,74 @@ class ActivityTemplateSerializer(serializers.ModelSerializer):
|
|||
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)
|
||||
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
|
||||
return activity_template
|
||||
|
||||
|
||||
# TODO rajouter des permissions
|
||||
class ActivitySerializer(serializers.ModelSerializer):
|
||||
pass
|
||||
event = EventSerializer(read_only=True)
|
||||
parent = serializers.PrimaryKeyRelatedField(
|
||||
queryset=ActivityTemplate.objects.all(), allow_null=True)
|
||||
en_perm = UserSerializer(read_only=True)
|
||||
tags = ActivityTagSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Activity
|
||||
fields = ('id', 'title', 'event', 'parent', 'is_public', 'has_perm',
|
||||
'min_perm', 'max_perm', 'en_perm', 'description', 'remarks',
|
||||
'tags', 'beginning', 'end', )
|
||||
|
||||
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(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')
|
||||
event_pk = validated_data.pop('event_pk')
|
||||
event = event_pk and get_object_or_404(Event, id=event_pk) or None
|
||||
activity = Activity.objects.create(event=event, **validated_data)
|
||||
tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0]
|
||||
for tag_data in tags_data]
|
||||
activity.tags = tags
|
||||
return activity
|
||||
|
|
|
@ -1,556 +0,0 @@
|
|||
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,
|
||||
}
|
|
@ -1,54 +1,18 @@
|
|||
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,
|
||||
)
|
||||
from api.event.serializers import EventSerializer, PlaceSerializer,\
|
||||
ActivityTagSerializer, ActivityTemplateSerializer, ActivitySerializer
|
||||
from event.models import Event, Place, ActivityTag, ActivityTemplate, Activity
|
||||
|
||||
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):
|
||||
class EventSpecificViewSet(ModelViewSet):
|
||||
"""
|
||||
ViewSet that returns :
|
||||
* rootlevel objects if no Event is specified
|
||||
|
@ -58,21 +22,33 @@ class EventSpecificModelViewSet(EventModelViewSet):
|
|||
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()
|
||||
filters = Q(event=None)
|
||||
if self.event:
|
||||
filters |= Q(event=self.event)
|
||||
return queryset.filter(filters)
|
||||
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)
|
||||
|
||||
|
||||
# ViewSets
|
||||
class EventViewSet(ModelViewSet):
|
||||
"""
|
||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||
`update` and `destroy` actions.
|
||||
|
||||
"""
|
||||
queryset = Event.objects.all()
|
||||
serializer_class = EventSerializer
|
||||
|
||||
|
@ -85,7 +61,7 @@ class EventViewSet(ModelViewSet):
|
|||
serializer.save(created_by=self.request.user)
|
||||
|
||||
|
||||
class PlaceViewSet(EventSpecificModelViewSet):
|
||||
class PlaceViewSet(EventSpecificViewSet):
|
||||
queryset = Place.objects.all()
|
||||
serializer_class = PlaceSerializer
|
||||
|
||||
|
@ -94,28 +70,54 @@ class PlaceViewSet(EventSpecificModelViewSet):
|
|||
ordering = ('name', )
|
||||
|
||||
|
||||
class ActivityTagViewSet(EventSpecificModelViewSet):
|
||||
class ActivityTagViewSet(EventSpecificViewSet):
|
||||
queryset = ActivityTag.objects.all()
|
||||
serializer_class = ActivityTagSerializer
|
||||
|
||||
filter_backends = (OrderingFilter,)
|
||||
ordering_fields = ('is_public', 'name', )
|
||||
ordering = ('is_public', 'name', )
|
||||
ordering_fields = ('name', )
|
||||
ordering = ('name', )
|
||||
|
||||
|
||||
class ActivityTemplateViewSet(EventModelViewSet):
|
||||
class ActivityTemplateViewSet(ModelViewSet):
|
||||
"""
|
||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||
`update` and `destroy` actions.
|
||||
|
||||
"""
|
||||
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(EventModelViewSet):
|
||||
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)
|
||||
|
||||
filter_backends = (OrderingFilter,)
|
||||
ordering_fields = ('title', )
|
||||
ordering = ('title', )
|
||||
|
|
|
@ -1,694 +0,0 @@
|
|||
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
|
|
@ -1,19 +0,0 @@
|
|||
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)
|
77
api/test_event.py
Normal file
77
api/test_event.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from event.models import Event, Place, ActivityTag, ActivityTemplate
|
||||
|
||||
from api.event.serializers import ActivityTemplateSerializer,\
|
||||
EventSerializer, Activity
|
||||
from api.test_mixins import EventBasedModelTestMixin, EventSpecificTestMixin,\
|
||||
ModelTestMixin
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class EventTests(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 ActivityTemplateTests(EventBasedModelTestMixin, APITestCase):
|
||||
model = ActivityTemplate
|
||||
base_name = 'event-activitytemplate'
|
||||
# Creation
|
||||
data_creation = 'act_temp2_data'
|
||||
# Update/Delete
|
||||
instance_name = 'act_temp1'
|
||||
field_tested = 'title'
|
||||
serializer = ActivityTemplateSerializer
|
||||
|
||||
def test_create_extra(self):
|
||||
self.assertEqual(self.model.objects.get(id=1).tags.count(), 1)
|
||||
|
||||
def pre_update_extra(self, data):
|
||||
data['tags'].append(self.tag2_data)
|
||||
return data
|
||||
|
||||
def post_update_extra(self, instance):
|
||||
self.assertEqual(instance.tags.count(), 2)
|
||||
|
||||
|
||||
class ActivityTests(EventBasedModelTestMixin, APITestCase):
|
||||
model = Activity
|
||||
base_name = 'event-activity'
|
||||
# Creation
|
||||
data_creation = 'act2_data'
|
||||
# Update/Delete
|
||||
instance_name = 'act1'
|
||||
field_tested = 'title'
|
||||
serializer = ActivityTemplateSerializer
|
||||
|
||||
def test_create_extra(self):
|
||||
self.assertEqual(self.model.objects.get(id=1).tags.count(), 1)
|
||||
|
||||
def pre_update_extra(self, data):
|
||||
data['tags'].append(self.tag2_data)
|
||||
return data
|
||||
|
||||
def post_update_extra(self, instance):
|
||||
self.assertEqual(instance.tags.count(), 2)
|
||||
|
||||
|
||||
class EventSpecficTagTests(EventSpecificTestMixin, APITestCase):
|
||||
model = ActivityTag
|
||||
root_base_name = 'activitytag'
|
||||
event_base_name = 'event-activitytag'
|
||||
|
||||
|
||||
class EventSpecficPlaceTests(EventSpecificTestMixin, APITestCase):
|
||||
model = Place
|
||||
root_base_name = 'place'
|
||||
event_base_name = 'event-place'
|
303
api/test_mixins.py
Normal file
303
api/test_mixins.py
Normal file
|
@ -0,0 +1,303 @@
|
|||
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, Activity
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class DataBaseMixin():
|
||||
"""
|
||||
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)
|
||||
|
||||
# Actitvity
|
||||
self.act1_data = {'title': "act1", 'is_public': True,
|
||||
'beginning': timezone.now(),
|
||||
'end': timezone.now()+timedelta(hours=2),
|
||||
'remarks': "test remark", 'event': self.event1}
|
||||
self.act2_data = {'title': "act2", 'is_public': False,
|
||||
"beginning": "2017-07-18T18:05:00Z",
|
||||
"end": "2017-07-19T18:05:00Z",
|
||||
'parent': None,
|
||||
'remarks': "test remark", 'tags': []}
|
||||
self.act1 = Activity.objects.create(**self.act1_data)
|
||||
self.act1.tags.add(self.tag1)
|
||||
|
||||
|
||||
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/<event_id>/%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)
|
||||
initial_count = self.model.objects.count()
|
||||
|
||||
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,
|
||||
msg=response.content)
|
||||
self.assertEqual(self.model.objects.count(), initial_count + 1)
|
||||
self.assertEqual(self.model.objects.get(id=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,
|
||||
msg=response.content)
|
||||
|
||||
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)
|
||||
initial_count = self.model.objects.count()
|
||||
|
||||
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,
|
||||
msg=response.content)
|
||||
self.assertEqual(self.model.objects.count(), 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():
|
||||
"""
|
||||
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):
|
||||
# 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_id>/%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,
|
||||
msg=response.content)
|
||||
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,
|
||||
msg=response.content)
|
||||
|
||||
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,
|
||||
msg=response.content)
|
||||
self.assertEqual(self.model.objects.count(), initial_count - 1)
|
31
api/urls.py
31
api/urls.py
|
@ -1,26 +1,29 @@
|
|||
from django.conf.urls import url, include
|
||||
|
||||
from rest_framework_nested.routers import SimpleRouter, NestedSimpleRouter
|
||||
from api.event.views import EventViewSet, PlaceViewSet, ActivityTagViewSet,\
|
||||
ActivityTemplateViewSet, ActivityViewSet
|
||||
|
||||
from api.event import views
|
||||
|
||||
|
||||
# Create a router and register our viewsets with it.
|
||||
router = SimpleRouter()
|
||||
router.register(r'event', views.EventViewSet)
|
||||
router.register(r'place', views.PlaceViewSet)
|
||||
router.register(r'tag', views.ActivityTagViewSet)
|
||||
router.register(r'event', EventViewSet, 'event')
|
||||
router.register(r'place', PlaceViewSet, 'place')
|
||||
router.register(r'activitytag', ActivityTagViewSet, 'activitytag')
|
||||
|
||||
|
||||
# Views behind /event/<event_pk>/...
|
||||
# Register nested router and register someviewsets vith it
|
||||
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)
|
||||
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'activity', ActivityViewSet, base_name='event-activity')
|
||||
|
||||
|
||||
# API URLconf: routers + auth for browsable API.
|
||||
# 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'^auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
url(r'^api-auth/', include('rest_framework.urls',
|
||||
namespace='rest_framework'))
|
||||
]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
class CommunicationConfig(AppConfig):
|
||||
name = 'communication'
|
||||
verbose_name = _("Communication")
|
||||
verbose_name = _("communication")
|
||||
|
|
|
@ -20,10 +20,7 @@ class Subscription(models.Model):
|
|||
|
||||
|
||||
class UserSubscription(Subscription):
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
user = models.ForeignKey(User)
|
||||
is_unsub = models.BooleanField(
|
||||
_("désinscription"),
|
||||
default=False
|
||||
|
@ -35,10 +32,7 @@ class UserSubscription(Subscription):
|
|||
|
||||
|
||||
class GroupSubscription(Subscription):
|
||||
group = models.ForeignKey(
|
||||
Group,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
group = models.ForeignKey(Group)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("souscription en groupe")
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from django import forms
|
||||
|
||||
from .models import Equipment, EquipmentDefault, EquipmentRevision, EquipmentCategory, EquipmentLost, EquipmentAttributeValue, EquipmentAttribute, EquipmentStorage
|
||||
from .fields import IdField, IdWidget
|
||||
|
||||
|
||||
class IdForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'min_value' in kwargs:
|
||||
kwargs.pop('min_value')
|
||||
if 'num_choices' in kwargs:
|
||||
num_choices = kwargs.pop('num_choices')
|
||||
else:
|
||||
num_choices = None
|
||||
super(IdForm, self).__init__(*args, **kwargs)
|
||||
|
||||
for field in self.instance._meta.fields:
|
||||
if isinstance(field, IdField):
|
||||
if num_choices is None:
|
||||
choices = []
|
||||
else:
|
||||
choices = [(k, str(k)) for k in range(1, num_choices+1)]
|
||||
self.fields[field.name].choices = choices
|
||||
self.fields[field.name].widget = IdWidget(choices=self.fields[field.name].choices)
|
||||
|
||||
|
||||
class IdFormset(forms.models.BaseInlineFormSet):
|
||||
def get_form_kwargs(self, index):
|
||||
kwargs = super().get_form_kwargs(index)
|
||||
if self.instance:
|
||||
kwargs["num_choices"] = self.instance.stock
|
||||
return kwargs
|
||||
|
||||
|
||||
class EquipmentRevisionExtraInline(admin.TabularInline):
|
||||
model = EquipmentRevision
|
||||
extra = 0
|
||||
form = IdForm
|
||||
formset = IdFormset
|
||||
classes = ['collapse']
|
||||
|
||||
|
||||
class EquipmentDefaultExtraInline(admin.TabularInline):
|
||||
model = EquipmentDefault
|
||||
extra = 0
|
||||
form = IdForm
|
||||
formset = IdFormset
|
||||
classes = ['collapse']
|
||||
|
||||
|
||||
class EquipmentLostExtraInline(admin.TabularInline):
|
||||
model = EquipmentLost
|
||||
extra = 0
|
||||
form = IdForm
|
||||
formset = IdFormset
|
||||
classes = ['collapse']
|
||||
|
||||
|
||||
class EquipmentAttributeValueInline(admin.TabularInline):
|
||||
model = EquipmentAttributeValue
|
||||
extra = 0
|
||||
|
||||
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ['full_name_p']
|
||||
list_display = ['name', 'parent', "full_name_p"]
|
||||
ordering = ['name', 'parent']
|
||||
search_fields = ['name',]
|
||||
autocomplete_fields = ['parent', ]
|
||||
|
||||
class StorageAdmin(admin.ModelAdmin):
|
||||
list_display = ['name']
|
||||
ordering = ['name']
|
||||
search_fields = ['name']
|
||||
|
||||
|
||||
class EquipmentAttributeAdmin(admin.ModelAdmin):
|
||||
list_display = ['name']
|
||||
ordering = ['name']
|
||||
search_fields = ['name',]
|
||||
|
||||
|
||||
class EquipmentAdmin(admin.ModelAdmin):
|
||||
save_as_continue = True
|
||||
save_on_top = True
|
||||
autocomplete_fields = ['category', 'storage', ]
|
||||
readonly_fields = ['full_category_p',
|
||||
'added_at',
|
||||
'modified_at',
|
||||
'stock_aviable_p',
|
||||
'ids_aviable_p',
|
||||
'stock_lost_p',
|
||||
'ids_lost_p',
|
||||
]
|
||||
list_display = ['name', 'stock', 'owner', 'category', 'storage', 'modified_at']
|
||||
fieldsets = (
|
||||
('Général', {
|
||||
'fields': ('name', 'owner', 'stock', )
|
||||
}),
|
||||
('Info stock',
|
||||
{
|
||||
'fields': (
|
||||
'stock_aviable_p',
|
||||
'ids_aviable_p',
|
||||
'stock_lost_p',
|
||||
'ids_lost_p',
|
||||
),
|
||||
}),
|
||||
('Attributs', {
|
||||
'fields': ('category', 'full_category_p', 'storage'),
|
||||
}),
|
||||
('Description', {
|
||||
'fields': ('description', 'added_at', 'modified_at',),
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
ordering = ['name', 'owner', 'category']
|
||||
inlines = [EquipmentAttributeValueInline,
|
||||
EquipmentDefaultExtraInline,
|
||||
EquipmentLostExtraInline,
|
||||
EquipmentRevisionExtraInline]
|
||||
search_fields = ['name', 'description',]
|
||||
list_filter = ['owner', 'category', 'storage', ]
|
||||
|
||||
|
||||
admin.site.register(Equipment, EquipmentAdmin)
|
||||
admin.site.register(EquipmentCategory, CategoryAdmin)
|
||||
admin.site.register(EquipmentStorage, StorageAdmin)
|
||||
admin.site.register(EquipmentAttribute, EquipmentAttributeAdmin)
|
|
@ -1,7 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class EquipmentConfig(AppConfig):
|
||||
name = 'equipment'
|
||||
verbose_name = _("Équipement")
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
from django.db import models
|
||||
from django import forms
|
||||
|
||||
|
||||
#class IdWidget(AdminMultipleChoiceFieldWidget):
|
||||
class IdWidget(forms.widgets.CheckboxSelectMultiple):
|
||||
template_name = 'equipment/widgets/checkbox_select.html'
|
||||
option_template_name = 'equipment/widgets/checkbox_option.html'
|
||||
def __init__(self, *args, **kwargs):
|
||||
# nb_items = kwargs.pop('nb_items')
|
||||
# kwargs['choices'] = list(range(1, nb_items+1))
|
||||
super(IdWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('css/idwidget.css',)
|
||||
}
|
||||
|
||||
class IdFormField(forms.MultipleChoiceField):
|
||||
#widget = IdWidget
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'min_value' in kwargs:
|
||||
kwargs.pop('min_value')
|
||||
if 'max_value' in kwargs:
|
||||
kwargs.pop('max_value')
|
||||
super(IdFormField, self).__init__(*args, **kwargs)
|
||||
|
||||
class IdField(models.BigIntegerField):
|
||||
def parse_integer(self, n):
|
||||
res = []
|
||||
k = 1
|
||||
while(n > 0):
|
||||
if n & 1:
|
||||
res.append(k)
|
||||
n >>= 1
|
||||
k += 1
|
||||
return res
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
return value
|
||||
return self.parse_integer(value)
|
||||
|
||||
def to_python(self, value):
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
return self.parse_integer(value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
res = 0
|
||||
for b in value:
|
||||
res |= 1 << (int(b)-1)
|
||||
return res
|
||||
|
||||
def __init__(self, separator=",", *args, **kwargs):
|
||||
self.separator = separator
|
||||
super(IdField, self).__init__(*args, **kwargs)
|
||||
self.validators = [] # TODO : validateurs pertinents
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(IdField, self).deconstruct()
|
||||
# Only include kwarg if it's not the default
|
||||
if self.separator != ",":
|
||||
kwargs['separator'] = self.separator
|
||||
return name, path, args, kwargs
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
# This is a fairly standard way to set up some defaults
|
||||
# while letting the caller override them.
|
||||
defaults = {'form_class': IdFormField}
|
||||
defaults.update(kwargs)
|
||||
return super(IdField, self).formfield(**defaults)
|
|
@ -1,11 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-06 17:29
|
||||
# Generated by Django 1.11.3 on 2017-07-21 14:20
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import equipment.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -34,7 +32,7 @@ class Migration(migrations.Migration):
|
|||
name='EquipmentAttribution',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('amount', models.BigIntegerField(verbose_name='quantité attribuée')),
|
||||
('amount', models.PositiveSmallIntegerField(verbose_name='quantité attribuée')),
|
||||
('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')),
|
||||
|
@ -44,45 +42,12 @@ class Migration(migrations.Migration):
|
|||
'verbose_name_plural': 'attributions de matériel',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EquipmentCategory',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, verbose_name='nom')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'catégories',
|
||||
'verbose_name_plural': 'catégories',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EquipmentOwner',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, verbose_name='nom')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'propriétaire de matériel',
|
||||
'verbose_name_plural': 'propriétaires de matériel',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EquipmentPole',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, verbose_name='nom')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'pôle',
|
||||
'verbose_name_plural': 'pôle',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EquipmentRemark',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('remark', models.TextField(verbose_name='remarque sur le matériel')),
|
||||
('ids', equipment.fields.IdField()),
|
||||
('amount', models.PositiveSmallIntegerField(verbose_name='quantité concernée')),
|
||||
('is_broken', models.BooleanField()),
|
||||
('is_lost', models.BooleanField()),
|
||||
('equipment', models.ForeignKey(help_text='Matériel concerné par la remarque', on_delete=django.db.models.deletion.CASCADE, related_name='remarks', to='equipment.Equipment')),
|
||||
|
@ -92,43 +57,14 @@ class Migration(migrations.Migration):
|
|||
'verbose_name_plural': 'remarques sur le matériel',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EquipmentRevision',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField(default=datetime.date.today, verbose_name='date')),
|
||||
('remark', models.TextField(verbose_name='remarque sur la révision')),
|
||||
('ids', equipment.fields.IdField()),
|
||||
('equipment', models.ForeignKey(help_text='Matériel concerné par les révisions', on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='equipment.Equipment')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'révision de matériel',
|
||||
'verbose_name_plural': 'révisions de 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='category',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentCategory'),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='equipment',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentOwner'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='equipment',
|
||||
name='pole',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentPole'),
|
||||
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'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-07 16:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import equipment.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EquipmentDefault',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('remark', models.TextField(verbose_name='remarque sur le défaut')),
|
||||
('ids', equipment.fields.IdField()),
|
||||
('is_unusable', models.BooleanField(verbose_name='inutilisable')),
|
||||
('send_repare', models.BooleanField(verbose_name='à envoyer réparareur')),
|
||||
('equipment', models.ForeignKey(help_text='Matériel concerné par le defaut', on_delete=django.db.models.deletion.CASCADE, related_name='remarks', to='equipment.Equipment')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'defaut matériel',
|
||||
'verbose_name_plural': 'défauts sur le matériel',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EquipmentLost',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('lost_at', models.DateField(default=datetime.date.today, verbose_name='perdu le')),
|
||||
('ids', equipment.fields.IdField()),
|
||||
('equipment', models.ForeignKey(help_text='Matériel concerné par la perte', on_delete=django.db.models.deletion.CASCADE, related_name='losts', to='equipment.Equipment')),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='equipmentremark',
|
||||
name='equipment',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='EquipmentRemark',
|
||||
),
|
||||
]
|
|
@ -1,22 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-07 18:43
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0002_auto_20180807_1658'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='equipment',
|
||||
name='owner',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='EquipmentOwner',
|
||||
),
|
||||
]
|
|
@ -1,22 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-07 18:44
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0008_alter_user_username_max_length'),
|
||||
('equipment', '0003_auto_20180807_1843'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='equipment',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='auth.Group'),
|
||||
),
|
||||
]
|
|
@ -1,28 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-08 10:13
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0004_equipment_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='equipment',
|
||||
name='pole',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='equipmentcategory',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentCategory'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='EquipmentPole',
|
||||
),
|
||||
]
|
|
@ -1,21 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-08 13:54
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0005_auto_20180808_1013'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='equipmentcategory',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, default=None, help_text='merci de ne pas faire de référence cyclique', null=True, on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentCategory'),
|
||||
),
|
||||
]
|
|
@ -1,40 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-08 14:40
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0006_auto_20180808_1354'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EquipmentAttribute',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, unique=True, verbose_name='nom')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'attribut',
|
||||
'verbose_name_plural': 'attributs',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EquipmentAttributeValue',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.CharField(max_length=200, verbose_name='valeur')),
|
||||
('attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentAttribute')),
|
||||
('equipment', models.ForeignKey(help_text='Matériel concerné par le defaut', on_delete=django.db.models.deletion.CASCADE, related_name='attributes', to='equipment.Equipment')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'attribut de matériel',
|
||||
'verbose_name_plural': 'attributs de matériel',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,21 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-08 14:54
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0007_equipmentattribute_equipmentattributevalue'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='equipment',
|
||||
name='category',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='equipment.EquipmentCategory'),
|
||||
),
|
||||
]
|
|
@ -1,28 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-09 12:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
from django.utils.timezone import utc
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0008_auto_20180808_1454'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='equipment',
|
||||
name='added_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2018, 8, 9, 12, 0, 50, 140250, tzinfo=utc), verbose_name='ajouté le'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='equipment',
|
||||
name='modified_at',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='dernière modification'),
|
||||
),
|
||||
]
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-09 12:11
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0009_auto_20180809_1200'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='equipment',
|
||||
name='stock',
|
||||
field=models.PositiveSmallIntegerField(verbose_name='quantité totale'),
|
||||
),
|
||||
]
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-09 14:05
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0010_auto_20180809_1211'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='equipment',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, verbose_name='description'),
|
||||
),
|
||||
]
|
|
@ -1,61 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-20 11:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0011_auto_20180809_1405'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='equipment',
|
||||
name='category',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='equipment.EquipmentCategory', verbose_name='catégorie'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipment',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='auth.Group', verbose_name='propriétaire'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipmentattributevalue',
|
||||
name='attribute',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentAttribute', verbose_name='attribut'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipmentattributevalue',
|
||||
name='equipment',
|
||||
field=models.ForeignKey(help_text='Matériel concerné par le defaut', on_delete=django.db.models.deletion.CASCADE, related_name='attributes', to='equipment.Equipment', verbose_name='matériel'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipmentattribution',
|
||||
name='equipment',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.Equipment', verbose_name='matériel'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipmentcategory',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, default=None, help_text='merci de ne pas faire de référence cyclique', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='equipment.EquipmentCategory', verbose_name='parent'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipmentdefault',
|
||||
name='equipment',
|
||||
field=models.ForeignKey(help_text='Matériel concerné par le defaut', on_delete=django.db.models.deletion.CASCADE, related_name='remarks', to='equipment.Equipment', verbose_name='matériel'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipmentlost',
|
||||
name='equipment',
|
||||
field=models.ForeignKey(help_text='Matériel concerné par la perte', on_delete=django.db.models.deletion.CASCADE, related_name='losts', to='equipment.Equipment', verbose_name='matériel'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipmentrevision',
|
||||
name='equipment',
|
||||
field=models.ForeignKey(help_text='Matériel concerné par les révisions', on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='equipment.Equipment', verbose_name='matériel'),
|
||||
),
|
||||
]
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-26 17:49
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0012_auto_20180820_1124'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='equipmentattribution',
|
||||
name='remarks',
|
||||
field=models.TextField(blank=True, verbose_name="remarques concernant l'attribution"),
|
||||
),
|
||||
]
|
|
@ -1,26 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-26 22:05
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0013_auto_20180826_1949'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='equipment',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='auth.Group', verbose_name='propriétaire'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipmentcategory',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, default=None, help_text='merci de ne pas faire de référence cyclique', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='equipment.EquipmentCategory', verbose_name='parent'),
|
||||
),
|
||||
]
|
|
@ -1,26 +0,0 @@
|
|||
# Generated by Django 2.1.5 on 2019-03-18 11:45
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0014_auto_20180827_0005'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EquipmentStorage',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, verbose_name='nom')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='equipment',
|
||||
name='storage',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='equipment.EquipmentStorage', verbose_name='stockage'),
|
||||
),
|
||||
]
|
|
@ -1,17 +0,0 @@
|
|||
# Generated by Django 2.1.5 on 2019-03-18 11:47
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0015_auto_20190318_1245'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='equipmentstorage',
|
||||
options={'verbose_name': 'stockage', 'verbose_name_plural': 'stockages'},
|
||||
),
|
||||
]
|
|
@ -1,98 +1,7 @@
|
|||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth.models import Group
|
||||
from event.models import Activity, EventSpecificMixin
|
||||
from django.db.models import Q
|
||||
|
||||
from .fields import IdField
|
||||
|
||||
from datetime import date
|
||||
|
||||
|
||||
class EquipmentStorage(models.Model):
|
||||
name = models.CharField(
|
||||
_("nom"),
|
||||
max_length=200,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("stockage")
|
||||
verbose_name_plural = _("stockages")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class EquipmentCategory(models.Model):
|
||||
name = models.CharField(
|
||||
_("nom"),
|
||||
max_length=200,
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="children",
|
||||
help_text=_("merci de ne pas faire de référence cyclique"),
|
||||
verbose_name=_("parent"),
|
||||
)
|
||||
|
||||
def has_parent(self, cat):
|
||||
current = self
|
||||
for k in range(100):
|
||||
if current is None:
|
||||
return False
|
||||
if current == cat:
|
||||
return True
|
||||
current = current.parent
|
||||
|
||||
def full_name(self):
|
||||
current = self
|
||||
res = ""
|
||||
for k in range(100):
|
||||
res = "/{current}{old}".format(
|
||||
current=current.name,
|
||||
old=res)
|
||||
if current.parent is None:
|
||||
break
|
||||
current = current.parent
|
||||
return res
|
||||
|
||||
full_name.short_description = _("Chemin complet")
|
||||
full_name_p = property(full_name)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("catégories")
|
||||
verbose_name_plural = _("catégories")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.pk:
|
||||
done = False
|
||||
current = self
|
||||
while not done:
|
||||
if current.parent == self:
|
||||
self.parent = None
|
||||
done = True
|
||||
elif current.parent is None:
|
||||
done = True
|
||||
current = current.parent
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class EquipmentQuerySet(models.QuerySet):
|
||||
def in_category(self, cat):
|
||||
filtre = Q(id__lt=0)
|
||||
childs_id = [c.id for c in EquipmentCategory.objects.all()
|
||||
if c.has_parent(cat)]
|
||||
for pk in childs_id:
|
||||
filtre |= Q(category__id=pk)
|
||||
return self.filter(filtre)
|
||||
|
||||
|
||||
class Equipment(EventSpecificMixin, models.Model):
|
||||
|
@ -100,98 +9,13 @@ class Equipment(EventSpecificMixin, models.Model):
|
|||
_("nom du matériel"),
|
||||
max_length=200,
|
||||
)
|
||||
stock = models.PositiveSmallIntegerField(_("quantité totale"))
|
||||
description = models.TextField(
|
||||
_("description"),
|
||||
blank=True,
|
||||
)
|
||||
stock = models.PositiveSmallIntegerField(_("quantité disponible"))
|
||||
description = models.TextField(_("description"))
|
||||
activities = models.ManyToManyField(
|
||||
Activity,
|
||||
related_name="equipment",
|
||||
through="EquipmentAttribution",
|
||||
)
|
||||
owner = models.ForeignKey(
|
||||
Group,
|
||||
verbose_name=_("propriétaire"),
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
category = models.ForeignKey(
|
||||
EquipmentCategory,
|
||||
verbose_name=_("catégorie"),
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
storage = models.ForeignKey(
|
||||
EquipmentStorage,
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("stockage"),
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
added_at = models.DateTimeField(
|
||||
_("ajouté le"),
|
||||
auto_now_add=True,
|
||||
)
|
||||
modified_at = models.DateTimeField(
|
||||
_("dernière modification"),
|
||||
auto_now=True,
|
||||
)
|
||||
|
||||
objects = EquipmentQuerySet.as_manager()
|
||||
|
||||
def is_in_category(self, cat):
|
||||
current = self.category
|
||||
for k in range(100):
|
||||
if current is None:
|
||||
return False
|
||||
if current == cat:
|
||||
return True
|
||||
current = current.parent
|
||||
|
||||
def ids_aviable(self):
|
||||
if self.stock is None:
|
||||
return []
|
||||
res = list(map(lambda x: x+1, range(self.stock)))
|
||||
for lost in self.losts.all():
|
||||
res = [x
|
||||
for x in res
|
||||
if x not in lost.ids]
|
||||
# TODO cassé
|
||||
# TODO utilisés
|
||||
return res
|
||||
|
||||
def ids_lost(self):
|
||||
res = []
|
||||
for lost in self.losts.all():
|
||||
res = res + [x
|
||||
for x in lost.ids
|
||||
if x not in res]
|
||||
return res
|
||||
|
||||
def stock_aviable(self):
|
||||
aviable = self.ids_aviable()
|
||||
return len(aviable)
|
||||
|
||||
def stock_lost(self):
|
||||
return len(self.ids_lost())
|
||||
|
||||
def full_category(self):
|
||||
return self.category.full_name()
|
||||
|
||||
full_category.short_description = _("Chemin complet")
|
||||
ids_aviable.short_description = _("disponibles")
|
||||
ids_lost.short_description = _("perdus")
|
||||
stock_aviable.short_description = _("quantité disponible")
|
||||
stock_lost.short_description = _("quantité perdue")
|
||||
|
||||
full_category_p = property(full_category)
|
||||
ids_aviable_p = property(ids_aviable)
|
||||
ids_lost_p = property(ids_lost)
|
||||
stock_aviable_p = property(stock_aviable)
|
||||
stock_lost_p = property(stock_lost)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("matériel")
|
||||
|
@ -201,63 +25,11 @@ class Equipment(EventSpecificMixin, models.Model):
|
|||
return self.name
|
||||
|
||||
|
||||
class EquipmentAttribute(models.Model):
|
||||
name = models.CharField(
|
||||
_("nom"),
|
||||
max_length=200,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("attribut")
|
||||
verbose_name_plural = _("attributs")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class EquipmentAttributeValue(models.Model):
|
||||
equipment = models.ForeignKey(
|
||||
Equipment,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="attributes",
|
||||
verbose_name=_("matériel"),
|
||||
help_text=_("Matériel concerné par le defaut"),
|
||||
)
|
||||
attribute = models.ForeignKey(
|
||||
EquipmentAttribute,
|
||||
verbose_name=_("attribut"),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
value = models.CharField(
|
||||
_("valeur"),
|
||||
max_length=200,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("attribut de matériel")
|
||||
verbose_name_plural = _("attributs de matériel")
|
||||
|
||||
def __str__(self):
|
||||
return "{attr}={value}".format(attr=self.attribute.name,
|
||||
value=self.value)
|
||||
|
||||
|
||||
class EquipmentAttribution(models.Model):
|
||||
equipment = models.ForeignKey(
|
||||
Equipment,
|
||||
verbose_name=_("matériel"),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
activity = models.ForeignKey(
|
||||
Activity,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
amount = models.BigIntegerField(_("quantité attribuée"))
|
||||
remarks = models.TextField(
|
||||
_("remarques concernant l'attribution"),
|
||||
blank=True,
|
||||
)
|
||||
equipment = models.ForeignKey(Equipment)
|
||||
activity = models.ForeignKey(Activity)
|
||||
amount = models.PositiveSmallIntegerField(_("quantité attribuée"))
|
||||
remarks = models.TextField(_("remarques concernant l'attribution"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("attribution de matériel")
|
||||
|
@ -265,72 +37,30 @@ class EquipmentAttribution(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
return "%s (%d) -> %s" % (self.equipment.name,
|
||||
self.amount,
|
||||
self.amout,
|
||||
self.activity.get_herited('title'))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if (self.equipment.event
|
||||
and self.equipment.event != self.activity.event):
|
||||
if self.equipment.event and self.equipment.event != self.activity.event:
|
||||
raise ValidationError
|
||||
|
||||
super(EquipmentAttribution, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class EquipmentDefault(models.Model):
|
||||
remark = models.TextField(_("remarque sur le défaut"))
|
||||
class EquipmentRemark(models.Model):
|
||||
remark = models.TextField(_("remarque sur le matériel"))
|
||||
equipment = models.ForeignKey(
|
||||
Equipment,
|
||||
verbose_name=_("matériel"),
|
||||
on_delete=models.CASCADE,
|
||||
related_name="remarks",
|
||||
help_text=_("Matériel concerné par le defaut"),
|
||||
help_text=_("Matériel concerné par la remarque"),
|
||||
)
|
||||
ids = IdField()
|
||||
is_unusable = models.BooleanField(_("inutilisable"))
|
||||
send_repare = models.BooleanField(_("à envoyer réparareur"))
|
||||
amount = models.PositiveSmallIntegerField(_("quantité concernée"))
|
||||
is_broken = models.BooleanField()
|
||||
is_lost = models.BooleanField()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("defaut matériel")
|
||||
verbose_name_plural = _("défauts sur le matériel")
|
||||
|
||||
def __str__(self):
|
||||
return "%s : %s" % (self.equipment.name,
|
||||
self.remark)
|
||||
|
||||
|
||||
class EquipmentLost(models.Model):
|
||||
lost_at = models.DateField(
|
||||
_("perdu le"),
|
||||
default=date.today,
|
||||
)
|
||||
equipment = models.ForeignKey(
|
||||
Equipment,
|
||||
verbose_name=_("matériel"),
|
||||
on_delete=models.CASCADE,
|
||||
related_name="losts",
|
||||
help_text=_("Matériel concerné par la perte"),
|
||||
)
|
||||
ids = IdField()
|
||||
|
||||
|
||||
class EquipmentRevision(models.Model):
|
||||
date = models.DateField(
|
||||
_("date"),
|
||||
default=date.today,
|
||||
)
|
||||
equipment = models.ForeignKey(
|
||||
Equipment,
|
||||
verbose_name=_("matériel"),
|
||||
on_delete=models.CASCADE,
|
||||
related_name="revisions",
|
||||
help_text=_("Matériel concerné par les révisions"),
|
||||
)
|
||||
remark = models.TextField(_("remarque sur la révision"))
|
||||
ids = IdField()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("révision de matériel")
|
||||
verbose_name_plural = _("révisions de matériel")
|
||||
verbose_name = _("remarque sur matériel")
|
||||
verbose_name_plural = _("remarques sur le matériel")
|
||||
|
||||
def __str__(self):
|
||||
return "%s : %s" % (self.equipment.name,
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
.nice_select input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nice_select input[type="checkbox"]:checked + label {
|
||||
background: red;
|
||||
color:white;
|
||||
}
|
||||
|
||||
.nice_select ul {
|
||||
display: inline-block;
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db.models import Q
|
||||
from django.http.request import QueryDict
|
||||
|
||||
import django_filters
|
||||
from django_filters.widgets import LinkWidget
|
||||
from django_tables2.utils import A
|
||||
import django_tables2 as tables
|
||||
|
||||
from .models import Equipment, EquipmentCategory
|
||||
|
||||
|
||||
class EquipmentFilter(django_filters.FilterSet):
|
||||
owner = django_filters.ModelChoiceFilter(
|
||||
field_name='owner',
|
||||
queryset=Group.objects.all(),
|
||||
widget=LinkWidget(),
|
||||
)
|
||||
category = django_filters.ModelChoiceFilter(
|
||||
field_name='category',
|
||||
queryset=EquipmentCategory.objects.all(),
|
||||
widget=LinkWidget(),
|
||||
method='filter_category',
|
||||
)
|
||||
|
||||
def filter_category(self, queryset, name, value):
|
||||
return queryset.in_category(value)
|
||||
|
||||
def get_categories(self, qs):
|
||||
"""
|
||||
rend les catégories qui servent à filtrer les Equipments de qs
|
||||
ie les catégories des equipments et tous leurs parents
|
||||
"""
|
||||
filtre = Q(id__lt=0)
|
||||
for eq in qs:
|
||||
current = eq.category
|
||||
for k in range(100):
|
||||
if current is None:
|
||||
break
|
||||
filtre |= Q(id=current.id)
|
||||
current = current.parent
|
||||
return EquipmentCategory.objects.filter(filtre)
|
||||
|
||||
def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
|
||||
# On que les requêtes vides rendent quelque chose
|
||||
if data is None:
|
||||
data = QueryDict('category&owner')
|
||||
super().__init__(data=data, queryset=queryset,
|
||||
request=request, prefix=prefix)
|
||||
if self.queryset is not None:
|
||||
for filter_ in self.filters.values():
|
||||
if filter_.queryset.model == Group:
|
||||
own_ids = [eq.owner.id for eq in self.queryset
|
||||
if eq.owner is not None]
|
||||
filtre = Q(id__lt=0)
|
||||
for own_id in own_ids:
|
||||
filtre |= Q(id=own_id)
|
||||
filter_.queryset = Group.objects.filter(filtre)
|
||||
if filter_.queryset.model == EquipmentCategory:
|
||||
filter_.queryset = self.get_categories(self.queryset)
|
||||
|
||||
class Meta:
|
||||
model = Equipment
|
||||
fields = ['category', 'owner']
|
||||
|
||||
|
||||
class AbstractEquipmentTable(tables.Table):
|
||||
stock_aviable_p = tables.Column(
|
||||
accessor=A('stock_aviable_p'),
|
||||
orderable=False, # TODO le rendre ordorable
|
||||
verbose_name=_("Quantité disponible"),
|
||||
)
|
||||
full_category_p = tables.Column(
|
||||
accessor=A('full_category_p'),
|
||||
order_by=('category'),
|
||||
verbose_name=_("Catégorie"),
|
||||
)
|
||||
name = tables.LinkColumn(
|
||||
'equipment:detail',
|
||||
args=[A('pk')],
|
||||
verbose_name=_("Matériel"),
|
||||
)
|
||||
admin = tables.LinkColumn(
|
||||
'admin:equipment_equipment_change',
|
||||
attrs={
|
||||
'a': {'class': 'glyphicon glyphicon-cog'}
|
||||
},
|
||||
text="",
|
||||
orderable=False,
|
||||
args=[A('pk')],
|
||||
verbose_name=_(""),
|
||||
)
|
||||
|
||||
def before_render(self, request):
|
||||
if (request.user.is_staff and
|
||||
request.user.has_perm('equipment_change_equipment')):
|
||||
self.columns.show('admin')
|
||||
else:
|
||||
self.columns.hide('admin')
|
||||
|
||||
|
||||
class EquipmentTable(AbstractEquipmentTable):
|
||||
class Meta:
|
||||
model = Equipment
|
||||
template_name = 'equipment/tables/bootstrap-responsive.html'
|
||||
fields = ['name', 'stock', 'owner', ]
|
||||
sequence = ['admin', 'name', 'stock', 'stock_aviable_p',
|
||||
'full_category_p', 'owner', ]
|
|
@ -1,13 +0,0 @@
|
|||
{% extends "shared/base.html" %}
|
||||
{% load i18n staticfiles %}
|
||||
|
||||
{% block title %}{% trans "Matériel" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{{ equipment.name }}<a class="pull-right glyphicon glyphicon-cog" href="{% url 'admin:equipment_equipment_change' equipment.id %}"></a></h2>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
Coucou :)
|
||||
{% endblock %}
|
|
@ -1,50 +0,0 @@
|
|||
{% extends "shared/base.html" %}
|
||||
{% load i18n staticfiles %}
|
||||
|
||||
{% block title %}{% trans "Matériel" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="equipment">{% trans "Inventaire" %}</h1>
|
||||
<div class="module-list">
|
||||
<a href="{% url 'equipment:list' %}" class="module equipment">
|
||||
<span class="glyphicon glyphicon-list-alt"></span>
|
||||
{% trans "Tout le matériel" %}
|
||||
</a>
|
||||
<a href="#TODO" class="module equipment">
|
||||
<span class="glyphicon glyphicon-list-alt"></span>
|
||||
{% trans "Disponible" %}
|
||||
</a>
|
||||
</div>
|
||||
<h2 class="staff">{% trans "Liste par Propriétaire" %}</h2>
|
||||
<div class="module-list">
|
||||
{% for owner in owners %}
|
||||
<a href="{% url 'equipment:list_by_owner' owner.id %}" class="module staff">
|
||||
<span class="glyphicon glyphicon-user"></span>
|
||||
{{ owner.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<h2 class="equipment">{% trans "Liste par Catégorie" %}</h2>
|
||||
<div class="tree">
|
||||
<ul>
|
||||
{% for node in root_cat %}
|
||||
{% include "equipment/tree_cat.html" %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
<div class="heading">
|
||||
{{ nb_type }} <span class="sub">{% trans "référence" %}{{ nb_type|pluralize}}</span>
|
||||
</div>
|
||||
<div class="heading separator">
|
||||
{{ stock }} <span class="sub">{% trans "item" %}{{ stock|pluralize}}</span>
|
||||
</div>
|
||||
<div class="heading inverted small">
|
||||
{{ categories.count }} <span class="sub">{% trans "catégorie" %}{{ categories.count|pluralize}}</span>
|
||||
</div>
|
||||
<div class="heading inverted small ">
|
||||
{{ owners.count }} <span class="sub">{% trans "propriéaire" %}{{ owners.count|pluralize}}</span>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,30 +0,0 @@
|
|||
{% extends "shared/base.html" %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n staticfiles %}
|
||||
|
||||
{% block title %}{% trans "Matériel" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="equipment">Inventaire</h1>
|
||||
{% if subtitle %}
|
||||
<h2 class="equipment">{{ subtitle }}</h2>
|
||||
{% endif %}
|
||||
{% render_table table %}
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
<div class="heading">
|
||||
{{ nb_type }} <span class="sub">{% trans "référence" %}{{ nb_type|pluralize}}</span>
|
||||
</div>
|
||||
<div class="heading separator">
|
||||
{{ stock }} <span class="sub">{% trans "item" %}{{ stock|pluralize}}</span>
|
||||
</div>
|
||||
{% if filter %}
|
||||
<div class="text inverted">
|
||||
<form id="filter_form" action="" method="get" class="form form-inline">
|
||||
{% bootstrap_form filter.form layout='horizontal' size='lg' %}
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -1,16 +0,0 @@
|
|||
{% extends 'equipment/tables/bootstrap.html' %}
|
||||
|
||||
{% block table-wrapper %}
|
||||
<div class="table-container table-responsive">
|
||||
{% block table %}
|
||||
{{ block.super }}
|
||||
{% endblock table %}
|
||||
|
||||
{% if table.page and table.paginator.num_pages > 1 %}
|
||||
{% block pagination %}
|
||||
{{ block.super }}
|
||||
{% endblock pagination %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock table-wrapper %}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
{% load django_tables2 %}
|
||||
{% load i18n %}
|
||||
{% block table-wrapper %}
|
||||
<div class="table-container">
|
||||
{% block table %}
|
||||
<table {% render_attrs table.attrs class="table table-striped" %}>
|
||||
{% block table.thead %}
|
||||
{% if table.show_header %}
|
||||
<thead>
|
||||
<tr>
|
||||
{% for column in table.columns %}
|
||||
<th {{ column.attrs.th.as_html }}>
|
||||
{% if column.orderable %}
|
||||
<a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a>
|
||||
{% else %}
|
||||
{{ column.header }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% endif %}
|
||||
{% endblock table.thead %}
|
||||
{% block table.tbody %}
|
||||
<tbody>
|
||||
{% for row in table.paginated_rows %}
|
||||
{% block table.tbody.row %}
|
||||
<tr {{ row.attrs.as_html }}>
|
||||
{% for column, cell in row.items %}
|
||||
<td {{ column.attrs.td.as_html }}>{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endblock table.tbody.row %}
|
||||
{% empty %}
|
||||
{% if table.empty_text %}
|
||||
{% block table.tbody.empty_text %}
|
||||
<tr><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
|
||||
{% endblock table.tbody.empty_text %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endblock table.tbody %}
|
||||
{% block table.tfoot %}
|
||||
{% if table.has_footer %}
|
||||
<tfoot>
|
||||
<tr>
|
||||
{% for column in table.columns %}
|
||||
<td {{ column.attrs.tf.as_html }}>{{ column.footer }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
{% endblock table.tfoot %}
|
||||
</table>
|
||||
{% endblock table %}
|
||||
|
||||
{% if table.page and table.paginator.num_pages > 1 %}
|
||||
{% block pagination %}
|
||||
<nav aria-label="Table navigation">
|
||||
<ul class="pagination">
|
||||
{% if table.page.has_previous %}
|
||||
{% block pagination.previous %}
|
||||
<li class="previous">
|
||||
<a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">
|
||||
<span aria-hidden="true">«</span>
|
||||
{% trans 'previous' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endblock pagination.previous %}
|
||||
{% endif %}
|
||||
{% if table.page.has_previous or table.page.has_next %}
|
||||
{% block pagination.range %}
|
||||
{% for p in table.page|table_page_range:table.paginator %}
|
||||
<li {% if p == table.page.number %}class="active"{% endif %}>
|
||||
{% if p == '...' %}
|
||||
<a href="#">{{ p }}</a>
|
||||
{% else %}
|
||||
<a href="{% querystring table.prefixed_page_field=p %}">
|
||||
{{ p }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endblock pagination.range %}
|
||||
{% endif %}
|
||||
|
||||
{% if table.page.has_next %}
|
||||
{% block pagination.next %}
|
||||
<li class="next">
|
||||
<a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">
|
||||
{% trans 'next' %}
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endblock pagination.next %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock pagination %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock table-wrapper %}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<li> <a href="{% url "equipment:list_by_category" node.id %}"><span class="category_node">{{node.name}}</span></a>
|
||||
{%if node.children %}
|
||||
<ul>
|
||||
{%for ch in node.children.all %}
|
||||
{%with node=ch template_name="equipment/tree_cat.html" %}
|
||||
{%include template_name%}
|
||||
{%endwith%}
|
||||
{%endfor%}
|
||||
</ul>
|
||||
{%endif%}
|
||||
</li>
|
|
@ -1 +0,0 @@
|
|||
{% include "django/forms/widgets/input.html" %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{{ widget.label }}</label>
|
|
@ -1,5 +0,0 @@
|
|||
{% with id=widget.attrs.id %}<ul{% if id %} id="{{ id }}"{% endif %}{% if widget.attrs.class %} class="{{ widget.attrs.class }} nice_select"{% endif %} style="display: inline-block">{% for group, options, index in widget.optgroups %}{% if group %}
|
||||
<li>{{ group }}<ul{% if id %} id="{{ id }}_{{ index }}"{% endif %}>{% endif %}{% for option in options %}
|
||||
<li style="list-style-type: none;">{% include option.template_name with widget=option %}</li>{% endfor %}{% if group %}
|
||||
</ul></li>{% endif %}{% endfor %}
|
||||
</ul>{% endwith %}
|
|
@ -1,11 +0,0 @@
|
|||
from django.conf.urls import url
|
||||
from .views import EquipmentList, EquipmentView, EquipmentListByCategory, EquipmentListByOwner, EquipmentHome
|
||||
|
||||
app_name = 'equipment'
|
||||
urlpatterns = [
|
||||
url(r'^$', EquipmentHome.as_view(), name='home'),
|
||||
url(r'^all/$', EquipmentList.as_view(), name='list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', EquipmentView.as_view(), name='detail'),
|
||||
url(r'^c/(?P<pk>[0-9]+)/$', EquipmentListByCategory.as_view(), name='list_by_category'),
|
||||
url(r'^o/(?P<pk>[0-9]+)/$', EquipmentListByOwner.as_view(), name='list_by_owner'),
|
||||
]
|
|
@ -1,97 +0,0 @@
|
|||
from .models import Equipment, EquipmentCategory
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db.models import Sum
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
|
||||
from django_filters.views import FilterView
|
||||
from django_tables2.views import SingleTableMixin
|
||||
|
||||
from .tables import EquipmentTable, EquipmentFilter
|
||||
|
||||
|
||||
class EquipmentHome(LoginRequiredMixin, ListView):
|
||||
template_name = 'equipment/home.html'
|
||||
context_object_name = 'categories'
|
||||
model = EquipmentCategory
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# TODO remplacer par les vrais owners
|
||||
context['owners'] = Group.objects.all()
|
||||
categories = (EquipmentCategory.objects.order_by('name')
|
||||
.prefetch_related('children'))
|
||||
context['root_cat'] = categories.filter(parent=None)
|
||||
queryset = Equipment.objects.all()
|
||||
context['stock'] = queryset.aggregate(Sum('stock'))['stock__sum']
|
||||
context['nb_type'] = queryset.count()
|
||||
return context
|
||||
|
||||
|
||||
class EquipmentListAbstract(LoginRequiredMixin, SingleTableMixin,FilterView):
|
||||
table_class = EquipmentTable
|
||||
filterset_class = EquipmentFilter
|
||||
template_name = 'equipment/list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['stock'] = self.queryset.aggregate(Sum('stock'))['stock__sum']
|
||||
context['nb_type'] = self.queryset.count()
|
||||
return context
|
||||
|
||||
|
||||
class EquipmentList(EquipmentListAbstract):
|
||||
def get_queryset(self):
|
||||
self.queryset = Equipment.objects.all()
|
||||
return self.queryset
|
||||
|
||||
|
||||
class EquipmentListByCategory(EquipmentListAbstract):
|
||||
def get_category(self):
|
||||
try:
|
||||
pk = self.kwargs.get('pk')
|
||||
except KeyError:
|
||||
raise AttributeError(
|
||||
"View %s must be called with an object "
|
||||
"pk in the URLconf." % self.__class__.__name__
|
||||
)
|
||||
return EquipmentCategory.objects.get(id=pk)
|
||||
|
||||
def get_queryset(self):
|
||||
cat = self.get_category()
|
||||
self.queryset = Equipment.objects.all().in_category(cat)
|
||||
return self.queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
cat = self.get_category()
|
||||
context['subtitle'] = "Dans {cat}".format(cat=cat.full_name())
|
||||
return context
|
||||
|
||||
|
||||
class EquipmentListByOwner(EquipmentListAbstract):
|
||||
def get_owner(self):
|
||||
try:
|
||||
pk = self.kwargs.get('pk')
|
||||
except KeyError:
|
||||
raise AttributeError(
|
||||
"View %s must be called with an object "
|
||||
"pk in the URLconf." % self.__class__.__name__
|
||||
)
|
||||
return Group.objects.get(id=pk)
|
||||
|
||||
def get_queryset(self):
|
||||
owner = self.get_owner()
|
||||
self.queryset = Equipment.objects.filter(owner=owner)
|
||||
return self.queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
owner = self.get_owner()
|
||||
context['subtitle'] = "Matériel de {owner}".format(owner=owner)
|
||||
return context
|
||||
|
||||
|
||||
class EquipmentView(LoginRequiredMixin, DetailView):
|
||||
model = Equipment
|
||||
template_name = 'equipment/detail.html'
|
|
@ -2,6 +2,6 @@ import os
|
|||
from channels.asgi import get_channel_layer
|
||||
|
||||
if "DJANGO_SETTINGS_MODULE" not in os.environ:
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "poulpe.settings")
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "evenementiel.settings")
|
||||
|
||||
channel_layer = get_channel_layer()
|
|
@ -10,8 +10,6 @@ We also load the secrets in this file.
|
|||
|
||||
import os
|
||||
from . import secret
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib import messages
|
||||
|
||||
|
||||
def import_secret(name):
|
||||
|
@ -27,7 +25,6 @@ def import_secret(name):
|
|||
|
||||
SECRET_KEY = import_secret("SECRET_KEY")
|
||||
ADMINS = import_secret("ADMINS")
|
||||
SERVER_EMAIL = import_secret("SERVER_EMAIL")
|
||||
|
||||
DBNAME = import_secret("DBNAME")
|
||||
DBUSER = import_secret("DBUSER")
|
||||
|
@ -47,46 +44,31 @@ BASE_DIR = os.path.dirname(
|
|||
|
||||
|
||||
INSTALLED_APPS = [
|
||||
# 'shared.apps.CustomAdminConfig',
|
||||
'shared.apps.SharedConfig',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.sites',
|
||||
|
||||
# 'channels',
|
||||
# 'rest_framework',
|
||||
'channels',
|
||||
'rest_framework',
|
||||
'bootstrapform',
|
||||
'widget_tweaks',
|
||||
'taggit',
|
||||
'django_tables2',
|
||||
'django_filters',
|
||||
'bootstrap3',
|
||||
|
||||
'allauth_ens',
|
||||
'allauth',
|
||||
'allauth.account',
|
||||
'allauth.socialaccount',
|
||||
'allauth_cas',
|
||||
'allauth_ens.providers.clipper',
|
||||
|
||||
# 'api',
|
||||
'api',
|
||||
'communication',
|
||||
'equipment',
|
||||
'event',
|
||||
'shared',
|
||||
'users',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
MIDDLEWARE_CLASSES = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
# 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
@ -96,10 +78,9 @@ REST_FRAMEWORK = {
|
|||
'rest_framework.permissions.AllowAny',
|
||||
],
|
||||
'PAGE_SIZE': 10,
|
||||
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
|
||||
}
|
||||
|
||||
ROOT_URLCONF = 'poulpe.urls'
|
||||
ROOT_URLCONF = 'evenementiel.urls'
|
||||
|
||||
STATIC_URL = "/static/"
|
||||
MEDIA_URL = "/media/"
|
||||
|
@ -137,19 +118,19 @@ DATABASES = {
|
|||
}
|
||||
}
|
||||
|
||||
# CHANNEL_LAYERS = {
|
||||
# "default": {
|
||||
# "BACKEND": "asgi_redis.RedisChannelLayer",
|
||||
# "CONFIG": {
|
||||
# "hosts": [(
|
||||
# "redis://:{passwd}@{host}:{port}/{db}"
|
||||
# .format(passwd=REDIS_PASSWD, host=REDIS_HOST,
|
||||
# port=REDIS_PORT, db=REDIS_DB)
|
||||
# )],
|
||||
# },
|
||||
# "ROUTING": "poulpe.routing.channel_routing",
|
||||
# }
|
||||
# }
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "asgi_redis.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [(
|
||||
"redis://:{passwd}@{host}:{port}/{db}"
|
||||
.format(passwd=REDIS_PASSWD, host=REDIS_HOST,
|
||||
port=REDIS_PORT, db=REDIS_DB)
|
||||
)],
|
||||
},
|
||||
"ROUTING": "evenementiel.routing.channel_routing",
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
|
||||
|
@ -166,55 +147,12 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'fr-fr'
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'Europe/Paris'
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'allauth.account.auth_backends.AuthenticationBackend',
|
||||
)
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
|
||||
CAS_SERVER_URL = "https://cas.eleves.ens.fr/" # SPI CAS
|
||||
CAS_VERIFY_URL = "https://cas.eleves.ens.fr/"
|
||||
CAS_VERSION = "2"
|
||||
CAS_IGNORE_REFERER = True
|
||||
CAS_LOGIN_MSG = None
|
||||
CAS_REDIRECT_URL = reverse_lazy('shared:home')
|
||||
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
|
||||
CAS_FORCE_CHANGE_USERNAME_CASE = "lower"
|
||||
|
||||
LOGIN_URL = reverse_lazy('account_login')
|
||||
LOGOUT_URL = reverse_lazy('account_logout')
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('shared:home')
|
||||
ACCOUNT_HOME_URL = reverse_lazy('shared:home')
|
||||
ACCOUNT_DETAILS_URL = reverse_lazy('shared:home')
|
||||
|
||||
|
||||
SOCIALACCOUNT_PROVIDERS = {
|
||||
# …
|
||||
|
||||
'clipper': {
|
||||
|
||||
# These settings control whether a message containing a link to
|
||||
# disconnect from the CAS server is added when users log out.
|
||||
'MESSAGE_SUGGEST_LOGOUT_ON_LOGOUT': True,
|
||||
'MESSAGE_SUGGEST_LOGOUT_ON_LOGOUT_LEVEL': messages.INFO,
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
ACCOUNT_ADAPTER = 'shared.allauth_adapter.AccountAdapter'
|
||||
#SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter'
|
||||
SOCIALACCOUNT_ADAPTER= 'shared.allauth_adapter.SocialAccountAdapter'
|
||||
|
|
@ -11,12 +11,10 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
|||
DEBUG = True
|
||||
|
||||
# Add some debugging tools
|
||||
INSTALLED_APPS += ["debug_toolbar", ] # NOQA
|
||||
MIDDLEWARE = (
|
||||
[
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
]
|
||||
+ MIDDLEWARE # NOQA
|
||||
INSTALLED_APPS += ["debug_toolbar", "debug_panel"] # NOQA
|
||||
MIDDLEWARE_CLASSES = (
|
||||
["debug_panel.middleware.DebugPanelMiddleware"]
|
||||
+ MIDDLEWARE_CLASSES # NOQA
|
||||
)
|
||||
|
||||
|
|
@ -18,6 +18,6 @@ DATABASES = {
|
|||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "asgiref.inmemory.ChannelLayer",
|
||||
"ROUTING": "poulpe.routing.channel_routing",
|
||||
"ROUTING": "evenementiel.routing.channel_routing",
|
||||
},
|
||||
}
|
|
@ -5,23 +5,11 @@ from django.conf import settings
|
|||
from django.conf.urls import url, include
|
||||
from django.contrib import admin
|
||||
|
||||
#from django_cas_ng import views as django_cas_views
|
||||
from allauth_ens.views import capture_login, capture_logout
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/login/$', capture_login),
|
||||
url(r'^admin/logout/$', capture_logout),
|
||||
url(r'^compte/', include('allauth.urls')),
|
||||
# Admin
|
||||
url(r'^admin/', admin.site.urls),
|
||||
# Apps
|
||||
url(r'^equipment/', include('equipment.urls')),
|
||||
url(r'^event/', include('event.urls')),
|
||||
#url(r'^user/', include('users.urls')),
|
||||
# REST
|
||||
url(r'^user/', include('users.urls')),
|
||||
url(r'^api/', include('api.urls')),
|
||||
# Reste
|
||||
url(r'^', include('shared.urls')),
|
||||
]
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
WSGI config for GestionEvenementiel project.
|
||||
WSGI config for evenementiel project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
|
@ -11,6 +11,6 @@ import os
|
|||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "poulpe.settings")
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "evenementiel.settings")
|
||||
|
||||
application = get_wsgi_application()
|
|
@ -1,97 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import Event, Place, ActivityTag, Activity, ActivityTemplate # TODO add me
|
||||
from equipment.models import EquipmentAttribution
|
||||
|
||||
|
||||
class EquipmentAttributionExtraInline(admin.TabularInline):
|
||||
autocomplete_fields = ['equipment', ]
|
||||
model = EquipmentAttribution
|
||||
extra = 0
|
||||
classes = ['collapse']
|
||||
|
||||
|
||||
class EventAdmin(admin.ModelAdmin):
|
||||
list_display = ['title', 'slug', 'beginning_date', 'ending_date']
|
||||
readonly_fields = ['created_by', 'created_at', ]
|
||||
ordering = ['title', 'beginning_date', 'ending_date', ]
|
||||
search_fields = ['title', 'decription', ]
|
||||
list_filter = ['beginning_date', 'ending_date', ]
|
||||
date_hierarchy = 'beginning_date'
|
||||
|
||||
|
||||
class PlaceAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'event', ]
|
||||
ordering = ['name', 'event', ]
|
||||
search_fields = ['name', ]
|
||||
list_filter = ['event', ]
|
||||
|
||||
|
||||
class ActivityTagAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'event', 'is_public', ]
|
||||
ordering = ['name', 'event', 'is_public', ]
|
||||
search_fields = ['name', ]
|
||||
list_filter = ['event', 'is_public', ]
|
||||
|
||||
|
||||
class ActivityTemplateAdmin(admin.ModelAdmin):
|
||||
save_as_continue = True
|
||||
save_on_top = True
|
||||
list_display = ['name', 'title', 'event', 'is_public', ]
|
||||
ordering = ['name', 'title', 'event', 'has_perm', ]
|
||||
search_fields = ['name', 'title', 'description', 'remark', ]
|
||||
list_filter = ['event', 'is_public', 'has_perm', 'tags', ]
|
||||
filter_horizontal = ['tags', 'places', ]
|
||||
fieldsets = (
|
||||
('Identifiant', {
|
||||
'fields': ('name', ),
|
||||
}),
|
||||
('Général', {
|
||||
'fields': ('event', 'title', 'is_public', 'places', ),
|
||||
'description': "Tous ces champs sont héritables (Sauf Évènement)",
|
||||
}),
|
||||
('Permanences', {
|
||||
'fields': ('has_perm', ('min_perm', 'max_perm', ), ),
|
||||
'classes': ('collapse',),
|
||||
'description': "Tous ces champs sont héritables",
|
||||
}),
|
||||
('Descriptions', {
|
||||
'fields': ('description', 'tags', 'remarks', ),
|
||||
'classes': ('collapse',),
|
||||
'description': "Tous ces champs sont héritables",
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
class ActivityAdmin(admin.ModelAdmin):
|
||||
save_as = True
|
||||
save_on_top = True
|
||||
list_display = ['title', 'event', 'is_public', 'parent', ]
|
||||
ordering = ['title', 'event', 'has_perm', 'parent', ]
|
||||
search_fields = ['title', 'description', 'remark', ]
|
||||
list_filter = ['event', 'is_public', 'has_perm', 'tags', ]
|
||||
filter_horizontal = ['tags', 'places', 'staff', ]
|
||||
inlines = [EquipmentAttributionExtraInline, ]
|
||||
fieldsets = (
|
||||
('Général', {
|
||||
'fields': ('event', 'parent', 'title', 'is_public', 'beginning', 'end', 'places', ),
|
||||
'description': "Tous ces champs sont héritables (sauf parent et Évènement)",
|
||||
}),
|
||||
('Permanences', {
|
||||
'fields': ('has_perm', ('min_perm', 'max_perm', ), 'staff', ),
|
||||
'classes': ('wide',),
|
||||
'description': "Tous ces champs sont héritables (sauf les gens en perm)",
|
||||
}),
|
||||
('Descriptions', {
|
||||
'fields': ('description', 'tags', 'remarks', ),
|
||||
'classes': ('collapse',),
|
||||
'description': "Tous ces champs sont héritables",
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
admin.site.register(Event, EventAdmin)
|
||||
admin.site.register(Place, PlaceAdmin)
|
||||
admin.site.register(ActivityTag, ActivityTagAdmin)
|
||||
admin.site.register(ActivityTemplate, ActivityTemplateAdmin)
|
||||
admin.site.register(Activity, ActivityAdmin)
|
||||
# Register your models here.
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class EventConfig(AppConfig):
|
||||
name = 'event'
|
||||
verbose_name = _("Évènement")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.15 on 2018-08-06 07:51
|
||||
# Generated by Django 1.11.3 on 2017-07-21 14:20
|
||||
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(verbose_name='est public')),
|
||||
('has_perm', models.NullBooleanField(verbose_name='inscription de permanents')),
|
||||
('is_public', models.NullBooleanField()),
|
||||
('has_perm', models.NullBooleanField()),
|
||||
('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="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')),
|
||||
('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')),
|
||||
('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 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')),
|
||||
('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')),
|
||||
],
|
||||
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(verbose_name='est public')),
|
||||
('has_perm', models.NullBooleanField(verbose_name='inscription de permanents')),
|
||||
('is_public', models.NullBooleanField()),
|
||||
('has_perm', models.NullBooleanField()),
|
||||
('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="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')),
|
||||
('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')),
|
||||
],
|
||||
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 ou les caractères '_' ou '-'.", unique=True, verbose_name='identificateur')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='date de création')),
|
||||
('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')),
|
||||
('description', models.TextField(verbose_name='description')),
|
||||
('beginning_date', models.DateTimeField(help_text="date publique de l'évènement", verbose_name='date de début')),
|
||||
('ending_date', models.DateTimeField(help_text="date publique de l'évènement", verbose_name='date de fin')),
|
||||
('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')),
|
||||
('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)),
|
||||
],
|
||||
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(editable=False, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'),
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='activitytemplate',
|
||||
name='places',
|
||||
field=models.ManyToManyField(blank=True, to='event.Place', verbose_name='lieux'),
|
||||
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', verbose_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'),
|
||||
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'),
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Event'),
|
||||
),
|
||||
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'),
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='children', to='event.ActivityTemplate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='activity',
|
||||
name='places',
|
||||
field=models.ManyToManyField(blank=True, to='event.Place', verbose_name='lieux'),
|
||||
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, verbose_name='permanents'),
|
||||
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', verbose_name='tags'),
|
||||
field=models.ManyToManyField(blank=True, to='event.ActivityTag'),
|
||||
),
|
||||
]
|
||||
|
|
21
event/migrations/0002_auto_20170723_1419.py
Normal file
21
event/migrations/0002_auto_20170723_1419.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# -*- 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'),
|
||||
),
|
||||
]
|
|
@ -1,32 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-20 15:29
|
||||
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.AddField(
|
||||
model_name='activitytemplate',
|
||||
name='name',
|
||||
field=models.CharField(default='change_me!', help_text='Ne sera pas affiché', max_length=200, verbose_name='Nom du template'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='activity',
|
||||
name='event',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='activitytemplate',
|
||||
name='event',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'),
|
||||
),
|
||||
]
|
26
event/migrations/0003_auto_20170726_1116.py
Normal file
26
event/migrations/0003_auto_20170726_1116.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# -*- 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'),
|
||||
),
|
||||
]
|
322
event/models.py
322
event/models.py
|
@ -1,47 +1,36 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import FieldDoesNotExist, FieldError, ValidationError
|
||||
from django.db import models
|
||||
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
|
||||
from communication.models import SubscriptionMixin
|
||||
|
||||
from .validators import ColorValidator
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
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,
|
||||
verbose_name=_("créé par"),
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="created_events",
|
||||
editable=False, null=True,
|
||||
)
|
||||
User,
|
||||
related_name="created_events",
|
||||
editable=False,
|
||||
)
|
||||
created_at = models.DateTimeField(
|
||||
_('date de création'),
|
||||
auto_now_add=True,
|
||||
)
|
||||
description = models.TextField(_('description'))
|
||||
beginning_date = models.DateTimeField(
|
||||
_('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"),
|
||||
)
|
||||
beginning_date = models.DateTimeField(_('date de début'))
|
||||
ending_date = models.DateTimeField(_('date de fin'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("évènement")
|
||||
|
@ -56,14 +45,12 @@ 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."
|
||||
),
|
||||
on_delete=models.CASCADE,
|
||||
blank=True, null=True,
|
||||
help_text=_("Si spécifié, l'instance du modèle "
|
||||
"est spécifique à l'évènement en question"),
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -72,9 +59,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:
|
||||
|
@ -87,22 +74,26 @@ 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(
|
||||
_("est public"),
|
||||
help_text=_(
|
||||
"Sert à faire une distinction dans l'affichage selon que le tag "
|
||||
"soit destiné au public ou à l'organisation."
|
||||
),
|
||||
)
|
||||
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."),
|
||||
)
|
||||
color = models.CharField(
|
||||
_('couleur'),
|
||||
max_length=7,
|
||||
validators=[ColorValidator],
|
||||
help_text=_("Rentrer une couleur en hexadécimal (#XXX ou #XXXXXX)."),
|
||||
)
|
||||
_('couleur'),
|
||||
max_length=7,
|
||||
validators=[color_regex],
|
||||
help_text=_("Rentrer une couleur en hexadécimal"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("tag")
|
||||
|
@ -114,206 +105,99 @@ 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,
|
||||
verbose_name=_("évènement"),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
event = models.ForeignKey(Event)
|
||||
is_public = models.NullBooleanField(
|
||||
_("est public"),
|
||||
blank=True,
|
||||
)
|
||||
blank=True,
|
||||
)
|
||||
has_perm = models.NullBooleanField(
|
||||
_("inscription de permanents"),
|
||||
blank=True,
|
||||
)
|
||||
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=_("Visible par tout le monde si l'événément est public."),
|
||||
blank=True, null=True,
|
||||
)
|
||||
_('description'),
|
||||
help_text=_("Public, Visible par tout le monde."),
|
||||
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,
|
||||
verbose_name=_('tags'),
|
||||
blank=True,
|
||||
)
|
||||
places = models.ManyToManyField(
|
||||
Place,
|
||||
verbose_name=_('lieux'),
|
||||
blank=True,
|
||||
)
|
||||
ActivityTag,
|
||||
blank=True,
|
||||
)
|
||||
place = models.ManyToManyField(
|
||||
Place,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class ActivityTemplate(AbstractActivityTemplate):
|
||||
name = models.CharField(
|
||||
_("Nom du template"),
|
||||
max_length=200,
|
||||
help_text=_("Ne sera pas affiché"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("template activité")
|
||||
verbose_name_plural = _("templates activité")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def clean(self):
|
||||
errors = []
|
||||
|
||||
# On clean les nombre de permanents
|
||||
if not self.has_perm:
|
||||
self.max_perm = None
|
||||
self.min_perm = None
|
||||
else:
|
||||
if self.min_perm > self.max_perm:
|
||||
errors.append(ValidationError(
|
||||
_("Nombres de permanents incompatibles"),
|
||||
code='wrong-nb-perm',
|
||||
))
|
||||
if errors != []:
|
||||
raise ValidationError(errors)
|
||||
return self.title
|
||||
|
||||
|
||||
class Activity(AbstractActivityTemplate):
|
||||
parent = models.ForeignKey(
|
||||
ActivityTemplate,
|
||||
verbose_name=_("template"),
|
||||
on_delete=models.PROTECT,
|
||||
related_name="children",
|
||||
blank=True, null=True,
|
||||
)
|
||||
ActivityTemplate,
|
||||
related_name="children",
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
staff = models.ManyToManyField(
|
||||
User,
|
||||
verbose_name=_("permanents"),
|
||||
related_name="in_perm_activities",
|
||||
blank=True,
|
||||
)
|
||||
User,
|
||||
related_name="in_perm_activities",
|
||||
blank=True,
|
||||
)
|
||||
|
||||
beginning = models.DateTimeField(_("heure de début"))
|
||||
end = models.DateTimeField(_("heure de fin"))
|
||||
|
||||
def clean(self):
|
||||
errors = []
|
||||
|
||||
# On clean les nombre de permanents
|
||||
if not self.get_herited('has_perm'):
|
||||
self.max_perm = None
|
||||
self.min_perm = None
|
||||
else:
|
||||
if self.get_herited('min_perm') > self.get_herited('max_perm'):
|
||||
errors.append(ValidationError(
|
||||
_("Nombres de permanents incompatibles"),
|
||||
code='wrong-nb-perm',
|
||||
))
|
||||
|
||||
# On valide l'héritage
|
||||
for f in self._meta.get_fields():
|
||||
try:
|
||||
# On réccupère le field du parent
|
||||
attrname = f.name
|
||||
tpl_field = ActivityTemplate._meta.get_field(attrname)
|
||||
# Peut-être que ce n'est pas un field
|
||||
# concerné par l'héritage
|
||||
except FieldDoesNotExist:
|
||||
continue
|
||||
|
||||
# Y'a certains champs dont on se moque
|
||||
if attrname in ['id', 'staff', 'tags', ]:
|
||||
continue
|
||||
|
||||
# C'est plus compliqué que ça pour les nb_perm
|
||||
if attrname in ['max_perm', 'min_perm', ]:
|
||||
if not self.get_herited('has_perm'):
|
||||
continue
|
||||
|
||||
|
||||
# On a un Many to Many, on lit différement
|
||||
if tpl_field.many_to_many:
|
||||
pass
|
||||
# # On a pas spécifié
|
||||
# if not value.exists():
|
||||
# # On a pas de parent
|
||||
# if self.parent is None:
|
||||
# errors.append(ValidationError(
|
||||
# _("N'hérite pas d'un template, spécifier le champs : %(attr)s"),
|
||||
# code='bad-overriding',
|
||||
# params={'attr': f.verbose_name},
|
||||
# ))
|
||||
# else:
|
||||
# pvalue = getattr(self.parent, attrname)
|
||||
# # On a un parent qui ne dit rien
|
||||
# if not pvalue.exists():
|
||||
# errors.append(ValidationError(
|
||||
# _("Champs non précisé chez le parent, spécifier : %(attr)s"),
|
||||
# code='bad-overriding',
|
||||
# params={'attr': f.verbose_name},
|
||||
# ))
|
||||
else:
|
||||
value = getattr(self, attrname)
|
||||
# On a pas spécifié
|
||||
if value is None:
|
||||
# On a pas de parent
|
||||
if self.parent is None:
|
||||
errors.append(ValidationError(
|
||||
_("N'hérite pas d'un template, spécifier le champs : %(attr)s"),
|
||||
code='bad-overriding',
|
||||
params={'attr': f.verbose_name},
|
||||
))
|
||||
else:
|
||||
pvalue = getattr(self.parent, attrname)
|
||||
# On a un parent qui ne dit rien
|
||||
if pvalue is None:
|
||||
errors.append(ValidationError(
|
||||
_("Champs non précisé chez le parent, spécifier : %(attr)s"),
|
||||
code='bad-overriding',
|
||||
params={'attr': f.verbose_name},
|
||||
))
|
||||
if errors != []:
|
||||
raise ValidationError(errors)
|
||||
|
||||
|
||||
def get_herited(self, attrname):
|
||||
try:
|
||||
tpl_field = ActivityTemplate._meta.get_field(attrname)
|
||||
except FieldDoesNotExist:
|
||||
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:
|
||||
raise FieldError(
|
||||
"%(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
|
||||
elif self.parent is not None:
|
||||
_("%(attrname)s n'est pas un champ héritable"),
|
||||
params={'attrname': attrname},
|
||||
)
|
||||
elif attrname in m2m_fields:
|
||||
if attr.exists():
|
||||
return attr
|
||||
else:
|
||||
return getattr(self.parent, attrname)
|
||||
elif value is None and self.parent is not None:
|
||||
elif attr is None:
|
||||
return getattr(self.parent, attrname)
|
||||
else:
|
||||
return value
|
||||
return attr
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("activité")
|
||||
|
|
|
@ -1,373 +0,0 @@
|
|||
/* Calendar */
|
||||
|
||||
#cal-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
|
||||
min-height: 80vh;
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
#cal-container,
|
||||
#cal-container * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Time slots */
|
||||
|
||||
#cal-container .cal-time-slot-container {
|
||||
display: grid;
|
||||
grid-template-rows: 30px auto;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(100% + 30px + 10px);
|
||||
padding: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot {
|
||||
border-right: 1px solid #EEE;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot:nth-child(even) {
|
||||
background-color: #F4F4F4;
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot-hour {
|
||||
padding: 0 0 0 calc(100% - 1.4rem + 7px);
|
||||
background-color: #F9F9F9;/* #3D3D3D; */
|
||||
border-bottom: 1px solid #AAA;
|
||||
color: #333;
|
||||
font-size: 1.4rem;
|
||||
line-height: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot-hour:nth-child(even) {
|
||||
background-color: #FCFCFC;/* #464646; */
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot-hour:first-child {
|
||||
color: transparent;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot-hour.cal-last-hour {
|
||||
padding: 0;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot.cal-last-hour,
|
||||
#cal-container .cal-time-slot-hour.cal-last-hour {
|
||||
border-right: 1px solid #AAA;
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot.cal-first-hour,
|
||||
#cal-container .cal-time-slot-hour.cal-first-hour {
|
||||
border-left: 1px solid #AAA;
|
||||
}
|
||||
|
||||
/* Events */
|
||||
|
||||
#cal-container .cal-event-container {
|
||||
display: grid;
|
||||
/* grid-template-columns: repeat(24, 1fr); */
|
||||
/* grid-template-rows: repeat(12, auto); */
|
||||
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#cal-container .cal-event {
|
||||
position: relative;
|
||||
height: 80px;
|
||||
margin: 2px 0;
|
||||
/* padding: 5px; */
|
||||
/* background-color: #EFEFEF; */
|
||||
border-radius: 3px;
|
||||
/* border: 1px solid #CCC; */
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
/* z-index: 500; */
|
||||
transition: 50ms ease-in;
|
||||
}
|
||||
|
||||
#cal-container .cal-event:hover {
|
||||
/* background-color: #FEDDDD; */
|
||||
}
|
||||
|
||||
#cal-container .cal-event > * {
|
||||
display: none;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
#cal-container .cal-event > .cal-event-name,
|
||||
#cal-container .cal-event > .cal-event-location,
|
||||
#cal-container .cal-event > .cal-event-perm-count {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#cal-container .cal-event > .cal-event-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#cal-container .cal-event > .cal-event-location {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#cal-container .cal-event > .cal-event-perm-count {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#cal-container .cal-event:not(.cal-event-subscribed) > .cal-event-perm-count.cal-perms-missing {
|
||||
width: calc(100% - 10px);
|
||||
right: auto;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
background-color: #FFF;
|
||||
border: 2px solid #E44;
|
||||
color: #E44;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#cal-container .cal-event.cal-event-subscribed {
|
||||
border-width: 3px;
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
#cal-container .cal-event.cal-event-subscribed::after {
|
||||
content: "✔";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 1px;
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
|
||||
/* Event details popup */
|
||||
|
||||
#cal-container .cal-event-details {
|
||||
position: absolute;
|
||||
min-height: 100px;
|
||||
/* min-width: 40%; */
|
||||
max-width: 80%;
|
||||
padding: 20px;
|
||||
background-color: #333;
|
||||
color: #FFF;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.6);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details:after {
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-bottom-color: #333;
|
||||
border-width: 20px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details.above-event:after {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-top-color: #333;
|
||||
border-width: 20px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details * {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-close-button {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
font-size: 1.6rem;
|
||||
color: #BBB;
|
||||
transition: 100ms ease-out;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-close-button:hover {
|
||||
background-color: #484848;
|
||||
color: #EFEFEF;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details a,
|
||||
#cal-container .cal-event-details a:hover {
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-name > h3 {
|
||||
margin: 0 0 25px 26px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
color: #FFF;
|
||||
font-size: 2.5rem;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-name > h3::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 0 0 10px;
|
||||
vertical-align: middle;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAA90lEQVQoz2WRsS6DURiGn/yaaNqBySpGicFg6iJcAYuuNBaJpJEg6gLcADFIdWR0BZZKS4SBRTe3YFf/Y/j/c3LUe4Zz3vd78+U734uUJ7Ptg7k68NDpoIdyU9V3TzzwRdXt1HCq9pwR510W616oZ8Gwoe6KWFN1ScStogui3sRJ9uxYLd/n6hTuaCy3XHfNWuR6hM+OojBQdSXyJ0cZizwSsMo3kEc+ZCEjxZgxE8j4oBFZhRww8gaff4fEL3UuGfK4uG5L4VLVfvrNCrDJHfd0gSrXQB2AJvu0+Cm8HbXnbGw9seqwWH2z65Wv/8MKcfdVHaZx/wKtOg5kifzQhwAAAABJRU5ErkJggg==);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-name:hover > h3 {
|
||||
margin-right: 26px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #484848;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-name:hover > h3::after {
|
||||
content: "Cliquez pour afficher les détails.";
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 15px;
|
||||
padding: 2px 0 0 0;
|
||||
font-size: 1.2rem;
|
||||
background-image: none;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-name:hover + .cal-detail-close-button {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details table {
|
||||
margin: 0 auto;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details td.cal-detail-label {
|
||||
padding: 0 10px 10px 0;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details td.cal-detail-value {
|
||||
padding: 0 0 10px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-area {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #DFDFDF;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-title {
|
||||
display: block;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-count {
|
||||
margin: 0 10px 0 0;
|
||||
font-size: 2.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-count.cal-perms-missing {
|
||||
color: #E44;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-count.cal-perms-full {
|
||||
color: #393;
|
||||
}
|
||||
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-subscription-switch {
|
||||
margin: 0 0 0 10px;
|
||||
padding: 10px;
|
||||
font-size: 1.8rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-nb-missing-perms {
|
||||
margin: 20px 0 0 0;
|
||||
padding: 5px;
|
||||
background-color: #FFF;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
color: #E44;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-description {
|
||||
margin: 20px 0 20px 0;
|
||||
color: #DDD;
|
||||
font-size: 1.8rem;
|
||||
font-style: italic;
|
||||
text-align: justify;
|
||||
line-height: 120%;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-tag {
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
border: 1px solid #DDD;
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
/* Tipso Bubble Styles */
|
||||
.tipso_bubble, .tipso_bubble > .tipso_arrow{
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tipso_bubble {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
z-index: 9999;
|
||||
}
|
||||
.tipso_style{
|
||||
/* cursor: help; */
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
.tipso_title {
|
||||
padding: 3px 0;
|
||||
border-radius: 6px 6px 0 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
.tipso_content {
|
||||
word-wrap: break-word;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
/* Tipso Bubble size classes - Similar to Foundation's syntax*/
|
||||
.tipso_bubble.tiny {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
.tipso_bubble.small {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.tipso_bubble.default {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.tipso_bubble.large {
|
||||
font-size: 1.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tipso_bubble.cal_small {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
/* Tipso Bubble Div */
|
||||
.tipso_bubble > .tipso_arrow{
|
||||
position: absolute;
|
||||
width: 0; height: 0;
|
||||
border: 8px solid;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tipso_bubble.top > .tipso_arrow {
|
||||
border-top-color: #000;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
}
|
||||
.tipso_bubble.bottom > .tipso_arrow {
|
||||
border-bottom-color: #000;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-top-color: transparent;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
}
|
||||
.tipso_bubble.left > .tipso_arrow {
|
||||
border-left-color: #000;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-right-color: transparent;
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
margin-top: -8px;
|
||||
}
|
||||
.tipso_bubble.right > .tipso_arrow {
|
||||
border-right-color: #000;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
top: 50%;
|
||||
right: 100%;
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.tipso_bubble .top_right_corner,
|
||||
.tipso_bubble.top_right_corner {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.tipso_bubble .bottom_right_corner,
|
||||
.tipso_bubble.bottom_right_corner {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
.tipso_bubble .top_left_corner,
|
||||
.tipso_bubble.top_left_corner {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.tipso_bubble .bottom_left_corner,
|
||||
.tipso_bubble.bottom_left_corner {
|
||||
border-top-right-radius: 0;
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,18 +0,0 @@
|
|||
$(function(){
|
||||
function initEnrolment(elt) {
|
||||
elt = $(elt);
|
||||
elt.find("form.enrolment").on("submit", function() {
|
||||
elt.addClass("sending-request");
|
||||
var form = this;
|
||||
var url = form.action + "?ajax";
|
||||
$.post(url, $(form).serialize(), function(data) {
|
||||
elt.html(data);
|
||||
elt.removeClass("sending-request");
|
||||
initEnrolment(elt);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
$.each($(".activity-summary"), function(i, item) { initEnrolment(item) });
|
||||
});
|
|
@ -1,84 +0,0 @@
|
|||
// Interval graph coloring algorithm, by Twal
|
||||
|
||||
class IntervalColoration {
|
||||
constructor (intervals) {
|
||||
this.intervals = intervals;
|
||||
this.n = this.intervals.length;
|
||||
this.computeInterferenceGraph();
|
||||
this.computePEO();
|
||||
this.computeColoration();
|
||||
}
|
||||
|
||||
computeInterferenceGraph() {
|
||||
this.adj = new Array(this.n);
|
||||
for (let i = 0; i < this.n; ++i) {
|
||||
this.adj[i] = [];
|
||||
}
|
||||
for (let i = 0; i < this.n; ++i) {
|
||||
for (let j = 0; j < i; ++j) {
|
||||
let inti = this.intervals[i];
|
||||
let intj = this.intervals[j];
|
||||
if (inti[0] < intj[1] && intj[0] < inti[1]) {
|
||||
this.adj[i].push(j);
|
||||
this.adj[j].push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Perfect elimination order using Maximum Cardinality Search
|
||||
//Runs in O(n^2), could be optimized in O(n log n)
|
||||
computePEO() {
|
||||
let marked = new Array(this.n);
|
||||
let nbMarkedNeighbor = new Array(this.n);
|
||||
this.perm = new Array(this.n);
|
||||
for (let i = 0; i < this.n; ++i) {
|
||||
marked[i] = false;
|
||||
nbMarkedNeighbor[i] = 0;
|
||||
}
|
||||
for (let k = this.n-1; k >= 0; --k) {
|
||||
let maxi = -1;
|
||||
for (let i = 0; i < this.n; ++i) {
|
||||
if (!marked[i] && (maxi == -1 || nbMarkedNeighbor[i] >= nbMarkedNeighbor[maxi])) {
|
||||
maxi = i;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < this.adj[maxi].length; ++i) {
|
||||
nbMarkedNeighbor[this.adj[maxi][i]] += 1;
|
||||
}
|
||||
this.perm[maxi] = k;
|
||||
marked[maxi] = true;
|
||||
}
|
||||
// console.log(this.perm);
|
||||
}
|
||||
|
||||
computeColoration() {
|
||||
this.colors = new Array(this.n);
|
||||
let isColorUsed = new Array(this.n);
|
||||
for (let i = 0; i < this.n; ++i) {
|
||||
this.colors[i] = -1;
|
||||
isColorUsed[i] = false;
|
||||
}
|
||||
for (let i = 0; i < this.n; ++i) {
|
||||
let ind = this.perm[i];
|
||||
for (let j = 0; j < this.adj[ind].length; ++j) {
|
||||
let neigh = this.adj[ind][j];
|
||||
if (this.colors[neigh] >= 0) {
|
||||
isColorUsed[this.colors[neigh]] = true;
|
||||
}
|
||||
}
|
||||
for (let j = 0; j < this.n; ++j) {
|
||||
if (!isColorUsed[j]) {
|
||||
this.colors[ind] = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let j = 0; j < this.adj[ind].length; ++j) {
|
||||
let neigh = this.adj[ind][j];
|
||||
if (this.colors[neigh] >= 0) {
|
||||
isColorUsed[this.colors[neigh]] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
event/static/js/tipso.min.js
vendored
1
event/static/js/tipso.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,52 +0,0 @@
|
|||
{% extends "shared/base.html" %}
|
||||
{% load i18n staticfiles event_tags %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}{% trans "Activity" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ activity.title}}
|
||||
{% if user.is_staff %}
|
||||
<a class='glyphicon glyphicon-cog pull-right' href='{% url "admin:event_activity_change" activity.id %}'></a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% include "event/activity_summary.html" with activity=activity %}
|
||||
|
||||
<h2>Description</h2>
|
||||
<p><strong>Description</strong>{{activity.description|default:" - "}}</p>
|
||||
<p><strong>Remarque (staff)</strong>{{activity.remark|default:" - "}}</p>
|
||||
<button class="collapsible active"><h3>Matériel</h3></button>
|
||||
<div class="content fluid">
|
||||
<table class="table table-responsive table-striped">
|
||||
<tr>
|
||||
<th>Matériel</th>
|
||||
<th>Quantité</th>
|
||||
<th>Propriétaire</th>
|
||||
<th>Remarque</th>
|
||||
</tr>
|
||||
{% for att in attributions %}
|
||||
<tr>
|
||||
<td><a href="{% url 'equipment:detail' att.equipment.id %}">
|
||||
{{ att.equipment }}</a></td>
|
||||
<td>{{ att.amount }}</td>
|
||||
<td>{{ att.equipment.owner }}</td>
|
||||
<td>{{ att.remark }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
<div class="text">
|
||||
Du
|
||||
<strong>
|
||||
{{ activity.beginning | date:"l d F Y H:i" }}
|
||||
</strong>
|
||||
au
|
||||
<strong>
|
||||
{{ activity.end | date:"l d F Y H:i" }}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,79 +0,0 @@
|
|||
{% load i18n event_tags %}
|
||||
{% with activity|get_herited:'has_perm' as has_perm %}
|
||||
<table class="table table-responsive table-striped">
|
||||
<tr>
|
||||
<td>
|
||||
<span class="glyphicon glyphicon-tree-deciduous"></span>
|
||||
{% with activity|get_herited:'places' as places %}
|
||||
{% if places.all %}
|
||||
<span>{{ places.all |join:", "}}</span>
|
||||
{% else %}
|
||||
<span> - </span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td>
|
||||
<strong>public</strong>
|
||||
:
|
||||
{% with activity|get_herited:'is_public' as is_public %}
|
||||
<span class="glyphicon {{ is_public|yesno:"yes glyphicon-ok-sign, no glyphicon-remove-sign, dunno glyphicon-question-sign"}}"></span>
|
||||
{% endwith %}
|
||||
</td>
|
||||
<tr>
|
||||
</tr>
|
||||
<td>
|
||||
<span class="glyphicon glyphicon-duplicate"></span>
|
||||
{% if activity.parent %}
|
||||
<span>{{ activity.parent}}</span>
|
||||
{% else %}
|
||||
<span> - </span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<strong>perm</strong>
|
||||
:
|
||||
<span class="glyphicon {{ has_perm|yesno:"yes glyphicon-ok-sign, no glyphicon-remove-sign, dunno glyphicon-question-sign"}}"></span>
|
||||
</td>
|
||||
<tr>
|
||||
</tr>
|
||||
<td>
|
||||
<span class="glyphicon glyphicon-tag"></span>
|
||||
{% with activity|get_herited:'tags' as tags %}
|
||||
{% if tags.all %}
|
||||
<span>{{ tags.all |join:", "}}</span>
|
||||
{% else %}
|
||||
<span> - </span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td>
|
||||
{% if has_perm %}
|
||||
{{ activity|get_herited:'min_perm' }}
|
||||
≤
|
||||
<strong>{{ activity.staff.count }}</strong>
|
||||
≤
|
||||
{{ activity|get_herited:'max_perm' }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if has_perm %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<strong>En perm : </strong>
|
||||
{% with activity.staff.all as staff %}
|
||||
{% if staff %}
|
||||
<span>{{ staff |join:", "}}</span>
|
||||
{% else %}
|
||||
<span> - </span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% enrol_btn activity request.user %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
|
@ -1,2 +1,25 @@
|
|||
{% extends "shared/base.html" %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block sidenav %}
|
||||
<div class="centered">
|
||||
<h5 class="centered banner-text">La Nuit 2017</h5>
|
||||
</div>
|
||||
|
||||
<li>
|
||||
<a href="index.html">
|
||||
<i class="fa fa-dashboard"></i>
|
||||
<span>Looool</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sub-menu">
|
||||
<a href="javascript:;" >
|
||||
<i class="fa fa-desktop"></i>
|
||||
<span>Prout</span>
|
||||
</a>
|
||||
<ul class="sub">
|
||||
<li><a href="general.html">Lolilol</a></li>
|
||||
<li><a href="buttons.html">Lorem</a></li>
|
||||
<li><a href="panels.html">Ipsum</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
{% extends "shared/fluid.html" %}
|
||||
{% load i18n staticfiles event_tags %}
|
||||
|
||||
{% block extra_css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static "css/tipso.css" %}">
|
||||
<link rel="stylesheet" href="{% static "css/calendar.css" %}">
|
||||
|
||||
<style>
|
||||
#cal-toggle-unsubscribed-events-display {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
left: 30px;
|
||||
border-bottom: 2px solid rgb(150, 50, 50);
|
||||
font-size: 1.6rem;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4),
|
||||
0 1px 1px rgba(150, 50, 50, 0.7);
|
||||
text-shadow: 0 0 7px rgb(150, 50, 50);
|
||||
z-index: 5000;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript" src="{% static "js/tipso.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "js/interval_coloration.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "js/calendar.js" %}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(() => {
|
||||
let calendar = new Calendar({
|
||||
startDate: new Date(2018, 10, 30, 8),
|
||||
endDate: new Date(2018, 11, 2, 6),
|
||||
eventDetailURLFormat: "https://cof.ens.fr/poulpe/event/activity/999999",
|
||||
subscriptionURLFormat: "{% url "event:enrol_activity" 999999 %}?ajax=json",
|
||||
csrfToken: $(".planning [name=csrfmiddlewaretoken]").val(),
|
||||
groupEventsByLocation: true
|
||||
});
|
||||
|
||||
// TODO: move this elsewhere
|
||||
// Button to switch between:
|
||||
// - displaying all events (default);
|
||||
// - only displaying events for which the current user is enroled.
|
||||
|
||||
// Create the button
|
||||
let toggleUnsubscribedEventDisplayButton = $("<button>")
|
||||
.attr("type", "button")
|
||||
.attr("id", "cal-toggle-unsubscribed-events-display")
|
||||
.addClass("btn btn-primary")
|
||||
.appendTo(calendar.containerNode);
|
||||
|
||||
// Set/update its label
|
||||
function updateToggleButtonLabel () {
|
||||
if (calendar.onlyDisplaySubscribedEvents) {
|
||||
toggleUnsubscribedEventDisplayButton.html("Afficher toutes les activités");
|
||||
}
|
||||
else {
|
||||
toggleUnsubscribedEventDisplayButton.html("Afficher seulement mes permanences");
|
||||
}
|
||||
}
|
||||
|
||||
updateToggleButtonLabel();
|
||||
|
||||
// Switch between display modes on click
|
||||
toggleUnsubscribedEventDisplayButton.on("click", () => {
|
||||
calendar.toggleEventsNotSubscribedByUser();
|
||||
updateToggleButtonLabel();
|
||||
});
|
||||
|
||||
|
||||
// DEBUG: js console helpers, to be removed
|
||||
console.log(calendar);
|
||||
window["cal"] = calendar;
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="planning">
|
||||
{% csrf_token %}
|
||||
{% regroup activities by beginning|date:"Y-m-d" as days_list %}
|
||||
<div class="content fluid" id="cal-container">
|
||||
{% for day in days_list %}
|
||||
{% for activity in day.list %}
|
||||
<div class="cal-event">
|
||||
<span class="cal-event-id">{{ activity.id }}</span>
|
||||
<span class="cal-event-name">{{ activity|get_herited:'title' }}</span>
|
||||
<span class="cal-event-start-date">{{ activity.beginning | date:"j/m/Y H:i" }}</span>
|
||||
<span class="cal-event-end-date">{{ activity.end | date:"j/m/Y H:i" }}</span>
|
||||
{% with activity|get_herited:'places' as places %}
|
||||
<span class="cal-event-location">{{ places.all | join:", " }}</span>
|
||||
{% endwith %}
|
||||
<span class="cal-event-description">{{ activity.description }}</span>
|
||||
<span class="cal-event-url">{% url "event:activity" activity.id %}</span>
|
||||
|
||||
{% if activity|get_herited:'has_perm' %}
|
||||
<span class="cal-event-has-perms">1</span>
|
||||
<span class="cal-event-min-nb-perms">{{ activity|get_herited:'min_perm' }}</span>
|
||||
<span class="cal-event-max-nb-perms">{{ activity|get_herited:'max_perm' }}</span>
|
||||
<span class="cal-event-nb-perms">{{ activity.staff.count }}</span>
|
||||
<span class="cal-event-subscribed">{% is_enrolled activity request.user %}</span>
|
||||
{% endif %}
|
||||
|
||||
{% with activity|get_herited:'tags' as tags %}
|
||||
{% for tag in tags.all %}
|
||||
<span class="cal-event-tag">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,101 +0,0 @@
|
|||
{% extends "shared/base.html" %}
|
||||
{% load i18n staticfiles event_tags %}
|
||||
|
||||
{% block title %}{% trans "Évènement" %}{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript" src="{% static "js/enrol_event.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ event.title}}
|
||||
{% if perms.event.event_can_change and user.is_staff %}
|
||||
<a class='glyphicon glyphicon-cog pull-right' href='{% url "admin:event_event_change" event.id %}'></a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<p>{{ event.description }}</p>
|
||||
<h2>Boîte à outils</h2>
|
||||
<div class="module-list">
|
||||
<a href="#TODO" class="module">
|
||||
<span class="glyphicon glyphicon-duplicate"></span>
|
||||
Templates d'activité
|
||||
</a>
|
||||
<a href="#TODO" class="module">
|
||||
<span class="glyphicon glyphicon-tag"></span>
|
||||
Tags spécifiques
|
||||
</a>
|
||||
<a href="#todo" class="module">
|
||||
<span class="glyphicon glyphicon-tree-deciduous"></span>
|
||||
lieux spécifiques
|
||||
</a>
|
||||
<a href="{% url "event:calendar" event.slug %}" class="module">
|
||||
Calendrier
|
||||
</a>
|
||||
{% if staffuser %}
|
||||
<a href="{% url "event:event" event.slug %}" class="module">
|
||||
Toutes les activités
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url "event:event-staff" event.slug user.username %}" class="module">
|
||||
Mes perms
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if staffuser %}
|
||||
<h2>Perms de {{ staffuser.first_name }} {{ staffuser.last_name }}</h2>
|
||||
{% else %}
|
||||
<h2>Planning</h2>
|
||||
{% endif %}
|
||||
|
||||
<div class="planning">
|
||||
{% regroup activities by beginning|date:"Y-m-d" as days_list %}
|
||||
{% for day in days_list %}
|
||||
{% with day.list|first as f_act %}
|
||||
<button class="collapsible active"><h3>{{ f_act.beginning|date:"l d F" }}</h3></button>
|
||||
<div class="content fluid">
|
||||
{% endwith %}
|
||||
{% for activity in day.list %}
|
||||
<div class="{% cycle "" "inverted" %} activity">
|
||||
<div class="activity-title">
|
||||
<h4>
|
||||
{% if perms.event.activity_can_change and user.is_staff %}
|
||||
<a class='glyphicon glyphicon-cog' href='{% url "admin:event_activity_change" activity.id %}'></a>
|
||||
{% endif %}
|
||||
<a href="{% url "event:activity" activity.id %}">
|
||||
{{ activity|get_herited:'title' }}
|
||||
</a>
|
||||
</h4>
|
||||
<span class="pull-right">
|
||||
de <strong>{{ activity.beginning | time:"H:i" }}</strong>
|
||||
à <strong>{{ activity.end| time:"H:i" }}</strong>
|
||||
</span>
|
||||
<div class="activity-summary">
|
||||
{% include "event/activity_summary.html" with activity=activity %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
<div class="heading separator">
|
||||
{{ activities.count }} <span class="sub">activité{{ activities.count|pluralize }}</span>
|
||||
</div>
|
||||
<div class="text inverted">
|
||||
<p>Créé le {{ event.created_at | date:"l d F Y à H:i" }} par {{ event.created_by }}</p>
|
||||
<p>
|
||||
Du
|
||||
<strong>
|
||||
{{ event.beginning_date | date:"l d F Y H:i" }}
|
||||
</strong>
|
||||
au
|
||||
<strong>
|
||||
{{ event.ending_date | date:"l d F Y H:i" }}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,6 +0,0 @@
|
|||
{% load i18n %}
|
||||
<form method="POST" action="{% url "event:enrol_activity" activity.pk %}" class="enrolment {{ enrolled|yesno:"enrolled,unenrolled" }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="goal" value="{{ enrolled|yesno:"unenrol,enrol" }}" />
|
||||
{{ enrolled|yesno:_("Inscrit,") }} <input type="submit" value="{{ enrolled|yesno:_("Se désinscrire,S'inscrire") }}" class="btn btn-warning"/>
|
||||
</form>
|
|
@ -1,21 +0,0 @@
|
|||
from django import template
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter()
|
||||
def get_herited(activity, attrname):
|
||||
return activity.get_herited(attrname)
|
||||
|
||||
@register.inclusion_tag("event/tags/enrol_btn.html")
|
||||
def enrol_btn(activity, user):
|
||||
return {
|
||||
"enrolled": activity.staff.filter(id=user.id).exists(),
|
||||
"activity": activity,
|
||||
}
|
||||
|
||||
@register.simple_tag
|
||||
def is_enrolled(activity, user):
|
||||
user_is_enrolled = activity.staff.filter(id=user.id).exists()
|
||||
return "1" if user_is_enrolled else "0"
|
|
@ -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.places.add(self.loge)
|
||||
self.template_act.place.add(self.loge)
|
||||
self.assertEqual(
|
||||
self.real_act.get_herited('places').get(),
|
||||
self.real_act.get_herited('place').get(),
|
||||
self.loge
|
||||
)
|
||||
self.real_act.places.add(self.aqua)
|
||||
self.real_act.place.add(self.aqua)
|
||||
self.assertEqual(
|
||||
self.real_act.get_herited('places').get(),
|
||||
self.real_act.get_herited('place').get(),
|
||||
self.aqua
|
||||
)
|
||||
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
from django.conf.urls import url
|
||||
from event.views import Index, EventView, EventViewStaff, ActivityView, EnrolActivityView, EventCalendar
|
||||
from event.views import Index
|
||||
|
||||
app_name = 'event'
|
||||
urlpatterns = [
|
||||
# url(r'^$', Index.as_view(), name='index'),
|
||||
url(r'^(?P<slug>[-\w]+)/$', EventView.as_view(), name='event'),
|
||||
url(r'^(?P<slug>[-\w]+)/s/(?P<username>[-\w]+)/$', EventViewStaff.as_view(), name='event-staff'),
|
||||
url(r'^(?P<slug>[-\w]+)/calendar/$', EventCalendar.as_view(), name='calendar'),
|
||||
url(r'^activity/(?P<pk>[0-9]+)/$', ActivityView.as_view(),
|
||||
name='activity'),
|
||||
url(r'^activity/(?P<pk>[0-9]+)/enrol/$',
|
||||
EnrolActivityView.as_view(), name="enrol_activity"),
|
||||
url(r'^$', Index.as_view(), name='index'),
|
||||
]
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
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."
|
||||
),
|
||||
)
|
|
@ -1,97 +1,5 @@
|
|||
from django.views.generic import TemplateView, DetailView, View
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.http import JsonResponse, HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
|
||||
from .models import Event, Activity
|
||||
from equipment.models import EquipmentAttribution
|
||||
|
||||
User = get_user_model()
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class Index(TemplateView):
|
||||
template_name = "event/index.html"
|
||||
|
||||
|
||||
class EventCalendar(LoginRequiredMixin, DetailView):
|
||||
model = Event
|
||||
template_name = 'event/calendar.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
event = self.object
|
||||
context['activities'] = (Activity.objects.filter(event=event)
|
||||
.order_by('beginning').prefetch_related(
|
||||
'tags', 'places', 'staff', 'parent'))
|
||||
return context
|
||||
|
||||
|
||||
class EventView(LoginRequiredMixin, DetailView):
|
||||
model = Event
|
||||
template_name = 'event/event.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
event = self.object
|
||||
context['activities'] = (Activity.objects.filter(event=event)
|
||||
.order_by('beginning').prefetch_related(
|
||||
'tags', 'places', 'staff', 'parent'))
|
||||
return context
|
||||
|
||||
|
||||
class EventViewStaff(LoginRequiredMixin, DetailView):
|
||||
model = Event
|
||||
template_name = 'event/event.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = User.objects.get(username=self.kwargs['username'])
|
||||
event = self.object
|
||||
context['staffuser'] = user
|
||||
context['activities'] = (user.in_perm_activities
|
||||
.filter(event=event)
|
||||
.order_by('beginning')
|
||||
.prefetch_related(
|
||||
'tags', 'places', 'staff', 'parent')
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class ActivityView(LoginRequiredMixin, DetailView):
|
||||
model = Activity
|
||||
template_name = 'event/activity.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
activity = self.object
|
||||
context['attributions'] = (EquipmentAttribution.objects
|
||||
.filter(activity=activity)
|
||||
.prefetch_related('equipment'))
|
||||
return context
|
||||
|
||||
|
||||
class EnrolActivityView(LoginRequiredMixin, View):
|
||||
http_method_names = ['post']
|
||||
|
||||
def post(self, request, pk, *args, **kwargs):
|
||||
activity = get_object_or_404(Activity, id=pk)
|
||||
action = request.POST.get("goal", None)
|
||||
success = True
|
||||
if action == "enrol":
|
||||
activity.staff.add(request.user)
|
||||
elif action == "unenrol":
|
||||
activity.staff.remove(request.user)
|
||||
else:
|
||||
success = False
|
||||
if "ajax" in request.GET:
|
||||
if request.GET["ajax"] == "json":
|
||||
enrols = activity.staff
|
||||
return JsonResponse({
|
||||
"enrolled": enrols.filter(id=request.user.id).exists(),
|
||||
"number": enrols.count(),
|
||||
})
|
||||
return render(request, "event/activity_summary.html",
|
||||
{"activity": activity})
|
||||
return HttpResponseRedirect(reverse("event:activity", kwargs={"pk":pk}))
|
||||
|
|
|
@ -3,7 +3,7 @@ import os
|
|||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "poulpe.settings.devlocal")
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "evenementiel.settings.devlocal")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ DBPASSWD="4KZt3nGPLVeWSvtBZPsd9jdssdJMds78"
|
|||
REDIS_PASSWD="dummy"
|
||||
|
||||
# It is used in quite a few places
|
||||
SETTINGS="poulpe.settings.dev"
|
||||
SETTINGS="evenementiel.settings.dev"
|
||||
|
||||
# Fills a "templated file" with the information specified in the variables above
|
||||
# e.g. every occurrence of {{DBUSER}} in the file will be replaced by the value
|
||||
|
@ -90,9 +90,9 @@ redis-cli -a $REDIS_PASSWD CONFIG REWRITE
|
|||
cd /vagrant
|
||||
|
||||
# Setup the secrets
|
||||
sudo -H -u vagrant cp poulpe/settings/secret_example.py \
|
||||
poulpe/settings/secret.py
|
||||
fill_template poulpe/settings/secret.py
|
||||
sudo -H -u vagrant cp evenementiel/settings/secret_example.py \
|
||||
evenementiel/settings/secret.py
|
||||
fill_template evenementiel/settings/secret.py
|
||||
|
||||
# Run the usual django admin commands
|
||||
function venv_python {
|
||||
|
|
|
@ -10,7 +10,7 @@ TimeoutSec=300
|
|||
WorkingDirectory=/vagrant
|
||||
Environment="DJANGO_SETTINGS_MODULE={{SETTINGS}}"
|
||||
ExecStart=/home/vagrant/venv/bin/daphne -u /srv/GE/GE.sock \
|
||||
poulpe.asgi:channel_layer
|
||||
evenementiel.asgi:channel_layer
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
-r requirements.txt
|
||||
django-debug-toolbar
|
||||
django-debug-panel
|
||||
ipython
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
-r requirements.txt
|
||||
|
||||
# Production specific
|
||||
daphne==1.3.0
|
||||
psycopg2
|
||||
gunicorn
|
|
@ -1,14 +1,14 @@
|
|||
asgi-redis==1.3.0
|
||||
asgiref==1.1.1
|
||||
django-bootstrap3==10.0.1
|
||||
channels==1.1.5
|
||||
Django==2.1
|
||||
django-tables2==2.0.0a2
|
||||
django-filter==2.0.0
|
||||
git+https://git.eleves.ens.fr/klub-dev-ens/django-allauth-ens.git@1.1.3
|
||||
django-bootstrap-form==3.4
|
||||
Django==1.11.*
|
||||
psycopg2
|
||||
asgi-redis
|
||||
Pillow
|
||||
channels
|
||||
django-bootstrap-form==3.2.1
|
||||
django-widget-tweaks
|
||||
djangorestframework==3.6.3
|
||||
drf-nested-routers==0.90.0
|
||||
django-notifications==0.1.dev0
|
||||
django-contrib-comments==1.8.0
|
||||
django-taggit==0.22.2
|
||||
Pillow==5.0.0
|
||||
django-notifications
|
||||
django-contrib-comments
|
||||
|
||||
# Production specific
|
||||
daphne
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
default_app_config = 'shared.apps.SharedConfig'
|
|
@ -1,34 +1,3 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.admin import AdminSite
|
||||
from django.contrib.sites.admin import SiteAdmin
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.admin import UserAdmin, GroupAdmin
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CustomAdminSite(AdminSite):
|
||||
site_header = "Administration du Poulpe"
|
||||
site_title = "Poulpe"
|
||||
index_title = "Administration"
|
||||
|
||||
def index(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
|
||||
# Move last app to the top of `app_list`.
|
||||
# TODO fournir un bon ordre
|
||||
app_list = self.get_app_list(request)
|
||||
app_list.insert(0, app_list.pop(-1))
|
||||
|
||||
extra_context['app_list'] = app_list
|
||||
return super().index(request, extra_context)
|
||||
|
||||
|
||||
# admin.site = CustomAdminSite(name='admin')
|
||||
admin.site.register(User, UserAdmin)
|
||||
# admin.site.register(Group, GroupAdmin)
|
||||
# admin.site.register(Site, SiteAdmin)
|
||||
# Register your models here.
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
from allauth.account.adapter import DefaultAccountAdapter
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
|
||||
|
||||
class AccountAdapter(DefaultAccountAdapter):
|
||||
def is_open_for_signup(self, request):
|
||||
return False
|
||||
|
||||
|
||||
class SocialAccountAdapter(DefaultSocialAccountAdapter):
|
||||
def is_open_for_signup(self, request, sociallogin):
|
||||
# sociallogin.account is a SocialAccount instance.
|
||||
# See https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/models.py
|
||||
|
||||
if sociallogin.account.provider == 'clipper':
|
||||
return True
|
||||
|
||||
# It returns AccountAdapter.is_open_for_signup().
|
||||
# See https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/adapter.py
|
||||
return super().is_open_for_signup(request, sociallogin)
|
|
@ -1,10 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
from django.contrib.admin.apps import AdminConfig
|
||||
|
||||
|
||||
class SharedConfig(AppConfig):
|
||||
name = 'shared'
|
||||
|
||||
|
||||
class CustomAdminConfig(AdminConfig):
|
||||
default_site = 'shared.admin.CustomAdminSite'
|
||||
|
|
|
@ -11,7 +11,6 @@ class EventSpecificMixin(models.Model):
|
|||
verbose_name=_("évènement"),
|
||||
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
|
||||
)
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
require 'compass/import-once/activate'
|
||||
# Require any additional compass plugins here.
|
||||
|
||||
# Set this to the root of your project when deployed:
|
||||
http_path = "/"
|
||||
css_dir = "css"
|
||||
sass_dir = "sass"
|
||||
images_dir = "images"
|
||||
javascripts_dir = "javascripts"
|
||||
|
||||
# You can select your preferred output style here (can be overridden via the command line):
|
||||
# output_style = :expanded or :nested or :compact or :compressed
|
||||
|
||||
# To enable relative paths to assets via compass helper functions. Uncomment:
|
||||
# relative_assets = true
|
||||
|
||||
# To disable debugging comments that display the original location of your selectors. Uncomment:
|
||||
# line_comments = false
|
||||
|
||||
|
||||
# If you prefer the indented syntax, you might want to regenerate this
|
||||
# project again passing --syntax sass, or you can uncomment this:
|
||||
# preferred_syntax = :sass
|
||||
# and then run:
|
||||
# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass
|
|
@ -1,114 +0,0 @@
|
|||
//@import "compass/css3";
|
||||
|
||||
//Variables here:
|
||||
//(alongside with commented suggestions)
|
||||
$foreground-color:#b85b3f;//black;
|
||||
$background-color:#e8e3c7;//white
|
||||
$shadow-color:#ba9186;//$foreground-color;
|
||||
$distance:8px;
|
||||
$cut-distance:3px;//$distance/4;
|
||||
$strips-size:6px; //10px
|
||||
$strips-ratio:50%;//70%
|
||||
$strips-angle:45deg;//90deg;
|
||||
|
||||
//cray stuff yo. be sure to try (if you please)
|
||||
$animate:false;//true
|
||||
$fixed:false;//true
|
||||
|
||||
body{
|
||||
font-family: 'Open Sans Condensed', sans-serif;
|
||||
font-size:85pt;
|
||||
background-color:$background-color;
|
||||
text-align:center;
|
||||
line-height:1.2em;
|
||||
padding-top:70px;
|
||||
}
|
||||
.dashed-shadow{
|
||||
position:relative;
|
||||
top:$distance;
|
||||
left:$distance;
|
||||
display:inline-block;
|
||||
color:$shadow-color;
|
||||
}
|
||||
.dashed-shadow:before{
|
||||
content:" ";
|
||||
display:block;
|
||||
|
||||
position:absolute;
|
||||
$bleeding-horizontal:10px;
|
||||
$bleeding-vertical:0px;
|
||||
top:-$bleeding-vertical - $distance;
|
||||
left:-$bleeding-vertical - $distance;
|
||||
bottom:-$bleeding-horizontal + $distance;
|
||||
right:-$bleeding-horizontal + $distance;
|
||||
z-index:1;
|
||||
$color:$background-color;
|
||||
$size:$strips-ratio/2;
|
||||
$halfSize:$size/2;
|
||||
$p1:$halfSize;
|
||||
$p2:50%-$halfSize;
|
||||
$p3:50%+$halfSize;
|
||||
$p4:100%-$halfSize;
|
||||
$transparent:transparentize($color,1);
|
||||
@include background-image(linear-gradient($strips-angle,$color $p1, $transparent $p1, $transparent $p2,$color $p2, $color $p3, $transparent $p3, $transparent $p4, $color $p4));
|
||||
background-size:$strips-size $strips-size;
|
||||
@if($animate){
|
||||
animation:dash-animation 30s infinite linear;
|
||||
}
|
||||
@if($fixed){
|
||||
background-attachment:fixed;
|
||||
}
|
||||
}
|
||||
.dashed-shadow:hover:before{
|
||||
animation:dash-animation 30s infinite linear;
|
||||
}
|
||||
|
||||
.dashed-shadow:after{
|
||||
z-index:2;
|
||||
content:attr(data-text);
|
||||
position:absolute;
|
||||
left:-$distance;
|
||||
top:-$distance;
|
||||
color:$foreground-color;
|
||||
text-shadow:$cut-distance $cut-distance $background-color;
|
||||
}
|
||||
|
||||
//fancy stuff - just useless fluff, don't mind from here onwards
|
||||
|
||||
.hello{
|
||||
font-family:'Cookie',cursive;
|
||||
font-size:140pt;
|
||||
}
|
||||
.sorta-block{
|
||||
font-size:50pt;
|
||||
line-height:1.1em;
|
||||
@include transform(skew(0,-5deg));
|
||||
z-index:3;
|
||||
position:relative;
|
||||
margin-top:20px;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
.sorta{
|
||||
border-top:4px solid $foreground-color;
|
||||
border-bottom:4px solid $foreground-color;
|
||||
|
||||
text-transform:uppercase;
|
||||
z-index:3;
|
||||
//position:relative;
|
||||
//display:block;
|
||||
//width:300px;
|
||||
font-style:italic;
|
||||
}
|
||||
.hipsterish{
|
||||
font-family: 'Sancreek', cursive;
|
||||
font-size:70pt;
|
||||
}
|
||||
.dashed-shadow-text{
|
||||
font-size:140pt;
|
||||
line-height:0.7em;
|
||||
//left:-10px;
|
||||
}
|
||||
.shadow{
|
||||
font-size:120pt;
|
||||
line-height:0.8em;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
// main: global.scss
|
||||
#filter_form {
|
||||
.form-group {
|
||||
.col-md-3, .col-md-9 {
|
||||
float: none;
|
||||
}
|
||||
ul.form-control {
|
||||
padding-left: 15px;
|
||||
list-style: none;
|
||||
height: auto;
|
||||
|
||||
a.selected {
|
||||
text-decoration: underline;
|
||||
color: darken($main_soft_color, 40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
// main: global.scss
|
||||
@mixin active {
|
||||
&:active,
|
||||
&.active {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin hover-focus {
|
||||
&:focus,
|
||||
&.focus,
|
||||
&:hover {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin disabled {
|
||||
&.disabled,
|
||||
&[disabled],
|
||||
fieldset[disabled] & {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin dropdown-open {
|
||||
.open > &.dropdown-toggle { @content }
|
||||
}
|
||||
|
||||
@mixin btn-special {
|
||||
/**
|
||||
* This mixin applies content if the button is in at least one of the
|
||||
* following states:
|
||||
*
|
||||
* - hovered,
|
||||
* - focused,
|
||||
* - actived,
|
||||
* - is responsible of an opened dropdown.
|
||||
*
|
||||
* Where possible, state is checked from class attribute and
|
||||
* :pseudo-classes.
|
||||
*
|
||||
* ## Bootstrap compatibility
|
||||
*
|
||||
* If content defines 'color', 'background-color' and 'border', it is safe
|
||||
* to use this mixin with Bootstrap buttons as it will overrides all
|
||||
* Bootstrap color defaults of the previous cases.
|
||||
* To be precise, this covers all special important-like cases of the
|
||||
* Bootstrap mixin 'button-variant' (except the 'disabled' case).
|
||||
*
|
||||
*/
|
||||
@include hover-focus { @content }
|
||||
|
||||
@include active {
|
||||
@content;
|
||||
@include hover-focus { @content }
|
||||
}
|
||||
|
||||
@include dropdown-open {
|
||||
@content;
|
||||
@include hover-focus { @content }
|
||||
}
|
||||
}
|
||||
|
||||
@mixin button-variant-2modes($color, $background-base, $background-special, $border) {
|
||||
/**
|
||||
* This mixins allows defining color-related properties of buttons.
|
||||
*
|
||||
* It sets the following properties:
|
||||
* color: $color, except for disabled-like buttons.
|
||||
* border-color: $border.
|
||||
* background-color: Depending on button state:
|
||||
* - Default, disabled:
|
||||
* $background-base
|
||||
* - Hovered, focused, actived, responsible of an opened dropdown:
|
||||
* (one is sufficent)
|
||||
* $background-special
|
||||
*
|
||||
* ## Bootstrap compatibility
|
||||
*
|
||||
* This mixin can be used to replace colors behaviors of Bootstrap buttons.
|
||||
* Indeed, this mixin aims to replace each definition done by the
|
||||
* 'button-variant' Bootstrap mixin.
|
||||
*
|
||||
*/
|
||||
color: $color;
|
||||
background-color: $background-base;
|
||||
border-color: $border;
|
||||
|
||||
@include btn-special {
|
||||
color: $color;
|
||||
background-color: $background-special;
|
||||
border-color: $border;
|
||||
}
|
||||
|
||||
@include disabled {
|
||||
@include hover-focus {
|
||||
background-color: $background-base;
|
||||
border-color: $border;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
color: $background-base;
|
||||
background-color: $color;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@include button-variant-2modes($btn-font-color, $btn-bg-base, $btn-bg-special, $btn-border);
|
||||
}
|
||||
|
||||
form#filter_form {
|
||||
.form-group {
|
||||
padding-right:20px;
|
||||
}
|
||||
|
||||
ul.form-control {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
// main: global.scss
|
||||
.strong-banner {
|
||||
padding-top : 20px;
|
||||
padding-bottom : 10px ;
|
||||
background-color : $header-background;
|
||||
color: $header-color;
|
||||
}
|
||||
|
||||
|
||||
.navbar-inverse {
|
||||
background-color : $header-background;
|
||||
background-color : transparent ;
|
||||
border-style : none ;
|
||||
.navbar-nav {
|
||||
& > .open > a,
|
||||
& > .open > a:focus,
|
||||
& > .open > a:hover {
|
||||
color: #fff;
|
||||
background-color: $header-second-backgroud;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
border-top: 0px solid transparent ;
|
||||
padding : 0px ;
|
||||
|
||||
/* only < 768px*/
|
||||
background-color : $header-second-backgroud;
|
||||
padding-left: 25px;
|
||||
margin-left: -15px;
|
||||
margin-right: -15px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
background-color : transparent;
|
||||
padding-left: 0px;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.navbar-nav {
|
||||
width: 100%;
|
||||
@media (min-width: 768px) {
|
||||
float : right ;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-inverse {
|
||||
/* BRAND */
|
||||
.navbar-brand {
|
||||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: white;
|
||||
font-family: $font_brand;
|
||||
font-size: xx-large;
|
||||
border-bottom: 5px solid $underline-brand;
|
||||
}
|
||||
}
|
||||
/* ICONE */
|
||||
.navbar-toggle {
|
||||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $second_bold_color;
|
||||
border-color: $second_bold_color;
|
||||
}
|
||||
.icon-bar {
|
||||
background-color: $second_white_color;
|
||||
}
|
||||
}
|
||||
/* LINKS */
|
||||
.navbar-nav {
|
||||
& > li > a {
|
||||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
font-family: $font_nav;
|
||||
font-size: large;
|
||||
color: $second_bold_color;
|
||||
background: transparent;
|
||||
@media (min-width: 768px) {
|
||||
color: $second_white_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue