diff --git a/.gitignore b/.gitignore index 886ee5c..c067e72 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__ venv evenementiel/settings.py +.*.swp diff --git a/equipment/apps.py b/equipment/apps.py new file mode 100644 index 0000000..ac98343 --- /dev/null +++ b/equipment/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class EquipmentConfig(AppConfig): + name = 'equipment' diff --git a/equipment/migrations/0001_initial.py b/equipment/migrations/0001_initial.py new file mode 100644 index 0000000..ccc9e91 --- /dev/null +++ b/equipment/migrations/0001_initial.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-21 20:14 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('event', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AbstractEquipment', + 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 matériel')), + ('stock', models.PositiveSmallIntegerField(verbose_name='Quantité disponible')), + ('description', models.TextField(verbose_name='Description')), + ], + options={ + 'verbose_name': 'matériel abstrait', + 'verbose_name_plural': 'matériels abstraits', + }, + ), + migrations.CreateModel( + name='EquipmentAttribution', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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')), + ], + options={ + 'verbose_name': 'attribution de matériel', + 'verbose_name_plural': 'attributions de matériel', + }, + ), + 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')), + ('is_broken', models.BooleanField()), + ('is_lost', models.BooleanField()), + ], + options={ + 'verbose_name': 'remarque sur matériel', + 'verbose_name_plural': 'remarques sur le matériel', + }, + ), + migrations.CreateModel( + name='Equipment', + fields=[ + ('abstractequipment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='equipment.AbstractEquipment')), + ], + options={ + 'verbose_name': 'matériel permanent', + 'verbose_name_plural': 'matériels permanents', + }, + bases=('equipment.abstractequipment',), + ), + migrations.CreateModel( + name='TemporaryEquipment', + fields=[ + ('abstractequipment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='equipment.AbstractEquipment')), + ('event', models.ForeignKey(help_text='Évènement pour lequel le matériel a été loué ou emprunté ou apporté', on_delete=django.db.models.deletion.CASCADE, related_name='specific_equipment', to='event.Event')), + ], + options={ + 'verbose_name': 'matériel temporaire', + 'verbose_name_plural': 'matériels temporaires', + }, + bases=('equipment.abstractequipment',), + ), + migrations.AddField( + model_name='equipmentremark', + name='equipment', + field=models.ForeignKey(help_text='Matériel concerné par la remarque', on_delete=django.db.models.deletion.CASCADE, related_name='remarks', to='equipment.AbstractEquipment'), + ), + migrations.AddField( + model_name='equipmentattribution', + name='equipment', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.AbstractEquipment'), + ), + migrations.AddField( + model_name='abstractequipment', + name='activities', + field=models.ManyToManyField(related_name='equipment', through='equipment.EquipmentAttribution', to='event.Activity'), + ), + ] diff --git a/equipment/migrations/__init__.py b/equipment/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/equipment/models.py b/equipment/models.py new file mode 100644 index 0000000..e93cbd3 --- /dev/null +++ b/equipment/models.py @@ -0,0 +1,78 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from event.models import Event, Activity + + +class AbstractEquipment(models.Model): + name = models.CharField( + _("Nom du matériel"), + max_length=200, + ) + stock = models.PositiveSmallIntegerField(_("Quantité disponible")) + description = models.TextField(_("Description")) + activities = models.ManyToManyField( + Activity, + related_name="equipment", + through="EquipmentAttribution", + ) + + class Meta: + verbose_name = _("matériel abstrait") + verbose_name_plural = _("matériels abstraits") + + def __str__(self): + return self.name + + +class Equipment(AbstractEquipment): + class Meta: + verbose_name = _("matériel permanent") + verbose_name_plural = _("matériels permanents") + + +class TemporaryEquipment(AbstractEquipment): + event = models.ForeignKey( + Event, + related_name="specific_equipment", + help_text=_("Évènement pour lequel le matériel " + "a été loué ou emprunté ou apporté"), + ) + + class Meta: + verbose_name = _("matériel temporaire") + verbose_name_plural = _("matériels temporaires") + + +class EquipmentAttribution(models.Model): + equipment = models.ForeignKey(AbstractEquipment) + 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") + verbose_name_plural = _("attributions de matériel") + + def __str__(self): + return "%s (%d) -> %s" % (self.equipment.name, + self.amout, + self.activity.get_herited('title')) + + +class EquipmentRemark(models.Model): + remark = models.TextField("Remarque sur le matériel") + equipment = models.ForeignKey( + AbstractEquipment, + related_name="remarks", + help_text=_("Matériel concerné par la remarque"), + ) + is_broken = models.BooleanField() + is_lost = models.BooleanField() + + class Meta: + verbose_name = _("remarque sur matériel") + verbose_name_plural = _("remarques sur le matériel") + + def __str__(self): + return "%s : %s" % (self.equipment.name, + self.remark) diff --git a/evenementiel/settings_dev.py b/evenementiel/settings_dev.py index 94c8158..e6a6909 100644 --- a/evenementiel/settings_dev.py +++ b/evenementiel/settings_dev.py @@ -31,6 +31,9 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ + 'equipment.apps.EquipmentConfig', + 'event.apps.EventConfig', + 'user.apps.UserConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -131,6 +134,7 @@ USE_TZ = True STATIC_URL = '/static/' + def show_toolbar(request): """ On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar diff --git a/evenementiel/urls.py b/evenementiel/urls.py index 40462fa..4ab8385 100644 --- a/evenementiel/urls.py +++ b/evenementiel/urls.py @@ -1,21 +1,7 @@ -"""evenementiel URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.9/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.conf.urls import url, include - 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) -""" -from django.conf.urls import url +from django.conf.urls import url, include from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), + url(r'^user/', include('user.urls')), ] diff --git a/event/__init__.py b/event/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/event/admin.py b/event/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/event/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/event/apps.py b/event/apps.py new file mode 100644 index 0000000..13b1f16 --- /dev/null +++ b/event/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class EventConfig(AppConfig): + name = 'event' diff --git a/event/migrations/0001_initial.py b/event/migrations/0001_initial.py new file mode 100644 index 0000000..067d32d --- /dev/null +++ b/event/migrations/0001_initial.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-21 20:14 +from __future__ import unicode_literals + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ActivityTag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Nom du tag')), + ('is_public', models.BooleanField(help_text="Sert à faire une distinction dans l'affichage selon que cela soit destiné au public ou à l'équipe organisatrice")), + ('color', models.CharField(help_text='Rentrer une couleur en hexadécimal', max_length=7, validators=[django.core.validators.RegexValidator(message="La chaîne de caractère rentrée n'est pasune couleur en hexadécimal.", regex='^#(?:[0-9a-fA-F]{3}){1,2}$')], verbose_name='Couleur')), + ], + options={ + 'verbose_name': 'tag', + 'verbose_name_plural': 'tags', + }, + ), + migrations.CreateModel( + name='ActivityTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(blank=True, max_length=200, null=True, verbose_name="Nom de l'activité")), + ('is_public', models.NullBooleanField()), + ('has_perm', models.NullBooleanField()), + ('min_perm', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Nombre minimum de permanents')), + ('max_perm', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Nombre maximum de permanents')), + ('description', models.TextField(blank=True, help_text='Public, Visible par tout le monde.', null=True, verbose_name='Description')), + ('remarks', models.TextField(blank=True, help_text='Visible uniquement par les organisateurs', null=True, verbose_name='Remarques')), + ], + options={ + 'verbose_name': 'template activité', + 'verbose_name_plural': 'templates activité', + }, + ), + migrations.CreateModel( + name='Event', + fields=[ + ('title', models.CharField(max_length=200, verbose_name="Nom de l'évènement")), + ('slug', models.SlugField(help_text="Seulement des lettres, des chiffres oules caractères '_' ou '-'.", primary_key=True, serialize=False, 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(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', + 'verbose_name_plural': 'évènements', + }, + ), + migrations.CreateModel( + name='Place', + 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 lieu')), + ('description', models.TextField(blank=True)), + ], + options={ + 'verbose_name': 'lieu', + 'verbose_name_plural': 'lieux', + }, + ), + migrations.CreateModel( + name='Activity', + fields=[ + ('activitytemplate_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='event.ActivityTemplate')), + ], + options={ + 'verbose_name': 'activité', + 'verbose_name_plural': 'activités', + }, + bases=('event.activitytemplate',), + ), + migrations.AddField( + model_name='activitytemplate', + name='event', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='event.Event'), + ), + migrations.AddField( + model_name='activitytemplate', + name='place', + field=models.ManyToManyField(blank=True, related_name='activities', to='event.Place'), + ), + migrations.AddField( + model_name='activitytemplate', + name='tags', + field=models.ManyToManyField(blank=True, related_name='activities', to='event.ActivityTag'), + ), + migrations.AddField( + model_name='activity', + name='parent', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='children', to='event.ActivityTemplate'), + ), + migrations.AddField( + model_name='activity', + name='staff', + field=models.ManyToManyField(blank=True, related_name='in_perm_activities', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/event/migrations/__init__.py b/event/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/event/models.py b/event/models.py new file mode 100644 index 0000000..7723796 --- /dev/null +++ b/event/models.py @@ -0,0 +1,181 @@ +from django.contrib.auth.models import User +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 + + +class Event(models.Model): + title = models.CharField( + _("Nom de l'évènement"), + max_length=200, + ) + slug = models.SlugField( + _('Identificateur'), + unique=True, + primary_key=True, + help_text=_("Seulement des lettres, des chiffres ou" + "les caractères '_' ou '-'."), + ) + created_by = models.ForeignKey( + User, + related_name="created_events", + editable=False, + ) + creation_date = models.DateTimeField( + _('Date de création'), + auto_now_add=True, + ) + description = models.TextField(_('Description')) + beginning_date = models.DateTimeField(_('Date de début')) + ending_date = models.DateTimeField(_('Date de fin')) + + class Meta: + verbose_name = _("évènement") + verbose_name_plural = _("évènements") + + def __str__(self): + return self.title + + +class Place(models.Model): + name = models.CharField( + _("Nom du lieu"), + max_length=200, + ) + description = models.TextField(blank=True) + + class Meta: + verbose_name = _("lieu") + verbose_name_plural = _("lieux") + + def __str__(self): + return self.name + + +class ActivityTag(models.Model): + name = models.CharField( + _("Nom du tag"), + max_length=200, + ) + is_public = models.BooleanField( + help_text=_("Sert à faire une distinction dans" + " l'affichage selon que cela soit" + " destiné au public ou à l'équipe" + " organisatrice"), + ) + color_regex = RegexValidator( + regex=r'^#(?:[0-9a-fA-F]{3}){1,2}$', + message="La chaîne de caractère rentrée n'est pas" + "une couleur en hexadécimal.", + ) + color = models.CharField( + _('Couleur'), + max_length=7, + validators=[color_regex], + help_text="Rentrer une couleur en hexadécimal", + ) + + class Meta: + verbose_name = _("tag") + verbose_name_plural = _("tags") + + def __str__(self): + return self.name + + +class ActivityTemplate(models.Model): + title = models.CharField( + _("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, + related_name="activities", + blank=True, + null=True, + ) + is_public = models.NullBooleanField( + blank=True, + ) + has_perm = models.NullBooleanField( + blank=True, + ) + min_perm = models.PositiveSmallIntegerField( + _('Nombre minimum de permanents'), + blank=True, + null=True, + ) + max_perm = models.PositiveSmallIntegerField( + _('Nombre maximum de permanents'), + blank=True, + null=True, + ) + description = models.TextField( + _('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, + ) + tags = models.ManyToManyField( + ActivityTag, + related_name="activities", + blank=True, + ) + place = models.ManyToManyField( + Place, + related_name="activities", + blank=True, + ) + + class Meta: + verbose_name = _("template activité") + verbose_name_plural = _("templates activité") + + def __str__(self): + return self.title + + +class Activity(ActivityTemplate): + parent = models.ForeignKey( + ActivityTemplate, + related_name="children", + ) + staff = models.ManyToManyField( + User, + related_name="in_perm_activities", + blank=True, + ) + + def get_herited(self, attrname): + attr = super(Activity, self).__getattribute__(attrname) + if attrname in {"parent", "staff", "equipment"}: + raise FieldError( + _("%(attrname)s n'est pas un champ héritable"), + params={'attrname': attrname}, + ) + elif (attrname == 'place' or attrname == 'tags'): + if attr.exists(): + return attr + else: + return self.parent.__getattribute__(attrname) + elif attr is None: + return self.parent.__getattribute__(attrname) + else: + return attr + + class Meta: + verbose_name = _("activité") + verbose_name_plural = _("activités") + + def __str__(self): + return self.get_herited('title') diff --git a/event/tests.py b/event/tests.py new file mode 100644 index 0000000..38b2add --- /dev/null +++ b/event/tests.py @@ -0,0 +1,149 @@ +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from django.test import TestCase +from datetime import timedelta +from django.utils import timezone +from .models import Event, ActivityTemplate, Activity, Place, \ + ActivityTag + + +class ActivityInheritanceTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.erkan = User.objects.create( + username='enarmanli', + email='erkan.narmanli@ens.fr', + first_name='Erkan', + last_name='Narmanli', + ) + cls.event = Event.objects.create( + title='La Nuit 2042', + slug='nuit42', + created_by=cls.erkan, + creation_date=timezone.now(), + description="La nuit c'est lol", + beginning_date=timezone.now() + + timedelta(days=30), + ending_date=timezone.now() + + timedelta(days=31), + ) + cls.loge = Place.objects.create(name="Loge 45") + cls.aqua = Place.objects.create(name="Aquarium") + cls.tag_foo = ActivityTag.objects.create( + name="foo", + is_public=True, + color="#0F0F0F", + ) + cls.tag_bar = ActivityTag.objects.create( + name="bar", + is_public=True, + color="#0F0F0F", + ) + + def setUp(self): + self.template_act = ActivityTemplate.objects.create() + self.real_act = Activity.objects.create( + parent=self.template_act, + ) + + def test_inherites_title(self): + self.template_act.title = "parent" + self.assertEqual(self.real_act.get_herited('title'), "parent") + self.real_act.title = "enfant" + self.assertEqual(self.real_act.get_herited('title'), "enfant") + + def test_inherites_description(self): + self.template_act.description = "parent" + self.assertEqual(self.real_act.get_herited('description'), "parent") + self.real_act.description = "enfant" + self.assertEqual(self.real_act.get_herited('description'), "enfant") + + def test_inherites_remarks(self): + self.template_act.remarks = "parent" + self.assertEqual(self.real_act.get_herited('remarks'), "parent") + self.real_act.remarks = "enfant" + self.assertEqual(self.real_act.get_herited('remarks'), "enfant") + + def test_inherites_is_public(self): + self.template_act.is_public = True + self.assertEqual(self.real_act.get_herited('is_public'), True) + self.real_act.is_public = False + self.assertEqual(self.real_act.get_herited('is_public'), False) + + def test_inherites_has_perm(self): + self.template_act.has_perm = True + self.assertEqual(self.real_act.get_herited('has_perm'), True) + self.real_act.has_perm = False + self.assertEqual(self.real_act.get_herited('has_perm'), False) + + def test_inherites_min_perm(self): + self.template_act.min_perm = 42 + self.assertEqual(self.real_act.get_herited('min_perm'), 42) + self.real_act.min_perm = 1 + self.assertEqual(self.real_act.get_herited('min_perm'), 1) + + def test_inherites_max_perm(self): + self.template_act.max_perm = 42 + self.assertEqual(self.real_act.get_herited('max_perm'), 42) + self.real_act.max_perm = 1 + self.assertEqual(self.real_act.get_herited('max_perm'), 1) + + def test_inherites_place(self): + self.template_act.place.add(self.loge) + self.assertEqual( + self.real_act.get_herited('place').get(), + self.loge + ) + self.real_act.place.add(self.aqua) + self.assertEqual( + self.real_act.get_herited('place').get(), + self.aqua + ) + + def test_inherites_tags(self): + self.template_act.tags.add(self.tag_foo) + self.assertEqual( + self.real_act.get_herited('tags').get(), + self.tag_foo + ) + self.real_act.tags.add(self.tag_bar) + self.assertEqual( + self.real_act.get_herited('tags').get(), + self.tag_bar + ) + + +class ActivityTagColorTest(TestCase): + def test_positive_color_long(self): + self.tag = ActivityTag.objects.create( + name="bar", + is_public=True, + color="#0F0F0F", + ) + self.tag.full_clean() + + def test_positive_color_small(self): + self.tag = ActivityTag.objects.create( + name="bar", + is_public=True, + color="#0F0", + ) + self.tag.full_clean() + + def test_negative_color_1(self): + self.tag = ActivityTag.objects.create( + name="bar", + is_public=True, + color="#ABCDEG", + ) + with self.assertRaises(ValidationError): + self.tag.full_clean() + + def test_negative_color_2(self): + self.tag = ActivityTag.objects.create( + name="bar", + is_public=True, + color="#ACDE-1", + ) + with self.assertRaises(ValidationError): + self.tag.full_clean() diff --git a/event/views.py b/event/views.py new file mode 100644 index 0000000..37b0398 --- /dev/null +++ b/event/views.py @@ -0,0 +1,6 @@ +from django.shortcuts import render +from django.views.generic import TemplateView + + +class Index(TemplateView): + template_name="event/index.html" diff --git a/user/__init__.py b/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user/admin.py b/user/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/user/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/user/apps.py b/user/apps.py new file mode 100644 index 0000000..35048d4 --- /dev/null +++ b/user/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + name = 'user' diff --git a/user/forms.py b/user/forms.py new file mode 100644 index 0000000..7ba4fc3 --- /dev/null +++ b/user/forms.py @@ -0,0 +1,36 @@ +from django.contrib.auth.models import User +from django.contrib.auth.forms import UserCreationForm +from django import forms +from django.conf import settings + + +class CreateUserForm(UserCreationForm): + key = forms.CharField( + label="Clef de sécurité", + widget=forms.PasswordInput, + help_text="Cette clef est fournie par l'administrat·rice·eur " + "du site. Pour en obtenir une veuillez la-le contacter." + ) + error_m = {'wrong_key': "La clef fournie est erronée."} + + class Meta: + model = User + fields = ('username', 'first_name', 'last_name', + 'email', 'password1', 'password2',) + + def save(self, commit=True): + user = super(CreateUserForm, self).save(commit=False) + user.email = self.cleaned_data["email"] + user.first_name = self.cleaned_data["first_name"] + user.last_name = self.cleaned_data["last_name"] + if commit: + user.save() + return user + + def clean_key(self): + key = self.cleaned_data.get("key") + if key != settings.CREATE_USER_KEY: + raise forms.ValidationError( + self.error_m['wrong_key'], + code='wrong_key') + return key diff --git a/user/migrations/0001_initial.py b/user/migrations/0001_initial.py new file mode 100644 index 0000000..04a012f --- /dev/null +++ b/user/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-14 22:17 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profil', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/user/migrations/0002_auto_20160616_1803.py b/user/migrations/0002_auto_20160616_1803.py new file mode 100644 index 0000000..4193a65 --- /dev/null +++ b/user/migrations/0002_auto_20160616_1803.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-16 16:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='profil', + name='modif_pad', + field=models.BooleanField(default=False, verbose_name='Modifier tous les pads'), + ), + migrations.AddField( + model_name='profil', + name='read_kholle', + field=models.BooleanField(default=False, verbose_name='Lecture de khôlles'), + ), + migrations.AddField( + model_name='profil', + name='write_kholle', + field=models.BooleanField(default=False, verbose_name='Écriture de khôlles'), + ), + migrations.AddField( + model_name='profil', + name='write_pad', + field=models.BooleanField(default=False, verbose_name='Écrire des pads'), + ), + ] diff --git a/user/migrations/0003_auto_20160623_1603.py b/user/migrations/0003_auto_20160623_1603.py new file mode 100644 index 0000000..401255c --- /dev/null +++ b/user/migrations/0003_auto_20160623_1603.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-23 14:03 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0002_auto_20160616_1803'), + ] + + operations = [ + migrations.RemoveField( + model_name='profil', + name='modif_pad', + ), + migrations.RemoveField( + model_name='profil', + name='read_kholle', + ), + migrations.RemoveField( + model_name='profil', + name='write_kholle', + ), + migrations.RemoveField( + model_name='profil', + name='write_pad', + ), + ] diff --git a/user/migrations/0004_auto_20160623_1808.py b/user/migrations/0004_auto_20160623_1808.py new file mode 100644 index 0000000..d2c52e2 --- /dev/null +++ b/user/migrations/0004_auto_20160623_1808.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-23 16:08 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0003_auto_20160623_1603'), + ] + + operations = [ + migrations.RemoveField( + model_name='profil', + name='user', + ), + migrations.DeleteModel( + name='Profil', + ), + ] diff --git a/user/migrations/__init__.py b/user/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user/models.py b/user/models.py new file mode 100644 index 0000000..4e54d78 --- /dev/null +++ b/user/models.py @@ -0,0 +1,3 @@ +from django.db import models +from django.contrib.auth.models import User + diff --git a/user/templates/user/base_user.html b/user/templates/user/base_user.html new file mode 100644 index 0000000..f3e200d --- /dev/null +++ b/user/templates/user/base_user.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block aside %} + {% block user_aside_before %}{% endblock %} +
Les comptes utilisateurs vous donnent accès à la gestion et d'évènements. Vous pouvez vous connecter par le CAS ENS ou via votre compte si vous n'avez pas d'identifiants CAS. Pour créer un tel compte il est nécessaire d'avoir une clée ; merci de contacter un-e administrat-rice-eur pour en obtenir une.
+ {% block user_aside_after %}{% endblock %} +{% endblock %} diff --git a/user/templates/user/change_pass.html b/user/templates/user/change_pass.html new file mode 100644 index 0000000..ff663aa --- /dev/null +++ b/user/templates/user/change_pass.html @@ -0,0 +1,4 @@ +{% extends "user/user_form.html" %} + +{% block action_name %}{% url 'user:password_change' %}{% endblock %} +{% block user_error %}Une erreur s'est produite, veuillez réessayer.{% endblock %} diff --git a/user/templates/user/email_password_reset.html b/user/templates/user/email_password_reset.html new file mode 100644 index 0000000..d9dbd1a --- /dev/null +++ b/user/templates/user/email_password_reset.html @@ -0,0 +1,6 @@ +Bonjour, + +Quelqu'un a demandé à réinitialiser le mot de passe pour le compte utilisateur de qwann.fr utilisant l'adresse mail : {{ email }}. Pour réinitialiser le mot de passe, veuillez suivre le lien suivant : +{{ protocol}}://qwann.fr{% url 'user:password_reset_confirm' uidb64=uid token=token %} + +Merci de ne pas répondre à ce mail. diff --git a/user/templates/user/login.html b/user/templates/user/login.html new file mode 100644 index 0000000..74b291b --- /dev/null +++ b/user/templates/user/login.html @@ -0,0 +1,28 @@ +{% extends "user/user_form.html" %} + +{% block action_name %}{% url 'user:login' %}{% endblock %} +{% block user_error %}L'identitfiant et le mot de passe ne correspondent pas !{% endblock %} + +{% block extra_form_input %} + +{% endblock %} + +{% block user_aside_after %} +Vous n'avez pas de compte utilisateur et vous souhaiteriez en créer un ?
+ +Vous avez déjà un compte utilisateur et vous avez oublié votre mot de passe ?
+ + +{% endblock %} diff --git a/user/templates/user/password_reset.html b/user/templates/user/password_reset.html new file mode 100644 index 0000000..799ab8e --- /dev/null +++ b/user/templates/user/password_reset.html @@ -0,0 +1,4 @@ +{% extends "user/user_form.html" %} + +{% block action_name %}{% url 'user:password_reset' %}{% endblock %} +{% block user_error %}L'identitfiant et le mot de passe ne correspondent pas !{% endblock %} diff --git a/user/templates/user/subject_password_reset.txt b/user/templates/user/subject_password_reset.txt new file mode 100644 index 0000000..68bd26c --- /dev/null +++ b/user/templates/user/subject_password_reset.txt @@ -0,0 +1 @@ +[Qwann.fr] Réinitialisation du mot de passe diff --git a/user/templates/user/user_form.html b/user/templates/user/user_form.html new file mode 100644 index 0000000..b344281 --- /dev/null +++ b/user/templates/user/user_form.html @@ -0,0 +1,35 @@ +{% extends "user/base_user.html" %} +{% load bootstrap %} + +{% block section_title %}{{ sec_title }}{% endblock %} +{% block content %} + {% if form.errors %} +{% block user_error %}{% endblock %}
+ {% endif %} + + +{% endblock %} + diff --git a/user/tests.py b/user/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/user/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/user/urls.py b/user/urls.py new file mode 100644 index 0000000..2d6ef44 --- /dev/null +++ b/user/urls.py @@ -0,0 +1,88 @@ +from django.conf.urls import url, include +from django.contrib.auth import views as auth_views +from django.core.urlresolvers import reverse_lazy +from event.views import Index #TODO : mettre le vrai home +from user.views import CreateUser + +app_name = 'user' +urlpatterns = [ + # CREATE USER + url('^create/$', CreateUser.as_view(), name='create_user'), + # LOGIN + url('^login/$', + auth_views.login, + { 'template_name': 'user/login.html', + 'extra_context': { + 'sec_title' : 'Connexion', + 'button' : 'Se connecter', + }, + }, + name='login', + ), + # LOGOUT + url('^logout/$', + auth_views.logout, + #TODO : mettre le vrai home + { 'next_page': reverse_lazy('event:index'), + }, + name='logout',), + # PASSWORD_CHANGE + url('^password_change/$', + auth_views.password_change, + { 'template_name': 'user/change_pass.html', + #TODO : mettre le vrai home + 'post_change_redirect': reverse_lazy('event:index'), + 'extra_context': { + 'sec_title' : 'Changement de mot de passe', + 'button' : 'Modifier', + }, + }, + name='password_change'), + # url('^password_change/done/$', name='password_change_done'), + # RESET PASSWORD + url('^password_reset/$', + auth_views.password_reset, + { 'template_name' : 'user/password_reset.html', + 'email_template_name': 'email_password_reset.html', + 'subject_template_name': 'subject_password_reset.txt', + 'post_reset_redirect': reverse_lazy('user:password_reset_done'), + 'extra_context': { + 'sec_title' : 'Demande de nouveau mot de passe', + 'button' : 'Envoyer' + }, + }, + name='password_reset'), + # PASS RESET DONE + url('^password_reset/done/$', + #TODO : mettre le vrai home + Index.as_view(), + name='password_reset_done'), + # PASS RESET CONFIRM + url('^reset/(?P