Browse Source

début UI

Élie Bouttier 8 years ago
parent
commit
23c3d46583

+ 13 - 2
accounts/admin.py

@@ -4,15 +4,26 @@ from django.contrib.auth.admin import UserAdmin as AuthUserAdmin
 from django.core.exceptions import PermissionDenied
 
 
+from .models import Profile
 from .forms import UserCreationForm
-from adhesions.admin import ProfileInline, AdherentInline
+from adhesions.admin import AdherentInline
+
+
+class ProfileInline(admin.StackedInline):
+    model = Profile
+
+    def has_add_permission(self, request):
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        return False
 
 
 class UserAdmin(AuthUserAdmin):
     list_display = AuthUserAdmin.list_display + ('adherent_id',)
 
     def adherent_id(self, user):
-        adherent = user.profile.adherent
+        adherent = user.profile.adhesion
         if adherent:
             return adherent.id
     adherent_id.short_description = 'Numéro d’adhérent'

+ 20 - 0
accounts/forms.py

@@ -1,8 +1,28 @@
 from django.forms import ModelForm
 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 UserForm(ModelForm):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        for key in self.Meta.readonly_fields:
+            self.fields[key].disabled = True
+
+    class Meta:
+        model = User
+        fields = ('username', 'first_name', 'last_name', 'email',)
+        readonly_fields = ('username', 'first_name', 'last_name',)
+
+
+class ProfileForm(ModelForm):
+    class Meta:
+        model = Profile
+        fields = ('phone_number', 'address',)

+ 39 - 0
accounts/migrations/0001_initial.py

@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-05 23:41
+from __future__ import unicode_literals
+
+import adhesions.models
+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),
+        ('adhesions', '0005_auto_20170206_0039'),
+    ]
+
+    state_operations = [
+        migrations.CreateModel(
+            name='Profile',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('phone_number', models.CharField(blank=True, default='', max_length=16, verbose_name='Numéro de téléphone')),
+                ('address', models.TextField(blank=True, default='', verbose_name='Adresse')),
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='Utilisateur')),
+            ],
+            options={
+                'verbose_name': 'profil',
+                'db_table': 'accounts_profile',
+            },
+            bases=(models.Model,),
+        ),
+    ]
+
+    operations = [
+        migrations.SeparateDatabaseAndState(state_operations=state_operations)
+    ]

+ 0 - 0
accounts/migrations/__init__.py


+ 36 - 0
accounts/models.py

@@ -0,0 +1,36 @@
+from django.db import models
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+
+
+from adhesions.models import Adherent, Corporation
+
+
+class Profile(models.Model):
+    user = models.OneToOneField(User, related_name='profile', verbose_name='Utilisateur')
+    phone_number = models.CharField(max_length=16, blank=True, default='',
+                                    verbose_name='Numéro de téléphone')
+    address = models.TextField(blank=True, default='', verbose_name='Adresse')
+
+    class Meta:
+        verbose_name = 'profil'
+
+    @property
+    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 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(
+            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'))
+        )
+
+    def __str__(self):
+        return self.user.get_full_name() or self.user.username

+ 9 - 1
accounts/signals.py

@@ -1,9 +1,17 @@
 from django.dispatch import receiver
-from django.db.models.signals import pre_save
+from django.db.models.signals import pre_save, post_save
 from django.contrib.auth.models import User
 
+from .models import Profile
+
 
 @receiver(pre_save, sender=User, dispatch_uid='set_unusable_password')
 def set_unusable_password(sender, instance, **kwargs):
     if not instance.password:
         instance.set_unusable_password()
+
+
+@receiver(post_save, sender=User, dispatch_uid='create_profile')
+def create_profile(sender, instance, created, **kwargs):
+    if created:
+        Profile.objects.create(user=instance)

+ 4 - 0
accounts/static/css/registration.css

@@ -0,0 +1,4 @@
+body {
+  padding-top: 40px;
+  padding-bottom: 40px;
+}

+ 20 - 0
accounts/templates/accounts/profile.html

@@ -0,0 +1,20 @@
+{% extends 'base.html' %}
+
+{% load bootstrap3 %}
+
+{% block profiletab %} class="active"{% endblock %}
+
+{% block content %}
+    <div class="row"><div class="col-md-offset-2 col-md-8">
+        {% bootstrap_form_errors user_form layout="horizontal" %}
+        {% bootstrap_form_errors profile_form layout="horizontal" %}
+        <form method="post" class="form-horizontal">
+            {% csrf_token %}
+            {% bootstrap_form user_form %}
+            {% bootstrap_form profile_form %}
+            {% buttons %}
+                <button type="submit" class="btn btn-success">Enregistrer</button>
+            {% endbuttons %}
+        </form>
+    </div></div>
+{% endblock %}

+ 23 - 0
accounts/templates/registration/base.html

@@ -0,0 +1,23 @@
+{% extends '_base.html' %}
+
+{% load staticfiles %}
+
+{% block css %}
+{{ block.super }}
+<link href="{% static '/css/registration.css' %}" rel="stylesheet">
+{% endblock %}
+
+{% block body %}
+
+<div class="container">
+    <div class="jumbotron">
+        <h1>Espace adhérent</h1>
+        <h2>Association tetaneutral.net</h2>
+    </div>
+
+    <div class="col-md-6 col-md-offset-3">
+{% block content %}{% endblock %}
+    </div>
+</div>
+
+{% endblock %}

+ 17 - 0
accounts/templates/registration/login.html

@@ -0,0 +1,17 @@
+{% extends 'registration/base.html' %}
+
+{% load bootstrap3 %}
+
+{% block content %}
+        <p>Vous pouvez vous connecter avec votre <b>nom d’utilisateur</b>, votre <b>adresse e-mail</b> ou votre <b>numéro d’adhérent</b>.</p>
+        {% bootstrap_form_errors form layout="horizontal" %}
+        <form method="post" class="form-horizontal">
+            {% csrf_token %}
+            {% bootstrap_field form.username layout="horizontal" placeholder="Nom d’utilisateur, e-mail ou numéro d’adhérent" %}
+            {% bootstrap_field form.password layout="horizontal" %}
+            {% buttons layout="horizontal" %}
+                <button type="submit" class="btn btn-primary">Login</button>
+                <a class="btn btn-default" href="{% url 'password_reset' %}">Mot de passe oublié ?</a>
+            {% endbuttons %}
+        </form>
+{% endblock %}

+ 7 - 0
accounts/templates/registration/password_reset_complete.html

@@ -0,0 +1,7 @@
+{% extends 'registration/base.html' %}
+
+{% load bootstrap3 %}
+
+{% block content %}
+        <p>Votre mot de passe a été réinitialisé, vous pouvez à présent vous <a href="{% url 'login' %}">connecter</a>.<p/>
+{% endblock %}

+ 22 - 0
accounts/templates/registration/password_reset_confirm.html

@@ -0,0 +1,22 @@
+{% extends 'registration/base.html' %}
+
+{% load bootstrap3 %}
+
+{% block content %}
+{% if validlink %}
+        <form method="post" class="form-horizontal">
+            {% csrf_token %}
+            {% bootstrap_form form layout="horizontal" %}
+            {% buttons layout="horizontal" %}
+                <button type="submit" class="btn btn-primary">Modifier le mot de passe</button>
+                <a class="btn btn-default" href="{% url 'login' %}">Annuler</a>
+            {% endbuttons %}
+        </form>
+{% else %}
+        <p>Ce lien de réinitialisation de mot de passe est invalide ou expiré.</p>
+        <p>
+            <a class="btn btn-primary" href="{% url 'password_reset' %}">Formulaire de réinitialisation du mot de passe</a>
+            <a class="btn btn-default" href="{% url 'login' %}">Login</a>
+        </p>
+{% endif %}
+{% endblock %}

+ 9 - 0
accounts/templates/registration/password_reset_done.html

@@ -0,0 +1,9 @@
+{% extends 'registration/base.html' %}
+
+{% block content %}
+        <p>
+            Un e-mail contenant un lien vous permettant de réinitialiser votre mot de passe vous a été envoyé.
+            Pensez à consulter votre dossier « spam ».
+        </p>
+        <a class="btn btn-primary" href="{% url 'login' %}">Se connecter</a>
+{% endblock %}

+ 15 - 0
accounts/templates/registration/password_reset_form.html

@@ -0,0 +1,15 @@
+{% extends 'registration/base.html' %}
+
+{% load bootstrap3 %}
+
+{% block content %}
+        <p>Pour réinitialiser votre mot de passe, veuillez saisir votre adresse e-email.</p>
+        <form method="post" class="form-horizontal">
+            {% csrf_token %}
+            {% bootstrap_form form layout="horizontal" %}
+            {% buttons layout="horizontal" %}
+                <button type="submit" class="btn btn-primary">Réinitialiser le mot de passe</button>
+                <a class="btn btn-default" href="{% url 'login' %}">Se connecter</a>
+            {% endbuttons %}
+        </form>
+{% endblock %}

+ 44 - 0
accounts/tests.py

@@ -0,0 +1,44 @@
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+
+from .models import Profile
+from .forms import UserForm, ProfileForm
+
+
+class AccountsTests(TestCase):
+    def setUp(self):
+        user = User.objects.create_user('user', 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_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'))
+
+    def test_profile(self):
+        response = self.client.get(reverse('profile'))
+        self.assertRedirects(response, reverse('login') + '?next=' + reverse('profile'))
+        self.client.login(username='user', password='user')
+        response = self.client.get(reverse('profile'))
+        self.assertEqual(response.status_code, 200)
+        user = User.objects.get(username='user')
+        user_form = UserForm(None, instance=user)
+        data = {key: getattr(user_form.instance, key) for key in user_form.fields}
+        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['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.email, 'user@example.org')
+        self.assertEquals(user.profile.address, '221B Baker Street')

+ 9 - 0
accounts/urls.py

@@ -0,0 +1,9 @@
+from django.conf.urls import url, include
+
+from . import views
+
+
+urlpatterns = [
+    url(r'^', include('django.contrib.auth.urls')),
+    url(r'^profile/$', views.profile, name='profile'),
+]

+ 21 - 0
accounts/views.py

@@ -0,0 +1,21 @@
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import get_object_or_404, redirect, render
+from django.contrib import messages
+
+from .forms import UserForm, ProfileForm
+
+
+@login_required
+def profile(request):
+    user_form = UserForm(request.POST or None, instance=request.user)
+    profile_form = ProfileForm(request.POST or None, instance=request.user.profile)
+    forms = [user_form, profile_form]
+    if request.method == 'POST' and all(form.is_valid() for form in forms):
+        for form in forms:
+            form.save()
+        messages.success(request, 'Profil mis à jour avec succès !')
+        return redirect('profile')
+    return render(request, 'accounts/profile.html', {
+        'user_form': user_form,
+        'profile_form': profile_form,
+    })

+ 1 - 11
adhesions/admin.py

@@ -2,20 +2,10 @@ from django.contrib import admin
 from django.contrib.contenttypes.admin import GenericStackedInline
 
 from .forms import AdherentForm
-from .models import Profile, Corporation, Adherent
+from .models import Corporation, Adherent
 from banking.admin import PaymentInline, ValidatedPaymentInline, PendingOrNewPaymentInline
 
 
-class ProfileInline(admin.StackedInline):
-    model = Profile
-
-    def has_add_permission(self, request):
-        return False
-
-    def has_delete_permission(self, request, obj=None):
-        return False
-
-
 class AdherentInline(GenericStackedInline):
     model = Adherent
     ct_field = 'adherent_type'

+ 0 - 3
adhesions/apps.py

@@ -4,6 +4,3 @@ from django.apps import AppConfig
 class AdhesionsConfig(AppConfig):
     name = 'adhesions'
     verbose_name = 'Adhésions'
-
-    def ready(self):
-        import adhesions.signals  # noqa

+ 27 - 0
adhesions/migrations/0005_auto_20170206_0039.py

@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-05 23:39
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('adhesions', '0004_auto_20170111_0040'),
+    ]
+
+    database_operations = [
+        migrations.AlterModelTable('Profile', 'accounts_profile')
+    ]
+
+    state_operations = [
+        migrations.DeleteModel('Profile')
+    ]
+
+    operations = [
+        migrations.SeparateDatabaseAndState(
+            database_operations=database_operations,
+            state_operations=state_operations,
+        )
+    ]

+ 7 - 41
adhesions/models.py

@@ -44,57 +44,23 @@ class Adherent(models.Model):
             return 'Adhérent #?'
 
 
-class AdhesionMixin:
-    @property
-    def adhesion(self):
-        try:
-            return self.adhesions.get()
-        except Adherent.DoesNotExist:
-            return None
-
-
-class Profile(AdhesionMixin, models.Model):
-    user = models.OneToOneField(User, related_name='profile', verbose_name='Utilisateur')
-    phone_number = models.CharField(max_length=16, blank=True, default='',
-                                    verbose_name='Numéro de téléphone')
-    address = models.TextField(blank=True, default='', verbose_name='Adresse')
-
-    class Meta:
-        verbose_name = 'profil'
-
-    @property
-    def adherent(self):
-        ctype = ContentType.objects.get_for_model(User)
-        try:
-            adherent = Adherent.objects.get(adherent_type=ctype, adherent_id=self.user.pk)
-        except Adherent.DoesNotExist:
-            return None
-        else:
-            return adherent
-
-    def __str__(self):
-        return self.user.get_full_name() or self.user.username
-
-
-class Corporation(AdhesionMixin, models.Model):
+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')
 
-    class Meta:
-        verbose_name = 'personne morale'
-        verbose_name_plural = 'personnes morales'
-
     @property
-    def adherent(self):
+    def adhesion(self):
         ctype = ContentType.objects.get_for_model(self)
         try:
-            adherent = Adherent.objects.get(adherent_type=ctype, adherent_id=self.pk)
+            return Adherent.objects.get(adherent_type=ctype, adherent_id=self.pk)
         except Adherent.DoesNotExist:
             return None
-        else:
-            return adherent
+
+    class Meta:
+        verbose_name = 'personne morale'
+        verbose_name_plural = 'personnes morales'
 
     def __str__(self):
         return self.social_reason

+ 0 - 11
adhesions/signals.py

@@ -1,11 +0,0 @@
-from django.dispatch import receiver
-from django.db.models.signals import post_save
-from django.contrib.auth.models import User
-
-from .models import Profile
-
-
-@receiver(post_save, sender=User, dispatch_uid='create_profile')
-def create_profile(sender, instance, created, **kwargs):
-    if created:
-        Profile.objects.create(user=instance)

+ 31 - 1
djadhere/settings.py

@@ -31,13 +31,16 @@ ALLOWED_HOSTS = []
 # Application definition
 
 INSTALLED_APPS = [
+    'accounts',
     'adhesions',
     'services',
     'banking',
     'djadhere',
+
+    'bootstrap3',
+
     'django.contrib.admin',
     'django.contrib.auth',
-    'accounts',  # must be after django.contrib.auth
     'django.contrib.contenttypes',
     'django.contrib.sessions',
     'django.contrib.messages',
@@ -123,3 +126,30 @@ USE_TZ = True
 # https://docs.djangoproject.com/en/1.10/howto/static-files/
 
 STATIC_URL = '/static/'
+
+LOGOUT_REDIRECT_URL = 'home'
+
+BOOTSTRAP3 = {
+
+    # The URL to the jQuery JavaScript file
+    # If not set, "build-in" CDN is used (maxcdn)
+    # 'jquery_url': '//code.jquery.com/jquery.min.js',
+    # 'jquery_url': STATIC_URL + 'jquery/dist/jquery.js',
+
+    # The Bootstrap base URL
+    # If not set, "build-in" CDN is used (maxcdn)
+    # 'base_url': '//netdna.bootstrapcdn.com/bootstrap/3.2.0/',
+    # 'base_url': STATIC_URL + 'bootstrap/dist/',
+
+    # The complete URL to the Bootstrap CSS file
+    # (None means derive it from base_url)
+    'css_url': None,
+
+    # The complete URL to the Bootstrap CSS file
+    # (None means no theme)
+    'theme_url': None,
+
+    # The complete URL to the Bootstrap JavaScript file
+    # (None means derive it from base_url)
+    'javascript_url': None,
+}

+ 0 - 0
djadhere/static/css/djadhere.css


+ 0 - 0
djadhere/static/js/djadhere.js


+ 32 - 0
djadhere/templates/_base.html

@@ -0,0 +1,32 @@
+{% load staticfiles bootstrap3 %}
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    {% comment %}<link rel="icon" href="{% static 'favicon.ico' %}">{% endcomment %}
+
+    <title>{% block title %}tetaneutral.net{% endblock %}</title>
+
+    {% bootstrap_css %}
+    {% block css %}{% endblock %}
+
+    {% block js %}{% endblock %}
+
+    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
+    <!--[if lt IE 9]>
+      <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
+      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
+    <![endif]-->
+  </head>
+  <body>
+
+{% block body %}{% endblock %}
+
+    <script src="{% bootstrap_jquery_url %}"></script>
+    {% bootstrap_javascript %}
+    {% block js_end %}{% endblock %}
+  </body>
+</html>

+ 18 - 0
djadhere/templates/_form.html

@@ -0,0 +1,18 @@
+{% load bootstrap3 i18n %}
+<form action="" method="post" class="form-horizontal"{% if multipart %} enctype=multipart/form-data{% endif %}>
+  {% csrf_token %}
+  {% block beforeform %}{% endblock %}
+  {% if form %}
+  {% bootstrap_form form layout="horizontal" %}
+  {% endif %}
+  {% for form in forms %}
+  {% bootstrap_form form layout="horizontal" %}
+  {% endfor %}
+  {% buttons layout="horizontal" %}
+  <button type="submit" class="btn btn-primary">{% trans "Submit" %}</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>
+  {% endbuttons %}
+</form>

+ 62 - 0
djadhere/templates/base.html

@@ -0,0 +1,62 @@
+{% extends '_base.html' %}
+{% load bootstrap3 staticfiles %} 
+
+{% block css %}
+{{ block.super }}
+<link href="{% static 'css/djadhere.css' %}" rel="stylesheet">
+{% endblock %}
+
+{% block js_end %}
+{{ block.super }}
+<script src="{% static 'js/djadhere.js' %}"></script>
+{% endblock %}
+
+{% block body %}
+
+{% block navbar %}
+    <nav class="navbar navbar-default" role="navigation">
+      <div class="container">
+        <div class="navbar-header">
+          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
+            <span class="sr-only">Toggle navigation</span>
+            <span class="icon-bar"></span>
+            <span class="icon-bar"></span>
+            <span class="icon-bar"></span>
+          </button>
+          <a class="navbar-brand" href="{% url 'home' %}">tetaneutral.net</a>
+        </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 servicestab %}{% endblock %}><a href="{% url 'service-list' %}"><span class="glyphicon glyphicon-list"></span>&nbsp;Services</a></li>
+          </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 %}
+            <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>
+        </div>
+      </div>
+    </nav>
+{% endblock %}
+
+{% block container %}
+	<div class="container">
+      <div class="row">
+        <div id="content" class="col-md-12">
+        {% bootstrap_messages %}
+{% block content %}{% endblock %}
+{% block pagefooter %}
+        <hr>
+        <footer>
+          <p class="text-muted">Propuslé par <a href="https://code.ffdn.org/tetaneutral.net/djadhere">djadhere</a></p>
+        </footer>
+{% endblock %}
+        </div>
+      </div>
+	</div>
+{% endblock %}
+
+{% endblock %}

+ 5 - 0
djadhere/templates/home.html

@@ -0,0 +1,5 @@
+{% extends 'base.html' %}
+
+{% block content %}
+<h1>Bienvenue <b>{{ name }}</b> !</h1>
+{% endblock %}

+ 15 - 0
djadhere/tests.py

@@ -0,0 +1,15 @@
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+
+
+class AccountsTests(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)

+ 6 - 3
djadhere/urls.py

@@ -13,13 +13,16 @@ 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
-from django.views.generic.base import RedirectView
+
+from djadhere import views
 
 
 urlpatterns = [
-    url(r'^$', RedirectView.as_view(url='/admin/')),
+    url(r'^$', views.home, name='home'),
+    url(r'^accounts/', include('accounts.urls')),
+    url(r'^services/', include('services.urls')),
     url(r'^admin/', admin.site.urls),
 ]
 

+ 9 - 0
djadhere/views.py

@@ -0,0 +1,9 @@
+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,
+    })

+ 1 - 0
requirements.txt

@@ -1 +1,2 @@
 django<1.11
+django-bootstrap3

+ 38 - 0
services/templates/services/service_list.html

@@ -0,0 +1,38 @@
+{% extends 'base.html' %}
+
+{% load bootstrap3 %}
+
+{% block servicestab %} class="active"{% endblock %}
+
+{% block content %}
+    {% if object_list %}
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Identifiant</th>
+                <th>Type</th>
+                <th>Début</th>
+                <th>Fin</th>
+                <th>Actif</th>
+            </tr>
+        </thead>
+        {% for service in object_list %}
+        {% if forloop.first %}
+        <tbody>
+        {% endif %}
+            <tr class="{% if service.is_ongoing %}success{% else %}{% endif %}">
+                <td>{{ service.pk }}</td>
+                <td>{{ service.service_type }}</td>
+                <td>{{ service.start|default:"–" }}</td>
+                <td>{{ service.end|default:"–" }}</td>
+                <td>{{ service.is_ongoing|yesno:"✔,✘" }}</td>
+            </tr>
+        {% if forloop.last %}
+        </tbody>
+        {% endif %}
+        {% endfor %}
+    </table>
+    {% else %}
+    <p>Vous n’avez aucun services.</p>
+    {% endif %}
+{% endblock %}

+ 9 - 0
services/urls.py

@@ -0,0 +1,9 @@
+from django.conf.urls import url
+
+
+from . import views
+
+
+urlpatterns = [
+    url(r'^list/$', views.ServiceList.as_view(), name='service-list'),
+]

+ 9 - 1
services/views.py

@@ -1,3 +1,11 @@
 from django.shortcuts import render
+from django.views.generic import ListView
+from django.contrib.auth.mixins import LoginRequiredMixin
 
-# Create your views here.
+from .models import Service
+
+
+class ServiceList(LoginRequiredMixin, ListView):
+    def get_queryset(self):
+        return Service.objects.filter(adherent=self.request.user.profile.adherents) \
+                    .order_by('-start')