Browse Source

Merge branch 'call_for_membership_fees' of https://code.ffdn.org/zorun/coin into call_for_membership_fees
+ Fix tests

Conflicts:
coin/members/management/commands/call_for_membership_fees.py

Fabs 10 years ago
parent
commit
61738e48b2

+ 75 - 7
README.md

@@ -23,11 +23,27 @@ A mirror of the code is available at:
   https://code.ffdn.org/zorun/coin/
   https://code.ffdn.org/zorun/coin/
 
 
 
 
+Demo
+====
+
+A demo of COIN is publicly available at:
+
+  https://coin-dev.illyse.org
+
+Login: ffdn
+Password: internet
+
+This user account has access to the administration interface.
+
+
 Quickstart
 Quickstart
 ==========
 ==========
 
 
-Get yourself a virtualenv. On Debian, install `python-virtualenv`. On
-Archlinux, the package is called `python2-virtualenv`, and you must
+Virtualenv
+----------
+
+Using a virtualenv is recommended.  On Debian, install `python-virtualenv`.
+On Archlinux, the package is called `python2-virtualenv`, and you must
 replace the `virtualenv` command with `virtualenv2` in the following.
 replace the `virtualenv` command with `virtualenv2` in the following.
 
 
 To create the virtualenv (the first time):
 To create the virtualenv (the first time):
@@ -56,6 +72,9 @@ You may experience problems with SSL certificates du to self-signed cert used by
 You should now be able to run `python manage.py` (within the
 You should now be able to run `python manage.py` (within the
 virtualenv, obviously) without error.
 virtualenv, obviously) without error.
 
 
+Settings
+--------
+
 The `coin/settings_local.py` file is ignored by Git: feel free to override any
 The `coin/settings_local.py` file is ignored by Git: feel free to override any
 setting by writing into that file. For example, to override the `DEBUG`
 setting by writing into that file. For example, to override the `DEBUG`
 settings:
 settings:
@@ -63,14 +82,27 @@ settings:
     echo '# -*- coding: utf-8 -*-' > coin/settings_local.py
     echo '# -*- coding: utf-8 -*-' > coin/settings_local.py
     echo 'DEBUG = TEMPLATE_DEBUG = True' >> coin/settings_local.py
     echo 'DEBUG = TEMPLATE_DEBUG = True' >> coin/settings_local.py
 
 
-
-At this point, you should setup your database.  Recommended is postgreSQL,
-but you might be able to use SQLite.  For more information, see https://www.illyse.org/projects/ils-si/wiki/Mise_en_place_environnement_de_dev
-
 If you don't want to use LDAP, just set in your `settings_local.py`:
 If you don't want to use LDAP, just set in your `settings_local.py`:
 
 
     LDAP_ACTIVATE = False
     LDAP_ACTIVATE = False
 
 
+See the end of this README for a reference of available configuration settings.
+
+Database
+--------
+
+At this point, you should setup your database: we highly recommend PostgreSQL.
+SQLite might work, but some features will not be available:
+
+- automatic allocation of IP subnets (needs proper subnet implementation in
+  the database)
+- sending automated emails to remind of expiring membership fee
+  (needs aggregation on date fields, see Django doc)
+
+For more information on the database setup, see:
+
+  https://www.illyse.org/projects/ils-si/wiki/Mise_en_place_environnement_de_dev
+
 The first time, you need to create the database, create a superuser, and
 The first time, you need to create the database, create a superuser, and
 import some base data to play with:
 import some base data to play with:
 
 
@@ -82,8 +114,10 @@ Note that the superuser will be inserted into the LDAP backend exactly in the
 same way as all other members, so you should use a real account (not just
 same way as all other members, so you should use a real account (not just
 admin/admin).
 admin/admin).
 
 
-Then, at each code update, you only need to apply migrations:
+Then, at each code update, you will only need to update dependencies and apply
+new migrations:
 
 
+    pip install -r requirements.txt
     python manage.py migrate
     python manage.py migrate
 
 
 
 
@@ -92,6 +126,27 @@ At this point, Django should run correctly:
     python manage.py runserver
     python manage.py runserver
 
 
 
 
+Available commands
+==================
+
+Some useful administration commands are available via `manage.py`.
+
+`python manage.py members_email`: returns email addresses of all members, one
+per line.  This may be useful to automatically feed a mailing list software.
+Note that membership is based on the `status` field of users, not on
+membership fees.  That is, even if a member has forgot to renew his or her
+membership fee, his or her address will still show up in this list.
+
+`python manage.py charge_subscriptions`: generate invoices (including a
+PDF version) for each subscriber.  You probably want to run this command
+every month as a cron task, see below.
+
+`python manage.py call_for_membership_fees`: send reminder emails to members
+whose membership fee is about to expire or is already expired (1 month before,
+on the day of expiration, 1 month after, 2 months after, and 3 months after).
+You should run this command in a cron job every day.
+
+
 Configuration
 Configuration
 =============
 =============
 
 
@@ -112,6 +167,19 @@ in the admin.  Information entered in this application has two purposes:
 Some bits of configuration are done in `settings.py`: LDAP branches, RSS feeds
 Some bits of configuration are done in `settings.py`: LDAP branches, RSS feeds
 to display on the home page, and so on.
 to display on the home page, and so on.
 
 
+Cron tasks
+----------
+
+You may want to run cron jobs for repetitive tasks.
+
+To generate invoices on the first day of each month, here at 3 am:
+
+`0 3 1 * * /home/coin/venv/bin/python manage.py charge_subscriptions`
+
+To send reminder emails for membership fee expiration:
+
+`42 3 * * * /home/coin/venv/bin/python manage.py call_for_membership_fees`
+
 
 
 More information
 More information
 ================
 ================

+ 3 - 1
coin/billing/create_subscriptions_invoices.py

@@ -13,11 +13,13 @@ from coin.members.models import Member
 from coin.billing.models import Invoice, InvoiceDetail
 from coin.billing.models import Invoice, InvoiceDetail
 
 
 
 
-def create_all_members_invoices_for_a_period(date=datetime.date.today()):
+def create_all_members_invoices_for_a_period(date=None):
     """
     """
     Pour chaque membre ayant au moins un abonnement actif, génère les factures
     Pour chaque membre ayant au moins un abonnement actif, génère les factures
     en prenant la date comme premier mois de la période de facturation
     en prenant la date comme premier mois de la période de facturation
     """
     """
+    if date is None:
+        date = datetime.date.today()
     members = Member.objects.filter(
     members = Member.objects.filter(
         Q(offersubscription__resign_date__isnull=True) |
         Q(offersubscription__resign_date__isnull=True) |
         Q(offersubscription__resign_date__gte=date))
         Q(offersubscription__resign_date__gte=date))

+ 3 - 1
coin/billing/models.py

@@ -41,7 +41,9 @@ class Invoice(models.Model):
         ('trouble', 'Litige')
         ('trouble', 'Litige')
     )
     )
 
 
-    validated = models.BooleanField(default=False, verbose_name='validée')
+    validated = models.BooleanField(default=False, verbose_name='validée',
+                                    help_text='Once validated, a PDF is generated'
+                                    ' and the invoice cannot be modified')
     number = models.CharField(max_length=25,
     number = models.CharField(max_length=25,
                               default=next_invoice_number,
                               default=next_invoice_number,
                               unique=True,
                               unique=True,

+ 19 - 14
coin/isp_database/models.py

@@ -36,6 +36,17 @@ class ISPInfo(SingleInstanceMixin, models.Model):
     The naming convention is different from Python/django so that it
     The naming convention is different from Python/django so that it
     matches exactly the format (which uses CamelCase...)
     matches exactly the format (which uses CamelCase...)
     """
     """
+    # These two properties can be overriden with static counters, see below.
+    @property
+    def memberCount(self):
+        """Number of members"""
+        return count_active_members()
+
+    @property
+    def subscriberCount(self):
+        """Number of subscribers to an internet access"""
+        return count_active_subscriptions()
+
     name = models.CharField(max_length=512,
     name = models.CharField(max_length=512,
                             help_text="The ISP's name")
                             help_text="The ISP's name")
     # Length required by the spec
     # Length required by the spec
@@ -70,10 +81,14 @@ class ISPInfo(SingleInstanceMixin, models.Model):
     longitude = models.FloatField(blank=True, null=True,
     longitude = models.FloatField(blank=True, null=True,
         help_text="Coordinates of the registered office (longitude)")
         help_text="Coordinates of the registered office (longitude)")
 
 
-    # Uncomment this if you want to manage these counters by hand.
-    #member_count = models.PositiveIntegerField(help_text="Number of members")
-    #subscriber_count = models.PositiveIntegerField(
-    #    help_text="Number of subscribers to an internet access")
+    # Uncomment this (and handle the necessary migrations) if you want to
+    # manage one of the counters by hand.  Otherwise, they are computed
+    # automatically, which is probably what you want.
+    #memberCount = models.PositiveIntegerField(help_text="Number of members",
+    #                                          default=0)
+    #subscriberCount = models.PositiveIntegerField(
+    #    help_text="Number of subscribers to an internet access",
+    #    default=0)
 
 
     # field outside of db-ffdn format:
     # field outside of db-ffdn format:
     administrative_email = models.EmailField(
     administrative_email = models.EmailField(
@@ -89,16 +104,6 @@ class ISPInfo(SingleInstanceMixin, models.Model):
         help_text="URL du serveur de listes de discussions/diffusion")
         help_text="URL du serveur de listes de discussions/diffusion")
 
 
     @property
     @property
-    def memberCount(self):
-        """Number of members"""
-        return count_active_members()
-
-    @property
-    def subscriberCount(self):
-        """Number of subscribers to an internet access"""
-        return count_active_subscriptions()
-
-    @property
     def version(self):
     def version(self):
         """Version of the API"""
         """Version of the API"""
         return API_VERSION
         return API_VERSION

+ 16 - 13
coin/members/management/commands/call_for_membership_fees.py

@@ -4,6 +4,7 @@ from __future__ import unicode_literals
 import datetime
 import datetime
 from dateutil.relativedelta import relativedelta
 from dateutil.relativedelta import relativedelta
 from django.core.management.base import BaseCommand, CommandError
 from django.core.management.base import BaseCommand, CommandError
+from django.db.models import Max
 from django.conf import settings
 from django.conf import settings
 from django.db.models import Max
 from django.db.models import Max
 
 
@@ -29,26 +30,28 @@ class Command(BaseCommand):
             raise CommandError(
             raise CommandError(
                 'Please enter a valid date : YYYY-mm-dd (ex: 2011-07-04)')
                 'Please enter a valid date : YYYY-mm-dd (ex: 2011-07-04)')
 
 
-        # Get membership_fees filtered by end date of membership at specific
-        # date relative to today
-        call_dates = [date + relativedelta(months=-3),
-                      date + relativedelta(months=-2),
-                      date + relativedelta(months=-1),
-                      date,
-                      date + relativedelta(months=+1)]
+        end_dates = [date + relativedelta(months=-3),
+                     date + relativedelta(months=-2),
+                     date + relativedelta(months=-1),
+                     date,
+                     date + relativedelta(months=+1)]
 
 
-        self.stdout.write(
-            'Select membership fees for following end dates : %s' % call_dates)
+        self.stdout.write("Selecting members whose membership fee end at the "
+                          "following dates : {dates}".format(
+                              dates=[str(d) for d in end_dates]))
 
 
-        members = Member.objects.annotate(end_date_membership=Max(
-            'membership_fees__end_date')).filter(end_date_membership__in=call_dates)
+        members = Member.objects.annotate(end=Max('membership_fees__end_date'))\
+                                .filter(end__in=end_dates)
+        self.stdout.write(
+            "Got {number} members.".format(number=members.count()))
 
 
         cpt = 0
         cpt = 0
         with respect_language(settings.LANGUAGE_CODE):
         with respect_language(settings.LANGUAGE_CODE):
             for member in members:
             for member in members:
                 if member.send_call_for_membership_fees_email():
                 if member.send_call_for_membership_fees_email():
                     self.stdout.write(
                     self.stdout.write(
-                        'Call for membership fees email was sent to %s' % member)
-                    cpt=cpt+1
+                        'Call for membership fees email was sent to {member} ({email})'.format(
+                            member=member, email=member.email))
+                    cpt = cpt + 1
 
 
         self.stdout.write('%d call for membership fees emails were sent' % cpt)
         self.stdout.write('%d call for membership fees emails were sent' % cpt)

+ 16 - 14
coin/members/models.py

@@ -6,7 +6,7 @@ import unicodedata
 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, Max
 from django.db.models.signals import pre_save
 from django.db.models.signals import pre_save
 from django.dispatch import receiver
 from django.dispatch import receiver
 from django.contrib.auth.models import AbstractUser
 from django.contrib.auth.models import AbstractUser
@@ -112,22 +112,20 @@ class Member(CoinLdapSyncMixin, AbstractUser):
 
 
     # Renvoie la date de fin de la dernière cotisation du membre
     # Renvoie la date de fin de la dernière cotisation du membre
     def end_date_of_membership(self):
     def end_date_of_membership(self):
-        try:
-            return self.membership_fees.order_by('-end_date')[0].end_date
-        # TODO: bad practice de tout matcher comme ca
-        except:
-            return None
+        aggregate = self.membership_fees.aggregate(end=Max('end_date'))
+        return aggregate['end']
     end_date_of_membership.short_description = "Date de fin d'adhésion"
     end_date_of_membership.short_description = "Date de fin d'adhésion"
 
 
-    def is_paid_up(self, date=datetime.date.today()):
+    def is_paid_up(self, date=None):
         """
         """
-        True si le membre est à jour de cotisation à la date passée. False sinon
+        Teste si le membre est à jour de cotisation à la date donnée.
         """
         """
-        if self.end_date_of_membership() \
-                and self.end_date_of_membership() >= date:
-            return True
-        else:
+        if date is None:
+            date = datetime.date.today()
+        end_date = self.end_date_of_membership()
+        if end_date is None:
             return False
             return False
+        return (end_date >= date)
 
 
     def set_password(self, new_password, *args, **kwargs):
     def set_password(self, new_password, *args, **kwargs):
         """
         """
@@ -136,19 +134,23 @@ class Member(CoinLdapSyncMixin, AbstractUser):
         super(Member, self).set_password(new_password, *args, **kwargs)
         super(Member, self).set_password(new_password, *args, **kwargs)
         self._password_ldap = utils.ldap_hash(new_password)
         self._password_ldap = utils.ldap_hash(new_password)
 
 
-    def get_active_subscriptions(self, date=datetime.date.today()):
+    def get_active_subscriptions(self, date=None):
         """
         """
         Return list of OfferSubscription which are active today
         Return list of OfferSubscription which are active today
         """
         """
+        if date is None:
+            date = datetime.date.today()
         return OfferSubscription.objects.filter(
         return OfferSubscription.objects.filter(
             Q(member__exact=self.pk),
             Q(member__exact=self.pk),
             Q(subscription_date__lte=date),
             Q(subscription_date__lte=date),
             Q(resign_date__isnull=True) | Q(resign_date__gte=date))
             Q(resign_date__isnull=True) | Q(resign_date__gte=date))
 
 
-    def get_inactive_subscriptions(self, date=datetime.date.today()):
+    def get_inactive_subscriptions(self, date=None):
         """
         """
         Return list of OfferSubscription which are not active today
         Return list of OfferSubscription which are not active today
         """
         """
+        if date is None:
+            date = datetime.date.today()
         return OfferSubscription.objects.filter(
         return OfferSubscription.objects.filter(
             Q(member__exact=self.pk),
             Q(member__exact=self.pk),
             Q(subscription_date__gt=date) |
             Q(subscription_date__gt=date) |

+ 18 - 5
coin/members/tests.py

@@ -436,20 +436,24 @@ class MemberTestCallForMembershipCommand(TestCase):
         membershipfee.save()
         membershipfee.save()
         return membershipfee
         return membershipfee
 
 
-    def do_test_email_sent(self, expected_emails=1):
+    def do_test_email_sent(self, expected_emails = 1, reset_date_last_call = True):
         # Vide la outbox
         # Vide la outbox
         mail.outbox = []
         mail.outbox = []
         # Call command
         # Call command
         management.call_command('call_for_membership_fees', stdout=StringIO())
         management.call_command('call_for_membership_fees', stdout=StringIO())
         # Test
         # Test
         self.assertEqual(len(mail.outbox), expected_emails)
         self.assertEqual(len(mail.outbox), expected_emails)
+        # Comme on utilise le même membre, on reset la date de dernier envoi
+        if reset_date_last_call:
+            self.member.date_last_call_for_membership_fees_email = None
+            self.member.save()
 
 
-    def do_test_for_a_end_date(self, end_date, expected_emails=1):
+    def do_test_for_a_end_date(self, end_date, expected_emails=1, reset_date_last_call = True):
         # Supprimer toutes les cotisations (au cas ou)
         # Supprimer toutes les cotisations (au cas ou)
         MembershipFee.objects.all().delete()
         MembershipFee.objects.all().delete()
         # Créé la cotisation
         # Créé la cotisation
         membershipfee = self.create_membership_fee(end_date)
         membershipfee = self.create_membership_fee(end_date)
-        self.do_test_email_sent(expected_emails)
+        self.do_test_email_sent(expected_emails, reset_date_last_call)
         membershipfee.delete()
         membershipfee.delete()
 
 
     def test_call_email_sent_at_expected_dates(self):
     def test_call_email_sent_at_expected_dates(self):
@@ -463,13 +467,22 @@ class MemberTestCallForMembershipCommand(TestCase):
     def test_call_email_not_sent_if_active_membership_fee(self):
     def test_call_email_not_sent_if_active_membership_fee(self):
         # Créé une cotisation se terminant dans un mois
         # Créé une cotisation se terminant dans un mois
         membershipfee = self.create_membership_fee(date.today() + relativedelta(months=+1))
         membershipfee = self.create_membership_fee(date.today() + relativedelta(months=+1))
-        # Un mail envoyé
-        self.do_test_email_sent(1)
+        # Un mail devrait être envoyé (ne pas vider date_last_call_for_membership_fees_email)
+        self.do_test_email_sent(1, False)
         # Créé une cotisation enchainant et se terminant dans un an
         # Créé une cotisation enchainant et se terminant dans un an
         membershipfee = self.create_membership_fee(date.today() + relativedelta(months=+1, years=+1))
         membershipfee = self.create_membership_fee(date.today() + relativedelta(months=+1, years=+1))
         # Pas de mail envoyé
         # Pas de mail envoyé
         self.do_test_email_sent(0)
         self.do_test_email_sent(0)
 
 
+    def test_date_last_call_for_membership_fees_email(self):
+        # Créé une cotisation se terminant dans un mois
+        membershipfee = self.create_membership_fee(date.today() + relativedelta(months=+1))
+        # Un mail envoyé (ne pas vider date_last_call_for_membership_fees_email)
+        self.do_test_email_sent(1, False)
+        # Tente un deuxième envoi, qui devrait être à 0
+        self.do_test_email_sent(0)
+
+
 class MemberTestsUtils(object):
 class MemberTestsUtils(object):
 
 
     @staticmethod
     @staticmethod

+ 27 - 0
coin/offers/migrations/0005_auto_20150210_0923.py

@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import django.core.validators
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('offers', '0004_auto_20150120_2309'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='offer',
+            name='billing_period',
+            field=models.IntegerField(default=1, help_text='en mois', verbose_name='p\xe9riode de facturation', validators=[django.core.validators.MinValueValidator(1)]),
+            preserve_default=True,
+        ),
+        migrations.AlterField(
+            model_name='offersubscription',
+            name='commitment',
+            field=models.IntegerField(default=0, help_text='en mois', verbose_name="p\xe9riode d'engagement", validators=[django.core.validators.MinValueValidator(0)]),
+            preserve_default=True,
+        ),
+    ]

+ 23 - 6
coin/offers/models.py

@@ -5,6 +5,7 @@ import datetime
 
 
 from django.db import models
 from django.db import models
 from django.db.models import Q
 from django.db.models import Q
+from django.core.validators import MinValueValidator
 
 
 
 
 class Offer(models.Model):
 class Offer(models.Model):
@@ -24,7 +25,8 @@ class Offer(models.Model):
                             help_text="Type de configuration à utiliser avec cette offre")
                             help_text="Type de configuration à utiliser avec cette offre")
     billing_period = models.IntegerField(blank=False, null=False, default=1,
     billing_period = models.IntegerField(blank=False, null=False, default=1,
                                          verbose_name='période de facturation',
                                          verbose_name='période de facturation',
-                                         help_text='en mois')
+                                         help_text='en mois',
+                                         validators=[MinValueValidator(1)])
     period_fees = models.DecimalField(max_digits=5, decimal_places=2,
     period_fees = models.DecimalField(max_digits=5, decimal_places=2,
                                       blank=False, null=False,
                                       blank=False, null=False,
                                       verbose_name='montant par période de '
                                       verbose_name='montant par période de '
@@ -49,11 +51,25 @@ class Offer(models.Model):
         return self.configuration_type
         return self.configuration_type
     get_configuration_type_display.short_description = 'type de configuration'
     get_configuration_type_display.short_description = 'type de configuration'
 
 
+    def display_price(self):
+        """Displays the price of an offer in a human-readable manner
+        (for instance "30€ / month")
+        """
+        if int(self.period_fees) == self.period_fees:
+            fee = int(self.period_fees)
+        else:
+            fee = self.period_fees
+        if self.billing_period == 1:
+            period = ""
+        else:
+            period = self.billing_period
+        return "{period_fee}€ / {billing_period} mois".format(
+            period_fee=fee,
+            billing_period=period)
+
     def __unicode__(self):
     def __unicode__(self):
-        return '{name} - {period_fee}€ / {billing_period}m'.format(
-            name=self.name,
-            period_fee=self.period_fees,
-            billing_period=self.billing_period)
+        return '{name} - {price}'.format(name=self.name,
+                                         price=self.display_price())
 
 
     class Meta:
     class Meta:
         verbose_name = 'offre'
         verbose_name = 'offre'
@@ -81,7 +97,8 @@ class OfferSubscription(models.Model):
     # TODO: move this to offers?
     # TODO: move this to offers?
     commitment = models.IntegerField(blank=False, null=False,
     commitment = models.IntegerField(blank=False, null=False,
                                      verbose_name="période d'engagement",
                                      verbose_name="période d'engagement",
-                                     help_text = 'en mois',
+                                     help_text='en mois',
+                                     validators=[MinValueValidator(0)],
                                      default=0)
                                      default=0)
     member = models.ForeignKey('members.Member', verbose_name='membre')
     member = models.ForeignKey('members.Member', verbose_name='membre')
     offer = models.ForeignKey('Offer', verbose_name='offre')
     offer = models.ForeignKey('Offer', verbose_name='offre')

+ 22 - 0
coin/resources/migrations/0003_auto_20150203_1043.py

@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import netfields.fields
+import coin.resources.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('resources', '0002_ipsubnet_name_server'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='ipsubnet',
+            name='inet',
+            field=netfields.fields.CidrAddressField(validators=[coin.resources.models.validate_subnet], max_length=43, blank=True, help_text='Laisser vide pour allouer automatiquement', unique=True, verbose_name='sous-r\xe9seau'),
+            preserve_default=True,
+        ),
+    ]

+ 3 - 6
coin/resources/models.py

@@ -55,13 +55,8 @@ class IPPool(models.Model):
 
 
 
 
 class IPSubnet(models.Model):
 class IPSubnet(models.Model):
-    # TODO: find some way to signal to Subscriptions objects when a subnet
-    # gets modified (so that the subscription can update the LDAP backend
-    # accordingly)
-    # Actually, a better idea would be to build a custom relation and update
-    # LDAP in the relation itself.
     inet = CidrAddressField(blank=True, validators=[validate_subnet],
     inet = CidrAddressField(blank=True, validators=[validate_subnet],
-                            verbose_name="sous-réseau",
+                            unique=True, verbose_name="sous-réseau",
                             help_text="Laisser vide pour allouer automatiquement")
                             help_text="Laisser vide pour allouer automatiquement")
     objects = NetManager()
     objects = NetManager()
     ip_pool = models.ForeignKey(IPPool, verbose_name="pool d'IP")
     ip_pool = models.ForeignKey(IPPool, verbose_name="pool d'IP")
@@ -91,6 +86,8 @@ class IPSubnet(models.Model):
             first_free = available.next()
             first_free = available.next()
         except StopIteration:
         except StopIteration:
             raise ValidationError("Impossible d'allouer un sous-réseau : bloc d'IP rempli.")
             raise ValidationError("Impossible d'allouer un sous-réseau : bloc d'IP rempli.")
+        # first_free is a subnet, but it might be too large for our needs.
+        # This selects the first sub-subnet of the right size.
         self.inet = first_free.subnet(self.ip_pool.default_subnetsize, 1).next()
         self.inet = first_free.subnet(self.ip_pool.default_subnetsize, 1).next()
 
 
     def validate_inclusion(self):
     def validate_inclusion(self):

+ 1 - 2
coin/vpn/models.py

@@ -157,8 +157,7 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
 
 
 
 
 class LdapVPNConfig(ldapdb.models.Model):
 class LdapVPNConfig(ldapdb.models.Model):
-    # TODO: déplacer ligne suivante dans settings.py
-    base_dn = settings.VPN_CONF_BASE_DN # "ou=vpn,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
+    base_dn = settings.VPN_CONF_BASE_DN
     object_classes = [b'person', b'organizationalPerson', b'inetOrgPerson',
     object_classes = [b'person', b'organizationalPerson', b'inetOrgPerson',
                       b'top', b'radiusprofile']
                       b'top', b'radiusprofile']