Compare commits

..

5 commits

Author SHA1 Message Date
Qwann
8568199de4 useless(2): ordering 2017-07-26 20:20:47 +02:00
Qwann
8d29049989 useless 2017-07-26 20:10:40 +02:00
Qwann
6bb176be01 Activity serialized 2017-07-26 19:27:28 +02:00
Qwann
2412c8344f Merge branch 'Qwann/Serializers' into Qwann/Serializers_event2 2017-07-26 15:51:59 +02:00
Qwann
26a62ea30b WIP Activity serializer 2017-07-25 18:42:20 +02:00
198 changed files with 9705 additions and 11294 deletions

3
.gitignore vendored
View file

@ -1,8 +1,7 @@
.vagrant/
__pycache__
venv
poulpe/settings.py
evenementiel/settings.py
.*.swp
*.pyc
*.sqlite3
*.scssc

View file

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

View file

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

View file

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

@ -0,0 +1,5 @@
class EventNestedMixin(object):
pass
class ventSpecificSerializerMixin(object):
pass

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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">&laquo;</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">&raquo;</span>
</a>
</li>
{% endblock pagination.next %}
{% endif %}
</ul>
</nav>
{% endblock pagination %}
{% endif %}
</div>
{% endblock table-wrapper %}

View file

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

View file

@ -1 +0,0 @@
{% include "django/forms/widgets/input.html" %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{{ widget.label }}</label>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,6 +18,6 @@ DATABASES = {
CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgiref.inmemory.ChannelLayer",
"ROUTING": "poulpe.routing.channel_routing",
"ROUTING": "evenementiel.routing.channel_routing",
},
}

View file

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

View file

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

View file

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

View file

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

View file

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

View 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'),
),
]

View file

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

View 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'),
),
]

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

@ -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:"&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;"}}</p>
<p><strong>Remarque (staff)</strong>{{activity.remark|default:"&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;"}}</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 %}

View file

@ -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>&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;</span>
{% endif %}
{% endwith %}
</td>
<td>
<strong>public</strong>
&nbsp;:&nbsp;
{% 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>&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;</span>
{% endif %}
</td>
<td>
<strong>perm</strong>
&nbsp;:&nbsp;
<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>&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;</span>
{% endif %}
{% endwith %}
</td>
<td>
{% if has_perm %}
{{ activity|get_herited:'min_perm' }}
&le;
<strong>{{ activity.staff.count }}</strong>
&le;
{{ 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>&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;</span>
{% endif %}
{% endwith %}
</div>
<div class="col-sm-4">
{% enrol_btn activity request.user %}
</div>
</div>
</div>
{% endif %}
{% endwith %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

View file

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

View file

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

View file

@ -1,3 +1,4 @@
-r requirements.txt
django-debug-toolbar
django-debug-panel
ipython

View file

@ -1,6 +0,0 @@
-r requirements.txt
# Production specific
daphne==1.3.0
psycopg2
gunicorn

View file

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

View file

@ -1 +0,0 @@
default_app_config = 'shared.apps.SharedConfig'

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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%);
}
}
}
}

View file

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

View file

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