Browse Source

Merge branch 'master' of git.illyse.org:coin

CapsLock 10 years ago
parent
commit
fda501fb90

+ 20 - 0
coin/configuration/migrations/0003_configuration_comment.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('configuration', '0002_auto_20141002_0204'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='configuration',
+            name='comment',
+            field=models.CharField(default='', max_length=512, verbose_name='commentaire', blank=True),
+            preserve_default=False,
+        ),
+    ]

+ 2 - 0
coin/configuration/models.py

@@ -26,6 +26,8 @@ class Configuration(PolymorphicModel):
     offersubscription = models.OneToOneField(OfferSubscription,
                                              related_name='configuration',
                                              verbose_name='abonnement')
+    comment = models.CharField(blank=True, max_length=512,
+                               verbose_name="commentaire")
 
     @staticmethod
     def get_configurations_choices_list():

+ 7 - 4
coin/members/admin.py

@@ -32,11 +32,12 @@ class MembershipFeeInline(admin.TabularInline):
 
 class MemberAdmin(UserAdmin):
     list_display = ('id', 'status', 'username', 'first_name', 'last_name',
-                    'organization_name', 'email', 'end_date_of_membership')
+                    'nickname', 'organization_name', 'email',
+                    'end_date_of_membership')
     list_display_links = ('id', 'username', 'first_name', 'last_name')
     list_filter = ('status', MembershipFeeFilter)
     search_fields = ['username', 'first_name', 'last_name', 'email']
-    ordering = ('last_name',)
+    ordering = ('status', 'username')
     actions = [delete_selected, 'set_as_member', 'set_as_non_member',
                'bulk_send_welcome_email']
 
@@ -47,7 +48,8 @@ class MemberAdmin(UserAdmin):
         ('Adhérent', {'fields': (
             ('status', 'resign_date'),
             'type',
-            ('first_name', 'last_name', 'organization_name'))}),
+            ('first_name', 'last_name', 'nickname'),
+            'organization_name')}),
         ('Coordonnées', {'fields': (
             'email',
             ('home_phone_number', 'mobile_phone_number'),
@@ -63,7 +65,8 @@ class MemberAdmin(UserAdmin):
         ('Adhérent', {'fields': (
             'status',
             'type',
-            ('first_name', 'last_name', 'organization_name'))}),
+            ('first_name', 'last_name', 'nickname'),
+            'organization_name')}),
         ('Coordonnées', {'fields': (
             'email',
             ('home_phone_number', 'mobile_phone_number'),

+ 4 - 2
coin/members/forms.py

@@ -16,9 +16,11 @@ class MemberCreationForm(forms.ModelForm):
     username = forms.RegexField(required=False,
                                 label="Nom d'utilisateur", max_length=30, regex=r"^[\w.@+-]+$",
                                 help_text="Laisser vide pour le générer automatiquement à partir du "
-                                "nom et du prénom")
+                                "nom d'usage, nom et prénom, ou nom de l'organisme")
     password = forms.CharField(
-        required=False, label='Mot de passe', widget=forms.PasswordInput)
+        required=False, label='Mot de passe', widget=forms.PasswordInput,
+        help_text="Laisser vide et envoyer un mail de bienvenue pour que "
+        "l'utilisateur choisisse son mot de passe lui-même")
 
     class Meta:
         model = Member

+ 20 - 0
coin/members/migrations/0008_member_nickname.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('members', '0007_auto_20141008_1107'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='member',
+            name='nickname',
+            field=models.CharField(default='', help_text='Pseudonyme, \u2026', max_length=64, verbose_name="nom d'usage", blank=True),
+            preserve_default=False,
+        ),
+    ]

+ 24 - 0
coin/members/migrations/0009_auto_20141008_2244.py

@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('members', '0008_member_nickname'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='member',
+            name='first_name',
+            field=models.CharField(max_length=30, verbose_name='first name', blank=True),
+        ),
+        migrations.AlterField(
+            model_name='member',
+            name='last_name',
+            field=models.CharField(max_length=30, verbose_name='last name', blank=True),
+        ),
+    ]

+ 19 - 0
coin/members/migrations/0010_auto_20141008_2246.py

@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('members', '0009_auto_20141008_2244'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='member',
+            name='status',
+            field=models.CharField(default='member', max_length=50, verbose_name='statut', choices=[('member', 'Adh\xe9rent'), ('not_member', 'Non adh\xe9rent'), ('pending', "Demande d'adh\xe9sion")]),
+        ),
+    ]

+ 51 - 26
coin/members/models.py

@@ -3,7 +3,6 @@ from __future__ import unicode_literals
 
 import ldapdb.models
 import unicodedata
-import string
 import datetime
 from django.db import models
 from django.db.models import Q
@@ -12,6 +11,7 @@ from django.dispatch import receiver
 from django.contrib.auth.models import AbstractUser
 from django.conf import settings
 from django.core.validators import RegexValidator
+from django.core.exceptions import ValidationError
 from ldapdb.models.fields import CharField, IntegerField, ListField
 
 from coin.offers.models import OfferSubscription
@@ -35,10 +35,13 @@ class Member(CoinLdapSyncMixin, AbstractUser):
     )
 
     status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
-                              default='pending', verbose_name='statut')
+                              default='member', verbose_name='statut')
     type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
                             default='natural_person', verbose_name='type')
 
+    nickname = models.CharField(max_length=64, blank=True,
+                                verbose_name="nom d'usage",
+                                help_text='Pseudonyme, …')
     organization_name = models.CharField(max_length=200, blank=True,
                                          verbose_name="nom de l'organisme",
                                          help_text='Pour une personne morale')
@@ -75,14 +78,26 @@ class Member(CoinLdapSyncMixin, AbstractUser):
     # passwords for both ldap and local db
     _password_ldap = None
 
+    def clean(self):
+        if self.type == 'legal_entity':
+            if not self.organization_name:
+                raise ValidationError("Le nom de l'organisme est obligatoire "
+                                      "pour une personne morale")
+        elif self.type == 'natural_person':
+            if not (self.first_name and self.last_name):
+                raise ValidationError("Le nom et prénom sont obligatoires "
+                                      "pour une personne physique")
+
     def __unicode__(self):
-        name = self.first_name + ' ' + self.last_name
-        if self.organization_name:
-            name += ' (%s)' % self.organization_name
-        return name
+        if self.type == 'legal_entity':
+            return self.organization_name
+        elif self.nickname:
+            return self.nickname
+        else:
+            return self.first_name + ' ' + self.last_name
 
     def get_full_name(self):
-        return '%s %s' % (self.first_name, self.last_name)
+        return str(self)
 
     def get_short_name(self):
         return self.username
@@ -172,8 +187,12 @@ class Member(CoinLdapSyncMixin, AbstractUser):
             ldap_user.nick_name = self.username
             ldap_user.uidNumber = uid_number
 
-        ldap_user.last_name = self.last_name
-        ldap_user.first_name = self.first_name
+        if self.type == 'natural_person':
+            ldap_user.last_name = self.last_name
+            ldap_user.first_name = self.first_name
+        elif self.type == 'legal_entity':
+            ldap_user.last_name = self.organization_name
+            ldap_user.first_name = ""
 
         # If a password is definied in _password_ldap, change it in LDAP
         if self._password_ldap:
@@ -216,37 +235,44 @@ class Member(CoinLdapSyncMixin, AbstractUser):
     class Meta:
         verbose_name = 'membre'
 
-# Hack to force email, first_name ans last_name to be required by Member model
+# Hack to force email to be required by Member model
 Member._meta.get_field('email')._unique = True
 Member._meta.get_field('email').blank = False
 Member._meta.get_field('email').null = False
-Member._meta.get_field('first_name').blank = False
-Member._meta.get_field('first_name').null = False
-Member._meta.get_field('last_name').blank = False
-Member._meta.get_field('last_name').null = False
 
 
 def count_active_members():
     return Member.objects.filter(status='member').count()
 
 
-def get_automatic_username(first_name, last_name):
+def get_automatic_username(member):
     """
     Calcul le username automatiquement en fonction
     du nom et du prénom
     """
 
-    # Première lettre de chaque partie du prénom
-    first_name_letters = ''.join(
-        [c[0] for c in first_name.split('-')]
-    )
-    # Concaténer avec nom de famille
-    username = ('%s%s' % (first_name_letters, last_name))
+    # S'il s'agit d'une entreprise, utilise son nom:
+    if member.type == 'legal_entity' and member.organization_name:
+        username = member.organization_name
+    # Sinon, si un pseudo est définit, l'utilise
+    elif member.nickname:
+        username = member.nickname
+    # Sinon, utilise nom et prenom
+    elif member.first_name and member.last_name:
+        # Première lettre de chaque partie du prénom
+        first_name_letters = ''.join(
+            [c[0] for c in member.first_name.split('-')]
+        )
+        # Concaténer avec nom de famille
+        username = ('%s%s' % (first_name_letters, member.last_name))
+    else:
+        raise Exception('Il n\'y a pas sufissement d\'informations pour déterminer un login automatiquement')
+
     # Remplacer ou enlever les caractères non ascii
     username = unicodedata.normalize('NFD', username)\
         .encode('ascii', 'ignore')
-    # Enlever ponctuation et espace
-    punctuation = (string.punctuation + ' ').encode('ascii')
+    # Enlever ponctuation (sauf _-.) et espace
+    punctuation = ('!"#$%&\'()*+,/:;<=>?@[\\]^`{|}~ ').encode('ascii')
     username = username.translate(None, punctuation)
     # En minuscule
     username = username.lower()
@@ -257,7 +283,7 @@ def get_automatic_username(first_name, last_name):
     member = Member.objects.filter(username=username)
     base_username = username
     incr = 2
-    # Tant qu'un membre est trouvé, incrément un entier à la fin
+    # Tant qu'un membre est trouvé, incrémente un entier à la fin
     while member:
         if len(base_username) >= 30:
             username = base_username[30 - len(str(incr)):]
@@ -379,8 +405,7 @@ def define_username(sender, instance, **kwargs):
     le calcul automatiquement en fonction du nom et du prénom
     """
     if not instance.username and not instance.pk:
-        instance.username = get_automatic_username(instance.first_name,
-                                                   instance.last_name)
+        instance.username = get_automatic_username(instance)
 
 
 @receiver(pre_save, sender=LdapUser)

+ 10 - 1
coin/members/templates/members/detail.html

@@ -12,8 +12,17 @@
             <h3>Me joindre</h3>
             <table class="full-width">
                 <tr>
+                  {% if user.type == 'natural_person' %}
                     <td class="center"><span class="label">Prénom - Nom</span></td>
-                    <td>{{user.first_name}} {{user.last_name}}</td>
+                    <td>{{user.first_name}} {{user.last_name}}
+                      {% if user.nickname %}
+                      ({{ user.nickname }})
+                      {% endif %}
+                    </td>
+                  {% else %}
+                    <td class="center"><span class="label">Nom de la structure</span></td>
+                    <td>{{ user.organization_name }}</td>
+                  {% endif %}
                 </tr>
                 <tr>
                     <td class="center"><span class="label">Adresse</span></td>

+ 33 - 0
coin/members/templates/members/index.html

@@ -16,6 +16,39 @@
         <h3>Stats</h3>
         <div class="panel">Use MOAR bandwidth !</div>
     </div>
+    <div class="large-6 columns">
+        <h3>News d'Illyse</h3>
+        <div class="panel" id="feed_isp"><i class="fa fa-spinner fa-spin"></i>
+ Chargement en cours</div>
+    </div>
+    <div class="large-6 columns">
+        <h3>News de FFDN</h3>
+        <div class="panel" id="feed_ffdn"><i class="fa fa-spinner fa-spin"></i>
+ Chargement en cours</div>
+    </div>
 </div>
 
 {% endblock %}
+
+
+{% block js %}
+    {{ block.super }}
+    <script>
+    $(function() {
+        $.ajax({
+            url: "{% url 'feed' feed_name='isp' %}",
+        }).done(function(html) {
+            $('#feed_isp').html(html);
+        }).fail(function() {
+            $('#feed_ffdn').html('Erreur lors du chargement du flux');
+        });
+        $.ajax({
+            url: "{% url 'feed' feed_name='ffdn' %}",
+        }).done(function(html) {
+            $('#feed_ffdn').html(html);
+        }).fail(function() {
+            $('#feed_ffdn').html('Erreur lors du chargement du flux');
+        });
+    });
+    </script>
+{% endblock js %}

+ 0 - 4
coin/members/templates/members/registration/password_reset_form.html

@@ -2,10 +2,6 @@
 {% load staticfiles %}
 {% load i18n %}
 
-{% block js %}
-<script src="{% static "js/password_reset.js" %}"></script>
-{% endblock %}
-
 {% block content %}
 
 <div class="row">

+ 34 - 1
coin/members/tests.py

@@ -202,6 +202,28 @@ class MemberTests(TestCase):
         member2.delete()
         member3.delete()
 
+    def test_when_creating_legal_entity_organization_name_is_used_for_username(self):
+        """
+        Lors de la créatio d'une entreprise, son nom doit être utilisée lors de
+        la détermination automatique du username
+        """
+        random = os.urandom(4).encode('hex')
+        member = Member(type='legal_entity', organization_name='ILLYSE' + random, email='illyse@coin.org')
+        member.save()
+        self.assertEqual(member.username, 'illyse' + random)
+        member.delete()
+
+    def test_when_creating_member_with_nickname_it_is_used_for_username(self):
+        """
+        Lors de la création d'une personne, qui a un pseudo, celui-ci est utilisé en priorité
+        """
+        random = os.urandom(4).encode('hex')
+        member = Member(first_name='Richard', last_name='Stallman', nickname='rms' + random, email='illyse@coin.org')
+        member.save()
+        self.assertEqual(member.username, 'rms' + random)
+
+        member.delete()
+
     def test_when_saving_member_and_ldap_fail_dont_save(self):
         """
         Test que lors de la sauvegarde d'un membre et que la sauvegarde en LDAP
@@ -291,7 +313,7 @@ class MemberTests(TestCase):
 
     def test_member_is_paid_up(self):
         """
-        Test que end_date_of_membership d'un membre envoi bien la date de fin d'adhésion
+        Test l'état "a jour de cotisation" d'un adhérent.
         """
         # Créé un membre
         first_name = 'Capitain'
@@ -327,6 +349,17 @@ class MemberTests(TestCase):
         # de cotisation
         self.assertEqual(member.is_paid_up(), True)
 
+    def test_member_cant_be_created_without_names(self):
+        """
+        Test qu'un membre ne peut pas être créé sans "noms"
+        (prenom, nom) ou pseudo ou nom d'organization
+        """
+        member = Member(username='blop')
+        self.assertRaises(Exception, member.save)
+
+        member = Member()
+        self.assertRaises(Exception, member.save)
+
 
 class MemberAdminTests(TestCase):
 

+ 1 - 1
coin/members/urls.py

@@ -50,5 +50,5 @@ urlpatterns = patterns(
     # url(r'^subscription/(?P<id>\d+)', views.subscriptions, name = 'subscription'),
 
     url(r'^invoices/', views.invoices, name='invoices'),
-    url(r'^contact/', views.contact, name='contact')
+    url(r'^contact/', views.contact, name='contact'),
 )

+ 0 - 1
coin/members/views.py

@@ -23,7 +23,6 @@ def detail(request):
 def subscriptions(request):
     subscriptions = request.user.get_active_subscriptions()
     old_subscriptions = request.user.get_inactive_subscriptions()
-    print(old_subscriptions)
 
     return render_to_response('members/subscriptions.html',
                               {'subscriptions': subscriptions,

+ 5 - 0
coin/settings.py

@@ -250,6 +250,11 @@ MEMBER_DEFAULT_COTISATION = 20
 # Reset session if cookie older than 2h.
 SESSION_COOKIE_AGE = 7200
 
+# RSS/Atom feeds to display on dashboard
+# feed name (used in template), url, max entries to display
+FEEDS = (('isp', 'http://www.illyse.net/feed/', 3),
+         ('ffdn', 'http://www.ffdn.org/fr/rss.xml', 3))
+
 # Surcharge les paramètres en utilisant le fichier settings_local.py
 try:
     from settings_local import *

+ 28 - 2
coin/static/css/illyse.css

@@ -201,7 +201,7 @@ span.italic {
     font-weight: bold;
 }
 
-h3.graphtitle select {
+#graph h3 select {
 	display: inline;
 	background-color: transparent;
 	border: 0 none transparent;
@@ -212,7 +212,7 @@ h3.graphtitle select {
 	margin: 0;
 	padding: 0;
 }
-h3.graphtitle select option {
+#graph h3 select option {
 	font-size: 0.6em;
 }
 
@@ -270,6 +270,18 @@ tr.inactive {
 	border: 2px solid #C0C0C0;
 }
 
+/* Feeds */
+.feed {
+    font-size:80%;
+}
+.feed .entry {
+    margin-bottom:1em;
+}
+.feed .entry .date {
+    color:gray;
+    font-size:80%;
+}
+
 .legend .button, .formcontrol .button {
 	padding: 0.25em 0.5em;
 	border-radius:5px;
@@ -288,4 +300,18 @@ tr.inactive {
 }
 .formcontrol {
 	text-align: right;
+	margin-right: 1em;
+}
+
+.message.success {
+	padding: 0.5em;
+	color: #FFFFFF;
+	text-align: center;
+	background-color: #20BA44;
+	/*background-color: #00A986;*/
+	border: 1px solid #E0E0E0;
+	margin: -1.5em 1em 1em 1em;
+}
+.message.success:before {
+    content: "✔ ";
 }

+ 0 - 50
coin/static/js/illyse.js

@@ -1,53 +1,3 @@
-// TODO : Move it to VPN template only. We havn't to load this code at each page loa
-// TODO : jQueryfy it
-window.onload = function() {
-	var field = document.getElementById("passgen");
-	if (field != undefined) field.onclick = function() {
-		if (!confirm("Ceci va effacer votre ancien mot de passe et en générer un nouveau. Continuer ?")) return false;
-		var cell = field.parentNode;
-		cell.removeChild(field);
-		cell.appendChild(document.createElement("img"));
-		cell.lastChild.src = "/static/img/coin.ajax.gif";
-		cell.appendChild(document.createTextNode(" Génération en cours…"));
-		var xhr = new XMLHttpRequest();
-		xhr.onreadystatechange = function() {
-			if (xhr.readyState != 4) return;
-			var table = cell.parentNode.parentNode;
-			if (xhr.responseXML == null)
-				var pass = (new DOMParser().parseFromString(xhr.responseText, "text/html")).getElementById("password");
-			else var pass = xhr.responseXML.getElementById("password");
-			table.insertBefore(pass.cloneNode(true), cell.parentNode);
-			do pass = pass.nextSibling; while (pass.nodeType == 3);
-			table.insertBefore(pass.cloneNode(true), cell.parentNode);
-			table.removeChild(cell.parentNode);
-		};
-		xhr.open("GET", field.href, false);
-		xhr.send(null);
-		return false;
-	};
-
-	var field = document.getElementById("trafic_zoom");
-	if (field != undefined) {
-		var select = document.createElement("select");
-		var options = {"hourly": "une heure", "daily": "24 heures", "weekly": "7 jours", "monthly": "un mois", "yearly": "un an"};
-		for (var i in options) {
-			var opt = document.createElement("option");
-			opt.appendChild(document.createTextNode(options[i]));
-			opt.value = i;
-			select.appendChild(opt);
-		}
-		select.childNodes[1].selected = "selected";
-		var graph = document.getElementById("trafic_graph");
-		var href = graph.src+"/";
-		select.onchange = function() {
-			graph.src = href+select.value;
-		};
-		field.parentNode.insertBefore(select, field);
-		field.parentNode.removeChild(field);
-	}
-};
-
-
 $(function() {
     // Make URL parameters accessibles everywere by $.urlParam('my_param')
     $.urlParam = function(name){

+ 3 - 4
coin/templates/base.html

@@ -65,10 +65,9 @@
     <script src="{% static "js/foundation.min.js" %}"></script>
     <script src="{% static "js/foundation/foundation.offcanvas.js" %}"></script>
     <script src="{% static "js/illyse.js" %}"></script>
-    {% block js %}{% endblock %}
+    <script> $(document).foundation(); </script>
+
+    {% block js %}{% endblock js %}
 
-    <script>
-      $(document).foundation();
-    </script>
   </body>
 </html>

+ 11 - 0
coin/templates/fragments/feed.html

@@ -0,0 +1,11 @@
+<div class="feed">
+{% for entry in feed_entries %}
+    <div class="entry">
+        <div class="date">{{ entry.published_parsed.tm_mday }}/{{ entry.published_parsed.tm_mon }}/{{ entry.published_parsed.tm_year }}</div>
+        <h5><a href="{{ entry.link }}" target="_blank">{{ entry.title }}</a></h5>
+        <div class="intro">
+            {{ entry.summary|striptags|safe|truncatewords:50 }}
+        </div>
+    </div>
+{% endfor %}
+</div>

+ 6 - 1
coin/urls.py

@@ -6,6 +6,9 @@ from django.conf.urls import patterns, include, url
 from django.conf.urls.static import static
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 
+from coin import views
+
+
 import autocomplete_light
 autocomplete_light.autodiscover()
 
@@ -27,7 +30,9 @@ urlpatterns = patterns(
     url(r'^admin/', include(admin.site.urls)),
 
     # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
-    
+
+    url(r'^feed/(?P<feed_name>.+)', views.feed, name='feed'),
+
     url(r'^autocomplete/', include('autocomplete_light.urls')),
 
 )

+ 43 - 0
coin/views.py

@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import feedparser
+import HTMLParser
+
+from django.views.decorators.cache import cache_page
+from django.template import RequestContext
+from django.shortcuts import render_to_response
+from django.http import HttpResponse, HttpResponseNotFound, HttpResponseServerError
+from django.conf import settings
+
+
+@cache_page(60 * 60 * 24) # Cache 24h
+def feed(request, feed_name):
+    feeds = settings.FEEDS
+    feed = None
+    # Recherce le flux passé en paramètre dans les flux définis dans settings
+    for feed_search in feeds:
+        if (feed_search[0] == feed_name):
+            feed = feed_search
+            break
+
+    # Si le flux n'a pas été trouvé ou qu'il n'y a pas d'URL donnée, renvoi 404
+    if not feed or len(feed)<2 or not feed[1]:
+        return HttpResponseNotFound('')
+    # Sinon récupère les informations (url et limit)
+    else:
+        feed_url = feed[1]
+        if len(feed) >=3:
+            limit = feed[2]
+        else:
+            limit = 3
+
+    try:
+        feed = feedparser.parse(feed_url)
+        entries = feed.entries[:limit]
+
+        return render_to_response('fragments/feed.html',
+                                  {'feed_entries': entries},
+                                  context_instance=RequestContext(request))
+    except:
+        return HttpResponseServerError('')

+ 2 - 1
coin/vpn/admin.py

@@ -12,7 +12,8 @@ from coin.utils import delete_selected
 class VPNConfigurationInline(admin.StackedInline):
     model = VPNConfiguration
     # fk_name = 'offersubscription'
-    readonly_fields = ['configuration_ptr']
+    exclude = ('password',)
+    readonly_fields = ['configuration_ptr', 'login']
 
 
 class VPNConfigurationAdmin(ConfigurationAdminFormMixin, PolymorphicChildModelAdmin):

+ 18 - 0
coin/vpn/migrations/0002_remove_vpnconfiguration_comment.py

@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('vpn', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='vpnconfiguration',
+            name='comment',
+        ),
+    ]

+ 0 - 2
coin/vpn/models.py

@@ -38,8 +38,6 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
                                      verbose_name="IPv6", blank=True, null=True,
                                      help_text="Adresse IPv6 utilisée par "
                                      "défaut sur le VPN")
-    comment = models.CharField(blank=True, max_length=512,
-                               verbose_name="commentaire")
 
     objects = NetManager()
 

+ 8 - 0
coin/vpn/templates/vpn/fragments/password.html

@@ -0,0 +1,8 @@
+<tr id="password">
+    <td class="center"><span class="label">Mot de passe</span></td>
+    <td><span class="pass">{{ password }}</span></td>
+</tr>
+<tr>
+    <td class="warning" colspan="2">Ce mot de passe ne sera affiché qu'une seule fois. Si vous le perdez, il faudra en générer un nouveau.</td>
+</tr>
+

+ 0 - 11
coin/vpn/templates/vpn/password.html

@@ -1,11 +0,0 @@
-{% extends "vpn/vpn.html" %}
-
-{% block password %}
-                <tr id="password">
-                    <td class="center"><span class="label">Mot de passe</span></td>
-                    <td><span class="pass">{{ password }}</span></td>
-                </tr>
-                <tr>
-                    <td class="warning" colspan="2">Ce mot de passe ne sera affiché qu'une seule fois. Si vous le perdez, il faudra en générer un nouveau.</td>
-                </tr>
-{% endblock %}

+ 54 - 15
coin/vpn/templates/vpn/vpn.html

@@ -6,21 +6,19 @@
     <form class="flatform" action="{{ object.get_absolute_url }}" method="post">{% csrf_token %}
     <p class="legend">Quand vous aurez terminé vos modifications, cliquez sur <input class="button" type="submit" value="Valider" /></p>
 
-    {% if messages %}
-    <ul class="messages">
-        {% for message in messages %}
-        <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
-        {% endfor %}
-    </ul>
-    {% endif %}
-    
+    {% for message in messages %}
+    <div class="message{% if message.tags %} {{ message.tags }}{% endif %}">
+        {{ message }}
+    </div>
+    {% endfor %}
+
     {% if form.non_field_errors or form.ipv4_endpoint.errors or form.ipv6_endpoint.errors %}
     <div class="alert-box alert nogap">
       {{ form.non_field_errors }}
       {{ form.ipv4_endpoint.errors }}
       {{ form.ipv6_endpoint.errors }}
     </div>{% endif %}
-    
+
     <div class="large-6 columns">
         <div class="panel">
             <h3>Statut</h3>
@@ -28,14 +26,14 @@
                 <tr>
                     <td class="center"><span class="label">Identifiant</span></td>
                     <td>{{object.login}}</td>
-                </tr>{% block password %}
+                </tr>
                 <tr>
                     <td class="center" colspan="2">
-                        <a class="button radius tiny" id="passgen" href="{% url 'vpn:generate_password' object.pk %}"><i class="fa fa-refresh"></i>
+                        <a class="button tiny radius" id="passgen" href="{% url 'vpn:generate_password' object.pk %}"><i class="fa fa-refresh"></i>
  Générer un nouveau mot de passe</a>
                     </td>
                 </tr>
-                {% endblock %}<tr class="flatfield">
+                <tr class="flatfield">
                     <td class="center">{{ form.comment.label_tag }}</td>
                     <td>{{ form.comment }}</td>
                 </tr>
@@ -74,9 +72,50 @@
     </form>
 </div>
 
-<div class="row">
-    <h3 class="graphtitle">Graphe de trafic sur <span id="trafic_zoom">24 heures</span> :</h3>
-    <img id="trafic_graph" src="{% url 'vpn:get_graph' vpn_id=object.pk %}" alt="Graphe de trafic {{ object.login }}" />
+<div class="row" id="graph">
+    <h3>Graphe de trafic sur
+        <select id="graph_period">
+            <option value="hourly">une heure</option>
+            <option value="daily" selected>24 heures</option>
+            <option value="weekly">7 jours</option>
+            <option value="monthly">un mois</option>
+            <option value="yearly">un an</option>
+        </select> : <small class="pending_request"></small></h3>
+    <img id="graph_trafic" src="{% url 'vpn:get_graph' vpn_id=object.pk %}" alt="Graphe de trafic {{ object.login }}" />
 </div>
 
 {% endblock %}
+
+{% block js %}
+    <script>
+        // Bouton génération du mot de passe
+        $('#passgen').click(function(){
+            if (!confirm("Ceci va effacer votre ancien mot de passe et en générer un nouveau. Continuer ?")) return false;
+
+            parent_cell = $(this).parent();
+            parent_cell.html('<span class="pending_request"><i class="fa fa-refresh fa-spin"></i> Génération en cours</span>');
+
+            $.ajax({
+                'url': $(this).attr('href')
+            }).done(function(html) {
+                //Remplace le tr parent par le contenu renvoyé (qui est deux tr successifs)
+                parent_cell.parent().replaceWith(html);
+            }).fail(function( jqXHR, textStatus ) {
+                parent_cell.html('<span class="error">Échec de la requête : ' + textStatus + '</span>');
+            });
+
+            return false;
+        });
+
+        // Graphe de conso data
+        $('#graph_period').change(function(){
+            $('#graph .pending_request').html('<i class="fa fa-refresh fa-spin"></i>');
+            base_url = "{% url 'vpn:get_graph' vpn_id=object.pk %}/";
+            $('#graph_trafic').attr('src', base_url + $(this).val());
+        });
+        $('#graph_trafic').load(function() {
+            $('#graph .pending_request').html('');
+        });
+
+    </script>
+{% endblock js %}

+ 1 - 1
coin/vpn/urls.py

@@ -9,7 +9,7 @@ urlpatterns = patterns(
     # This is part of the generic configuration interface (the "name" is
     # the same as the "backend_name" of the model).
     url(r'^(?P<id>\d+)$', VPNView.as_view(template_name="vpn/vpn.html"), name="details"),
-    url(r'^password/(?P<id>\d+)$', VPNGeneratePasswordView.as_view(template_name="vpn/password.html"), name="generate_password"),
+    url(r'^password/(?P<id>\d+)$', VPNGeneratePasswordView.as_view(template_name="vpn/fragments/password.html"), name="generate_password"),
     url(r'^graph/(?P<vpn_id>[0-9]+)/(?P<period>[a-z]+)$', get_graph, name="get_graph"),
     url(r'^graph/(?P<vpn_id>[0-9]+)$', get_graph, name="get_graph"),
 )

+ 2 - 2
coin/vpn/views.py

@@ -42,7 +42,7 @@ def generate_password(request, id):
     # This will hash the password automatically
     vpn.full_clean()
     vpn.save()
-    return render_to_response('vpn/password.html', {"vpn": vpn,
+    return render_to_response('vpn/fragments/password.html', {"vpn": vpn,
                                                     "password": password})
 
 
@@ -68,7 +68,7 @@ def get_graph(request, vpn_id, period="daily"):
     """
     vpn = get_object_or_404(VPNConfiguration, pk=vpn_id,
                             offersubscription__member=request.user)
-    
+
     time_periods = { 'hourly': '-1hour', 'daily': '-24hours', 'weekly': '-8days', 'monthly': '-32days', 'yearly': '-13months', }
     if period not in time_periods:
         period = 'daily'

+ 2 - 1
requirements.txt

@@ -1,4 +1,4 @@
-Django==1.7
+Django==1.7.1
 psycopg2==2.5.2
 python-ldap==2.4.15
 wsgiref==0.1.2
@@ -12,3 +12,4 @@ django-sendfile==0.3.6
 -e git+https://code.ffdn.org/zorun/django-postgresql-netfields.git#egg=django-netfields
 -e git+https://github.com/chrisglass/xhtml2pdf@a5d37ffd0ccb0603bdf668198de0f21766816104#egg=xhtml2pdf-master
 -e git+https://github.com/jlaine/django-ldapdb@1c4f9f29e52176f4367a1dffec2ecd2e123e2e7a#egg=django-ldapdb
+feedparser