#156 Reformates all the code with Black to unify it make it more PEP8-friendly

Closed
rezemika wants to merge 1 commits from rezemika/black into FFDN/master
73 changed files with 2785 additions and 1909 deletions
  1. 9 6
      coin/apps.py
  2. 66 44
      coin/billing/admin.py
  3. 43 31
      coin/billing/create_subscriptions_invoices.py
  4. 12 10
      coin/billing/management/commands/charge_subscriptions.py
  5. 77 62
      coin/billing/management/commands/import_payments_from_csv.py
  6. 0 1
      coin/billing/management/commands/send_reminders_for_unpaid_bills.py
  7. 289 185
      coin/billing/models.py
  8. 164 131
      coin/billing/tests.py
  9. 4 5
      coin/billing/urls.py
  10. 2 3
      coin/billing/utils.py
  11. 17 8
      coin/billing/views.py
  12. 14 4
      coin/configuration/admin.py
  13. 11 7
      coin/configuration/forms.py
  14. 20 16
      coin/configuration/models.py
  15. 1 1
      coin/context_processors.py
  16. 9 9
      coin/filtering_queryset.py
  17. 6 8
      coin/html2pdf.py
  18. 76 37
      coin/isp_database/admin.py
  19. 2 1
      coin/isp_database/context_processors.py
  20. 153 86
      coin/isp_database/models.py
  21. 6 4
      coin/isp_database/templatetags/isptags.py
  22. 15 12
      coin/isp_database/tests.py
  23. 160 92
      coin/members/admin.py
  24. 17 12
      coin/members/autocomplete_light_registry.py
  25. 50 19
      coin/members/forms.py
  26. 34 23
      coin/members/management/commands/call_for_membership_fees.py
  27. 23 17
      coin/members/management/commands/members_email.py
  28. 10 11
      coin/members/membershipfee_filter.py
  29. 241 170
      coin/members/models.py
  30. 154 129
      coin/members/tests.py
  31. 63 42
      coin/members/urls.py
  32. 34 27
      coin/members/views.py
  33. 10 9
      coin/mixins.py
  34. 55 28
      coin/offers/admin.py
  35. 5 2
      coin/offers/forms.py
  36. 28 11
      coin/offers/management/commands/offer_subscriptions_count.py
  37. 98 58
      coin/offers/models.py
  38. 28 22
      coin/offers/offersubscription_filter.py
  39. 7 3
      coin/offers/urls.py
  40. 35 16
      coin/offers/views.py
  41. 7 6
      coin/resources/admin.py
  42. 83 35
      coin/resources/models.py
  43. 1 0
      coin/resources/templatetags/subnets.py
  44. 2 2
      coin/reverse_dns/admin.py
  45. 38 23
      coin/reverse_dns/models.py
  46. 87 98
      coin/settings_base.py
  47. 21 21
      coin/settings_local.example-illyse.py
  48. 1 4
      coin/settings_test.py
  49. 13 18
      coin/urls.py
  50. 27 17
      coin/utils.py
  51. 4 4
      coin/validation.py
  52. 11 9
      coin/views.py
  53. 1 0
      coin/wsgi.py
  54. 58 20
      contrib/vpn_acct.py
  55. 1 1
      hardware_provisioning/__init__.py
  56. 78 56
      hardware_provisioning/admin.py
  57. 3 3
      hardware_provisioning/app.py
  58. 4 6
      hardware_provisioning/fields.py
  59. 14 14
      hardware_provisioning/forms.py
  60. 71 59
      hardware_provisioning/models.py
  61. 14 16
      hardware_provisioning/tests.py
  62. 6 6
      hardware_provisioning/urls.py
  63. 1 2
      hardware_provisioning/validators.py
  64. 42 45
      hardware_provisioning/views.py
  65. 1 0
      simple_dsl/admin.py
  66. 10 7
      simple_dsl/models.py
  67. 1 1
      vpn/__init__.py
  68. 34 13
      vpn/admin.py
  69. 2 2
      vpn/apps.py
  70. 53 32
      vpn/models.py
  71. 8 7
      vpn/tests.py
  72. 11 5
      vpn/urls.py
  73. 29 15
      vpn/views.py

+ 9 - 6
coin/apps.py

@@ -8,19 +8,21 @@ from .utils import rstrip_str
 
 
 class AppURLsMeta(type):
 class AppURLsMeta(type):
     def __init__(cls, name, bases, data):
     def __init__(cls, name, bases, data):
-        if len(bases) > 1: # execute only on leaf class
-            exported_urlpatterns = data.pop('exported_urlpatterns', None)
+        if len(bases) > 1:  # execute only on leaf class
+            exported_urlpatterns = data.pop("exported_urlpatterns", None)
 
 
             if exported_urlpatterns:
             if exported_urlpatterns:
                 cls.exported_urlpatterns = exported_urlpatterns
                 cls.exported_urlpatterns = exported_urlpatterns
             else:
             else:
                 # Default : sets
                 # Default : sets
                 #   exported_urlpatterns = [(<app_name>, <app_url_module>)]
                 #   exported_urlpatterns = [(<app_name>, <app_url_module>)]
-                current_path = '.' + rstrip_str(rstrip_str(basename(__file__), '.pyc'), '.py')
-                url_module = rstrip_str(cls.__module__, current_path) + '.urls'
-                cls.exported_urlpatterns = [(data['name'], url_module)]
+                current_path = "." + rstrip_str(
+                    rstrip_str(basename(__file__), ".pyc"), ".py"
+                )
+                url_module = rstrip_str(cls.__module__, current_path) + ".urls"
+                cls.exported_urlpatterns = [(data["name"], url_module)]
 
 
-            cls.urlprefix = data.pop('urlprefix', None)
+            cls.urlprefix = data.pop("urlprefix", None)
 
 
 
 
 class AppURLs(six.with_metaclass(AppURLsMeta)):
 class AppURLs(six.with_metaclass(AppURLsMeta)):
@@ -41,4 +43,5 @@ class AppURLs(six.with_metaclass(AppURLsMeta)):
             name = 'my_app'
             name = 'my_app'
             exported_urlpatterns = [('my_app', 'myapp.cool_urls')]
             exported_urlpatterns = [('my_app', 'myapp.cool_urls')]
     """
     """
+
     pass
     pass

+ 66 - 44
coin/billing/admin.py

@@ -17,8 +17,10 @@ import autocomplete_light
 class InvoiceDetailInline(LimitedAdminInlineMixin, admin.StackedInline):
 class InvoiceDetailInline(LimitedAdminInlineMixin, admin.StackedInline):
     model = InvoiceDetail
     model = InvoiceDetail
     extra = 0
     extra = 0
-    fields = (('label', 'amount', 'quantity', 'tax'),
-              ('offersubscription', 'period_from', 'period_to'))
+    fields = (
+        ("label", "amount", "quantity", "tax"),
+        ("offersubscription", "period_from", "period_to"),
+    )
 
 
     def get_filters(self, obj):
     def get_filters(self, obj):
         """
         """
@@ -27,13 +29,13 @@ class InvoiceDetailInline(LimitedAdminInlineMixin, admin.StackedInline):
         une liste vide
         une liste vide
         """
         """
         if obj and obj.member:
         if obj and obj.member:
-            return (('offersubscription', {'member': obj.member}),)
+            return (("offersubscription", {"member": obj.member}),)
         else:
         else:
-            return (('offersubscription', None),)
+            return (("offersubscription", None),)
 
 
     def get_readonly_fields(self, request, obj=None):
     def get_readonly_fields(self, request, obj=None):
         if not obj or not obj.member:
         if not obj or not obj.member:
-            return self.readonly_fields + ('offersubscription',)
+            return self.readonly_fields + ("offersubscription",)
         return self.readonly_fields
         return self.readonly_fields
 
 
 
 
@@ -44,6 +46,7 @@ class InvoiceDetailInlineReadOnly(admin.StackedInline):
     Ce inline est donc identique à InvoiceDetailInline, mais tous
     Ce inline est donc identique à InvoiceDetailInline, mais tous
     les champs sont en lecture seule
     les champs sont en lecture seule
     """
     """
+
     model = InvoiceDetail
     model = InvoiceDetail
     extra = 0
     extra = 0
     fields = InvoiceDetailInline.fields
     fields = InvoiceDetailInline.fields
@@ -56,11 +59,13 @@ class InvoiceDetailInlineReadOnly(admin.StackedInline):
         if self.declared_fieldsets:
         if self.declared_fieldsets:
             result = flatten_fieldsets(self.declared_fieldsets)
             result = flatten_fieldsets(self.declared_fieldsets)
         else:
         else:
-            result = list(set(
-                [field.name for field in self.opts.local_fields] +
-                [field.name for field in self.opts.local_many_to_many]
-            ))
-            result.remove('id')
+            result = list(
+                set(
+                    [field.name for field in self.opts.local_fields]
+                    + [field.name for field in self.opts.local_many_to_many]
+                )
+            )
+            result.remove("id")
         return result
         return result
 
 
 
 
@@ -82,7 +87,7 @@ class PaymentAllocatedReadOnly(admin.TabularInline):
 class PaymentInlineAdd(admin.StackedInline):
 class PaymentInlineAdd(admin.StackedInline):
     model = Payment
     model = Payment
     extra = 0
     extra = 0
-    fields = (('date', 'payment_mean', 'amount'),)
+    fields = (("date", "payment_mean", "amount"),)
     can_delete = False
     can_delete = False
 
 
     verbose_name_plural = "Ajouter des paiements"
     verbose_name_plural = "Ajouter des paiements"
@@ -92,16 +97,17 @@ class PaymentInlineAdd(admin.StackedInline):
 
 
 
 
 class InvoiceAdmin(admin.ModelAdmin):
 class InvoiceAdmin(admin.ModelAdmin):
-    list_display = ('number', 'date', 'status', 'amount', 'member',
-                    'validated')
-    list_display_links = ('number', 'date')
-    fields = (('number', 'date', 'status'),
-              ('date_due'),
-              ('member'),
-              ('amount', 'amount_paid'),
-              ('validated', 'pdf'))
-    readonly_fields = ('amount', 'amount_paid', 'validated', 'pdf', 'number')
-    form = autocomplete_light.modelform_factory(Invoice, fields='__all__')
+    list_display = ("number", "date", "status", "amount", "member", "validated")
+    list_display_links = ("number", "date")
+    fields = (
+        ("number", "date", "status"),
+        ("date_due"),
+        ("member"),
+        ("amount", "amount_paid"),
+        ("validated", "pdf"),
+    )
+    readonly_fields = ("amount", "amount_paid", "validated", "pdf", "number")
+    form = autocomplete_light.modelform_factory(Invoice, fields="__all__")
 
 
     def get_readonly_fields(self, request, obj=None):
     def get_readonly_fields(self, request, obj=None):
         """
         """
@@ -111,10 +117,12 @@ class InvoiceAdmin(admin.ModelAdmin):
             if self.declared_fieldsets:
             if self.declared_fieldsets:
                 return flatten_fieldsets(self.declared_fieldsets)
                 return flatten_fieldsets(self.declared_fieldsets)
             else:
             else:
-                return list(set(
-                    [field.name for field in self.opts.local_fields] +
-                    [field.name for field in self.opts.local_many_to_many]
-                ))
+                return list(
+                    set(
+                        [field.name for field in self.opts.local_fields]
+                        + [field.name for field in self.opts.local_many_to_many]
+                    )
+                )
         return self.readonly_fields
         return self.readonly_fields
 
 
     def get_inline_instances(self, request, obj=None):
     def get_inline_instances(self, request, obj=None):
@@ -142,9 +150,11 @@ class InvoiceAdmin(admin.ModelAdmin):
             inline = inline_class(self.model, self.admin_site)
             inline = inline_class(self.model, self.admin_site)
 
 
             if request:
             if request:
-                if not (inline.has_add_permission(request) or
-                        inline.has_change_permission(request) or
-                        inline.has_delete_permission(request)):
+                if not (
+                    inline.has_add_permission(request)
+                    or inline.has_change_permission(request)
+                    or inline.has_delete_permission(request)
+                ):
                     continue
                     continue
                 if not inline.has_add_permission(request):
                 if not inline.has_add_permission(request):
                     inline.max_num = 0
                     inline.max_num = 0
@@ -158,9 +168,11 @@ class InvoiceAdmin(admin.ModelAdmin):
         """
         """
         urls = super(InvoiceAdmin, self).get_urls()
         urls = super(InvoiceAdmin, self).get_urls()
         my_urls = [
         my_urls = [
-            url(r'^validate/(?P<id>.+)$',
+            url(
+                r"^validate/(?P<id>.+)$",
                 self.admin_site.admin_view(self.validate_view),
                 self.admin_site.admin_view(self.validate_view),
-                name='invoice_validate'),
+                name="invoice_validate",
+            )
         ]
         ]
         return my_urls + urls
         return my_urls + urls
 
 
@@ -174,18 +186,18 @@ class InvoiceAdmin(admin.ModelAdmin):
         if request.user.is_superuser:
         if request.user.is_superuser:
             invoice = get_invoice_from_id_or_number(id)
             invoice = get_invoice_from_id_or_number(id)
             if invoice.amount() == 0:
             if invoice.amount() == 0:
-                messages.error(request, 'Une facture validée ne peut pas avoir'
-                                        ' un total de 0€.')
+                messages.error(
+                    request, "Une facture validée ne peut pas avoir" " un total de 0€."
+                )
             else:
             else:
                 invoice.validate()
                 invoice.validate()
-                messages.success(request, 'La facture a été validée.')
+                messages.success(request, "La facture a été validée.")
         else:
         else:
             messages.error(
             messages.error(
-                request, 'Vous n\'avez pas l\'autorisation de valider '
-                         'une facture.')
+                request, "Vous n'avez pas l'autorisation de valider " "une facture."
+            )
 
 
-        return HttpResponseRedirect(reverse('admin:billing_invoice_change',
-                                            args=(id,)))
+        return HttpResponseRedirect(reverse("admin:billing_invoice_change", args=(id,)))
 
 
 
 
 class PaymentAllocationInlineReadOnly(admin.TabularInline):
 class PaymentAllocationInlineReadOnly(admin.TabularInline):
@@ -205,14 +217,23 @@ class PaymentAllocationInlineReadOnly(admin.TabularInline):
 
 
 class PaymentAdmin(admin.ModelAdmin):
 class PaymentAdmin(admin.ModelAdmin):
 
 
-    list_display = ('__unicode__', 'member', 'payment_mean', 'amount', 'date',
-                    'amount_already_allocated', 'label')
+    list_display = (
+        "__unicode__",
+        "member",
+        "payment_mean",
+        "amount",
+        "date",
+        "amount_already_allocated",
+        "label",
+    )
     list_display_links = ()
     list_display_links = ()
-    fields = (('member'),
-              ('amount', 'payment_mean', 'date', 'label'),
-              ('amount_already_allocated'))
-    readonly_fields = ('amount_already_allocated', 'label')
-    form = autocomplete_light.modelform_factory(Payment, fields='__all__')
+    fields = (
+        ("member"),
+        ("amount", "payment_mean", "date", "label"),
+        ("amount_already_allocated"),
+    )
+    readonly_fields = ("amount_already_allocated", "label")
+    form = autocomplete_light.modelform_factory(Payment, fields="__all__")
 
 
     def get_readonly_fields(self, request, obj=None):
     def get_readonly_fields(self, request, obj=None):
 
 
@@ -226,5 +247,6 @@ class PaymentAdmin(admin.ModelAdmin):
     def get_inline_instances(self, request, obj=None):
     def get_inline_instances(self, request, obj=None):
         return [PaymentAllocationInlineReadOnly(self.model, self.admin_site)]
         return [PaymentAllocationInlineReadOnly(self.model, self.admin_site)]
 
 
+
 admin.site.register(Invoice, InvoiceAdmin)
 admin.site.register(Invoice, InvoiceAdmin)
 admin.site.register(Payment, PaymentAdmin)
 admin.site.register(Payment, PaymentAdmin)

+ 43 - 31
coin/billing/create_subscriptions_invoices.py

@@ -21,8 +21,9 @@ def create_all_members_invoices_for_a_period(date=None):
     if date is None:
     if date is None:
         date = datetime.date.today()
         date = datetime.date.today()
     members = Member.objects.filter(
     members = Member.objects.filter(
-        Q(offersubscription__resign_date__isnull=True) |
-        Q(offersubscription__resign_date__gte=date))
+        Q(offersubscription__resign_date__isnull=True)
+        | Q(offersubscription__resign_date__gte=date)
+    )
 
 
     invoices = []
     invoices = []
 
 
@@ -33,6 +34,7 @@ def create_all_members_invoices_for_a_period(date=None):
 
 
     return invoices
     return invoices
 
 
+
 @transaction.atomic
 @transaction.atomic
 def create_member_invoice_for_a_period(member, date):
 def create_member_invoice_for_a_period(member, date):
     """
     """
@@ -43,14 +45,12 @@ def create_member_invoice_for_a_period(member, date):
     sid = transaction.savepoint()
     sid = transaction.savepoint()
 
 
     date_first_of_month = datetime.date(date.year, date.month, 1)
     date_first_of_month = datetime.date(date.year, date.month, 1)
-    date_last_of_month = (date_first_of_month + relativedelta(months=+1) -
-                          relativedelta(days=+1))
-
-    invoice = Invoice.objects.create(
-        date_due=datetime.date.today(),
-        member=member
+    date_last_of_month = (
+        date_first_of_month + relativedelta(months=+1) - relativedelta(days=+1)
     )
     )
 
 
+    invoice = Invoice.objects.create(date_due=datetime.date.today(), member=member)
+
     # Récupère les abonnements actifs du membre à la fin du mois
     # Récupère les abonnements actifs du membre à la fin du mois
     offer_subscriptions = member.get_active_subscriptions(date_last_of_month)
     offer_subscriptions = member.get_active_subscriptions(date_last_of_month)
 
 
@@ -67,22 +67,26 @@ def create_member_invoice_for_a_period(member, date):
         # Alors facture en plus les frais de mise en service
         # Alors facture en plus les frais de mise en service
         invoicedetail_test_first = InvoiceDetail.objects.filter(
         invoicedetail_test_first = InvoiceDetail.objects.filter(
             offersubscription__exact=offer_subscription.pk,
             offersubscription__exact=offer_subscription.pk,
-            invoice__member__exact=member.pk)
+            invoice__member__exact=member.pk,
+        )
         if not invoicedetail_test_first.exists():
         if not invoicedetail_test_first.exists():
             invoice.details.create(
             invoice.details.create(
                 label=offer.name + " - Frais de mise en service",
                 label=offer.name + " - Frais de mise en service",
                 amount=offer.initial_fees,
                 amount=offer.initial_fees,
                 offersubscription=offer_subscription,
                 offersubscription=offer_subscription,
                 period_from=None,
                 period_from=None,
-                period_to=None)
+                period_to=None,
+            )
 
 
         # Période de facturation de l'item par defaut
         # Période de facturation de l'item par defaut
         # - Du début du mois de la date passée en paramètre
         # - Du début du mois de la date passée en paramètre
         # - Jusqu'à la fin du mois de la période de facturation de l'offre
         # - Jusqu'à la fin du mois de la période de facturation de l'offre
         period_from = date_first_of_month
         period_from = date_first_of_month
-        period_to = (date_first_of_month +
-                     relativedelta(months=+offer.billing_period) -
-                     relativedelta(days=+1))
+        period_to = (
+            date_first_of_month
+            + relativedelta(months=+offer.billing_period)
+            - relativedelta(days=+1)
+        )
         planned_period_number_of_days = (period_to - period_from).days + 1
         planned_period_number_of_days = (period_to - period_from).days + 1
         quantity = 1
         quantity = 1
 
 
@@ -90,8 +94,10 @@ def create_member_invoice_for_a_period(member, date):
         # date de début de facturation au jour de l'ouverture de
         # date de début de facturation au jour de l'ouverture de
         # l'abonnement
         # l'abonnement
         if date_first_of_month == datetime.date(
         if date_first_of_month == datetime.date(
-                offer_subscription.subscription_date.year,
-                offer_subscription.subscription_date.month, 1):
+            offer_subscription.subscription_date.year,
+            offer_subscription.subscription_date.month,
+            1,
+        ):
             period_from = offer_subscription.subscription_date
             period_from = offer_subscription.subscription_date
 
 
         # Recherche dans les factures déjà existantes de ce membre des
         # Recherche dans les factures déjà existantes de ce membre des
@@ -102,7 +108,8 @@ def create_member_invoice_for_a_period(member, date):
             offersubscription__exact=offer_subscription.pk,
             offersubscription__exact=offer_subscription.pk,
             period_from__lte=period_from,
             period_from__lte=period_from,
             period_to__gt=period_from,
             period_to__gt=period_from,
-            invoice__member__exact=member.pk)
+            invoice__member__exact=member.pk,
+        )
 
 
         # Si une facture de ce genre existe alors ne fait rien.
         # Si une facture de ce genre existe alors ne fait rien.
         if not invoicedetail_test_before.exists():
         if not invoicedetail_test_before.exists():
@@ -115,23 +122,26 @@ def create_member_invoice_for_a_period(member, date):
                 offersubscription__exact=offer_subscription.pk,
                 offersubscription__exact=offer_subscription.pk,
                 period_from__lte=period_to,
                 period_from__lte=period_to,
                 period_from__gte=period_from,
                 period_from__gte=period_from,
-                invoice__member__exact=member.pk)
+                invoice__member__exact=member.pk,
+            )
 
 
             # Si une telle facture existe, récupère la date de début de
             # Si une telle facture existe, récupère la date de début de
             # facturation pour en faire la date de fin de facturation
             # facturation pour en faire la date de fin de facturation
             if invoicedetail_test_after.exists():
             if invoicedetail_test_after.exists():
                 invoicedetail_after = invoicedetail_test_after.first()
                 invoicedetail_after = invoicedetail_test_after.first()
-                period_to = (
-                    datetime.date(invoicedetail_after.period_from.year,
-                                  invoicedetail_after.period_from.month, 1) -
-                    relativedelta(days=+1))
+                period_to = datetime.date(
+                    invoicedetail_after.period_from.year,
+                    invoicedetail_after.period_from.month,
+                    1,
+                ) - relativedelta(days=+1)
 
 
             # Si la période de facturation varie par rapport à celle prévue par
             # Si la période de facturation varie par rapport à celle prévue par
             # l'offre, calcul au prorata en faisant varier la quantité
             # l'offre, calcul au prorata en faisant varier la quantité
             period_number_of_days = (period_to - period_from).days + 1
             period_number_of_days = (period_to - period_from).days + 1
             if planned_period_number_of_days != period_number_of_days:
             if planned_period_number_of_days != period_number_of_days:
-                quantity = (Decimal(period_number_of_days) /
-                            Decimal(planned_period_number_of_days))
+                quantity = Decimal(period_number_of_days) / Decimal(
+                    planned_period_number_of_days
+                )
 
 
             # Si durée de 0jours ou dates incohérentes, alors on ajoute pas
             # Si durée de 0jours ou dates incohérentes, alors on ajoute pas
             # (Si la period est de 0jours c'est que la facture existe déjà.)
             # (Si la period est de 0jours c'est que la facture existe déjà.)
@@ -140,23 +150,25 @@ def create_member_invoice_for_a_period(member, date):
                 # à la facture
                 # à la facture
                 label = offer.name
                 label = offer.name
                 try:
                 try:
-                    if (offer_subscription.configuration.comment):
+                    if offer_subscription.configuration.comment:
                         label += " (%s)" % offer_subscription.configuration.comment
                         label += " (%s)" % offer_subscription.configuration.comment
                 except ObjectDoesNotExist:
                 except ObjectDoesNotExist:
                     pass
                     pass
 
 
-                invoice.details.create(label=label,
-                                       amount=offer.period_fees,
-                                       quantity=quantity,
-                                       offersubscription=offer_subscription,
-                                       period_from=period_from,
-                                       period_to=period_to)
+                invoice.details.create(
+                    label=label,
+                    amount=offer.period_fees,
+                    quantity=quantity,
+                    offersubscription=offer_subscription,
+                    period_from=period_from,
+                    period_to=period_to,
+                )
 
 
     # S'il n'y a pas d'items dans la facture, ne commit pas la transaction.
     # S'il n'y a pas d'items dans la facture, ne commit pas la transaction.
     if invoice.details.count() > 0:
     if invoice.details.count() > 0:
         invoice.save()
         invoice.save()
         transaction.savepoint_commit(sid)
         transaction.savepoint_commit(sid)
-        invoice.validate() # Valide la facture et génère le PDF
+        invoice.validate()  # Valide la facture et génère le PDF
         return invoice
         return invoice
     else:
     else:
         transaction.savepoint_rollback(sid)
         transaction.savepoint_rollback(sid)

+ 12 - 10
coin/billing/management/commands/charge_subscriptions.py

@@ -4,30 +4,32 @@ from django.core.management.base import BaseCommand, CommandError
 from django.conf import settings
 from django.conf import settings
 
 
 from coin.utils import respect_language
 from coin.utils import respect_language
-from coin.billing.create_subscriptions_invoices import create_all_members_invoices_for_a_period
+from coin.billing.create_subscriptions_invoices import (
+    create_all_members_invoices_for_a_period
+)
 
 
 
 
 class Command(BaseCommand):
 class Command(BaseCommand):
-    args = '[date=2011-07-04]'
-    help = 'Create invoices for members subscriptions for date specified (or today if no date passed)'
+    args = "[date=2011-07-04]"
+    help = "Create invoices for members subscriptions for date specified (or today if no date passed)"
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
-        verbosity = int(options['verbosity'])
+        verbosity = int(options["verbosity"])
         try:
         try:
-            date = datetime.datetime.strptime(args[0], '%Y-%m-%d').date()
+            date = datetime.datetime.strptime(args[0], "%Y-%m-%d").date()
         except IndexError:
         except IndexError:
             date = datetime.date.today()
             date = datetime.date.today()
         except ValueError:
         except ValueError:
             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)"
+            )
 
 
         if verbosity >= 2:
         if verbosity >= 2:
             self.stdout.write(
             self.stdout.write(
-                'Create invoices for all members for the date : %s' % date)
+                "Create invoices for all members for the date : %s" % date
+            )
         with respect_language(settings.LANGUAGE_CODE):
         with respect_language(settings.LANGUAGE_CODE):
             invoices = create_all_members_invoices_for_a_period(date)
             invoices = create_all_members_invoices_for_a_period(date)
 
 
         if len(invoices) > 0 or verbosity >= 2:
         if len(invoices) > 0 or verbosity >= 2:
-            self.stdout.write(
-                u'%d invoices were created' % len(invoices))
-
+            self.stdout.write(u"%d invoices were created" % len(invoices))

+ 77 - 62
coin/billing/management/commands/import_payments_from_csv.py

@@ -38,14 +38,15 @@ from coin.billing.models import Payment
 # Parser / import / matcher configuration
 # Parser / import / matcher configuration
 
 
 # The CSV delimiter
 # The CSV delimiter
-DELIMITER=str(';')
+DELIMITER = str(";")
 # The date format in the CSV
 # The date format in the CSV
-DATE_FORMAT="%d/%m/%Y"
+DATE_FORMAT = "%d/%m/%Y"
 # The default regex used to match the label of a payment with a member ID
 # The default regex used to match the label of a payment with a member ID
-ID_REGEX=r"(?i)(\b|_)ID[\s\-\_\/]*(\d+)(\b|_)"
+ID_REGEX = r"(?i)(\b|_)ID[\s\-\_\/]*(\d+)(\b|_)"
 # If the label of the payment contains one of these, the payment won't be
 # If the label of the payment contains one of these, the payment won't be
 # matched to a member when importing it.
 # matched to a member when importing it.
-KEYWORDS_TO_NOTMATCH=[ "DON", "MECENAT", "REM CHQ" ]
+KEYWORDS_TO_NOTMATCH = ["DON", "MECENAT", "REM CHQ"]
+
 
 
 class Command(BaseCommand):
 class Command(BaseCommand):
 
 
@@ -58,21 +59,16 @@ class Command(BaseCommand):
 
 
     def add_arguments(self, parser):
     def add_arguments(self, parser):
 
 
-        parser.add_argument(
-            'filename',
-            type=str,
-            help="The CSV filename to be parsed"
-        )
+        parser.add_argument("filename", type=str, help="The CSV filename to be parsed")
 
 
         parser.add_argument(
         parser.add_argument(
-            '--commit',
-            action='store_true',
-            dest='commit',
+            "--commit",
+            action="store_true",
+            dest="commit",
             default=False,
             default=False,
-            help='Agree with the proposed change and commit them'
+            help="Agree with the proposed change and commit them",
         )
         )
 
 
-
     def handle(self, *args, **options):
     def handle(self, *args, **options):
 
 
         assert options["filename"] != ""
         assert options["filename"] != ""
@@ -80,24 +76,35 @@ class Command(BaseCommand):
         if not os.path.isfile(options["filename"]):
         if not os.path.isfile(options["filename"]):
             raise CommandError("This file does not exists.")
             raise CommandError("This file does not exists.")
 
 
-        payments = self.convert_csv_to_dicts(self.clean_csv(self.load_csv(options["filename"])))
+        payments = self.convert_csv_to_dicts(
+            self.clean_csv(self.load_csv(options["filename"]))
+        )
 
 
         payments = self.try_to_match_payment_with_members(payments)
         payments = self.try_to_match_payment_with_members(payments)
         new_payments = self.filter_already_known_payments(payments)
         new_payments = self.filter_already_known_payments(payments)
         new_payments = self.unmatch_payment_with_keywords(new_payments)
         new_payments = self.unmatch_payment_with_keywords(new_payments)
 
 
-        number_of_already_known_payments = len(payments)-len(new_payments)
+        number_of_already_known_payments = len(payments) - len(new_payments)
         number_of_new_payments = len(new_payments)
         number_of_new_payments = len(new_payments)
 
 
-        if (number_of_new_payments > 0) :
+        if number_of_new_payments > 0:
             print("======================================================")
             print("======================================================")
             print("   > New payments found")
             print("   > New payments found")
-            print(json.dumps(new_payments, indent=4, separators=(',', ': ')))
+            print(json.dumps(new_payments, indent=4, separators=(",", ": ")))
         print("======================================================")
         print("======================================================")
-        print("Number of already known payments found : " + str(number_of_already_known_payments))
+        print(
+            "Number of already known payments found : "
+            + str(number_of_already_known_payments)
+        )
         print("Number of new payments found           : " + str(number_of_new_payments))
         print("Number of new payments found           : " + str(number_of_new_payments))
-        print("Number of new payments matched         : " + str(len([p for p in new_payments if     p["member_matched"]])))
-        print("Number of payments not matched         : " + str(len([p for p in new_payments if not p["member_matched"]])))
+        print(
+            "Number of new payments matched         : "
+            + str(len([p for p in new_payments if p["member_matched"]]))
+        )
+        print(
+            "Number of payments not matched         : "
+            + str(len([p for p in new_payments if not p["member_matched"]]))
+        )
         print("======================================================")
         print("======================================================")
 
 
         if number_of_new_payments == 0:
         if number_of_new_payments == 0:
@@ -105,12 +112,13 @@ class Command(BaseCommand):
             return
             return
 
 
         if not options["commit"]:
         if not options["commit"]:
-            print("Please carefully review the matches, then if everything \n" \
-                  "looks alright, use --commit to register these new payments.")
+            print(
+                "Please carefully review the matches, then if everything \n"
+                "looks alright, use --commit to register these new payments."
+            )
         else:
         else:
             self.add_new_payments(new_payments)
             self.add_new_payments(new_payments)
 
 
-
     def is_date(self, text):
     def is_date(self, text):
         try:
         try:
             datetime.datetime.strptime(text, DATE_FORMAT)
             datetime.datetime.strptime(text, DATE_FORMAT)
@@ -118,20 +126,17 @@ class Command(BaseCommand):
         except ValueError:
         except ValueError:
             return False
             return False
 
 
-
     def is_money_amount(self, text):
     def is_money_amount(self, text):
         try:
         try:
-            float(text.replace(",","."))
+            float(text.replace(",", "."))
             return True
             return True
         except ValueError:
         except ValueError:
             return False
             return False
 
 
-
     def load_csv(self, filename):
     def load_csv(self, filename):
         with open(filename, "r") as f:
         with open(filename, "r") as f:
             return list(csv.reader(f, delimiter=DELIMITER))
             return list(csv.reader(f, delimiter=DELIMITER))
 
 
-
     def clean_csv(self, data):
     def clean_csv(self, data):
 
 
         output = []
         output = []
@@ -139,13 +144,15 @@ class Command(BaseCommand):
         for i, row in enumerate(data):
         for i, row in enumerate(data):
 
 
             for j in range(len(row)):
             for j in range(len(row)):
-                row[j] = row[j].decode('utf-8')
+                row[j] = row[j].decode("utf-8")
 
 
             if len(row) < 4:
             if len(row) < 4:
                 continue
                 continue
 
 
             if not self.is_date(row[0]):
             if not self.is_date(row[0]):
-                logging.warning("Ignoring the following row (bad format for date in the first column) :")
+                logging.warning(
+                    "Ignoring the following row (bad format for date in the first column) :"
+                )
                 logging.warning(str(row))
                 logging.warning(str(row))
                 continue
                 continue
 
 
@@ -155,22 +162,25 @@ class Command(BaseCommand):
                 continue
                 continue
 
 
             if not self.is_money_amount(row[3]):
             if not self.is_money_amount(row[3]):
-                logging.warning("Ignoring the following row (bad format for money amount in colun three) :")
+                logging.warning(
+                    "Ignoring the following row (bad format for money amount in colun three) :"
+                )
                 logging.warning(str(row))
                 logging.warning(str(row))
                 continue
                 continue
 
 
             # Clean the date
             # Clean the date
-            row[0] = datetime.datetime.strptime(row[0], DATE_FORMAT).strftime("%Y-%m-%d")
+            row[0] = datetime.datetime.strptime(row[0], DATE_FORMAT).strftime(
+                "%Y-%m-%d"
+            )
 
 
             # Clean the label ...
             # Clean the label ...
-            row[4] = row[4].replace('\r', ' ')
-            row[4] = row[4].replace('\n', ' ')
+            row[4] = row[4].replace("\r", " ")
+            row[4] = row[4].replace("\n", " ")
 
 
             output.append(row)
             output.append(row)
 
 
         return output
         return output
 
 
-
     def convert_csv_to_dicts(self, data):
     def convert_csv_to_dicts(self, data):
 
 
         output = []
         output = []
@@ -180,13 +190,12 @@ class Command(BaseCommand):
 
 
             payment["date"] = row[0]
             payment["date"] = row[0]
             payment["label"] = row[4]
             payment["label"] = row[4]
-            payment["amount"] = float(row[3].replace(",","."))
+            payment["amount"] = float(row[3].replace(",", "."))
 
 
             output.append(payment)
             output.append(payment)
 
 
         return output
         return output
 
 
-
     def try_to_match_payment_with_members(self, payments):
     def try_to_match_payment_with_members(self, payments):
 
 
         members = Member.objects.filter(status="member")
         members = Member.objects.filter(status="member")
@@ -201,18 +210,20 @@ class Command(BaseCommand):
             idmatches = idregex.findall(payment_label)
             idmatches = idregex.findall(payment_label)
             if len(idmatches) == 1:
             if len(idmatches) == 1:
                 i = int(idmatches[0][1])
                 i = int(idmatches[0][1])
-                member_matches = [ member.username for member in members if member.pk==i ]
+                member_matches = [
+                    member.username for member in members if member.pk == i
+                ]
                 if len(member_matches) == 1:
                 if len(member_matches) == 1:
                     payment["member_matched"] = member_matches[0]
                     payment["member_matched"] = member_matches[0]
-                    #print("Matched by ID to "+member_matches[0])
+                    # print("Matched by ID to "+member_matches[0])
                     continue
                     continue
 
 
-
             # Second, attempt to find the username
             # Second, attempt to find the username
             usernamematch = None
             usernamematch = None
             for member in members:
             for member in members:
-                matches = re.compile(r"(?i)(\b|_)"+re.escape(member.username)+r"(\b|_)") \
-                            .findall(payment_label)
+                matches = re.compile(
+                    r"(?i)(\b|_)" + re.escape(member.username) + r"(\b|_)"
+                ).findall(payment_label)
                 # If not found, try next
                 # If not found, try next
                 if len(matches) == 0:
                 if len(matches) == 0:
                     continue
                     continue
@@ -226,18 +237,18 @@ class Command(BaseCommand):
 
 
             if usernamematch != None:
             if usernamematch != None:
                 payment["member_matched"] = usernamematch
                 payment["member_matched"] = usernamematch
-                #print("Matched by username to "+usernamematch)
+                # print("Matched by username to "+usernamematch)
                 continue
                 continue
 
 
-
             # Third, attempt to match by family name
             # Third, attempt to match by family name
             familynamematch = None
             familynamematch = None
             for member in members:
             for member in members:
                 if member.last_name == "":
                 if member.last_name == "":
                     continue
                     continue
 
 
-                matches = re.compile(r"(?i)(\b|_)"+re.escape(str(member.last_name))+r"(\b|_)") \
-                            .findall(payment_label)
+                matches = re.compile(
+                    r"(?i)(\b|_)" + re.escape(str(member.last_name)) + r"(\b|_)"
+                ).findall(payment_label)
                 # If not found, try next
                 # If not found, try next
                 if len(matches) == 0:
                 if len(matches) == 0:
                     continue
                     continue
@@ -256,20 +267,21 @@ class Command(BaseCommand):
 
 
             if familynamematch != None:
             if familynamematch != None:
                 payment["member_matched"] = usernamematch
                 payment["member_matched"] = usernamematch
-                #print("Matched by familyname to "+familynamematch)
+                # print("Matched by familyname to "+familynamematch)
                 continue
                 continue
 
 
-            #print("Could not match")
+            # print("Could not match")
             payment["member_matched"] = None
             payment["member_matched"] = None
 
 
         return payments
         return payments
 
 
-
     def unmatch_payment_with_keywords(self, payments):
     def unmatch_payment_with_keywords(self, payments):
 
 
         matchers = {}
         matchers = {}
         for keyword in KEYWORDS_TO_NOTMATCH:
         for keyword in KEYWORDS_TO_NOTMATCH:
-            matchers[keyword] = re.compile(r"(?i)(\b|_|-)"+re.escape(keyword)+r"(\b|_|-)")
+            matchers[keyword] = re.compile(
+                r"(?i)(\b|_|-)" + re.escape(keyword) + r"(\b|_|-)"
+            )
 
 
         for i, payment in enumerate(payments):
         for i, payment in enumerate(payments):
 
 
@@ -284,9 +296,10 @@ class Command(BaseCommand):
                 if len(matches) == 0:
                 if len(matches) == 0:
                     continue
                     continue
 
 
-                print("Ignoring possible match for payment '%s' because " \
-                      "it contains the keyword %s"                        \
-                      % (payment["label"], keyword))
+                print(
+                    "Ignoring possible match for payment '%s' because "
+                    "it contains the keyword %s" % (payment["label"], keyword)
+                )
                 payments[i]["member_matched"] = None
                 payments[i]["member_matched"] = None
 
 
                 break
                 break
@@ -304,9 +317,11 @@ class Command(BaseCommand):
             found_match = False
             found_match = False
             for known_payment in known_payments:
             for known_payment in known_payments:
 
 
-                if  (str(known_payment.date) == payment["date"].encode('utf-8')) \
-                and (known_payment.label == payment["label"]) \
-                and (float(known_payment.amount) == float(payment["amount"])):
+                if (
+                    (str(known_payment.date) == payment["date"].encode("utf-8"))
+                    and (known_payment.label == payment["label"])
+                    and (float(known_payment.amount) == float(payment["amount"]))
+                ):
                     found_match = True
                     found_match = True
                     break
                     break
 
 
@@ -315,7 +330,6 @@ class Command(BaseCommand):
 
 
         return new_payments
         return new_payments
 
 
-
     def add_new_payments(self, new_payments):
     def add_new_payments(self, new_payments):
 
 
         for new_payment in new_payments:
         for new_payment in new_payments:
@@ -331,8 +345,9 @@ class Command(BaseCommand):
             print(new_payment)
             print(new_payment)
 
 
             # Create the payment
             # Create the payment
-            payment = Payment.objects.create(amount=float(new_payment["amount"]),
-                                             label=new_payment["label"],
-                                             date=new_payment["date"],
-                                             member=member)
-
+            payment = Payment.objects.create(
+                amount=float(new_payment["amount"]),
+                label=new_payment["label"],
+                date=new_payment["date"],
+                member=member,
+            )

+ 0 - 1
coin/billing/management/commands/send_reminders_for_unpaid_bills.py

@@ -34,4 +34,3 @@ weeks.
                 continue
                 continue
 
 
             invoice.send_reminder(auto=True)
             invoice.send_reminder(auto=True)
-

+ 289 - 185
coin/billing/models.py

@@ -20,9 +20,14 @@ from django.core.urlresolvers import reverse
 from coin.offers.models import OfferSubscription
 from coin.offers.models import OfferSubscription
 from coin.members.models import Member
 from coin.members.models import Member
 from coin.html2pdf import render_as_pdf
 from coin.html2pdf import render_as_pdf
-from coin.utils import private_files_storage, start_of_month, end_of_month, \
-                       postgresql_regexp, send_templated_email,             \
-                       disable_for_loaddata
+from coin.utils import (
+    private_files_storage,
+    start_of_month,
+    end_of_month,
+    postgresql_regexp,
+    send_templated_email,
+    disable_for_loaddata,
+)
 from coin.isp_database.context_processors import branding
 from coin.isp_database.context_processors import branding
 from coin.isp_database.models import ISPInfo
 from coin.isp_database.models import ISPInfo
 
 
@@ -32,9 +37,7 @@ accounting_log = logging.getLogger("coin.billing")
 def invoice_pdf_filename(instance, filename):
 def invoice_pdf_filename(instance, filename):
     """Nom et chemin du fichier pdf à stocker pour les factures"""
     """Nom et chemin du fichier pdf à stocker pour les factures"""
     member_id = instance.member.id if instance.member else 0
     member_id = instance.member.id if instance.member else 0
-    return 'invoices/%d_%s_%s.pdf' % (member_id,
-                                      instance.number,
-                                      uuid.uuid4())
+    return "invoices/%d_%s_%s.pdf" % (member_id, instance.number, uuid.uuid4())
 
 
 
 
 @python_2_unicode_compatible
 @python_2_unicode_compatible
@@ -50,8 +53,8 @@ class InvoiceNumber:
     - MM month of the bill
     - MM month of the bill
     - XXXXXX a per-month sequence
     - XXXXXX a per-month sequence
     """
     """
-    RE_INVOICE_NUMBER = re.compile(
-        r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<index>\d{6})')
+
+    RE_INVOICE_NUMBER = re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<index>\d{6})")
 
 
     def __init__(self, date, index):
     def __init__(self, date, index):
         self.date = date
         self.date = date
@@ -61,7 +64,7 @@ class InvoiceNumber:
         return InvoiceNumber(self.date, self.index + 1)
         return InvoiceNumber(self.date, self.index + 1)
 
 
     def __str__(self):
     def __str__(self):
-        return '{:%Y-%m}-{:0>6}'.format(self.date, self.index)
+        return "{:%Y-%m}-{:0>6}".format(self.date, self.index)
 
 
     @classmethod
     @classmethod
     def parse(cls, string):
     def parse(cls, string):
@@ -71,13 +74,13 @@ class InvoiceNumber:
 
 
         return cls(
         return cls(
             datetime.date(
             datetime.date(
-                year=int(m.group('year')),
-                month=int(m.group('month')),
-                day=1),
-            int(m.group('index')))
+                year=int(m.group("year")), month=int(m.group("month")), day=1
+            ),
+            int(m.group("index")),
+        )
 
 
     @staticmethod
     @staticmethod
-    def time_sequence_filter(date, field_name='date'):
+    def time_sequence_filter(date, field_name="date"):
         """ Build queryset filter to be used to get the invoices from the
         """ Build queryset filter to be used to get the invoices from the
         numbering sequence of a given date.
         numbering sequence of a given date.
 
 
@@ -88,8 +91,8 @@ class InvoiceNumber:
         """
         """
 
 
         return {
         return {
-            '{}__month'.format(field_name): date.month,
-            '{}__year'.format(field_name): date.year
+            "{}__month".format(field_name): date.month,
+            "{}__year".format(field_name): date.year,
         }
         }
 
 
 
 
@@ -107,58 +110,82 @@ class InvoiceQuerySet(models.QuerySet):
 
 
     def _get_last_invoice_number(self, date):
     def _get_last_invoice_number(self, date):
         same_seq_filter = InvoiceNumber.time_sequence_filter(date)
         same_seq_filter = InvoiceNumber.time_sequence_filter(date)
-        return self.filter(**same_seq_filter).with_valid_number().aggregate(
-            models.Max('number'))['number__max']
+        return (
+            self.filter(**same_seq_filter)
+            .with_valid_number()
+            .aggregate(models.Max("number"))["number__max"]
+        )
 
 
     def with_valid_number(self):
     def with_valid_number(self):
         """ Excludes previous numbering schemes or draft invoices
         """ Excludes previous numbering schemes or draft invoices
         """
         """
-        return self.filter(number__regex=postgresql_regexp(
-            InvoiceNumber.RE_INVOICE_NUMBER))
+        return self.filter(
+            number__regex=postgresql_regexp(InvoiceNumber.RE_INVOICE_NUMBER)
+        )
 
 
 
 
 class Invoice(models.Model):
 class Invoice(models.Model):
 
 
     INVOICES_STATUS_CHOICES = (
     INVOICES_STATUS_CHOICES = (
-        ('open', 'À payer'),
-        ('closed', 'Réglée'),
-        ('trouble', 'Litige')
+        ("open", "À payer"),
+        ("closed", "Réglée"),
+        ("trouble", "Litige"),
     )
     )
 
 
-    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,
-                              unique=True,
-                              verbose_name='numéro')
-    status = models.CharField(max_length=50, choices=INVOICES_STATUS_CHOICES,
-                              default='open',
-                              verbose_name='statut')
+    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, unique=True, verbose_name="numéro")
+    status = models.CharField(
+        max_length=50,
+        choices=INVOICES_STATUS_CHOICES,
+        default="open",
+        verbose_name="statut",
+    )
     date = models.DateField(
     date = models.DateField(
-        default=datetime.date.today, null=True, verbose_name='date',
-        help_text='Cette date sera définie à la date de validation dans la facture finale')
+        default=datetime.date.today,
+        null=True,
+        verbose_name="date",
+        help_text="Cette date sera définie à la date de validation dans la facture finale",
+    )
     date_due = models.DateField(
     date_due = models.DateField(
-        null=True, blank=True,
+        null=True,
+        blank=True,
         verbose_name="date d'échéance de paiement",
         verbose_name="date d'échéance de paiement",
-        help_text='Le délai de paiement sera fixé à {} jours à la validation si laissé vide'.format(settings.PAYMENT_DELAY))
-    member = models.ForeignKey(Member, null=True, blank=True, default=None,
-                               related_name='invoices',
-                               verbose_name='membre',
-                               on_delete=models.SET_NULL)
-    pdf = models.FileField(storage=private_files_storage,
-                           upload_to=invoice_pdf_filename,
-                           null=True, blank=True,
-                           verbose_name='PDF')
-
-    date_last_reminder_email = models.DateTimeField(null=True, blank=True,
-                        verbose_name="Date du dernier email de relance envoyé")
+        help_text="Le délai de paiement sera fixé à {} jours à la validation si laissé vide".format(
+            settings.PAYMENT_DELAY
+        ),
+    )
+    member = models.ForeignKey(
+        Member,
+        null=True,
+        blank=True,
+        default=None,
+        related_name="invoices",
+        verbose_name="membre",
+        on_delete=models.SET_NULL,
+    )
+    pdf = models.FileField(
+        storage=private_files_storage,
+        upload_to=invoice_pdf_filename,
+        null=True,
+        blank=True,
+        verbose_name="PDF",
+    )
+
+    date_last_reminder_email = models.DateTimeField(
+        null=True, blank=True, verbose_name="Date du dernier email de relance envoyé"
+    )
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
         # First save to get a PK
         # First save to get a PK
         super(Invoice, self).save(*args, **kwargs)
         super(Invoice, self).save(*args, **kwargs)
         # Then use that pk to build draft invoice number
         # Then use that pk to build draft invoice number
         if not self.validated and self.pk and not self.number:
         if not self.validated and self.pk and not self.number:
-            self.number = 'DRAFT-{}'.format(self.pk)
+            self.number = "DRAFT-{}".format(self.pk)
             self.save()
             self.save()
 
 
     def amount(self):
     def amount(self):
@@ -166,38 +193,42 @@ class Invoice(models.Model):
         Calcul le montant de la facture
         Calcul le montant de la facture
         en fonction des éléments de détails
         en fonction des éléments de détails
         """
         """
-        total = Decimal('0.0')
+        total = Decimal("0.0")
         for detail in self.details.all():
         for detail in self.details.all():
             total += detail.total()
             total += detail.total()
-        return total.quantize(Decimal('0.01'))
-    amount.short_description = 'Montant'
+        return total.quantize(Decimal("0.01"))
+
+    amount.short_description = "Montant"
 
 
     def amount_before_tax(self):
     def amount_before_tax(self):
-        total = Decimal('0.0')
+        total = Decimal("0.0")
         for detail in self.details.all():
         for detail in self.details.all():
             total += detail.amount
             total += detail.amount
-        return total.quantize(Decimal('0.01'))
-    amount_before_tax.short_description = 'Montant HT'
+        return total.quantize(Decimal("0.01"))
+
+    amount_before_tax.short_description = "Montant HT"
 
 
     def amount_paid(self):
     def amount_paid(self):
         """
         """
         Calcul le montant déjà payé à partir des allocations de paiements
         Calcul le montant déjà payé à partir des allocations de paiements
         """
         """
         return sum([a.amount for a in self.allocations.all()])
         return sum([a.amount for a in self.allocations.all()])
-    amount_paid.short_description = 'Montant payé'
+
+    amount_paid.short_description = "Montant payé"
 
 
     def amount_remaining_to_pay(self):
     def amount_remaining_to_pay(self):
         """
         """
         Calcul le montant restant à payer
         Calcul le montant restant à payer
         """
         """
         return self.amount() - self.amount_paid()
         return self.amount() - self.amount_paid()
-    amount_remaining_to_pay.short_description = 'Reste à payer'
+
+    amount_remaining_to_pay.short_description = "Reste à payer"
 
 
     def has_owner(self, username):
     def has_owner(self, username):
         """
         """
         Check if passed username (ex gmajax) is owner of the invoice
         Check if passed username (ex gmajax) is owner of the invoice
         """
         """
-        return (self.member and self.member.username == username)
+        return self.member and self.member.username == username
 
 
     def generate_pdf(self):
     def generate_pdf(self):
         """
         """
@@ -205,8 +236,8 @@ class Invoice(models.Model):
         """
         """
         context = {"invoice": self}
         context = {"invoice": self}
         context.update(branding(None))
         context.update(branding(None))
-        pdf_file = render_as_pdf('billing/invoice_pdf.html', context)
-        self.pdf.save('%s.pdf' % self.number, pdf_file)
+        pdf_file = render_as_pdf("billing/invoice_pdf.html", context)
+        self.pdf.save("%s.pdf" % self.number, pdf_file)
 
 
     @transaction.atomic
     @transaction.atomic
     def validate(self):
     def validate(self):
@@ -225,26 +256,25 @@ class Invoice(models.Model):
         self.generate_pdf()
         self.generate_pdf()
 
 
         accounting_log.info(
         accounting_log.info(
-            "Draft invoice {} validated as invoice {}. ".format(
-                old_number, self.number) +
-            "(Total amount : {} ; Member : {})".format(
-                self.amount(), self.member))
+            "Draft invoice {} validated as invoice {}. ".format(old_number, self.number)
+            + "(Total amount : {} ; Member : {})".format(self.amount(), self.member)
+        )
         assert self.pdf_exists()
         assert self.pdf_exists()
         if self.member is not None:
         if self.member is not None:
             update_accounting_for_member(self.member)
             update_accounting_for_member(self.member)
 
 
-
     def pdf_exists(self):
     def pdf_exists(self):
-        return (self.validated
-                and bool(self.pdf)
-                and private_files_storage.exists(self.pdf.name))
+        return (
+            self.validated
+            and bool(self.pdf)
+            and private_files_storage.exists(self.pdf.name)
+        )
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return reverse('billing:invoice', args=[self.number])
+        return reverse("billing:invoice", args=[self.number])
 
 
     def __unicode__(self):
     def __unicode__(self):
-        return '#{} {:0.2f}€ {}'.format(
-            self.number, self.amount(), self.date_due)
+        return "#{} {:0.2f}€ {}".format(self.number, self.amount(), self.date_due)
 
 
     def reminder_needed(self):
     def reminder_needed(self):
 
 
@@ -253,17 +283,17 @@ class Invoice(models.Model):
             return False
             return False
 
 
         # If bill is close or not validated yet, nope
         # If bill is close or not validated yet, nope
-        if self.status != 'open' or not self.validated:
+        if self.status != "open" or not self.validated:
             return False
             return False
 
 
         # If bill is not at least one month old, nope
         # If bill is not at least one month old, nope
-        if self.date_due >= timezone.now()+relativedelta(weeks=-4):
+        if self.date_due >= timezone.now() + relativedelta(weeks=-4):
             return False
             return False
 
 
         # If a reminder has been recently sent, nope
         # If a reminder has been recently sent, nope
-        if (self.date_last_reminder_email
-            and (self.date_last_reminder_email
-                 >= timezone.now() + relativedelta(weeks=-3))):
+        if self.date_last_reminder_email and (
+            self.date_last_reminder_email >= timezone.now() + relativedelta(weeks=-3)
+        ):
             return False
             return False
 
 
         return True
         return True
@@ -281,25 +311,31 @@ class Invoice(models.Model):
 
 
         accounting_log.info(
         accounting_log.info(
             "Sending reminder email to {} to pay invoice {}".format(
             "Sending reminder email to {} to pay invoice {}".format(
-                self.member, str(self.number)))
+                self.member, str(self.number)
+            )
+        )
 
 
         isp_info = ISPInfo.objects.first()
         isp_info = ISPInfo.objects.first()
         kwargs = {}
         kwargs = {}
         # Il peut ne pas y avir d'ISPInfo, ou bien pas d'administrative_email
         # Il peut ne pas y avir d'ISPInfo, ou bien pas d'administrative_email
         if isp_info and isp_info.administrative_email:
         if isp_info and isp_info.administrative_email:
-            kwargs['from_email'] = isp_info.administrative_email
+            kwargs["from_email"] = isp_info.administrative_email
 
 
         # Si le dernier courriel de relance a été envoyé il y a moins de trois
         # Si le dernier courriel de relance a été envoyé il y a moins de trois
         # semaines, n'envoi pas un nouveau courriel
         # semaines, n'envoi pas un nouveau courriel
         send_templated_email(
         send_templated_email(
             to=self.member.email,
             to=self.member.email,
-            subject_template='billing/emails/reminder_for_unpaid_bill.txt',
-            body_template='billing/emails/reminder_for_unpaid_bill.html',
-            context={'member': self.member, 'branding': isp_info,
-                     'membership_info_url': settings.MEMBER_MEMBERSHIP_INFO_URL,
-                     'today': datetime.date.today,
-                     'auto_sent': auto},
-            **kwargs)
+            subject_template="billing/emails/reminder_for_unpaid_bill.txt",
+            body_template="billing/emails/reminder_for_unpaid_bill.html",
+            context={
+                "member": self.member,
+                "branding": isp_info,
+                "membership_info_url": settings.MEMBER_MEMBERSHIP_INFO_URL,
+                "today": datetime.date.today,
+                "auto_sent": auto,
+            },
+            **kwargs
+        )
 
 
         # Sauvegarde en base la date du dernier envoi de mail de relance
         # Sauvegarde en base la date du dernier envoi de mail de relance
         self.date_last_reminder_email = timezone.now()
         self.date_last_reminder_email = timezone.now()
@@ -307,7 +343,7 @@ class Invoice(models.Model):
         return True
         return True
 
 
     class Meta:
     class Meta:
-        verbose_name = 'facture'
+        verbose_name = "facture"
 
 
     objects = InvoiceQuerySet().as_manager()
     objects = InvoiceQuerySet().as_manager()
 
 
@@ -315,71 +351,95 @@ class Invoice(models.Model):
 class InvoiceDetail(models.Model):
 class InvoiceDetail(models.Model):
 
 
     label = models.CharField(max_length=100)
     label = models.CharField(max_length=100)
-    amount = models.DecimalField(max_digits=5, decimal_places=2,
-                                 verbose_name='montant')
-    quantity = models.DecimalField(null=True, verbose_name='quantité',
-                                   default=1.0, decimal_places=2, max_digits=4)
-    tax = models.DecimalField(null=True, default=0.0, decimal_places=2,
-                              max_digits=4, verbose_name='TVA',
-                              help_text='en %')
-    invoice = models.ForeignKey(Invoice, verbose_name='facture',
-                                related_name='details')
-    offersubscription = models.ForeignKey(OfferSubscription, null=True,
-                                          blank=True, default=None,
-                                          verbose_name='abonnement')
+    amount = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="montant")
+    quantity = models.DecimalField(
+        null=True, verbose_name="quantité", default=1.0, decimal_places=2, max_digits=4
+    )
+    tax = models.DecimalField(
+        null=True,
+        default=0.0,
+        decimal_places=2,
+        max_digits=4,
+        verbose_name="TVA",
+        help_text="en %",
+    )
+    invoice = models.ForeignKey(Invoice, verbose_name="facture", related_name="details")
+    offersubscription = models.ForeignKey(
+        OfferSubscription,
+        null=True,
+        blank=True,
+        default=None,
+        verbose_name="abonnement",
+    )
     period_from = models.DateField(
     period_from = models.DateField(
         default=start_of_month,
         default=start_of_month,
         null=True,
         null=True,
         blank=True,
         blank=True,
-        verbose_name='début de période',
-        help_text='Date de début de période sur laquelle est facturé cet item')
+        verbose_name="début de période",
+        help_text="Date de début de période sur laquelle est facturé cet item",
+    )
     period_to = models.DateField(
     period_to = models.DateField(
         default=end_of_month,
         default=end_of_month,
         null=True,
         null=True,
         blank=True,
         blank=True,
-        verbose_name='fin de période',
-        help_text='Date de fin de période sur laquelle est facturé cet item')
+        verbose_name="fin de période",
+        help_text="Date de fin de période sur laquelle est facturé cet item",
+    )
 
 
     def __unicode__(self):
     def __unicode__(self):
         return self.label
         return self.label
 
 
     def total(self):
     def total(self):
         """Calcul le total"""
         """Calcul le total"""
-        return (self.amount * (self.tax / Decimal('100.0') +
-                               Decimal('1.0')) *
-                self.quantity).quantize(Decimal('0.01'))
+        return (
+            self.amount * (self.tax / Decimal("100.0") + Decimal("1.0")) * self.quantity
+        ).quantize(Decimal("0.01"))
 
 
     class Meta:
     class Meta:
-        verbose_name = 'détail de facture'
+        verbose_name = "détail de facture"
 
 
 
 
 class Payment(models.Model):
 class Payment(models.Model):
 
 
     PAYMENT_MEAN_CHOICES = (
     PAYMENT_MEAN_CHOICES = (
-        ('cash', 'Espèces'),
-        ('check', 'Chèque'),
-        ('transfer', 'Virement'),
-        ('other', 'Autre')
+        ("cash", "Espèces"),
+        ("check", "Chèque"),
+        ("transfer", "Virement"),
+        ("other", "Autre"),
     )
     )
 
 
-    member = models.ForeignKey(Member, null=True, blank=True, default=None,
-                               related_name='payments',
-                               verbose_name='membre',
-                               on_delete=models.SET_NULL)
-
-    payment_mean = models.CharField(max_length=100, null=True,
-                                    default='transfer',
-                                    choices=PAYMENT_MEAN_CHOICES,
-                                    verbose_name='moyen de paiement')
-    amount = models.DecimalField(max_digits=5, decimal_places=2, null=True,
-                                 verbose_name='montant')
+    member = models.ForeignKey(
+        Member,
+        null=True,
+        blank=True,
+        default=None,
+        related_name="payments",
+        verbose_name="membre",
+        on_delete=models.SET_NULL,
+    )
+
+    payment_mean = models.CharField(
+        max_length=100,
+        null=True,
+        default="transfer",
+        choices=PAYMENT_MEAN_CHOICES,
+        verbose_name="moyen de paiement",
+    )
+    amount = models.DecimalField(
+        max_digits=5, decimal_places=2, null=True, verbose_name="montant"
+    )
     date = models.DateField(default=datetime.date.today)
     date = models.DateField(default=datetime.date.today)
-    invoice = models.ForeignKey(Invoice, verbose_name='facture associée', null=True,
-                                blank=True, related_name='payments')
+    invoice = models.ForeignKey(
+        Invoice,
+        verbose_name="facture associée",
+        null=True,
+        blank=True,
+        related_name="payments",
+    )
 
 
-    label = models.CharField(max_length=500,
-                             null=True, blank=True, default="",
-                             verbose_name='libellé')
+    label = models.CharField(
+        max_length=500, null=True, blank=True, default="", verbose_name="libellé"
+    )
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
 
 
@@ -393,7 +453,6 @@ class Payment(models.Model):
 
 
         super(Payment, self).save(*args, **kwargs)
         super(Payment, self).save(*args, **kwargs)
 
 
-
     def clean(self):
     def clean(self):
 
 
         # Only if no amount already alloca ted...
         # Only if no amount already alloca ted...
@@ -402,10 +461,12 @@ class Payment(models.Model):
             # If there's a linked invoice and this payment would pay more than
             # If there's a linked invoice and this payment would pay more than
             # the remaining amount needed to pay the invoice...
             # the remaining amount needed to pay the invoice...
             if self.invoice and self.amount > self.invoice.amount_remaining_to_pay():
             if self.invoice and self.amount > self.invoice.amount_remaining_to_pay():
-                raise ValidationError("This payment would pay more than the invoice's remaining to pay")
+                raise ValidationError(
+                    "This payment would pay more than the invoice's remaining to pay"
+                )
 
 
     def amount_already_allocated(self):
     def amount_already_allocated(self):
-        return sum([ a.amount for a in self.allocations.all() ])
+        return sum([a.amount for a in self.allocations.all()])
 
 
     def amount_not_allocated(self):
     def amount_not_allocated(self):
         return self.amount - self.amount_already_allocated()
         return self.amount - self.amount_already_allocated()
@@ -417,22 +478,24 @@ class Payment(models.Model):
         # ...
         # ...
 
 
         amount_can_pay = self.amount_not_allocated()
         amount_can_pay = self.amount_not_allocated()
-        amount_to_pay  = invoice.amount_remaining_to_pay()
+        amount_to_pay = invoice.amount_remaining_to_pay()
         amount_to_allocate = min(amount_can_pay, amount_to_pay)
         amount_to_allocate = min(amount_can_pay, amount_to_pay)
 
 
         accounting_log.info(
         accounting_log.info(
             "Allocating {} from payment {} to invoice {}".format(
             "Allocating {} from payment {} to invoice {}".format(
-                amount_to_allocate, self.date, invoice.number))
+                amount_to_allocate, self.date, invoice.number
+            )
+        )
 
 
-        PaymentAllocation.objects.create(invoice=invoice,
-                                         payment=self,
-                                         amount=amount_to_allocate)
+        PaymentAllocation.objects.create(
+            invoice=invoice, payment=self, amount=amount_to_allocate
+        )
 
 
         # Close invoice if relevant
         # Close invoice if relevant
         if (invoice.amount_remaining_to_pay() <= 0) and (invoice.status == "open"):
         if (invoice.amount_remaining_to_pay() <= 0) and (invoice.status == "open"):
             accounting_log.info(
             accounting_log.info(
-                "Invoice {} has been paid and is now closed".format(
-                    invoice.number))
+                "Invoice {} has been paid and is now closed".format(invoice.number)
+            )
             invoice.status = "closed"
             invoice.status = "closed"
 
 
         invoice.save()
         invoice.save()
@@ -440,14 +503,14 @@ class Payment(models.Model):
 
 
     def __unicode__(self):
     def __unicode__(self):
         if self.member is not None:
         if self.member is not None:
-            return 'Paiment de {:0.2f}€ le {} par {}'.format(
-                self.amount, self.date, self.member)
+            return "Paiment de {:0.2f}€ le {} par {}".format(
+                self.amount, self.date, self.member
+            )
         else:
         else:
-            return 'Paiment de {:0.2f}€ le {}'.format(
-                self.amount, self.date)
+            return "Paiment de {:0.2f}€ le {}".format(self.amount, self.date)
 
 
     class Meta:
     class Meta:
-        verbose_name = 'paiement'
+        verbose_name = "paiement"
 
 
 
 
 # This corresponds to a (possibly partial) allocation of a given payment to
 # This corresponds to a (possibly partial) allocation of a given payment to
@@ -456,14 +519,23 @@ class Payment(models.Model):
 # There can be for example an allocation of 3.14€ from P to I.
 # There can be for example an allocation of 3.14€ from P to I.
 class PaymentAllocation(models.Model):
 class PaymentAllocation(models.Model):
 
 
-    invoice = models.ForeignKey(Invoice, verbose_name='facture associée',
-                                null=False, blank=False,
-                                related_name='allocations')
-    payment = models.ForeignKey(Payment, verbose_name='facture associée',
-                                null=False, blank=False,
-                                related_name='allocations')
-    amount = models.DecimalField(max_digits=5, decimal_places=2, null=True,
-                                 verbose_name='montant')
+    invoice = models.ForeignKey(
+        Invoice,
+        verbose_name="facture associée",
+        null=False,
+        blank=False,
+        related_name="allocations",
+    )
+    payment = models.ForeignKey(
+        Payment,
+        verbose_name="facture associée",
+        null=False,
+        blank=False,
+        related_name="allocations",
+    )
+    amount = models.DecimalField(
+        max_digits=5, decimal_places=2, null=True, verbose_name="montant"
+    )
 
 
 
 
 def get_active_payment_and_invoices(member):
 def get_active_payment_and_invoices(member):
@@ -471,14 +543,18 @@ def get_active_payment_and_invoices(member):
     # Fetch relevant and active payments / invoices
     # Fetch relevant and active payments / invoices
     # and sort then by chronological order : olders first, newers last.
     # and sort then by chronological order : olders first, newers last.
 
 
-    this_member_invoices = [i for i in member.invoices.filter(validated=True).order_by("date")]
+    this_member_invoices = [
+        i for i in member.invoices.filter(validated=True).order_by("date")
+    ]
     this_member_payments = [p for p in member.payments.order_by("date")]
     this_member_payments = [p for p in member.payments.order_by("date")]
 
 
     # TODO / FIXME ^^^ maybe also consider only 'opened' invoices (i.e. not
     # TODO / FIXME ^^^ maybe also consider only 'opened' invoices (i.e. not
     # conflict / trouble invoices)
     # conflict / trouble invoices)
 
 
-    active_payments = [p for p in this_member_payments if p.amount_not_allocated()    > 0]
-    active_invoices = [p for p in this_member_invoices if p.amount_remaining_to_pay() > 0]
+    active_payments = [p for p in this_member_payments if p.amount_not_allocated() > 0]
+    active_invoices = [
+        p for p in this_member_invoices if p.amount_remaining_to_pay() > 0
+    ]
 
 
     return active_payments, active_invoices
     return active_payments, active_invoices
 
 
@@ -493,19 +569,20 @@ def update_accounting_for_member(member):
 
 
     accounting_log.info("Updating accounting for member {} ...".format(member))
     accounting_log.info("Updating accounting for member {} ...".format(member))
     accounting_log.info(
     accounting_log.info(
-        "Member {} current balance is {} ...".format(member, member.balance))
+        "Member {} current balance is {} ...".format(member, member.balance)
+    )
 
 
     reconcile_invoices_and_payments(member)
     reconcile_invoices_and_payments(member)
 
 
-    this_member_invoices = [i for i in member.invoices.filter(validated=True).order_by("date")]
+    this_member_invoices = [
+        i for i in member.invoices.filter(validated=True).order_by("date")
+    ]
     this_member_payments = [p for p in member.payments.order_by("date")]
     this_member_payments = [p for p in member.payments.order_by("date")]
 
 
-    member.balance = compute_balance(this_member_invoices,
-                                     this_member_payments)
+    member.balance = compute_balance(this_member_invoices, this_member_payments)
     member.save()
     member.save()
 
 
-    accounting_log.info("Member {} new balance is {:f}".format(
-        member, member.balance))
+    accounting_log.info("Member {} new balance is {:f}".format(member, member.balance))
 
 
 
 
 def reconcile_invoices_and_payments(member):
 def reconcile_invoices_and_payments(member):
@@ -519,17 +596,19 @@ def reconcile_invoices_and_payments(member):
     if active_payments == []:
     if active_payments == []:
         accounting_log.info(
         accounting_log.info(
             "(No active payment for {}.".format(member)
             "(No active payment for {}.".format(member)
-            + " No invoice/payment reconciliation needed.).")
+            + " No invoice/payment reconciliation needed.)."
+        )
         return
         return
     elif active_invoices == []:
     elif active_invoices == []:
         accounting_log.info(
         accounting_log.info(
-            "(No active invoice for {}. No invoice/payment ".format(member) +
-            "reconciliation needed.).")
+            "(No active invoice for {}. No invoice/payment ".format(member)
+            + "reconciliation needed.)."
+        )
         return
         return
 
 
     accounting_log.info(
     accounting_log.info(
-        "Initiating reconciliation between invoice and payments for {}".format(
-            member))
+        "Initiating reconciliation between invoice and payments for {}".format(member)
+    )
 
 
     while active_payments != [] and active_invoices != []:
     while active_payments != [] and active_invoices != []:
 
 
@@ -542,8 +621,8 @@ def reconcile_invoices_and_payments(member):
             assert p.invoice in active_invoices
             assert p.invoice in active_invoices
             i = p.invoice
             i = p.invoice
             accounting_log.info(
             accounting_log.info(
-                "Payment is to be allocated specifically to invoice {}".format(
-                    i.number))
+                "Payment is to be allocated specifically to invoice {}".format(i.number)
+            )
         else:
         else:
             i = active_invoices[0]
             i = active_invoices[0]
 
 
@@ -563,12 +642,12 @@ def reconcile_invoices_and_payments(member):
 
 
 def compute_balance(invoices, payments):
 def compute_balance(invoices, payments):
 
 
-    active_payments = [p for p in payments if p.amount_not_allocated()    > 0]
+    active_payments = [p for p in payments if p.amount_not_allocated() > 0]
     active_invoices = [i for i in invoices if i.amount_remaining_to_pay() > 0]
     active_invoices = [i for i in invoices if i.amount_remaining_to_pay() > 0]
 
 
     s = 0
     s = 0
     s -= sum([i.amount_remaining_to_pay() for i in active_invoices])
     s -= sum([i.amount_remaining_to_pay() for i in active_invoices])
-    s += sum([p.amount_not_allocated()    for p in active_payments])
+    s += sum([p.amount_not_allocated() for p in active_payments])
 
 
     return s
     return s
 
 
@@ -578,19 +657,34 @@ def compute_balance(invoices, payments):
 def payment_changed(sender, instance, created, **kwargs):
 def payment_changed(sender, instance, created, **kwargs):
 
 
     if created:
     if created:
-        accounting_log.info("Adding payment %s (Date: %s, Member: %s, Amount: %s, Label: %s)."
-                            % (instance.pk, instance.date, instance.member,
-                                instance.amount, instance.label))
+        accounting_log.info(
+            "Adding payment %s (Date: %s, Member: %s, Amount: %s, Label: %s)."
+            % (
+                instance.pk,
+                instance.date,
+                instance.member,
+                instance.amount,
+                instance.label,
+            )
+        )
     else:
     else:
-        accounting_log.info("Updating payment %s (Date: %s, Member: %s, Amount: %s, Label: %s, Allocated: %s)."
-                            % (instance.pk, instance.date, instance.member,
-                                instance.amount, instance.label,
-                                instance.amount_already_allocated()))
+        accounting_log.info(
+            "Updating payment %s (Date: %s, Member: %s, Amount: %s, Label: %s, Allocated: %s)."
+            % (
+                instance.pk,
+                instance.date,
+                instance.member,
+                instance.amount,
+                instance.label,
+                instance.amount_already_allocated(),
+            )
+        )
 
 
     # If this payment is related to a member, update the accounting for
     # If this payment is related to a member, update the accounting for
     # this member
     # this member
-    if (created or instance.amount_not_allocated() != 0) \
-    and (instance.member is not None):
+    if (created or instance.amount_not_allocated() != 0) and (
+        instance.member is not None
+    ):
         update_accounting_for_member(instance.member)
         update_accounting_for_member(instance.member)
 
 
 
 
@@ -601,17 +695,26 @@ def invoice_changed(sender, instance, created, **kwargs):
     if created:
     if created:
         accounting_log.info(
         accounting_log.info(
             "Creating draft invoice DRAFT-{} (Member: {}).".format(
             "Creating draft invoice DRAFT-{} (Member: {}).".format(
-                instance.pk, instance.member))
+                instance.pk, instance.member
+            )
+        )
     else:
     else:
         if not instance.validated:
         if not instance.validated:
             accounting_log.info(
             accounting_log.info(
                 "Updating draft invoice DRAFT-{} (Member: {}).".format(
                 "Updating draft invoice DRAFT-{} (Member: {}).".format(
-                    instance.number, instance.member))
+                    instance.number, instance.member
+                )
+            )
         else:
         else:
             accounting_log.info(
             accounting_log.info(
                 "Updating invoice {} (Member: {}, Total amount: {}, Amount paid: {}).".format(
                 "Updating invoice {} (Member: {}, Total amount: {}, Amount paid: {}).".format(
-                    instance.number, instance.member,
-                    instance.amount(), instance.amount_paid()))
+                    instance.number,
+                    instance.member,
+                    instance.amount(),
+                    instance.amount_paid(),
+                )
+            )
+
 
 
 @receiver(post_delete, sender=PaymentAllocation)
 @receiver(post_delete, sender=PaymentAllocation)
 def paymentallocation_deleted(sender, instance, **kwargs):
 def paymentallocation_deleted(sender, instance, **kwargs):
@@ -630,18 +733,19 @@ def payment_deleted(sender, instance, **kwargs):
 
 
     accounting_log.info(
     accounting_log.info(
         "Deleted payment {} (Date: {}, Member: {}, Amount: {}, Label: {}).".format(
         "Deleted payment {} (Date: {}, Member: {}, Amount: {}, Label: {}).".format(
-            instance.pk, instance.date, instance.member, instance.amount, instance.label))
+            instance.pk, instance.date, instance.member, instance.amount, instance.label
+        )
+    )
 
 
     member = instance.member
     member = instance.member
 
 
     if member is None:
     if member is None:
         return
         return
 
 
-    this_member_invoices = [i for i in member.invoices.filter(validated=True).order_by("date")]
+    this_member_invoices = [
+        i for i in member.invoices.filter(validated=True).order_by("date")
+    ]
     this_member_payments = [p for p in member.payments.order_by("date")]
     this_member_payments = [p for p in member.payments.order_by("date")]
 
 
-    member.balance = compute_balance(this_member_invoices,
-                                     this_member_payments)
+    member.balance = compute_balance(this_member_invoices, this_member_payments)
     member.save()
     member.save()
-
-

+ 164 - 131
coin/billing/tests.py

@@ -11,28 +11,34 @@ from coin.members.tests import MemberTestsUtils
 from coin.members.models import Member, LdapUser
 from coin.members.models import Member, LdapUser
 from coin.billing.models import Invoice, InvoiceQuerySet, InvoiceDetail, Payment
 from coin.billing.models import Invoice, InvoiceQuerySet, InvoiceDetail, Payment
 from coin.offers.models import Offer, OfferSubscription
 from coin.offers.models import Offer, OfferSubscription
-from coin.billing.create_subscriptions_invoices import create_member_invoice_for_a_period
-from coin.billing.create_subscriptions_invoices import create_all_members_invoices_for_a_period
+from coin.billing.create_subscriptions_invoices import (
+    create_member_invoice_for_a_period
+)
+from coin.billing.create_subscriptions_invoices import (
+    create_all_members_invoices_for_a_period
+)
 
 
 
 
 @override_settings(HANDLE_BALANCE=True)
 @override_settings(HANDLE_BALANCE=True)
 class BillingInvoiceCreationTests(TestCase):
 class BillingInvoiceCreationTests(TestCase):
-
     def setUp(self):
     def setUp(self):
         # Créé une offre
         # Créé une offre
-        self.offer = Offer(name='Offre', billing_period=3, period_fees=30,
-                           initial_fees=50)
+        self.offer = Offer(
+            name="Offre", billing_period=3, period_fees=30, initial_fees=50
+        )
         self.offer.save()
         self.offer.save()
         # Créé un membre
         # Créé un membre
         self.username = MemberTestsUtils.get_random_username()
         self.username = MemberTestsUtils.get_random_username()
-        self.member = Member(first_name='Balthazar', last_name='Picsou',
-                             username=self.username)
+        self.member = Member(
+            first_name="Balthazar", last_name="Picsou", username=self.username
+        )
         self.member.save()
         self.member.save()
         # Créé un abonnement
         # Créé un abonnement
         self.subscription = OfferSubscription(
         self.subscription = OfferSubscription(
             subscription_date=datetime.date(2014, 1, 10),
             subscription_date=datetime.date(2014, 1, 10),
             member=self.member,
             member=self.member,
-            offer=self.offer)
+            offer=self.offer,
+        )
         self.subscription.save()
         self.subscription.save()
 
 
     def tearDown(self):
     def tearDown(self):
@@ -47,12 +53,13 @@ class BillingInvoiceCreationTests(TestCase):
         """
         """
         # Demande la création de la première facture
         # Demande la création de la première facture
         invoice = create_member_invoice_for_a_period(
         invoice = create_member_invoice_for_a_period(
-            self.member, datetime.date(2014, 1, 1))
+            self.member, datetime.date(2014, 1, 1)
+        )
         # La facture doit avoir les frais de mise en service
         # La facture doit avoir les frais de mise en service
         # Pour tester cela on tri par montant d'item décroissant.
         # Pour tester cela on tri par montant d'item décroissant.
         # Comme dans l'offre créé, les initial_fees sont plus élevées que
         # Comme dans l'offre créé, les initial_fees sont plus élevées que
         # les period_fees, il doit sortir en premier
         # les period_fees, il doit sortir en premier
-        self.assertEqual(invoice.details.order_by('-amount').first().amount, 50)
+        self.assertEqual(invoice.details.order_by("-amount").first().amount, 50)
 
 
     def test_prorata_for_first_month_subscription(self):
     def test_prorata_for_first_month_subscription(self):
         """
         """
@@ -61,18 +68,21 @@ class BillingInvoiceCreationTests(TestCase):
         """
         """
         # Créé la facture pour le mois de janvier
         # Créé la facture pour le mois de janvier
         invoice = create_member_invoice_for_a_period(
         invoice = create_member_invoice_for_a_period(
-            self.member, datetime.date(2014, 1, 1))
+            self.member, datetime.date(2014, 1, 1)
+        )
         # Comme l'abonnement a été souscris le 10/01 et que la période de
         # Comme l'abonnement a été souscris le 10/01 et que la période de
         # facturation est de 3 mois, alors le prorata doit être :
         # facturation est de 3 mois, alors le prorata doit être :
         # janvier :  22j (31-9)
         # janvier :  22j (31-9)
         # fevrier :  28j
         # fevrier :  28j
         # mars :     31j
         # mars :     31j
-        #22+28+31 / 31+28+31
+        # 22+28+31 / 31+28+31
         quantity = Decimal((22.0 + 28.0 + 31.0) / (31.0 + 28.0 + 31.0))
         quantity = Decimal((22.0 + 28.0 + 31.0) / (31.0 + 28.0 + 31.0))
         for detail in invoice.details.all():
         for detail in invoice.details.all():
             if detail.amount != 50:
             if detail.amount != 50:
-                self.assertEqual(detail.quantity.quantize(Decimal('0.01')),
-                                 quantity.quantize(Decimal('0.01')))
+                self.assertEqual(
+                    detail.quantity.quantize(Decimal("0.01")),
+                    quantity.quantize(Decimal("0.01")),
+                )
 
 
     def test_subscription_cant_be_charged_twice(self):
     def test_subscription_cant_be_charged_twice(self):
         """
         """
@@ -84,25 +94,30 @@ class BillingInvoiceCreationTests(TestCase):
         invoice.save()
         invoice.save()
         # Créé une facturation pour cet abonnement pour la première période
         # Créé une facturation pour cet abonnement pour la première période
         # de janvier à mars
         # de janvier à mars
-        invoice.details.create(label=self.offer.name,
-                               amount=self.offer.period_fees,
-                               offersubscription=self.subscription,
-                               period_from=datetime.date(2014, 1, 1),
-                               period_to=datetime.date(2014, 3, 31))
+        invoice.details.create(
+            label=self.offer.name,
+            amount=self.offer.period_fees,
+            offersubscription=self.subscription,
+            period_from=datetime.date(2014, 1, 1),
+            period_to=datetime.date(2014, 3, 31),
+        )
 
 
         # Créé une facturation pour cet abonnement pour une seconde période
         # Créé une facturation pour cet abonnement pour une seconde période
         # de juin à aout
         # de juin à aout
-        invoice.details.create(label=self.offer.name,
-                               amount=self.offer.period_fees,
-                               offersubscription=self.subscription,
-                               period_from=datetime.date(2014, 6, 1),
-                               period_to=datetime.date(2014, 8, 31))
+        invoice.details.create(
+            label=self.offer.name,
+            amount=self.offer.period_fees,
+            offersubscription=self.subscription,
+            period_from=datetime.date(2014, 6, 1),
+            period_to=datetime.date(2014, 8, 31),
+        )
 
 
         # Demande la génération d'une facture pour février
         # Demande la génération d'une facture pour février
         # Elle doit renvoyer None car l'offre est déjà facturée de
         # Elle doit renvoyer None car l'offre est déjà facturée de
         # janvier à mars
         # janvier à mars
         invoice_test_1 = create_member_invoice_for_a_period(
         invoice_test_1 = create_member_invoice_for_a_period(
-            self.member, datetime.date(2014, 2, 1))
+            self.member, datetime.date(2014, 2, 1)
+        )
         self.assertEqual(invoice_test_1, None)
         self.assertEqual(invoice_test_1, None)
 
 
         # Demande la création d'une facture pour avril
         # Demande la création d'une facture pour avril
@@ -110,29 +125,36 @@ class BillingInvoiceCreationTests(TestCase):
         # que de 2 mois, d'avril à mai car il y a déjà une facture pour
         # que de 2 mois, d'avril à mai car il y a déjà une facture pour
         # la période de juin à aout
         # la période de juin à aout
         invoice_test_2 = create_member_invoice_for_a_period(
         invoice_test_2 = create_member_invoice_for_a_period(
-            self.member, datetime.date(2014, 4, 1))
-        self.assertEqual(invoice_test_2.details.first().period_from,
-                         datetime.date(2014, 4, 1))
-        self.assertEqual(invoice_test_2.details.first().period_to,
-                         datetime.date(2014, 5, 31))
+            self.member, datetime.date(2014, 4, 1)
+        )
+        self.assertEqual(
+            invoice_test_2.details.first().period_from, datetime.date(2014, 4, 1)
+        )
+        self.assertEqual(
+            invoice_test_2.details.first().period_to, datetime.date(2014, 5, 31)
+        )
 
 
     def test_invoice_amount(self):
     def test_invoice_amount(self):
         invoice = Invoice(member=self.member)
         invoice = Invoice(member=self.member)
         invoice.save()
         invoice.save()
 
 
-        invoice.details.create(label=self.offer.name,
-                               amount=100,
-                               offersubscription=self.subscription,
-                               period_from=datetime.date(2014, 1, 1),
-                               period_to=datetime.date(2014, 3, 31),
-                               tax=0)
-
-        invoice.details.create(label=self.offer.name,
-                               amount=10,
-                               offersubscription=self.subscription,
-                               period_from=datetime.date(2014, 6, 1),
-                               period_to=datetime.date(2014, 8, 31),
-                               tax=10)
+        invoice.details.create(
+            label=self.offer.name,
+            amount=100,
+            offersubscription=self.subscription,
+            period_from=datetime.date(2014, 1, 1),
+            period_to=datetime.date(2014, 3, 31),
+            tax=0,
+        )
+
+        invoice.details.create(
+            label=self.offer.name,
+            amount=10,
+            offersubscription=self.subscription,
+            period_from=datetime.date(2014, 6, 1),
+            period_to=datetime.date(2014, 8, 31),
+            tax=10,
+        )
 
 
         self.assertEqual(invoice.amount(), 111)
         self.assertEqual(invoice.amount(), 111)
 
 
@@ -140,51 +162,55 @@ class BillingInvoiceCreationTests(TestCase):
         invoice = Invoice(member=self.member)
         invoice = Invoice(member=self.member)
         invoice.save()
         invoice.save()
 
 
-        invoice.details.create(label=self.offer.name,
-                               amount=100,
-                               offersubscription=self.subscription,
-                               period_from=datetime.date(2014, 1, 1),
-                               period_to=datetime.date(2014, 3, 31),
-                               tax=0)
+        invoice.details.create(
+            label=self.offer.name,
+            amount=100,
+            offersubscription=self.subscription,
+            period_from=datetime.date(2014, 1, 1),
+            period_to=datetime.date(2014, 3, 31),
+            tax=0,
+        )
         invoice.validate()
         invoice.validate()
         invoice.save()
         invoice.save()
 
 
-        self.assertEqual(invoice.status, 'open')
-        p1 = Payment.objects.create(member=self.member,
-                                    invoice=invoice,
-                                    payment_mean='cash',
-                                    amount=10)
+        self.assertEqual(invoice.status, "open")
+        p1 = Payment.objects.create(
+            member=self.member, invoice=invoice, payment_mean="cash", amount=10
+        )
         p1.save()
         p1.save()
 
 
         invoice = Invoice.objects.get(pk=invoice.pk)
         invoice = Invoice.objects.get(pk=invoice.pk)
-        self.assertEqual(invoice.status, 'open')
+        self.assertEqual(invoice.status, "open")
 
 
-        p2 = Payment.objects.create(member=self.member,
-                                    invoice=invoice,
-                                    payment_mean='cash',
-                                    amount=90)
+        p2 = Payment.objects.create(
+            member=self.member, invoice=invoice, payment_mean="cash", amount=90
+        )
         p2.save()
         p2.save()
 
 
         invoice = Invoice.objects.get(pk=invoice.pk)
         invoice = Invoice.objects.get(pk=invoice.pk)
-        self.assertEqual(invoice.status, 'closed')
+        self.assertEqual(invoice.status, "closed")
 
 
     def test_invoice_amount_before_tax(self):
     def test_invoice_amount_before_tax(self):
         invoice = Invoice(member=self.member)
         invoice = Invoice(member=self.member)
         invoice.save()
         invoice.save()
 
 
-        invoice.details.create(label=self.offer.name,
-                               amount=100,
-                               offersubscription=self.subscription,
-                               period_from=datetime.date(2014, 1, 1),
-                               period_to=datetime.date(2014, 3, 31),
-                               tax=0)
-
-        invoice.details.create(label=self.offer.name,
-                               amount=10,
-                               offersubscription=self.subscription,
-                               period_from=datetime.date(2014, 6, 1),
-                               period_to=datetime.date(2014, 8, 31),
-                               tax=10)
+        invoice.details.create(
+            label=self.offer.name,
+            amount=100,
+            offersubscription=self.subscription,
+            period_from=datetime.date(2014, 1, 1),
+            period_to=datetime.date(2014, 3, 31),
+            tax=0,
+        )
+
+        invoice.details.create(
+            label=self.offer.name,
+            amount=10,
+            offersubscription=self.subscription,
+            period_from=datetime.date(2014, 6, 1),
+            period_to=datetime.date(2014, 8, 31),
+            tax=10,
+        )
 
 
         self.assertEqual(invoice.amount_before_tax(), 110)
         self.assertEqual(invoice.amount_before_tax(), 110)
 
 
@@ -193,19 +219,26 @@ class BillingInvoiceCreationTests(TestCase):
         Test qu'une offre non facturable n'est pas prise en compte
         Test qu'une offre non facturable n'est pas prise en compte
         """
         """
         # Créé une offre non facturable
         # Créé une offre non facturable
-        offer = Offer(name='Offre', billing_period=3, period_fees=30,
-                           initial_fees=50, non_billable=True)
+        offer = Offer(
+            name="Offre",
+            billing_period=3,
+            period_fees=30,
+            initial_fees=50,
+            non_billable=True,
+        )
         offer.save()
         offer.save()
         # Créé un abonnement
         # Créé un abonnement
         self.subscription = OfferSubscription(
         self.subscription = OfferSubscription(
             subscription_date=datetime.date(2014, 1, 10),
             subscription_date=datetime.date(2014, 1, 10),
             member=self.member,
             member=self.member,
-            offer=offer)
+            offer=offer,
+        )
         self.subscription.save()
         self.subscription.save()
 
 
         # Demande la création de la première facture
         # Demande la création de la première facture
         invoice = create_member_invoice_for_a_period(
         invoice = create_member_invoice_for_a_period(
-            self.member, datetime.date(2014, 1, 1))
+            self.member, datetime.date(2014, 1, 1)
+        )
 
 
         # Vérifie qu'il n'y a pas l'offre dans la facture, si c'est le cas génère une exception
         # Vérifie qu'il n'y a pas l'offre dans la facture, si c'est le cas génère une exception
         if invoice:
         if invoice:
@@ -215,7 +248,6 @@ class BillingInvoiceCreationTests(TestCase):
 
 
 
 
 class BillingTests(TestCase):
 class BillingTests(TestCase):
-
     def test_download_invoice_pdf_return_a_pdf(self):
     def test_download_invoice_pdf_return_a_pdf(self):
         """
         """
         Test que le téléchargement d'une facture en format pdf retourne bien un
         Test que le téléchargement d'une facture en format pdf retourne bien un
@@ -223,9 +255,8 @@ class BillingTests(TestCase):
         """
         """
         # Créé un membre
         # Créé un membre
         username = MemberTestsUtils.get_random_username()
         username = MemberTestsUtils.get_random_username()
-        member = Member(first_name='A', last_name='A',
-                        username=username)
-        member.set_password('1234')
+        member = Member(first_name="A", last_name="A", username=username)
+        member.set_password("1234")
         member.save()
         member.save()
 
 
         # Créé une facture
         # Créé une facture
@@ -235,11 +266,11 @@ class BillingTests(TestCase):
 
 
         # Se connect en tant que le membre
         # Se connect en tant que le membre
         client = Client()
         client = Client()
-        client.login(username=username, password='1234')
+        client.login(username=username, password="1234")
         # Tente de télécharger la facture
         # Tente de télécharger la facture
-        response = client.get('/billing/invoice/%i/pdf' % invoice.id)
+        response = client.get("/billing/invoice/%i/pdf" % invoice.id)
         # Vérifie return code 200 et contient chaine %PDF-1.
         # Vérifie return code 200 et contient chaine %PDF-1.
-        self.assertContains(response, b'%PDF-1.', status_code=200, html=False)
+        self.assertContains(response, b"%PDF-1.", status_code=200, html=False)
         member.delete()
         member.delete()
 
 
     def test_that_only_owner_of_invoice_can_access_it(self):
     def test_that_only_owner_of_invoice_can_access_it(self):
@@ -251,17 +282,19 @@ class BillingTests(TestCase):
         """
         """
         # Créé un membre A
         # Créé un membre A
         member_a_login = MemberTestsUtils.get_random_username()
         member_a_login = MemberTestsUtils.get_random_username()
-        member_a_pwd = '1234'
-        member_a = Member(first_name='A', last_name='A', email='a@a.com',
-                          username=member_a_login)
+        member_a_pwd = "1234"
+        member_a = Member(
+            first_name="A", last_name="A", email="a@a.com", username=member_a_login
+        )
         member_a.set_password(member_a_pwd)
         member_a.set_password(member_a_pwd)
         member_a.save()
         member_a.save()
 
 
         # Créé un membre B
         # Créé un membre B
         member_b_login = MemberTestsUtils.get_random_username()
         member_b_login = MemberTestsUtils.get_random_username()
-        member_b_pwd = '1234'
-        member_b = Member(first_name='B', last_name='B', email='b@b.com',
-                          username=member_b_login)
+        member_b_pwd = "1234"
+        member_b = Member(
+            first_name="B", last_name="B", email="b@b.com", username=member_b_login
+        )
         member_b.set_password(member_b_pwd)
         member_b.set_password(member_b_pwd)
         member_b.save()
         member_b.save()
 
 
@@ -278,7 +311,7 @@ class BillingTests(TestCase):
         # Vérifie que A a reçu retour OK 200
         # Vérifie que A a reçu retour OK 200
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         # Tente de télécharger la facture pdf de A en tant que A
         # Tente de télécharger la facture pdf de A en tant que A
-        response = client.get('/billing/invoice/%i/pdf' % invoice_a.id)
+        response = client.get("/billing/invoice/%i/pdf" % invoice_a.id)
         # Vérifie que A a reçu retour OK 200
         # Vérifie que A a reçu retour OK 200
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -290,7 +323,7 @@ class BillingTests(TestCase):
         # Vérifie que B a reçu retour Forbissen 403
         # Vérifie que B a reçu retour Forbissen 403
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
         # Tente de télécharger la facture pdf de A en tant que B
         # Tente de télécharger la facture pdf de A en tant que B
-        response = client.get('/billing/invoice/%i/pdf' % invoice_a.id)
+        response = client.get("/billing/invoice/%i/pdf" % invoice_a.id)
         # Vérifie que B a reçu retour Forbidden 403
         # Vérifie que B a reçu retour Forbidden 403
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
@@ -301,89 +334,89 @@ class BillingTests(TestCase):
 class InvoiceQuerySetTests(TestCase):
 class InvoiceQuerySetTests(TestCase):
     def test_get_first_invoice_number_ever(self):
     def test_get_first_invoice_number_ever(self):
         self.assertEqual(
         self.assertEqual(
-            Invoice.objects.get_next_invoice_number(datetime.date(2016,1,1)),
-            '2016-01-000001')
+            Invoice.objects.get_next_invoice_number(datetime.date(2016, 1, 1)),
+            "2016-01-000001",
+        )
 
 
-    @freeze_time('2016-01-01')
+    @freeze_time("2016-01-01")
     def test_get_first_of_month_invoice_number(self):
     def test_get_first_of_month_invoice_number(self):
         # One bill on a month…
         # One bill on a month…
         Invoice.objects.create().validate()
         Invoice.objects.create().validate()
 
 
         # … Does not affect the numbering of following month.
         # … Does not affect the numbering of following month.
         self.assertEqual(
         self.assertEqual(
-            Invoice.objects.get_next_invoice_number(datetime.date(2016,2,15)),
-            '2016-02-000001')
+            Invoice.objects.get_next_invoice_number(datetime.date(2016, 2, 15)),
+            "2016-02-000001",
+        )
 
 
-    @freeze_time('2016-01-01')
+    @freeze_time("2016-01-01")
     def test_number_workflow(self):
     def test_number_workflow(self):
         iv = Invoice.objects.create()
         iv = Invoice.objects.create()
-        self.assertEqual(iv.number, 'DRAFT-{}'.format(iv.pk))
+        self.assertEqual(iv.number, "DRAFT-{}".format(iv.pk))
         iv.validate()
         iv.validate()
-        self.assertRegexpMatches(iv.number, r'2016-01-000001$')
+        self.assertRegexpMatches(iv.number, r"2016-01-000001$")
 
 
-    @freeze_time('2016-01-01')
+    @freeze_time("2016-01-01")
     def test_get_second_of_month_invoice_number(self):
     def test_get_second_of_month_invoice_number(self):
-        first_bill = Invoice.objects.create(date=datetime.date(2016,1,1))
+        first_bill = Invoice.objects.create(date=datetime.date(2016, 1, 1))
         first_bill.validate()
         first_bill.validate()
         self.assertEqual(
         self.assertEqual(
-            Invoice.objects.get_next_invoice_number(datetime.date(2016,1,1)),
-            '2016-01-000002')
+            Invoice.objects.get_next_invoice_number(datetime.date(2016, 1, 1)),
+            "2016-01-000002",
+        )
 
 
     def test_get_right_year_invoice_number(self):
     def test_get_right_year_invoice_number(self):
-        with freeze_time('2016-01-01'):
+        with freeze_time("2016-01-01"):
             Invoice.objects.create(date=datetime.date(2016, 1, 1)).validate()
             Invoice.objects.create(date=datetime.date(2016, 1, 1)).validate()
-        with freeze_time('2017-01-01'):
+        with freeze_time("2017-01-01"):
             Invoice.objects.create(date=datetime.date(2017, 1, 1)).validate()
             Invoice.objects.create(date=datetime.date(2017, 1, 1)).validate()
-        with freeze_time('2018-01-01'):
+        with freeze_time("2018-01-01"):
             Invoice.objects.create(date=datetime.date(2018, 1, 1)).validate()
             Invoice.objects.create(date=datetime.date(2018, 1, 1)).validate()
 
 
         self.assertEqual(
         self.assertEqual(
             Invoice.objects.get_next_invoice_number(datetime.date(2017, 1, 1)),
             Invoice.objects.get_next_invoice_number(datetime.date(2017, 1, 1)),
-            '2017-01-000002')
+            "2017-01-000002",
+        )
 
 
     def test_bill_date_is_validation_date(self):
     def test_bill_date_is_validation_date(self):
-        bill = Invoice.objects.create(date=datetime.date(2016,1,1))
-        self.assertEqual(bill.date, datetime.date(2016,1,1))
+        bill = Invoice.objects.create(date=datetime.date(2016, 1, 1))
+        self.assertEqual(bill.date, datetime.date(2016, 1, 1))
 
 
-        with freeze_time('2017-01-01'):
+        with freeze_time("2017-01-01"):
             bill.validate()
             bill.validate()
             self.assertEqual(bill.date, datetime.date(2017, 1, 1))
             self.assertEqual(bill.date, datetime.date(2017, 1, 1))
-            self.assertEqual(bill.number, '2017-01-000001')
+            self.assertEqual(bill.number, "2017-01-000001")
 
 
 
 
 class PaymentInvoiceAutoReconciliationTests(TestCase):
 class PaymentInvoiceAutoReconciliationTests(TestCase):
-
     def test_accounting_update(self):
     def test_accounting_update(self):
 
 
-        johndoe =  Member.objects.create(username=MemberTestsUtils.get_random_username(),
-                                         first_name="John",
-                                         last_name="Doe",
-                                         email="johndoe@yolo.test")
+        johndoe = Member.objects.create(
+            username=MemberTestsUtils.get_random_username(),
+            first_name="John",
+            last_name="Doe",
+            email="johndoe@yolo.test",
+        )
         johndoe.set_password("trololo")
         johndoe.set_password("trololo")
 
 
         # First facture
         # First facture
-        invoice = Invoice.objects.create(number="1337",
-                                         member=johndoe)
-        InvoiceDetail.objects.create(label="superservice",
-                                     amount="15.0",
-                                     invoice=invoice)
+        invoice = Invoice.objects.create(number="1337", member=johndoe)
+        InvoiceDetail.objects.create(
+            label="superservice", amount="15.0", invoice=invoice
+        )
         invoice.validate()
         invoice.validate()
 
 
         # Second facture
         # Second facture
-        invoice2 = Invoice.objects.create(number="42",
-                                         member=johndoe)
-        InvoiceDetail.objects.create(label="superservice",
-                                     amount="42",
-                                     invoice=invoice2)
+        invoice2 = Invoice.objects.create(number="42", member=johndoe)
+        InvoiceDetail.objects.create(
+            label="superservice", amount="42", invoice=invoice2
+        )
         invoice2.validate()
         invoice2.validate()
 
 
         # Payment
         # Payment
-        payment = Payment.objects.create(amount=20,
-                                         member=johndoe)
+        payment = Payment.objects.create(amount=20, member=johndoe)
 
 
         invoice.delete()
         invoice.delete()
         invoice2.delete()
         invoice2.delete()
         payment.delete()
         payment.delete()
         johndoe.delete()
         johndoe.delete()
-
-

+ 4 - 5
coin/billing/urls.py

@@ -6,10 +6,9 @@ from django.views.generic import DetailView
 from coin.billing import views
 from coin.billing import views
 
 
 urlpatterns = patterns(
 urlpatterns = patterns(
-    '',
-    url(r'^invoice/(?P<id>.+)/pdf$', views.invoice_pdf, name="invoice_pdf"),
-    url(r'^invoice/(?P<id>.+)$', views.invoice, name="invoice"),
+    "",
+    url(r"^invoice/(?P<id>.+)/pdf$", views.invoice_pdf, name="invoice_pdf"),
+    url(r"^invoice/(?P<id>.+)$", views.invoice, name="invoice"),
     # url(r'^invoice/(?P<id>.+)/validate$', views.invoice_validate, name="invoice_validate"),
     # url(r'^invoice/(?P<id>.+)/validate$', views.invoice_validate, name="invoice_validate"),
-
-    url('invoice/create_all_members_invoices_for_a_period', views.gen_invoices)
+    url("invoice/create_all_members_invoices_for_a_period", views.gen_invoices),
 )
 )

+ 2 - 3
coin/billing/utils.py

@@ -21,6 +21,5 @@ def assert_user_can_view_the_invoice(request, invoice):
     """
     """
     Raise PermissionDenied if logged user can't access given invoice
     Raise PermissionDenied if logged user can't access given invoice
     """
     """
-    if not invoice.has_owner(request.user.username)\
-       and not request.user.is_superuser:
-        raise PermissionDenied
+    if not invoice.has_owner(request.user.username) and not request.user.is_superuser:
+        raise PermissionDenied

+ 17 - 8
coin/billing/views.py

@@ -11,13 +11,18 @@ from sendfile import sendfile
 from coin.billing.models import Invoice
 from coin.billing.models import Invoice
 from coin.members.models import Member
 from coin.members.models import Member
 from coin.html2pdf import render_as_pdf
 from coin.html2pdf import render_as_pdf
-from coin.billing.create_subscriptions_invoices import create_all_members_invoices_for_a_period
-from coin.billing.utils import get_invoice_from_id_or_number, assert_user_can_view_the_invoice
+from coin.billing.create_subscriptions_invoices import (
+    create_all_members_invoices_for_a_period
+)
+from coin.billing.utils import (
+    get_invoice_from_id_or_number,
+    assert_user_can_view_the_invoice,
+)
 
 
 
 
 def gen_invoices(request):
 def gen_invoices(request):
     create_all_members_invoices_for_a_period()
     create_all_members_invoices_for_a_period()
-    return HttpResponse('blop')
+    return HttpResponse("blop")
 
 
 
 
 def invoice_pdf(request, id):
 def invoice_pdf(request, id):
@@ -29,10 +34,11 @@ def invoice_pdf(request, id):
 
 
     assert_user_can_view_the_invoice(request, invoice)
     assert_user_can_view_the_invoice(request, invoice)
 
 
-    pdf_filename = 'Facture_%s.pdf' % invoice.number
+    pdf_filename = "Facture_%s.pdf" % invoice.number
 
 
-    return sendfile(request, invoice.pdf.path,
-                    attachment=True, attachment_filename=pdf_filename)
+    return sendfile(
+        request, invoice.pdf.path, attachment=True, attachment_filename=pdf_filename
+    )
 
 
 
 
 def invoice(request, id):
 def invoice(request, id):
@@ -44,7 +50,10 @@ def invoice(request, id):
 
 
     assert_user_can_view_the_invoice(request, invoice)
     assert_user_can_view_the_invoice(request, invoice)
 
 
-    return render_to_response('billing/invoice.html', {"invoice": invoice},
-                              context_instance=RequestContext(request))
+    return render_to_response(
+        "billing/invoice.html",
+        {"invoice": invoice},
+        context_instance=RequestContext(request),
+    )
 
 
     return response
     return response

+ 14 - 4
coin/configuration/admin.py

@@ -14,6 +14,7 @@ ConfigurationAdminFormMixin. This make use of ConfigurationForm form that
 filter offersubscription select input to avoid selecting wrong subscription.
 filter offersubscription select input to avoid selecting wrong subscription.
 """
 """
 
 
+
 class IPSubnetInline(admin.TabularInline):
 class IPSubnetInline(admin.TabularInline):
     model = IPSubnet
     model = IPSubnet
     extra = 0
     extra = 0
@@ -22,11 +23,17 @@ class IPSubnetInline(admin.TabularInline):
 class ParentConfigurationAdmin(PolymorphicParentModelAdmin):
 class ParentConfigurationAdmin(PolymorphicParentModelAdmin):
     base_model = Configuration
     base_model = Configuration
     polymorphic_list = True
     polymorphic_list = True
-    list_display = ('model_name','configuration_type_name', 'offersubscription', 'offer_subscription_member')
+    list_display = (
+        "model_name",
+        "configuration_type_name",
+        "offersubscription",
+        "offer_subscription_member",
+    )
 
 
     def offer_subscription_member(self, config):
     def offer_subscription_member(self, config):
         return config.offersubscription.member
         return config.offersubscription.member
-    offer_subscription_member.short_description = 'Membre'
+
+    offer_subscription_member.short_description = "Membre"
 
 
     def get_child_models(self):
     def get_child_models(self):
         """
         """
@@ -34,7 +41,9 @@ class ParentConfigurationAdmin(PolymorphicParentModelAdmin):
         ex :((VPNConfiguration, VPNConfigurationAdmin),
         ex :((VPNConfiguration, VPNConfigurationAdmin),
             (ADSLConfiguration, ADSLConfigurationAdmin))
             (ADSLConfiguration, ADSLConfigurationAdmin))
         """
         """
-        return (tuple((x.base_model, x) for x in PolymorphicChildModelAdmin.__subclasses__()))
+        return tuple(
+            (x.base_model, x) for x in PolymorphicChildModelAdmin.__subclasses__()
+        )
 
 
     def get_urls(self):
     def get_urls(self):
         """
         """
@@ -57,6 +66,7 @@ class ConfigurationAdminFormMixin(object):
     base_form = ConfigurationForm
     base_form = ConfigurationForm
     # For each child (admin object for configurations), this will display
     # For each child (admin object for configurations), this will display
     # an inline form to assign IP addresses.
     # an inline form to assign IP addresses.
-    inlines = (IPSubnetInline, )
+    inlines = (IPSubnetInline,)
+
 
 
 admin.site.register(Configuration, ParentConfigurationAdmin)
 admin.site.register(Configuration, ParentConfigurationAdmin)

+ 11 - 7
coin/configuration/forms.py

@@ -9,10 +9,9 @@ from coin.configuration.models import Configuration
 
 
 
 
 class ConfigurationForm(ModelForm):
 class ConfigurationForm(ModelForm):
-
     class Meta:
     class Meta:
         model = Configuration
         model = Configuration
-        fields = '__all__'
+        fields = "__all__"
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         """
         """
@@ -23,17 +22,22 @@ class ConfigurationForm(ModelForm):
         super(ConfigurationForm, self).__init__(*args, **kwargs)
         super(ConfigurationForm, self).__init__(*args, **kwargs)
         if self.instance:
         if self.instance:
             queryset = OfferSubscription.objects.filter(
             queryset = OfferSubscription.objects.filter(
-                Q(offer__configuration_type=self.instance.model_name) & (
-                Q(configuration=None) | Q(configuration=self.instance.pk)))
-            self.fields['offersubscription'].queryset = queryset
+                Q(offer__configuration_type=self.instance.model_name)
+                & (Q(configuration=None) | Q(configuration=self.instance.pk))
+            )
+            self.fields["offersubscription"].queryset = queryset
 
 
     def clean_offersubscription(self):
     def clean_offersubscription(self):
         """
         """
         This check if the selected administrative subscription is linked to an
         This check if the selected administrative subscription is linked to an
         offer which use the same configuration type than the edited configuration.
         offer which use the same configuration type than the edited configuration.
         """
         """
-        offersubscription = self.cleaned_data['offersubscription']
+        offersubscription = self.cleaned_data["offersubscription"]
         if offersubscription.offer.configuration_type != self.instance.model_name():
         if offersubscription.offer.configuration_type != self.instance.model_name():
-            raise ValidationError('Administrative subscription must refer an offer having a "{}" configuration type.'.format(self.instance.model_name()))
+            raise ValidationError(
+                'Administrative subscription must refer an offer having a "{}" configuration type.'.format(
+                    self.instance.model_name()
+                )
+            )
 
 
         return offersubscription
         return offersubscription

+ 20 - 16
coin/configuration/models.py

@@ -21,13 +21,13 @@ Your model can implement Meta verbose_name to have human readable name and a
 url_namespace variable to specify the url namespace used by this model.
 url_namespace variable to specify the url namespace used by this model.
 """
 """
 
 
+
 class Configuration(PolymorphicModel):
 class Configuration(PolymorphicModel):
 
 
-    offersubscription = models.OneToOneField(OfferSubscription,
-                                             related_name='configuration',
-                                             verbose_name='abonnement')
-    comment = models.CharField(blank=True, max_length=512,
-                               verbose_name="commentaire")
+    offersubscription = models.OneToOneField(
+        OfferSubscription, related_name="configuration", verbose_name="abonnement"
+    )
+    comment = models.CharField(blank=True, max_length=512, verbose_name="commentaire")
 
 
     @staticmethod
     @staticmethod
     def get_configurations_choices_list():
     def get_configurations_choices_list():
@@ -35,16 +35,20 @@ class Configuration(PolymorphicModel):
         Génère automatiquement la liste de choix possibles de configurations
         Génère automatiquement la liste de choix possibles de configurations
         en fonction des classes enfants de Configuration
         en fonction des classes enfants de Configuration
         """
         """
-        return tuple((x().__class__.__name__,x()._meta.verbose_name) 
-            for x in Configuration.__subclasses__())
-    
+        return tuple(
+            (x().__class__.__name__, x()._meta.verbose_name)
+            for x in Configuration.__subclasses__()
+        )
+
     def model_name(self):
     def model_name(self):
         return self.__class__.__name__
         return self.__class__.__name__
-    model_name.short_description = 'Nom du modèle'
+
+    model_name.short_description = "Nom du modèle"
 
 
     def configuration_type_name(self):
     def configuration_type_name(self):
         return self._meta.verbose_name
         return self._meta.verbose_name
-    configuration_type_name.short_description = 'Type'
+
+    configuration_type_name.short_description = "Type"
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
         """
         """
@@ -52,8 +56,8 @@ class Configuration(PolymorphicModel):
         Une url doit être nommée "details"
         Une url doit être nommée "details"
         """
         """
         from django.core.urlresolvers import reverse
         from django.core.urlresolvers import reverse
-        return reverse('%s:details' % self.get_url_namespace(), 
-                       args=[str(self.id)])
+
+        return reverse("%s:details" % self.get_url_namespace(), args=[str(self.id)])
 
 
     def get_url_namespace(self):
     def get_url_namespace(self):
         """
         """
@@ -61,13 +65,13 @@ class Configuration(PolymorphicModel):
         celui définit dans la classe enfant dans url_namespace sinon
         celui définit dans la classe enfant dans url_namespace sinon
         par défaut utilise le nom de la classe en minuscule
         par défaut utilise le nom de la classe en minuscule
         """
         """
-        if hasattr(self, 'url_namespace') and self.url_namespace:
+        if hasattr(self, "url_namespace") and self.url_namespace:
             return self.url_namespace
             return self.url_namespace
         else:
         else:
             return self.model_name().lower()
             return self.model_name().lower()
 
 
     class Meta:
     class Meta:
-        verbose_name = 'configuration'
+        verbose_name = "configuration"
 
 
 
 
 @receiver(post_save, sender=IPSubnet)
 @receiver(post_save, sender=IPSubnet)
@@ -100,10 +104,10 @@ def subnet_event(sender, **kwargs):
     raised, which is great (even if undocumented).
     raised, which is great (even if undocumented).
 
 
     """
     """
-    subnet = kwargs['instance']
+    subnet = kwargs["instance"]
     try:
     try:
         config = subnet.configuration
         config = subnet.configuration
-        if hasattr(config, 'subnet_event'):
+        if hasattr(config, "subnet_event"):
             config.subnet_event()
             config.subnet_event()
     except ObjectDoesNotExist:
     except ObjectDoesNotExist:
         pass
         pass

+ 1 - 1
coin/context_processors.py

@@ -4,4 +4,4 @@ from django.conf import settings
 def installed_apps(request):
 def installed_apps(request):
     """ Expose the settings INSTALLED_APPS to templates
     """ Expose the settings INSTALLED_APPS to templates
     """
     """
-    return {'INSTALLED_APPS': settings.INSTALLED_APPS}
+    return {"INSTALLED_APPS": settings.INSTALLED_APPS}

+ 9 - 9
coin/filtering_queryset.py

@@ -7,6 +7,7 @@ import logging
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
+
 class LimitedAdminInlineMixin(object):
 class LimitedAdminInlineMixin(object):
     """
     """
     InlineAdmin mixin limiting the selection of related items according to
     InlineAdmin mixin limiting the selection of related items according to
@@ -30,16 +31,16 @@ class LimitedAdminInlineMixin(object):
         `field` and filters it based on the criteria specified in filters,
         `field` and filters it based on the criteria specified in filters,
         unless `empty=True`. In this case, no choices will be made available.
         unless `empty=True`. In this case, no choices will be made available.
         """
         """
-        try:        
+        try:
             assert formset.form.base_fields.has_key(field)
             assert formset.form.base_fields.has_key(field)
 
 
             qs = formset.form.base_fields[field].queryset
             qs = formset.form.base_fields[field].queryset
             if empty:
             if empty:
-                logger.debug('Limiting the queryset to none')
+                logger.debug("Limiting the queryset to none")
                 formset.form.base_fields[field].queryset = qs.none()
                 formset.form.base_fields[field].queryset = qs.none()
             else:
             else:
                 qs = qs.filter(**filters)
                 qs = qs.filter(**filters)
-                logger.debug('Limiting queryset for formset to: %s', qs)
+                logger.debug("Limiting queryset for formset to: %s", qs)
 
 
                 formset.form.base_fields[field].queryset = qs
                 formset.form.base_fields[field].queryset = qs
         except:
         except:
@@ -50,16 +51,15 @@ class LimitedAdminInlineMixin(object):
         Make sure we can only select variations that relate to the current
         Make sure we can only select variations that relate to the current
         item.
         item.
         """
         """
-        formset = \
-            super(LimitedAdminInlineMixin, self).get_formset(request,
-                                                             obj,
-                                                             **kwargs)
+        formset = super(LimitedAdminInlineMixin, self).get_formset(
+            request, obj, **kwargs
+        )
         for (field, filters) in self.get_filters(obj):
         for (field, filters) in self.get_filters(obj):
             if obj and filters:
             if obj and filters:
                 self.limit_inline_choices(formset, field, **filters)
                 self.limit_inline_choices(formset, field, **filters)
             else:
             else:
                 self.limit_inline_choices(formset, field, empty=True)
                 self.limit_inline_choices(formset, field, empty=True)
-        
+
         return formset
         return formset
 
 
     def get_filters(self, obj):
     def get_filters(self, obj):
@@ -73,4 +73,4 @@ class LimitedAdminInlineMixin(object):
         subclass or define a `filters` property with the same syntax as this
         subclass or define a `filters` property with the same syntax as this
         one.
         one.
         """
         """
-        return getattr(self, 'filters', ())
+        return getattr(self, "filters", ())

+ 6 - 8
coin/html2pdf.py

@@ -16,10 +16,10 @@ def link_callback(uri, rel):
     # Convert HTML URIs to absolute system paths so xhtml2pdf can access
     # Convert HTML URIs to absolute system paths so xhtml2pdf can access
     # those resources
     # those resources
     """
     """
-    sUrl = settings.STATIC_URL    # Typically /static/
+    sUrl = settings.STATIC_URL  # Typically /static/
     sRoot = settings.STATIC_ROOT  # Typically /home/userX/project_static/
     sRoot = settings.STATIC_ROOT  # Typically /home/userX/project_static/
-    mUrl = settings.MEDIA_URL     # Typically /static/media/
-    mRoot = settings.MEDIA_ROOT   # Typically /home/userX/project_static/media/
+    mUrl = settings.MEDIA_URL  # Typically /static/media/
+    mRoot = settings.MEDIA_ROOT  # Typically /home/userX/project_static/media/
     projectDir = settings.PROJECT_PATH  # Typically /home/userX/project/
     projectDir = settings.PROJECT_PATH  # Typically /home/userX/project/
 
 
     # convert URIs to absolute system paths
     # convert URIs to absolute system paths
@@ -28,20 +28,18 @@ def link_callback(uri, rel):
     elif uri.startswith(sUrl):
     elif uri.startswith(sUrl):
         path = os.path.join(sRoot, uri.replace(sUrl, ""))
         path = os.path.join(sRoot, uri.replace(sUrl, ""))
     else:
     else:
-        return uri # handle the absolute URIs
+        return uri  # handle the absolute URIs
 
 
     # If file doesn't exist try to find it in app static folder
     # If file doesn't exist try to find it in app static folder
     # This case occur in developpement env
     # This case occur in developpement env
     if not os.path.isfile(path):
     if not os.path.isfile(path):
-        app_search = re.search(r'^(%s|%s)(.*)/.*' % (sUrl, mUrl), uri)
+        app_search = re.search(r"^(%s|%s)(.*)/.*" % (sUrl, mUrl), uri)
         app = app_search.group(2)
         app = app_search.group(2)
         path = os.path.join(projectDir, app, uri[1:])
         path = os.path.join(projectDir, app, uri[1:])
 
 
     # make sure that file exists
     # make sure that file exists
     if not os.path.isfile(path):
     if not os.path.isfile(path):
-        raise Exception(
-            'media URI must start with %s or %s' %
-            (sUrl, mUrl))
+        raise Exception("media URI must start with %s or %s" % (sUrl, mUrl))
     return path
     return path
 
 
 
 

+ 76 - 37
coin/isp_database/admin.py

@@ -6,20 +6,29 @@ from django.forms import ModelForm
 
 
 from localflavor.fr.forms import FRPhoneNumberField
 from localflavor.fr.forms import FRPhoneNumberField
 
 
-from coin.isp_database.models import ISPInfo, RegisteredOffice, OtherWebsite, ChatRoom, CoveredArea, BankInfo
+from coin.isp_database.models import (
+    ISPInfo,
+    RegisteredOffice,
+    OtherWebsite,
+    ChatRoom,
+    CoveredArea,
+    BankInfo,
+)
 
 
-class ISPAdminForm(ModelForm):
 
 
+class ISPAdminForm(ModelForm):
     class Meta:
     class Meta:
         model = ISPInfo
         model = ISPInfo
         exclude = []
         exclude = []
 
 
-    phone_number = FRPhoneNumberField(required=False,
-                                      help_text='Main contact phone number')
+    phone_number = FRPhoneNumberField(
+        required=False, help_text="Main contact phone number"
+    )
 
 
 
 
 class SingleInstanceAdminMixin(object):
 class SingleInstanceAdminMixin(object):
     """Hides the "Add" button when there is already an instance"""
     """Hides the "Add" button when there is already an instance"""
+
     def has_add_permission(self, request):
     def has_add_permission(self, request):
         num_objects = self.model.objects.count()
         num_objects = self.model.objects.count()
         if num_objects >= 1:
         if num_objects >= 1:
@@ -31,15 +40,24 @@ class RegisteredOfficeInline(admin.StackedInline):
     model = RegisteredOffice
     model = RegisteredOffice
     extra = 0
     extra = 0
     fieldsets = (
     fieldsets = (
-        ('', {'fields': (
-             ('street_address', 'extended_address', 'post_office_box'),
-             ('postal_code', 'locality'),
-             ('region', 'country_name'))}),
-        ('Extras', {
-             'fields': ('siret',),
-             'description': 'Ces champs ne font pas partie de la spécification db.ffdn.org mais sont utilisés sur le site'})
-        )
-
+        (
+            "",
+            {
+                "fields": (
+                    ("street_address", "extended_address", "post_office_box"),
+                    ("postal_code", "locality"),
+                    ("region", "country_name"),
+                )
+            },
+        ),
+        (
+            "Extras",
+            {
+                "fields": ("siret",),
+                "description": "Ces champs ne font pas partie de la spécification db.ffdn.org mais sont utilisés sur le site",
+            },
+        ),
+    )
 
 
 
 
 class OtherWebsiteInline(admin.StackedInline):
 class OtherWebsiteInline(admin.StackedInline):
@@ -61,40 +79,61 @@ class BankInfoInline(admin.StackedInline):
     model = BankInfo
     model = BankInfo
     extra = 0
     extra = 0
 
 
-    fieldsets = (('', {
-                'fields': ('iban', 'bic', 'bank_name', 'check_order'),
-                'description': (
-                    'Les coordonnées bancaires ne font pas partie de la'+
-                    ' spécification db.ffdn.org mais sont utilisées par le'+
-                    ' site (facturation notamment).')
-    }),)
+    fieldsets = (
+        (
+            "",
+            {
+                "fields": ("iban", "bic", "bank_name", "check_order"),
+                "description": (
+                    "Les coordonnées bancaires ne font pas partie de la"
+                    + " spécification db.ffdn.org mais sont utilisées par le"
+                    + " site (facturation notamment)."
+                ),
+            },
+        ),
+    )
 
 
 
 
 class ISPInfoAdmin(SingleInstanceAdminMixin, admin.ModelAdmin):
 class ISPInfoAdmin(SingleInstanceAdminMixin, admin.ModelAdmin):
     model = ISPInfo
     model = ISPInfo
     fieldsets = (
     fieldsets = (
-        ('General', {'fields': (
-            ('name', 'shortname'),
-            'description',
-            'logoURL',
-            ('creationDate', 'ffdnMemberSince'),
-            'progressStatus',
-            ('latitude', 'longitude'))}),
-        ('Contact', {'fields': (
-            ('email', 'mainMailingList'),
-            'website', 'phone_number')}),
-        ('Extras', {
-            'fields': ('administrative_email', 'support_email', 'lists_url'),
-            'description':
-                'Ces champs ne font pas partie de la spécification db.ffdn.org mais sont utilisés sur le site'
-        }),
+        (
+            "General",
+            {
+                "fields": (
+                    ("name", "shortname"),
+                    "description",
+                    "logoURL",
+                    ("creationDate", "ffdnMemberSince"),
+                    "progressStatus",
+                    ("latitude", "longitude"),
+                )
+            },
+        ),
+        (
+            "Contact",
+            {"fields": (("email", "mainMailingList"), "website", "phone_number")},
+        ),
+        (
+            "Extras",
+            {
+                "fields": ("administrative_email", "support_email", "lists_url"),
+                "description": "Ces champs ne font pas partie de la spécification db.ffdn.org mais sont utilisés sur le site",
+            },
+        ),
     )
     )
 
 
-    inlines = (RegisteredOfficeInline, BankInfoInline, OtherWebsiteInline, ChatRoomInline,
-               CoveredAreaInline)
+    inlines = (
+        RegisteredOfficeInline,
+        BankInfoInline,
+        OtherWebsiteInline,
+        ChatRoomInline,
+        CoveredAreaInline,
+    )
     save_on_top = True
     save_on_top = True
 
 
     # Use custom form
     # Use custom form
     form = ISPAdminForm
     form = ISPAdminForm
 
 
+
 admin.site.register(ISPInfo, ISPInfoAdmin)
 admin.site.register(ISPInfo, ISPInfoAdmin)

+ 2 - 1
coin/isp_database/context_processors.py

@@ -3,7 +3,8 @@ from __future__ import unicode_literals
 
 
 from coin.isp_database.models import ISPInfo
 from coin.isp_database.models import ISPInfo
 
 
+
 def branding(request):
 def branding(request):
     """ Just a shortcut to get the ISP object in templates
     """ Just a shortcut to get the ISP object in templates
     """
     """
-    return {'branding': ISPInfo.objects.first()}
+    return {"branding": ISPInfo.objects.first()}

+ 153 - 86
coin/isp_database/models.py

@@ -18,11 +18,13 @@ from coin.validation import chatroom_url_validator
 # API version, see http://db.ffdn.org/format
 # API version, see http://db.ffdn.org/format
 API_VERSION = 0.1
 API_VERSION = 0.1
 
 
-TECHNOLOGIES = (('ftth', 'FTTH'),
-                ('dsl', '*DSL'),
-                ('wifi', 'WiFi'),
-                ('vpn', 'VPN'),
-                ('cube', 'Brique Internet'))
+TECHNOLOGIES = (
+    ("ftth", "FTTH"),
+    ("dsl", "*DSL"),
+    ("wifi", "WiFi"),
+    ("vpn", "VPN"),
+    ("cube", "Brique Internet"),
+)
 
 
 
 
 class SingleInstanceMixin(object):
 class SingleInstanceMixin(object):
@@ -30,7 +32,7 @@ class SingleInstanceMixin(object):
 
 
     def clean(self):
     def clean(self):
         model = self.__class__
         model = self.__class__
-        if (model.objects.count() > 0 and self.id != model.objects.get().id):
+        if model.objects.count() > 0 and self.id != model.objects.get().id:
             raise ValidationError("Can only create 1 instance of %s" % model.__name__)
             raise ValidationError("Can only create 1 instance of %s" % model.__name__)
         super(SingleInstanceMixin, self).clean()
         super(SingleInstanceMixin, self).clean()
 
 
@@ -41,6 +43,7 @@ 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.
     # These two properties can be overriden with static counters, see below.
     @property
     @property
     def memberCount(self):
     def memberCount(self):
@@ -52,70 +55,102 @@ class ISPInfo(SingleInstanceMixin, models.Model):
         """Number of subscribers to an internet access"""
         """Number of subscribers to an internet access"""
         return count_active_subscriptions()
         return count_active_subscriptions()
 
 
-    name = models.CharField(max_length=512,
-                            verbose_name="Nom",
-                            help_text="Nom du FAI")
+    name = models.CharField(max_length=512, verbose_name="Nom", help_text="Nom du FAI")
     # Length required by the spec
     # Length required by the spec
-    shortname = models.CharField(max_length=15, blank=True,
-                                 verbose_name="Abréviation",
-                                 help_text="Nom plus court")
-    description = models.TextField(blank=True,
-                                   verbose_name="Description",
-                                   help_text="Description courte du projet")
-    logoURL = models.URLField(blank=True,
-                              verbose_name="URL du logo",
-                              help_text="Adresse HTTP(S) du logo du FAI")
-    website = models.URLField(blank=True,
-                              verbose_name="URL du site Internet",
-                              help_text='Adresse URL du site Internet')
-    email = models.EmailField(verbose_name="Courriel",
-                              help_text="Adresse courriel de contact")
-    mainMailingList = models.EmailField(blank=True,
-                                        verbose_name="Liste de discussion principale",
-                                        help_text="Principale liste de discussion publique")
-    phone_number = models.CharField(max_length=25, blank=True,
-                                    verbose_name="Numéro de téléphone",
-                                    help_text='Numéro de téléphone de contact principal')
-    creationDate = models.DateField(blank=True, null=True,
-                                    verbose_name="Date de création",
-                                    help_text="Date de création de la structure légale")
-    ffdnMemberSince = models.DateField(blank=True, null=True,
-                                       verbose_name="Membre de FFDN depuis",
-                                       help_text="Date à laquelle le FAI a rejoint la Fédération FDN")
+    shortname = models.CharField(
+        max_length=15,
+        blank=True,
+        verbose_name="Abréviation",
+        help_text="Nom plus court",
+    )
+    description = models.TextField(
+        blank=True, verbose_name="Description", help_text="Description courte du projet"
+    )
+    logoURL = models.URLField(
+        blank=True,
+        verbose_name="URL du logo",
+        help_text="Adresse HTTP(S) du logo du FAI",
+    )
+    website = models.URLField(
+        blank=True,
+        verbose_name="URL du site Internet",
+        help_text="Adresse URL du site Internet",
+    )
+    email = models.EmailField(
+        verbose_name="Courriel", help_text="Adresse courriel de contact"
+    )
+    mainMailingList = models.EmailField(
+        blank=True,
+        verbose_name="Liste de discussion principale",
+        help_text="Principale liste de discussion publique",
+    )
+    phone_number = models.CharField(
+        max_length=25,
+        blank=True,
+        verbose_name="Numéro de téléphone",
+        help_text="Numéro de téléphone de contact principal",
+    )
+    creationDate = models.DateField(
+        blank=True,
+        null=True,
+        verbose_name="Date de création",
+        help_text="Date de création de la structure légale",
+    )
+    ffdnMemberSince = models.DateField(
+        blank=True,
+        null=True,
+        verbose_name="Membre de FFDN depuis",
+        help_text="Date à laquelle le FAI a rejoint la Fédération FDN",
+    )
     # TODO: choice field
     # TODO: choice field
     progressStatus = models.PositiveSmallIntegerField(
     progressStatus = models.PositiveSmallIntegerField(
         validators=[MaxValueValidator(7)],
         validators=[MaxValueValidator(7)],
-        blank=True, null=True, verbose_name="État d'avancement",
-        help_text="État d'avancement du FAI")
+        blank=True,
+        null=True,
+        verbose_name="État d'avancement",
+        help_text="État d'avancement du FAI",
+    )
     # TODO: better model for coordinates
     # TODO: better model for coordinates
-    latitude = models.FloatField(blank=True, null=True,
-                                 verbose_name="Latitude",
-                                 help_text="Coordonnées latitudinales du siège")
-    longitude = models.FloatField(blank=True, null=True,
-                                  verbose_name="Longitude",
-                                  help_text="Coordonnées longitudinales du siège")
+    latitude = models.FloatField(
+        blank=True,
+        null=True,
+        verbose_name="Latitude",
+        help_text="Coordonnées latitudinales du siège",
+    )
+    longitude = models.FloatField(
+        blank=True,
+        null=True,
+        verbose_name="Longitude",
+        help_text="Coordonnées longitudinales du siège",
+    )
 
 
     # Uncomment this (and handle the necessary migrations) if you want to
     # Uncomment this (and handle the necessary migrations) if you want to
     # manage one of the counters by hand.  Otherwise, they are computed
     # manage one of the counters by hand.  Otherwise, they are computed
     # automatically, which is probably what you want.
     # automatically, which is probably what you want.
-    #memberCount = models.PositiveIntegerField(help_text="Nombre de membres",
+    # memberCount = models.PositiveIntegerField(help_text="Nombre de membres",
     #                                          default=0)
     #                                          default=0)
-    #subscriberCount = models.PositiveIntegerField(
+    # subscriberCount = models.PositiveIntegerField(
     #    help_text="Nombre d'abonnés à un accès Internet",
     #    help_text="Nombre d'abonnés à un accès Internet",
     #    default=0)
     #    default=0)
 
 
     # field outside of db-ffdn format:
     # field outside of db-ffdn format:
     administrative_email = models.EmailField(
     administrative_email = models.EmailField(
-        blank=True, verbose_name="contact administratif",
-        help_text='Adresse email pour les contacts administratifs (ex: bureau)')
+        blank=True,
+        verbose_name="contact administratif",
+        help_text="Adresse email pour les contacts administratifs (ex: bureau)",
+    )
 
 
     support_email = models.EmailField(
     support_email = models.EmailField(
-        blank=True, verbose_name="contact de support",
-        help_text="Adresse email pour les demandes de support technique")
+        blank=True,
+        verbose_name="contact de support",
+        help_text="Adresse email pour les demandes de support technique",
+    )
 
 
     lists_url = models.URLField(
     lists_url = models.URLField(
-        verbose_name="serveur de listes", blank=True,
-        help_text="URL du serveur de listes de discussions/diffusion")
+        verbose_name="serveur de listes",
+        blank=True,
+        help_text="URL du serveur de listes de discussions/diffusion",
+    )
 
 
     class Meta:
     class Meta:
         verbose_name = "Information du FAI"
         verbose_name = "Information du FAI"
@@ -132,45 +167,53 @@ class ISPInfo(SingleInstanceMixin, models.Model):
         if first_chatroom:
         if first_chatroom:
             m = utils.re_chat_url.match(first_chatroom.url)
             m = utils.re_chat_url.match(first_chatroom.url)
             if m:
             if m:
-                return '{channel} sur {server}'.format(**(m.groupdict()))
+                return "{channel} sur {server}".format(**(m.groupdict()))
 
 
         return None
         return None
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return '/isp.json'
+        return "/isp.json"
 
 
     def to_dict(self):
     def to_dict(self):
         data = dict()
         data = dict()
         # These are required
         # These are required
-        for f in ('version', 'name', 'email', 'memberCount', 'subscriberCount'):
+        for f in ("version", "name", "email", "memberCount", "subscriberCount"):
             data[f] = getattr(self, f)
             data[f] = getattr(self, f)
 
 
         # These are optional
         # These are optional
-        for f in ('shortname', 'description', 'logoURL', 'website',
-                  'mainMailingList', 'progressStatus'):
+        for f in (
+            "shortname",
+            "description",
+            "logoURL",
+            "website",
+            "mainMailingList",
+            "progressStatus",
+        ):
             if getattr(self, f):
             if getattr(self, f):
                 data[f] = getattr(self, f)
                 data[f] = getattr(self, f)
 
 
         # Dates
         # Dates
-        for d in ('creationDate', 'ffdnMemberSince'):
+        for d in ("creationDate", "ffdnMemberSince"):
             if getattr(self, d):
             if getattr(self, d):
                 data[d] = getattr(self, d).isoformat()
                 data[d] = getattr(self, d).isoformat()
 
 
         # Hackish for now
         # Hackish for now
         if self.latitude or self.longitude:
         if self.latitude or self.longitude:
-            data['coordinates'] = { "latitude": self.latitude,
-                                    "longitude": self.longitude }
+            data["coordinates"] = {
+                "latitude": self.latitude,
+                "longitude": self.longitude,
+            }
 
 
         # Related objects
         # Related objects
-        data['coveredAreas'] = [c.to_dict() for c in self.coveredarea_set.all()]
+        data["coveredAreas"] = [c.to_dict() for c in self.coveredarea_set.all()]
         otherwebsites = self.otherwebsite_set.all()
         otherwebsites = self.otherwebsite_set.all()
         if otherwebsites:
         if otherwebsites:
-            data['otherWebsites'] = { site.name: site.url for site in otherwebsites }
+            data["otherWebsites"] = {site.name: site.url for site in otherwebsites}
         chatrooms = self.chatroom_set.all()
         chatrooms = self.chatroom_set.all()
         if chatrooms:
         if chatrooms:
-            data['chatrooms'] = [chatroom.url for chatroom in chatrooms]
-        if hasattr(self, 'registeredoffice'):
-            data['registeredOffice'] = self.registeredoffice.to_dict()
+            data["chatrooms"] = [chatroom.url for chatroom in chatrooms]
+        if hasattr(self, "registeredoffice"):
+            data["registeredOffice"] = self.registeredoffice.to_dict()
 
 
         return data
         return data
 
 
@@ -190,17 +233,26 @@ class OtherWebsite(models.Model):
 
 
 class RegisteredOffice(models.Model):
 class RegisteredOffice(models.Model):
     """ http://json-schema.org/address """
     """ http://json-schema.org/address """
-    post_office_box = models.CharField(max_length=512, blank=True, verbose_name="Boîte postale")
-    extended_address = models.CharField(max_length=512, blank=True, verbose_name="Adresse complémentaire")
-    street_address = models.CharField(max_length=512, blank=True, verbose_name="Adresse")
+
+    post_office_box = models.CharField(
+        max_length=512, blank=True, verbose_name="Boîte postale"
+    )
+    extended_address = models.CharField(
+        max_length=512, blank=True, verbose_name="Adresse complémentaire"
+    )
+    street_address = models.CharField(
+        max_length=512, blank=True, verbose_name="Adresse"
+    )
     locality = models.CharField(max_length=512, verbose_name="Ville")
     locality = models.CharField(max_length=512, verbose_name="Ville")
     region = models.CharField(max_length=512, verbose_name="Région")
     region = models.CharField(max_length=512, verbose_name="Région")
-    postal_code = models.CharField(max_length=512, blank=True, verbose_name="Code postal")
+    postal_code = models.CharField(
+        max_length=512, blank=True, verbose_name="Code postal"
+    )
     country_name = models.CharField(max_length=512, verbose_name="Pays")
     country_name = models.CharField(max_length=512, verbose_name="Pays")
     isp = models.OneToOneField(ISPInfo)
     isp = models.OneToOneField(ISPInfo)
 
 
     # not in db.ffdn.org spec
     # not in db.ffdn.org spec
-    siret = FRSIRETField('SIRET')
+    siret = FRSIRETField("SIRET")
 
 
     class Meta:
     class Meta:
         verbose_name = "Siège social"
         verbose_name = "Siège social"
@@ -208,17 +260,25 @@ class RegisteredOffice(models.Model):
 
 
     def to_dict(self):
     def to_dict(self):
         d = dict()
         d = dict()
-        for field in ('post_office_box', 'extended_address', 'street_address',
-                      'locality', 'region', 'postal_code', 'country_name'):
+        for field in (
+            "post_office_box",
+            "extended_address",
+            "street_address",
+            "locality",
+            "region",
+            "postal_code",
+            "country_name",
+        ):
             if getattr(self, field):
             if getattr(self, field):
-                key = field.replace('_', '-')
+                key = field.replace("_", "-")
                 d[key] = getattr(self, field)
                 d[key] = getattr(self, field)
         return d
         return d
 
 
 
 
 class ChatRoom(models.Model):
 class ChatRoom(models.Model):
     url = models.CharField(
     url = models.CharField(
-        verbose_name="URL", max_length=256, validators=[chatroom_url_validator])
+        verbose_name="URL", max_length=256, validators=[chatroom_url_validator]
+    )
     isp = models.ForeignKey(ISPInfo)
     isp = models.ForeignKey(ISPInfo)
 
 
     class Meta:
     class Meta:
@@ -229,14 +289,15 @@ class ChatRoom(models.Model):
 class CoveredArea(models.Model):
 class CoveredArea(models.Model):
     name = models.CharField(max_length=512, verbose_name="Nom")
     name = models.CharField(max_length=512, verbose_name="Nom")
 
 
-    technologies = MultiSelectField(choices=TECHNOLOGIES, max_length=42, verbose_name="Technologie")
+    technologies = MultiSelectField(
+        choices=TECHNOLOGIES, max_length=42, verbose_name="Technologie"
+    )
     # TODO: find a geojson library
     # TODO: find a geojson library
-    #area =
+    # area =
     isp = models.ForeignKey(ISPInfo)
     isp = models.ForeignKey(ISPInfo)
 
 
     def to_dict(self):
     def to_dict(self):
-        return {"name": self.name,
-                "technologies": self.technologies}
+        return {"name": self.name, "technologies": self.technologies}
 
 
     class Meta:
     class Meta:
         verbose_name = "Zone couverte"
         verbose_name = "Zone couverte"
@@ -248,17 +309,23 @@ class BankInfo(models.Model):
 
 
     This is out of the scope of db.ffdn.org spec.
     This is out of the scope of db.ffdn.org spec.
     """
     """
+
     isp = models.OneToOneField(ISPInfo)
     isp = models.OneToOneField(ISPInfo)
-    iban = IBANField('IBAN')
-    bic = BICField('BIC', blank=True, null=True)
-    bank_name = models.CharField('établissement bancaire',
-                                 max_length=100, blank=True, null=True)
-    check_order = models.CharField('ordre',
-                                   max_length=100, blank=False, null=False,
-                                   help_text='Ordre devant figurer sur un \
+    iban = IBANField("IBAN")
+    bic = BICField("BIC", blank=True, null=True)
+    bank_name = models.CharField(
+        "établissement bancaire", max_length=100, blank=True, null=True
+    )
+    check_order = models.CharField(
+        "ordre",
+        max_length=100,
+        blank=False,
+        null=False,
+        help_text="Ordre devant figurer sur un \
                                    chèque bancaire à destination de\
                                    chèque bancaire à destination de\
-                                   l\'association')
+                                   l'association",
+    )
 
 
     class Meta:
     class Meta:
-        verbose_name = 'coordonnées bancaires'
+        verbose_name = "coordonnées bancaires"
         verbose_name_plural = verbose_name
         verbose_name_plural = verbose_name

+ 6 - 4
coin/isp_database/templatetags/isptags.py

@@ -2,11 +2,13 @@ from django.template import Template, Library
 
 
 register = Library()
 register = Library()
 
 
-@register.inclusion_tag('isp_database/includes/isp_address_multiline.html')
+
+@register.inclusion_tag("isp_database/includes/isp_address_multiline.html")
 def multiline_isp_addr(branding):
 def multiline_isp_addr(branding):
-    return {'branding': branding}
+    return {"branding": branding}
+
 
 
 @register.filter
 @register.filter
 def pretty_iban(s):
 def pretty_iban(s):
-    #FR764 2559 0001 2410 2002 3285 19
-    return ' '.join([s[i:i+4] for i in xrange(0, len(s), 4)])
+    # FR764 2559 0001 2410 2002 3285 19
+    return " ".join([s[i : i + 4] for i in xrange(0, len(s), 4)])

+ 15 - 12
coin/isp_database/tests.py

@@ -7,6 +7,7 @@ from coin.members.models import Member
 from coin.isp_database.templatetags.isptags import *
 from coin.isp_database.templatetags.isptags import *
 from .models import ChatRoom, ISPInfo
 from .models import ChatRoom, ISPInfo
 
 
+
 class TestPrettifiers(TestCase):
 class TestPrettifiers(TestCase):
     def test_pretty_iban(self):
     def test_pretty_iban(self):
         """ Prints pretty readable IBAN
         """ Prints pretty readable IBAN
@@ -14,28 +15,30 @@ class TestPrettifiers(TestCase):
         Takes the IBAN in compact form and displays it according to the display spec
         Takes the IBAN in compact form and displays it according to the display spec
         See http://en.wikipedia.org/wiki/International_Bank_Account_Number#Practicalities
         See http://en.wikipedia.org/wiki/International_Bank_Account_Number#Practicalities
         """
         """
-        self.assertEqual(pretty_iban('DEkkBBBBBBBBCCCCCCCCCC'),
-                         'DEkk BBBB BBBB CCCC CCCC CC')
-        self.assertEqual(pretty_iban('ADkkBBBBSSSSCCCCCCCCCCCC'),
-                         'ADkk BBBB SSSS CCCC CCCC CCCC')
-        self.assertEqual(pretty_iban(''), '')
+        self.assertEqual(
+            pretty_iban("DEkkBBBBBBBBCCCCCCCCCC"), "DEkk BBBB BBBB CCCC CCCC CC"
+        )
+        self.assertEqual(
+            pretty_iban("ADkkBBBBSSSSCCCCCCCCCCCC"), "ADkk BBBB SSSS CCCC CCCC CCCC"
+        )
+        self.assertEqual(pretty_iban(""), "")
+
 
 
 class TestContactPage(TestCase):
 class TestContactPage(TestCase):
     def setUp(self):
     def setUp(self):
         # Could be replaced by a force_login when we will be at Django 1.9
         # 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')
+        Member.objects.create_user("user", password="password")
+        self.client.login(username="user", password="password")
 
 
     def test_chat_view(self):
     def test_chat_view(self):
-        isp = ISPInfo.objects.create(name='test', email='foo@example.com', )
+        isp = ISPInfo.objects.create(name="test", email="foo@example.com")
 
 
         # Without chatroom
         # Without chatroom
-        response = self.client.get('/members/contact/')
+        response = self.client.get("/members/contact/")
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # With chatroom
         # With chatroom
-        ChatRoom.objects.create(
-            isp=isp, url='irc://irc.example.com/#chan')
+        ChatRoom.objects.create(isp=isp, url="irc://irc.example.com/#chan")
 
 
-        response = self.client.get('/members/contact/')
+        response = self.client.get("/members/contact/")
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 160 - 92
coin/members/admin.py

@@ -15,7 +15,14 @@ from django.core.urlresolvers import reverse
 from django.utils.html import format_html
 from django.utils.html import format_html
 
 
 from coin.members.models import (
 from coin.members.models import (
-    Member, CryptoKey, LdapUser, MembershipFee, Offer, OfferSubscription, RowLevelPermission)
+    Member,
+    CryptoKey,
+    LdapUser,
+    MembershipFee,
+    Offer,
+    OfferSubscription,
+    RowLevelPermission,
+)
 from coin.members.membershipfee_filter import MembershipFeeFilter
 from coin.members.membershipfee_filter import MembershipFeeFilter
 from coin.members.forms import AdminMemberChangeForm, MemberCreationForm
 from coin.members.forms import AdminMemberChangeForm, MemberCreationForm
 from coin.utils import delete_selected
 from coin.utils import delete_selected
@@ -30,16 +37,22 @@ class CryptoKeyInline(admin.StackedInline):
 class MembershipFeeInline(admin.TabularInline):
 class MembershipFeeInline(admin.TabularInline):
     model = MembershipFee
     model = MembershipFee
     extra = 0
     extra = 0
-    fields = ('start_date', 'end_date', 'amount', 'payment_method',
-              'reference', 'payment_date')
+    fields = (
+        "start_date",
+        "end_date",
+        "amount",
+        "payment_method",
+        "reference",
+        "payment_date",
+    )
 
 
 
 
 class OfferSubscriptionInline(admin.TabularInline):
 class OfferSubscriptionInline(admin.TabularInline):
     model = OfferSubscription
     model = OfferSubscription
     extra = 0
     extra = 0
 
 
-    writable_fields = ('subscription_date', 'resign_date', 'commitment', 'offer')
-    all_fields = ('get_subscription_reference',) + writable_fields
+    writable_fields = ("subscription_date", "resign_date", "commitment", "offer")
+    all_fields = ("get_subscription_reference",) + writable_fields
 
 
     def get_fields(self, request, obj=None):
     def get_fields(self, request, obj=None):
         if obj:
         if obj:
@@ -50,7 +63,7 @@ class OfferSubscriptionInline(admin.TabularInline):
     def get_readonly_fields(self, request, obj=None):
     def get_readonly_fields(self, request, obj=None):
         # création ou superuser : lecture écriture
         # création ou superuser : lecture écriture
         if not obj or request.user.is_superuser:
         if not obj or request.user.is_superuser:
-            return ('get_subscription_reference',)
+            return ("get_subscription_reference",)
         # modification : lecture seule seulement
         # modification : lecture seule seulement
         else:
         else:
             return self.all_fields
             return self.all_fields
@@ -59,11 +72,15 @@ class OfferSubscriptionInline(admin.TabularInline):
 
 
     def formfield_for_foreignkey(self, db_field, request, **kwargs):
     def formfield_for_foreignkey(self, db_field, request, **kwargs):
         if request.user.is_superuser:
         if request.user.is_superuser:
-            return super(OfferSubscriptionInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
+            return super(OfferSubscriptionInline, self).formfield_for_foreignkey(
+                db_field, request, **kwargs
+            )
         else:
         else:
             if db_field.name == "offer":
             if db_field.name == "offer":
                 kwargs["queryset"] = Offer.objects.manageable_by(request.user)
                 kwargs["queryset"] = Offer.objects.manageable_by(request.user)
-            return super(OfferSubscriptionInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
+            return super(OfferSubscriptionInline, self).formfield_for_foreignkey(
+                db_field, request, **kwargs
+            )
 
 
     def has_add_permission(self, request):
     def has_add_permission(self, request):
         # - Quand on *crée* un membre on autorise à ajouter un abonnement
         # - Quand on *crée* un membre on autorise à ajouter un abonnement
@@ -72,9 +89,8 @@ class OfferSubscriptionInline(admin.TabularInline):
         #   toute fiche adhérent en lui ajoutant un abonnement à une offre dont
         #   toute fiche adhérent en lui ajoutant un abonnement à une offre dont
         #   on a la gestion).
         #   on a la gestion).
         return (
         return (
-            request.resolver_match.view_name == 'admin:members_member_add'
-            or
-            request.user.is_superuser
+            request.resolver_match.view_name == "admin:members_member_add"
+            or request.user.is_superuser
         )
         )
 
 
     # sinon on pourrait supprimer les abo qu'on ne peut pas gérer
     # sinon on pourrait supprimer les abo qu'on ne peut pas gérer
@@ -84,59 +100,90 @@ class OfferSubscriptionInline(admin.TabularInline):
 
 
 
 
 class MemberAdmin(UserAdmin):
 class MemberAdmin(UserAdmin):
-    list_display = ('id', 'status', 'username', 'first_name', 'last_name',
-                    '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', 'nickname']
-    ordering = ('status', 'username')
-    actions = [delete_selected, 'set_as_member', 'set_as_non_member',
-               'bulk_send_welcome_email', 'bulk_send_call_for_membership_fee_email']
+    list_display = (
+        "id",
+        "status",
+        "username",
+        "first_name",
+        "last_name",
+        "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", "nickname"]
+    ordering = ("status", "username")
+    actions = [
+        delete_selected,
+        "set_as_member",
+        "set_as_non_member",
+        "bulk_send_welcome_email",
+        "bulk_send_call_for_membership_fee_email",
+    ]
 
 
     form = AdminMemberChangeForm
     form = AdminMemberChangeForm
     add_form = MemberCreationForm
     add_form = MemberCreationForm
 
 
     def get_fieldsets(self, request, obj=None):
     def get_fieldsets(self, request, obj=None):
-        coord_fieldset = ('Coordonnées', {'fields': (
-            ('email', 'send_membership_fees_email'),
-            ('home_phone_number', 'mobile_phone_number'),
-            'address',
-            ('postal_code', 'city', 'country'))})
-        auth_fieldset = ('Authentification', {'fields': (
-            ('username', 'password'))})
-        perm_fieldset = ('Permissions', {'fields': (
-            ('is_active', 'is_staff', 'is_superuser', 'groups'))})
+        coord_fieldset = (
+            "Coordonnées",
+            {
+                "fields": (
+                    ("email", "send_membership_fees_email"),
+                    ("home_phone_number", "mobile_phone_number"),
+                    "address",
+                    ("postal_code", "city", "country"),
+                )
+            },
+        )
+        auth_fieldset = ("Authentification", {"fields": (("username", "password"))})
+        perm_fieldset = (
+            "Permissions",
+            {"fields": (("is_active", "is_staff", "is_superuser", "groups"))},
+        )
 
 
         # if obj is null then it is a creation, otherwise it is a modification
         # if obj is null then it is a creation, otherwise it is a modification
         if obj:
         if obj:
             fieldsets = (
             fieldsets = (
-                ('Adhérent', {'fields': (
-                    ('status', 'date_joined', 'resign_date'),
-                    'type',
-                    ('first_name', 'last_name', 'nickname'),
-                    'organization_name',
-                    'comments'
-                )}),
+                (
+                    "Adhérent",
+                    {
+                        "fields": (
+                            ("status", "date_joined", "resign_date"),
+                            "type",
+                            ("first_name", "last_name", "nickname"),
+                            "organization_name",
+                            "comments",
+                        )
+                    },
+                ),
                 coord_fieldset,
                 coord_fieldset,
                 auth_fieldset,
                 auth_fieldset,
                 perm_fieldset,
                 perm_fieldset,
-                (None, {'fields': ('date_last_call_for_membership_fees_email',)})
+                (None, {"fields": ("date_last_call_for_membership_fees_email",)}),
             )
             )
         else:
         else:
             fieldsets = (
             fieldsets = (
-                ('Adhérent', {'fields': (
-                    ('status', 'date_joined'),
-                    'type',
-                    ('first_name', 'last_name', 'nickname'),
-                    'organization_name',
-                    'comments')}),
+                (
+                    "Adhérent",
+                    {
+                        "fields": (
+                            ("status", "date_joined"),
+                            "type",
+                            ("first_name", "last_name", "nickname"),
+                            "organization_name",
+                            "comments",
+                        )
+                    },
+                ),
                 coord_fieldset,
                 coord_fieldset,
                 auth_fieldset,
                 auth_fieldset,
-                perm_fieldset
+                perm_fieldset,
             )
             )
         if settings.HANDLE_BALANCE:
         if settings.HANDLE_BALANCE:
-            fieldsets[0][1]['fields'] += ('balance',)
+            fieldsets[0][1]["fields"] += ("balance",)
         return fieldsets
         return fieldsets
 
 
     radio_fields = {"type": admin.HORIZONTAL}
     radio_fields = {"type": admin.HORIZONTAL}
@@ -158,36 +205,45 @@ class MemberAdmin(UserAdmin):
         if obj:
         if obj:
             # Remove help_text for readonly field (can't do that in the Form
             # Remove help_text for readonly field (can't do that in the Form
             # django seems to user help_text from model for readonly fields)
             # django seems to user help_text from model for readonly fields)
-            username_field = [
-                f for f in obj._meta.fields if f.name == 'username']
-            username_field[0].help_text = ''
+            username_field = [f for f in obj._meta.fields if f.name == "username"]
+            username_field[0].help_text = ""
 
 
-            readonly_fields.append('username')
+            readonly_fields.append("username")
         if not request.user.is_superuser:
         if not request.user.is_superuser:
-            readonly_fields += ['is_active', 'is_staff', 'is_superuser', 'groups', 'date_last_call_for_membership_fees_email']
+            readonly_fields += [
+                "is_active",
+                "is_staff",
+                "is_superuser",
+                "groups",
+                "date_last_call_for_membership_fees_email",
+            ]
         return readonly_fields
         return readonly_fields
 
 
     def set_as_member(self, request, queryset):
     def set_as_member(self, request, queryset):
-        rows_updated = queryset.update(status='member')
+        rows_updated = queryset.update(status="member")
         self.message_user(
         self.message_user(
-            request,
-            '%d membre(s) définis comme adhérent(s).' % rows_updated)
-    set_as_member.short_description = 'Définir comme adhérent'
+            request, "%d membre(s) définis comme adhérent(s)." % rows_updated
+        )
+
+    set_as_member.short_description = "Définir comme adhérent"
 
 
     def set_as_non_member(self, request, queryset):
     def set_as_non_member(self, request, queryset):
-        rows_updated = queryset.update(status='not_member')
+        rows_updated = queryset.update(status="not_member")
         self.message_user(
         self.message_user(
-            request,
-            '%d membre(s) définis comme non adhérent(s).' % rows_updated)
+            request, "%d membre(s) définis comme non adhérent(s)." % rows_updated
+        )
+
     set_as_non_member.short_description = "Définir comme non adhérent"
     set_as_non_member.short_description = "Définir comme non adhérent"
 
 
     def get_urls(self):
     def get_urls(self):
         """Custom admin urls"""
         """Custom admin urls"""
         urls = super(MemberAdmin, self).get_urls()
         urls = super(MemberAdmin, self).get_urls()
         my_urls = [
         my_urls = [
-            url(r'^send_welcome_email/(?P<id>\d+)$',
+            url(
+                r"^send_welcome_email/(?P<id>\d+)$",
                 self.admin_site.admin_view(self.send_welcome_email),
                 self.admin_site.admin_view(self.send_welcome_email),
-                name='send_welcome_email'),
+                name="send_welcome_email",
+            )
         ]
         ]
         return my_urls + urls
         return my_urls + urls
 
 
@@ -200,15 +256,18 @@ class MemberAdmin(UserAdmin):
         if request.user.is_superuser:
         if request.user.is_superuser:
             member = get_object_or_404(Member, pk=id)
             member = get_object_or_404(Member, pk=id)
             member.send_welcome_email()
             member.send_welcome_email()
-            messages.success(request,
-                             'Le courriel de bienvenue a été envoyé à %s' % member.email)
+            messages.success(
+                request, "Le courriel de bienvenue a été envoyé à %s" % member.email
+            )
         else:
         else:
             messages.error(
             messages.error(
-                request, 'Vous n\'avez pas l\'autorisation d\'envoyer des '
-                         'courriels de bienvenue.')
+                request,
+                "Vous n'avez pas l'autorisation d'envoyer des "
+                "courriels de bienvenue.",
+            )
 
 
         if return_httpredirect:
         if return_httpredirect:
-            return HttpResponseRedirect(reverse('admin:members_member_changelist'))
+            return HttpResponseRedirect(reverse("admin:members_member_changelist"))
 
 
     def bulk_send_welcome_email(self, request, queryset):
     def bulk_send_welcome_email(self, request, queryset):
         """
         """
@@ -216,18 +275,21 @@ class MemberAdmin(UserAdmin):
         depuis une sélection de membre dans la vue liste de l'admin
         depuis une sélection de membre dans la vue liste de l'admin
         """
         """
         for member in queryset.all():
         for member in queryset.all():
-            self.send_welcome_email(
-                request, member.id, return_httpredirect=False)
-        messages.success(request,
-                         'Le courriel de bienvenue a été envoyé à %d membre(s).' % queryset.count())
+            self.send_welcome_email(request, member.id, return_httpredirect=False)
+        messages.success(
+            request,
+            "Le courriel de bienvenue a été envoyé à %d membre(s)." % queryset.count(),
+        )
+
     bulk_send_welcome_email.short_description = "Envoyer le courriel de bienvenue"
     bulk_send_welcome_email.short_description = "Envoyer le courriel de bienvenue"
 
 
     def bulk_send_call_for_membership_fee_email(self, request, queryset):
     def bulk_send_call_for_membership_fee_email(self, request, queryset):
         # TODO : Add better perm here
         # TODO : Add better perm here
         if not request.user.is_superuser:
         if not request.user.is_superuser:
             messages.error(
             messages.error(
-                request, 'Vous n\'avez pas l\'autorisation d\'envoyer des '
-                         'courriels de relance.')
+                request,
+                "Vous n'avez pas l'autorisation d'envoyer des " "courriels de relance.",
+            )
             return
             return
         cpt_success = 0
         cpt_success = 0
         for member in queryset.all():
         for member in queryset.all():
@@ -235,38 +297,44 @@ class MemberAdmin(UserAdmin):
             if member.send_call_for_membership_fees_email():
             if member.send_call_for_membership_fees_email():
                 cpt_success += 1
                 cpt_success += 1
             else:
             else:
-                messages.warning(request,
-                              "Le courriel de relance de cotisation n\'a pas "
-                              "été envoyé à {member} ({email}) car il a déjà "
-                              "reçu une relance le {last_call_date}"\
-                              .format(member=member,
-                                     email=member.email,
-                                     last_call_date=member.date_last_call_for_membership_fees_email))
+                messages.warning(
+                    request,
+                    "Le courriel de relance de cotisation n'a pas "
+                    "été envoyé à {member} ({email}) car il a déjà "
+                    "reçu une relance le {last_call_date}".format(
+                        member=member,
+                        email=member.email,
+                        last_call_date=member.date_last_call_for_membership_fees_email,
+                    ),
+                )
 
 
         if queryset.count() == 1 and cpt_success == 1:
         if queryset.count() == 1 and cpt_success == 1:
             member = queryset.first()
             member = queryset.first()
-            messages.success(request,
-                             "Le courriel de relance de cotisation a été "
-                             "envoyé à {member} ({email})"\
-                             .format(member=member, email=member.email))
-        elif cpt_success>1:
-            messages.success(request,
-                             "Le courriel de relance de cotisation a été "
-                             "envoyé à {cpt} membres"\
-                             .format(cpt=cpt_success))
+            messages.success(
+                request,
+                "Le courriel de relance de cotisation a été "
+                "envoyé à {member} ({email})".format(member=member, email=member.email),
+            )
+        elif cpt_success > 1:
+            messages.success(
+                request,
+                "Le courriel de relance de cotisation a été "
+                "envoyé à {cpt} membres".format(cpt=cpt_success),
+            )
 
 
-    bulk_send_call_for_membership_fee_email.short_description = 'Envoyer le courriel de relance de cotisation'
+    bulk_send_call_for_membership_fee_email.short_description = (
+        "Envoyer le courriel de relance de cotisation"
+    )
 
 
 
 
 class MembershipFeeAdmin(admin.ModelAdmin):
 class MembershipFeeAdmin(admin.ModelAdmin):
-    list_display = ('member', 'end_date', 'amount', 'payment_method',
-                    'payment_date')
-    form = autocomplete_light.modelform_factory(MembershipFee, fields='__all__')
+    list_display = ("member", "end_date", "amount", "payment_method", "payment_date")
+    form = autocomplete_light.modelform_factory(MembershipFee, fields="__all__")
+
 
 
 class RowLevelPermissionAdmin(admin.ModelAdmin):
 class RowLevelPermissionAdmin(admin.ModelAdmin):
     def get_changeform_initial_data(self, request):
     def get_changeform_initial_data(self, request):
-        return {'content_type': ContentType.objects.get_for_model(OfferSubscription)}
-
+        return {"content_type": ContentType.objects.get_for_model(OfferSubscription)}
 
 
 
 
 admin.site.register(Member, MemberAdmin)
 admin.site.register(Member, MemberAdmin)

+ 17 - 12
coin/members/autocomplete_light_registry.py

@@ -5,16 +5,21 @@ import autocomplete_light
 from models import Member
 from models import Member
 
 
 # This will generate a MemberAutocomplete class
 # This will generate a MemberAutocomplete class
-autocomplete_light.register(Member,
-                            # Just like in ModelAdmin.search_fields
-                            search_fields=[
-                                '^first_name', '^last_name', 'organization_name',
-                                '^username', '^nickname'],
-                            attrs={
-                                # This will set the input placeholder attribute:
-                                'placeholder': 'Nom/Prénom/Pseudo (min 3 caractères)',
-                                # Nombre minimum de caractères à saisir avant de compléter.
-                                # Fixé à 3 pour ne pas qu'on puisse avoir accès à la liste de tous les membres facilement quand on n'est pas superuser.
-                                'data-autocomplete-minimum-characters': 3,
-                            },
+autocomplete_light.register(
+    Member,
+    # Just like in ModelAdmin.search_fields
+    search_fields=[
+        "^first_name",
+        "^last_name",
+        "organization_name",
+        "^username",
+        "^nickname",
+    ],
+    attrs={
+        # This will set the input placeholder attribute:
+        "placeholder": "Nom/Prénom/Pseudo (min 3 caractères)",
+        # Nombre minimum de caractères à saisir avant de compléter.
+        # Fixé à 3 pour ne pas qu'on puisse avoir accès à la liste de tous les membres facilement quand on n'est pas superuser.
+        "data-autocomplete-minimum-characters": 3,
+    },
 )
 )

+ 50 - 19
coin/members/forms.py

@@ -14,18 +14,26 @@ class MemberCreationForm(forms.ModelForm):
     This form was inspired from django.contrib.auth.forms.UserCreationForm
     This form was inspired from django.contrib.auth.forms.UserCreationForm
     and adapted to coin specificities
     and adapted to coin specificities
     """
     """
-    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 d'usage, nom et prénom, ou nom de l'organisme")
+
+    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 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 "
         help_text="Laisser vide et envoyer un mail de bienvenue pour que "
-        "l'utilisateur choisisse son mot de passe lui-même")
+        "l'utilisateur choisisse son mot de passe lui-même",
+    )
 
 
     class Meta:
     class Meta:
         model = Member
         model = Member
-        fields = '__all__'
+        fields = "__all__"
 
 
     def save(self, commit=True):
     def save(self, commit=True):
         """
         """
@@ -46,13 +54,13 @@ class AbstractMemberChangeForm(forms.ModelForm):
 
 
     class Meta:
     class Meta:
         model = Member
         model = Member
-        fields = '__all__'
+        fields = "__all__"
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super(AbstractMemberChangeForm, self).__init__(*args, **kwargs)
         super(AbstractMemberChangeForm, self).__init__(*args, **kwargs)
-        f = self.fields.get('user_permissions', None)
+        f = self.fields.get("user_permissions", None)
         if f is not None:
         if f is not None:
-            f.queryset = f.queryset.select_related('content_type')
+            f.queryset = f.queryset.select_related("content_type")
 
 
     def clean_password(self):
     def clean_password(self):
         # Regardless of what the user provides, return the initial value.
         # Regardless of what the user provides, return the initial value.
@@ -72,21 +80,35 @@ class AdminMemberChangeForm(AbstractMemberChangeForm):
 class SpanError(ErrorList):
 class SpanError(ErrorList):
     def __unicode__(self):
     def __unicode__(self):
         return self.as_spans()
         return self.as_spans()
+
     def __str__(self):
     def __str__(self):
         return self.as_spans()
         return self.as_spans()
+
     def as_spans(self):
     def as_spans(self):
-        if not self: return ''
-        return ''.join(['<span class="error">%s</span>' % e for e in self])
+        if not self:
+            return ""
+        return "".join(['<span class="error">%s</span>' % e for e in self])
+
 
 
 class PersonMemberChangeForm(AbstractMemberChangeForm):
 class PersonMemberChangeForm(AbstractMemberChangeForm):
     """
     """
     Form use to allow natural person to change their info
     Form use to allow natural person to change their info
     """
     """
+
     class Meta:
     class Meta:
         model = Member
         model = Member
-        fields = ['first_name', 'last_name', 'email', 'nickname',
-                  'home_phone_number', 'mobile_phone_number',
-                  'address', 'postal_code', 'city', 'country']
+        fields = [
+            "first_name",
+            "last_name",
+            "email",
+            "nickname",
+            "home_phone_number",
+            "mobile_phone_number",
+            "address",
+            "postal_code",
+            "city",
+            "country",
+        ]
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super(PersonMemberChangeForm, self).__init__(*args, **kwargs)
         super(PersonMemberChangeForm, self).__init__(*args, **kwargs)
@@ -99,11 +121,20 @@ class OrganizationMemberChangeForm(AbstractMemberChangeForm):
     """
     """
     Form use to allow organization to change their info
     Form use to allow organization to change their info
     """
     """
+
     class Meta:
     class Meta:
         model = Member
         model = Member
-        fields = ['organization_name', 'email', 'nickname',
-                  'home_phone_number', 'mobile_phone_number',
-                  'address', 'postal_code', 'city', 'country']
+        fields = [
+            "organization_name",
+            "email",
+            "nickname",
+            "home_phone_number",
+            "mobile_phone_number",
+            "address",
+            "postal_code",
+            "city",
+            "country",
+        ]
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super(OrganizationChangeForm, self).__init__(*args, **kwargs)
         super(OrganizationChangeForm, self).__init__(*args, **kwargs)
@@ -111,6 +142,6 @@ class OrganizationMemberChangeForm(AbstractMemberChangeForm):
         for fieldname in self.fields:
         for fieldname in self.fields:
             self.fields[fieldname].help_text = None
             self.fields[fieldname].help_text = None
 
 
+
 class MemberPasswordResetForm(PasswordResetForm):
 class MemberPasswordResetForm(PasswordResetForm):
     pass
     pass
-

+ 34 - 23
coin/members/management/commands/call_for_membership_fees.py

@@ -12,7 +12,7 @@ from coin.members.models import Member, MembershipFee
 
 
 
 
 class Command(BaseCommand):
 class Command(BaseCommand):
-    args = '[date=2011-07-04]'
+    args = "[date=2011-07-04]"
     help = """Send a call for membership email to members.
     help = """Send a call for membership email to members.
               A mail is sent when end date of membership
               A mail is sent when end date of membership
               reach the anniversary date, 1 month before and once a month
               reach the anniversary date, 1 month before and once a month
@@ -21,43 +21,54 @@ class Command(BaseCommand):
               can be passed as argument."""
               can be passed as argument."""
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
-        verbosity = int(options['verbosity'])
+        verbosity = int(options["verbosity"])
         try:
         try:
-            date = datetime.datetime.strptime(args[0], '%Y-%m-%d').date()
+            date = datetime.datetime.strptime(args[0], "%Y-%m-%d").date()
         except IndexError:
         except IndexError:
             date = datetime.date.today()
             date = datetime.date.today()
         except ValueError:
         except ValueError:
             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)"
+            )
 
 
-        end_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),
+        ]
 
 
         if verbosity >= 2:
         if verbosity >= 2:
-            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.filter(status='member')\
-                                .annotate(end=Max('membership_fees__end_date'))\
-                                .filter(end__in=end_dates)\
-                                .filter(send_membership_fees_email=True)
-        if verbosity >= 2:
             self.stdout.write(
             self.stdout.write(
-                "Got {number} members.".format(number=members.count()))
+                "Selecting members whose membership fee end at "
+                "the following dates : {dates}".format(
+                    dates=[str(d) for d in end_dates]
+                )
+            )
+
+        members = (
+            Member.objects.filter(status="member")
+            .annotate(end=Max("membership_fees__end_date"))
+            .filter(end__in=end_dates)
+            .filter(send_membership_fees_email=True)
+        )
+        if verbosity >= 2:
+            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(auto=True):
                 if member.send_call_for_membership_fees_email(auto=True):
                     self.stdout.write(
                     self.stdout.write(
-                        'Call for membership fees email was sent to {member} ({email})'.format(
-                            member=member, email=member.email))
+                        "Call for membership fees email was sent to {member} ({email})".format(
+                            member=member, email=member.email
+                        )
+                    )
                     cpt = cpt + 1
                     cpt = cpt + 1
 
 
         if cpt > 0 or verbosity >= 2:
         if cpt > 0 or verbosity >= 2:
-            self.stdout.write("{number} call for membership fees emails were "
-                              "sent".format(number=cpt))
+            self.stdout.write(
+                "{number} call for membership fees emails were "
+                "sent".format(number=cpt)
+            )

+ 23 - 17
coin/members/management/commands/members_email.py

@@ -10,49 +10,55 @@ from coin.members.models import Member
 from coin.offers.models import Offer
 from coin.offers.models import Offer
 from coin.offers.models import OfferSubscription
 from coin.offers.models import OfferSubscription
 
 
+
 class Command(BaseCommand):
 class Command(BaseCommand):
     help = """Returns email addresses of members in a format suitable for bulk importing in Sympa."""
     help = """Returns email addresses of members in a format suitable for bulk importing in Sympa."""
 
 
     def add_arguments(self, parser):
     def add_arguments(self, parser):
-        parser.add_argument('--subscribers', action='store_true',
-                            help='Return only the email addresses of subscribers to any offers.')
-        parser.add_argument('--offer', metavar='OFFER-ID or OFFER-REF',
-                            help='Return only the email addresses of subscribers to the specified offer')
+        parser.add_argument(
+            "--subscribers",
+            action="store_true",
+            help="Return only the email addresses of subscribers to any offers.",
+        )
+        parser.add_argument(
+            "--offer",
+            metavar="OFFER-ID or OFFER-REF",
+            help="Return only the email addresses of subscribers to the specified offer",
+        )
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
-        if options['subscribers']:
+        if options["subscribers"]:
             today = datetime.date.today()
             today = datetime.date.today()
-                        
+
             offer_subscriptions = OfferSubscription.objects.filter(
             offer_subscriptions = OfferSubscription.objects.filter(
-                Q(resign_date__gt=today)
-                | Q(resign_date__isnull=True)
+                Q(resign_date__gt=today) | Q(resign_date__isnull=True)
             )
             )
             members = [s.member for s in offer_subscriptions]
             members = [s.member for s in offer_subscriptions]
-        elif options['offer']:
+        elif options["offer"]:
             try:
             try:
                 # Try to find the offer by its reference
                 # Try to find the offer by its reference
-                offer = Offer.objects.get(reference=options['offer'])
+                offer = Offer.objects.get(reference=options["offer"])
             except Offer.DoesNotExist:
             except Offer.DoesNotExist:
                 try:
                 try:
                     # No reference found, maybe it's an offer_id
                     # No reference found, maybe it's an offer_id
-                    offer_id = int(options['offer'])
+                    offer_id = int(options["offer"])
                     offer = Offer.objects.get(pk=offer_id)
                     offer = Offer.objects.get(pk=offer_id)
                 except Offer.DoesNotExist:
                 except Offer.DoesNotExist:
-                    raise CommandError('Offer "%s" does not exist' % options['offer'])
+                    raise CommandError('Offer "%s" does not exist' % options["offer"])
                 except (IndexError, ValueError):
                 except (IndexError, ValueError):
-                    raise CommandError('Please enter a valid offer reference or id')
+                    raise CommandError("Please enter a valid offer reference or id")
             today = datetime.date.today()
             today = datetime.date.today()
 
 
             offer_subscriptions = OfferSubscription.objects.filter(
             offer_subscriptions = OfferSubscription.objects.filter(
-                 # Fetch all OfferSubscription to the given Offer
+                # Fetch all OfferSubscription to the given Offer
                 Q(offer=offer)
                 Q(offer=offer)
                 # Check if OfferSubscription isn't resigned
                 # Check if OfferSubscription isn't resigned
                 & (Q(resign_date__isnull=True) | Q(resign_date__gt=today))
                 & (Q(resign_date__isnull=True) | Q(resign_date__gt=today))
-            ).select_related('member')
+            ).select_related("member")
             members = [s.member for s in offer_subscriptions]
             members = [s.member for s in offer_subscriptions]
         else:
         else:
-            members = Member.objects.filter(status='member')
+            members = Member.objects.filter(status="member")
 
 
-        emails = list(set([m.email for m in members if m.status == 'member']))
+        emails = list(set([m.email for m in members if m.status == "member"]))
         for email in emails:
         for email in emails:
             self.stdout.write(email)
             self.stdout.write(email)

+ 10 - 11
coin/members/membershipfee_filter.py

@@ -8,10 +8,10 @@ import datetime
 class MembershipFeeFilter(SimpleListFilter):
 class MembershipFeeFilter(SimpleListFilter):
     # Human-readable title which will be displayed in the
     # Human-readable title which will be displayed in the
     # right admin sidebar just above the filter options.
     # right admin sidebar just above the filter options.
-    title = 'Cotisations'
+    title = "Cotisations"
 
 
     # Parameter for the filter that will be used in the URL query.
     # Parameter for the filter that will be used in the URL query.
-    parameter_name = 'fee'
+    parameter_name = "fee"
 
 
     def lookups(self, request, model_admin):
     def lookups(self, request, model_admin):
         """
         """
@@ -21,10 +21,7 @@ class MembershipFeeFilter(SimpleListFilter):
         human-readable name for the option that will appear
         human-readable name for the option that will appear
         in the right sidebar.
         in the right sidebar.
         """
         """
-        return (
-            ('paidup', 'À jour de cotisation'),
-            ('late', 'En retard'),
-        )
+        return (("paidup", "À jour de cotisation"), ("late", "En retard"))
 
 
     def queryset(self, request, queryset):
     def queryset(self, request, queryset):
         """
         """
@@ -32,11 +29,13 @@ class MembershipFeeFilter(SimpleListFilter):
         provided in the query string and retrievable via
         provided in the query string and retrievable via
         `self.value()`.
         `self.value()`.
         """
         """
-        if self.value() == 'paidup':
+        if self.value() == "paidup":
             return queryset.filter(
             return queryset.filter(
                 membership_fees__start_date__lte=datetime.date.today,
                 membership_fees__start_date__lte=datetime.date.today,
-                membership_fees__end_date__gte=datetime.date.today)
-        if self.value() == 'late':
-            return queryset.filter(status='member').exclude(
+                membership_fees__end_date__gte=datetime.date.today,
+            )
+        if self.value() == "late":
+            return queryset.filter(status="member").exclude(
                 membership_fees__start_date__lte=datetime.date.today,
                 membership_fees__start_date__lte=datetime.date.today,
-                membership_fees__end_date__gte=datetime.date.today)
+                membership_fees__end_date__gte=datetime.date.today,
+            )

+ 241 - 170
coin/members/models.py

@@ -23,7 +23,6 @@ from coin.mixins import CoinLdapSyncMixin
 from coin import utils
 from coin import utils
 
 
 
 
-
 class MemberManager(UserManager):
 class MemberManager(UserManager):
     def manageable_by(self, user):
     def manageable_by(self, user):
         """" Renvoie la liste des members que l'utilisateur est autorisé à voir
         """" Renvoie la liste des members que l'utilisateur est autorisé à voir
@@ -33,68 +32,100 @@ class MemberManager(UserManager):
             return super(MemberManager, self).all()
             return super(MemberManager, self).all()
         else:
         else:
             offers = Offer.objects.manageable_by(user)
             offers = Offer.objects.manageable_by(user)
-            return super(MemberManager, self).filter(offersubscription__offer__in=offers).distinct()
+            return (
+                super(MemberManager, self)
+                .filter(offersubscription__offer__in=offers)
+                .distinct()
+            )
 
 
 
 
 class Member(CoinLdapSyncMixin, AbstractUser):
 class Member(CoinLdapSyncMixin, AbstractUser):
 
 
     # USERNAME_FIELD = 'login'
     # USERNAME_FIELD = 'login'
-    REQUIRED_FIELDS = ['first_name', 'last_name', 'email', ]
+    REQUIRED_FIELDS = ["first_name", "last_name", "email"]
 
 
     MEMBER_TYPE_CHOICES = (
     MEMBER_TYPE_CHOICES = (
-        ('natural_person', 'Personne physique'),
-        ('legal_entity', 'Personne morale'),
+        ("natural_person", "Personne physique"),
+        ("legal_entity", "Personne morale"),
     )
     )
     MEMBER_STATUS_CHOICES = (
     MEMBER_STATUS_CHOICES = (
-        ('member', 'Adhérent'),
-        ('not_member', 'Non adhérent'),
-        ('pending', "Demande d'adhésion"),
+        ("member", "Adhérent"),
+        ("not_member", "Non adhérent"),
+        ("pending", "Demande d'adhésion"),
+    )
+
+    status = models.CharField(
+        max_length=50,
+        choices=MEMBER_STATUS_CHOICES,
+        default="member",
+        verbose_name="statut",
+    )
+    type = models.CharField(
+        max_length=20,
+        choices=MEMBER_TYPE_CHOICES,
+        default="natural_person",
+        verbose_name="type",
     )
     )
 
 
-    status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
-                              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')
-    home_phone_number = models.CharField(max_length=25, blank=True,
-                                         verbose_name='téléphone fixe')
-    mobile_phone_number = models.CharField(max_length=25, blank=True,
-                                           verbose_name='téléphone mobile')
+    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",
+    )
+    home_phone_number = models.CharField(
+        max_length=25, blank=True, verbose_name="téléphone fixe"
+    )
+    mobile_phone_number = models.CharField(
+        max_length=25, blank=True, verbose_name="téléphone mobile"
+    )
     # TODO: use a django module that provides an address model? (would
     # TODO: use a django module that provides an address model? (would
     # support more countries and address types)
     # support more countries and address types)
-    address = models.TextField(
-        verbose_name='adresse postale', blank=True, null=True)
-    postal_code = models.CharField(max_length=5, blank=True, null=True,
-                                   validators=[RegexValidator(regex=r'^\d{5}$',
-                                                              message='Code postal non valide.')],
-                                   verbose_name='code postal')
-    city = models.CharField(max_length=200, blank=True, null=True,
-                            verbose_name='commune')
-    country = models.CharField(max_length=200, blank=True, null=True,
-                               default='France',
-                               verbose_name='pays')
-    resign_date = models.DateField(null=True, blank=True,
-                                   verbose_name="date de départ de "
-                                   "l'association",
-                                   help_text="En cas de départ prématuré")
-    comments = models.TextField(blank=True, verbose_name='commentaires',
-                                help_text="Commentaires libres (informations"
-                                " spécifiques concernant l'adhésion,"
-                                " raison du départ, etc)")
-    date_last_call_for_membership_fees_email = models.DateTimeField(null=True,
-                        blank=True,
-                        verbose_name="Date du dernier email de relance de cotisation envoyé")
+    address = models.TextField(verbose_name="adresse postale", blank=True, null=True)
+    postal_code = models.CharField(
+        max_length=5,
+        blank=True,
+        null=True,
+        validators=[
+            RegexValidator(regex=r"^\d{5}$", message="Code postal non valide.")
+        ],
+        verbose_name="code postal",
+    )
+    city = models.CharField(
+        max_length=200, blank=True, null=True, verbose_name="commune"
+    )
+    country = models.CharField(
+        max_length=200, blank=True, null=True, default="France", verbose_name="pays"
+    )
+    resign_date = models.DateField(
+        null=True,
+        blank=True,
+        verbose_name="date de départ de " "l'association",
+        help_text="En cas de départ prématuré",
+    )
+    comments = models.TextField(
+        blank=True,
+        verbose_name="commentaires",
+        help_text="Commentaires libres (informations"
+        " spécifiques concernant l'adhésion,"
+        " raison du départ, etc)",
+    )
+    date_last_call_for_membership_fees_email = models.DateTimeField(
+        null=True,
+        blank=True,
+        verbose_name="Date du dernier email de relance de cotisation envoyé",
+    )
     send_membership_fees_email = models.BooleanField(
     send_membership_fees_email = models.BooleanField(
-        default=True, verbose_name='relance de cotisation',
-        help_text='Précise si l\'utilisateur doit recevoir des mails de relance pour la cotisation. Certains membres n\'ont pas à recevoir de relance (prélèvement automatique, membres d\'honneurs, etc.)')
-    balance = models.DecimalField(max_digits=5, decimal_places=2, default=0,
-                                  verbose_name='account balance')
+        default=True,
+        verbose_name="relance de cotisation",
+        help_text="Précise si l'utilisateur doit recevoir des mails de relance pour la cotisation. Certains membres n'ont pas à recevoir de relance (prélèvement automatique, membres d'honneurs, etc.)",
+    )
+    balance = models.DecimalField(
+        max_digits=5, decimal_places=2, default=0, verbose_name="account balance"
+    )
 
 
     objects = MemberManager()
     objects = MemberManager()
 
 
@@ -109,22 +140,24 @@ class Member(CoinLdapSyncMixin, AbstractUser):
     _password_ldap = None
     _password_ldap = None
 
 
     def clean(self):
     def clean(self):
-        if self.type == 'legal_entity':
+        if self.type == "legal_entity":
             if not self.organization_name:
             if not self.organization_name:
-                raise ValidationError("Le nom de l'organisme est obligatoire "
-                                      "pour une personne morale")
-        elif self.type == 'natural_person':
+                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):
             if not (self.first_name and self.last_name):
-                raise ValidationError("Le nom et prénom sont obligatoires "
-                                      "pour une personne physique")
+                raise ValidationError(
+                    "Le nom et prénom sont obligatoires " "pour une personne physique"
+                )
 
 
     def __unicode__(self):
     def __unicode__(self):
-        if self.type == 'legal_entity':
+        if self.type == "legal_entity":
             return self.organization_name
             return self.organization_name
         elif self.nickname:
         elif self.nickname:
             return self.nickname
             return self.nickname
         else:
         else:
-            return self.first_name + ' ' + self.last_name
+            return self.first_name + " " + self.last_name
 
 
     def get_full_name(self):
     def get_full_name(self):
         return str(self)
         return str(self)
@@ -134,8 +167,9 @@ 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):
-        aggregate = self.membership_fees.aggregate(end=Max('end_date'))
-        return aggregate['end']
+        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=None):
     def is_paid_up(self, date=None):
@@ -147,7 +181,7 @@ class Member(CoinLdapSyncMixin, AbstractUser):
         end_date = self.end_date_of_membership()
         end_date = self.end_date_of_membership()
         if end_date is None:
         if end_date is None:
             return False
             return False
-        return (end_date >= date)
+        return end_date >= date
 
 
     def set_password(self, new_password, *args, **kwargs):
     def set_password(self, new_password, *args, **kwargs):
         """
         """
@@ -165,7 +199,8 @@ class Member(CoinLdapSyncMixin, AbstractUser):
         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=None):
     def get_inactive_subscriptions(self, date=None):
         """
         """
@@ -175,12 +210,12 @@ class Member(CoinLdapSyncMixin, AbstractUser):
             date = datetime.date.today()
             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(resign_date__lt=date))
+            Q(subscription_date__gt=date) | Q(resign_date__lt=date),
+        )
 
 
     def get_ssh_keys(self):
     def get_ssh_keys(self):
         # Quick & dirty, ensure that keys are unique (otherwise, LDAP complains)
         # Quick & dirty, ensure that keys are unique (otherwise, LDAP complains)
-        return list({k.key for k in self.cryptokey_set.filter(type='RSA')})
+        return list({k.key for k in self.cryptokey_set.filter(type="RSA")})
 
 
     def sync_ssh_keys(self):
     def sync_ssh_keys(self):
         """
         """
@@ -198,14 +233,21 @@ class Member(CoinLdapSyncMixin, AbstractUser):
         # Do not perform LDAP query if no usefull fields to update are specified
         # Do not perform LDAP query if no usefull fields to update are specified
         # in update_fields
         # in update_fields
         # Ex : at login, last_login field is updated by django auth module.
         # Ex : at login, last_login field is updated by django auth module.
-        relevant_fields = {'username', 'last_name', 'first_name',
-                           'organization_name', 'email'}
+        relevant_fields = {
+            "username",
+            "last_name",
+            "first_name",
+            "organization_name",
+            "email",
+        }
         if update_fields and relevant_fields.isdisjoint(set(update_fields)):
         if update_fields and relevant_fields.isdisjoint(set(update_fields)):
             return
             return
 
 
         # Fail if no username specified
         # Fail if no username specified
-        assert self.username, ('Can\'t sync with LDAP because missing username '
-                               'value for the Member : %s' % self)
+        assert self.username, (
+            "Can't sync with LDAP because missing username "
+            "value for the Member : %s" % self
+        )
 
 
         # If try to sync a superuser in creation mode
         # If try to sync a superuser in creation mode
         # Try to retrieve the user in ldap. If exists, switch to update mode
         # Try to retrieve the user in ldap. If exists, switch to update mode
@@ -224,7 +266,7 @@ class Member(CoinLdapSyncMixin, AbstractUser):
         if creation:
         if creation:
             users = LdapUser.objects
             users = LdapUser.objects
             if users.exists():
             if users.exists():
-                uid_number = users.order_by('-uidNumber')[0].uidNumber + 1
+                uid_number = users.order_by("-uidNumber")[0].uidNumber + 1
             else:
             else:
                 uid_number = settings.LDAP_USER_FIRST_UID
                 uid_number = settings.LDAP_USER_FIRST_UID
             ldap_user = LdapUser()
             ldap_user = LdapUser()
@@ -232,12 +274,12 @@ class Member(CoinLdapSyncMixin, AbstractUser):
             ldap_user.uid = self.username
             ldap_user.uid = self.username
             ldap_user.nick_name = self.username
             ldap_user.nick_name = self.username
             ldap_user.uidNumber = uid_number
             ldap_user.uidNumber = uid_number
-            ldap_user.homeDirectory = '/home/' + self.username
+            ldap_user.homeDirectory = "/home/" + self.username
 
 
-        if self.type == 'natural_person':
+        if self.type == "natural_person":
             ldap_user.last_name = self.last_name
             ldap_user.last_name = self.last_name
             ldap_user.first_name = self.first_name
             ldap_user.first_name = self.first_name
-        elif self.type == 'legal_entity':
+        elif self.type == "legal_entity":
             ldap_user.last_name = self.organization_name
             ldap_user.last_name = self.organization_name
             ldap_user.first_name = ""
             ldap_user.first_name = ""
 
 
@@ -261,8 +303,10 @@ class Member(CoinLdapSyncMixin, AbstractUser):
         """
         """
         Delete member from the LDAP
         Delete member from the LDAP
         """
         """
-        assert self.username, ('Can\'t delete from LDAP because missing '
-                               'username value for the Member : %s' % self)
+        assert self.username, (
+            "Can't delete from LDAP because missing "
+            "username value for the Member : %s" % self
+        )
 
 
         # Delete user from LDAP
         # Delete user from LDAP
         ldap_user = LdapUser.objects.get(pk=self.username)
         ldap_user = LdapUser.objects.get(pk=self.username)
@@ -284,14 +328,15 @@ class Member(CoinLdapSyncMixin, AbstractUser):
 
 
         kwargs = {}
         kwargs = {}
         if isp_info.administrative_email:
         if isp_info.administrative_email:
-            kwargs['from_email'] = isp_info.administrative_email
+            kwargs["from_email"] = isp_info.administrative_email
 
 
         utils.send_templated_email(
         utils.send_templated_email(
             to=self.email,
             to=self.email,
-            subject_template='members/emails/welcome_email_subject.txt',
-            body_template='members/emails/welcome_email.html',
-            context={'member': self, 'branding': isp_info},
-            **kwargs)
+            subject_template="members/emails/welcome_email_subject.txt",
+            body_template="members/emails/welcome_email.html",
+            context={"member": self, "branding": isp_info},
+            **kwargs
+        )
 
 
     def send_call_for_membership_fees_email(self, auto=False):
     def send_call_for_membership_fees_email(self, auto=False):
         """ Envoie le courriel d'appel à cotisation du membre
         """ Envoie le courriel d'appel à cotisation du membre
@@ -308,22 +353,27 @@ class Member(CoinLdapSyncMixin, AbstractUser):
         kwargs = {}
         kwargs = {}
         # Il peut ne pas y avir d'ISPInfo, ou bien pas d'administrative_email
         # Il peut ne pas y avir d'ISPInfo, ou bien pas d'administrative_email
         if isp_info and isp_info.administrative_email:
         if isp_info and isp_info.administrative_email:
-            kwargs['from_email'] = isp_info.administrative_email
+            kwargs["from_email"] = isp_info.administrative_email
 
 
         # Si le dernier courriel de relance a été envoyé il y a moins de trois
         # Si le dernier courriel de relance a été envoyé il y a moins de trois
         # semaines, n'envoi pas un nouveau courriel
         # semaines, n'envoi pas un nouveau courriel
-        if (not self.date_last_call_for_membership_fees_email
-            or (self.date_last_call_for_membership_fees_email
-                <= timezone.now() + relativedelta(weeks=-3))):
+        if not self.date_last_call_for_membership_fees_email or (
+            self.date_last_call_for_membership_fees_email
+            <= timezone.now() + relativedelta(weeks=-3)
+        ):
             utils.send_templated_email(
             utils.send_templated_email(
                 to=self.email,
                 to=self.email,
-                subject_template='members/emails/call_for_membership_fees_subject.txt',
-                body_template='members/emails/call_for_membership_fees.html',
-                context={'member': self, 'branding': isp_info,
-                         'membership_info_url': settings.MEMBER_MEMBERSHIP_INFO_URL,
-                         'today': datetime.date.today,
-                         'auto_sent': auto},
-                **kwargs)
+                subject_template="members/emails/call_for_membership_fees_subject.txt",
+                body_template="members/emails/call_for_membership_fees.html",
+                context={
+                    "member": self,
+                    "branding": isp_info,
+                    "membership_info_url": settings.MEMBER_MEMBERSHIP_INFO_URL,
+                    "today": datetime.date.today,
+                    "auto_sent": auto,
+                },
+                **kwargs
+            )
             # Sauvegarde en base la date du dernier envoi de mail de relance
             # Sauvegarde en base la date du dernier envoi de mail de relance
             self.date_last_call_for_membership_fees_email = timezone.now()
             self.date_last_call_for_membership_fees_email = timezone.now()
             self.save()
             self.save()
@@ -331,18 +381,18 @@ class Member(CoinLdapSyncMixin, AbstractUser):
 
 
         return False
         return False
 
 
-
     class Meta:
     class Meta:
-        verbose_name = 'membre'
+        verbose_name = "membre"
+
 
 
 # Hack to force email 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("email")._unique = True
+Member._meta.get_field("email").blank = False
+Member._meta.get_field("email").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(member):
 def get_automatic_username(member):
@@ -352,7 +402,7 @@ def get_automatic_username(member):
     """
     """
 
 
     # S'il s'agit d'une entreprise, utilise son nom:
     # S'il s'agit d'une entreprise, utilise son nom:
-    if member.type == 'legal_entity' and member.organization_name:
+    if member.type == "legal_entity" and member.organization_name:
         username = member.organization_name
         username = member.organization_name
     # Sinon, si un pseudo est définit, l'utilise
     # Sinon, si un pseudo est définit, l'utilise
     elif member.nickname:
     elif member.nickname:
@@ -360,19 +410,18 @@ def get_automatic_username(member):
     # Sinon, utilise nom et prenom
     # Sinon, utilise nom et prenom
     elif member.first_name and member.last_name:
     elif member.first_name and member.last_name:
         # Première lettre de chaque partie du prénom
         # Première lettre de chaque partie du prénom
-        first_name_letters = ''.join(
-            [c[0] for c in member.first_name.split('-')]
-        )
+        first_name_letters = "".join([c[0] for c in member.first_name.split("-")])
         # Concaténer avec nom de famille
         # Concaténer avec nom de famille
-        username = ('%s%s' % (first_name_letters, member.last_name))
+        username = "%s%s" % (first_name_letters, member.last_name)
     else:
     else:
-        raise Exception('Il n\'y a pas sufissement d\'informations pour déterminer un login automatiquement')
+        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)\
-        .encode('ascii', 'ignore')
+    username = unicodedata.normalize("NFD", username).encode("ascii", "ignore")
     # Enlever ponctuation (sauf _-.) et espace
     # Enlever ponctuation (sauf _-.) et espace
-    punctuation = ('!"#$%&\'()*+,/:;<=>?@[\\]^`{|}~ ').encode('ascii')
+    punctuation = ("!\"#$%&'()*+,/:;<=>?@[\\]^`{|}~ ").encode("ascii")
     username = username.translate(None, punctuation)
     username = username.translate(None, punctuation)
     # En minuscule
     # En minuscule
     username = username.lower()
     username = username.lower()
@@ -386,7 +435,7 @@ def get_automatic_username(member):
     # Tant qu'un membre est trouvé, incrémente 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)) :]
         else:
         else:
             username = base_username
             username = base_username
         username = username + str(incr)
         username = username + str(incr)
@@ -398,12 +447,11 @@ def get_automatic_username(member):
 
 
 class CryptoKey(CoinLdapSyncMixin, models.Model):
 class CryptoKey(CoinLdapSyncMixin, models.Model):
 
 
-    KEY_TYPE_CHOICES = (('RSA', 'RSA'), ('GPG', 'GPG'))
+    KEY_TYPE_CHOICES = (("RSA", "RSA"), ("GPG", "GPG"))
 
 
-    type = models.CharField(max_length=3, choices=KEY_TYPE_CHOICES,
-                            verbose_name='type')
-    key = models.TextField(verbose_name='clé')
-    member = models.ForeignKey('Member', verbose_name='membre')
+    type = models.CharField(max_length=3, choices=KEY_TYPE_CHOICES, verbose_name="type")
+    key = models.TextField(verbose_name="clé")
+    member = models.ForeignKey("Member", verbose_name="membre")
 
 
     def sync_to_ldap(self, creation, *args, **kwargs):
     def sync_to_ldap(self, creation, *args, **kwargs):
         """Simply tell the member object to resync all its SSH keys to LDAP"""
         """Simply tell the member object to resync all its SSH keys to LDAP"""
@@ -413,80 +461,99 @@ class CryptoKey(CoinLdapSyncMixin, models.Model):
         self.member.sync_ssh_keys()
         self.member.sync_ssh_keys()
 
 
     def __unicode__(self):
     def __unicode__(self):
-        return 'Clé %s de %s' % (self.type, self.member)
+        return "Clé %s de %s" % (self.type, self.member)
 
 
     class Meta:
     class Meta:
-        verbose_name = 'clé'
+        verbose_name = "clé"
 
 
 
 
 class MembershipFee(models.Model):
 class MembershipFee(models.Model):
     PAYMENT_METHOD_CHOICES = (
     PAYMENT_METHOD_CHOICES = (
-        ('cash', 'Espèces'),
-        ('check', 'Chèque'),
-        ('transfer', 'Virement'),
-        ('other', 'Autre')
+        ("cash", "Espèces"),
+        ("check", "Chèque"),
+        ("transfer", "Virement"),
+        ("other", "Autre"),
     )
     )
 
 
-    member = models.ForeignKey('Member', related_name='membership_fees',
-                               verbose_name='membre')
-    amount = models.DecimalField(null=False, max_digits=5, decimal_places=2,
-                                 default=settings.MEMBER_DEFAULT_COTISATION,
-                                 verbose_name='montant', help_text='en €')
-    start_date = models.DateField(
+    member = models.ForeignKey(
+        "Member", related_name="membership_fees", verbose_name="membre"
+    )
+    amount = models.DecimalField(
         null=False,
         null=False,
-        blank=False,
-        verbose_name='date de début de cotisation')
+        max_digits=5,
+        decimal_places=2,
+        default=settings.MEMBER_DEFAULT_COTISATION,
+        verbose_name="montant",
+        help_text="en €",
+    )
+    start_date = models.DateField(
+        null=False, blank=False, verbose_name="date de début de cotisation"
+    )
     end_date = models.DateField(
     end_date = models.DateField(
         null=False,
         null=False,
         blank=True,
         blank=True,
-        verbose_name='date de fin de cotisation',
-        help_text='par défaut, la cotisation dure un an')
-
-    payment_method = models.CharField(max_length=100, null=True, blank=True,
-                                      choices=PAYMENT_METHOD_CHOICES,
-                                      verbose_name='moyen de paiement')
-    reference = models.CharField(max_length=125, null=True, blank=True,
-                                 verbose_name='référence du paiement',
-                                 help_text='numéro de chèque, '
-                                 'référence de virement, commentaire...')
-    payment_date = models.DateField(null=True, blank=True,
-                                    verbose_name='date du paiement')
+        verbose_name="date de fin de cotisation",
+        help_text="par défaut, la cotisation dure un an",
+    )
+
+    payment_method = models.CharField(
+        max_length=100,
+        null=True,
+        blank=True,
+        choices=PAYMENT_METHOD_CHOICES,
+        verbose_name="moyen de paiement",
+    )
+    reference = models.CharField(
+        max_length=125,
+        null=True,
+        blank=True,
+        verbose_name="référence du paiement",
+        help_text="numéro de chèque, " "référence de virement, commentaire...",
+    )
+    payment_date = models.DateField(
+        null=True, blank=True, verbose_name="date du paiement"
+    )
 
 
     def clean(self):
     def clean(self):
         if self.start_date is not None and self.end_date is None:
         if self.start_date is not None and self.end_date is None:
             self.end_date = self.start_date + datetime.timedelta(364)
             self.end_date = self.start_date + datetime.timedelta(364)
 
 
     def __unicode__(self):
     def __unicode__(self):
-        return '%s - %s - %i€' % (self.member, self.start_date, self.amount)
+        return "%s - %s - %i€" % (self.member, self.start_date, self.amount)
 
 
     class Meta:
     class Meta:
-        verbose_name = 'cotisation'
+        verbose_name = "cotisation"
 
 
 
 
 class LdapUser(ldapdb.models.Model):
 class LdapUser(ldapdb.models.Model):
     # "ou=users,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
     # "ou=users,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
     base_dn = settings.LDAP_USER_BASE_DN
     base_dn = settings.LDAP_USER_BASE_DN
-    object_classes = [b'inetOrgPerson', b'organizationalPerson', b'person',
-                      b'top', b'posixAccount', b'ldapPublicKey']
-
-    uid = CharField(db_column=b'uid', unique=True, max_length=255)
-    nick_name = CharField(db_column=b'cn', unique=True, primary_key=True,
-                          max_length=255)
-    first_name = CharField(db_column=b'givenName', max_length=255)
-    last_name = CharField(db_column=b'sn', max_length=255)
-    display_name = CharField(db_column=b'displayName', max_length=255,
-                             blank=True)
-    password = CharField(db_column=b'userPassword', max_length=255)
-    uidNumber = IntegerField(db_column=b'uidNumber', unique=True)
-    gidNumber = IntegerField(db_column=b'gidNumber', default=2000)
+    object_classes = [
+        b"inetOrgPerson",
+        b"organizationalPerson",
+        b"person",
+        b"top",
+        b"posixAccount",
+        b"ldapPublicKey",
+    ]
+
+    uid = CharField(db_column=b"uid", unique=True, max_length=255)
+    nick_name = CharField(
+        db_column=b"cn", unique=True, primary_key=True, max_length=255
+    )
+    first_name = CharField(db_column=b"givenName", max_length=255)
+    last_name = CharField(db_column=b"sn", max_length=255)
+    display_name = CharField(db_column=b"displayName", max_length=255, blank=True)
+    password = CharField(db_column=b"userPassword", max_length=255)
+    uidNumber = IntegerField(db_column=b"uidNumber", unique=True)
+    gidNumber = IntegerField(db_column=b"gidNumber", default=2000)
     # Used by Sympa for logging in.
     # Used by Sympa for logging in.
-    mail = CharField(db_column=b'mail', max_length=255, blank=True,
-                     unique=True)
-    homeDirectory = CharField(db_column=b'homeDirectory', max_length=255,
-                              default='/tmp')
-    loginShell = CharField(db_column=b'loginShell', max_length=255,
-                              default='/bin/bash')
-    sshPublicKey = ListField(db_column=b'sshPublicKey', default=[])
+    mail = CharField(db_column=b"mail", max_length=255, blank=True, unique=True)
+    homeDirectory = CharField(
+        db_column=b"homeDirectory", max_length=255, default="/tmp"
+    )
+    loginShell = CharField(db_column=b"loginShell", max_length=255, default="/bin/bash")
+    sshPublicKey = ListField(db_column=b"sshPublicKey", default=[])
 
 
     def __unicode__(self):
     def __unicode__(self):
         return self.display_name
         return self.display_name
@@ -511,7 +578,6 @@ class LdapUser(ldapdb.models.Model):
 # managed = False  # Indique à Django de ne pas intégrer ce model en base
 # managed = False  # Indique à Django de ne pas intégrer ce model en base
 
 
 
 
-
 @receiver(pre_save, sender=Member)
 @receiver(pre_save, sender=Member)
 def define_username(sender, instance, **kwargs):
 def define_username(sender, instance, **kwargs):
     """
     """
@@ -529,15 +595,16 @@ def define_display_name(sender, instance, **kwargs):
     concaténation de first_name et last_name
     concaténation de first_name et last_name
     """
     """
     if not instance.display_name:
     if not instance.display_name:
-        instance.display_name = '%s %s' % (instance.first_name,
-                                           instance.last_name)
-
+        instance.display_name = "%s %s" % (instance.first_name, instance.last_name)
 
 
 
 
 class RowLevelPermission(Permission):
 class RowLevelPermission(Permission):
     offer = models.ForeignKey(
     offer = models.ForeignKey(
-        'offers.Offer', null=True, verbose_name="Offre",
-        help_text="Offre dont l'utilisateur est autorisé à voir et modifier les membres et les abonnements.")
+        "offers.Offer",
+        null=True,
+        verbose_name="Offre",
+        help_text="Offre dont l'utilisateur est autorisé à voir et modifier les membres et les abonnements.",
+    )
     description = models.TextField(blank=True)
     description = models.TextField(blank=True)
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
@@ -569,10 +636,14 @@ class RowLevelPermission(Permission):
         return codename
         return codename
 
 
     class Meta:
     class Meta:
-        verbose_name = 'permission fine'
-        verbose_name_plural = 'permissions fines'
+        verbose_name = "permission fine"
+        verbose_name_plural = "permissions fines"
 
 
 
 
-RowLevelPermission._meta.get_field('codename').blank = True
-RowLevelPermission._meta.get_field('codename').help_text = 'Laisser vide pour le générer automatiquement'
-RowLevelPermission._meta.get_field('content_type').help_text = "Garder 'abonnement' pour une utilisation normale"
+RowLevelPermission._meta.get_field("codename").blank = True
+RowLevelPermission._meta.get_field(
+    "codename"
+).help_text = "Laisser vide pour le générer automatiquement"
+RowLevelPermission._meta.get_field(
+    "content_type"
+).help_text = "Garder 'abonnement' pour une utilisation normale"

+ 154 - 129
coin/members/tests.py

@@ -30,16 +30,14 @@ class LDAPMemberTests(TestCase):
         Cela concerne le nom et le prénom
         Cela concerne le nom et le prénom
         """
         """
 
 
-        #~ Créé un membre
-        first_name = 'Gérard'
-        last_name = 'Majax'
+        # ~ Créé un membre
+        first_name = "Gérard"
+        last_name = "Majax"
         username = MemberTestsUtils.get_random_username()
         username = MemberTestsUtils.get_random_username()
-        member = Member(first_name=first_name,
-                        last_name=last_name,
-                        username=username)
+        member = Member(first_name=first_name, last_name=last_name, username=username)
         member.save()
         member.save()
 
 
-        #~ Récupère l'utilisateur LDAP et fait les tests
+        # ~ Récupère l'utilisateur LDAP et fait les tests
         ldap_user = LdapUser.objects.get(pk=username)
         ldap_user = LdapUser.objects.get(pk=username)
 
 
         self.assertEqual(ldap_user.first_name, first_name)
         self.assertEqual(ldap_user.first_name, first_name)
@@ -48,28 +46,29 @@ class LDAPMemberTests(TestCase):
 
 
         member.delete()
         member.delete()
 
 
-    def test_when_modifiying_member_corresponding_ldap_user_is_also_modified_with_same_data(self):
+    def test_when_modifiying_member_corresponding_ldap_user_is_also_modified_with_same_data(
+        self
+    ):
         """
         """
         Test que lorsque l'on modifie un membre, l'utilisateur LDAP
         Test que lorsque l'on modifie un membre, l'utilisateur LDAP
         correspondant est bien modifié
         correspondant est bien modifié
         Cela concerne le no met le prénom
         Cela concerne le no met le prénom
         """
         """
-        #~ Créé un membre
-        first_name = 'Ronald'
-        last_name = 'Mac Donald'
+        # ~ Créé un membre
+        first_name = "Ronald"
+        last_name = "Mac Donald"
         username = MemberTestsUtils.get_random_username()
         username = MemberTestsUtils.get_random_username()
-        member = Member(first_name=first_name,
-                        last_name=last_name, username=username)
+        member = Member(first_name=first_name, last_name=last_name, username=username)
         member.save()
         member.save()
 
 
-        #~  Le modifie
-        new_first_name = 'José'
-        new_last_name = 'Bové'
+        # ~  Le modifie
+        new_first_name = "José"
+        new_last_name = "Bové"
         member.first_name = new_first_name
         member.first_name = new_first_name
         member.last_name = new_last_name
         member.last_name = new_last_name
         member.save()
         member.save()
 
 
-        #~ Récupère l'utilisateur LDAP et fait les tests
+        # ~ Récupère l'utilisateur LDAP et fait les tests
         ldap_user = LdapUser.objects.get(pk=username)
         ldap_user = LdapUser.objects.get(pk=username)
 
 
         self.assertEqual(ldap_user.first_name, new_first_name)
         self.assertEqual(ldap_user.first_name, new_first_name)
@@ -110,36 +109,37 @@ class LDAPMemberTests(TestCase):
         username = MemberTestsUtils.get_random_username()
         username = MemberTestsUtils.get_random_username()
         password = "1234"
         password = "1234"
 
 
-        #~ Créé un nouveau membre
-        member = Member(first_name='Passe-partout',
-                        last_name='Du fort Boyard', username=username)
+        # ~ Créé un nouveau membre
+        member = Member(
+            first_name="Passe-partout", last_name="Du fort Boyard", username=username
+        )
         member.save()
         member.save()
 
 
-        #~ Récupère l'utilisateur LDAP
+        # ~ Récupère l'utilisateur LDAP
         ldap_user = LdapUser.objects.get(pk=username)
         ldap_user = LdapUser.objects.get(pk=username)
 
 
-        #~ Change son mot de passe
+        # ~ Change son mot de passe
         member.set_password(password)
         member.set_password(password)
         member.save()
         member.save()
 
 
-        #~ Test l'authentification django
+        # ~ Test l'authentification django
         c = Client()
         c = Client()
         self.assertEqual(c.login(username=username, password=password), True)
         self.assertEqual(c.login(username=username, password=password), True)
 
 
         # Test l'authentification ldap
         # Test l'authentification ldap
         import ldap
         import ldap
-        ldap_conn_settings = db.connections['ldap'].settings_dict
-        l = ldap.initialize(ldap_conn_settings['NAME'])
-        options = ldap_conn_settings.get('CONNECTION_OPTIONS', {})
+
+        ldap_conn_settings = db.connections["ldap"].settings_dict
+        l = ldap.initialize(ldap_conn_settings["NAME"])
+        options = ldap_conn_settings.get("CONNECTION_OPTIONS", {})
         for opt, value in options.items():
         for opt, value in options.items():
             l.set_option(opt, value)
             l.set_option(opt, value)
 
 
-        if ldap_conn_settings.get('TLS', False):
+        if ldap_conn_settings.get("TLS", False):
             l.start_tls_s()
             l.start_tls_s()
 
 
         # Raise "Invalid credentials" exception if auth fail
         # Raise "Invalid credentials" exception if auth fail
-        l.simple_bind_s(ldap_conn_settings['USER'],
-                        ldap_conn_settings['PASSWORD'])
+        l.simple_bind_s(ldap_conn_settings["USER"], ldap_conn_settings["PASSWORD"])
 
 
         l.unbind_s()
         l.unbind_s()
 
 
@@ -150,22 +150,19 @@ class LDAPMemberTests(TestCase):
         Lors de la création d'un membre, le champ "display_name" du LDAP est
         Lors de la création d'un membre, le champ "display_name" du LDAP est
         prenom + nom
         prenom + nom
         """
         """
-        first_name = 'Gérard'
-        last_name = 'Majax'
+        first_name = "Gérard"
+        last_name = "Majax"
         username = MemberTestsUtils.get_random_username()
         username = MemberTestsUtils.get_random_username()
-        member = Member(first_name=first_name,
-                        last_name=last_name, username=username)
+        member = Member(first_name=first_name, last_name=last_name, username=username)
         member.save()
         member.save()
 
 
-        #~ Récupère l'utilisateur LDAP
+        # ~ Récupère l'utilisateur LDAP
         ldap_user = LdapUser.objects.get(pk=username)
         ldap_user = LdapUser.objects.get(pk=username)
 
 
-        self.assertEqual(ldap_user.display_name, '%s %s' %
-                         (first_name, last_name))
+        self.assertEqual(ldap_user.display_name, "%s %s" % (first_name, last_name))
 
 
         member.delete()
         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
@@ -174,18 +171,17 @@ class LDAPMemberTests(TestCase):
 
 
         # Fait échouer le LDAP en définissant un mauvais mot de passe
         # Fait échouer le LDAP en définissant un mauvais mot de passe
         for dbconnection in db.connections.all():
         for dbconnection in db.connections.all():
-            if (type(dbconnection) is
-                    ldapdb.backends.ldap.base.DatabaseWrapper):
+            if type(dbconnection) is ldapdb.backends.ldap.base.DatabaseWrapper:
                 dbconnection.settings_dict[
                 dbconnection.settings_dict[
-                    'PREVIOUSPASSWORD'] = dbconnection.settings_dict['PASSWORD']
-                dbconnection.settings_dict['PASSWORD'] = 'wrong password test'
+                    "PREVIOUSPASSWORD"
+                ] = dbconnection.settings_dict["PASSWORD"]
+                dbconnection.settings_dict["PASSWORD"] = "wrong password test"
 
 
         # Créé un membre
         # Créé un membre
-        first_name = 'Du'
-        last_name = 'Pont'
+        first_name = "Du"
+        last_name = "Pont"
         username = MemberTestsUtils.get_random_username()
         username = MemberTestsUtils.get_random_username()
-        member = Member(first_name=first_name,
-                        last_name=last_name, username=username)
+        member = Member(first_name=first_name, last_name=last_name, username=username)
 
 
         # Le sauvegarde en base de donnée
         # Le sauvegarde en base de donnée
         # Le save devrait renvoyer une exception parceque le LDAP échoue
         # Le save devrait renvoyer une exception parceque le LDAP échoue
@@ -197,10 +193,10 @@ class LDAPMemberTests(TestCase):
 
 
         # Restaure le mot de passe pour les tests suivants
         # Restaure le mot de passe pour les tests suivants
         for dbconnection in db.connections.all():
         for dbconnection in db.connections.all():
-            if (type(dbconnection) is
-                    ldapdb.backends.ldap.base.DatabaseWrapper):
-                dbconnection.settings_dict[
-                    'PASSWORD'] = dbconnection.settings_dict['PREVIOUSPASSWORD']
+            if type(dbconnection) is ldapdb.backends.ldap.base.DatabaseWrapper:
+                dbconnection.settings_dict["PASSWORD"] = dbconnection.settings_dict[
+                    "PREVIOUSPASSWORD"
+                ]
 
 
     # def test_when_user_login_member_user_field_is_updated(self):
     # def test_when_user_login_member_user_field_is_updated(self):
     #     """
     #     """
@@ -239,11 +235,11 @@ class MemberTests(TestCase):
         premières lettres du prénom + nom le tout en minuscule,
         premières lettres du prénom + nom le tout en minuscule,
         sans caractères accentués et sans espaces.
         sans caractères accentués et sans espaces.
         """
         """
-        random = os.urandom(4).encode('hex')
-        first_name = 'Gérard-Étienne'
-        last_name = 'Majax de la Boétie!B' + random
+        random = os.urandom(4).encode("hex")
+        first_name = "Gérard-Étienne"
+        last_name = "Majax de la Boétie!B" + random
 
 
-        control = 'gemajaxdelaboetieb' + random
+        control = "gemajaxdelaboetieb" + random
         control = control[:30]
         control = control[:30]
 
 
         member = Member(first_name=first_name, last_name=last_name)
         member = Member(first_name=first_name, last_name=last_name)
@@ -258,19 +254,25 @@ class MemberTests(TestCase):
         Lors de la création d'un membre, test si le username existe déjà,
         Lors de la création d'un membre, test si le username existe déjà,
         renvoi avec un incrément à la fin
         renvoi avec un incrément à la fin
         """
         """
-        random = os.urandom(4).encode('hex')
+        random = os.urandom(4).encode("hex")
 
 
-        member1 = Member(first_name='Hervé', last_name='DUPOND' + random, email='hdupond@coin.org')
+        member1 = Member(
+            first_name="Hervé", last_name="DUPOND" + random, email="hdupond@coin.org"
+        )
         member1.save()
         member1.save()
-        self.assertEqual(member1.username, 'hdupond' + random)
+        self.assertEqual(member1.username, "hdupond" + random)
 
 
-        member2 = Member(first_name='Henri', last_name='DUPOND' + random, email='hdupond2@coin.org')
+        member2 = Member(
+            first_name="Henri", last_name="DUPOND" + random, email="hdupond2@coin.org"
+        )
         member2.save()
         member2.save()
-        self.assertEqual(member2.username, 'hdupond' + random + '2')
+        self.assertEqual(member2.username, "hdupond" + random + "2")
 
 
-        member3 = Member(first_name='Hector', last_name='DUPOND' + random, email='hdupond3@coin.org')
+        member3 = Member(
+            first_name="Hector", last_name="DUPOND" + random, email="hdupond3@coin.org"
+        )
         member3.save()
         member3.save()
-        self.assertEqual(member3.username, 'hdupond' + random + '3')
+        self.assertEqual(member3.username, "hdupond" + random + "3")
 
 
         member1.delete()
         member1.delete()
         member2.delete()
         member2.delete()
@@ -281,20 +283,29 @@ class MemberTests(TestCase):
         Lors de la créatio d'une entreprise, son nom doit être utilisée lors de
         Lors de la créatio d'une entreprise, son nom doit être utilisée lors de
         la détermination automatique du username
         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')
+        random = os.urandom(4).encode("hex")
+        member = Member(
+            type="legal_entity",
+            organization_name="ILLYSE" + random,
+            email="illyse@coin.org",
+        )
         member.save()
         member.save()
-        self.assertEqual(member.username, 'illyse' + random)
+        self.assertEqual(member.username, "illyse" + random)
         member.delete()
         member.delete()
 
 
     def test_when_creating_member_with_nickname_it_is_used_for_username(self):
     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é
         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')
+        random = os.urandom(4).encode("hex")
+        member = Member(
+            first_name="Richard",
+            last_name="Stallman",
+            nickname="rms" + random,
+            email="illyse@coin.org",
+        )
         member.save()
         member.save()
-        self.assertEqual(member.username, 'rms' + random)
+        self.assertEqual(member.username, "rms" + random)
 
 
         member.delete()
         member.delete()
 
 
@@ -303,20 +314,19 @@ class MemberTests(TestCase):
         Test que end_date_of_membership d'un membre envoi bien la date de fin d'adhésion
         Test que end_date_of_membership d'un membre envoi bien la date de fin d'adhésion
         """
         """
         # Créer un membre
         # Créer un membre
-        first_name = 'Tin'
-        last_name = 'Tin'
+        first_name = "Tin"
+        last_name = "Tin"
         username = MemberTestsUtils.get_random_username()
         username = MemberTestsUtils.get_random_username()
-        member = Member(first_name=first_name,
-                        last_name=last_name, username=username)
+        member = Member(first_name=first_name, last_name=last_name, username=username)
         member.save()
         member.save()
 
 
         start_date = date.today()
         start_date = date.today()
         end_date = start_date + relativedelta(years=+1)
         end_date = start_date + relativedelta(years=+1)
 
 
         # Créé une cotisation
         # Créé une cotisation
-        membershipfee = MembershipFee(member=member, amount=20,
-                                      start_date=start_date,
-                                      end_date=end_date)
+        membershipfee = MembershipFee(
+            member=member, amount=20, start_date=start_date, end_date=end_date
+        )
         membershipfee.save()
         membershipfee.save()
 
 
         self.assertEqual(member.end_date_of_membership(), end_date)
         self.assertEqual(member.end_date_of_membership(), end_date)
@@ -326,11 +336,10 @@ class MemberTests(TestCase):
         Test l'état "a jour de cotisation" d'un adhérent.
         Test l'état "a jour de cotisation" d'un adhérent.
         """
         """
         # Créé un membre
         # Créé un membre
-        first_name = 'Capitain'
-        last_name = 'Haddock'
+        first_name = "Capitain"
+        last_name = "Haddock"
         username = MemberTestsUtils.get_random_username()
         username = MemberTestsUtils.get_random_username()
-        member = Member(first_name=first_name,
-                        last_name=last_name, username=username)
+        member = Member(first_name=first_name, last_name=last_name, username=username)
         member.save()
         member.save()
 
 
         start_date = date.today()
         start_date = date.today()
@@ -340,20 +349,24 @@ class MemberTests(TestCase):
         self.assertEqual(member.is_paid_up(), False)
         self.assertEqual(member.is_paid_up(), False)
 
 
         # Créé une cotisation passée
         # Créé une cotisation passée
-        membershipfee = MembershipFee(member=member, amount=20,
-                                      start_date=date.today() +
-                                      relativedelta(years=-1),
-                                      end_date=date.today() + relativedelta(days=-10))
+        membershipfee = MembershipFee(
+            member=member,
+            amount=20,
+            start_date=date.today() + relativedelta(years=-1),
+            end_date=date.today() + relativedelta(days=-10),
+        )
         membershipfee.save()
         membershipfee.save()
         # La cotisation s'étant terminée il y a 10 jours, il ne devrait pas
         # La cotisation s'étant terminée il y a 10 jours, il ne devrait pas
         # être à jour de cotistion
         # être à jour de cotistion
         self.assertEqual(member.is_paid_up(), False)
         self.assertEqual(member.is_paid_up(), False)
 
 
         # Créé une cotisation actuelle
         # Créé une cotisation actuelle
-        membershipfee = MembershipFee(member=member, amount=20,
-                                      start_date=date.today() +
-                                      relativedelta(days=-10),
-                                      end_date=date.today() + relativedelta(days=+10))
+        membershipfee = MembershipFee(
+            member=member,
+            amount=20,
+            start_date=date.today() + relativedelta(days=-10),
+            end_date=date.today() + relativedelta(days=+10),
+        )
         membershipfee.save()
         membershipfee.save()
         # La cotisation se terminant dans 10 jour, il devrait être à jour
         # La cotisation se terminant dans 10 jour, il devrait être à jour
         # de cotisation
         # de cotisation
@@ -364,7 +377,7 @@ class MemberTests(TestCase):
         Test qu'un membre ne peut pas être créé sans "noms"
         Test qu'un membre ne peut pas être créé sans "noms"
         (prenom, nom) ou pseudo ou nom d'organization
         (prenom, nom) ou pseudo ou nom d'organization
         """
         """
-        member = Member(username='blop')
+        member = Member(username="blop")
         with self.assertRaises(Exception):
         with self.assertRaises(Exception):
             member.full_clean()
             member.full_clean()
             member.save()
             member.save()
@@ -374,23 +387,26 @@ class MemberTests(TestCase):
             member.save()
             member.save()
 
 
 
 
-
 class MemberAdminTests(TestCase):
 class MemberAdminTests(TestCase):
-
     def setUp(self):
     def setUp(self):
-        #~ Client web
+        # ~ Client web
         self.client = Client()
         self.client = Client()
-        #~ Créé un superuser
-        self.admin_user_password = '1234'
+        # ~ Créé un superuser
+        self.admin_user_password = "1234"
         self.admin_user = Member.objects.create_superuser(
         self.admin_user = Member.objects.create_superuser(
-            username='test_admin_user',
-            first_name='test',
-            last_name='Admin user',
-            email='i@mail.com',
-            password=self.admin_user_password)
-        #~ Connection
-        self.assertEqual(self.client.login(
-            username=self.admin_user.username, password=self.admin_user_password), True)
+            username="test_admin_user",
+            first_name="test",
+            last_name="Admin user",
+            email="i@mail.com",
+            password=self.admin_user_password,
+        )
+        # ~ Connection
+        self.assertEqual(
+            self.client.login(
+                username=self.admin_user.username, password=self.admin_user_password
+            ),
+            True,
+        )
 
 
     def tearDown(self):
     def tearDown(self):
         # Supprime le superuser
         # Supprime le superuser
@@ -401,32 +417,28 @@ class MemberAdminTests(TestCase):
         Vérifie que dans l'admin Django, le champ username n'est pad modifiable
         Vérifie que dans l'admin Django, le champ username n'est pad modifiable
         sur une fiche existante
         sur une fiche existante
         """
         """
-        #~ Créé un membre
-        first_name = 'Gérard'
-        last_name = 'Majax'
+        # ~ Créé un membre
+        first_name = "Gérard"
+        last_name = "Majax"
         username = MemberTestsUtils.get_random_username()
         username = MemberTestsUtils.get_random_username()
-        member = Member(first_name=first_name,
-                        last_name=last_name, username=username)
+        member = Member(first_name=first_name, last_name=last_name, username=username)
         member.save()
         member.save()
 
 
-        edit_page = self.client.get('/admin/members/member/%i/' % member.id)
-        self.assertNotContains(edit_page,
-                               '''<input id="id_username" />''',
-                               html=True)
+        edit_page = self.client.get("/admin/members/member/%i/" % member.id)
+        self.assertNotContains(edit_page, """<input id="id_username" />""", html=True)
 
 
         member.delete()
         member.delete()
 
 
 
 
 class MemberTestCallForMembershipCommand(TestCase):
 class MemberTestCallForMembershipCommand(TestCase):
-
     def setUp(self):
     def setUp(self):
         # Créé un membre
         # Créé un membre
         self.username = MemberTestsUtils.get_random_username()
         self.username = MemberTestsUtils.get_random_username()
-        self.member = Member(first_name='Richard', last_name='Stallman',
-                             username=self.username)
+        self.member = Member(
+            first_name="Richard", last_name="Stallman", username=self.username
+        )
         self.member.save()
         self.member.save()
 
 
-
     def tearDown(self):
     def tearDown(self):
         # Supprime le membre
         # Supprime le membre
         self.member.delete()
         self.member.delete()
@@ -434,24 +446,30 @@ class MemberTestCallForMembershipCommand(TestCase):
 
 
     def create_membership_fee(self, end_date):
     def create_membership_fee(self, end_date):
         # Créé une cotisation passée se terminant dans un mois
         # Créé une cotisation passée se terminant dans un mois
-        membershipfee = MembershipFee(member=self.member, amount=20,
-                                      start_date=end_date + relativedelta(years=-1),
-                                      end_date=end_date)
+        membershipfee = MembershipFee(
+            member=self.member,
+            amount=20,
+            start_date=end_date + relativedelta(years=-1),
+            end_date=end_date,
+        )
         membershipfee.save()
         membershipfee.save()
 
 
     def create_membership_fee(self, end_date):
     def create_membership_fee(self, end_date):
         # Créé une cotisation se terminant à la date indiquée
         # Créé une cotisation se terminant à la date indiquée
-        membershipfee = MembershipFee(member=self.member, amount=20,
-                                      start_date=end_date + relativedelta(years=-1),
-                                      end_date=end_date)
+        membershipfee = MembershipFee(
+            member=self.member,
+            amount=20,
+            start_date=end_date + relativedelta(years=-1),
+            end_date=end_date,
+        )
         membershipfee.save()
         membershipfee.save()
         return membershipfee
         return membershipfee
 
 
-    def do_test_email_sent(self, expected_emails = 1, reset_date_last_call = True):
+    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
         # Comme on utilise le même membre, on reset la date de dernier envoi
@@ -459,7 +477,9 @@ class MemberTestCallForMembershipCommand(TestCase):
             self.member.date_last_call_for_membership_fees_email = None
             self.member.date_last_call_for_membership_fees_email = None
             self.member.save()
             self.member.save()
 
 
-    def do_test_for_a_end_date(self, end_date, expected_emails=1, reset_date_last_call = True):
+    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
@@ -477,17 +497,23 @@ 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 devrait être envoyé (ne pas vider date_last_call_for_membership_fees_email)
         # Un mail devrait être envoyé (ne pas vider date_last_call_for_membership_fees_email)
         self.do_test_email_sent(1, False)
         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):
     def test_date_last_call_for_membership_fees_email(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é (ne pas vider date_last_call_for_membership_fees_email)
         # Un mail envoyé (ne pas vider date_last_call_for_membership_fees_email)
         self.do_test_email_sent(1, False)
         self.do_test_email_sent(1, False)
         # Tente un deuxième envoi, qui devrait être à 0
         # Tente un deuxième envoi, qui devrait être à 0
@@ -495,25 +521,24 @@ class MemberTestCallForMembershipCommand(TestCase):
 
 
 
 
 class MemberTestsUtils(object):
 class MemberTestsUtils(object):
-
     @staticmethod
     @staticmethod
     def get_random_username():
     def get_random_username():
         """
         """
         Renvoi une clé aléatoire pour un utilisateur LDAP
         Renvoi une clé aléatoire pour un utilisateur LDAP
         """
         """
-        return 'coin_test_' + os.urandom(8).encode('hex')
+        return "coin_test_" + os.urandom(8).encode("hex")
 
 
 
 
 class TestValidators(TestCase):
 class TestValidators(TestCase):
     def test_valid_chatroom(self):
     def test_valid_chatroom(self):
-        chatroom_url_validator('irc://irc.example.com/#chan')
+        chatroom_url_validator("irc://irc.example.com/#chan")
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
-            chatroom_url_validator('http://#faimaison@irc.geeknode.org')
+            chatroom_url_validator("http://#faimaison@irc.geeknode.org")
 
 
 
 
 class MembershipFeeTests(TestCase):
 class MembershipFeeTests(TestCase):
     def test_mandatory_start_date(self):
     def test_mandatory_start_date(self):
-        member = Member(first_name='foo', last_name='foo', password='foo', email='foo')
+        member = Member(first_name="foo", last_name="foo", password="foo", email="foo")
         member.save()
         member.save()
 
 
         # If there is no start_date clean_fields() should raise an
         # If there is no start_date clean_fields() should raise an

+ 63 - 42
coin/members/urls.py

@@ -8,47 +8,68 @@ from coin.members.models import Member
 
 
 
 
 urlpatterns = patterns(
 urlpatterns = patterns(
-    '',
-    url(r'^$', views.index, name='index'),
-    url(r'^login/$', 'django.contrib.auth.views.login',
-        {'template_name': 'members/registration/login.html'},
-        name='login'),
-    url(r'^logout/$', 'django.contrib.auth.views.logout_then_login',
-        name='logout'),
-
-    url(r'^password_change/$', 'django.contrib.auth.views.password_change',
-        {'post_change_redirect': 'members:password_change_done',
-         'template_name': 'members/registration/password_change_form.html'},
-        name='password_change'),
-    url(r'^password_change_done/$', 'django.contrib.auth.views.password_change_done',
-        {'template_name': 'members/registration/password_change_done.html'},
-        name='password_change_done'),
-
-    url(r'^password_reset/$', 'django.contrib.auth.views.password_reset',
-        {'post_reset_redirect': 'members:password_reset_done',
-         'template_name': 'members/registration/password_reset_form.html',
-         'email_template_name': 'members/registration/password_reset_email.html',
-         'subject_template_name': 'members/registration/password_reset_subject.txt'},
-        name='password_reset'),
-    url(r'^password_reset/done/$', 'django.contrib.auth.views.password_reset_done',
-        {'template_name': 'members/registration/password_reset_done.html',
-         'current_app': 'members'},
-        name='password_reset_done'),
-    url(r'^password_reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', 'django.contrib.auth.views.password_reset_confirm',
-        {'post_reset_redirect': 'members:password_reset_complete',
-         'template_name': 'members/registration/password_reset_confirm.html'},
-        name='password_reset_confirm'),
-    url(r'^password_reset/complete/$', 'django.contrib.auth.views.password_reset_complete',
-        {'template_name': 'members/registration/password_reset_complete.html'},
-        name='password_reset_complete'),
-
-
-    url(r'^detail/$', views.detail,
-        name='detail'),
-
-    url(r'^subscriptions/', views.subscriptions, name='subscriptions'),
+    "",
+    url(r"^$", views.index, name="index"),
+    url(
+        r"^login/$",
+        "django.contrib.auth.views.login",
+        {"template_name": "members/registration/login.html"},
+        name="login",
+    ),
+    url(r"^logout/$", "django.contrib.auth.views.logout_then_login", name="logout"),
+    url(
+        r"^password_change/$",
+        "django.contrib.auth.views.password_change",
+        {
+            "post_change_redirect": "members:password_change_done",
+            "template_name": "members/registration/password_change_form.html",
+        },
+        name="password_change",
+    ),
+    url(
+        r"^password_change_done/$",
+        "django.contrib.auth.views.password_change_done",
+        {"template_name": "members/registration/password_change_done.html"},
+        name="password_change_done",
+    ),
+    url(
+        r"^password_reset/$",
+        "django.contrib.auth.views.password_reset",
+        {
+            "post_reset_redirect": "members:password_reset_done",
+            "template_name": "members/registration/password_reset_form.html",
+            "email_template_name": "members/registration/password_reset_email.html",
+            "subject_template_name": "members/registration/password_reset_subject.txt",
+        },
+        name="password_reset",
+    ),
+    url(
+        r"^password_reset/done/$",
+        "django.contrib.auth.views.password_reset_done",
+        {
+            "template_name": "members/registration/password_reset_done.html",
+            "current_app": "members",
+        },
+        name="password_reset_done",
+    ),
+    url(
+        r"^password_reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$",
+        "django.contrib.auth.views.password_reset_confirm",
+        {
+            "post_reset_redirect": "members:password_reset_complete",
+            "template_name": "members/registration/password_reset_confirm.html",
+        },
+        name="password_reset_confirm",
+    ),
+    url(
+        r"^password_reset/complete/$",
+        "django.contrib.auth.views.password_reset_complete",
+        {"template_name": "members/registration/password_reset_complete.html"},
+        name="password_reset_complete",
+    ),
+    url(r"^detail/$", views.detail, name="detail"),
+    url(r"^subscriptions/", views.subscriptions, name="subscriptions"),
     # 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'^contact/', views.contact, name='contact'),
+    url(r"^invoices/", views.invoices, name="invoices"),
+    url(r"^contact/", views.contact, name="contact"),
 )
 )

+ 34 - 27
coin/members/views.py

@@ -8,21 +8,22 @@ from django.http import Http404
 from django.conf import settings
 from django.conf import settings
 from forms import PersonMemberChangeForm, OrganizationMemberChangeForm
 from forms import PersonMemberChangeForm, OrganizationMemberChangeForm
 
 
+
 @login_required
 @login_required
 def index(request):
 def index(request):
-    has_isp_feed = 'isp' in [k for k, _, _ in settings.FEEDS]
-    return render_to_response('members/index.html',
-                              {'has_isp_feed': has_isp_feed},
-                              context_instance=RequestContext(request))
+    has_isp_feed = "isp" in [k for k, _, _ in settings.FEEDS]
+    return render_to_response(
+        "members/index.html",
+        {"has_isp_feed": has_isp_feed},
+        context_instance=RequestContext(request),
+    )
 
 
 
 
 @login_required
 @login_required
 def detail(request):
 def detail(request):
 
 
     membership_info_url = settings.MEMBER_MEMBERSHIP_INFO_URL
     membership_info_url = settings.MEMBER_MEMBERSHIP_INFO_URL
-    context={
-        'membership_info_url': membership_info_url,
-    }
+    context = {"membership_info_url": membership_info_url}
 
 
     if settings.MEMBER_CAN_EDIT_PROFILE:
     if settings.MEMBER_CAN_EDIT_PROFILE:
         if request.user.type == "natural_person":
         if request.user.type == "natural_person":
@@ -31,15 +32,15 @@ def detail(request):
             form_cls = OrganizationMemberChangeForm
             form_cls = OrganizationMemberChangeForm
 
 
         if request.method == "POST":
         if request.method == "POST":
-            form = form_cls(data = request.POST, instance = request.user)
+            form = form_cls(data=request.POST, instance=request.user)
             if form.is_valid():
             if form.is_valid():
                 form.save()
                 form.save()
         else:
         else:
-            form = form_cls(instance = request.user)
+            form = form_cls(instance=request.user)
 
 
-        context['form'] = form
+        context["form"] = form
 
 
-    return render(request, 'members/detail.html', context)
+    return render(request, "members/detail.html", context)
 
 
 
 
 @login_required
 @login_required
@@ -47,27 +48,33 @@ def subscriptions(request):
     subscriptions = request.user.get_active_subscriptions()
     subscriptions = request.user.get_active_subscriptions()
     old_subscriptions = request.user.get_inactive_subscriptions()
     old_subscriptions = request.user.get_inactive_subscriptions()
 
 
-    return render_to_response('members/subscriptions.html',
-                              {'subscriptions': subscriptions,
-                               'old_subscriptions': old_subscriptions},
-                              context_instance=RequestContext(request))
+    return render_to_response(
+        "members/subscriptions.html",
+        {"subscriptions": subscriptions, "old_subscriptions": old_subscriptions},
+        context_instance=RequestContext(request),
+    )
 
 
 
 
 @login_required
 @login_required
 def invoices(request):
 def invoices(request):
-    balance  = request.user.balance
-    invoices = request.user.invoices.filter(validated=True).order_by('-date')
-    payments = request.user.payments.filter().order_by('-date')
-
-    return render_to_response('members/invoices.html',
-                              {'balance' : balance, 
-                               'handle_balance' : settings.HANDLE_BALANCE, 
-                               'invoices': invoices, 
-                               'payments': payments},
-                              context_instance=RequestContext(request))
+    balance = request.user.balance
+    invoices = request.user.invoices.filter(validated=True).order_by("-date")
+    payments = request.user.payments.filter().order_by("-date")
+
+    return render_to_response(
+        "members/invoices.html",
+        {
+            "balance": balance,
+            "handle_balance": settings.HANDLE_BALANCE,
+            "invoices": invoices,
+            "payments": payments,
+        },
+        context_instance=RequestContext(request),
+    )
 
 
 
 
 @login_required
 @login_required
 def contact(request):
 def contact(request):
-    return render_to_response('members/contact.html',
-                              context_instance=RequestContext(request))
+    return render_to_response(
+        "members/contact.html", context_instance=RequestContext(request)
+    )

+ 10 - 9
coin/mixins.py

@@ -17,21 +17,23 @@ class CoinLdapSyncMixin(object):
     """
     """
 
 
     def sync_to_ldap(self, creation, *args, **kwargs):
     def sync_to_ldap(self, creation, *args, **kwargs):
-        raise NotImplementedError('Using CoinLdapSyncModel require '
-                                  'sync_to_ldap method being implemented')
+        raise NotImplementedError(
+            "Using CoinLdapSyncModel require " "sync_to_ldap method being implemented"
+        )
 
 
     def delete_from_ldap(self, *args, **kwargs):
     def delete_from_ldap(self, *args, **kwargs):
-        raise NotImplementedError('Using CoinLdapSyncModel require '
-                                  'delete_from_ldap method being implemented')
+        raise NotImplementedError(
+            "Using CoinLdapSyncModel require "
+            "delete_from_ldap method being implemented"
+        )
 
 
     @transaction.atomic
     @transaction.atomic
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
         # Détermine si on est dans une création ou une mise à jour
         # Détermine si on est dans une création ou une mise à jour
-        creation = (self.pk == None)
+        creation = self.pk == None
 
 
         # Récupère les champs mis à jour si cela est précisé
         # Récupère les champs mis à jour si cela est précisé
-        update_fields = kwargs[
-            'update_fields'] if 'update_fields' in kwargs else None
+        update_fields = kwargs["update_fields"] if "update_fields" in kwargs else None
 
 
         # Sauvegarde en base de donnée (mais sans commit, cf decorator)
         # Sauvegarde en base de donnée (mais sans commit, cf decorator)
         super(CoinLdapSyncMixin, self).save(*args, **kwargs)
         super(CoinLdapSyncMixin, self).save(*args, **kwargs)
@@ -41,8 +43,7 @@ class CoinLdapSyncMixin(object):
         # commit
         # commit
         if settings.LDAP_ACTIVATE:
         if settings.LDAP_ACTIVATE:
             try:
             try:
-                self.sync_to_ldap(
-                    creation=creation, update_fields=update_fields)
+                self.sync_to_ldap(creation=creation, update_fields=update_fields)
             except:
             except:
                 raise
                 raise
 
 

+ 55 - 28
coin/offers/admin.py

@@ -7,19 +7,26 @@ from polymorphic.admin import PolymorphicChildModelAdmin
 
 
 from coin.members.models import Member
 from coin.members.models import Member
 from coin.offers.models import Offer, OfferSubscription
 from coin.offers.models import Offer, OfferSubscription
-from coin.offers.offersubscription_filter import\
-            OfferSubscriptionTerminationFilter,\
-            OfferSubscriptionCommitmentFilter
+from coin.offers.offersubscription_filter import (
+    OfferSubscriptionTerminationFilter,
+    OfferSubscriptionCommitmentFilter,
+)
 from coin.offers.forms import OfferAdminForm
 from coin.offers.forms import OfferAdminForm
 import autocomplete_light
 import autocomplete_light
 
 
 
 
 class OfferAdmin(admin.ModelAdmin):
 class OfferAdmin(admin.ModelAdmin):
-    list_display = ('get_configuration_type_display', 'name', 'reference', 'billing_period', 'period_fees',
-                    'initial_fees')
-    list_display_links = ('name',)
-    list_filter = ('configuration_type',)
-    search_fields = ['name']
+    list_display = (
+        "get_configuration_type_display",
+        "name",
+        "reference",
+        "billing_period",
+        "period_fees",
+        "initial_fees",
+    )
+    list_display_links = ("name",)
+    list_filter = ("configuration_type",)
+    search_fields = ["name"]
     form = OfferAdminForm
     form = OfferAdminForm
 
 
     # def get_readonly_fields(self, request, obj=None):
     # def get_readonly_fields(self, request, obj=None):
@@ -30,27 +37,42 @@ class OfferAdmin(admin.ModelAdmin):
 
 
 
 
 class OfferSubscriptionAdmin(admin.ModelAdmin):
 class OfferSubscriptionAdmin(admin.ModelAdmin):
-    list_display = ('get_subscription_reference', 'member', 'offer',
-                    'subscription_date', 'commitment', 'resign_date')
-    list_display_links = ('member','offer')
-    list_filter = ( OfferSubscriptionTerminationFilter,
-                    OfferSubscriptionCommitmentFilter,
-                    'offer', 'member')
-    search_fields = ['member__first_name', 'member__last_name', 'member__email',
-                     'member__nickname']
-    
+    list_display = (
+        "get_subscription_reference",
+        "member",
+        "offer",
+        "subscription_date",
+        "commitment",
+        "resign_date",
+    )
+    list_display_links = ("member", "offer")
+    list_filter = (
+        OfferSubscriptionTerminationFilter,
+        OfferSubscriptionCommitmentFilter,
+        "offer",
+        "member",
+    )
+    search_fields = [
+        "member__first_name",
+        "member__last_name",
+        "member__email",
+        "member__nickname",
+    ]
+
     fields = (
     fields = (
-                'member',
-                'offer',
-                'subscription_date',
-                'commitment',
-                'resign_date',
-                'comments'
-             )
+        "member",
+        "offer",
+        "subscription_date",
+        "commitment",
+        "resign_date",
+        "comments",
+    )
     # Si c'est un super user on renvoie un formulaire avec tous les membres et toutes les offres (donc autocomplétion pour les membres)
     # Si c'est un super user on renvoie un formulaire avec tous les membres et toutes les offres (donc autocomplétion pour les membres)
     def get_form(self, request, obj=None, **kwargs):
     def get_form(self, request, obj=None, **kwargs):
         if request.user.is_superuser:
         if request.user.is_superuser:
-            kwargs['form'] = autocomplete_light.modelform_factory(OfferSubscription, fields='__all__')
+            kwargs["form"] = autocomplete_light.modelform_factory(
+                OfferSubscription, fields="__all__"
+            )
         return super(OfferSubscriptionAdmin, self).get_form(request, obj, **kwargs)
         return super(OfferSubscriptionAdmin, self).get_form(request, obj, **kwargs)
 
 
     # Si pas super user on restreint les membres et offres accessibles
     # Si pas super user on restreint les membres et offres accessibles
@@ -59,8 +81,12 @@ class OfferSubscriptionAdmin(admin.ModelAdmin):
             if db_field.name == "member":
             if db_field.name == "member":
                 kwargs["queryset"] = Member.objects.manageable_by(request.user)
                 kwargs["queryset"] = Member.objects.manageable_by(request.user)
             if db_field.name == "offer":
             if db_field.name == "offer":
-                kwargs["queryset"] = Offer.objects.filter(id__in=[p.id for p in Offer.objects.manageable_by(request.user)])
-        return super(OfferSubscriptionAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
+                kwargs["queryset"] = Offer.objects.filter(
+                    id__in=[p.id for p in Offer.objects.manageable_by(request.user)]
+                )
+        return super(OfferSubscriptionAdmin, self).formfield_for_foreignkey(
+            db_field, request, **kwargs
+        )
 
 
     # Si pas super user on restreint la liste des offres que l'on peut voir
     # Si pas super user on restreint la liste des offres que l'on peut voir
     def get_queryset(self, request):
     def get_queryset(self, request):
@@ -78,9 +104,10 @@ class OfferSubscriptionAdmin(admin.ModelAdmin):
         """
         """
         if obj is not None:
         if obj is not None:
             for item in PolymorphicChildModelAdmin.__subclasses__():
             for item in PolymorphicChildModelAdmin.__subclasses__():
-                if (item.base_model.__name__ == obj.offer.configuration_type):
+                if item.base_model.__name__ == obj.offer.configuration_type:
                     return [item.inline(self.model, self.admin_site)]
                     return [item.inline(self.model, self.admin_site)]
         return []
         return []
 
 
+
 admin.site.register(Offer, OfferAdmin)
 admin.site.register(Offer, OfferAdmin)
 admin.site.register(OfferSubscription, OfferSubscriptionAdmin)
 admin.site.register(OfferSubscription, OfferSubscriptionAdmin)

+ 5 - 2
coin/offers/forms.py

@@ -9,6 +9,9 @@ class OfferAdminForm(ModelForm):
     class Meta:
     class Meta:
         model = Offer
         model = Offer
         widgets = {
         widgets = {
-            'configuration_type': Select(choices=(('','---------'),) + Configuration.get_configurations_choices_list())
+            "configuration_type": Select(
+                choices=(("", "---------"),)
+                + Configuration.get_configurations_choices_list()
+            )
         }
         }
-        exclude = ('', )
+        exclude = ("",)

+ 28 - 11
coin/offers/management/commands/offer_subscriptions_count.py

@@ -9,36 +9,53 @@ from django.db.models import Q, Count
 
 
 from coin.offers.models import Offer, OfferSubscription
 from coin.offers.models import Offer, OfferSubscription
 
 
+# This file could not be formatted by Black.
+
 
 
 BOLD_START = '\033[1m'
 BOLD_START = '\033[1m'
 BOLD_END = '\033[0m'
 BOLD_END = '\033[0m'
 
 
+
 class Command(BaseCommand):
 class Command(BaseCommand):
     option_list = BaseCommand.option_list + (
     option_list = BaseCommand.option_list + (
-        make_option('--date', action='store', dest='date',
-                default=datetime.date.today(), help='Specifies the date to use. Format is YYYY-MM-DD. Default is "today".'),
+        make_option(
+            "--date",
+            action="store", dest="date",
+            default=datetime.date.today(),
+            help='Specifies the date to use. Format is YYYY-MM-DD. Default is "today".'
+        ),
     )
     )
     help = "Return subscription count for each offer type"
     help = "Return subscription count for each offer type"
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
         # Get date option
         # Get date option
-        date = options.get('date')
+        date = options.get("date")
 
 
         # Validate date type
         # Validate date type
         if type(date) is not datetime.date:
         if type(date) is not datetime.date:
             try:
             try:
-                datetime.datetime.strptime(date, '%Y-%m-%d')
+                datetime.datetime.strptime(date, "%Y-%m-%d")
             except ValueError, TypeError:
             except ValueError, TypeError:
                 raise CommandError("Incorrect date format, should be YYYY-MM-DD")
                 raise CommandError("Incorrect date format, should be YYYY-MM-DD")
 
 
         # Count offer subscription
         # Count offer subscription
-        offers = Offer.objects\
-                      .filter(Q(offersubscription__subscription_date__lte=date) & (Q(offersubscription__resign_date__gt=date) | Q(offersubscription__resign_date__isnull=True)))\
-                      .annotate(num_subscribtions=Count('offersubscription'))\
-                      .order_by('name')
+        offers = (Offer.objects
+            .filter(
+                Q(offersubscription__subscription_date__lte=date) &
+                (
+                    Q(offersubscription__resign_date__gt=date) |
+                    Q(offersubscription__resign_date__isnull=True)
+                )
+            )
+            .annotate(num_subscribtions=Count("offersubscription"))
+            .order_by("name")
+        )
 
 
         # Print count by offer type
         # Print count by offer type
         for offer in offers:
         for offer in offers:
-            self.stdout.write("{offer} offer has {count} subscriber(s)".format(
-                offer=BOLD_START + offer.name + BOLD_END,
-                count=BOLD_START + str(offer.num_subscribtions) + BOLD_END))
+            self.stdout.write(
+                "{offer} offer has {count} subscriber(s)".format(
+                    offer=BOLD_START + offer.name + BOLD_END,
+                    count=BOLD_START + str(offer.num_subscribtions) + BOLD_END
+                )
+            )

+ 98 - 58
coin/offers/models.py

@@ -16,14 +16,23 @@ class OfferManager(models.Manager):
         voir les membres et les abonnements dans l'interface d'administration.
         voir les membres et les abonnements dans l'interface d'administration.
         """
         """
         from coin.members.models import RowLevelPermission
         from coin.members.models import RowLevelPermission
+
         # toutes les permissions appliquées à cet utilisateur
         # toutes les permissions appliquées à cet utilisateur
         # (liste de chaines de caractères)
         # (liste de chaines de caractères)
         perms = user.get_all_permissions()
         perms = user.get_all_permissions()
-        allowedcodenames = [ s.split('offers.',1)[1] for s in perms if s.startswith('offers.')]
+        allowedcodenames = [
+            s.split("offers.", 1)[1] for s in perms if s.startswith("offers.")
+        ]
         # parmi toutes les RowLevelPermission, celles qui sont relatives à des OfferSubscription et qui sont dans allowedcodenames
         # parmi toutes les RowLevelPermission, celles qui sont relatives à des OfferSubscription et qui sont dans allowedcodenames
-        rowperms = RowLevelPermission.objects.filter(content_type=ContentType.objects.get_for_model(OfferSubscription), codename__in=allowedcodenames)
+        rowperms = RowLevelPermission.objects.filter(
+            content_type=ContentType.objects.get_for_model(OfferSubscription),
+            codename__in=allowedcodenames,
+        )
         # toutes les Offers pour lesquelles il existe une RowLevelpermission correspondante dans rowperms
         # toutes les Offers pour lesquelles il existe une RowLevelpermission correspondante dans rowperms
-        return super(OfferManager, self).filter(rowlevelpermission__in=rowperms).distinct()
+        return (
+            super(OfferManager, self).filter(rowlevelpermission__in=rowperms).distinct()
+        )
+
 
 
 class Offer(models.Model):
 class Offer(models.Model):
     """Description of an offer available to subscribers.
     """Description of an offer available to subscribers.
@@ -34,31 +43,50 @@ class Offer(models.Model):
     The choices list is dynamically generated at start in the __init__
     The choices list is dynamically generated at start in the __init__
     """
     """
 
 
-    name = models.CharField(max_length=255, blank=False, null=False,
-                            verbose_name="nom de l'offre")
-    reference = models.CharField(max_length=255, blank=True,
-                                    verbose_name="référence de l'offre",
-                                    help_text="Identifiant a utiliser par exemple comme identifiant de virement")
-    configuration_type = models.CharField(max_length=50,
-                            blank=True,
-                            verbose_name='type de configuration',
-                            help_text="Type de configuration à utiliser avec cette offre")
-    billing_period = models.IntegerField(blank=False, null=False, default=1,
-                                         verbose_name='période de facturation',
-                                         help_text='en mois',
-                                         validators=[MinValueValidator(1)])
-    period_fees = models.DecimalField(max_digits=5, decimal_places=2,
-                                      blank=False, null=False,
-                                      verbose_name='montant par période de '
-                                                   'facturation',
-                                      help_text='en €')
-    initial_fees = models.DecimalField(max_digits=5, decimal_places=2,
-                                      blank=False, null=False,
-                                      verbose_name='frais de mise en service',
-                                      help_text='en €')
-    non_billable = models.BooleanField(default=False,
-                                       verbose_name='n\'est pas facturable',
-                                       help_text='L\'offre ne sera pas facturée par la commande charge_members')
+    name = models.CharField(
+        max_length=255, blank=False, null=False, verbose_name="nom de l'offre"
+    )
+    reference = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name="référence de l'offre",
+        help_text="Identifiant a utiliser par exemple comme identifiant de virement",
+    )
+    configuration_type = models.CharField(
+        max_length=50,
+        blank=True,
+        verbose_name="type de configuration",
+        help_text="Type de configuration à utiliser avec cette offre",
+    )
+    billing_period = models.IntegerField(
+        blank=False,
+        null=False,
+        default=1,
+        verbose_name="période de facturation",
+        help_text="en mois",
+        validators=[MinValueValidator(1)],
+    )
+    period_fees = models.DecimalField(
+        max_digits=5,
+        decimal_places=2,
+        blank=False,
+        null=False,
+        verbose_name="montant par période de " "facturation",
+        help_text="en €",
+    )
+    initial_fees = models.DecimalField(
+        max_digits=5,
+        decimal_places=2,
+        blank=False,
+        null=False,
+        verbose_name="frais de mise en service",
+        help_text="en €",
+    )
+    non_billable = models.BooleanField(
+        default=False,
+        verbose_name="n'est pas facturable",
+        help_text="L'offre ne sera pas facturée par la commande charge_members",
+    )
 
 
     objects = OfferManager()
     objects = OfferManager()
 
 
@@ -67,11 +95,13 @@ class Offer(models.Model):
         Renvoi le nom affichable du type de configuration
         Renvoi le nom affichable du type de configuration
         """
         """
         from coin.configuration.models import Configuration
         from coin.configuration.models import Configuration
+
         for item in Configuration.get_configurations_choices_list():
         for item in Configuration.get_configurations_choices_list():
             if item and self.configuration_type in item:
             if item and self.configuration_type in item:
                 return item[1]
                 return item[1]
         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):
     def display_price(self):
         """Displays the price of an offer in a human-readable manner
         """Displays the price of an offer in a human-readable manner
@@ -86,15 +116,14 @@ class Offer(models.Model):
         else:
         else:
             period = self.billing_period
             period = self.billing_period
         return "{period_fee}€ / {billing_period} mois".format(
         return "{period_fee}€ / {billing_period} mois".format(
-            period_fee=fee,
-            billing_period=period)
+            period_fee=fee, billing_period=period
+        )
 
 
     def __unicode__(self):
     def __unicode__(self):
-        return '{name} - {price}'.format(name=self.name,
-                                         price=self.display_price())
+        return "{name} - {price}".format(name=self.name, price=self.display_price())
 
 
     class Meta:
     class Meta:
-        verbose_name = 'offre'
+        verbose_name = "offre"
 
 
 
 
 class OfferSubscriptionQuerySet(models.QuerySet):
 class OfferSubscriptionQuerySet(models.QuerySet):
@@ -107,15 +136,17 @@ class OfferSubscriptionQuerySet(models.QuerySet):
         if at_date is None:
         if at_date is None:
             at_date = datetime.date.today()
             at_date = datetime.date.today()
 
 
-        return self.filter(Q(subscription_date__lte=at_date) &
-                           (Q(resign_date__gt=at_date) |
-                            Q(resign_date__isnull=True)))
+        return self.filter(
+            Q(subscription_date__lte=at_date)
+            & (Q(resign_date__gt=at_date) | Q(resign_date__isnull=True))
+        )
 
 
     def offer_summary(self):
     def offer_summary(self):
         """ Agregates as a count of subscriptions per offer
         """ Agregates as a count of subscriptions per offer
         """
         """
-        return self.values('offer__name', 'offer__reference').annotate(
-            num_subscriptions=Count('offer'))
+        return self.values("offer__name", "offer__reference").annotate(
+            num_subscriptions=Count("offer")
+        )
 
 
 
 
 class OfferSubscription(models.Model):
 class OfferSubscription(models.Model):
@@ -126,44 +157,53 @@ class OfferSubscription(models.Model):
     (technical configuration for the technology)) relate to this class
     (technical configuration for the technology)) relate to this class
     with a OneToOneField
     with a OneToOneField
     """
     """
+
     subscription_date = models.DateField(
     subscription_date = models.DateField(
         null=False,
         null=False,
         blank=False,
         blank=False,
         default=datetime.date.today,
         default=datetime.date.today,
-        verbose_name="date de souscription à l'offre")
+        verbose_name="date de souscription à l'offre",
+    )
     # TODO: for data retention, prevent deletion of a subscription object
     # TODO: for data retention, prevent deletion of a subscription object
     # while the resign date is recent enough (e.g. one year in France).
     # while the resign date is recent enough (e.g. one year in France).
     resign_date = models.DateField(
     resign_date = models.DateField(
-        null=True,
-        blank=True,
-        verbose_name='date de résiliation')
+        null=True, blank=True, verbose_name="date de résiliation"
+    )
     # TODO: move this to offers?
     # TODO: move this to offers?
-    commitment = models.IntegerField(blank=False, null=False,
-                                     verbose_name="période d'engagement",
-                                     help_text='en mois',
-                                     validators=[MinValueValidator(0)],
-                                     default=0)
-    comments = models.TextField(blank=True, verbose_name='commentaires',
-                                help_text="Commentaires libres (informations"
-                                " spécifiques concernant l'abonnement)")
-    member = models.ForeignKey('members.Member', verbose_name='membre')
-    offer = models.ForeignKey('Offer', verbose_name='offre')
+    commitment = models.IntegerField(
+        blank=False,
+        null=False,
+        verbose_name="période d'engagement",
+        help_text="en mois",
+        validators=[MinValueValidator(0)],
+        default=0,
+    )
+    comments = models.TextField(
+        blank=True,
+        verbose_name="commentaires",
+        help_text="Commentaires libres (informations"
+        " spécifiques concernant l'abonnement)",
+    )
+    member = models.ForeignKey("members.Member", verbose_name="membre")
+    offer = models.ForeignKey("Offer", verbose_name="offre")
 
 
     objects = OfferSubscriptionQuerySet().as_manager()
     objects = OfferSubscriptionQuerySet().as_manager()
 
 
     def get_subscription_reference(self):
     def get_subscription_reference(self):
         return settings.SUBSCRIPTION_REFERENCE.format(subscription=self)
         return settings.SUBSCRIPTION_REFERENCE.format(subscription=self)
-    get_subscription_reference.short_description = 'Référence'
+
+    get_subscription_reference.short_description = "Référence"
 
 
     def __unicode__(self):
     def __unicode__(self):
-        return '%s - %s - %s' % (self.member, self.offer.name,
-                                       self.subscription_date)
+        return "%s - %s - %s" % (self.member, self.offer.name, self.subscription_date)
 
 
     class Meta:
     class Meta:
-        verbose_name = 'abonnement'
+        verbose_name = "abonnement"
 
 
 
 
 def count_active_subscriptions():
 def count_active_subscriptions():
     today = datetime.date.today()
     today = datetime.date.today()
-    query = Q(subscription_date__lte=today) & (Q(resign_date__isnull=True) | Q(resign_date__gte=today))
+    query = Q(subscription_date__lte=today) & (
+        Q(resign_date__isnull=True) | Q(resign_date__gte=today)
+    )
     return OfferSubscription.objects.filter(query).count()
     return OfferSubscription.objects.filter(query).count()

+ 28 - 22
coin/offers/offersubscription_filter.py

@@ -2,44 +2,50 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 from django.contrib.admin import SimpleListFilter
 from django.contrib.admin import SimpleListFilter
-from django.db.models import Q,F
+from django.db.models import Q, F
 import datetime
 import datetime
 
 
 
 
 class OfferSubscriptionTerminationFilter(SimpleListFilter):
 class OfferSubscriptionTerminationFilter(SimpleListFilter):
-    title = 'Abonnement'
-    parameter_name = 'termination'
+    title = "Abonnement"
+    parameter_name = "termination"
 
 
     def lookups(self, request, model_admin):
     def lookups(self, request, model_admin):
         return (
         return (
-            ('not_terminated', 'Abonnements en cours'),
-            ('terminated', 'Abonnements résiliés'),
+            ("not_terminated", "Abonnements en cours"),
+            ("terminated", "Abonnements résiliés"),
         )
         )
 
 
     def queryset(self, request, queryset):
     def queryset(self, request, queryset):
-        if self.value() == 'not_terminated':
-            return queryset.filter(Q(resign_date__gt=datetime.date.today) | Q(resign_date__isnull=True))
-        if self.value() == 'terminated':
+        if self.value() == "not_terminated":
+            return queryset.filter(
+                Q(resign_date__gt=datetime.date.today) | Q(resign_date__isnull=True)
+            )
+        if self.value() == "terminated":
             return queryset.filter(resign_date__lte=datetime.date.today)
             return queryset.filter(resign_date__lte=datetime.date.today)
 
 
 
 
 class OfferSubscriptionCommitmentFilter(SimpleListFilter):
 class OfferSubscriptionCommitmentFilter(SimpleListFilter):
-    title = 'Engagement'
-    parameter_name = 'commitment'
+    title = "Engagement"
+    parameter_name = "commitment"
 
 
     def lookups(self, request, model_admin):
     def lookups(self, request, model_admin):
-        return (
-            ('committed', 'Est engagé'),
-            ('not_committed', 'N\'est plus engagé'),
-        )
+        return (("committed", "Est engagé"), ("not_committed", "N'est plus engagé"))
 
 
     def queryset(self, request, queryset):
     def queryset(self, request, queryset):
-        if self.value() == 'committed':
-            # TODO : Faire mieux que du SQL écrit en dur. La ligne 
+        if self.value() == "committed":
+            # TODO : Faire mieux que du SQL écrit en dur. La ligne
             # en dessous ne fonctionne pas et je ne sais pas pourquoi
             # en dessous ne fonctionne pas et je ne sais pas pourquoi
-            return queryset.extra(where = ["subscription_date + INTERVAL '1 month' * commitment > current_date"])
-            #~ return queryset.filter(subscription_date__gte=datetime.date.today - relativedelta(months=F('commitment'))) 
-        if self.value() == 'not_committed':
-            return queryset.extra(where = ["subscription_date + INTERVAL '1 month' * commitment <= current_date"])
-            #~ return queryset.filter(subscription_date__lte=datetime.date.today - relativedelta(months=F('commitment'))) 
-
+            return queryset.extra(
+                where=[
+                    "subscription_date + INTERVAL '1 month' * commitment > current_date"
+                ]
+            )
+            # ~ return queryset.filter(subscription_date__gte=datetime.date.today - relativedelta(months=F('commitment')))
+        if self.value() == "not_committed":
+            return queryset.extra(
+                where=[
+                    "subscription_date + INTERVAL '1 month' * commitment <= current_date"
+                ]
+            )
+            # ~ return queryset.filter(subscription_date__lte=datetime.date.today - relativedelta(months=F('commitment')))

+ 7 - 3
coin/offers/urls.py

@@ -5,8 +5,12 @@ from django.conf.urls import patterns, url
 from coin.offers.views import ConfigurationRedirectView, subscription_count_json
 from coin.offers.views import ConfigurationRedirectView, subscription_count_json
 
 
 urlpatterns = patterns(
 urlpatterns = patterns(
-    '',
+    "",
     # Redirect to the appropriate configuration backend.
     # Redirect to the appropriate configuration backend.
-    url(r'^configuration/(?P<id>.+)$', ConfigurationRedirectView.as_view(), name="configuration-redirect"),
-    url(r'^api/v1/count$', subscription_count_json),
+    url(
+        r"^configuration/(?P<id>.+)$",
+        ConfigurationRedirectView.as_view(),
+        name="configuration-redirect",
+    ),
+    url(r"^api/v1/count$", subscription_count_json),
 )
 )

+ 35 - 16
coin/offers/views.py

@@ -13,17 +13,30 @@ from django.http import JsonResponse, HttpResponseServerError
 
 
 from coin.offers.models import Offer, OfferSubscription
 from coin.offers.models import Offer, OfferSubscription
 
 
+# This file could not be formatted by Black.
+
 class ConfigurationRedirectView(RedirectView):
 class ConfigurationRedirectView(RedirectView):
-    """Redirects to the appropriate view for the configuration backend of the
-    specified subscription."""
+    """
+    Redirects to the appropriate view for the configuration backend of the
+    specified subscription.
+    """
 
 
     permanent = False
     permanent = False
 
 
     def get_redirect_url(self, *args, **kwargs):
     def get_redirect_url(self, *args, **kwargs):
-        subscription = get_object_or_404(OfferSubscription, pk=self.kwargs['id'],
-                                         member=self.request.user)
-        return reverse(subscription.configuration.url_namespace + ':' + subscription.configuration.backend_name,
-                       args=[subscription.configuration.pk])
+        subscription = get_object_or_404(
+            OfferSubscription,
+            pk=self.kwargs["id"],
+            member=self.request.user
+        )
+        return reverse(
+            (
+                subscription.configuration.url_namespace +
+                ':' +
+                subscription.configuration.backend_name
+            ),
+            args=[subscription.configuration.pk]
+        )
 
 
 
 
 # @cache_control(max_age=7200)
 # @cache_control(max_age=7200)
@@ -31,23 +44,29 @@ def subscription_count_json(request):
     output = []
     output = []
 
 
     # Get date form url, or set default
     # Get date form url, or set default
-    date = request.GET.get('date', datetime.date.today())
+    date = request.GET.get("date", datetime.date.today())
 
 
     # Validate date type
     # Validate date type
     if not isinstance(date, datetime.date):
     if not isinstance(date, datetime.date):
         try:
         try:
-            datetime.datetime.strptime(date, '%Y-%m-%d')
+            datetime.datetime.strptime(date, "%Y-%m-%d")
         except ValueError, TypeError:
         except ValueError, TypeError:
-            return HttpResponseServerError("Incorrect date format, should be YYYY-MM-DD")
+            return HttpResponseServerError(
+                "Incorrect date format, should be YYYY-MM-DD"
+            )
 
 
     # Get current offer subscription
     # Get current offer subscription
-    offersubscriptions = list(OfferSubscription.objects.running(date).offer_summary())
+    offersubscriptions = list(
+        OfferSubscription.objects.running(date).offer_summary()
+    )
     for offersub in offersubscriptions:
     for offersub in offersubscriptions:
-        output.append({
-            'reference' : offersub['offer__reference'],
-            'name' : offersub['offer__name'],
-            'subscriptions_count' : offersub['num_subscriptions']
-        })
+        output.append(
+            {
+                "reference" : offersub["offer__reference"],
+                "name" : offersub["offer__name"],
+                "subscriptions_count" : offersub["num_subscriptions"]
+            }
+        )
 
 
     # Return JSON
     # Return JSON
-    return JsonResponse(output, safe=False)
+    return JsonResponse(output, safe=False)

+ 7 - 6
coin/resources/admin.py

@@ -5,19 +5,20 @@ from django.contrib import admin
 
 
 from coin.resources.models import IPPool, IPSubnet
 from coin.resources.models import IPPool, IPSubnet
 
 
+
 class IPPoolAdmin(admin.ModelAdmin):
 class IPPoolAdmin(admin.ModelAdmin):
-    list_display = ('name', 'inet', 'default_subnetsize')
-    ordering = ('inet',)
+    list_display = ("name", "inet", "default_subnetsize")
+    ordering = ("inet",)
 
 
 
 
 # TODO: don't display "Delegate reverse DNS" checkbox and Nameservers when
 # TODO: don't display "Delegate reverse DNS" checkbox and Nameservers when
 # creating/editing the object in the admin (since it is a purely
 # creating/editing the object in the admin (since it is a purely
 # user-specific parameter)
 # user-specific parameter)
 class IPSubnetAdmin(admin.ModelAdmin):
 class IPSubnetAdmin(admin.ModelAdmin):
-    list_display = ('inet', 'ip_pool', 'configuration')
-    list_filter = ('ip_pool',)
-    search_fields = ('inet',)
-    ordering = ('inet',)
+    list_display = ("inet", "ip_pool", "configuration")
+    list_filter = ("ip_pool",)
+    search_fields = ("inet",)
+    ordering = ("inet",)
 
 
 
 
 admin.site.register(IPPool, IPPoolAdmin)
 admin.site.register(IPPool, IPPoolAdmin)

+ 83 - 35
coin/resources/models.py

@@ -10,29 +10,42 @@ from netaddr import IPSet
 
 
 class IPPool(models.Model):
 class IPPool(models.Model):
     """Pool of IP addresses (either v4 or v6)."""
     """Pool of IP addresses (either v4 or v6)."""
-    name = models.CharField(max_length=255, blank=False, null=False,
-                            verbose_name='nom',
-                            help_text="Nom du pool d'IP")
-    default_subnetsize = models.PositiveSmallIntegerField(blank=False,
-                                                          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(verbose_name='réseau',
-                            help_text="Bloc d'adresses IP du pool")
+
+    name = models.CharField(
+        max_length=255,
+        blank=False,
+        null=False,
+        verbose_name="nom",
+        help_text="Nom du pool d'IP",
+    )
+    default_subnetsize = models.PositiveSmallIntegerField(
+        blank=False,
+        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(
+        verbose_name="réseau", help_text="Bloc d'adresses IP du pool"
+    )
     objects = NetManager()
     objects = NetManager()
 
 
     def clean(self):
     def clean(self):
         if self.inet:
         if self.inet:
             max_subnetsize = 64 if self.inet.version == 6 else 32
             max_subnetsize = 64 if self.inet.version == 6 else 32
             if not self.inet.prefixlen <= self.default_subnetsize <= max_subnetsize:
             if not self.inet.prefixlen <= self.default_subnetsize <= max_subnetsize:
-                raise ValidationError('Taille de sous-réseau invalide')
+                raise ValidationError("Taille de sous-réseau invalide")
             # Check that related subnet are in the pool (useful when
             # Check that related subnet are in the pool (useful when
             # modifying an existing pool that already has subnets
             # modifying an existing pool that already has subnets
             # allocated in it)
             # allocated in it)
-            incorrect = [str(subnet) for subnet in self.ipsubnet_set.all()
-                         if not subnet.inet in self.inet]
+            incorrect = [
+                str(subnet)
+                for subnet in self.ipsubnet_set.all()
+                if not subnet.inet in self.inet
+            ]
             if incorrect:
             if incorrect:
-                err = "Des sous-réseaux se retrouveraient en-dehors du bloc d'IP: {}".format(incorrect)
+                err = "Des sous-réseaux se retrouveraient en-dehors du bloc d'IP: {}".format(
+                    incorrect
+                )
                 raise ValidationError(err)
                 raise ValidationError(err)
 
 
     def __unicode__(self):
     def __unicode__(self):
@@ -44,21 +57,30 @@ class IPPool(models.Model):
 
 
 
 
 class IPSubnet(models.Model):
 class IPSubnet(models.Model):
-    inet = CidrAddressField(blank=True,
-                            unique=True, verbose_name="sous-réseau",
-                            help_text="Laisser vide pour allouer automatiquement")
+    inet = CidrAddressField(
+        blank=True,
+        unique=True,
+        verbose_name="sous-réseau",
+        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")
-    configuration = models.ForeignKey('configuration.Configuration',
-                                      related_name='ip_subnet',
-                                      verbose_name='configuration')
-    delegate_reverse_dns = models.BooleanField(default=False,
-                                               verbose_name='déléguer le reverse DNS',
-                                               help_text='Déléguer la résolution DNS inverse de ce sous-réseau à un ou plusieurs serveurs de noms')
-    name_server = models.ManyToManyField('reverse_dns.NameServer',
-                                         blank=True,
-                                         verbose_name='serveur de noms',
-                                         help_text="Serveur de noms à qui déléguer la résolution DNS inverse")
+    configuration = models.ForeignKey(
+        "configuration.Configuration",
+        related_name="ip_subnet",
+        verbose_name="configuration",
+    )
+    delegate_reverse_dns = models.BooleanField(
+        default=False,
+        verbose_name="déléguer le reverse DNS",
+        help_text="Déléguer la résolution DNS inverse de ce sous-réseau à un ou plusieurs serveurs de noms",
+    )
+    name_server = models.ManyToManyField(
+        "reverse_dns.NameServer",
+        blank=True,
+        verbose_name="serveur de noms",
+        help_text="Serveur de noms à qui déléguer la résolution DNS inverse",
+    )
 
 
     def allocate(self):
     def allocate(self):
         """Automatically allocate a free subnet"""
         """Automatically allocate a free subnet"""
@@ -66,7 +88,11 @@ class IPSubnet(models.Model):
         used = IPSet((s.inet for s in self.ip_pool.ipsubnet_set.all()))
         used = IPSet((s.inet for s in self.ip_pool.ipsubnet_set.all()))
         free = pool.difference(used)
         free = pool.difference(used)
         # Generator for efficiency (we don't build the whole list)
         # Generator for efficiency (we don't build the whole list)
-        available = (p for p in free.iter_cidrs() if p.prefixlen <= self.ip_pool.default_subnetsize)
+        available = (
+            p
+            for p in free.iter_cidrs()
+            if p.prefixlen <= self.ip_pool.default_subnetsize
+        )
         # TODO: for IPv4, get rid of the network and broadcast
         # TODO: for IPv4, get rid of the network and broadcast
         # addresses? Not really needed nowadays, and we usually don't
         # addresses? Not really needed nowadays, and we usually don't
         # have a real subnet in practice (i.e. Ethernet segment), but
         # have a real subnet in practice (i.e. Ethernet segment), but
@@ -74,7 +100,9 @@ class IPSubnet(models.Model):
         try:
         try:
             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.
         # first_free is a subnet, but it might be too large for our needs.
         # This selects the first sub-subnet of the right size.
         # 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()
@@ -90,19 +118,39 @@ class IPSubnet(models.Model):
         # two requests, but the optimal solution will have to be retried once
         # two requests, but the optimal solution will have to be retried once
         # we use django-netfields>=0.7
         # we use django-netfields>=0.7
 
 
-        #conflicting = self.ip_pool.ipsubnet_set.filter(Q(inet__net_contained_or_equal=self.inet) |
+        # 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)
         #                                               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)
+        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:
         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))
+            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):
     def validate_reverse_dns(self):
         """Check that reverse DNS entries, if any, are included in the subnet"""
         """Check that reverse DNS entries, if any, are included in the subnet"""
-        incorrect = [str(rev.ip) for rev in self.reversednsentry_set.all() if not rev.ip in self.inet]
+        incorrect = [
+            str(rev.ip)
+            for rev in self.reversednsentry_set.all()
+            if not rev.ip in self.inet
+        ]
         if incorrect:
         if incorrect:
-            raise ValidationError("Des entrées DNS inverse ne sont pas dans le sous-réseau: {}.".format(incorrect))
+            raise ValidationError(
+                "Des entrées DNS inverse ne sont pas dans le sous-réseau: {}.".format(
+                    incorrect
+                )
+            )
 
 
     def clean(self):
     def clean(self):
         if not self.inet:
         if not self.inet:

+ 1 - 0
coin/resources/templatetags/subnets.py

@@ -7,6 +7,7 @@ from netaddr import IPNetwork
 
 
 register = template.Library()
 register = template.Library()
 
 
+
 @register.filter
 @register.filter
 def prettify(subnet):
 def prettify(subnet):
     """Prettify an IPv4 subnet by remove the subnet length when it is equal to /32
     """Prettify an IPv4 subnet by remove the subnet length when it is equal to /32

+ 2 - 2
coin/reverse_dns/admin.py

@@ -5,5 +5,5 @@ from django.contrib import admin
 
 
 from coin.reverse_dns.models import NameServer, ReverseDNSEntry
 from coin.reverse_dns.models import NameServer, ReverseDNSEntry
 
 
-admin.site.register(NameServer,)
-admin.site.register(ReverseDNSEntry,)
+admin.site.register(NameServer)
+admin.site.register(ReverseDNSEntry)

+ 38 - 23
coin/reverse_dns/models.py

@@ -10,53 +10,68 @@ import ldapdb.models
 from ldapdb.models.fields import CharField, IntegerField, ListField
 from ldapdb.models.fields import CharField, IntegerField, ListField
 
 
 # TODO: validate DNS names with this regex
 # TODO: validate DNS names with this regex
-REGEX = r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,})\.?$'
+REGEX = r"(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,})\.?$"
+
 
 
 class NameServer(models.Model):
 class NameServer(models.Model):
     # TODO: signal to IPSubnet when we are modified, so that is saves the
     # TODO: signal to IPSubnet when we are modified, so that is saves the
     # result into LDAP.  Actually, better: build a custom M2M relation
     # result into LDAP.  Actually, better: build a custom M2M relation
     # between NameServer and IPSubnet (see Capslock), and save in LDAP
     # between NameServer and IPSubnet (see Capslock), and save in LDAP
     # there.
     # there.
-    dns_name = models.CharField(max_length=255,
-                                verbose_name="nom du serveur",
-                                help_text="Exemple : ns1.example.com")
-    description = models.CharField(max_length=255, blank=True,
-                                   verbose_name="description du serveur",
-                                   help_text="Exemple : Mon serveur de noms principal")
+    dns_name = models.CharField(
+        max_length=255,
+        verbose_name="nom du serveur",
+        help_text="Exemple : ns1.example.com",
+    )
+    description = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name="description du serveur",
+        help_text="Exemple : Mon serveur de noms principal",
+    )
     owner = models.ForeignKey("members.Member", verbose_name="propriétaire")
     owner = models.ForeignKey("members.Member", verbose_name="propriétaire")
 
 
     def __unicode__(self):
     def __unicode__(self):
-        return "{} ({})".format(self.description, self.dns_name) if self.description else self.dns_name
+        return (
+            "{} ({})".format(self.description, self.dns_name)
+            if self.description
+            else self.dns_name
+        )
 
 
     class Meta:
     class Meta:
-        verbose_name = 'serveur de noms'
-        verbose_name_plural = 'serveurs de noms'
+        verbose_name = "serveur de noms"
+        verbose_name_plural = "serveurs de noms"
 
 
 
 
 class ReverseDNSEntry(models.Model):
 class ReverseDNSEntry(models.Model):
-    ip = InetAddressField(unique=True, verbose_name='adresse IP')
-    reverse = models.CharField(max_length=255, verbose_name='reverse',
-                               help_text="Nom à associer à l'adresse IP")
-    ttl = models.IntegerField(default=3600, verbose_name="TTL",
-                              help_text="en secondes",
-                              validators=[MinValueValidator(60)])
-    ip_subnet = models.ForeignKey('resources.IPSubnet',
-                                  verbose_name='sous-réseau IP')
+    ip = InetAddressField(unique=True, verbose_name="adresse IP")
+    reverse = models.CharField(
+        max_length=255,
+        verbose_name="reverse",
+        help_text="Nom à associer à l'adresse IP",
+    )
+    ttl = models.IntegerField(
+        default=3600,
+        verbose_name="TTL",
+        help_text="en secondes",
+        validators=[MinValueValidator(60)],
+    )
+    ip_subnet = models.ForeignKey("resources.IPSubnet", verbose_name="sous-réseau IP")
 
 
     objects = NetManager()
     objects = NetManager()
 
 
     def clean(self):
     def clean(self):
         if self.reverse:
         if self.reverse:
             # Check that the reverse ends with a "." (add it if necessary)
             # Check that the reverse ends with a "." (add it if necessary)
-            if not self.reverse.endswith('.'):
-                self.reverse += '.'
+            if not self.reverse.endswith("."):
+                self.reverse += "."
         if self.ip:
         if self.ip:
             if not self.ip in self.ip_subnet.inet:
             if not self.ip in self.ip_subnet.inet:
-                raise ValidationError('IP address must be included in the IP subnet.')
+                raise ValidationError("IP address must be included in the IP subnet.")
 
 
     def __unicode__(self):
     def __unicode__(self):
         return "{} → {}".format(self.ip, self.reverse)
         return "{} → {}".format(self.ip, self.reverse)
 
 
     class Meta:
     class Meta:
-        verbose_name = 'entrée DNS inverse'
-        verbose_name_plural = 'entrées DNS inverses'
+        verbose_name = "entrée DNS inverse"
+        verbose_name_plural = "entrées DNS inverses"

+ 87 - 98
coin/settings_base.py

@@ -20,14 +20,14 @@ MANAGERS = ADMINS
 
 
 DATABASES = {
 DATABASES = {
     # Database hosted on vagant test box
     # Database hosted on vagant test box
-    'default': {
-        'ENGINE': 'django.db.backends.postgresql_psycopg2',
-        'NAME': 'coin',
-        'USER': 'coin',
-        'PASSWORD': 'coin',
-        'HOST': 'localhost',  # Empty for localhost through domain sockets
-        'PORT': '15432',  # Empty for default
-    },
+    "default": {
+        "ENGINE": "django.db.backends.postgresql_psycopg2",
+        "NAME": "coin",
+        "USER": "coin",
+        "PASSWORD": "coin",
+        "HOST": "localhost",  # Empty for localhost through domain sockets
+        "PORT": "15432",  # Empty for default
+    }
 }
 }
 
 
 # Hosts/domain names that are valid for this site; required if DEBUG is False
 # Hosts/domain names that are valid for this site; required if DEBUG is False
@@ -38,11 +38,11 @@ ALLOWED_HOSTS = []
 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
 # although not all choices may be available on all operating systems.
 # although not all choices may be available on all operating systems.
 # In a Windows environment this must be set to your system time zone.
 # In a Windows environment this must be set to your system time zone.
-TIME_ZONE = 'Europe/Paris'
+TIME_ZONE = "Europe/Paris"
 
 
 # Language code for this installation. All choices can be found here:
 # Language code for this installation. All choices can be found here:
 # http://www.i18nguy.com/unicode/language-identifiers.html
 # http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'fr-fr'
+LANGUAGE_CODE = "fr-fr"
 
 
 SITE_ID = 1
 SITE_ID = 1
 
 
@@ -58,28 +58,28 @@ USE_L10N = True
 USE_TZ = True
 USE_TZ = True
 
 
 # Default URL for login and logout
 # Default URL for login and logout
-LOGIN_URL = '/members/login'
-LOGIN_REDIRECT_URL = '/members'
-LOGOUT_URL = '/members/logout'
+LOGIN_URL = "/members/login"
+LOGIN_REDIRECT_URL = "/members"
+LOGOUT_URL = "/members/logout"
 
 
 # Absolute filesystem path to the directory that will hold user-uploaded files.
 # Absolute filesystem path to the directory that will hold user-uploaded files.
 # Example: "/var/www/example.com/media/"
 # Example: "/var/www/example.com/media/"
-MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
+MEDIA_ROOT = os.path.join(BASE_DIR, "media/")
 
 
 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
 # trailing slash.
 # trailing slash.
 # Examples: "http://example.com/media/", "http://media.example.com/"
 # Examples: "http://example.com/media/", "http://media.example.com/"
-MEDIA_URL = '/media/'
+MEDIA_URL = "/media/"
 
 
 # Absolute path to the directory static files should be collected to.
 # Absolute path to the directory static files should be collected to.
 # Don't put anything in this directory yourself; store your static files
 # Don't put anything in this directory yourself; store your static files
 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
 # Example: "/var/www/example.com/static/"
 # Example: "/var/www/example.com/static/"
-STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
+STATIC_ROOT = os.path.join(BASE_DIR, "static/")
 
 
 # URL prefix for static files.
 # URL prefix for static files.
 # Example: "http://example.com/static/", "http://static.example.com/"
 # Example: "http://example.com/static/", "http://static.example.com/"
-STATIC_URL = '/static/'
+STATIC_URL = "/static/"
 
 
 # Additional locations of static files
 # Additional locations of static files
 STATICFILES_DIRS = (
 STATICFILES_DIRS = (
@@ -91,79 +91,79 @@ STATICFILES_DIRS = (
 # List of finder classes that know how to find static files in
 # List of finder classes that know how to find static files in
 # various locations.
 # various locations.
 STATICFILES_FINDERS = (
 STATICFILES_FINDERS = (
-    'django.contrib.staticfiles.finders.FileSystemFinder',
-    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
-    #'django.contrib.staticfiles.finders.DefaultStorageFinder',
+    "django.contrib.staticfiles.finders.FileSystemFinder",
+    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
+    # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
 )
 )
 
 
 # Location of private files. (Like invoices)
 # Location of private files. (Like invoices)
 # In production, this location should not be publicly accessible through
 # In production, this location should not be publicly accessible through
 # the web server
 # the web server
-PRIVATE_FILES_ROOT = os.path.join(BASE_DIR, 'smedia/')
+PRIVATE_FILES_ROOT = os.path.join(BASE_DIR, "smedia/")
 
 
 # Backend to use when sending private files to client
 # Backend to use when sending private files to client
 # In production, must be sendfile.backends.xsendfile with Apache xsend file mod
 # In production, must be sendfile.backends.xsendfile with Apache xsend file mod
 # Or failing xsendfile, use : sendfile.backends.simple
 # Or failing xsendfile, use : sendfile.backends.simple
 # https://github.com/johnsensible/django-sendfile
 # https://github.com/johnsensible/django-sendfile
-SENDFILE_BACKEND = 'sendfile.backends.development'
+SENDFILE_BACKEND = "sendfile.backends.development"
 
 
 # Make this unique, and don't share it with anybody.
 # Make this unique, and don't share it with anybody.
-SECRET_KEY = '!qy_)gao6q)57#mz1s-d$5^+dp1nt=lk1d19&9bb3co37vn)!3'
+SECRET_KEY = "!qy_)gao6q)57#mz1s-d$5^+dp1nt=lk1d19&9bb3co37vn)!3"
 
 
 # List of callables that know how to import templates from various sources.
 # List of callables that know how to import templates from various sources.
 TEMPLATE_LOADERS = (
 TEMPLATE_LOADERS = (
-    'django.template.loaders.filesystem.Loader',
-    'django.template.loaders.app_directories.Loader',
-    #'django.template.loaders.eggs.Loader',
+    "django.template.loaders.filesystem.Loader",
+    "django.template.loaders.app_directories.Loader",
+    # 'django.template.loaders.eggs.Loader',
 )
 )
 
 
 MIDDLEWARE_CLASSES = (
 MIDDLEWARE_CLASSES = (
-    'django.middleware.common.CommonMiddleware',
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
+    "django.middleware.common.CommonMiddleware",
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
     # Uncomment the next line for simple clickjacking protection:
     # Uncomment the next line for simple clickjacking protection:
     # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
     # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 )
 )
 
 
-ROOT_URLCONF = 'coin.urls'
+ROOT_URLCONF = "coin.urls"
 
 
 # Python dotted path to the WSGI application used by Django's runserver.
 # Python dotted path to the WSGI application used by Django's runserver.
-WSGI_APPLICATION = 'coin.wsgi.application'
+WSGI_APPLICATION = "coin.wsgi.application"
 
 
 TEMPLATE_DIRS = (
 TEMPLATE_DIRS = (
     # Only absolute paths, always forward slashes
     # Only absolute paths, always forward slashes
-    os.path.join(PROJECT_PATH, 'templates/'),
+    os.path.join(PROJECT_PATH, "templates/"),
 )
 )
 
 
 EXTRA_TEMPLATE_DIRS = tuple()
 EXTRA_TEMPLATE_DIRS = tuple()
 
 
 INSTALLED_APPS = (
 INSTALLED_APPS = (
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django.contrib.sessions',
-    'django.contrib.sites',
-    'ldapdb',  # LDAP as database backend
-    'django.contrib.messages',
-    'django.contrib.staticfiles',
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.sites",
+    "ldapdb",  # LDAP as database backend
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
     # Uncomment the next line to enable the admin:
     # Uncomment the next line to enable the admin:
-    'django.contrib.admin',
-    'netfields',
+    "django.contrib.admin",
+    "netfields",
     # Uncomment the next line to enable admin documentation:
     # Uncomment the next line to enable admin documentation:
-    #'django.contrib.admindocs',
-    'polymorphic',
+    # 'django.contrib.admindocs',
+    "polymorphic",
     # 'south',
     # 'south',
-    'autocomplete_light', #Automagic autocomplete foreingkey form component
-    'activelink', #Detect if a link match actual page
-    'coin',
-    'coin.members',
-    'coin.offers',
-    'coin.billing',
-    'coin.resources',
-    'coin.reverse_dns',
-    'coin.configuration',
-    'coin.isp_database',
+    "autocomplete_light",  # Automagic autocomplete foreingkey form component
+    "activelink",  # Detect if a link match actual page
+    "coin",
+    "coin.members",
+    "coin.offers",
+    "coin.billing",
+    "coin.resources",
+    "coin.reverse_dns",
+    "coin.configuration",
+    "coin.isp_database",
 )
 )
 
 
 EXTRA_INSTALLED_APPS = tuple()
 EXTRA_INSTALLED_APPS = tuple()
@@ -174,40 +174,30 @@ EXTRA_INSTALLED_APPS = tuple()
 # See http://docs.djangoproject.com/en/dev/topics/logging for
 # See http://docs.djangoproject.com/en/dev/topics/logging for
 # more details on how to customize your logging configuration.
 # more details on how to customize your logging configuration.
 LOGGING = {
 LOGGING = {
-    'version': 1,
-    'disable_existing_loggers': False,
-    'formatters': {
-    },
-    'filters': {
-        'require_debug_false': {
-            '()': 'django.utils.log.RequireDebugFalse'
-        }
-    },
-    'handlers': {
-        'mail_admins': {
-            'level': 'ERROR',
-            'filters': ['require_debug_false'],
-            'class': 'django.utils.log.AdminEmailHandler'
-        },
-        'console': {
-            'class': 'logging.StreamHandler',
+    "version": 1,
+    "disable_existing_loggers": False,
+    "formatters": {},
+    "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
+    "handlers": {
+        "mail_admins": {
+            "level": "ERROR",
+            "filters": ["require_debug_false"],
+            "class": "django.utils.log.AdminEmailHandler",
         },
         },
+        "console": {"class": "logging.StreamHandler"},
     },
     },
-    'loggers': {
-        'django.request': {
-            'handlers': ['mail_admins'],
-            'level': 'ERROR',
-            'propagate': True,
+    "loggers": {
+        "django.request": {
+            "handlers": ["mail_admins"],
+            "level": "ERROR",
+            "propagate": True,
         },
         },
-        'django': {
-            'handlers': ['console'],
-            'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
+        "django": {
+            "handlers": ["console"],
+            "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"),
         },
         },
-        "coin.billing": {
-            'handlers': ['console'],
-            'level': 'INFO',
-        }
-    }
+        "coin.billing": {"handlers": ["console"], "level": "INFO"},
+    },
 }
 }
 
 
 TEMPLATE_CONTEXT_PROCESSORS = (
 TEMPLATE_CONTEXT_PROCESSORS = (
@@ -220,22 +210,21 @@ TEMPLATE_CONTEXT_PROCESSORS = (
     "django.core.context_processors.request",
     "django.core.context_processors.request",
     "coin.isp_database.context_processors.branding",
     "coin.isp_database.context_processors.branding",
     "coin.context_processors.installed_apps",
     "coin.context_processors.installed_apps",
-    "django.contrib.messages.context_processors.messages")
+    "django.contrib.messages.context_processors.messages",
+)
 
 
-AUTH_USER_MODEL = 'members.Member'
+AUTH_USER_MODEL = "members.Member"
 
 
-AUTHENTICATION_BACKENDS = (
-    'django.contrib.auth.backends.ModelBackend',
-)
+AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",)
 
 
-TEST_RUNNER = 'django.test.runner.DiscoverRunner'
+TEST_RUNNER = "django.test.runner.DiscoverRunner"
 
 
 GRAPHITE_SERVER = "http://localhost"
 GRAPHITE_SERVER = "http://localhost"
 
 
 # Configuration for outgoing emails
 # Configuration for outgoing emails
-#DEFAULT_FROM_EMAIL = "coin@example.com"
-#EMAIL_USE_TLS = True
-#EMAIL_HOST = "smtp.chezmoi.tld"
+# DEFAULT_FROM_EMAIL = "coin@example.com"
+# EMAIL_USE_TLS = True
+# EMAIL_HOST = "smtp.chezmoi.tld"
 
 
 # Do we use LDAP or not
 # Do we use LDAP or not
 LDAP_ACTIVATE = False
 LDAP_ACTIVATE = False
@@ -250,11 +239,11 @@ MEMBER_DEFAULT_COTISATION = 20
 
 
 # Link to a page with information on how to become a member or pay the
 # Link to a page with information on how to become a member or pay the
 # membership fee
 # membership fee
-MEMBER_MEMBERSHIP_INFO_URL = ''
+MEMBER_MEMBERSHIP_INFO_URL = ""
 
 
 # Pattern used to display a unique reference for any subscription
 # Pattern used to display a unique reference for any subscription
 # Helpful for bank wire transfer identification
 # Helpful for bank wire transfer identification
-SUBSCRIPTION_REFERENCE = 'REF-{subscription.offer.reference}-{subscription.pk}'
+SUBSCRIPTION_REFERENCE = "REF-{subscription.offer.reference}-{subscription.pk}"
 
 
 # Payment delay in days
 # Payment delay in days
 PAYMENT_DELAY = 30
 PAYMENT_DELAY = 30
@@ -266,8 +255,8 @@ SESSION_COOKIE_AGE = 7200
 # feed name (used in template), url, max entries to display
 # feed name (used in template), url, max entries to display
 # "isp" entry gets picked automatically in default index template
 # "isp" entry gets picked automatically in default index template
 FEEDS = (
 FEEDS = (
-    ('ffdn', 'http://www.ffdn.org/fr/rss.xml', 3),
-#    ('isp', 'http://isp.example.com/feed/', 3),
+    ("ffdn", "http://www.ffdn.org/fr/rss.xml", 3),
+    #    ('isp', 'http://isp.example.com/feed/', 3),
 )
 )
 
 
 # Member can edit their own data
 # Member can edit their own data

+ 21 - 21
coin/settings_local.example-illyse.py

@@ -1,9 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-EXTRA_INSTALLED_APPS = (
-    'vpn',
-)
+EXTRA_INSTALLED_APPS = ("vpn",)
 
 
 LDAP_ACTIVATE = True
 LDAP_ACTIVATE = True
 
 
@@ -16,22 +14,22 @@ ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
 
 
 DATABASES = {
 DATABASES = {
     # Base de donnée du SI
     # Base de donnée du SI
-    'default': {
-        'ENGINE': 'django.db.backends.postgresql_psycopg2',
-        'NAME': 'illyse_coin',
-        'USER': 'illyse_coin',
-        'PASSWORD': '',
-        'HOST': '',  # Empty for localhost through domain sockets
-        'PORT': '',  # Empty for default
+    "default": {
+        "ENGINE": "django.db.backends.postgresql_psycopg2",
+        "NAME": "illyse_coin",
+        "USER": "illyse_coin",
+        "PASSWORD": "",
+        "HOST": "",  # Empty for localhost through domain sockets
+        "PORT": "",  # Empty for default
     },
     },
     # LDAP backend pour stockage et mise à jour de données
     # LDAP backend pour stockage et mise à jour de données
-    'ldap': {
-        'ENGINE': 'ldapdb.backends.ldap',
-        'NAME': 'ldap://ldapdev.illyse.org:389/',
-        'TLS': True,
-        'USER': 'cn=illysedev,ou=services,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR',
-        'PASSWORD': 'gfj83-E8ECgGh23JK_Ol12'
-    }
+    "ldap": {
+        "ENGINE": "ldapdb.backends.ldap",
+        "NAME": "ldap://ldapdev.illyse.org:389/",
+        "TLS": True,
+        "USER": "cn=illysedev,ou=services,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR",
+        "PASSWORD": "gfj83-E8ECgGh23JK_Ol12",
+    },
 }
 }
 
 
 # LDAP Base DNs
 # LDAP Base DNs
@@ -42,13 +40,15 @@ VPN_CONF_BASE_DN = "ou=vpn,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
 # First UID to use for users
 # First UID to use for users
 LDAP_USER_FIRST_UID = 2000
 LDAP_USER_FIRST_UID = 2000
 
 
-DATABASE_ROUTERS = ['ldapdb.router.Router']
+DATABASE_ROUTERS = ["ldapdb.router.Router"]
 
 
 GRAPHITE_SERVER = "http://graphite-dev.illyse.org"
 GRAPHITE_SERVER = "http://graphite-dev.illyse.org"
 
 
 DEFAULT_FROM_EMAIL = "adminsys@illyse.org"
 DEFAULT_FROM_EMAIL = "adminsys@illyse.org"
 
 
-FEEDS = (('isp', 'http://www.illyse.net/feed/', 3),
-         ('ffdn', 'http://www.ffdn.org/fr/rss.xml', 3))
+FEEDS = (
+    ("isp", "http://www.illyse.net/feed/", 3),
+    ("ffdn", "http://www.ffdn.org/fr/rss.xml", 3),
+)
 
 
-MEMBER_MEMBERSHIP_INFO_URL = 'https://www.illyse.org/projects/failocal/wiki/Cotisation'
+MEMBER_MEMBERSHIP_INFO_URL = "https://www.illyse.org/projects/failocal/wiki/Cotisation"

+ 1 - 4
coin/settings_test.py

@@ -2,10 +2,7 @@ from settings_base import *
 
 
 # settings for unit tests
 # settings for unit tests
 
 
-EXTRA_INSTALLED_APPS = (
-    'hardware_provisioning',
-    'vpn',
-)
+EXTRA_INSTALLED_APPS = ("hardware_provisioning", "vpn")
 
 
 TEMPLATE_DIRS = EXTRA_TEMPLATE_DIRS + TEMPLATE_DIRS
 TEMPLATE_DIRS = EXTRA_TEMPLATE_DIRS + TEMPLATE_DIRS
 INSTALLED_APPS = INSTALLED_APPS + EXTRA_INSTALLED_APPS
 INSTALLED_APPS = INSTALLED_APPS + EXTRA_INSTALLED_APPS

+ 13 - 18
coin/urls.py

@@ -11,9 +11,11 @@ from coin import views
 import coin.apps
 import coin.apps
 
 
 import autocomplete_light
 import autocomplete_light
+
 autocomplete_light.autodiscover()
 autocomplete_light.autodiscover()
 
 
 from django.contrib import admin
 from django.contrib import admin
+
 admin.autodiscover()
 admin.autodiscover()
 
 
 from coin.isp_database.views import isp_json
 from coin.isp_database.views import isp_json
@@ -25,27 +27,20 @@ def apps_urlpatterns():
     for app_config in apps.get_app_configs():
     for app_config in apps.get_app_configs():
         if isinstance(app_config, coin.apps.AppURLs):
         if isinstance(app_config, coin.apps.AppURLs):
             for prefix, pats in app_config.exported_urlpatterns:
             for prefix, pats in app_config.exported_urlpatterns:
-                yield url(
-                    r'^{}/'.format(prefix),
-                    include(pats, namespace=prefix))
-
-urlpatterns = patterns(
-    '',
-    url(r'^$', 'coin.members.views.index', name='home'),
-
-    url(r'^isp.json$', isp_json),
-    url(r'^members/', include('coin.members.urls', namespace='members')),
-    url(r'^billing/', include('coin.billing.urls', namespace='billing')),
-    url(r'^subscription/', include('coin.offers.urls', namespace='subscription')),
+                yield url(r"^{}/".format(prefix), include(pats, namespace=prefix))
 
 
-    url(r'^admin/', include(admin.site.urls)),
 
 
+urlpatterns = patterns(
+    "",
+    url(r"^$", "coin.members.views.index", name="home"),
+    url(r"^isp.json$", isp_json),
+    url(r"^members/", include("coin.members.urls", namespace="members")),
+    url(r"^billing/", include("coin.billing.urls", namespace="billing")),
+    url(r"^subscription/", include("coin.offers.urls", namespace="subscription")),
+    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"^feed/(?P<feed_name>.+)", views.feed, name="feed"),
+    url(r"^autocomplete/", include("autocomplete_light.urls")),
 )
 )
 
 
 urlpatterns += staticfiles_urlpatterns()
 urlpatterns += staticfiles_urlpatterns()

+ 27 - 17
coin/utils.py

@@ -25,19 +25,22 @@ from django.contrib.sites.models import Site
 private_files_storage = FileSystemStorage(location=settings.PRIVATE_FILES_ROOT)
 private_files_storage = FileSystemStorage(location=settings.PRIVATE_FILES_ROOT)
 
 
 # regexp which matches for ex irc://irc.example.tld/#channel
 # regexp which matches for ex irc://irc.example.tld/#channel
-re_chat_url = re.compile(r'(?P<protocol>\w+://)(?P<server>[\w\.]+)/(?P<channel>.*)')
+re_chat_url = re.compile(r"(?P<protocol>\w+://)(?P<server>[\w\.]+)/(?P<channel>.*)")
+
 
 
 def str_or_none(obj):
 def str_or_none(obj):
     return str(obj) if obj else None
     return str(obj) if obj else None
 
 
+
 def rstrip_str(s, suffix):
 def rstrip_str(s, suffix):
     """Return a copy of the string [s] with the string [suffix] removed from
     """Return a copy of the string [s] with the string [suffix] removed from
     the end (if [s] ends with [suffix], otherwise return s)."""
     the end (if [s] ends with [suffix], otherwise return s)."""
     if s.endswith(suffix):
     if s.endswith(suffix):
-        return s[:-len(suffix)]
+        return s[: -len(suffix)]
     else:
     else:
         return s
         return s
 
 
+
 def ldap_hash(password):
 def ldap_hash(password):
     """Hash a password for use with LDAP.  If the password is already hashed,
     """Hash a password for use with LDAP.  If the password is already hashed,
     do nothing.
     do nothing.
@@ -46,15 +49,17 @@ def ldap_hash(password):
     we have to encode/decode it as needed to switch between unicode and
     we have to encode/decode it as needed to switch between unicode and
     bytes.  The code should work fine with both python2 and python3.
     bytes.  The code should work fine with both python2 and python3.
     """
     """
-    if password and not password.startswith('{SSHA}'):
+    if password and not password.startswith("{SSHA}"):
         salt = binascii.hexlify(os.urandom(8))
         salt = binascii.hexlify(os.urandom(8))
         digest = hashlib.sha1(password.encode("utf-8") + salt).digest()
         digest = hashlib.sha1(password.encode("utf-8") + salt).digest()
-        return '{SSHA}' + base64.b64encode(digest + salt).decode("utf-8")
+        return "{SSHA}" + base64.b64encode(digest + salt).decode("utf-8")
     else:
     else:
         return password
         return password
 
 
 
 
-def send_templated_email(to, subject_template, body_template, context={}, attachements=[], **kwargs):
+def send_templated_email(
+    to, subject_template, body_template, context={}, attachements=[], **kwargs
+):
     """
     """
     Send a multialternative email based on html and optional txt template.
     Send a multialternative email based on html and optional txt template.
 
 
@@ -68,26 +73,26 @@ def send_templated_email(to, subject_template, body_template, context={}, attach
         attachements = [attachements]
         attachements = [attachements]
 
 
     # Add domain in context
     # Add domain in context
-    context['domain'] = Site.objects.get_current()
+    context["domain"] = Site.objects.get_current()
 
 
     # If .html/.txt is specified in template name remove it
     # If .html/.txt is specified in template name remove it
-    body_template = body_template.split('.')[0]
-    subject_template = subject_template.split('.')[0]
+    body_template = body_template.split(".")[0]
+    subject_template = subject_template.split(".")[0]
 
 
     # Get html template for body, fail if not exists
     # Get html template for body, fail if not exists
-    template_html = get_template('%s.html' % (body_template,))
+    template_html = get_template("%s.html" % (body_template,))
     html_content = template_html.render(Context(context))
     html_content = template_html.render(Context(context))
 
 
     # Get txt template for subject, fail if not exists
     # Get txt template for subject, fail if not exists
-    subject_template = get_template('%s.txt' % (subject_template,))
+    subject_template = get_template("%s.txt" % (subject_template,))
     subject = subject_template.render(Context(context))
     subject = subject_template.render(Context(context))
     # Get rid of newlines
     # Get rid of newlines
-    subject = subject.strip().replace('\n', '')
+    subject = subject.strip().replace("\n", "")
 
 
     # Try to get a txt version, convert from html to markdown style
     # Try to get a txt version, convert from html to markdown style
     # (using html2text) if fail
     # (using html2text) if fail
     try:
     try:
-        template_txt = get_template('%s.txt' % (body_template,))
+        template_txt = get_template("%s.txt" % (body_template,))
         text_content = template_txt.render_to_string(Context(context))
         text_content = template_txt.render_to_string(Context(context))
     except TemplateDoesNotExist:
     except TemplateDoesNotExist:
         text_content = html2text.html2text(html_content)
         text_content = html2text.html2text(html_content)
@@ -110,6 +115,7 @@ def delete_selected(modeladmin, request, queryset):
     for obj in queryset:
     for obj in queryset:
         obj.delete()
         obj.delete()
 
 
+
 delete_selected.short_description = "Supprimer tous les objets sélectionnés."
 delete_selected.short_description = "Supprimer tous les objets sélectionnés."
 
 
 # Time-related functions
 # Time-related functions
@@ -130,6 +136,7 @@ def end_of_month():
     else:
     else:
         return date(today.year, today.month + 1, 1) - timedelta(days=1)
         return date(today.year, today.month + 1, 1) - timedelta(days=1)
 
 
+
 @contextmanager
 @contextmanager
 def respect_language(language):
 def respect_language(language):
     """Context manager that changes the current translation language for
     """Context manager that changes the current translation language for
@@ -153,21 +160,25 @@ def respect_language(language):
 
 
 def respects_language(fun):
 def respects_language(fun):
     """Associated decorator"""
     """Associated decorator"""
+
     @wraps(fun)
     @wraps(fun)
     def _inner(*args, **kwargs):
     def _inner(*args, **kwargs):
-        with respect_language(kwargs.pop('language', None)):
+        with respect_language(kwargs.pop("language", None)):
             return fun(*args, **kwargs)
             return fun(*args, **kwargs)
+
     return _inner
     return _inner
 
 
 
 
 def disable_for_loaddata(signal_handler):
 def disable_for_loaddata(signal_handler):
     """Decorator for post_save events that disables them when loading
     """Decorator for post_save events that disables them when loading
     data from fixtures."""
     data from fixtures."""
+
     @wraps(signal_handler)
     @wraps(signal_handler)
     def wrapper(*args, **kwargs):
     def wrapper(*args, **kwargs):
-        if kwargs['raw']:
+        if kwargs["raw"]:
             return
             return
         signal_handler(*args, **kwargs)
         signal_handler(*args, **kwargs)
+
     return wrapper
     return wrapper
 
 
 
 
@@ -183,10 +194,9 @@ def postgresql_regexp(regexp):
     except AttributeError:
     except AttributeError:
         original_pattern = regexp
         original_pattern = regexp
 
 
-    return re.sub(
-        r'\?P<.*?>', '', original_pattern)
+    return re.sub(r"\?P<.*?>", "", original_pattern)
 
 
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     # ldap_hash expects an unicode string
     # ldap_hash expects an unicode string
     print(ldap_hash(sys.argv[1].decode("utf-8")))
     print(ldap_hash(sys.argv[1].decode("utf-8")))

+ 4 - 4
coin/validation.py

@@ -9,14 +9,14 @@ from .utils import re_chat_url
 
 
 def validate_v4(address):
 def validate_v4(address):
     if address.version != 4:
     if address.version != 4:
-        raise ValidationError('{} is not an IPv4 address'.format(address))
+        raise ValidationError("{} is not an IPv4 address".format(address))
 
 
 
 
 def validate_v6(address):
 def validate_v6(address):
     if address.version != 6:
     if address.version != 6:
-        raise ValidationError('{} is not an IPv6 address'.format(address))
+        raise ValidationError("{} is not an IPv6 address".format(address))
 
 
 
 
 chatroom_url_validator = RegexValidator(
 chatroom_url_validator = RegexValidator(
-    regex=re_chat_url,
-    message="Enter a value of the form  <proto>://<server>/<channel>")
+    regex=re_chat_url, message="Enter a value of the form  <proto>://<server>/<channel>"
+)

+ 11 - 9
coin/views.py

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

+ 1 - 0
coin/wsgi.py

@@ -25,6 +25,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "coin.settings")
 # file. This includes Django's development server, if the WSGI_APPLICATION
 # file. This includes Django's development server, if the WSGI_APPLICATION
 # setting points here.
 # setting points here.
 from django.core.wsgi import get_wsgi_application
 from django.core.wsgi import get_wsgi_application
+
 application = get_wsgi_application()
 application = get_wsgi_application()
 
 
 # Apply WSGI middleware here.
 # Apply WSGI middleware here.

+ 58 - 20
contrib/vpn_acct.py

@@ -10,6 +10,8 @@ import pyinotify
 sock = socket.socket()
 sock = socket.socket()
 last_timestamp = 0
 last_timestamp = 0
 
 
+# This file could not be formatted by Black.
+
 def logging(message, level = syslog.LOG_INFO):
 def logging(message, level = syslog.LOG_INFO):
     syslog.syslog(level, message)
     syslog.syslog(level, message)
     #print message
     #print message
@@ -34,8 +36,10 @@ def read_vpn_acct_file(filename):
         # format: username ip qos uptxbytes downrxbytes
         # format: username ip qos uptxbytes downrxbytes
         if len(d) == 5:
         if len(d) == 5:
             metrics.extend([
             metrics.extend([
-                ( '%s.%s.uptxbytes'   % (options.prefix, d[0]), (tstamp, int(d[3])) ),
-                ( '%s.%s.downrxbytes' % (options.prefix, d[0]), (tstamp, int(d[4])) ),
+                ('%s.%s.uptxbytes' %
+                (options.prefix, d[0]), (tstamp, int(d[3]))),
+                ('%s.%s.downrxbytes' %
+                (options.prefix, d[0]), (tstamp, int(d[4]))),
             ])
             ])
     return tstamp, metrics
     return tstamp, metrics
 
 
@@ -50,7 +54,7 @@ def send_to_carbon(metrics):
     
     
 
 
 def process_vpn_file(filename, old_tstamp = 0):
 def process_vpn_file(filename, old_tstamp = 0):
-    logging("Processing VPN file %s..." % (filename, ), syslog.LOG_DEBUG)
+    logging("Processing VPN file %s..." % (filename,), syslog.LOG_DEBUG)
 
 
     global last_timestamp
     global last_timestamp
     tstamp, metrics = read_vpn_acct_file(filename)
     tstamp, metrics = read_vpn_acct_file(filename)
@@ -68,31 +72,48 @@ def handle_old_vpn_files(directory, old_tstamp):
     # comparison when handling the matching files (see process_vpn_file() function).
     # comparison when handling the matching files (see process_vpn_file() function).
     
     
     old_date = datetime.fromtimestamp(old_tstamp-60)
     old_date = datetime.fromtimestamp(old_tstamp-60)
-    logging("Starting processing deferred files starting at %s..." % (old_date.strftime('%Y%m%d%H%M%S'), ), syslog.LOG_DEBUG)
+    logging(
+        "Starting processing deferred files starting at %s..." %
+        (old_date.strftime('%Y%m%d%H%M%S'),),
+        syslog.LOG_DEBUG
+    )
     for f in iglob(directory + '/*'):
     for f in iglob(directory + '/*'):
         try:
         try:
-            tmp_tstamp = datetime.strptime(os.path.basename(f).split('-')[0], '%Y%m%d%H%M%S')
+            tmp_tstamp = datetime.strptime(
+                os.path.basename(f).split('-')[0],
+                '%Y%m%d%H%M%S'
+            )
         except ValueError:
         except ValueError:
             # Bad filename format: likely not an accounting file...
             # Bad filename format: likely not an accounting file...
             pass
             pass
         else:
         else:
             if tmp_tstamp > old_date:
             if tmp_tstamp > old_date:
-                logging("Processing deferred file %s..." % (f, ), syslog.LOG_DEBUG)
+                logging(
+                    "Processing deferred file %s..." % (f,),
+                    syslog.LOG_DEBUG
+                )
                 process_vpn_file(f, old_tstamp)
                 process_vpn_file(f, old_tstamp)
 
 
 
 
 class VPNAcctHandler(pyinotify.ProcessEvent):
 class VPNAcctHandler(pyinotify.ProcessEvent):
-
     def process_IN_MOVED_TO(self, event):
     def process_IN_MOVED_TO(self, event):
         self.handle_file(event)
         self.handle_file(event)
+
     def process_IN_CLOSE_WRITE(self, event):
     def process_IN_CLOSE_WRITE(self, event):
         self.handle_file(event)
         self.handle_file(event)
+
     def handle_file(self, event):
     def handle_file(self, event):
-        logging("Event detected for file %s..." % (event.pathname, ), syslog.LOG_DEBUG)
+        logging(
+            "Event detected for file %s..." % (event.pathname,),
+            syslog.LOG_DEBUG
+        )
 
 
         if last_timestamp > 0:
         if last_timestamp > 0:
             # Error ongoing: will wait till the connection is back (and last_timestamp == 0)
             # Error ongoing: will wait till the connection is back (and last_timestamp == 0)
-            logging("Error ongoing: %s will be processed later..." % (event.pathname, ), syslog.LOG_DEBUG)
+            logging(
+                "Error ongoing: %s will be processed later..." % (event.pathname,),
+                syslog.LOG_DEBUG
+            )
             return
             return
 
 
         process_vpn_file(event.pathname)
         process_vpn_file(event.pathname)
@@ -113,19 +134,26 @@ if __name__ == '__main__':
     parser.add_argument('-p', '--port', dest='port', type=int, default=2004, help='Carbon daemon port')
     parser.add_argument('-p', '--port', dest='port', type=int, default=2004, help='Carbon daemon port')
     options = parser.parse_args()
     options = parser.parse_args()
 
 
-    logging("Connecting to Carbon agent on %(server)s on port %(port)d..." %
-        { 'server': options.server, 'port': options.port, }, syslog.LOG_DEBUG)
+    logging(
+        "Connecting to Carbon agent on %(server)s on port %(port)d..." %
+        {"server": options.server, "port": options.port},
+        syslog.LOG_DEBUG
+    )
     try:
     try:
         sock = socket.create_connection( (options.server, options.port) )
         sock = socket.create_connection( (options.server, options.port) )
     except socket.error:
     except socket.error:
-        logging("Couldn't connect to %(server)s on port %(port)d, is Carbon agent running?" %
-            { 'server': options.server, 'port': options.port, }, syslog.LOG_ERR)
+        logging(
+            "Couldn't connect to %(server)s on port %(port)d, is Carbon agent running?" %
+            {"server": options.server, "port": options.port},
+            syslog.LOG_ERR
+        )
         sys.exit(1)
         sys.exit(1)
 
 
     if options.tstamp > 0:
     if options.tstamp > 0:
         handle_old_vpn_files(options.directory, options.tstamp)
         handle_old_vpn_files(options.directory, options.tstamp)
     
     
     last_timestamp = 0
     last_timestamp = 0
+    
     def on_loop(notifier):
     def on_loop(notifier):
         """
         """
         Function called after each event loop to handle connexion errors.
         Function called after each event loop to handle connexion errors.
@@ -135,23 +163,33 @@ if __name__ == '__main__':
         if last_timestamp > 0:
         if last_timestamp > 0:
             sock.close()
             sock.close()
             try:
             try:
-                sock = socket.create_connection( (options.server, options.port) )
+                sock = socket.create_connection((options.server, options.port))
             except socket.error:
             except socket.error:
-                logging("Couldn't connect to %(server)s on port %(port)d, is Carbon agent running?" %
-                    { 'server': options.server, 'port': options.port, }, syslog.LOG_ERR)
+                logging(
+                    "Couldn't connect to %(server)s on port %(port)d, is Carbon agent running?" %
+                    {"server": options.server, "port": options.port},
+                    syslog.LOG_ERR
+                )
             else:
             else:
                 handle_old_vpn_files(options.directory, last_timestamp)
                 handle_old_vpn_files(options.directory, last_timestamp)
                 last_timestamp = 0
                 last_timestamp = 0
 
 
     # https://github.com/seb-m/pyinotify/blob/master/python2/examples/daemon.py
     # https://github.com/seb-m/pyinotify/blob/master/python2/examples/daemon.py
-    logging("Starting to watch %s directory..." % (options.directory, ))
+    logging("Starting to watch %s directory..." % (options.directory,))
     wm = pyinotify.WatchManager()
     wm = pyinotify.WatchManager()
     handler = VPNAcctHandler()
     handler = VPNAcctHandler()
     notifier = pyinotify.Notifier(wm, handler)
     notifier = pyinotify.Notifier(wm, handler)
-    wm.add_watch(options.directory, pyinotify.IN_CLOSE_WRITE|pyinotify.IN_MOVED_TO)
+    wm.add_watch(
+        options.directory,
+        pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO
+    )
 
 
     try:
     try:
-        notifier.loop(daemonize=True, callback=on_loop, pid_file='/var/run/pyinotify.pid', stdout='/var/log/pyinotify.log')
+        notifier.loop(
+            daemonize=True,
+            callback=on_loop,
+            pid_file="/var/run/pyinotify.pid",
+            stdout="/var/log/pyinotify.log"
+        )
     except pyinotify.NotifierError, err:
     except pyinotify.NotifierError, err:
         print >> sys.stderr, err
         print >> sys.stderr, err
-

+ 1 - 1
hardware_provisioning/__init__.py

@@ -1 +1 @@
-default_app_config = 'hardware_provisioning.app.HardwareProvisioningConfig'
+default_app_config = "hardware_provisioning.app.HardwareProvisioningConfig"

+ 78 - 56
hardware_provisioning/admin.py

@@ -19,11 +19,10 @@ admin.site.register(ItemType)
 
 
 class OwnerFilter(admin.SimpleListFilter):
 class OwnerFilter(admin.SimpleListFilter):
     title = "Propriétaire"
     title = "Propriétaire"
-    parameter_name = 'owner'
+    parameter_name = "owner"
 
 
     def lookups(self, request, model_admin):
     def lookups(self, request, model_admin):
-        owners = [
-            (i.pk, i) for i in User.objects.filter(items__isnull=False)]
+        owners = [(i.pk, i) for i in User.objects.filter(items__isnull=False)]
 
 
         return [(None, "L'association")] + owners
         return [(None, "L'association")] + owners
 
 
@@ -36,21 +35,21 @@ class OwnerFilter(admin.SimpleListFilter):
 
 
 class AvailabilityFilter(admin.SimpleListFilter):
 class AvailabilityFilter(admin.SimpleListFilter):
     title = "Disponibilité"
     title = "Disponibilité"
-    parameter_name = 'availability'
+    parameter_name = "availability"
 
 
     def lookups(self, request, model_admin):
     def lookups(self, request, model_admin):
         return [
         return [
-            ('available', 'Disponible'),
-            ('borrowed', 'Emprunté'),
-            ('deployed', 'Déployé'),
+            ("available", "Disponible"),
+            ("borrowed", "Emprunté"),
+            ("deployed", "Déployé"),
         ]
         ]
 
 
     def queryset(self, request, queryset):
     def queryset(self, request, queryset):
-        if self.value() == 'available':
+        if self.value() == "available":
             return queryset.available()
             return queryset.available()
-        elif self.value() == 'borrowed':
+        elif self.value() == "borrowed":
             return queryset.borrowed()
             return queryset.borrowed()
-        elif self.value() == 'deployed':
+        elif self.value() == "deployed":
             return queryset.deployed()
             return queryset.deployed()
         else:
         else:
             return queryset
             return queryset
@@ -59,60 +58,67 @@ class AvailabilityFilter(admin.SimpleListFilter):
 @admin.register(Item)
 @admin.register(Item)
 class ItemAdmin(admin.ModelAdmin):
 class ItemAdmin(admin.ModelAdmin):
     list_display = (
     list_display = (
-        'designation', 'mac_address', 'serial', 'owner',
-        'buy_date', 'deployed', 'is_available', 'storage')
+        "designation",
+        "mac_address",
+        "serial",
+        "owner",
+        "buy_date",
+        "deployed",
+        "is_available",
+        "storage"
+    )
     list_filter = (
     list_filter = (
-        AvailabilityFilter, 'type', 'storage',
-        'buy_date', OwnerFilter)
+        AvailabilityFilter, "type", "storage", "buy_date", OwnerFilter
+    )
     search_fields = (
     search_fields = (
-        'designation', 'mac_address', 'serial',
-        'owner__email', 'owner__nickname',
-        'owner__first_name', 'owner__last_name')
+        "designation",
+        "mac_address",
+        "serial",
+        "owner__email",
+        "owner__nickname",
+        "owner__first_name",
+        "owner__last_name",
+    )
     save_as = True
     save_as = True
-    actions = ['give_back']
+    actions = ["give_back"]
 
 
-    form = autocomplete_light.modelform_factory(Loan, fields='__all__')
+    form = autocomplete_light.modelform_factory(Loan, fields="__all__")
 
 
     def give_back(self, request, queryset):
     def give_back(self, request, queryset):
         for item in queryset.filter(loans__loan_date_end=None):
         for item in queryset.filter(loans__loan_date_end=None):
             item.give_back()
             item.give_back()
-    give_back.short_description = 'Rendre le matériel'
+
+    give_back.short_description = "Rendre le matériel"
 
 
 
 
 class StatusFilter(admin.SimpleListFilter):
 class StatusFilter(admin.SimpleListFilter):
-    title = 'Statut'
-    parameter_name = 'status'
+    title = "Statut"
+    parameter_name = "status"
 
 
     def lookups(self, request, model_admin):
     def lookups(self, request, model_admin):
-        return [
-            ('all', 'Tout'),
-            (None, 'En cours'),
-            ('finished', 'Passés'),
-        ]
+        return [("all", "Tout"), (None, "En cours"), ("finished", "Passés")]
 
 
     def choices(self, cl):
     def choices(self, cl):
         for lookup, title in self.lookup_choices:
         for lookup, title in self.lookup_choices:
             yield {
             yield {
-                'selected': self.value() == lookup,
-                'query_string': cl.get_query_string({
-                    self.parameter_name: lookup,
-                }, []),
-                'display': title,
+                "selected": self.value() == lookup,
+                "query_string": cl.get_query_string({self.parameter_name: lookup}, []),
+                "display": title,
             }
             }
 
 
     def queryset(self, request, queryset):
     def queryset(self, request, queryset):
         v = self.value()
         v = self.value()
-        if v in (None, 'running'):
+        if v in (None, "running"):
             return queryset.running()
             return queryset.running()
-        elif v == 'finished':
+        elif v == "finished":
             return queryset.finished()
             return queryset.finished()
         else:
         else:
             return queryset
             return queryset
 
 
 
 
 class BorrowerFilter(admin.SimpleListFilter):
 class BorrowerFilter(admin.SimpleListFilter):
-    title = 'Adhérent emprunteur'
-    parameter_name = 'user'
+    title = "Adhérent emprunteur"
+    parameter_name = "user"
 
 
     def lookups(self, request, model_admin):
     def lookups(self, request, model_admin):
         users = set()
         users = set()
@@ -131,55 +137,69 @@ class ItemChoiceField(ModelChoiceField):
     # On surcharge cette méthode pour afficher mac et n° de série dans le menu
     # On surcharge cette méthode pour afficher mac et n° de série dans le menu
     # déroulant de sélection d'un objet dans la création d'un prêt.
     # déroulant de sélection d'un objet dans la création d'un prêt.
     def label_from_instance(self, obj):
     def label_from_instance(self, obj):
-        return obj.designation + ' ' + obj.get_mac_and_serial()
+        return obj.designation + " " + obj.get_mac_and_serial()
+
 
 
 @admin.register(Loan)
 @admin.register(Loan)
 class LoanAdmin(admin.ModelAdmin):
 class LoanAdmin(admin.ModelAdmin):
-    list_display = ('item', 'get_mac_and_serial', 'user', 'loan_date', 'loan_date_end')
-    list_filter = (StatusFilter, BorrowerFilter, 'item__designation')
+    list_display = ("item", "get_mac_and_serial", "user", "loan_date", "loan_date_end")
+    list_filter = (StatusFilter, BorrowerFilter, "item__designation")
     search_fields = (
     search_fields = (
-        'item__designation',
-        'user__nickname', 'user__username',
-        'user__first_name', 'user__last_name', )
-    actions = ['end_loan']
+        "item__designation",
+        "user__nickname",
+        "user__username",
+        "user__first_name",
+        "user__last_name",
+    )
+    actions = ["end_loan"]
 
 
     def end_loan(self, request, queryset):
     def end_loan(self, request, queryset):
-        queryset.filter(loan_date_end=None).update(
-            loan_date_end=datetime.now())
-    end_loan.short_description = 'Mettre fin au prêt'
+        queryset.filter(loan_date_end=None).update(loan_date_end=datetime.now())
+
+    end_loan.short_description = "Mettre fin au prêt"
 
 
-    form = autocomplete_light.modelform_factory(Loan, fields='__all__')
+    form = autocomplete_light.modelform_factory(Loan, fields="__all__")
 
 
     def formfield_for_foreignkey(self, db_field, request, **kwargs):
     def formfield_for_foreignkey(self, db_field, request, **kwargs):
-        if db_field.name == 'item':
-            kwargs['queryset'] = Item.objects.all()
+        if db_field.name == "item":
+            kwargs["queryset"] = Item.objects.all()
             return ItemChoiceField(**kwargs)
             return ItemChoiceField(**kwargs)
         else:
         else:
-            return super(LoanAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
+            return super(LoanAdmin, self).formfield_for_foreignkey(
+                db_field, request, **kwargs
+            )
 
 
 
 
 @admin.register(Storage)
 @admin.register(Storage)
 class StorageAdmin(admin.ModelAdmin):
 class StorageAdmin(admin.ModelAdmin):
-    list_display = ('name', 'truncated_notes', 'items_count')
+    list_display = ("name", "truncated_notes", "items_count")
 
 
     def truncated_notes(self, obj):
     def truncated_notes(self, obj):
         if len(obj.notes) > 50:
         if len(obj.notes) > 50:
-            return '{}…'.format(obj.notes[:50])
+            return "{}…".format(obj.notes[:50])
         else:
         else:
             return obj.notes
             return obj.notes
-    truncated_notes.short_description = 'notes'
+
+    truncated_notes.short_description = "notes"
+
 
 
 class LoanInline(admin.TabularInline):
 class LoanInline(admin.TabularInline):
     model = Loan
     model = Loan
     extra = 0
     extra = 0
-    exclude = ('notes',)
-    readonly_fields = ('item', 'get_mac_and_serial', 'loan_date', 'loan_date_end', 'is_running')
+    exclude = ("notes",)
+    readonly_fields = (
+        "item",
+        "get_mac_and_serial",
+        "loan_date",
+        "loan_date_end",
+        "is_running",
+    )
 
 
     show_change_link = True
     show_change_link = True
 
 
     def get_queryset(self, request):
     def get_queryset(self, request):
         qs = super(LoanInline, self).get_queryset(request)
         qs = super(LoanInline, self).get_queryset(request)
-        return qs.order_by('-loan_date_end')
+        return qs.order_by("-loan_date_end")
 
 
     def has_add_permission(self, request, obj=None):
     def has_add_permission(self, request, obj=None):
         return False
         return False
@@ -187,8 +207,10 @@ class LoanInline(admin.TabularInline):
     def has_delete_permission(self, request, obj=None):
     def has_delete_permission(self, request, obj=None):
         return False
         return False
 
 
+
 class MemberAdmin(coin.members.admin.MemberAdmin):
 class MemberAdmin(coin.members.admin.MemberAdmin):
     inlines = coin.members.admin.MemberAdmin.inlines + [LoanInline]
     inlines = coin.members.admin.MemberAdmin.inlines + [LoanInline]
 
 
+
 admin.site.unregister(coin.members.admin.Member)
 admin.site.unregister(coin.members.admin.Member)
 admin.site.register(coin.members.admin.Member, MemberAdmin)
 admin.site.register(coin.members.admin.Member, MemberAdmin)

+ 3 - 3
hardware_provisioning/app.py

@@ -6,6 +6,6 @@ import coin.apps
 
 
 
 
 class HardwareProvisioningConfig(AppConfig, coin.apps.AppURLs):
 class HardwareProvisioningConfig(AppConfig, coin.apps.AppURLs):
-    name = 'hardware_provisioning'
-    verbose_name = 'Prêt de matériel'
-    exported_urlpatterns = [('hardware_provisioning', 'hardware_provisioning.urls')]
+    name = "hardware_provisioning"
+    verbose_name = "Prêt de matériel"
+    exported_urlpatterns = [("hardware_provisioning", "hardware_provisioning.urls")]

+ 4 - 6
hardware_provisioning/fields.py

@@ -7,14 +7,12 @@ from django.utils.translation import ugettext_lazy as _
 from django.forms import fields
 from django.forms import fields
 from django.db import models
 from django.db import models
 
 
-MAC_RE = r'^([0-9a-fA-F]{2}([:-]?|$)){6}$'
+MAC_RE = r"^([0-9a-fA-F]{2}([:-]?|$)){6}$"
 mac_re = re.compile(MAC_RE)
 mac_re = re.compile(MAC_RE)
 
 
 
 
 class MACAddressFormField(fields.RegexField):
 class MACAddressFormField(fields.RegexField):
-    default_error_messages = {
-        'invalid': _(u'Enter a valid MAC address.'),
-    }
+    default_error_messages = {"invalid": _("Enter a valid MAC address.")}
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super(MACAddressFormField, self).__init__(mac_re, *args, **kwargs)
         super(MACAddressFormField, self).__init__(mac_re, *args, **kwargs)
@@ -24,14 +22,14 @@ class MACAddressField(models.Field):
     empty_strings_allowed = False
     empty_strings_allowed = False
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
-        kwargs['max_length'] = 17
+        kwargs["max_length"] = 17
         super(MACAddressField, self).__init__(*args, **kwargs)
         super(MACAddressField, self).__init__(*args, **kwargs)
 
 
     def get_internal_type(self):
     def get_internal_type(self):
         return "CharField"
         return "CharField"
 
 
     def formfield(self, **kwargs):
     def formfield(self, **kwargs):
-        defaults = {'form_class': MACAddressFormField}
+        defaults = {"form_class": MACAddressFormField}
         defaults.update(kwargs)
         defaults.update(kwargs)
         return super(MACAddressField, self).formfield(**defaults)
         return super(MACAddressField, self).formfield(**defaults)
 
 

+ 14 - 14
hardware_provisioning/forms.py

@@ -13,31 +13,33 @@ User = get_user_model()
 
 
 class LoanDeclareForm(forms.Form):
 class LoanDeclareForm(forms.Form):
     loan_date_end = forms.DateField(
     loan_date_end = forms.DateField(
-        label='Date de retour prévue',
+        label="Date de retour prévue",
         required=False,
         required=False,
         validators=[validate_future_date],
         validators=[validate_future_date],
-        input_formats=['%d/%m/%Y'],
-        help_text='laisser vide si non planifié',
-        widget=forms.TextInput(
-            attrs={'type': 'date', 'placeholder': 'JJ/MM/AAAA'}))
+        input_formats=["%d/%m/%Y"],
+        help_text="laisser vide si non planifié",
+        widget=forms.TextInput(attrs={"type": "date", "placeholder": "JJ/MM/AAAA"}),
+    )
 
 
 
 
 class LoanReturnForm(forms.Form):
 class LoanReturnForm(forms.Form):
     storage = forms.ModelChoiceField(
     storage = forms.ModelChoiceField(
-        label='Dans quel lieu de stockage ai-je remis le matériel ?',
+        label="Dans quel lieu de stockage ai-je remis le matériel ?",
         required=False,
         required=False,
-        queryset=Storage.objects.all(), empty_label='Je ne sais pas')
+        queryset=Storage.objects.all(),
+        empty_label="Je ne sais pas",
+    )
 
 
 
 
 class LoanTransferForm(forms.Form):
 class LoanTransferForm(forms.Form):
     target_user = forms.CharField(
     target_user = forms.CharField(
         max_length=100,
         max_length=100,
-        label='Adhérent',
-        help_text='email, pseudonyme ou numéro de l\'adhérent',
+        label="Adhérent",
+        help_text="email, pseudonyme ou numéro de l'adhérent",
     )
     )
 
 
     def clean_target_user(self):
     def clean_target_user(self):
-        value = self.cleaned_data['target_user']
+        value = self.cleaned_data["target_user"]
         result = User.objects.filter(
         result = User.objects.filter(
             Q(email__iexact=value)
             Q(email__iexact=value)
             | Q(pk__iexact=value)
             | Q(pk__iexact=value)
@@ -45,10 +47,8 @@ class LoanTransferForm(forms.Form):
             | Q(username__iexact=value)
             | Q(username__iexact=value)
         )
         )
         if result.count() > 1:
         if result.count() > 1:
-            raise ValidationError(
-                "La recherche retourne plus d'un adhérent")
+            raise ValidationError("La recherche retourne plus d'un adhérent")
         elif result.count() < 1:
         elif result.count() < 1:
-            raise ValidationError(
-                "Aucun adhérent ne correspond à cette recherche")
+            raise ValidationError("Aucun adhérent ne correspond à cette recherche")
 
 
         return result.first()
         return result.first()

+ 71 - 59
hardware_provisioning/models.py

@@ -10,23 +10,22 @@ from .fields import MACAddressField
 
 
 
 
 class ItemType(models.Model):
 class ItemType(models.Model):
-    name = models.CharField(max_length=100, verbose_name='nom')
+    name = models.CharField(max_length=100, verbose_name="nom")
 
 
     def __unicode__(self):
     def __unicode__(self):
         return self.name
         return self.name
 
 
     class Meta:
     class Meta:
-        verbose_name = 'type d’objet'
-        verbose_name_plural = 'types d’objet'
+        verbose_name = "type d’objet"
+        verbose_name_plural = "types d’objet"
 
 
 
 
 class ItemQuerySet(models.QuerySet):
 class ItemQuerySet(models.QuerySet):
     def _get_borrowed_pks(self):
     def _get_borrowed_pks(self):
-        return Loan.objects.running().values_list('item', flat=True)
+        return Loan.objects.running().values_list("item", flat=True)
 
 
     def available(self):
     def available(self):
-        return self.exclude(
-            pk__in=self._get_borrowed_pks()).exclude(deployed=True)
+        return self.exclude(pk__in=self._get_borrowed_pks()).exclude(deployed=True)
 
 
     def borrowed(self):
     def borrowed(self):
         return self.filter(pk__in=self._get_borrowed_pks())
         return self.filter(pk__in=self._get_borrowed_pks())
@@ -37,39 +36,52 @@ class ItemQuerySet(models.QuerySet):
     def unavailable(self):
     def unavailable(self):
         """ deployed or borrowed
         """ deployed or borrowed
         """
         """
-        return self.filter(
-            Q(pk__in=self._get_borrowed_pks()) |
-            Q(deployed=True))
+        return self.filter(Q(pk__in=self._get_borrowed_pks()) | Q(deployed=True))
 
 
 
 
 class Item(models.Model):
 class Item(models.Model):
-    type = models.ForeignKey(ItemType, verbose_name='type de matériel',
-                             related_name='items')
-    designation = models.CharField(max_length=100, verbose_name='désignation')
+    type = models.ForeignKey(
+        ItemType, verbose_name="type de matériel", related_name="items"
+    )
+    designation = models.CharField(max_length=100, verbose_name="désignation")
     storage = models.ForeignKey(
     storage = models.ForeignKey(
-        'Storage', related_name='items',
-        verbose_name='Lieu de stockage',
-        null=True, blank=True,
-        help_text='Laisser vide si inconnu')
+        "Storage",
+        related_name="items",
+        verbose_name="Lieu de stockage",
+        null=True,
+        blank=True,
+        help_text="Laisser vide si inconnu",
+    )
     mac_address = MACAddressField(
     mac_address = MACAddressField(
-        verbose_name='adresse MAC',
-        blank=True, null=True, unique=True,
-        help_text="préférable au n° de série si possible")
+        verbose_name="adresse MAC",
+        blank=True,
+        null=True,
+        unique=True,
+        help_text="préférable au n° de série si possible",
+    )
     serial = models.CharField(
     serial = models.CharField(
-        verbose_name='N° de série',
-        max_length=250, blank=True, null=True, unique=True,
-        help_text='ou toute autre référence unique')
-    buy_date = models.DateField(verbose_name='date d’achat' , blank=True , null=True)
+        verbose_name="N° de série",
+        max_length=250,
+        blank=True,
+        null=True,
+        unique=True,
+        help_text="ou toute autre référence unique",
+    )
+    buy_date = models.DateField(verbose_name="date d’achat", blank=True, null=True)
     owner = models.ForeignKey(
     owner = models.ForeignKey(
         settings.AUTH_USER_MODEL,
         settings.AUTH_USER_MODEL,
-        verbose_name='Propriétaire',
-        related_name='items',
-        null=True, blank=True,
-        help_text="dans le cas de matériel n'appartenant pas à l'association")
-    deployed = models.BooleanField(verbose_name='déployé', default=False,
-                                   help_text='Cocher si le matériel est en production')
-    comment = models.TextField(verbose_name='commentaire', blank=True,
-                               null=True)
+        verbose_name="Propriétaire",
+        related_name="items",
+        null=True,
+        blank=True,
+        help_text="dans le cas de matériel n'appartenant pas à l'association",
+    )
+    deployed = models.BooleanField(
+        verbose_name="déployé",
+        default=False,
+        help_text="Cocher si le matériel est en production",
+    )
+    comment = models.TextField(verbose_name="commentaire", blank=True, null=True)
 
 
     objects = ItemQuerySet().as_manager()
     objects = ItemQuerySet().as_manager()
 
 
@@ -99,8 +111,9 @@ class Item(models.Model):
         or if the item is deployed, returns False (else True).
         or if the item is deployed, returns False (else True).
         """
         """
         return (not self.deployed) and (not self.loans.running().exists())
         return (not self.deployed) and (not self.loans.running().exists())
+
     is_available.boolean = True
     is_available.boolean = True
-    is_available.short_description = 'disponible'
+    is_available.short_description = "disponible"
 
 
     def get_mac_and_serial(self):
     def get_mac_and_serial(self):
         mac = self.mac_address
         mac = self.mac_address
@@ -108,26 +121,24 @@ class Item(models.Model):
         if mac and serial:
         if mac and serial:
             return "{} / {}".format(mac, serial)
             return "{} / {}".format(mac, serial)
         else:
         else:
-            return mac or serial or ''
+            return mac or serial or ""
 
 
     class Meta:
     class Meta:
-        verbose_name = 'objet'
-        ordering = ['designation', 'mac_address', 'serial']
+        verbose_name = "objet"
+        ordering = ["designation", "mac_address", "serial"]
 
 
     def give_back(self, storage=None):
     def give_back(self, storage=None):
         self.storage = storage
         self.storage = storage
         self.save()
         self.save()
-        self.loans.running().update(
-            loan_date_end=timezone.now())
+        self.loans.running().update(loan_date_end=timezone.now())
 
 
 
 
 class LoanQuerySet(models.QuerySet):
 class LoanQuerySet(models.QuerySet):
-
     @staticmethod
     @staticmethod
     def _running_filter():
     def _running_filter():
-        return (
-            models.Q(loan_date_end__gt=timezone.now()) |
-            models.Q(loan_date_end__isnull=True))
+        return models.Q(loan_date_end__gt=timezone.now()) | models.Q(
+            loan_date_end__isnull=True
+        )
 
 
     def running(self):
     def running(self):
         return self.filter(self._running_filter())
         return self.filter(self._running_filter())
@@ -137,17 +148,18 @@ class LoanQuerySet(models.QuerySet):
 
 
 
 
 class Loan(models.Model):
 class Loan(models.Model):
-    item = models.ForeignKey(Item, verbose_name='objet', related_name='loans')
-    user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='membre',
-                             related_name='loans')
-    loan_date = models.DateTimeField(verbose_name='date de prêt')
-    loan_date_end = models.DateTimeField(verbose_name='date de fin de prêt',
-                                     null=True, blank=True)
+    item = models.ForeignKey(Item, verbose_name="objet", related_name="loans")
+    user = models.ForeignKey(
+        settings.AUTH_USER_MODEL, verbose_name="membre", related_name="loans"
+    )
+    loan_date = models.DateTimeField(verbose_name="date de prêt")
+    loan_date_end = models.DateTimeField(
+        verbose_name="date de fin de prêt", null=True, blank=True
+    )
     notes = models.TextField(null=True, blank=True)
     notes = models.TextField(null=True, blank=True)
 
 
     def __unicode__(self):
     def __unicode__(self):
-        return 'prêt de {item} à {user}'.format(
-            item=self.item, user=self.user)
+        return "prêt de {item} à {user}".format(item=self.item, user=self.user)
 
 
     def get_mac_and_serial(self):
     def get_mac_and_serial(self):
         return self.item.get_mac_and_serial()
         return self.item.get_mac_and_serial()
@@ -159,29 +171,29 @@ class Loan(models.Model):
 
 
     def is_running(self):
     def is_running(self):
         return not self.loan_date_end or self.loan_date_end > timezone.now()
         return not self.loan_date_end or self.loan_date_end > timezone.now()
+
     is_running.boolean = True
     is_running.boolean = True
-    is_running.short_description = 'En cours ?'
+    is_running.short_description = "En cours ?"
 
 
     class Meta:
     class Meta:
-        verbose_name = 'prêt d’objet'
-        verbose_name_plural = 'prêts d’objets'
+        verbose_name = "prêt d’objet"
+        verbose_name_plural = "prêts d’objets"
 
 
     objects = LoanQuerySet().as_manager()
     objects = LoanQuerySet().as_manager()
 
 
 
 
 class Storage(models.Model):
 class Storage(models.Model):
-    name = models.CharField(max_length=100, verbose_name='nom')
-    notes = models.TextField(
-        blank=True,
-        help_text='Lisible par tous les adhérents')
+    name = models.CharField(max_length=100, verbose_name="nom")
+    notes = models.TextField(blank=True, help_text="Lisible par tous les adhérents")
 
 
     def __unicode__(self):
     def __unicode__(self):
         return self.name
         return self.name
 
 
     def items_count(self):
     def items_count(self):
         return self.items.count()
         return self.items.count()
-    items_count.short_description = 'Nb. items stockés'
+
+    items_count.short_description = "Nb. items stockés"
 
 
     class Meta:
     class Meta:
-        verbose_name = 'lieu de stockage'
-        verbose_name_plural = 'lieux de stockage'
+        verbose_name = "lieu de stockage"
+        verbose_name_plural = "lieux de stockage"

+ 14 - 16
hardware_provisioning/tests.py

@@ -9,23 +9,20 @@ from .models import Item, ItemType, Loan
 
 
 def localize(naive_dt):
 def localize(naive_dt):
     if not timezone.is_naive(naive_dt):
     if not timezone.is_naive(naive_dt):
-        raise ValueError('Expecting a naive datetime')
+        raise ValueError("Expecting a naive datetime")
     else:
     else:
         return timezone.make_aware(naive_dt, timezone.get_current_timezone())
         return timezone.make_aware(naive_dt, timezone.get_current_timezone())
 
 
 
 
 class HardwareModelsFactoryMixin:
 class HardwareModelsFactoryMixin:
     def get_item_type(self, **kwargs):
     def get_item_type(self, **kwargs):
-        params = {'name': 'Foos'}
+        params = {"name": "Foos"}
         params.update(**kwargs)
         params.update(**kwargs)
         item_type, _ = ItemType.objects.get_or_create(**kwargs)
         item_type, _ = ItemType.objects.get_or_create(**kwargs)
         return item_type
         return item_type
 
 
     def get_item(self, **kwargs):
     def get_item(self, **kwargs):
-        params = {
-            'type': self.get_item_type(),
-            'designation': 'Test item',
-        }
+        params = {"type": self.get_item_type(), "designation": "Test item"}
         params.update(**kwargs)
         params.update(**kwargs)
         item, _ = Item.objects.get_or_create(**params)
         item, _ = Item.objects.get_or_create(**params)
         return item
         return item
@@ -33,14 +30,14 @@ class HardwareModelsFactoryMixin:
 
 
 class HardwareLoaningTestCase(HardwareModelsFactoryMixin, TestCase):
 class HardwareLoaningTestCase(HardwareModelsFactoryMixin, TestCase):
     def setUp(self):
     def setUp(self):
-        self.member = Member.objects.create(username='jdoe')
+        self.member = Member.objects.create(username="jdoe")
         self.item = self.get_item()
         self.item = self.get_item()
 
 
     def test_running_(self):
     def test_running_(self):
         loan_start_date = localize(datetime(2011, 1, 14, 12, 0, 0))
         loan_start_date = localize(datetime(2011, 1, 14, 12, 0, 0))
         loan = Loan.objects.create(
         loan = Loan.objects.create(
-            item=self.item, user=self.member,
-            loan_date=loan_start_date)
+            item=self.item, user=self.member, loan_date=loan_start_date
+        )
 
 
         self.assertEqual(Loan.objects.running().count(), 1)
         self.assertEqual(Loan.objects.running().count(), 1)
         self.assertEqual(Loan.objects.finished().count(), 0)
         self.assertEqual(Loan.objects.finished().count(), 0)
@@ -51,12 +48,11 @@ class HardwareLoaningTestCase(HardwareModelsFactoryMixin, TestCase):
 
 
 class ItemTestCase(HardwareModelsFactoryMixin, TestCase):
 class ItemTestCase(HardwareModelsFactoryMixin, TestCase):
     def setUp(self):
     def setUp(self):
-        self.member = Member.objects.create(username='jdoe')
+        self.member = Member.objects.create(username="jdoe")
 
 
-        self.free_item = self.get_item(designation='free')
-        self.deployed_item = self.get_item(
-            designation='deployed', deployed=True)
-        self.borrowed_item = self.get_item(designation='borrowed')
+        self.free_item = self.get_item(designation="free")
+        self.deployed_item = self.get_item(designation="deployed", deployed=True)
+        self.borrowed_item = self.get_item(designation="borrowed")
 
 
     def test_queryset_methods(self):
     def test_queryset_methods(self):
         self.assertEqual(Item.objects.borrowed().count(), 0)
         self.assertEqual(Item.objects.borrowed().count(), 0)
@@ -65,8 +61,10 @@ class ItemTestCase(HardwareModelsFactoryMixin, TestCase):
         self.assertEqual(Item.objects.unavailable().count(), 1)
         self.assertEqual(Item.objects.unavailable().count(), 1)
 
 
         Loan.objects.create(
         Loan.objects.create(
-            item=self.borrowed_item, user=self.member,
-            loan_date=localize(datetime(2011, 1, 14, 12, 0, 0)))
+            item=self.borrowed_item,
+            user=self.member,
+            loan_date=localize(datetime(2011, 1, 14, 12, 0, 0)),
+        )
 
 
         self.assertEqual(Item.objects.borrowed().count(), 1)
         self.assertEqual(Item.objects.borrowed().count(), 1)
         self.assertEqual(Item.objects.deployed().count(), 1)
         self.assertEqual(Item.objects.deployed().count(), 1)

+ 6 - 6
hardware_provisioning/urls.py

@@ -6,10 +6,10 @@ from . import views
 
 
 
 
 urlpatterns = [
 urlpatterns = [
-    url(r'^$', views.loan_list, name='loan-list'),
-    url(r'^items/list$', views.item_list, name='item-list'),
-    url(r'^items/(?P<pk>[0-9]+)/borrow$', views.item_borrow, name='item-borrow'),
-    url(r'^(?P<pk>[0-9]+)/return$', views.loan_return, name='loan-return'),
-    url(r'^(?P<pk>[0-9]+)/transfer$', views.loan_transfer, name='loan-transfer'),
-    url(r'^(?P<pk>[0-9]+)$', views.loan_detail, name='loan-detail'),
+    url(r"^$", views.loan_list, name="loan-list"),
+    url(r"^items/list$", views.item_list, name="item-list"),
+    url(r"^items/(?P<pk>[0-9]+)/borrow$", views.item_borrow, name="item-borrow"),
+    url(r"^(?P<pk>[0-9]+)/return$", views.loan_return, name="loan-return"),
+    url(r"^(?P<pk>[0-9]+)/transfer$", views.loan_transfer, name="loan-transfer"),
+    url(r"^(?P<pk>[0-9]+)$", views.loan_detail, name="loan-detail"),
 ]
 ]

+ 1 - 2
hardware_provisioning/validators.py

@@ -8,5 +8,4 @@ from django.utils import timezone
 
 
 def validate_future_date(value):
 def validate_future_date(value):
     if value <= timezone.now():
     if value <= timezone.now():
-        raise ValidationError(
-            'La date de retour doit être dans le futur')
+        raise ValidationError("La date de retour doit être dans le futur")

+ 42 - 45
hardware_provisioning/views.py

@@ -16,13 +16,11 @@ from .models import Item, Loan
 
 
 @login_required
 @login_required
 def item_list(request):
 def item_list(request):
-    items = Item.objects.all().order_by('storage', 'type', 'designation')
+    items = Item.objects.all().order_by("storage", "type", "designation")
 
 
     # FIXME: suboptimal
     # FIXME: suboptimal
     items = [i for i in items.filter() if i.is_available()]
     items = [i for i in items.filter() if i.is_available()]
-    return render(request, 'hardware_provisioning/item_list.html', {
-        'items': items,
-    })
+    return render(request, "hardware_provisioning/item_list.html", {"items": items})
 
 
 
 
 @login_required
 @login_required
@@ -30,32 +28,33 @@ def item_borrow(request, pk):
     item = get_object_or_404(Item, pk=pk)
     item = get_object_or_404(Item, pk=pk)
 
 
     if not item.is_available():
     if not item.is_available():
-        return HttpResponseForbidden('Item non disponible')
+        return HttpResponseForbidden("Item non disponible")
 
 
-    if request.method == 'POST':
+    if request.method == "POST":
         form = LoanDeclareForm(request.POST)
         form = LoanDeclareForm(request.POST)
         if form.is_valid():
         if form.is_valid():
             loan = Loan.objects.create(
             loan = Loan.objects.create(
                 item=item,
                 item=item,
                 loan_date=timezone.now(),
                 loan_date=timezone.now(),
-                loan_date_end=form.cleaned_data['loan_date_end'],
+                loan_date_end=form.cleaned_data["loan_date_end"],
                 user=request.user,
                 user=request.user,
             )
             )
             messages.success(
             messages.success(
-                request, "Emprunt de {} ({}) enregistré".format(
-                    item.designation, item.type))
+                request,
+                "Emprunt de {} ({}) enregistré".format(item.designation, item.type),
+            )
             if not loan.loan_date_end:
             if not loan.loan_date_end:
                 messages.warning(
                 messages.warning(
                     request,
                     request,
-                    "N'oubliez pas de notifier le retour de l'objet le temps venu")
-            return redirect(reverse('hardware_provisioning:loan-list'))
+                    "N'oubliez pas de notifier le retour de l'objet le temps venu",
+                )
+            return redirect(reverse("hardware_provisioning:loan-list"))
     else:
     else:
         form = LoanDeclareForm()
         form = LoanDeclareForm()
 
 
-    return render(request, 'hardware_provisioning/item_borrow.html', {
-        'item': item,
-        'form': form,
-    })
+    return render(
+        request, "hardware_provisioning/item_borrow.html", {"item": item, "form": form}
+    )
 
 
 
 
 @login_required
 @login_required
@@ -63,24 +62,22 @@ def loan_return(request, pk):
     loan = get_object_or_404(Loan, pk=pk)
     loan = get_object_or_404(Loan, pk=pk)
 
 
     if not loan.user_can_close(request.user):
     if not loan.user_can_close(request.user):
-        return HttpResponseForbidden('Non autorisé')
+        return HttpResponseForbidden("Non autorisé")
 
 
-    if request.method == 'POST':
+    if request.method == "POST":
         form = LoanReturnForm(request.POST)
         form = LoanReturnForm(request.POST)
         if form.is_valid():
         if form.is_valid():
             messages.success(
             messages.success(
-                request,
-                'Le matériel {} a été marqué comme rendu'.format(
-                    loan.item))
-            loan.item.give_back(form.cleaned_data['storage'])
-            return redirect(reverse('hardware_provisioning:loan-list'))
+                request, "Le matériel {} a été marqué comme rendu".format(loan.item)
+            )
+            loan.item.give_back(form.cleaned_data["storage"])
+            return redirect(reverse("hardware_provisioning:loan-list"))
     else:
     else:
         form = LoanReturnForm()
         form = LoanReturnForm()
 
 
-    return render(request, 'hardware_provisioning/return.html', {
-        'loan': loan,
-        'form': form,
-    })
+    return render(
+        request, "hardware_provisioning/return.html", {"loan": loan, "form": form}
+    )
 
 
 
 
 @login_required
 @login_required
@@ -92,45 +89,45 @@ def loan_transfer(request, pk):
     if not old_loan.user_can_close(request.user):
     if not old_loan.user_can_close(request.user):
         return HttpResponseForbidden()
         return HttpResponseForbidden()
 
 
-    if request.method == 'POST':
+    if request.method == "POST":
         form = LoanTransferForm(request.POST)
         form = LoanTransferForm(request.POST)
         if form.is_valid():
         if form.is_valid():
             old_loan.item.give_back()
             old_loan.item.give_back()
             Loan.objects.create(
             Loan.objects.create(
-                user=form.cleaned_data['target_user'],
+                user=form.cleaned_data["target_user"],
                 loan_date=timezone.now(),
                 loan_date=timezone.now(),
-                item=old_loan.item)
+                item=old_loan.item,
+            )
             messages.success(
             messages.success(
                 request,
                 request,
-                "Le matériel {} a été transféré à l'adhérent \"{}\"".format(
-                    old_loan.item,
-                    form.data['target_user']))
-            return redirect(reverse('hardware_provisioning:loan-list'))
+                'Le matériel {} a été transféré à l\'adhérent "{}"'.format(
+                    old_loan.item, form.data["target_user"]
+                ),
+            )
+            return redirect(reverse("hardware_provisioning:loan-list"))
 
 
     else:
     else:
         form = LoanTransferForm()
         form = LoanTransferForm()
 
 
-    return render(request, 'hardware_provisioning/transfer.html', {
-        'form': form,
-        'loan': old_loan,
-    })
+    return render(
+        request, "hardware_provisioning/transfer.html", {"form": form, "loan": old_loan}
+    )
 
 
 
 
 @login_required
 @login_required
 def loan_list(request):
 def loan_list(request):
-    view = 'old' if 'old' in request.GET else ''
+    view = "old" if "old" in request.GET else ""
 
 
-    if view == 'old':
-        loans = request.user.loans.finished().order_by('-loan_date_end')
+    if view == "old":
+        loans = request.user.loans.finished().order_by("-loan_date_end")
     else:
     else:
         loans = request.user.loans.running()
         loans = request.user.loans.running()
 
 
-    return render(request, 'hardware_provisioning/list.html', {
-        'loans': loans,
-        'view': view,
-    })
+    return render(
+        request, "hardware_provisioning/list.html", {"loans": loans, "view": view}
+    )
 
 
 
 
 @login_required
 @login_required
 def loan_detail(request, pk):
 def loan_detail(request, pk):
-    return render(request, 'hardware_provisioning/detail.html', {})
+    return render(request, "hardware_provisioning/detail.html", {})

+ 1 - 0
simple_dsl/admin.py

@@ -21,4 +21,5 @@ class SimpleDSLAdmin(ConfigurationAdminFormMixin, PolymorphicChildModelAdmin):
     # in the admin.
     # in the admin.
     view_on_site = False
     view_on_site = False
 
 
+
 admin.site.register(SimpleDSL, SimpleDSLAdmin)
 admin.site.register(SimpleDSL, SimpleDSLAdmin)

+ 10 - 7
simple_dsl/models.py

@@ -13,19 +13,22 @@ class SimpleDSL(Configuration):
     and IP addresses of subscribers, which may be useful for "white label"
     and IP addresses of subscribers, which may be useful for "white label"
     DSL reselling.
     DSL reselling.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = 'DSL line'
+        verbose_name = "DSL line"
         # If Django's default pluralisation is not satisfactory
         # If Django's default pluralisation is not satisfactory
-        #verbose_name_plural = 'very many DSL lines'
+        # verbose_name_plural = 'very many DSL lines'
 
 
     # URL namespace associated to this configuration type, to build URLs
     # URL namespace associated to this configuration type, to build URLs
     # in various view.  Should also be defined in urls.py.  Here, we don't
     # in various view.  Should also be defined in urls.py.  Here, we don't
     # define any view, so there's no need for an URL namespace.
     # define any view, so there's no need for an URL namespace.
-    #url_namespace = "dsl"
-    phone_number = models.CharField(max_length=20,
-                                    verbose_name='phone number',
-                                    help_text="Phone number associated to the DSL line")
-    
+    # url_namespace = "dsl"
+    phone_number = models.CharField(
+        max_length=20,
+        verbose_name="phone number",
+        help_text="Phone number associated to the DSL line",
+    )
+
     def __unicode__(self):
     def __unicode__(self):
         return self.phone_number
         return self.phone_number
 
 

+ 1 - 1
vpn/__init__.py

@@ -1 +1 @@
-default_app_config = 'vpn.apps.VPNConfig'
+default_app_config = "vpn.apps.VPNConfig"

+ 34 - 13
vpn/admin.py

@@ -13,28 +13,43 @@ from .models import VPNConfiguration
 class VPNConfigurationInline(admin.StackedInline):
 class VPNConfigurationInline(admin.StackedInline):
     model = VPNConfiguration
     model = VPNConfiguration
     # fk_name = 'offersubscription'
     # fk_name = 'offersubscription'
-    exclude = ('password',)
-    readonly_fields = ['configuration_ptr', 'login']
+    exclude = ("password",)
+    readonly_fields = ["configuration_ptr", "login"]
 
 
 
 
 class VPNConfigurationAdmin(ConfigurationAdminFormMixin, PolymorphicChildModelAdmin):
 class VPNConfigurationAdmin(ConfigurationAdminFormMixin, PolymorphicChildModelAdmin):
     base_model = VPNConfiguration
     base_model = VPNConfiguration
-    list_display = ('offersubscription', 'activated', 'login',
-                    'ipv4_endpoint', 'ipv6_endpoint', 'comment')
-    list_filter = ('activated',)
-    search_fields = ('login', 'comment',
-                     # TODO: searching on member directly doesn't work
-                     'offersubscription__member__first_name',
-                     'offersubscription__member__last_name',
-                     'offersubscription__member__email')
-    actions = (delete_selected, "generate_endpoints", "generate_endpoints_v4",
-               "generate_endpoints_v6", "activate", "deactivate")
+    list_display = (
+        "offersubscription",
+        "activated",
+        "login",
+        "ipv4_endpoint",
+        "ipv6_endpoint",
+        "comment",
+    )
+    list_filter = ("activated",)
+    search_fields = (
+        "login",
+        "comment",
+        # TODO: searching on member directly doesn't work
+        "offersubscription__member__first_name",
+        "offersubscription__member__last_name",
+        "offersubscription__member__email",
+    )
+    actions = (
+        delete_selected,
+        "generate_endpoints",
+        "generate_endpoints_v4",
+        "generate_endpoints_v6",
+        "activate",
+        "deactivate",
+    )
     exclude = ("password",)
     exclude = ("password",)
     inline = VPNConfigurationInline
     inline = VPNConfigurationInline
 
 
     def get_readonly_fields(self, request, obj=None):
     def get_readonly_fields(self, request, obj=None):
         if obj:
         if obj:
-            return ['login',]
+            return ["login"]
         else:
         else:
             return []
             return []
 
 
@@ -54,10 +69,12 @@ class VPNConfigurationAdmin(ConfigurationAdminFormMixin, PolymorphicChildModelAd
 
 
     def activate(self, request, queryset):
     def activate(self, request, queryset):
         self.set_activation(request, queryset, True)
         self.set_activation(request, queryset, True)
+
     activate.short_description = "Activer les VPN sélectionnés"
     activate.short_description = "Activer les VPN sélectionnés"
 
 
     def deactivate(self, request, queryset):
     def deactivate(self, request, queryset):
         self.set_activation(request, queryset, False)
         self.set_activation(request, queryset, False)
+
     deactivate.short_description = "Désactiver les VPN sélectionnés"
     deactivate.short_description = "Désactiver les VPN sélectionnés"
 
 
     def generate_endpoints_generic(self, request, queryset, v4=True, v6=True):
     def generate_endpoints_generic(self, request, queryset, v4=True, v6=True):
@@ -72,14 +89,18 @@ class VPNConfigurationAdmin(ConfigurationAdminFormMixin, PolymorphicChildModelAd
 
 
     def generate_endpoints(self, request, queryset):
     def generate_endpoints(self, request, queryset):
         self.generate_endpoints_generic(request, queryset)
         self.generate_endpoints_generic(request, queryset)
+
     generate_endpoints.short_description = "Attribuer des adresses IPv4 et IPv6"
     generate_endpoints.short_description = "Attribuer des adresses IPv4 et IPv6"
 
 
     def generate_endpoints_v4(self, request, queryset):
     def generate_endpoints_v4(self, request, queryset):
         self.generate_endpoints_generic(request, queryset, v6=False)
         self.generate_endpoints_generic(request, queryset, v6=False)
+
     generate_endpoints_v4.short_description = "Attribuer des adresses IPv4"
     generate_endpoints_v4.short_description = "Attribuer des adresses IPv4"
 
 
     def generate_endpoints_v6(self, request, queryset):
     def generate_endpoints_v6(self, request, queryset):
         self.generate_endpoints_generic(request, queryset, v4=False)
         self.generate_endpoints_generic(request, queryset, v4=False)
+
     generate_endpoints_v6.short_description = "Attribuer des adresses IPv6"
     generate_endpoints_v6.short_description = "Attribuer des adresses IPv6"
 
 
+
 admin.site.register(VPNConfiguration, VPNConfigurationAdmin)
 admin.site.register(VPNConfiguration, VPNConfigurationAdmin)

+ 2 - 2
vpn/apps.py

@@ -8,7 +8,7 @@ from . import urls
 
 
 
 
 class VPNConfig(AppConfig, coin.apps.AppURLs):
 class VPNConfig(AppConfig, coin.apps.AppURLs):
-    name = 'vpn'
+    name = "vpn"
     verbose_name = "Gestion d'accès VPN"
     verbose_name = "Gestion d'accès VPN"
 
 
-    exported_urlpatterns = [('vpn', urls.urlpatterns)]
+    exported_urlpatterns = [("vpn", urls.urlpatterns)]

+ 53 - 32
vpn/models.py

@@ -12,6 +12,7 @@ from ldapdb.models.fields import CharField, ListField
 from coin.mixins import CoinLdapSyncMixin
 from coin.mixins import CoinLdapSyncMixin
 from coin.offers.models import OfferSubscription
 from coin.offers.models import OfferSubscription
 from coin.configuration.models import Configuration
 from coin.configuration.models import Configuration
+
 # from coin.offers.backends import ValidateBackendType
 # from coin.offers.backends import ValidateBackendType
 from coin import utils
 from coin import utils
 from coin import validation
 from coin import validation
@@ -24,25 +25,36 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
     #     'offers.OfferSubscription',
     #     'offers.OfferSubscription',
     #     related_name=backend_name,
     #     related_name=backend_name,
     #     validators=[ValidateBackendType(backend_name)])
     #     validators=[ValidateBackendType(backend_name)])
-    activated = models.BooleanField(default=False, verbose_name='activé')
-    login = models.CharField(max_length=50, unique=True, blank=True,
-                             verbose_name="identifiant",
-                             help_text="Laisser vide pour une génération automatique")
-    password = models.CharField(max_length=256, verbose_name="mot de passe",
-                                blank=True, null=True)
-    ipv4_endpoint = InetAddressField(validators=[validation.validate_v4],
-                                     verbose_name="IPv4", blank=True, null=True,
-                                     help_text="Adresse IPv4 utilisée par "
-                                     "défaut sur le VPN")
-    ipv6_endpoint = InetAddressField(validators=[validation.validate_v6],
-                                     verbose_name="IPv6", blank=True, null=True,
-                                     help_text="Adresse IPv6 utilisée par "
-                                     "défaut sur le VPN")
+    activated = models.BooleanField(default=False, verbose_name="activé")
+    login = models.CharField(
+        max_length=50,
+        unique=True,
+        blank=True,
+        verbose_name="identifiant",
+        help_text="Laisser vide pour une génération automatique",
+    )
+    password = models.CharField(
+        max_length=256, verbose_name="mot de passe", blank=True, null=True
+    )
+    ipv4_endpoint = InetAddressField(
+        validators=[validation.validate_v4],
+        verbose_name="IPv4",
+        blank=True,
+        null=True,
+        help_text="Adresse IPv4 utilisée par " "défaut sur le VPN",
+    )
+    ipv6_endpoint = InetAddressField(
+        validators=[validation.validate_v6],
+        verbose_name="IPv6",
+        blank=True,
+        null=True,
+        help_text="Adresse IPv6 utilisée par " "défaut sur le VPN",
+    )
 
 
     objects = NetManager()
     objects = NetManager()
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return reverse('vpn:details', args=[str(self.pk)])
+        return reverse("vpn:details", args=[str(self.pk)])
 
 
     # This method is part of the general configuration interface.
     # This method is part of the general configuration interface.
     def subnet_event(self):
     def subnet_event(self):
@@ -63,7 +75,7 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
             config = LdapVPNConfig.objects.get(pk=self.login)
             config = LdapVPNConfig.objects.get(pk=self.login)
         config.login = config.sn = self.login
         config.login = config.sn = self.login
         config.password = self.password
         config.password = self.password
-        config.active = 'yes' if self.activated else 'no'
+        config.active = "yes" if self.activated else "no"
         config.ipv4_endpoint = utils.str_or_none(self.ipv4_endpoint)
         config.ipv4_endpoint = utils.str_or_none(self.ipv4_endpoint)
         config.ipv6_endpoint = utils.str_or_none(self.ipv6_endpoint)
         config.ipv6_endpoint = utils.str_or_none(self.ipv6_endpoint)
         config.ranges_v4 = [str(s) for s in self.get_subnets(4)]
         config.ranges_v4 = [str(s) for s in self.get_subnets(4)]
@@ -110,7 +122,9 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
         """
         """
         error = "L'IP {} n'est pas dans un réseau attribué."
         error = "L'IP {} n'est pas dans un réseau attribué."
         subnets = self.ip_subnet.all()
         subnets = self.ip_subnet.all()
-        is_faulty = lambda endpoint : endpoint and not any([endpoint in subnet.inet for subnet in subnets])
+        is_faulty = lambda endpoint: endpoint and not any(
+            [endpoint in subnet.inet for subnet in subnets]
+        )
         if is_faulty(self.ipv4_endpoint):
         if is_faulty(self.ipv4_endpoint):
             if delete:
             if delete:
                 self.ipv4_endpoint = None
                 self.ipv4_endpoint = None
@@ -128,7 +142,9 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
         # separator.
         # separator.
         if not self.login:
         if not self.login:
             username = self.offersubscription.member.username
             username = self.offersubscription.member.username
-            vpns = VPNConfiguration.objects.filter(offersubscription__member__username=username)
+            vpns = VPNConfiguration.objects.filter(
+                offersubscription__member__username=username
+            )
             # This is the list of existing VPN logins for this user.
             # This is the list of existing VPN logins for this user.
             logins = [vpn.login for vpn in vpns]
             logins = [vpn.login for vpn in vpns]
             # 100 VPNs ought to be enough for anybody.
             # 100 VPNs ought to be enough for anybody.
@@ -148,26 +164,31 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
         self.check_endpoints()
         self.check_endpoints()
 
 
     def __unicode__(self):
     def __unicode__(self):
-        return 'VPN ' + self.login
+        return "VPN " + self.login
 
 
     class Meta:
     class Meta:
-        verbose_name = 'VPN'
-        verbose_name_plural = 'VPN'
+        verbose_name = "VPN"
+        verbose_name_plural = "VPN"
 
 
 
 
 class LdapVPNConfig(ldapdb.models.Model):
 class LdapVPNConfig(ldapdb.models.Model):
     base_dn = settings.VPN_CONF_BASE_DN
     base_dn = settings.VPN_CONF_BASE_DN
-    object_classes = [b'person', b'organizationalPerson', b'inetOrgPerson',
-                      b'top', b'radiusprofile']
-
-    login = CharField(db_column=b'cn', primary_key=True, max_length=255)
-    sn = CharField(db_column=b'sn', max_length=255)
-    password = CharField(db_column=b'userPassword', max_length=255)
-    active = CharField(db_column=b'dialupAccess', max_length=3)
-    ipv4_endpoint = CharField(db_column=b'radiusFramedIPAddress', max_length=16)
-    ipv6_endpoint = CharField(db_column=b'postalAddress', max_length=40)
-    ranges_v4 = ListField(db_column=b'radiusFramedRoute')
-    ranges_v6 = ListField(db_column=b'registeredAddress')
+    object_classes = [
+        b"person",
+        b"organizationalPerson",
+        b"inetOrgPerson",
+        b"top",
+        b"radiusprofile",
+    ]
+
+    login = CharField(db_column=b"cn", primary_key=True, max_length=255)
+    sn = CharField(db_column=b"sn", max_length=255)
+    password = CharField(db_column=b"userPassword", max_length=255)
+    active = CharField(db_column=b"dialupAccess", max_length=3)
+    ipv4_endpoint = CharField(db_column=b"radiusFramedIPAddress", max_length=16)
+    ipv6_endpoint = CharField(db_column=b"postalAddress", max_length=40)
+    ranges_v4 = ListField(db_column=b"radiusFramedRoute")
+    ranges_v6 = ListField(db_column=b"registeredAddress")
 
 
     def __unicode__(self):
     def __unicode__(self):
         return self.login
         return self.login

+ 8 - 7
vpn/tests.py

@@ -13,12 +13,13 @@ from coin.members.tests import MemberTestsUtils
 
 
 from .models import VPNConfiguration
 from .models import VPNConfiguration
 
 
-USING_POSTGRES = (settings.DATABASES['default']['ENGINE']
-                  ==
-                  'django.db.backends.postgresql_psycopg2')
+USING_POSTGRES = (
+    settings.DATABASES["default"]["ENGINE"] == "django.db.backends.postgresql_psycopg2"
+)
+
 
 
 class VPNTestCase(TestCase):
 class VPNTestCase(TestCase):
-    fixtures = ['example_pools.json', 'offers.json']
+    fixtures = ["example_pools.json", "offers.json"]
 
 
     def setUp(self):
     def setUp(self):
         self.v6_pool = IPPool.objects.get(default_subnetsize=56)
         self.v6_pool = IPPool.objects.get(default_subnetsize=56)
@@ -27,9 +28,9 @@ class VPNTestCase(TestCase):
 
 
         # Create a member.
         # Create a member.
         cn = MemberTestsUtils.get_random_username()
         cn = MemberTestsUtils.get_random_username()
-        self.member = Member.objects.create(first_name=u"Toto",
-                                            last_name=u"L'artichaut",
-                                            username=cn)
+        self.member = Member.objects.create(
+            first_name="Toto", last_name="L'artichaut", username=cn
+        )
 
 
         # Create a new VPN with subnets.
         # Create a new VPN with subnets.
         # We need Django to call clean() so that magic happens.
         # We need Django to call clean() so that magic happens.

+ 11 - 5
vpn/urls.py

@@ -6,11 +6,17 @@ from django.conf.urls import patterns, url
 from .views import VPNView, VPNGeneratePasswordView, get_graph
 from .views import VPNView, VPNGeneratePasswordView, get_graph
 
 
 urlpatterns = patterns(
 urlpatterns = patterns(
-    '',
+    "",
     # This is part of the generic configuration interface (the "name" is
     # This is part of the generic configuration interface (the "name" is
     # the same as the "backend_name" of the model).
     # the same as the "backend_name" of the model).
-    url(r'^(?P<pk>\d+)$', VPNView.as_view(template_name="vpn/vpn.html"), name="details"),
-    url(r'^password/(?P<pk>\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"),
+    url(
+        r"^(?P<pk>\d+)$", VPNView.as_view(template_name="vpn/vpn.html"), name="details"
+    ),
+    url(
+        r"^password/(?P<pk>\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"),
 )
 )

+ 29 - 15
vpn/views.py

@@ -20,7 +20,7 @@ from .models import VPNConfiguration
 
 
 class VPNView(SuccessMessageMixin, UpdateView):
 class VPNView(SuccessMessageMixin, UpdateView):
     model = VPNConfiguration
     model = VPNConfiguration
-    fields = ['ipv4_endpoint', 'ipv6_endpoint', 'comment']
+    fields = ["ipv4_endpoint", "ipv6_endpoint", "comment"]
     success_message = "Configuration enregistrée avec succès !"
     success_message = "Configuration enregistrée avec succès !"
 
 
     @method_decorator(login_required)
     @method_decorator(login_required)
@@ -31,8 +31,11 @@ class VPNView(SuccessMessageMixin, UpdateView):
         if self.request.user.is_superuser:
         if self.request.user.is_superuser:
             return get_object_or_404(VPNConfiguration, pk=self.kwargs.get("pk"))
             return get_object_or_404(VPNConfiguration, pk=self.kwargs.get("pk"))
         # For normal users, ensure the VPN belongs to them.
         # For normal users, ensure the VPN belongs to them.
-        return get_object_or_404(VPNConfiguration, pk=self.kwargs.get("pk"),
-                                 offersubscription__member=self.request.user)
+        return get_object_or_404(
+            VPNConfiguration,
+            pk=self.kwargs.get("pk"),
+            offersubscription__member=self.request.user,
+        )
 
 
 
 
 class VPNGeneratePasswordView(VPNView):
 class VPNGeneratePasswordView(VPNView):
@@ -48,7 +51,7 @@ class VPNGeneratePasswordView(VPNView):
         # This will hash the password automatically
         # This will hash the password automatically
         self.object.full_clean()
         self.object.full_clean()
         self.object.save()
         self.object.save()
-        context['password'] = password
+        context["password"] = password
         return context
         return context
 
 
 
 
@@ -60,20 +63,31 @@ def get_graph(request, vpn_id, period="daily"):
         vpn = get_object_or_404(VPNConfiguration, pk=vpn_id)
         vpn = get_object_or_404(VPNConfiguration, pk=vpn_id)
     else:
     else:
         # For normal users, ensure the VPN belongs to them
         # For normal users, ensure the VPN belongs to them
-        vpn = get_object_or_404(VPNConfiguration, pk=vpn_id,
-                                offersubscription__member=request.user)
+        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', }
+    time_periods = {
+        "hourly": "-1hour",
+        "daily": "-24hours",
+        "weekly": "-8days",
+        "monthly": "-32days",
+        "yearly": "-13months",
+    }
     if period not in time_periods:
     if period not in time_periods:
-        period = 'daily'
+        period = "daily"
 
 
-    graph_url = os.path.join(settings.GRAPHITE_SERVER,
-                "render/?width=586&height=308&from=%(period)s&" \
-                "target=alias%%28scaleToSeconds%%28vpn1.%(login)s.downrxbytes%%2C1%%29%%2C%%20%%22Download%%22%%29&" \
-                "target=alias%%28scaleToSeconds%%28vpn1.%(login)s.uptxbytes%%2C1%%29%%2C%%20%%22Upload%%22%%29&" \
-                "title=VPN%%20Usage%%20%(login)s" % \
-                    { 'period': time_periods[period], 'login': vpn.login })
+    graph_url = os.path.join(
+        settings.GRAPHITE_SERVER,
+        "render/?width=586&height=308&from=%(period)s&"
+        "target=alias%%28scaleToSeconds%%28vpn1.%(login)s.downrxbytes%%2C1%%29%%2C%%20%%22Download%%22%%29&"
+        "target=alias%%28scaleToSeconds%%28vpn1.%(login)s.uptxbytes%%2C1%%29%%2C%%20%%22Upload%%22%%29&"
+        "title=VPN%%20Usage%%20%(login)s"
+        % {"period": time_periods[period], "login": vpn.login},
+    )
     try:
     try:
-        return StreamingHttpResponse(urllib2.urlopen(graph_url), content_type="image/png")
+        return StreamingHttpResponse(
+            urllib2.urlopen(graph_url), content_type="image/png"
+        )
     except urllib2.URLError:
     except urllib2.URLError:
         return HttpResponseServerError()
         return HttpResponseServerError()