Browse Source

Merge branch 'django-1.8' of FFDN/coin into master

jocelyn 8 years ago
parent
commit
0277bfedd2

+ 1 - 1
README.md

@@ -55,7 +55,7 @@ and `libjpeg-dev` packages.
 You need a recent *pip* for the installation of dependencies to work. If you
 don't meet that requirement (Ubuntu trusty does not), run:
 
-    pip install pip>=1.5.6
+    pip install "pip>=1.5.6"
 
 In any case, you then need to install coin python dependencies:
 

+ 19 - 0
coin/configuration/migrations/0004_auto_20161015_1837.py

@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('configuration', '0003_configuration_comment'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='configuration',
+            name='polymorphic_ctype',
+            field=models.ForeignKey(related_name='polymorphic_configuration.configuration_set+', editable=False, to='contenttypes.ContentType', null=True),
+        ),
+    ]

+ 1 - 0
coin/isp_database/admin.py

@@ -12,6 +12,7 @@ class ISPAdminForm(ModelForm):
 
     class Meta:
         model = ISPInfo
+        exclude = []
 
     phone_number = FRPhoneNumberField(required=False,
                                       help_text='Main contact phone number')

+ 14 - 8
coin/isp_database/models.py

@@ -11,6 +11,7 @@ from localflavor.fr.models import FRSIRETField
 from coin.members.models import count_active_members
 from coin.offers.models import count_active_subscriptions
 from coin import utils
+from coin.validation import chatroom_url_validator
 
 # API version, see http://db.ffdn.org/format
 API_VERSION = 0.1
@@ -61,9 +62,8 @@ class ISPInfo(SingleInstanceMixin, models.Model):
                               help_text="HTTP(S) URL of the ISP's logo")
     website = models.URLField(blank=True,
                               help_text='URL to the official website')
-    email = models.EmailField(max_length=254,
-                              help_text="Contact email address")
-    mainMailingList = models.EmailField(max_length=254, blank=True,
+    email = models.EmailField(help_text="Contact email address")
+    mainMailingList = models.EmailField(blank=True,
                                         verbose_name="main mailing list",
                                         help_text="Main public mailing-list")
     phone_number = models.CharField(max_length=25, blank=True,
@@ -97,11 +97,11 @@ class ISPInfo(SingleInstanceMixin, models.Model):
 
     # field outside of db-ffdn format:
     administrative_email = models.EmailField(
-        max_length=254, blank=True, verbose_name="contact administratif",
+        blank=True, verbose_name="contact administratif",
         help_text='Adresse email pour les contacts administratifs (ex: bureau)')
 
     support_email = models.EmailField(
-        max_length=254, blank=True, verbose_name="contact de support",
+        blank=True, verbose_name="contact de support",
         help_text="Adresse email pour les demandes de support technique")
 
     lists_url = models.URLField(
@@ -115,8 +115,13 @@ class ISPInfo(SingleInstanceMixin, models.Model):
 
     @property
     def main_chat_verbose(self):
-        m = utils.re_chat_url.match(self.chatroom_set.first().url)
-        return '{channel} sur {server}'.format(**(m.groupdict()))
+        first_chatroom = self.chatroom_set.first()
+        if first_chatroom:
+            m = utils.re_chat_url.match(first_chatroom.url)
+            if m:
+                return '{channel} sur {server}'.format(**(m.groupdict()))
+
+        return None
 
     def get_absolute_url(self):
         return '/isp.json'
@@ -191,7 +196,8 @@ class RegisteredOffice(models.Model):
 
 
 class ChatRoom(models.Model):
-    url = models.CharField(verbose_name="URL", max_length=256)
+    url = models.CharField(
+        verbose_name="URL", max_length=256, validators=[chatroom_url_validator])
     isp = models.ForeignKey(ISPInfo)
 
 

+ 23 - 0
coin/isp_database/tests.py

@@ -1,8 +1,11 @@
+from django.contrib.auth.models import UserManager
 from django.test import TestCase
 
 # Create your tests here.
 
+from coin.members.models import Member
 from coin.isp_database.templatetags.isptags import *
+from .models import ChatRoom, ISPInfo
 
 class TestPrettifiers(TestCase):
     def test_pretty_iban(self):
@@ -16,3 +19,23 @@ class TestPrettifiers(TestCase):
         self.assertEqual(pretty_iban('ADkkBBBBSSSSCCCCCCCCCCCC'),
                          'ADkk BBBB SSSS CCCC CCCC CCCC')
         self.assertEqual(pretty_iban(''), '')
+
+class TestContactPage(TestCase):
+    def setUp(self):
+        # Could be replaced by a force_login when we will be at Django 1.9
+        Member.objects.create_user('user', password='password')
+        self.client.login(username='user', password='password')
+
+    def test_chat_view(self):
+        isp = ISPInfo.objects.create(name='test', email='foo@example.com', )
+
+        # Without chatroom
+        response = self.client.get('/members/contact/')
+        self.assertEqual(response.status_code, 200)
+
+        # With chatroom
+        ChatRoom.objects.create(
+            isp=isp, url='irc://irc.example.com/#chan')
+
+        response = self.client.get('/members/contact/')
+        self.assertEqual(response.status_code, 200)

+ 42 - 0
coin/members/migrations/0013_auto_20161015_1837.py

@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.core.validators
+import django.contrib.auth.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('members', '0012_member_date_last_call_for_membership_fees_email'),
+    ]
+
+    operations = [
+        migrations.AlterModelManagers(
+            name='member',
+            managers=[
+                ('objects', django.contrib.auth.models.UserManager()),
+            ],
+        ),
+        migrations.AlterField(
+            model_name='member',
+            name='email',
+            field=models.EmailField(unique=True, max_length=254, verbose_name='email address'),
+        ),
+        migrations.AlterField(
+            model_name='member',
+            name='groups',
+            field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups'),
+        ),
+        migrations.AlterField(
+            model_name='member',
+            name='last_login',
+            field=models.DateTimeField(null=True, verbose_name='last login', blank=True),
+        ),
+        migrations.AlterField(
+            model_name='member',
+            name='username',
+            field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=30, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username'),
+        ),
+    ]

+ 9 - 0
coin/members/tests.py

@@ -14,8 +14,10 @@ from django.conf import settings
 from django.test import TestCase, Client, override_settings
 from django.contrib.auth.models import User
 from django.core import mail, management
+from django.core.exceptions import ValidationError
 
 from coin.members.models import Member, MembershipFee, LdapUser
+from coin.validation import chatroom_url_validator
 
 
 @unittest.skipIf(not settings.LDAP_ACTIVATE, "LDAP disabled")
@@ -500,3 +502,10 @@ class MemberTestsUtils(object):
         Renvoi une clé aléatoire pour un utilisateur LDAP
         """
         return 'coin_test_' + os.urandom(8).encode('hex')
+
+
+class TestValidators(TestCase):
+    def test_valid_chatroom(self):
+        chatroom_url_validator('irc://irc.example.com/#chan')
+        with self.assertRaises(ValidationError):
+            chatroom_url_validator('http://#faimaison@irc.geeknode.org')

+ 2 - 2
coin/resources/migrations/0001_initial.py

@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('name', models.CharField(help_text="Nom du pool d'IP", max_length=255, verbose_name='nom')),
                 ('default_subnetsize', models.PositiveSmallIntegerField(help_text='Taille par d\xe9faut du sous-r\xe9seau \xe0 allouer aux abonn\xe9s dans ce pool', verbose_name='taille de sous-r\xe9seau par d\xe9faut', validators=[django.core.validators.MaxValueValidator(64)])),
-                ('inet', netfields.fields.CidrAddressField(help_text="Bloc d'adresses IP du pool", max_length=43, verbose_name='r\xe9seau', validators=[coin.resources.models.validate_subnet])),
+                ('inet', netfields.fields.CidrAddressField(help_text="Bloc d'adresses IP du pool", max_length=43, verbose_name='r\xe9seau')),
             ],
             options={
                 'verbose_name': "pool d'IP",
@@ -32,7 +32,7 @@ class Migration(migrations.Migration):
             name='IPSubnet',
             fields=[
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
-                ('inet', netfields.fields.CidrAddressField(blank=True, help_text='Laisser vide pour allouer automatiquement', max_length=43, verbose_name='sous-r\xe9seau', validators=[coin.resources.models.validate_subnet])),
+                ('inet', netfields.fields.CidrAddressField(blank=True, help_text='Laisser vide pour allouer automatiquement', max_length=43, verbose_name='sous-r\xe9seau')),
                 ('delegate_reverse_dns', models.BooleanField(default=False, help_text='D\xe9l\xe9guer la r\xe9solution DNS inverse de ce sous-r\xe9seau \xe0 un ou plusieurs serveurs de noms', verbose_name='d\xe9l\xe9guer le reverse DNS')),
                 ('configuration', models.ForeignKey(related_name='ip_subnet', verbose_name='configuration', to='configuration.Configuration')),
                 ('ip_pool', models.ForeignKey(verbose_name="pool d'IP", to='resources.IPPool')),

+ 1 - 1
coin/resources/migrations/0003_auto_20150203_1043.py

@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
         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'),
+            field=netfields.fields.CidrAddressField(max_length=43, blank=True, help_text='Laisser vide pour allouer automatiquement', unique=True, verbose_name='sous-r\xe9seau'),
             preserve_default=True,
         ),
     ]

+ 15 - 17
coin/resources/models.py

@@ -4,18 +4,8 @@ from __future__ import unicode_literals
 from django.db import models
 from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator
-from django.db.models import Q
 from netfields import CidrAddressField, NetManager
-from netaddr import IPNetwork, IPSet
-
-
-def validate_subnet(cidr):
-    """Checks that a CIDR object is indeed a subnet, i.e. the host bits are
-    all set to zero."""
-    if not isinstance(cidr, IPNetwork):
-        raise ValidationError("Erreur, objet IPNetwork attendu.")
-    if cidr.ip != cidr.network:
-        raise ValidationError("{} n'est pas un sous-réseau valide, voulez-vous dire {} ?".format(cidr, cidr.cidr))
+from netaddr import IPSet
 
 
 class IPPool(models.Model):
@@ -27,8 +17,7 @@ class IPPool(models.Model):
                                                           verbose_name='taille de sous-réseau par défaut',
                                                           help_text='Taille par défaut du sous-réseau à allouer aux abonnés dans ce pool',
                                                           validators=[MaxValueValidator(64)])
-    inet = CidrAddressField(validators=[validate_subnet],
-                            verbose_name='réseau',
+    inet = CidrAddressField(verbose_name='réseau',
                             help_text="Bloc d'adresses IP du pool")
     objects = NetManager()
 
@@ -55,7 +44,7 @@ class IPPool(models.Model):
 
 
 class IPSubnet(models.Model):
-    inet = CidrAddressField(blank=True, validators=[validate_subnet],
+    inet = CidrAddressField(blank=True,
                             unique=True, verbose_name="sous-réseau",
                             help_text="Laisser vide pour allouer automatiquement")
     objects = NetManager()
@@ -95,9 +84,18 @@ class IPSubnet(models.Model):
         if not self.inet in self.ip_pool.inet:
             raise ValidationError("Le sous-réseau doit être inclus dans le bloc d'IP.")
         # Check that we don't conflict with existing subnets.
-        conflicting = self.ip_pool.ipsubnet_set.filter(Q(inet__net_contained_or_equal=self.inet) |
-                                                       Q(inet__net_contains_or_equals=self.inet)).exclude(id=self.id)
-        if conflicting:
+
+        # The optimal request would be the following commented request, but
+        # django-netfields 0.4.x seems buggy with Q-expressions. For now use
+        # two requests, but the optimal solution will have to be retried once
+        # we use django-netfields>=0.7
+
+        #conflicting = self.ip_pool.ipsubnet_set.filter(Q(inet__net_contained_or_equal=self.inet) |
+        #                                               Q(inet__net_contains_or_equals=self.inet)).exclude(id=self.id)
+        conflicting_contained = self.ip_pool.ipsubnet_set.filter(inet__net_contained_or_equal=self.inet).exclude(id=self.id)
+        conflicting_containing = self.ip_pool.ipsubnet_set.filter(inet__net_contains_or_equals=self.inet).exclude(id=self.id)
+        if conflicting_contained or conflicting_containing:
+            conflicting = conflicting_contained if conflicting_contained else conflicting_containing
             raise ValidationError("Le sous-réseau est en conflit avec des sous-réseaux existants: {}.".format(conflicting))
 
     def validate_reverse_dns(self):

+ 1 - 0
coin/settings_base.py

@@ -149,6 +149,7 @@ INSTALLED_APPS = (
     'django.contrib.staticfiles',
     # Uncomment the next line to enable the admin:
     'django.contrib.admin',
+    'netfields',
     # Uncomment the next line to enable admin documentation:
     #'django.contrib.admindocs',
     'polymorphic',

+ 8 - 0
coin/validation.py

@@ -2,6 +2,9 @@
 from __future__ import unicode_literals
 
 from django.core.exceptions import ValidationError
+from django.core.validators import RegexValidator
+
+from .utils import re_chat_url
 
 
 def validate_v4(address):
@@ -12,3 +15,8 @@ def validate_v4(address):
 def validate_v6(address):
     if address.version != 6:
         raise ValidationError('{} is not an IPv6 address'.format(address))
+
+
+chatroom_url_validator = RegexValidator(
+    regex=re_chat_url,
+    message="Enter a value of the form  <proto>://<server>/<channel>")

+ 6 - 6
requirements.txt

@@ -1,16 +1,16 @@
-Django==1.7.11
+Django>=1.8.17,<1.9
 psycopg2==2.5.2
 python-ldap==2.4.15
 wsgiref==0.1.2
 python-dateutil==2.2
-django-autocomplete-light==2.0.7
+django-autocomplete-light==2.1.1
 django-activelink==0.4
 html2text
-django-polymorphic==0.6
-django-sendfile==0.3.6
+django-polymorphic==0.7.2
+django-sendfile==0.3.10
 django-localflavor==1.1
--e git+https://code.ffdn.org/zorun/django-postgresql-netfields.git#egg=django-netfields
--e git+https://github.com/jlaine/django-ldapdb@1c4f9f29e52176f4367a1dffec2ecd2e123e2e7a#egg=django-ldapdb
+django-netfields>=0.4,<0.5
+django-ldapdb>=0.4.0,<5.0
 feedparser
 six==1.10.0
 WeasyPrint==0.31

+ 42 - 0
vpn/migrations/0001_squashed_0002_remove_vpnconfiguration_comment.py

@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import netfields.fields
+import coin.validation
+import coin.mixins
+
+
+class Migration(migrations.Migration):
+
+    replaces = [('vpn', '0001_initial'), ('vpn', '0002_remove_vpnconfiguration_comment')]
+
+    dependencies = [
+        ('configuration', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='VPNConfiguration',
+            fields=[
+                ('configuration_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='configuration.Configuration')),
+                ('activated', models.BooleanField(default=False, verbose_name='activ\xe9')),
+                ('login', models.CharField(help_text='leave empty for automatic generation', unique=True, max_length=50, verbose_name='identifiant', blank=True)),
+                ('password', models.CharField(max_length=256, null=True, verbose_name='mot de passe', blank=True)),
+                ('ipv4_endpoint', netfields.fields.InetAddressField(validators=[coin.validation.validate_v4], max_length=39, blank=True, help_text='Adresse IPv4 utilis\xe9e par d\xe9faut sur le VPN', null=True, verbose_name='IPv4')),
+                ('ipv6_endpoint', netfields.fields.InetAddressField(validators=[coin.validation.validate_v6], max_length=39, blank=True, help_text='Adresse IPv6 utilis\xe9e par d\xe9faut sur le VPN', null=True, verbose_name='IPv6')),
+            ],
+            options={
+                'verbose_name': 'VPN',
+            },
+            bases=(coin.mixins.CoinLdapSyncMixin, 'configuration.configuration'),
+        ),
+        migrations.CreateModel(
+            name='LdapVPNConfig',
+            fields=[
+            ],
+            options={
+                'managed': False,
+            },
+        ),
+    ]