Parcourir la source

Merge branch 'demo_ffdn'

Baptiste Jonglez il y a 10 ans
Parent
commit
910772228d

+ 6 - 3
coin/members/admin.py

@@ -32,7 +32,8 @@ class MembershipFeeInline(admin.TabularInline):
 
 
 class MemberAdmin(UserAdmin):
 class MemberAdmin(UserAdmin):
     list_display = ('id', 'status', 'username', 'first_name', 'last_name',
     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_display_links = ('id', 'username', 'first_name', 'last_name')
     list_filter = ('status', MembershipFeeFilter)
     list_filter = ('status', MembershipFeeFilter)
     search_fields = ['username', 'first_name', 'last_name', 'email']
     search_fields = ['username', 'first_name', 'last_name', 'email']
@@ -47,7 +48,8 @@ class MemberAdmin(UserAdmin):
         ('Adhérent', {'fields': (
         ('Adhérent', {'fields': (
             ('status', 'resign_date'),
             ('status', 'resign_date'),
             'type',
             'type',
-            ('first_name', 'last_name', 'organization_name'))}),
+            ('first_name', 'last_name', 'nickname'),
+            'organization_name')}),
         ('Coordonnées', {'fields': (
         ('Coordonnées', {'fields': (
             'email',
             'email',
             ('home_phone_number', 'mobile_phone_number'),
             ('home_phone_number', 'mobile_phone_number'),
@@ -63,7 +65,8 @@ class MemberAdmin(UserAdmin):
         ('Adhérent', {'fields': (
         ('Adhérent', {'fields': (
             'status',
             'status',
             'type',
             'type',
-            ('first_name', 'last_name', 'organization_name'))}),
+            ('first_name', 'last_name', 'nickname'),
+            'organization_name')}),
         ('Coordonnées', {'fields': (
         ('Coordonnées', {'fields': (
             'email',
             'email',
             ('home_phone_number', 'mobile_phone_number'),
             ('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,
     username = forms.RegexField(required=False,
                                 label="Nom d'utilisateur", max_length=30, regex=r"^[\w.@+-]+$",
                                 label="Nom d'utilisateur", max_length=30, regex=r"^[\w.@+-]+$",
                                 help_text="Laisser vide pour le générer automatiquement à partir du "
                                 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(
     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:
     class Meta:
         model = Member
         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 ldapdb.models
 import unicodedata
 import unicodedata
-import string
 import datetime
 import datetime
 from django.db import models
 from django.db import models
 from django.db.models import Q
 from django.db.models import Q
@@ -12,6 +11,7 @@ from django.dispatch import receiver
 from django.contrib.auth.models import AbstractUser
 from django.contrib.auth.models import AbstractUser
 from django.conf import settings
 from django.conf import settings
 from django.core.validators import RegexValidator
 from django.core.validators import RegexValidator
+from django.core.exceptions import ValidationError
 from ldapdb.models.fields import CharField, IntegerField, ListField
 from ldapdb.models.fields import CharField, IntegerField, ListField
 
 
 from coin.offers.models import OfferSubscription
 from coin.offers.models import OfferSubscription
@@ -35,10 +35,13 @@ class Member(CoinLdapSyncMixin, AbstractUser):
     )
     )
 
 
     status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
     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,
     type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
                             default='natural_person', verbose_name='type')
                             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,
     organization_name = models.CharField(max_length=200, blank=True,
                                          verbose_name="nom de l'organisme",
                                          verbose_name="nom de l'organisme",
                                          help_text='Pour une personne morale')
                                          help_text='Pour une personne morale')
@@ -75,14 +78,26 @@ class Member(CoinLdapSyncMixin, AbstractUser):
     # passwords for both ldap and local db
     # passwords for both ldap and local db
     _password_ldap = None
     _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):
     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):
     def get_full_name(self):
-        return '%s %s' % (self.first_name, self.last_name)
+        return str(self)
 
 
     def get_short_name(self):
     def get_short_name(self):
         return self.username
         return self.username
@@ -172,8 +187,12 @@ class Member(CoinLdapSyncMixin, AbstractUser):
             ldap_user.nick_name = self.username
             ldap_user.nick_name = self.username
             ldap_user.uidNumber = uid_number
             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 a password is definied in _password_ldap, change it in LDAP
         if self._password_ldap:
         if self._password_ldap:
@@ -216,37 +235,44 @@ class Member(CoinLdapSyncMixin, AbstractUser):
     class Meta:
     class Meta:
         verbose_name = 'membre'
         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')._unique = True
 Member._meta.get_field('email').blank = False
 Member._meta.get_field('email').blank = False
 Member._meta.get_field('email').null = 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():
 def count_active_members():
     return Member.objects.filter(status='member').count()
     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
     Calcul le username automatiquement en fonction
     du nom et du prénom
     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
     # Remplacer ou enlever les caractères non ascii
     username = unicodedata.normalize('NFD', username)\
     username = unicodedata.normalize('NFD', username)\
         .encode('ascii', 'ignore')
         .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)
     username = username.translate(None, punctuation)
     # En minuscule
     # En minuscule
     username = username.lower()
     username = username.lower()
@@ -257,7 +283,7 @@ def get_automatic_username(first_name, last_name):
     member = Member.objects.filter(username=username)
     member = Member.objects.filter(username=username)
     base_username = username
     base_username = username
     incr = 2
     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:
     while member:
         if len(base_username) >= 30:
         if len(base_username) >= 30:
             username = base_username[30 - len(str(incr)):]
             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
     le calcul automatiquement en fonction du nom et du prénom
     """
     """
     if not instance.username and not instance.pk:
     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)
 @receiver(pre_save, sender=LdapUser)

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

@@ -12,8 +12,17 @@
             <h3>Me joindre</h3>
             <h3>Me joindre</h3>
             <table class="full-width">
             <table class="full-width">
                 <tr>
                 <tr>
+                  {% if user.type == 'natural_person' %}
                     <td class="center"><span class="label">Prénom - Nom</span></td>
                     <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>
                 <tr>
                 <tr>
                     <td class="center"><span class="label">Adresse</span></td>
                     <td class="center"><span class="label">Adresse</span></td>

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

@@ -16,6 +16,39 @@
         <h3>Stats</h3>
         <h3>Stats</h3>
         <div class="panel">Use MOAR bandwidth !</div>
         <div class="panel">Use MOAR bandwidth !</div>
     </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>
 </div>
 
 
 {% endblock %}
 {% 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 %}

+ 34 - 1
coin/members/tests.py

@@ -202,6 +202,28 @@ class MemberTests(TestCase):
         member2.delete()
         member2.delete()
         member3.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):
     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
         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):
     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
         # Créé un membre
         first_name = 'Capitain'
         first_name = 'Capitain'
@@ -327,6 +349,17 @@ class MemberTests(TestCase):
         # de cotisation
         # de cotisation
         self.assertEqual(member.is_paid_up(), True)
         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):
 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'^subscription/(?P<id>\d+)', views.subscriptions, name = 'subscription'),
 
 
     url(r'^invoices/', views.invoices, name='invoices'),
     url(r'^invoices/', views.invoices, name='invoices'),
-    url(r'^contact/', views.contact, name='contact')
+    url(r'^contact/', views.contact, name='contact'),
 )
 )

+ 5 - 0
coin/settings.py

@@ -250,6 +250,11 @@ MEMBER_DEFAULT_COTISATION = 20
 # Reset session if cookie older than 2h.
 # Reset session if cookie older than 2h.
 SESSION_COOKIE_AGE = 7200
 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
 # Surcharge les paramètres en utilisant le fichier settings_local.py
 try:
 try:
     from settings_local import *
     from settings_local import *

+ 12 - 0
coin/static/css/illyse.css

@@ -270,6 +270,18 @@ tr.inactive {
 	border: 2px solid #C0C0C0;
 	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 {
 .legend .button, .formcontrol .button {
 	padding: 0.25em 0.5em;
 	padding: 0.25em 0.5em;
 	border-radius:5px;
 	border-radius:5px;

+ 3 - 4
coin/templates/base.html

@@ -65,10 +65,9 @@
     <script src="{% static "js/foundation.min.js" %}"></script>
     <script src="{% static "js/foundation.min.js" %}"></script>
     <script src="{% static "js/foundation/foundation.offcanvas.js" %}"></script>
     <script src="{% static "js/foundation/foundation.offcanvas.js" %}"></script>
     <script src="{% static "js/illyse.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>
   </body>
 </html>
 </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.conf.urls.static import static
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 
 
+from coin import views
+
+
 import autocomplete_light
 import autocomplete_light
 autocomplete_light.autodiscover()
 autocomplete_light.autodiscover()
 
 
@@ -27,7 +30,9 @@ urlpatterns = patterns(
     url(r'^admin/', include(admin.site.urls)),
     url(r'^admin/', include(admin.site.urls)),
 
 
     # url(r'^admin/doc/', include('django.contrib.admindocs.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')),
     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('')

+ 1 - 0
requirements.txt

@@ -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://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/chrisglass/xhtml2pdf@a5d37ffd0ccb0603bdf668198de0f21766816104#egg=xhtml2pdf-master
 -e git+https://github.com/jlaine/django-ldapdb@1c4f9f29e52176f4367a1dffec2ecd2e123e2e7a#egg=django-ldapdb
 -e git+https://github.com/jlaine/django-ldapdb@1c4f9f29e52176f4367a1dffec2ecd2e123e2e7a#egg=django-ldapdb
+feedparser