Browse Source

interface d’administration (autre que django-admin)

Élie Bouttier 8 years ago
parent
commit
a37ec56af5
50 changed files with 1175 additions and 525 deletions
  1. 1 0
      .gitignore
  2. 56 52
      accounts/admin.py
  3. 6 6
      accounts/forms.py
  4. 4 4
      accounts/models.py
  5. 13 4
      accounts/tests.py
  6. 13 21
      adhesions/admin.py
  7. 6 6
      adhesions/backends.py
  8. 6 10
      adhesions/forms.py
  9. 20 0
      adhesions/migrations/0007_auto_20170301_1429.py
  10. 35 10
      adhesions/models.py
  11. 25 0
      adhesions/templates/adhesions/_adhesion_detail.html
  12. 13 0
      adhesions/templates/adhesions/adherent.html
  13. 0 24
      adhesions/templates/adhesions/adhesion.html
  14. 9 0
      adhesions/templates/adhesions/adhesion_detail.html
  15. 9 0
      adhesions/templates/adhesions/adhesion_edit.html
  16. 38 0
      adhesions/templates/adhesions/adhesion_list.html
  17. 4 29
      adhesions/templates/adhesions/corporation.html
  18. 59 0
      adhesions/templates/adhesions/corporation_detail.html
  19. 11 0
      adhesions/templates/adhesions/corporation_form.html
  20. 30 0
      adhesions/templates/adhesions/corporation_list.html
  21. 7 0
      adhesions/templates/adhesions/user.html
  22. 80 0
      adhesions/templates/adhesions/user_detail.html
  23. 11 0
      adhesions/templates/adhesions/user_form.html
  24. 39 0
      adhesions/templates/adhesions/user_list.html
  25. 85 1
      adhesions/tests.py
  26. 12 2
      adhesions/urls.py
  27. 124 6
      adhesions/views.py
  28. 156 140
      banking/admin.py
  29. 12 8
      banking/models.py
  30. 1 0
      coverage.sh
  31. 2 2
      djadhere/settings.py
  32. 3 3
      djadhere/templates/_form.html
  33. 22 9
      djadhere/templates/base.html
  34. 0 7
      djadhere/templates/home.html
  35. 0 15
      djadhere/tests.py
  36. 0 3
      djadhere/urls.py
  37. 14 2
      djadhere/utils.py
  38. 0 9
      djadhere/views.py
  39. 103 58
      services/admin.py
  40. 0 3
      services/apps.py
  41. 32 0
      services/migrations/0011_auto_20170301_1502.py
  42. 19 14
      services/models.py
  43. 0 33
      services/signals.py
  44. 15 2
      services/templates/services/_service_list.html
  45. 12 13
      services/templates/services/service_detail.html
  46. 9 0
      services/templates/services/service_form.html
  47. 32 14
      services/templates/services/service_list.html
  48. 7 3
      services/tests.py
  49. 2 1
      services/urls.py
  50. 18 11
      services/views.py

+ 1 - 0
.gitignore

@@ -6,3 +6,4 @@
 djadhere/*_settings.py
 .coverage
 htmlcov
+/bower/

+ 56 - 52
accounts/admin.py

@@ -1,12 +1,12 @@
 from django.contrib import admin
 from django.contrib.auth.models import User
 from django.contrib.auth.admin import UserAdmin as AuthUserAdmin
-from django.core.exceptions import PermissionDenied
+#from django.core.exceptions import PermissionDenied
 
 
 from .models import Profile
-from .forms import UserCreationForm
-from adhesions.admin import AdherentInline
+#from .forms import UserCreationForm
+from adhesions.admin import AdhesionInline
 
 
 class ProfileInline(admin.StackedInline):
@@ -21,61 +21,65 @@ class ProfileInline(admin.StackedInline):
 
 class UserAdmin(AuthUserAdmin):
     list_display = AuthUserAdmin.list_display + ('adherent_id',)
+    inlines = (ProfileInline, AdhesionInline,)
 
     def adherent_id(self, user):
         adherent = user.profile.adhesion
         if adherent:
             return adherent.id
     adherent_id.short_description = 'Numéro d’adhérent'
-
-    def get_readonly_fields(self, request, obj=None):
-        readonly_fields = super().get_readonly_fields(request, obj)
-        if obj and not request.user.is_superuser:
-            readonly_fields += ('username',)
-        return readonly_fields
-
-    def get_fieldsets(self, request, obj=None):
-        if request.user.is_superuser:
-            return (
-                AuthUserAdmin.fieldsets[0],
-                AuthUserAdmin.fieldsets[1],
-                (AuthUserAdmin.fieldsets[2][0], {
-                    'classes': ('collapse',),
-                    'fields': ('is_active', 'is_staff', 'is_superuser', 'groups',), # removing of user_permissions
-                }),
-                (AuthUserAdmin.fieldsets[3][0], {
-                    'classes': ('collapse',),
-                    'fields': AuthUserAdmin.fieldsets[3][1]['fields'],
-                }),
-            )
-        if obj:
-            return (
-                AuthUserAdmin.fieldsets[0],  # Note: password is mandatory (but readonly)
-                AuthUserAdmin.fieldsets[1],
-            )
-        else:
-            return (
-                (None, {'fields': ('username',)}),
-                AuthUserAdmin.fieldsets[1],
-            )
-
-    def get_form(self, request, obj=None, **kwargs):
-        # get_inlines does not exists :-(
-        if obj:
-            self.inlines = (ProfileInline, AdherentInline,)
-        else:
-            self.inlines = ()
-
-        if request.user.is_superuser or obj:
-            return super().get_form(request, obj, **kwargs)
-        else:
-            # This creation form does not ask for a password
-            return UserCreationForm
-
-    def user_change_password(self, request, id):
-        if not request.user.is_superuser:
-            raise PermissionDenied
-        return super().user_change_password(request, id)
+#
+#    def get_readonly_fields(self, request, obj=None):
+#        readonly_fields = super().get_readonly_fields(request, obj)
+#        if obj and not request.user.is_superuser:
+#            readonly_fields += ('username',)
+#        return readonly_fields
+#
+#    def get_fieldsets(self, request, obj=None):
+#        if request.user.is_superuser:
+#            return (
+#                AuthUserAdmin.fieldsets[0],
+#                AuthUserAdmin.fieldsets[1],
+#                (AuthUserAdmin.fieldsets[2][0], {
+#                    'classes': ('collapse',),
+#                    'fields': ('is_active', 'is_staff', 'is_superuser', 'groups',), # removing of user_permissions
+#                }),
+#                (AuthUserAdmin.fieldsets[3][0], {
+#                    'classes': ('collapse',),
+#                    'fields': AuthUserAdmin.fieldsets[3][1]['fields'],
+#                }),
+#            )
+#        if obj:
+#            return (
+#                AuthUserAdmin.fieldsets[0],  # Note: password is mandatory (but readonly)
+#                AuthUserAdmin.fieldsets[1],
+#            )
+#        else:
+#            return (
+#                (None, {'fields': ('username',)}),
+#                AuthUserAdmin.fieldsets[1],
+#            )
+#
+#    def get_form(self, request, obj=None, **kwargs):
+#        # get_inlines does not exists :-(
+#        if obj:
+#            self.inlines = (ProfileInline, AdherentInline,)
+#        else:
+#            self.inlines = ()
+#
+#        if request.user.is_superuser or obj:
+#            return super().get_form(request, obj, **kwargs)
+#        else:
+#            # This creation form does not ask for a password
+#            return UserCreationForm
+#
+#    def user_change_password(self, request, id):
+#        if not request.user.is_superuser:
+#            # Les non admin peuvent modifier le mot de passe des utilisateurs qui ne sont ni staff ni admin
+#            user = User.objects.get(pk=id)
+#            if user.is_staff or user.is_superuser:
+#                raise PermissionDenied
+#        return super().user_change_password(request, id)
 
 
 admin.site.unregister(User)

+ 6 - 6
accounts/forms.py

@@ -4,12 +4,12 @@ from django.contrib.auth.models import User
 from .models import Profile
 
 
-class UserCreationForm(ModelForm):
-    class Meta:
-        model = User
-        fields = ('username', 'first_name', 'last_name', 'email',)
-
-
+#class UserCreationForm(ModelForm):
+#    class Meta:
+#        model = User
+#        fields = ('username', 'first_name', 'last_name', 'email',)
+#
+#
 class UserForm(ModelForm):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)

+ 4 - 4
accounts/models.py

@@ -3,7 +3,7 @@ from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 
 
-from adhesions.models import Adherent, Corporation
+from adhesions.models import Adhesion, Corporation
 
 
 class Profile(models.Model):
@@ -20,15 +20,15 @@ class Profile(models.Model):
     def adhesion(self): # user adhesion
         ctype = ContentType.objects.get_for_model(User)
         try:
-            return Adherent.objects.get(adherent_type=ctype, adherent_id=self.user.pk)
-        except Adherent.DoesNotExist:
+            return Adhesion.objects.get(adherent_type=ctype, adherent_id=self.user.pk)
+        except Adhesion.DoesNotExist:
             return None
 
     @property
     def adhesions(self): # user and corporations (for which the user is a member) adhesions
         user_type = ContentType.objects.get_for_model(User)
         corp_type = ContentType.objects.get_for_model(Corporation)
-        return Adherent.objects.filter(
+        return Adhesion.objects.filter(
             models.Q(adherent_type=user_type, adherent_id=self.user.pk)
             | models.Q(adherent_type=corp_type, adherent_id__in=Corporation.objects.filter(members=self.user).values_list('pk'))
         )

+ 13 - 4
accounts/tests.py

@@ -8,19 +8,24 @@ from .forms import UserForm, ProfileForm
 
 class AccountsTests(TestCase):
     def setUp(self):
-        user = User.objects.create_user('user', email='user@example.net', password='user')
+        user = User.objects.create_user('user', first_name='first', last_name='last', email='user@example.net', password='user')
 
     def test_auth(self):
         self.assertEquals(self.client.get(reverse('login')).status_code, 200)
         self.assertEquals(self.client.get(reverse('password_reset')).status_code, 200)
         self.assertEquals(self.client.get(reverse('password_reset_done')).status_code, 200)
 
+    def test_email_backend(self):
+        self.assertFalse(self.client.login(username='user@example.net', password='wrong'))
+        self.assertFalse(self.client.login(username='wrong@example.net', password='user'))
+        self.assertTrue(self.client.login(username='user@example.net', password='user'))
+
     def test_login_logout(self):
         self.assertEquals(self.client.get(reverse('login')).status_code, 200)
         self.client.login(username='user', password='user')
-        self.assertEquals(self.client.get(reverse('home')).status_code, 200)
-        self.assertRedirects(self.client.get(reverse('logout')), reverse('home'), target_status_code=302) # home is redirected to login
-        self.assertRedirects(self.client.get(reverse('home')), reverse('login') + '?next=' + reverse('home'))
+        self.assertEquals(self.client.get(reverse('user')).status_code, 200)
+        self.assertRedirects(self.client.get(reverse('logout')), reverse('user'), target_status_code=302) # user page is redirected to login
+        self.assertRedirects(self.client.get(reverse('user')), reverse('login') + '?next=' + reverse('user'))
 
     def test_profile(self):
         response = self.client.get(reverse('profile'))
@@ -34,11 +39,15 @@ class AccountsTests(TestCase):
         profile_form = ProfileForm(instance=user.profile)
         data.update({key: getattr(profile_form.instance, key) for key in profile_form.fields})
         data['username'] = 'user2' # try to tamper username
+        data['first_name'] = 'first2' # try to tamper username
+        data['last_name'] = 'last2' # try to tamper username
         data['email'] = 'user@example.org'
         data['address'] = '221B Baker Street'
         response = self.client.post(reverse('profile'), data)
         self.assertRedirects(response, reverse('profile'))
         user = User.objects.get(pk=user.pk) # refresh user
         self.assertEquals(user.username, 'user') # should not be modified
+        self.assertEquals(user.first_name, 'first') # should not be modified
+        self.assertEquals(user.last_name, 'last') # should not be modified
         self.assertEquals(user.email, 'user@example.org')
         self.assertEquals(user.profile.address, '221B Baker Street')

+ 13 - 21
adhesions/admin.py

@@ -4,16 +4,16 @@ from django.contrib.contenttypes.admin import GenericStackedInline
 from django.db.models import Q
 from django.contrib.contenttypes.models import ContentType
 
-from .forms import AdherentForm
-from .models import Corporation, Adherent
-from banking.admin import PaymentInline, ValidatedPaymentInline, PendingOrNewPaymentInline
+from .forms import AdhesionAdminForm
+from .models import Adhesion, Corporation
+from banking.admin import PaymentInline
 
 
-class AdherentInline(GenericStackedInline):
-    model = Adherent
+class AdhesionInline(GenericStackedInline):
+    model = Adhesion
     ct_field = 'adherent_type'
     ct_fk_field = 'adherent_id'
-    form = AdherentForm
+    form = AdhesionAdminForm
     max_num = 1
     extra = 0
 
@@ -37,12 +37,13 @@ class AdherentTypeFilter(admin.SimpleListFilter):
                                    adherent_type__model='corporation')
 
 
-class AdherentAdmin(admin.ModelAdmin):
-    list_display = ('id', 'get_name', 'type',)
+class AdhesionAdmin(admin.ModelAdmin):
+    list_display = ('id', 'get_adherent_name', 'type',)
     list_filter = (AdherentTypeFilter,)
     fields = ('id',)
     readonly_fields = ('id',)
     search_fields = ('id',)
+    inlines = (PaymentInline,)
 
     def get_search_results(self, request, queryset, search_term):
         queryset, use_distinct = super().get_search_results(request, queryset, search_term)
@@ -52,28 +53,19 @@ class AdherentAdmin(admin.ModelAdmin):
                     | Q(last_name__icontains=search_term)
         )
         user_type = ContentType.objects.get_for_model(User)
-        queryset |= Adherent.objects.filter(adherent_type=user_type, adherent_id__in=users.values_list('pk'))
+        queryset |= Adhesion.objects.filter(adherent_type=user_type, adherent_id__in=users.values_list('pk'))
         corporations = Corporation.objects.filter(social_reason__icontains=search_term)
         corporation_type = ContentType.objects.get_for_model(Corporation)
-        queryset |= Adherent.objects.filter(adherent_type=corporation_type, adherent_id__in=corporations.values_list('pk'))
+        queryset |= Adhesion.objects.filter(adherent_type=corporation_type, adherent_id__in=corporations.values_list('pk'))
         return queryset, use_distinct
 
-    def get_form(self, request, obj=None, **kwargs):
-        # get_inlines does not exists :-(
-        if request.user.has_perm('banking.validate_payment'):
-            self.inlines = (PaymentInline,)
-        else:
-            self.inlines = (ValidatedPaymentInline, PendingOrNewPaymentInline,)
-
-        return super().get_form(request, obj, **kwargs)
-
     def has_add_permission(self, request):
         return False
 
 
 class CorporationAdmin(admin.ModelAdmin):
     list_display = ('social_reason', 'adherent_id')
-    inlines = (AdherentInline,)
+    inlines = (AdhesionInline,)
     search_fields = ('social_reason',)
 
     def adherent_id(self, corporation):
@@ -84,4 +76,4 @@ class CorporationAdmin(admin.ModelAdmin):
 
 
 admin.site.register(Corporation, CorporationAdmin)
-admin.site.register(Adherent, AdherentAdmin)
+admin.site.register(Adhesion, AdhesionAdmin)

+ 6 - 6
adhesions/backends.py

@@ -2,22 +2,22 @@ from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.auth.backends import ModelBackend
 
-from .models import Adherent
+from .models import Adhesion
 
 
-class AdherentBackend(ModelBackend):
+class AdhesionBackend(ModelBackend):
     def authenticate(self, username, password=None, **kwargs):
         if str(username).lower().startswith("adt"):
             username = str(username)[3:]
         try:
-            adherent_id = int(username)
+            adhesion_id = int(username)
         except ValueError:
             return None
         user_type = ContentType.objects.get_for_model(User)
         try:
-            user = Adherent.objects.get(adherent_type=user_type, id=adherent_id).adherent
-        except Adherent.DoesNotExist:
-            UserModel().set_password(password) # https://code.djangoproject.com/ticket/20760
+            user = Adhesion.objects.get(adherent_type=user_type, id=adhesion_id).adherent
+        except Adhesion.DoesNotExist:
+            User().set_password(password) # https://code.djangoproject.com/ticket/20760
         else:
             if user.check_password(password) and self.user_can_authenticate(user):
                 return user

+ 6 - 10
adhesions/forms.py

@@ -1,22 +1,18 @@
 from django import forms
-from django.forms import widgets
-from django.utils.safestring import mark_safe
 
-from .models import Adherent
+from djadhere.utils import StringWidget
+from .models import Adhesion
 
 
-class StringWidget(widgets.Input):
-    def render(self, name, value, attrs=None):
-        # Create a hidden field first
-        hidden_field = widgets.HiddenInput(attrs)
-        return mark_safe(u'%s %s' % (value, hidden_field.render(value, attrs)))
+class AdhesionForm(forms.Form):
+    adherent = forms.BooleanField(label='Adhérent ?')
 
 
-class AdherentForm(forms.ModelForm):
+class AdhesionAdminForm(forms.ModelForm):
     adherent_id = forms.CharField(disabled=True, label='Numéro d’adhérent')
 
     class Meta:
-        model = Adherent
+        model = Adhesion
         exclude = ()
 
     def __init__(self, *args, **kwargs):

+ 20 - 0
adhesions/migrations/0007_auto_20170301_1429.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-03-01 13:29
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('adhesions', '0006_adherent_created'),
+        ('services', '0010_auto_20170211_1635'),
+    ]
+
+    operations = [
+        migrations.RenameModel(
+            old_name='Adherent',
+            new_name='Adhesion',
+        ),
+    ]

+ 35 - 10
adhesions/models.py

@@ -1,4 +1,5 @@
 from django.db import models
+from django.urls import reverse
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
 from django.contrib.contenttypes.models import ContentType
@@ -7,7 +8,9 @@ from djadhere.utils import get_active_filter
 from banking.models import Payment
 
 
-class Adherent(models.Model):
+# Terminologie : une « adhésion » désgine une instance de ce modèle
+# tandis qu’un « adhérent » désigne un user ou une corporation.
+class Adhesion(models.Model):
     limit = models.Q(app_label='auth', model='user') \
           | models.Q(app_label='adhesions', model='corporation')
     id = models.AutoField(verbose_name='Numéro d’adhérent', primary_key=True, editable=True)
@@ -18,7 +21,7 @@ class Adherent(models.Model):
     contributions = GenericRelation(Payment,
                                    content_type_field='reason_type',
                                    object_id_field='reason_id',
-                                   related_query_name='adherent')
+                                   related_query_name='adhesion')
     created = models.DateTimeField(null=True, blank=True, auto_now_add=True)
 
     class Meta:
@@ -34,23 +37,41 @@ class Adherent(models.Model):
         # MultipleObjectsReturned non catché volontairement, le filtrage par la méthode clean est censé
         # empêcher cette possibilité, si cette exception est levé on veut recevoir un mail avec l’erreur !
 
+    def is_physical(self):
+        return self.adherent_type.app_label == 'auth' and self.adherent_type.model == 'user'
+
+    def is_moral(self):
+        return self.adherent_type.app_label == 'adhesions' and self.adherent_type.model == 'corporation'
+
     @property
     def type(self):
-        if self.adherent_type.app_label == 'auth' and self.adherent_type.model == 'user':
+        if self.is_physical():
             return 'Personne physique'
         else:
             return 'Personne morale'
 
-    def get_name(self):
-        if self.adherent_type.app_label == 'auth' and self.adherent_type.model == 'user':
+    def get_adherent_name(self):
+        if self.is_physical():
             return str(self.adherent.profile)
         else:
             return str(self.adherent)
-    get_name.short_description = 'Nom ou raison sociale'
+    get_adherent_name.short_description = 'Nom ou raison sociale'
+
+    def get_adherent_detail_url(self):
+        if self.is_physical():
+            return reverse('user-detail', kwargs={'pk': self.adherent.pk})
+        else:
+            return reverse('corporation-detail', kwargs={'pk': self.adherent.pk})
+
+    def get_adherent_edit_url(self):
+        if self.is_physical():
+            return reverse('user-edit', kwargs={'pk': self.adherent.pk})
+        else:
+            return reverse('corporation-edit', kwargs={'pk': self.adherent.pk})
 
     def __str__(self):
         if self.id:
-            return 'ADT%d – %s' % (self.id, self.get_name())
+            return self.get_adherent_name()
         else:
             return '?'
 
@@ -59,16 +80,20 @@ class Corporation(models.Model):
     social_reason = models.CharField(max_length=256, verbose_name='Raison sociale', unique=True)
     description = models.TextField(blank=True, default='')
     address = models.TextField(blank=True, default='', verbose_name='Adresse')
-    members = models.ManyToManyField(User, blank=True, verbose_name='Membres')
+    members = models.ManyToManyField(User, blank=True, verbose_name='Membres',
+                                     related_name='corporations', related_query_name='corporation')
 
     @property
     def adhesion(self):
         ctype = ContentType.objects.get_for_model(self)
         try:
-            return Adherent.objects.get(adherent_type=ctype, adherent_id=self.pk)
-        except Adherent.DoesNotExist:
+            return Adhesion.objects.get(adherent_type=ctype, adherent_id=self.pk)
+        except Adhesion.DoesNotExist:
             return None
 
+    def get_absolute_url(self):
+        return reverse('corporation-detail', kwargs={'pk': self.pk})
+
     class Meta:
         verbose_name = 'personne morale'
         verbose_name_plural = 'personnes morales'

+ 25 - 0
adhesions/templates/adhesions/_adhesion_detail.html

@@ -0,0 +1,25 @@
+{% load djadhere services %}
+
+<div class="panel panel-primary">
+    <div class="panel-heading">
+        <h3>Adhésion</h3>
+    </div>
+    <div class="panel-body">
+        {% if adhesion %}
+        {% if show_adherent %}
+        <p>Adhérent : <a href="{{ adhesion.get_adherent_detail_url }}">{{ adhesion }}</a></p>
+        {% endif %}
+        <p>Numéro d’adhérent : ADT{{ adhesion.id }}</p>
+        {% if adhesion.contribution %}
+        <p>Montant de la cotisation : {{ adhesion.contribution }}</p>
+        {% else %}
+        <p>Pas de cotisation.</p>
+        {% endif %}
+        {% if adhesion.created %}
+        <p>Date de première adhésion : {{ adhesion.created }}</p>
+        {% endif %}
+        {% else %}
+        <p>Non adhérent <mark>tetaneutral.net</mark>.</p>
+        {% endif %}
+    </div>
+</div>

+ 13 - 0
adhesions/templates/adhesions/adherent.html

@@ -0,0 +1,13 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+{% block header %}{% endblock %}
+
+{% include 'adhesions/_adhesion_detail.html' %}
+
+{% with services=adhesion.services.all %}
+{% include 'services/_service_list.html' %}
+{% endwith %}
+
+{% endblock %}

+ 0 - 24
adhesions/templates/adhesions/adhesion.html

@@ -1,24 +0,0 @@
-{% extends 'base.html' %}
-
-{% block adhesiontab %} class="active"{% endblock %}
-
-{% block content %}
-{% if adhesion %}
-<div class="panel panel-default">
-    <div class="panel-heading"><h4>À propos de votre adhésion</h4></div>
-    <div class="panel-body">
-        <p>Votre numéro d’adhérent : ADT{{ adhesion.id }}</p>
-        {% if adhesion.contribution %}
-        <p>Montant de votre cotisation : {{ adhesion.contribution }}</p>
-        {% else %}
-        <p>Pas de cotisation.</p>
-        {% endif %}
-        {% if adhesion.created %}
-        <p>Date de première adhésion : {{ adhesion.created }}</p>
-        {% endif %}
-    </div>
-</div>
-{% else %}
-<p>Vous n’êtes pas adhérent.</p>
-{% endif %}
-{% endblock %}

+ 9 - 0
adhesions/templates/adhesions/adhesion_detail.html

@@ -0,0 +1,9 @@
+{% extends 'adhesions/adherent.html' %}
+
+{% block header %}
+<a class="btn btn-primary" href="{% url 'adhesion-list' %}"><span class="glyphicon glyphicon-arrow-left"></span>&nbsp;Retour à la liste des adhérents</a>
+
+<br />
+
+<h1>{{ adhesion }}</h1>
+{% endblock %}

+ 9 - 0
adhesions/templates/adhesions/adhesion_edit.html

@@ -0,0 +1,9 @@
+{% extends 'base.html' %}
+
+{% block content %}
+<h1>{% if adhesion %}Modification {{ adhesion }}{% else %}Ajouter un adhérents{% endif %}</h1>
+
+
+{% include '_form.html' %}
+
+{% endblock %}

+ 38 - 0
adhesions/templates/adhesions/adhesion_list.html

@@ -0,0 +1,38 @@
+{% extends 'base.html' %}
+
+{% block content %}
+{% comment %}
+
+<br /><br />
+{% endcomment %}
+
+<div class="panel panel-primary">
+    <div class="panel-heading">
+        <div class="pull-right">
+            <a class="btn btn-success" href="{% url 'user-add' %}"><span class="glyphicon glyphicon-user"></span>&nbsp;Ajouter un utilisateur</a>
+            <a class="btn btn-success" href="{% url 'corporation-add' %}"><span class="glyphicon glyphicon-globe"></span>&nbsp;Ajouter une association</a>
+        </div>
+        <h1>Adhérents</h1>
+    </div>
+    {% for adhesion in object_list %}
+    {% if forloop.first %}
+    <table class="table">
+        <tr>
+            <th>Numéro d’adhérent</th>
+            <th>Nom ou raison social</th>
+            <th>Type</th>
+            <th></th>
+        </tr>
+    {% endif %}
+        <tr>
+            <td><a href="{% url 'adhesion-detail' adhesion.id %}">ADT{{ adhesion.id }}</a></td>
+            <td><a href="{{ adhesion.get_adherent_detail_url }}">{{ adhesion }}</a></td>
+            <td>{{ adhesion.type }}</td>
+            <td class="text-right"><a href="{{ adhesion.get_adherent_edit_url }}"><span class="glyphicon glyphicon-pencil"></span>&nbsp;Modifier</a></td>
+        </tr>
+    {% if forloop.last %}
+    </table>
+    {% endif %}
+    {% endfor %}
+</div>
+{% endblock %}

+ 4 - 29
adhesions/templates/adhesions/corporation.html

@@ -1,32 +1,7 @@
-{% extends 'base.html' %}
+{% extends 'adhesions/adherent.html' %}
 
-{% load djadhere services %}
+{% block corptab %} active{% endblock %}
 
-{% block assotab %} active{% endblock %}
-
-{% block content %}
-<div class="panel panel-default">
-    <div class="panel-heading"><h4>{{ corporation.social_reason }}</h4></div>
-    <div class="panel-body">
-        {% if corporation.adhesion %}
-        <p>Numéro d’adhérent de l’association : ADT{{ corporation.adhesion.id }}</p>
-        {% if corporation.adhesion.contribution %}
-        <p>Montant de la cotisation : {{ corporation.adhesion.contribution }}</p>
-        {% else %}
-        <p>Pas de cotisation.</p>
-        {% endif %}
-        {% if corporation.adhesion.created %}
-        <p>Date de première adhésion : {{ corporation.adhesion.created }}</p>
-        {% endif %}
-        <p>Services actifs : {{ corporation.adhesion.services|active|count }}</p>
-        <p>Services inactifs : {{ corporation.adhesion.services|inactive|count }}</p>
-        {% else %}
-        <p>Cette association n’est pas adhérente <mark>tetaneutral.net</mark>.</p>
-        {% endif %}
-    </div>
-</div>
-
-{% with services=corporation.adhesion.services.all %}
-{% include 'services/_service_list.html' %}
-{% endwith %}
+{% block header %}
+<h1>{{ adherent }}</h1>
 {% endblock %}

+ 59 - 0
adhesions/templates/adhesions/corporation_detail.html

@@ -0,0 +1,59 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+<a class="btn btn-primary" href="{% url 'corporation-list' %}"><span class="glyphicon glyphicon-arrow-left"></span>&nbsp;Retour à la liste des personnes morales</a>
+
+<br /><br />
+
+<div class="panel panel-primary">
+    <div class="panel-heading">
+        {{ corporation }}
+    </div>
+    <table class="table table-bordered">
+        <tr>
+            <th>Numéro d’adhérent</th>
+            <td>
+                {% if corporation.adhesion %}
+                <a href="{% url 'adhesion-detail' corporation.adhesion.id %}">ADT{{ corporation.adhesion.id }}</a>
+                {% else %}
+                <em>Non adhérent.</em>
+                {% endif %}
+            </td>
+        </tr>
+    </table>
+</div>
+
+<div class="panel panel-primary">
+    <div class="panel-heading">
+        Membres
+    </div>
+    <table class="table table-bordered">
+        {% for user in corporation.members.all %}
+        {% if forloop.first %}
+        <tr>
+            <th>Nom</th>
+            <th>Numéro d’adhérent</th>
+        </tr>
+        {% endif %}
+        <tr>
+            <td>
+                <a href="{% url 'user-detail' user.pk %}">{{ user.profile }}</a>
+            </td>
+            <td>
+                {% if user.profile.adhesion %}
+                <a href="{% url 'adhesion-detail' user.profile.adhesion.pk %}">ADT{{ user.profile.adhesion.pk }}</a>
+                {% else %}
+                –
+                {% endif %}
+            </td>
+        </tr>
+        {% empty %}
+        <tr>
+            <td><em>Aucun membre.</em></td>
+        </tr>
+        {% endfor %}
+    </table>
+</div>
+
+{% endblock %}

+ 11 - 0
adhesions/templates/adhesions/corporation_form.html

@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+<a class="btn btn-primary pull-right" href="{% url 'corporation-list' %}"><span class="glyphicon glyphicon-arrow-left"></span>&nbsp;Retour à la liste des associations</a>
+
+<h1>{% if adhesion %}Modifier{% else %}Ajouter{% endif %} une association</h1>
+
+{% include '_form.html' %}
+
+{% endblock %}

+ 30 - 0
adhesions/templates/adhesions/corporation_list.html

@@ -0,0 +1,30 @@
+{% extends 'base.html' %}
+
+{% block content %}
+<div class="panel panel-primary">
+    <div class="panel-heading">
+        <a class="btn btn-success pull-right" href="{% url 'corporation-add' %}"><span class="glyphicon glyphicon-user"></span>&nbsp;Ajouter une association</a>
+        <h1>Associations</h1>
+    </div>
+    {% for corp in object_list %}
+    {% if forloop.first %}
+    <table class="table">
+        <tr>
+            <th>Raison sociale</th>
+            <th>Numéro d’adhérent</th>
+            <th></th>
+        </tr>
+    {% endif %}
+        <tr>
+            <td><a href="{% url 'corporation-detail' corp.pk %}">{{ corp.social_reason }}</a></td>
+            <td>{% if corp.adhesion %}<a href="{% url 'adhesion-detail' corp.adhesion.pk %}">ADT{{ corp.adhesion.pk }}</a>{% else %}–{% endif %}</td>
+            <td class="text-right">
+                <a href="{% url 'corporation-edit' corp.pk %}"><span class="glyphicon glyphicon-pencil"></span>&nbsp;Modifier</a>
+            </td>
+        </tr>
+    {% if forloop.last %}
+    </table>
+    {% endif %}
+    {% endfor %}
+</div>
+{% endblock %}

+ 7 - 0
adhesions/templates/adhesions/user.html

@@ -0,0 +1,7 @@
+{% extends 'adhesions/adherent.html' %}
+
+{% block usertab %} class="active"{% endblock %}
+
+{% block header %}
+<h1>Bienvenue <b>{{ adherent.profile }}</b> !</h1>
+{% endblock %}

+ 80 - 0
adhesions/templates/adhesions/user_detail.html

@@ -0,0 +1,80 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+<a class="btn btn-primary" href="{% url 'user-list' %}"><span class="glyphicon glyphicon-arrow-left"></span>&nbsp;Retour à la liste des utilisateurs</a>
+
+<br /><br />
+
+<div class="panel panel-primary">
+    <div class="panel-heading">
+        {{ user.profile }}
+    </div>
+    <table class="table table-bordered">
+        <tr>
+            <th>Numéro d’adhérent</th>
+            <td>
+                {% if user.profile.adhesion %}
+                <a href="{% url 'adhesion-detail' user.profile.adhesion.id %}">ADT{{ user.profile.adhesion.id }}</a>
+                {% else %}
+                <em>non adhérent</em>
+                {% endif %}
+            </td>
+        </tr>
+        <tr>
+            <th>Nom d’utilisateur</th>
+            <td>{{ user }}</td>
+        </tr>
+        <tr>
+            <th>Prénom</th>
+            <td>{{ user.first_name }}</td>
+        </tr>
+        <tr>
+            <th>Nom</th>
+            <td>{{ user.last_name }}</td>
+        </tr>
+        <tr>
+            <th>Adresse e-mail</th>
+            <td>{{ user.email }}</td>
+        </tr>
+        <tr>
+            <th>Numéro de téléphone</th>
+            <td>{{ user.profile.phone_number }}</td>
+        </tr>
+        <tr>
+            <th>Adresse</th>
+            <td>{{ user.profile.address }}</td>
+        </tr>
+    </table>
+</div>
+
+<div class="panel panel-primary">
+    <div class="panel-heading">
+        Associations
+    </div>
+    <table class="table table-bordered">
+        {% for corp in user.corporations.all %}
+        {% if forloop.first %}
+        <tr>
+            <th>Raison sociale</th>
+            <th>Numéro d’adhérent</th>
+        </tr>
+        {% endif %}
+        <tr>
+            <td><a href="{% url 'corporation-detail' corp.pk %}">{{ corp }}</a></td>
+            <td>
+                {% if corp.adhesion %}
+                <a href="{% url 'adhesion-detail' corp.adhesion.pk %}">ADT{{ corp.adhesion.pk }}</a>
+                {% else %}
+                –
+                {% endif %}
+            </td>
+        </tr>
+        {% empty %}
+        <tr>
+            <td><em>Aucune association.</em></td>
+        </tr>
+        {% endfor %}
+    </table>
+</div>
+{% endblock %}

+ 11 - 0
adhesions/templates/adhesions/user_form.html

@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+<a class="btn btn-primary pull-right" href="{% url 'user-list' %}"><span class="glyphicon glyphicon-arrow-left"></span>&nbsp;Retour à la liste des utilisateurs</a>
+
+<h1>{% if user %}Modifier{% else %}Ajouter{% endif %} un utilisateur</h1>
+
+{% include '_form.html' %}
+
+{% endblock %}

+ 39 - 0
adhesions/templates/adhesions/user_list.html

@@ -0,0 +1,39 @@
+{% extends 'base.html' %}
+
+{% block content %}
+{% comment %}
+
+<br /><br />
+{% endcomment %}
+
+<div class="panel panel-primary">
+    <div class="panel-heading">
+        <a class="btn btn-success pull-right" href="{% url 'user-add' %}"><span class="glyphicon glyphicon-user"></span>&nbsp;Ajouter un utilisateur</a>
+        <h1>Utilisateurs</h1>
+    </div>
+    {% for user in object_list %}
+    {% if forloop.first %}
+    <table class="table">
+        <tr>
+            <th>Nom d’utilisateur</th>
+            <th>Prénom</th>
+            <th>Nom</th>
+            <th>Numéro d’adhérent</th>
+            <th></th>
+        </tr>
+    {% endif %}
+        <tr>
+            <td><a href="{% url 'user-detail' user.pk %}">{{ user.username }}</a></td>
+            <td>{{ user.first_name }}</td>
+            <td>{{ user.last_name }}</td>
+            <td>{% if user.profile.adhesion %}<a href="{% url 'adhesion-detail' user.profile.adhesion.pk %}">ADT{{ user.profile.adhesion.pk }}</a>{% else %}–{% endif %}</td>
+            <td class="text-right">
+                <a href="{% url 'user-edit' user.pk %}"><span class="glyphicon glyphicon-pencil"></span>&nbsp;Modifier</a>
+            </td>
+        </tr>
+    {% if forloop.last %}
+    </table>
+    {% endif %}
+    {% endfor %}
+</div>
+{% endblock %}

+ 85 - 1
adhesions/tests.py

@@ -1,3 +1,87 @@
 from django.test import TestCase
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.core.urlresolvers import reverse
 
-# Create your tests here.
+from .models import Corporation, Adhesion
+
+
+class AdhesionsTests(TestCase):
+    def setUp(self):
+        admin = User.objects.create_user('admin', email='admin@example.net', password='admin', is_superuser=True)
+        user = User.objects.create_user('user', first_name='first', last_name='last', email='user@example.net', password='user')
+        adhesion = Adhesion.objects.create(adherent_type=ContentType.objects.get_for_model(User), adherent_id=user.pk)
+        corp1 = Corporation.objects.create(social_reason='GoodCorp')
+        corp1.members.add(user)
+        corp2 = Corporation.objects.create(social_reason='EvilCorp')
+
+    def test_adhesion_backend(self):
+        user = User.objects.get(username='user')
+        adhesion = user.profile.adhesion
+        self.assertFalse(self.client.login(username='%d' % adhesion.pk, password='wrong'))
+        self.assertFalse(self.client.login(username='ADT%d' % adhesion.pk, password='wrong'))
+        self.assertFalse(self.client.login(username='9999', password='user'))
+        self.assertFalse(self.client.login(username='ADT9999', password='user'))
+        self.assertTrue(self.client.login(username='%d' % adhesion.pk, password='user'))
+        self.assertTrue(self.client.login(username='ADT%d' % adhesion.pk, password='user'))
+
+    def test_user(self):
+        response = self.client.get(reverse('user'))
+        self.assertRedirects(response, reverse('login') + '?next=' + reverse('user'))
+        self.client.login(username='user', password='user')
+        response = self.client.get(reverse('user'))
+        self.assertEqual(response.status_code, 200)
+
+    def test_corporation(self):
+        user = User.objects.get(username='user')
+        corp = Corporation.objects.get(social_reason='EvilCorp')
+        url = reverse('corporation', kwargs={'pk': corp.pk})
+        response = self.client.get(url)
+        self.assertRedirects(response, reverse('login') + '?next=' + url)
+        self.client.login(username='user', password='user')
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 403)
+        corp.members.add(user)
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, '')
+
+    def test_corporation_menu(self):
+        self.client.login(username='user', password='user')
+        response = self.client.get('/')
+        self.assertContains(response, 'GoodCorp')
+        self.assertNotContains(response, 'EvilCorp')
+
+    def test_users_admin(self):
+        self.client.login(username='admin', password='admin')
+        response = self.client.get(reverse('user-list'))
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'admin')
+        self.assertContains(response, 'user')
+        response = self.client.get(reverse('user-add'))
+        self.assertEqual(response.status_code, 200)
+        response = self.client.get(reverse('user-detail', kwargs={'pk': 1}))
+        self.assertEqual(response.status_code, 200)
+        response = self.client.get(reverse('user-edit', kwargs={'pk': 1}))
+        self.assertEqual(response.status_code, 200)
+
+    def test_corporations_admin(self):
+        self.client.login(username='admin', password='admin')
+        response = self.client.get(reverse('corporation-list'))
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'GoodCorp')
+        self.assertContains(response, 'EvilCorp')
+        response = self.client.get(reverse('corporation-add'))
+        self.assertEqual(response.status_code, 200)
+        response = self.client.get(reverse('corporation-detail', kwargs={'pk': 1}))
+        self.assertEqual(response.status_code, 200)
+        response = self.client.get(reverse('corporation-edit', kwargs={'pk': 1}))
+        self.assertEqual(response.status_code, 200)
+
+    def test_adhesions_admin(self):
+        self.client.login(username='admin', password='admin')
+        response = self.client.get(reverse('adhesion-list'))
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'user')
+        response = self.client.get(reverse('adhesion-detail', kwargs={'pk': 1}))
+        self.assertEqual(response.status_code, 200)

+ 12 - 2
adhesions/urls.py

@@ -4,6 +4,16 @@ from . import views
 
 
 urlpatterns = [
-    url(r'^adhesion/$', views.adhesion, name='adhesion'),
-    url(r'^asso/(?P<pk>[0-9]+)/$', views.corporation , name='corporation'),
+    url(r'^$', views.user, name='user'),
+    url(r'^asso/(?P<pk>[0-9]+)/$', views.corporation, name='corporation'),
+    url(r'^users/$', views.UserList.as_view(), name='user-list'),
+    url(r'^users/add/$', views.user_edit, name='user-add'),
+    url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view(), name='user-detail'),
+    url(r'^users/(?P<pk>[0-9]+)/edit/$', views.user_edit, name='user-edit'),
+    url(r'^corporations/$', views.CorporationList.as_view(), name='corporation-list'),
+    url(r'^corporations/add/$', views.CorporationCreate.as_view(), name='corporation-add'),
+    url(r'^corporations/(?P<pk>[0-9]+)/', views.CorporationDetail.as_view(), name='corporation-detail'),
+    url(r'^corporations/(?P<pk>[0-9]+)/edit/', views.CorporationUpdate.as_view(), name='corporation-edit'),
+    url(r'^adhesions/$', views.AdhesionList.as_view(), name='adhesion-list'),
+    url(r'^adhesions/(?P<pk>[0-9]+)/$', views.AdhesionDetail.as_view(), name='adhesion-detail'),
 ]

+ 124 - 6
adhesions/views.py

@@ -1,18 +1,28 @@
 from django.contrib.auth.decorators import login_required
 from django.core.exceptions import PermissionDenied
-from django.shortcuts import render, get_object_or_404
+from django.shortcuts import render, redirect, get_object_or_404
 from django.contrib.auth.models import User
+from django.contrib.auth.mixins import PermissionRequiredMixin
+from django.contrib.auth.decorators import permission_required
+from django.views.generic import ListView, DetailView, CreateView, UpdateView
+from django.contrib.contenttypes.models import ContentType
+from django.contrib import messages
 
-from .models import Corporation
+from accounts.models import Profile
+from accounts.forms import UserForm, ProfileForm
+#from banking.forms import PaymentForm
+
+from .models import Adhesion, Corporation
+from .forms import AdhesionForm
 
 
 @login_required
-def adhesion(request):
-    return render(request, 'adhesions/adhesion.html', {
+def user(request):
+    return render(request, 'adhesions/user.html', {
+        'adherent': request.user,
         'adhesion': request.user.profile.adhesion,
     })
 
-
 @login_required
 def corporation(request, pk):
     corporation = get_object_or_404(Corporation, pk=pk)
@@ -21,5 +31,113 @@ def corporation(request, pk):
     except User.DoesNotExist:
         raise PermissionDenied
     return render(request, 'adhesions/corporation.html', {
-        'corporation': corporation,
+        'adherent': corporation,
+        'adhesion': corporation.adhesion,
+    })
+
+
+class UserMixin(PermissionRequiredMixin):
+    model = User
+    permission_required = 'auth.change_user'
+
+
+class UserList(UserMixin, ListView):
+    template_name = 'adhesions/user_list.html'
+
+
+class UserDetail(UserMixin, DetailView):
+    template_name = 'adhesions/user_detail.html'
+
+
+#class UserCreate(UserMixin, CreateView):
+#    form_class = UserForm
+#    template_name = 'adhesions/user_form.html'
+@permission_required('auth.change_user')
+def user_edit(request, pk=None):
+    if pk:
+        user = get_object_or_404(User, pk=pk)
+        profile = user.profile
+    else:
+        user = None
+        profile = None
+    user_form = UserForm(request.POST or None, instance=user)
+    profile_form = ProfileForm(request.POST or None, instance=profile)
+    forms = [user_form, profile_form]
+    if not pk or not user.profile.adhesion:
+        adhesion_form = AdhesionForm(request.POST or None)
+        forms += [adhesion_form]
+    else:
+        adhesion_form = None
+    if request.method == 'POST' and all([form.is_valid() for form in forms]):
+        error = False
+        if user: # update
+            user_form.save()
+            profile_form.save()
+        else:
+            user = user_form.save()
+            # profile created by user post_save signal
+            profile = Profile.objects.get(user__pk=user.pk)
+            profile_form = ProfileForm(request.POST, instance=profile)
+            if profile_form.is_valid():
+                profile_form.save()
+            else:
+                # we need a new user form bind to user instance
+                user_form = UserForm(request.POST or None, instance=user)
+                forms[0] = user_form
+                error = True
+        if adhesion_form:
+            if adhesion_form.is_valid():
+                if adhesion_form.cleaned_data['adherent']:
+                    Adhesion.objects.create(
+                        adherent_type=ContentType.objects.get_for_model(User),
+                        adherent_id=user.pk,
+                    )
+                    # TODO: contribution
+            else:
+                error = True
+        if not error:
+            messages.success(request, 'Utilisateur créé avec succès.')
+            return redirect('user-detail', pk=user.pk)
+        messages.warn(request, 'Utilisateur créé mais certaines informations du profil '
+                               'n’ont pas été sauvegardé, veuillez les corriger.')
+    return render(request, 'adhesions/user_form.html', {
+        'forms': forms,
+        'user': user,
     })
+
+
+class CorporationMixin(PermissionRequiredMixin):
+    model = Corporation
+    permission_required = 'adhesions.change_corporation'
+
+
+class CorporationList(CorporationMixin, ListView):
+    pass
+
+
+class CorporationDetail(CorporationMixin, DetailView):
+    pass
+
+
+class CorporationCreate(CorporationMixin, CreateView):
+    fields = ('social_reason',)
+
+
+class CorporationUpdate(CorporationMixin, UpdateView):
+    fields = ('social_reason',)
+
+
+class AdhesionMixin(PermissionRequiredMixin):
+    model = Adhesion
+    permission_required = 'adhesions.change_adhesion'
+
+
+class AdhesionList(AdhesionMixin, ListView):
+    pass
+
+
+class AdhesionDetail(AdhesionMixin, DetailView):
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context['show_adherent'] = True
+        return context

+ 156 - 140
banking/admin.py

@@ -8,148 +8,164 @@ from services.models import ServiceType
 from .models import Payment
 
 
-class PaymentMixin:
-    def get_fields(self, request, obj=None):
-        fields = ('amount', 'period', 'payment_method', 'start', 'end')
-        if request.user.has_perm('banking.validate_payment'):
-            fields += ('validated',)
-        return fields
+### Inlines
 
-    def get_queryset(self, request):
-        qs = super().get_queryset(request)
-        if request.user.has_perm('banking.validate_payment'):
-            return qs
-        # Show only adherent contribution and payment related to a service for which the user is in managment group.
-        user_ctype = ContentType.objects.get(app_label='adhesions', model='adherent')
-        return qs.filter(Q(reason_type=user_ctype) | Q(service__service_type__group__in=request.user.groups.all()))
-
-
-class PaymentInline(PaymentMixin, GenericTabularInline):
+class PaymentInline(GenericTabularInline):
     model = Payment
     ct_field = 'reason_type'
     ct_fk_field = 'reason_id'
-    extra = 1
-
-
-class ValidatedPaymentInline(PaymentInline):
     extra = 0
-    verbose_name_plural = 'Paiements validés'
-
-    def get_readonly_fields(self, request, obj=None):
-        if request.user.has_perm('banking.validate_payment'):
-            return ()
-        else:
-            return self.get_fields(request, obj)
-
-    def has_add_permission(self, request):
-        return False
-
-    def has_delete_permission(self, request, obj=None):
-        return request.user.has_perm('banking.validate_payment')
-
-    def get_queryset(self, request):
-        return super().get_queryset(request).filter(validated=True)
-
-
-class PendingOrNewPaymentInline(PaymentInline):
-    verbose_name_plural = 'Paiements en attente de validation et nouveaux paiements'
-
-    def get_queryset(self, request):
-        return super().get_queryset(request).filter(validated=False)
-
-
-class PaymentTypeFilter(admin.SimpleListFilter):
-    title = 'type de paiement'
-    parameter_name = 'type'
-
-    def lookups(self, request, model_admin):
-        choices = [
-            ('membership', 'Cotisation'),
-            ('service', 'Service'),
-        ]
-        service_types = ServiceType.objects.all()
-        if not (request.user.is_superuser or request.user.has_perm('banking.validate_payment')):
-            service_types = service_types.filter(group__in=request.user.groups.all())
-        for stype in service_types:
-            choices.append((stype.pk, 'Service (%s)' % stype.name))
-        return choices
-
-    def queryset(self, request, queryset):
-        if self.value() == 'membership':
-            return queryset.filter(reason_type__app_label='adhesions',
-                                   reason_type__model='adherent')
-        if self.value() == 'service':
-            return queryset.filter(reason_type__app_label='services',
-                                   reason_type__model='service')
-        try:
-            service_type = ServiceType.objects.get(pk=int(self.value()))
-        except (ValueError, TypeError, ServiceType.DoesNotExist,):
-            return queryset
-        else:
-            return queryset.filter(service__service_type=service_type)
-
-
-def validate_payment(payment, request, queryset):
-    queryset.update(validated=True)
-
-
-class PaymentAdmin(PaymentMixin, admin.ModelAdmin):
-    list_display_links = None
-    list_filter = (PaymentTypeFilter, 'payment_method', 'validated',)
-
-    def get_list_display(self, request):
-        list_display = ()
-        if request.user.has_perm('auth.change_user'):
-            list_display += ('adherent_link',)
-        else:
-            list_display += ('get_adherent',)
-        list_display += ('payment_type_verbose', 'amount',
-                        'period', 'payment_method', 'validated',)
-        if request.user.has_perm('banking.validate_payment'):
-            list_display += ('change',)
-        else:
-            list_display += ('change_pending',)
-        return list_display
-
-    def adherent_link(self, obj):
-        adherent = obj.get_adherent()
-        url = reverse('admin:adhesions_adherent_change', args=[adherent.pk])
-        return '<a href="%s">%s</a>' % (url, adherent)
-    adherent_link.short_description = 'Adhérent'
-    adherent_link.allow_tags = True
-
-    def change(self, obj):
-        url = reverse('admin:banking_payment_change', args=[obj.pk])
-        return '<a href="%s" class="changelink">Modifier</a>' % url
-    change.short_description = ''
-    change.allow_tags = True
-
-    def change_pending(self, obj):
-        if obj.validated:
-            return '-'
-        else:
-            return self.change(obj)
-    change_pending.short_description = ''
-    change_pending.allow_tags = True
-
-    def get_actions(self, request):
-        actions = super().get_actions(request)
-        if request.user.has_perm('banking.validate_payment'):
-            actions['validate'] = (validate_payment, 'validate', 'Valider les paiements sélectionnés')
-        return actions
-
-    def has_add_permission(self, request):
-        return False
-
-    def has_change_permission(self, request, obj=None):
-        if obj and not request.user.has_perm('banking.validate_payment'):
-            return not obj.validated
-        return True
-
-    def has_delete_permission(self, request, obj=None):
-        if obj and not request.user.has_perm('banking.validate_payment'):
-            return not obj.validated
-        return False
-
-
-admin.site.register(Payment, PaymentAdmin)
+    #max_num = 0
+    fields = ('amount', 'period', 'payment_method', 'start', 'end',)
+    #readonly_fields = ('amount', 'period', 'payment_method', 'start',)
+    verbose_name_plural = 'Contributions'
+
+    #def get_queryset(self, request):
+    #    # Paiement récurrent en cours (sans date de fin)
+    #    return super().get_queryset(request).filter(period__gt=0, end__isnull=True)
+
+    #def has_delete_permission(self, request, obj=None):
+    #    return False
+
+
+#class ValidatedPaymentInline(PaymentInline):
+#    extra = 0
+#    verbose_name_plural = 'Paiements validés'
+#
+#    def get_readonly_fields(self, request, obj=None):
+#        if request.user.has_perm('banking.validate_payment'):
+#            return ()
+#        else:
+#            return self.get_fields(request, obj)
+#
+#    def has_add_permission(self, request):
+#        return False
+#
+#    def has_delete_permission(self, request, obj=None):
+#        return request.user.has_perm('banking.validate_payment')
+#
+#    def get_queryset(self, request):
+#        return super().get_queryset(request).filter(validated=True)
+#
+#
+#class PendingOrNewPaymentInline(PaymentInline):
+#    verbose_name_plural = 'Paiements en attente de validation et nouveaux paiements'
+#
+#    def get_queryset(self, request):
+#        return super().get_queryset(request).filter(validated=False)
+
+
+### Filters
+
+#class PaymentTypeFilter(admin.SimpleListFilter):
+#    title = 'type de paiement'
+#    parameter_name = 'type'
+#
+#    def lookups(self, request, model_admin):
+#        choices = [
+#            ('membership', 'Cotisation'),
+#            ('service', 'Service'),
+#        ]
+#        service_types = ServiceType.objects.all()
+#        if not (request.user.is_superuser or request.user.has_perm('banking.validate_payment')):
+#            service_types = service_types.filter(group__in=request.user.groups.all())
+#        for stype in service_types:
+#            choices.append((stype.pk, 'Service (%s)' % stype.name))
+#        return choices
+#
+#    def queryset(self, request, queryset):
+#        if self.value() == 'membership':
+#            return queryset.filter(reason_type__app_label='adhesions',
+#                                   reason_type__model='adherent')
+#        if self.value() == 'service':
+#            return queryset.filter(reason_type__app_label='services',
+#                                   reason_type__model='service')
+#        try:
+#            service_type = ServiceType.objects.get(pk=int(self.value()))
+#        except (ValueError, TypeError, ServiceType.DoesNotExist,):
+#            return queryset
+#        else:
+#            return queryset.filter(service__service_type=service_type)
+#
+#
+#### Actions
+#
+#def validate_payment(payment, request, queryset):
+#    queryset.update(validated=True)
+#
+#
+#### ModelAdmin
+#
+#class PaymentAdmin(admin.ModelAdmin):
+#    #list_display_links = None
+#    list_filter = (PaymentTypeFilter, 'payment_method', 'validated',)
+#
+#    def get_list_display(self, request):
+#        list_display = ()
+#        #if request.user.has_perm('auth.change_user'):
+#        #    list_display += ('adherent_link',)
+#        #else:
+#        #list_display += ('get_adherent',)
+#        list_display += ('payment_type_verbose', 'get_adherent', 'amount',
+#                        'period', 'payment_method', 'start_display', 'end_display', 'validated_display',)
+#        #if request.user.has_perm('banking.validate_payment'):
+#        #    list_display += ('change',)
+#        #else:
+#        #    list_display += ('change_pending',)
+#        return list_display
+#
+#    def start_display(self, obj):
+#        return obj.start
+#    start_display.short_description = 'Début'
+#
+#    def end_display(self, obj):
+#        return obj.end
+#    end_display.short_description = 'Fin'
+#
+#    def validated_display(self, obj):
+#        return obj.validated
+#    validated_display.short_description = 'Validé'
+#    validated_display.boolean = True
+#
+#    #def adherent_link(self, obj):
+#    #    adherent = obj.get_adherent()
+#    #    url = reverse('admin:adhesions_adherent_change', args=[adherent.pk])
+#    #    return '<a href="%s">%s</a>' % (url, adherent)
+#    #adherent_link.short_description = 'Adhérent'
+#    #adherent_link.allow_tags = True
+#
+#    def change(self, obj):
+#        url = reverse('admin:banking_payment_change', args=[obj.pk])
+#        return '<a href="%s" class="changelink">Modifier</a>' % url
+#    change.short_description = ''
+#    change.allow_tags = True
+#
+#    def change_pending(self, obj):
+#        if obj.validated:
+#            return '-'
+#        else:
+#            return self.change(obj)
+#    change_pending.short_description = ''
+#    change_pending.allow_tags = True
+#
+#    def get_actions(self, request):
+#        actions = super().get_actions(request)
+#        if request.user.has_perm('banking.validate_payment'):
+#            actions['validate'] = (validate_payment, 'validate', 'Valider les paiements sélectionnés')
+#        return actions
+#
+#    def has_add_permission(self, request):
+#        return False
+#
+#    def has_change_permission(self, request, obj=None):
+#        if obj and not request.user.has_perm('banking.validate_payment'):
+#            return not obj.validated
+#        return True
+#
+#    def has_delete_permission(self, request, obj=None):
+#        if obj and not request.user.has_perm('banking.validate_payment'):
+#            return not obj.validated
+#        return False
+#
+#
+#admin.site.register(Payment, PaymentAdmin)

+ 12 - 8
banking/models.py

@@ -15,7 +15,7 @@ class Payment(models.Model):
         (TRANSFERT, 'Virement'),
         (WITHDRAWAL, 'Prélèvement'),
     )
-    limit = models.Q(app_label='adhesions', model='Adherent') \
+    limit = models.Q(app_label='adhesions', model='Adhesion') \
         | models.Q(app_label='services', model='Service')
     reason_type = models.ForeignKey(ContentType, on_delete=models.CASCADE,
                                     limit_choices_to=limit)
@@ -38,7 +38,7 @@ class Payment(models.Model):
 
     def payment_type_verbose(self):
         if self.reason_type.app_label == 'adhesions' \
-                and self.reason_type.model == 'adherent':
+                and self.reason_type.model == 'adhesion':
             return 'Cotisation'
         if self.reason_type.app_label == 'services' \
                 and self.reason_type.model == 'service':
@@ -59,16 +59,19 @@ class Payment(models.Model):
         else:
             return '%d mois' % self.period
 
-    # Note: 'adherent' is already used as the related_query_name
-    #        of the GenericRelation on Adherent
-    def get_adherent(self):
+    # FIXME
+    @property
+    def adherent(self):
+        return self.get_adhesion().adherent
+
+    def get_adhesion(self):
         if self.reason_type.app_label == 'adhesions' \
-                and self.reason_type.model == 'adherent':
+                and self.reason_type.model == 'adhesion':
             return self.reason
         if self.reason_type.app_label == 'services' \
                 and self.reason_type.model == 'service':
-            return self.reason.adherent
-    get_adherent.short_description = 'Adhérent'
+            return self.reason.adhesion
+    get_adhesion.short_description = 'Adhésion'
 
     def clean(self):
         super().clean()
@@ -79,6 +82,7 @@ class Payment(models.Model):
         if self.end and self.start > self.end:
             raise ValidationError("La date de début de paiement doit être antérieur à la date de fin de paiement.")
         # Vérification de l’absence de chevauchement avec une période existante
+        # FIXME ne fonctionne pas vraiment via les inlines…
         if self.reason:
             payments = Payment.objects.filter(reason_type=self.reason_type, reason_id=self.reason_id)
             if is_overlapping(self, payments):

+ 1 - 0
coverage.sh

@@ -11,4 +11,5 @@ if [ "$?" -ne 0 ]; then
 fi
 
 coverage run --branch --source=djadhere,accounts,adhesions,banking,services --omit=*/migrations/*.py "$BASEDIR"/manage.py test
+coverage html
 coverage report

+ 2 - 2
djadhere/settings.py

@@ -112,7 +112,7 @@ AUTH_PASSWORD_VALIDATORS = [
 AUTHENTICATION_BACKENDS = [
     'django.contrib.auth.backends.ModelBackend',
     'accounts.backends.EmailBackend',
-    'adhesions.backends.AdherentBackend',
+    'adhesions.backends.AdhesionBackend',
 ]
 
 
@@ -135,7 +135,7 @@ USE_TZ = True
 
 STATIC_URL = '/static/'
 
-LOGOUT_REDIRECT_URL = 'home'
+LOGOUT_REDIRECT_URL = 'user'
 
 BOOTSTRAP3 = {
 

+ 3 - 3
djadhere/templates/_form.html

@@ -1,4 +1,4 @@
-{% load bootstrap3 i18n %}
+{% load bootstrap3 %}
 <form action="" method="post" class="form-horizontal"{% if multipart %} enctype=multipart/form-data{% endif %}>
   {% csrf_token %}
   {% block beforeform %}{% endblock %}
@@ -9,10 +9,10 @@
   {% bootstrap_form form layout="horizontal" %}
   {% endfor %}
   {% buttons layout="horizontal" %}
-  <button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
+  <button type="submit" class="btn btn-primary">Envoyer</button>
   {% for url, class, text in buttons %}
   <a href="{% url url %}" class="btn btn-{{ class }}">{{ text }}</a>
   {% endfor %}
-  <a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{% url 'home' %}{% endif %}" class="btn btn-default">{% trans "Cancel" %}</a>
+  {% if request.META.HTTP_REFERER %}<a href="{{ request.META.HTTP_REFERER }}" class="btn btn-default">Annuler</a>{% endif %}
   {% endbuttons %}
 </form>

+ 22 - 9
djadhere/templates/base.html

@@ -23,30 +23,43 @@
             <span class="icon-bar"></span>
             <span class="icon-bar"></span>
           </button>
-          <a class="navbar-brand" href="{% url 'home' %}">tetaneutral.net</a>
+          <span class="navbar-brand">tetaneutral.net</span>
         </div>
         <div id="navbar" class="navbar-collapse collapse">
           <ul class="nav navbar-nav">
-            <li{% block hometab %}{% endblock %}><a href="{% url 'home' %}"><span class="glyphicon glyphicon-home"></span>&nbsp;Accueil</a></li>
-            <li{% block adhesiontab %}{% endblock %}><a href="{% url 'adhesion' %}"><span class="glyphicon glyphicon-heart-empty"></span>&nbsp;Mon adhésion</a></li>
-            <li{% block servicestab %}{% endblock %}><a href="{% url 'service-list' %}"><span class="glyphicon glyphicon-list"></span>&nbsp;Mes services</a></li>
-            <li class="dropdown{% block assotab %}{% endblock %}">
+            <li{% block usertab %}{% endblock %}><a href="{% url 'user' %}"><span class="glyphicon glyphicon-heart-empty"></span>&nbsp;Mon adhésion</a></li>
+            {% for corp in request.corporations %}
+            {% if forloop.first %}
+            <li class="dropdown{% block corptab %}{% endblock %}">
                 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
                     <span class="glyphicon glyphicon-globe"></span>&nbsp;Mes asso <span class="caret"></span>
                 </a>
                 <ul class="dropdown-menu">
-                    {% for corp in request.corporations %}
+            {% endif %}
                     <li><a href="{% url 'corporation' corp.object.pk %}">{{ corp.object.social_reason }}</a></li>
-                    {% empty %}
-                    <li><em>Aucune associations</em></li>
-                    {% endfor %}
+            {% if forloop.last %}
                 </ul>
             </li>
+            {% endif %}
+            {% endfor %}
           </ul>
           <ul class="nav navbar-nav navbar-right">
             {% if request.user.is_staff %}
             <li><a href="{% url 'admin:index' %}"><span class="glyphicon glyphicon-dashboard"></span>&nbsp;Django-Admin</a></li>
             {% endif %}
+            {% if request.user.is_staff %}
+            <li class="dropdown{% block managetab %}{% endblock %}">
+                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
+                    <span class="glyphicon glyphicon-cog"></span>&nbsp;Gestion <span class="caret"></span>
+                </a>
+                <ul class="dropdown-menu">
+                    <li><a href="{% url 'user-list' %}"><span class="glyphicon glyphicon-user">&nbsp;Utilisateurs</a></li>
+                    <li><a href="{% url 'corporation-list' %}"><span class="glyphicon glyphicon-globe">&nbsp;Associations</a></li>
+                    <li><a href="{% url 'adhesion-list' %}"><span class="glyphicon glyphicon-heart-empty">&nbsp;Adhérents</a></li>
+                    <li><a href="{% url 'service-list' %}"><span class="glyphicon glyphicon-tasks">&nbsp;Services</a></li>
+                </ul>
+            </li>
+            {% endif %}
             <li{% block profiletab %}{% endblock %}><a href="{% url 'profile' %}"><span class="glyphicon glyphicon-user"></span>&nbsp;Profil</a></li>
             <li><a href="{% url 'logout' %}" data-toggle="tooltip" data-placement="bottom" title="Logout"><span class="glyphicon glyphicon-log-out"></span></a></li>
           </ul>

+ 0 - 7
djadhere/templates/home.html

@@ -1,7 +0,0 @@
-{% extends 'base.html' %}
-
-{% block hometab %} class="active"{% endblock %}
-
-{% block content %}
-<h1>Bienvenue <b>{{ name }}</b> !</h1>
-{% endblock %}

+ 0 - 15
djadhere/tests.py

@@ -1,15 +0,0 @@
-from django.contrib.auth.models import User
-from django.core.urlresolvers import reverse
-from django.test import TestCase
-
-
-class DjadhereTests(TestCase):
-    def setUp(self):
-        user = User.objects.create_user('user', email='user@example.net', password='user')
-
-    def test_home(self):
-        response = self.client.get(reverse('home'))
-        self.assertRedirects(response, reverse('login') + '?next=' + reverse('home'))
-        self.client.login(username='user', password='user')
-        response = self.client.get(reverse('home'))
-        self.assertEqual(response.status_code, 200)

+ 0 - 3
djadhere/urls.py

@@ -16,11 +16,8 @@ Including another URLconf
 from django.conf.urls import url, include
 from django.contrib import admin
 
-from djadhere import views
-
 
 urlpatterns = [
-    url(r'^$', views.home, name='home'),
     url(r'^accounts/', include('accounts.urls')),
     url(r'^services/', include('services.urls')),
     url(r'^', include('adhesions.urls')),

+ 14 - 2
djadhere/utils.py

@@ -1,12 +1,24 @@
 from django.db.models import Q
 from django.utils import timezone
+from django.forms import widgets
+from django.utils.safestring import mark_safe
+
+
+# Ce widget permet d’afficher un champ de formulaire sous forme de texte
+class StringWidget(widgets.Input):
+    def render(self, name, value, attrs=None):
+        # Create a hidden field first
+        hidden_field = widgets.HiddenInput(attrs)
+        return mark_safe(u'%s %s' % (value, hidden_field.render(value, attrs)))
 
 
 # Cette fonction permet d’obtenir un filtre s’appliquant aux paiements et aux allocations de ressources
-def get_active_filter():
+def get_active_filter(prefix=''):
+    if prefix and not prefix.endswith('__'):
+        prefix += '__'
     now = timezone.now()
     # Début antérieur et fin non spécifié ou postérieur
-    return Q(start__lte=now) & (Q(end__isnull=True) | Q(end__gte=now))
+    return Q(**{prefix + 'start__lte': now}) & (Q(**{prefix + 'end__isnull': True}) | Q(**{prefix + 'end__gte': now}))
 
 
 # Cette fonction vérifie que l’object « instance » ne chevauche pas temporellement un objet présent dans « queryset »

+ 0 - 9
djadhere/views.py

@@ -1,9 +0,0 @@
-from django.contrib.auth.decorators import login_required
-from django.shortcuts import render
-
-
-@login_required
-def home(request):
-    return render(request, 'home.html', {
-        'name': request.user.get_short_name() or request.user.username,
-    })

+ 103 - 58
services/admin.py

@@ -1,15 +1,54 @@
 from django.contrib import admin
-from django.db.models import Q
+from django.db import models
+from django.forms import ModelForm
 from django.utils import timezone
+from django.core.urlresolvers import reverse
+from django.utils.html import format_html
 
+from djadhere.utils import get_active_filter
+from banking.models import Payment
+from banking.admin import PaymentInline
 from .models import Service, ServiceType, IPResource, ResourceAllocation
-from banking.admin import PaymentInline, ValidatedPaymentInline, PendingOrNewPaymentInline
 
 
-class AllocationInline(admin.TabularInline):
-    model = ResourceAllocation
-    extra = 1
-    can_delete = False
+### Filters
+
+class ResourceInUseFilter(admin.SimpleListFilter):
+    title = 'disponibilité'
+    parameter_name = 'available'
+
+    def lookups(self, request, model_admin):
+        return (
+            (1, 'Disponible'),
+            (0, 'Non disponible'),
+        )
+
+    def queryset(self, request, queryset):
+        now = timezone.now()
+        active_filter = get_active_filter('allocation')
+        if self.value() == '0': # non disponible
+            return queryset.filter(active_filter)
+        if self.value() == '1': # disponible
+            return queryset.exclude(active_filter)
+
+
+class AllocationStatusFilter(admin.SimpleListFilter):
+    title = 'statut'
+    parameter_name = 'status'
+
+    def lookups(self, request, model_admin):
+        return (
+            (1, 'En cours'),
+            (0, 'Terminée'),
+        )
+
+    def queryset(self, request, queryset):
+        now = timezone.now()
+        active_filter = get_active_filter()
+        if self.value() == '0': # inactif
+            return queryset.exclude(active_filter)
+        if self.value() == '1': # actif
+            return queryset.filter(active_filter)
 
 
 class ServiceTypeFilter(admin.SimpleListFilter):
@@ -17,75 +56,81 @@ class ServiceTypeFilter(admin.SimpleListFilter):
     parameter_name = 'type'
 
     def lookups(self, request, model_admin):
-        service_types = ServiceType.objects.all()
-        if not request.user.is_superuser:
-            service_types = service_types.filter(group=request.user.groups.all())
-        return service_types.values_list('pk', 'name')
+        return ServiceType.objects.values_list('pk', 'name')
 
     def queryset(self, request, queryset):
         if self.value():
             return queryset.filter(service_type__pk=self.value())
 
 
-class ServiceAdmin(admin.ModelAdmin):
-    list_display = ('id', 'adherent', 'service_type', 'label', 'active')
-    list_filter = ('active', ServiceTypeFilter,)
+### Helpers
 
-    def get_form(self, request, obj=None, **kwargs):
-        # get_inlines does not exists :-(
-        if request.user.has_perm('banking.validate_payment'):
-            self.inlines = (PaymentInline,)
-        else:
-            self.inlines = (ValidatedPaymentInline, PendingOrNewPaymentInline,)
-        self.inlines = self.inlines + (AllocationInline,)
-        return super().get_form(request, obj, **kwargs)
-
-    def get_queryset(self, request):
-        qs = super().get_queryset(request)
-        if request.user.is_superuser:
-            return qs
-        return qs.filter(service_type__group__in=request.user.groups.all())
-
-    def get_readonly_fields(self, request, obj=None):
-        fields = super().get_readonly_fields(request, obj)
-        if not request.user.is_superuser:
-            return fields + ('service_type', 'ip_resources',)
-        return fields
-
-
-class InUseFilter(admin.SimpleListFilter):
-    title = 'disponibilité'
-    parameter_name = 'available'
+def get_foreignkey_link_func(field):
+    def foreignkey_link(self, instance):
+        obj = getattr(instance, field)
+        url = reverse('admin:%s_%s_change' % (obj._meta.app_label,  
+                                                obj._meta.model_name),
+                        args=(obj.id,))
+        return format_html(u'<a href="{}">{}</a>', url, str(obj))
+    return foreignkey_link
 
-    def lookups(self, request, model_admin):
-        return (
-            (1, 'Disponible'),
-            (0, 'Non disponible'),
-        )
 
-    def queryset(self, request, queryset):
-        # On utilise le même filtre que get_active_filter(), mais préfixé par « allocation__ » …
-        # Ce billet donne une technique avec une classe PrefixedQ pour éviter cette duplication de code :
-        # https://hackerluddite.wordpress.com/2012/07/07/making-django-orm-more-dry-with-prefixes-and-qs/
-        now = timezone.now()
-        if self.value() == '0': # non disponible
-            return queryset.filter(Q(allocation__start__lte=now) & (Q(allocation__end__isnull=True) | Q(allocation__end__gte=now)))
-        if self.value() == '1': # disponible
-            return queryset.exclude(Q(allocation__start__lte=now) & (Q(allocation__end__isnull=True) | Q(allocation__end__gte=now)))
+### Inlines
+
+class AllocationInline(admin.TabularInline):
+    model = ResourceAllocation
+    extra = 0
+    fields = ('id', 'service', 'resource', 'start', 'end')
+    raw_id_fields = ('service', 'resource',)
+    verbose_name_plural = 'Allocations'
+
+
+### Actions
+
+def ends_resource(resource, request, queryset):
+    now = timezone.now()
+    queryset.exclude(start__lte=now, end__isnull=False).update(end=now)
+ends_resource.short_description = 'Terminer les allocations sélectionnées'
+
+
+### ModelAdmin
+
+class ServiceAdmin(admin.ModelAdmin):
+    list_display = ('id', 'adhesion', 'service_type', 'label', 'active')
+    list_filter = ('active', ServiceTypeFilter,)
+    inlines = (PaymentInline, AllocationInline,)
+    search_fields = ('id', 'service_type__name', 'label')
+    raw_id_fields = ('adhesion',)
 
 
 class IPResourceAdmin(admin.ModelAdmin):
-    list_display = ('__str__', 'in_use_view')
-    list_filter = (InUseFilter,)
+    list_display = ('__str__', 'in_use_display')
+    list_filter = (ResourceInUseFilter,)
+    fields =('ip', 'mask',)
+    search_fields = ('ip',)
     inlines = (AllocationInline,)
 
-    def in_use_view(self, obj):
+    def in_use_display(self, obj):
         return not obj.in_use
-    in_use_view.short_description = 'Disponible'
-    in_use_view.boolean = True
+    in_use_display.short_description = 'Disponible'
+    in_use_display.boolean = True
+
+
+class ResourceAllocationAdmin(admin.ModelAdmin):
+    list_display = ('id', 'resource_link', 'service_link', 'start', 'end',)
+    list_filter = (AllocationStatusFilter,)
+    actions = (ends_resource,)
+    raw_id_fields = ('service',)
+    search_fields = ('resource__ip', 'service__id', 'service__service_type__name', 'service__label')
+
+    resource_link = get_foreignkey_link_func('resource')
+    resource_link.short_description = 'Ressource'
+
+    service_link = get_foreignkey_link_func('service')
+    service_link.short_description = 'Service'
 
 
 admin.site.register(ServiceType)
 admin.site.register(Service, ServiceAdmin)
 admin.site.register(IPResource, IPResourceAdmin)
-admin.site.register(ResourceAllocation)
+admin.site.register(ResourceAllocation, ResourceAllocationAdmin)

+ 0 - 3
services/apps.py

@@ -4,6 +4,3 @@ from django.apps import AppConfig
 class ServicesConfig(AppConfig):
     name = 'services'
     verbose_name = 'Services'
-
-    def ready(self):
-        import services.signals  # noqa

+ 32 - 0
services/migrations/0011_auto_20170301_1502.py

@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-03-01 14:02
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('services', '0010_auto_20170211_1635'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='ipresource',
+            options={'verbose_name': 'ressource IP', 'verbose_name_plural': 'ressources IP'},
+        ),
+        migrations.AlterModelOptions(
+            name='resourceallocation',
+            options={'verbose_name': 'allocation', 'verbose_name_plural': 'allocations'},
+        ),
+        migrations.AlterModelOptions(
+            name='servicetype',
+            options={'verbose_name': 'type de service', 'verbose_name_plural': 'types de service'},
+        ),
+        migrations.RenameField(
+            model_name='service',
+            old_name='adherent',
+            new_name='adhesion',
+        ),
+    ]

+ 19 - 14
services/models.py

@@ -5,9 +5,10 @@ from django.utils import timezone
 from django.contrib.auth.models import Group
 from django.contrib.contenttypes.fields import GenericRelation
 from django.core.exceptions import ValidationError
+from django.urls import reverse
 
 from djadhere.utils import get_active_filter, is_overlapping
-from adhesions.models import Adherent
+from adhesions.models import Adhesion
 from banking.models import Payment
 
 
@@ -24,8 +25,8 @@ class IPResource(models.Model):
             return False
 
     class Meta:
-        verbose_name = 'Ressource IP'
-        verbose_name_plural = 'Ressources IP'
+        verbose_name = 'ressource IP'
+        verbose_name_plural = 'ressources IP'
 
     def __str__(self):
         r = str(self.ip)
@@ -41,15 +42,15 @@ class ServiceType(models.Model):
                               related_name='service_types')
 
     class Meta:
-        verbose_name = 'Type de service'
-        verbose_name_plural = 'Types de service'
+        verbose_name = 'type de service'
+        verbose_name_plural = 'types de service'
 
     def __str__(self):
         return self.name
 
 
 class Service(models.Model):
-    adherent = models.ForeignKey(Adherent, verbose_name='Adhérent', related_name='services')
+    adhesion = models.ForeignKey(Adhesion, verbose_name='Adhérent', related_name='services')
     service_type = models.ForeignKey(ServiceType, related_name='services',
                                      verbose_name='Type de service')
     label = models.CharField(blank=True, default='', max_length=128)
@@ -67,7 +68,7 @@ class Service(models.Model):
             return self.contributions.get(get_active_filter())
         except Payment.DoesNotExist:
             return None
-        # MultipleObjectsReturned non catché volontairement, cf remarque adhesions.Adherent.contribution
+        # MultipleObjectsReturned non catché volontairement, cf remarque adhesions.Adhesion.contribution
 
     @property
     def active_allocations(self):
@@ -83,6 +84,9 @@ class Service(models.Model):
         if self.label != '' and Service.objects.exclude(pk=self.pk).filter(service_type=self.service_type, label=self.label):
             raise ValidationError("Un service du même type existe déjà avec ce label.")
 
+    def get_absolute_url(self):
+        return reverse('service-detail', kwargs={'pk': self.pk})
+
     def __str__(self):
         s = str(self.service_type)
         if self.label:
@@ -103,14 +107,15 @@ class ResourceAllocation(models.Model):
             raise ValidationError("La date de début de l’allocation doit être antérieur "
                                   "à la date de fin de l’allocation.")
         # Vérification de l’abscence de chevauchement de la période d’allocation
-        allocations = ResourceAllocation.objects.filter(resource__pk=self.resource.pk)
-        if is_overlapping(self, allocations):
-            raise ValidationError("La période d’allocation de cette ressource chevauche "
-                                    "avec une période d’allocation précédente.")
+        if self.resource_id:
+            allocations = ResourceAllocation.objects.filter(resource__pk=self.resource.pk)
+            if is_overlapping(self, allocations):
+                raise ValidationError("La période d’allocation de cette ressource chevauche "
+                                        "avec une période d’allocation précédente.")
 
     class Meta:
-        verbose_name = 'Allocation de ressource'
-        verbose_name_plural = 'Allocations de ressources'
+        verbose_name = 'allocation'
+        verbose_name_plural = 'allocations'
 
     def __str__(self):
-        return str(self.resource) + ' pour ' + str(self.service)
+        return str(self.resource)

+ 0 - 33
services/signals.py

@@ -1,33 +0,0 @@
-from django.dispatch import receiver
-from django.db.models.signals import pre_save
-from django.contrib.auth.models import Permission
-
-from .models import ServiceType
-
-
-@receiver(pre_save, sender=ServiceType)
-def update_user_permissions(sender, instance, raw, **kwargs):
-    perms = [
-        'add_user',
-        'change_user',
-        'change_profile',
-        'add_adherent',
-        'change_adherent',
-        'add_payment',
-        'change_payment',
-        'delete_payment',
-        'change_service',
-    ]
-    try:
-        old_group = ServiceType.objects.get(pk=instance.pk).group
-    except ServiceType.DoesNotExist:
-        old_group = None
-    new_group = instance.group
-    perms = Permission.objects.filter(codename__in=perms).all()
-    if old_group is None and new_group is not None:
-        # add permissions to new_group
-        new_group.permissions.add(*perms)
-    elif old_group is not None and new_group is None:
-        # remove permissions from old_group *if not needed elsewhere*
-        if not old_group.service_types.exclude(pk=instance.pk).exists():
-            old_group.permissions.remove(*perms)

+ 15 - 2
services/templates/services/_service_list.html

@@ -1,10 +1,22 @@
+{% load djadhere services %}
+
 {% for service in services %}
 {% if forloop.first %}
-<div class="list-group">
+<div class="panel panel-primary">
+    <div class="panel-heading">
+        <h3 class="list-group-item-heading">Services</h3>
+        <p>
+            Services actifs : {{ services|active|count }}<br />
+            Services inactifs : {{ services|inactive|count }}
+        </p>
+    </div>
+    <div class="list-group">
 {% endif %}
     <a href="{% url 'service-detail' service.pk %}" class="list-group-item {% if service.active %}list-group-item-success{% else %}list-group-item-danger{% endif %}">
+        <span class="badge">#{{ service.id }}</span>
         <h4 class="list-group-item-heading">
-            <b>{{ service.service_type }}</b> {{ service.label }}
+            <b>{{ service.service_type }}</b>
+            {{ service.label }}
         </h4>
         {% if service.contribution.count == 1 %}
         <p class="list-group-item-text">
@@ -20,6 +32,7 @@
         </p>
     </a>
 {% if forloop.last %}
+    </div>
 </div>
 {% endif %}
 {% endfor %}

+ 12 - 13
services/templates/services/service_detail.html

@@ -2,28 +2,27 @@
 
 {% load bootstrap3 %}
 
-{% block servicestab %}{% if service.adherent == request.user.profile.adhesion %} class="active"{% endif %}{% endblock %}
-{% block assotab %}{% if service.adherent != request.user.profile.adhesion %} active{% endif %}{% endblock %}
+{% block usertab %}{% if service.adhesion == request.user.profile.adhesion %} class="active"{% endif %}{% endblock %}
+{% block corptab %}{% if service.adhesion != request.user.profile.adhesion %} active{% endif %}{% endblock %}
 
 {% block content %}
-{% if service.adherent == request.user.profile.adhesion %}
-<a href="{% url 'service-list' %}" class="btn btn-primary"><span class="glyphicon glyphicon-arrow-left"></span>&nbsp;Retour à la liste de mes services</a>
-{% else %}
-<a href="{% url 'corporation' service.adherent.adherent.pk %}" class="btn btn-primary">
-    <span class="glyphicon glyphicon-arrow-left"></span>&nbsp;Retour à la liste des services de {{ service.adherent.adherent }}
-</a>
-{% endif %}
-
-<br /><br />
-
-<div class="panel panel-default">
+<div class="panel panel-primary">
     <div class="panel-heading">
+        {% if perms.services.change_service %}
+        <a class="btn btn-success pull-right" href="{% url 'service-edit' service.pk %}"><span class="glyphicon glyphicon-edit"></span>&nbsp;Éditer</a>
+        {% endif %}
         <h3>{{ service.service_type }}</h3>
         {% if service.label %}
         <h4>{{ service.label }}</h4>
         {% endif %}
     </div>
     <div class="panel-body">
+        <p>Identifiant du service : #{{ service.id }}</p>
+        <p>Responsable :
+            <a href="{% url 'adhesion-detail' service.adhesion.id %}">ADT{{ service.adhesion.id }}</a>
+            –
+            <a href="{{ service.adhesion.get_adherent_url }}">{{ service.adhesion.get_adherent_name }}</a>
+        </p>
         {% if service.contribution %}
         <p>Contribution : {{ service.contribution }}</p>
         {% else %}

+ 9 - 0
services/templates/services/service_form.html

@@ -0,0 +1,9 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+<h1>{% if service %}Modifier{% else %}Ajouter{% endif %} un service</h1>
+
+{% include '_form.html' %}
+
+{% endblock %}

+ 32 - 14
services/templates/services/service_list.html

@@ -1,21 +1,39 @@
 {% extends 'base.html' %}
 
-{% load bootstrap3 djadhere services %}
-
-{% block servicestab %} class="active"{% endblock %}
+{% load bootstrap3 %}
 
 {% block content %}
-<div class="well">
-    {% if object_list %}
-    <h3>Mes services</h3>
-    <p>Services actifs : {{ object_list|active|count }}</p>
-    <p>Services inactifs : {{ object_list|inactive|count }}</p>
-    {% else %}
-    <h3>Vous n’avez aucun service :-(</h3>
+<div class="panel panel-primary">
+    <div class="panel-heading">
+        <h1>Services</h1>
+    </div>
+    {% for service in object_list %}
+    {% if forloop.first %}
+    <table class="table">
+        <tr>
+            <th>#</th>
+            <th>Type</th>
+            <th>Label</th>
+            <th>Adhérent</th>
+            <th></th>
+        </tr>
+    {% endif %}
+        <tr>
+            <td><a href="{% url 'service-detail' service.pk %}">#{{ service.pk }}</a></td>
+            <td>{{ service.service_type }}</td>
+            <td>{{ service.label }}</td>
+            <td>
+                <a href="{% url 'adhesion-detail' service.adhesion.id %}">ADT{{ service.adhesion.pk }}</a>
+                –
+                <a href="{{ service.adhesion.get_adherent_detail_url }}">{{ service.adhesion }}</a>
+            </td>
+            <td class="text-right">
+                <a href="{% url 'service-edit' service.pk %}"><span class="glyphicon glyphicon-pencil"></span>&nbsp;Modifier</a>
+            </td>
+        </tr>
+    {% if forloop.last %}
+    </table>
     {% endif %}
+    {% endfor %}
 </div>
-
-{% with services=object_list %}
-{% include 'services/_service_list.html' %}
-{% endwith %}
 {% endblock %}

+ 7 - 3
services/tests.py

@@ -3,19 +3,23 @@ from django.core.urlresolvers import reverse
 from django.test import TestCase
 from django.contrib.contenttypes.models import ContentType
 
-from adhesions.models import Adherent
+from adhesions.models import Adhesion
 from .models import Service, ServiceType
 
 
 class ServicesTests(TestCase):
     def setUp(self):
         user = User.objects.create_user('user', email='user@example.net', password='user')
-        adh = Adherent.objects.create(adherent_type=ContentType.objects.get_for_model(user), adherent_id=user.pk)
+        user = User.objects.create_user('admin', email='admin@example.net', password='admin', is_superuser=True)
+        adhesion = Adhesion.objects.create(adherent_type=ContentType.objects.get_for_model(user), adherent_id=user.pk)
         stype = ServiceType.objects.create(name='VM')
-        s1 = Service.objects.create(adherent=adh, service_type=stype, label='Service 1')
+        s1 = Service.objects.create(adhesion=adhesion, service_type=stype, label='Service 1')
 
     def test_service_list(self):
         self.client.login(username='user', password='user')
         response = self.client.get(reverse('service-list'))
+        self.assertRedirects(response, reverse('login') + '?next=' + reverse('service-list'))
+        self.client.login(username='admin', password='admin')
+        response = self.client.get(reverse('service-list'))
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, 'Service 1')

+ 2 - 1
services/urls.py

@@ -4,6 +4,7 @@ from . import views
 
 
 urlpatterns = [
-    url(r'^list/$', views.ServiceList.as_view(), name='service-list'),
+    url(r'^$', views.ServiceList.as_view(), name='service-list'),
     url(r'^(?P<pk>[0-9]+)/$', views.ServiceDetail.as_view(), name='service-detail'),
+    url(r'^(?P<pk>[0-9]+)/edit/$', views.ServiceUpdate.as_view(), name='service-edit'),
 ]

+ 18 - 11
services/views.py

@@ -1,21 +1,28 @@
 from django.shortcuts import render
-from django.views.generic import ListView, DetailView
+from django.views.generic import ListView, DetailView, UpdateView
 from django.contrib.auth.mixins import LoginRequiredMixin
+from django.contrib.auth.mixins import PermissionRequiredMixin
 
 from .models import Service
 
 
-# Cette page liste uniquement les services de l’utilisateur
-class ServiceList(LoginRequiredMixin, ListView):
-    def get_queryset(self):
-        if self.request.user.profile.adhesion:
-            return Service.objects.filter(adherent=self.request.user.profile.adhesion).order_by('-created')
-        else:
-            return Service.objects.none()
+class ServiceMixin(PermissionRequiredMixin):
+    model = Service
+    permission_required = 'services.change_service'
+
+
+class ServiceList(ServiceMixin, ListView):
+    pass
 
 
-# L’utilisateur peut accéder aux détails de ces services et de ceux de ces asso
 class ServiceDetail(LoginRequiredMixin, DetailView):
     def get_queryset(self):
-        return Service.objects.filter(adherent__pk__in=self.request.user.profile.adhesions.values_list('pk')) \
-                    .order_by('-created')
+        if self.request.user.is_superuser:
+            return Service.objects.all()
+        else:
+            return Service.objects.filter(adhesion__pk__in=self.request.user.profile.adhesions.values_list('pk')) \
+                        .order_by('-created')
+
+
+class ServiceUpdate(ServiceMixin, UpdateView):
+    fields = ['label', 'notes', 'active']