diff --git a/CHANGELOG.md b/CHANGELOG.md index 01b9b8ca..1b9020e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## MR 160: Datepicker Install libjs-jquery libjs-jquery-ui libjs-jquery-timepicker libjs-bootstrap javascript-common -``` +```bash apt-get -y install \ libjs-jquery \ libjs-jquery-ui \ @@ -10,12 +10,12 @@ apt-get -y install \ javascript-common ``` Enable javascript-common conf -``` +```bash a2enconf javascript-common ``` Delete old jquery files : -``` +```bash rm -r static_files/js/jquery-ui-* rm static_files/js/jquery-2.2.4.min.js rm static/css/jquery-ui-timepicker-addon.css @@ -42,6 +42,7 @@ Refactored install_re2o.sh script. ``` install_re2o.sh help ``` + * The installation templates (LDIF files and `re2o/settings_locale.example.py`) have been changed to use `example.net` instead of `example.org` (more neutral and generic) @@ -75,7 +76,6 @@ OPTIONAL_APPS = ( ``` - ## MR 177: Add django-debug-toolbar support Add the possibility to enable `django-debug-toolbar` in debug mode. First install the APT package: @@ -94,3 +94,29 @@ If you to restrict the IP which can see the debug, use the `INTERNAL_IPS` option ``` INTERNAL_IPS = ["10.0.0.1", "10.0.0.2"] ``` + +## MR 145: Fix #117 : Use unix_name instead of name for ldap groups + +Fix a mixing between unix_name and name for groups +After this modification you need to: +* Double-check your defined groups' unix-name only contain small letters +* Run the following commands to rebuild your ldap's groups: + ```shell + python3 manage.py ldap_rebuild + ``` + +* You may need to force your nslcd cache to be reloaded on some servers (else you will have to wait for the cache to be refreshed): + ```bash + sudo nslcd -i groups + ``` + +## MR 174 : Fix online payment + allow users to pay their subscription + +Add the possibility to use custom payment methods. There is also a boolean field on the +Payments allowing every user to use some kinds of payment. You have to add the rights `cotisations.use_every_payment` and `cotisations.buy_every_article` +to the staff members so they can use every type of payment to buy anything. + +Don't forget to run migrations, several settings previously in the `preferences` app ar now +in their own Payment models. + +To have a closer look on how the payments works, please go to the wiki. diff --git a/api/acl.py b/api/acl.py index 8c39aed0..4d634beb 100644 --- a/api/acl.py +++ b/api/acl.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. diff --git a/api/authentication.py b/api/authentication.py index 469c51f1..05dc626b 100644 --- a/api/authentication.py +++ b/api/authentication.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. diff --git a/api/pagination.py b/api/pagination.py index 20dcad6e..d34c17ab 100644 --- a/api/pagination.py +++ b/api/pagination.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. diff --git a/api/permissions.py b/api/permissions.py index 53f06620..0b666ebd 100644 --- a/api/permissions.py +++ b/api/permissions.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. diff --git a/api/routers.py b/api/routers.py index fcfb5077..2d245382 100644 --- a/api/routers.py +++ b/api/routers.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. diff --git a/api/serializers.py b/api/serializers.py index 48988365..bff1bd9c 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. @@ -190,6 +191,13 @@ class MxSerializer(NamespacedHMSerializer): fields = ('zone', 'priority', 'name', 'api_url') +class DNameSerializer(NamespacedHMSerializer): + """Serialize `machines.models.DName` objects. + """ + class Meta: + model = machines.DName + fields = ('zone', 'alias', 'api_url') + class NsSerializer(NamespacedHMSerializer): """Serialize `machines.models.Ns` objects. """ diff --git a/api/settings.py b/api/settings.py index f8171638..925d503a 100644 --- a/api/settings.py +++ b/api/settings.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. diff --git a/api/tests.py b/api/tests.py index ef05cec2..0931ab8e 100644 --- a/api/tests.py +++ b/api/tests.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. diff --git a/api/urls.py b/api/urls.py index 942435dd..2947850e 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. @@ -51,6 +52,7 @@ router.register_viewset(r'machines/extension', views.ExtensionViewSet) router.register_viewset(r'machines/mx', views.MxViewSet) router.register_viewset(r'machines/ns', views.NsViewSet) router.register_viewset(r'machines/txt', views.TxtViewSet) +router.register_viewset(r'machines/dname', views.DNameViewSet) router.register_viewset(r'machines/srv', views.SrvViewSet) router.register_viewset(r'machines/interface', views.InterfaceViewSet) router.register_viewset(r'machines/ipv6list', views.Ipv6ListViewSet) diff --git a/api/views.py b/api/views.py index 8fffe606..45e083cc 100644 --- a/api/views.py +++ b/api/views.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. @@ -162,6 +163,12 @@ class TxtViewSet(viewsets.ReadOnlyModelViewSet): queryset = machines.Txt.objects.all() serializer_class = serializers.TxtSerializer +class DNameViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.DName` objects. + """ + queryset = machines.DName.objects.all() + serializer_class = serializers.DNameSerializer + class SrvViewSet(viewsets.ReadOnlyModelViewSet): """Exposes list and details of `machines.models.Srv` objects. diff --git a/cotisations/forms.py b/cotisations/forms.py index a6647a45..7ad9e413 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -5,6 +5,7 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle +# Copyright © 2018 Hugo Levy-Falk # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -41,75 +42,49 @@ from django.forms import ModelForm, Form from django.core.validators import MinValueValidator from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _l +from django.shortcuts import get_object_or_404 -from preferences.models import OptionalUser from re2o.field_permissions import FieldPermissionFormMixin from re2o.mixins import FormRevMixin from .models import Article, Paiement, Facture, Banque +from .payment_methods import balance -class NewFactureForm(FormRevMixin, ModelForm): +class FactureForm(FieldPermissionFormMixin, FormRevMixin, ModelForm): """ - Form used to create a new invoice by using a payment method, a bank and a - cheque number. + Form used to manage and create an invoice and its fields. """ - def __init__(self, *args, **kwargs): + + def __init__(self, *args, creation=False, **kwargs): + user = kwargs['user'] prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs) - # TODO : remove the use of cheque and banque and paiement - # for something more generic or at least in English - self.fields['cheque'].required = False - self.fields['banque'].required = False - self.fields['cheque'].label = _("Cheque number") - self.fields['banque'].empty_label = _("Not specified") + super(FactureForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['paiement'].empty_label = \ _("Select a payment method") - paiement_list = Paiement.objects.filter(type_paiement=1) - if paiement_list: - self.fields['paiement'].widget\ - .attrs['data-cheque'] = paiement_list.first().id + self.fields['paiement'].queryset = Paiement.find_allowed_payments(user) + if not creation: + self.fields['user'].label = _("Member") + self.fields['user'].empty_label = \ + _("Select the proprietary member") + self.fields['valid'].label = _("Validated invoice") + else: + self.fields = {'paiement': self.fields['paiement']} class Meta: model = Facture - fields = ['paiement', 'banque', 'cheque'] + fields = '__all__' def clean(self): - cleaned_data = super(NewFactureForm, self).clean() + cleaned_data = super(FactureForm, self).clean() paiement = cleaned_data.get('paiement') - cheque = cleaned_data.get('cheque') - banque = cleaned_data.get('banque') if not paiement: raise forms.ValidationError( _("A payment method must be specified.") ) - elif paiement.type_paiement == 'check' and not (cheque and banque): - raise forms.ValidationError( - _("A cheque number and a bank must be specified.") - ) return cleaned_data -class CreditSoldeForm(NewFactureForm): - """ - Form used to make some operations on the user's balance if the option is - activated. - """ - class Meta(NewFactureForm.Meta): - model = Facture - fields = ['paiement', 'banque', 'cheque'] - - def __init__(self, *args, **kwargs): - super(CreditSoldeForm, self).__init__(*args, **kwargs) - # TODO : change solde to balance - self.fields['paiement'].queryset = Paiement.objects.exclude( - moyen='solde' - ).exclude(moyen='Solde') - - montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True) - - -class SelectUserArticleForm( - FormRevMixin, Form): +class SelectUserArticleForm(FormRevMixin, Form): """ Form used to select an article during the creation of an invoice for a member. @@ -127,6 +102,11 @@ class SelectUserArticleForm( required=True ) + def __init__(self, *args, **kwargs): + user = kwargs.pop('user') + super(SelectUserArticleForm, self).__init__(*args, **kwargs) + self.fields['article'].queryset = Article.find_allowed_articles(user) + class SelectClubArticleForm(Form): """ @@ -146,6 +126,10 @@ class SelectClubArticleForm(Form): required=True ) + def __init__(self, user, *args, **kwargs): + super(SelectClubArticleForm, self).__init__(*args, **kwargs) + self.fields['article'].queryset = Article.find_allowed_articles(user) + # TODO : change Facture to Invoice class NewFactureFormPdf(Form): @@ -167,26 +151,6 @@ class NewFactureFormPdf(Form): ) -# TODO : change Facture to Invoice -class EditFactureForm(FieldPermissionFormMixin, NewFactureForm): - """ - Form used to edit an invoice and its fields : payment method, bank, - user associated, ... - """ - class Meta(NewFactureForm.Meta): - # TODO : change Facture to Invoice - model = Facture - fields = '__all__' - - def __init__(self, *args, **kwargs): - # TODO : change Facture to Invoice - super(EditFactureForm, self).__init__(*args, **kwargs) - self.fields['user'].label = _("Member") - self.fields['user'].empty_label = \ - _("Select the proprietary member") - self.fields['valid'].label = _("Validated invoice") - - class ArticleForm(FormRevMixin, ModelForm): """ Form used to create an article. @@ -231,17 +195,12 @@ class PaiementForm(FormRevMixin, ModelForm): class Meta: model = Paiement # TODO : change moyen to method and type_paiement to payment_type - fields = ['moyen', 'type_paiement'] + fields = ['moyen', 'available_for_everyone'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['moyen'].label = _("Payment method name") - self.fields['type_paiement'].label = _("Payment type") - self.fields['type_paiement'].help_text = \ - _("The payement type is used for specific behaviour.\ - The \"cheque\" type means a cheque number and a bank name\ - may be added when using this payment method.") # TODO : change paiement to payment @@ -304,56 +263,6 @@ class DelBanqueForm(FormRevMixin, Form): self.fields['banques'].queryset = Banque.objects.all() -# TODO : change facture to Invoice -class NewFactureSoldeForm(NewFactureForm): - """ - Form used to create an invoice - """ - def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(NewFactureSoldeForm, self).__init__( - *args, - prefix=prefix, - **kwargs - ) - self.fields['cheque'].required = False - self.fields['banque'].required = False - self.fields['cheque'].label = _('Cheque number') - self.fields['banque'].empty_label = _("Not specified") - self.fields['paiement'].empty_label = \ - _("Select a payment method") - # TODO : change paiement to payment - paiement_list = Paiement.objects.filter(type_paiement=1) - if paiement_list: - self.fields['paiement'].widget\ - .attrs['data-cheque'] = paiement_list.first().id - - class Meta: - # TODO : change facture to invoice - model = Facture - # TODO : change paiement to payment and baque to bank - fields = ['paiement', 'banque'] - - def clean(self): - cleaned_data = super(NewFactureSoldeForm, self).clean() - # TODO : change paiement to payment - paiement = cleaned_data.get("paiement") - cheque = cleaned_data.get("cheque") - # TODO : change banque to bank - banque = cleaned_data.get("banque") - # TODO : change paiement to payment - if not paiement: - raise forms.ValidationError( - _("A payment method must be specified.") - ) - # TODO : change paiement and banque to payment and bank - elif paiement.type_paiement == "check" and not (cheque and banque): - raise forms.ValidationError( - _("A cheque number and a bank must be specified.") - ) - return cleaned_data - - # TODO : Better name and docstring class RechargeForm(FormRevMixin, Form): """ @@ -364,34 +273,31 @@ class RechargeForm(FormRevMixin, Form): min_value=0.01, validators=[] ) + payment = forms.ModelChoiceField( + queryset=Paiement.objects.none(), + label=_l("Payment method") + ) - def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user') + def __init__(self, *args, user=None, **kwargs): + self.user = user super(RechargeForm, self).__init__(*args, **kwargs) + self.fields['payment'].empty_label = \ + _("Select a payment method") + self.fields['payment'].queryset = Paiement.find_allowed_payments(user) - def clean_value(self): + def clean(self): """ - Returns a cleaned vlaue from the received form by validating + Returns a cleaned value from the received form by validating the value is well inside the possible limits """ value = self.cleaned_data['value'] - if value < OptionalUser.get_cached_value('min_online_payment'): - raise forms.ValidationError( - _("Requested amount is too small. Minimum amount possible : \ - %(min_online_amount)s €.") % { - 'min_online_amount': OptionalUser.get_cached_value( - 'min_online_payment' - ) - } - ) - if value + self.user.solde > \ - OptionalUser.get_cached_value('max_solde'): + balance_method = get_object_or_404(balance.PaymentMethod) + if balance_method.maximum_balance is not None and \ + value + self.user.solde > balance_method.maximum_balance: raise forms.ValidationError( _("Requested amount is too high. Your balance can't exceed \ %(max_online_balance)s €.") % { - 'max_online_balance': OptionalUser.get_cached_value( - 'max_solde' - ) + 'max_online_balance': balance_method.maximum_balance } ) - return value + return self.cleaned_data diff --git a/cotisations/migrations/0030_custom_payment.py b/cotisations/migrations/0030_custom_payment.py new file mode 100644 index 00000000..ec6f08d1 --- /dev/null +++ b/cotisations/migrations/0030_custom_payment.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-02 18:56 +from __future__ import unicode_literals + +import re2o.aes_field +import cotisations.payment_methods.mixins +from django.db import migrations, models +import django.db.models.deletion + + +def add_cheque(apps, schema_editor): + ChequePayment = apps.get_model('cotisations', 'ChequePayment') + Payment = apps.get_model('cotisations', 'Paiement') + for p in Payment.objects.filter(type_paiement=1): + cheque = ChequePayment() + cheque.payment = p + cheque.save() + + +def add_comnpay(apps, schema_editor): + ComnpayPayment = apps.get_model('cotisations', 'ComnpayPayment') + Payment = apps.get_model('cotisations', 'Paiement') + AssoOption = apps.get_model('preferences', 'AssoOption') + options, _created = AssoOption.objects.get_or_create() + try: + payment = Payment.objects.get( + moyen='Rechargement en ligne' + ) + except Payment.DoesNotExist: + return + comnpay = ComnpayPayment() + comnpay.payment_user = options.payment_id + comnpay.payment = payment + comnpay.save() + payment.moyen = "ComnPay" + + payment.save() + + +def add_solde(apps, schema_editor): + OptionalUser = apps.get_model('preferences', 'OptionalUser') + options, _created = OptionalUser.objects.get_or_create() + + Payment = apps.get_model('cotisations', 'Paiement') + BalancePayment = apps.get_model('cotisations', 'BalancePayment') + + try: + solde = Payment.objects.get(moyen="solde") + except Payment.DoesNotExist: + return + balance = BalancePayment() + balance.payment = solde + balance.minimum_balance = options.solde_negatif + balance.maximum_balance = options.max_solde + solde.is_balance = True + balance.save() + solde.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0044_remove_payment_pass'), + ('cotisations', '0029_auto_20180414_2056'), + ] + + operations = [ + migrations.AlterModelOptions( + name='paiement', + options={'permissions': (('view_paiement', "Can see a payement's details"), ('use_every_payment', 'Can use every payement')), 'verbose_name': 'Payment method', 'verbose_name_plural': 'Payment methods'}, + ), + migrations.AlterModelOptions( + name='article', + options={'permissions': (('view_article', "Can see an article's details"), ('buy_every_article', 'Can buy every_article')), 'verbose_name': 'Article', 'verbose_name_plural': 'Articles'}, + ), + migrations.AddField( + model_name='paiement', + name='available_for_everyone', + field=models.BooleanField(default=False, verbose_name='Is available for every user'), + ), + migrations.AddField( + model_name='paiement', + name='is_balance', + field=models.BooleanField(default=False, editable=False, help_text='There should be only one balance payment method.', verbose_name='Is user balance', validators=[cotisations.models.check_no_balance]), + ), + migrations.AddField( + model_name='article', + name='available_for_everyone', + field=models.BooleanField(default=False, verbose_name='Is available for every user'), + ), + migrations.CreateModel( + name='ChequePayment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')), + ], + bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model), + options={'verbose_name': 'Cheque'}, + ), + migrations.CreateModel( + name='ComnpayPayment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('payment_credential', models.CharField(blank=True, default='', max_length=255, verbose_name='ComNpay VAD Number')), + ('payment_pass', re2o.aes_field.AESEncryptedField(blank=True, max_length=255, null=True, verbose_name='ComNpay Secret Key')), + ('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')), + ('minimum_payment', models.DecimalField(decimal_places=2, default=1, help_text='The minimal amount of money you have to use when paying with ComNpay', max_digits=5, verbose_name='Minimum payment')), + ], + bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model), + options={'verbose_name': 'ComNpay'}, + ), + migrations.CreateModel( + name='BalancePayment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('minimum_balance', models.DecimalField(decimal_places=2, default=0, help_text='The minimal amount of money allowed for the balance at the end of a payment. You can specify negative amount.', max_digits=5, verbose_name='Minimum balance')), + ('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')), + ('maximum_balance', models.DecimalField(decimal_places=2, default=50, help_text='The maximal amount of money allowed for the balance.', max_digits=5, verbose_name='Maximum balance', null=True, blank=True)), + ('credit_balance_allowed', models.BooleanField(default=False, verbose_name='Allow user to credit their balance')), + ], + bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model), + options={'verbose_name': 'User Balance'}, + ), + migrations.RunPython(add_comnpay), + migrations.RunPython(add_cheque), + migrations.RunPython(add_solde), + migrations.RemoveField( + model_name='paiement', + name='type_paiement', + ), + + ] diff --git a/cotisations/models.py b/cotisations/models.py index c4c6d4af..52d71b58 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -6,6 +6,7 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle +# Copyright © 2018 Hugo Levy-Falk # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -42,11 +43,17 @@ from django.core.validators import MinValueValidator from django.utils import timezone from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _l +from django.urls import reverse +from django.shortcuts import redirect +from django.contrib import messages from machines.models import regen from re2o.field_permissions import FieldPermissionModelMixin from re2o.mixins import AclMixin, RevMixin +from cotisations.utils import find_payment_method +from cotisations.validators import check_no_balance + # TODO : change facture to invoice class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @@ -131,7 +138,7 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): """ price = Vente.objects.filter( facture=self - ).aggregate(models.Sum('prix'))['prix__sum'] + ).aggregate(models.Sum('prix'))['prix__sum'] return price # TODO : change prix to price @@ -143,12 +150,12 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): # TODO : change Vente to somethingelse return Vente.objects.filter( facture=self - ).aggregate( - total=models.Sum( - models.F('prix')*models.F('number'), - output_field=models.FloatField() - ) - )['total'] + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] or 0 def name(self): """ @@ -157,7 +164,7 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): """ name = ' - '.join(Vente.objects.filter( facture=self - ).values_list('name', flat=True)) + ).values_list('name', flat=True)) return name def can_edit(self, user_request, *args, **kwargs): @@ -213,6 +220,22 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): _("You don't have the right to edit an invoice.") ) + @staticmethod + def can_create(user_request, *_args, **_kwargs): + """Check if a user can create an invoice. + + :param user_request: The user who wants to create an invoice. + :return: a message and a boolean which is True if the user can create + an invoice or if the `options.allow_self_subscription` is set. + """ + if user_request.has_perm('cotisations.add_facture'): + return True, None + if len(Paiement.find_allowed_payments(user_request)) <= 0: + return False, _("There are no payment types which you can use.") + if len(Article.find_allowed_articles(user_request)) <= 0: + return False, _("There are no article that you can buy.") + return True, None + def __init__(self, *args, **kwargs): super(Facture, self).__init__(*args, **kwargs) self.field_permissions = { @@ -341,12 +364,12 @@ class Vente(RevMixin, AclMixin, models.Model): facture__in=Facture.objects.filter( user=self.facture.user ).exclude(valid=False)) - ).filter( - Q(type_cotisation='All') | - Q(type_cotisation=self.type_cotisation) - ).filter( - date_start__lt=date_start - ).aggregate(Max('date_end'))['date_end__max'] + ).filter( + Q(type_cotisation='All') | + Q(type_cotisation=self.type_cotisation) + ).filter( + date_start__lt=date_start + ).aggregate(Max('date_end'))['date_end__max'] elif self.type_cotisation == "Adhesion": end_cotisation = self.facture.user.end_adhesion() else: @@ -357,7 +380,7 @@ class Vente(RevMixin, AclMixin, models.Model): cotisation.date_start = date_max cotisation.date_end = cotisation.date_start + relativedelta( months=self.duration*self.number - ) + ) return def save(self, *args, **kwargs): @@ -380,7 +403,7 @@ class Vente(RevMixin, AclMixin, models.Model): elif (not user_request.has_perm('cotisations.change_all_facture') and not self.facture.user.can_edit( user_request, *args, **kwargs - )[0]): + )[0]): return False, _("You don't have the right to edit this user's " "purchases.") elif (not user_request.has_perm('cotisations.change_all_vente') and @@ -501,12 +524,17 @@ class Article(RevMixin, AclMixin, models.Model): max_length=255, verbose_name=_l("Type of cotisation") ) + available_for_everyone = models.BooleanField( + default=False, + verbose_name=_l("Is available for every user") + ) unique_together = ('name', 'type_user') class Meta: permissions = ( ('view_article', _l("Can see an article's details")), + ('buy_every_article', _l("Can buy every_article")) ) verbose_name = "Article" verbose_name_plural = "Articles" @@ -524,6 +552,35 @@ class Article(RevMixin, AclMixin, models.Model): def __str__(self): return self.name + def can_buy_article(self, user, *_args, **_kwargs): + """Check if a user can buy this article. + + Args: + self: The article + user: The user requesting buying + + Returns: + A boolean stating if usage is granted and an explanation + message if the boolean is `False`. + """ + return ( + self.available_for_everyone + or user.has_perm('cotisations.buy_every_article') + or user.has_perm('cotisations.add_facture'), + _("You cannot buy this Article.") + ) + + @classmethod + def find_allowed_articles(cls, user): + """Finds every allowed articles for an user. + + Args: + user: The user requesting articles. + """ + if user.has_perm('cotisations.buy_every_article'): + return cls.objects.all() + return cls.objects.filter(available_for_everyone=True) + class Banque(RevMixin, AclMixin, models.Model): """ @@ -557,29 +614,29 @@ class Paiement(RevMixin, AclMixin, models.Model): invoice. It's easier to know this information when doing the accouts. It is represented by: * a name - * a type (used for the type 'cheque' which implies the use of a bank - and an account number in related models) """ - PAYMENT_TYPES = ( - (0, _l("Standard")), - (1, _l("Cheque")), - ) - # TODO : change moyen to method moyen = models.CharField( max_length=255, verbose_name=_l("Method") ) - type_paiement = models.IntegerField( - choices=PAYMENT_TYPES, - default=0, - verbose_name=_l("Payment type") + available_for_everyone = models.BooleanField( + default=False, + verbose_name=_l("Is available for every user") + ) + is_balance = models.BooleanField( + default=False, + editable=False, + verbose_name=_l("Is user balance"), + help_text=_l("There should be only one balance payment method."), + validators=[check_no_balance] ) class Meta: permissions = ( ('view_paiement', _l("Can see a payement's details")), + ('use_every_payment', _l("Can use every payement")), ) verbose_name = _l("Payment method") verbose_name_plural = _l("Payment methods") @@ -593,16 +650,79 @@ class Paiement(RevMixin, AclMixin, models.Model): """ self.moyen = self.moyen.title() - def save(self, *args, **kwargs): + def end_payment(self, invoice, request, use_payment_method=True): """ - Override of the herited save function to be sure only one payment - method of type 'cheque' exists. + The general way of ending a payment. + + Args: + invoice: The invoice being created. + request: Request sent by the user. + use_payment_method: If this flag is set to True and`self` has + an attribute `payment_method`, returns the result of + `self.payment_method.end_payment(invoice, request)` + + Returns: + An `HttpResponse`-like object. """ - if Paiement.objects.filter(type_paiement=1).count() > 1: - raise ValidationError( - _("You cannot have multiple payment method of type cheque") + payment_method = find_payment_method(self) + if payment_method is not None and use_payment_method: + return payment_method.end_payment(invoice, request) + + # In case a cotisation was bought, inform the user, the + # cotisation time has been extended too + if any(sell.type_cotisation for sell in invoice.vente_set.all()): + messages.success( + request, + _("The cotisation of %(member_name)s has been \ + extended to %(end_date)s.") % { + 'member_name': invoice.user.pseudo, + 'end_date': invoice.user.end_adhesion() + } ) - super(Paiement, self).save(*args, **kwargs) + # Else, only tell the invoice was created + else: + messages.success( + request, + _("The invoice has been created.") + ) + return redirect(reverse( + 'users:profil', + kwargs={'userid': invoice.user.pk} + )) + + def can_use_payment(self, user, *_args, **_kwargs): + """Check if a user can use this payment. + + Args: + self: The payment + user: The user requesting usage + Returns: + A boolean stating if usage is granted and an explanation + message if the boolean is `False`. + """ + return ( + self.available_for_everyone + or user.has_perm('cotisations.use_every_payment') + or user.has_perm('cotisations.add_facture'), + _("You cannot use this Payment.") + ) + + @classmethod + def find_allowed_payments(cls, user): + """Finds every allowed payments for an user. + + Args: + user: The user requesting payment methods. + """ + if user.has_perm('cotisations.use_every_payment'): + return cls.objects.all() + return cls.objects.filter(available_for_everyone=True) + + def get_payment_method_name(self): + p = find_payment_method(self) + if p is not None: + return p._meta.verbose_name + return _("No custom payment method") class Cotisation(RevMixin, AclMixin, models.Model): diff --git a/cotisations/payment_methods/__init__.py b/cotisations/payment_methods/__init__.py new file mode 100644 index 00000000..b78a86fd --- /dev/null +++ b/cotisations/payment_methods/__init__.py @@ -0,0 +1,136 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +# Custom Payment methods + +When creating an invoice with a classic payment method, the creation view calls +the `end_payment` method of the `Payment` object of the invoice. This method +checks for a payment method associated to the `Payment` and if nothing is +found, adds a message for payment confirmation and redirects the user towards +their profil page. This is fine for most of the payment method, but you might +want to define custom payment methods. As an example for negociating with an +other server for online payment or updating some fields in your models. + +# Defining a custom payment method +To define a custom payment method, you can add a Python module to +`cotisations/payment_methods/`. This module should be organized like +a Django application. +As an example, if you want to add the payment method `foo`. + +## Basic + +The first thing to do is to create a `foo` Python module with a `models.py`. + +``` +payment_methods +├── foo +│ ├── __init__.py +│ └── models.py +├── forms.py +├── __init__.py +├── mixins.py +└── urls.py +``` + +Then, in `models.py` you could add a model like this : +```python +from django.db import models + +from cotisations.models import Paiement +from cotisations.payment_methods.mixins import PaymentMethodMixin + + +# The `PaymentMethodMixin` defines the default `end_payment` +class FooPayment(PaymentMethodMixin, models.Model): + + # This field is required, it is used by `Paiement` in order to + # determine if a payment method is associated to it. + payment = models.OneToOneField( + Paiement, + on_delete=models.CASCADE, + related_name='payment_method', + editable=False + ) +``` + +And in `__init__.py` : +```python +from . import models +NAME = "FOO" # Name displayed when you crate a payment type +PaymentMethod = models.FooPayment # You must define this alias +``` + +Then you just have to register your payment method in +`payment_methods/__init__.py` in the `PAYMENT_METHODS` list : + +``` +from . import ... # Some existing imports +from . import foo + +PAYMENT_METHODS = [ + # Some already registered payment methods... + foo +] +``` + +And... that's it, you can use your new payment method after running +`makemigrations` and `migrate`. + +But this payment method is not really usefull, since it does noting ! + +## A payment method which does something + +You have to redefine the `end_payment` method. Here is its prototype : + +```python +def end_payment(self, invoice, request): + pass +``` + +With `invoice` the invoice being created and `request` the request which +created it. This method has to return an HttpResponse-like object. + +## Additional views + +You can add specific urls for your payment method like in any django app. To +register these urls, modify `payment_methods/urls.py`. + +## Alter the `Paiement` object after creation + +You can do that by adding a `alter_payment(self, payment)` +method to your model. + +## Validate the creation field + +You may want to perform some additionals verifications on the form +creating the payment. You can do that by adding a `valid_form(self, form)` +method to your model, where `form` is an instance of +`cotisations.payment_methods.forms.PaymentMethodForm`. +""" + + +from . import comnpay, cheque, balance, urls + +PAYMENT_METHODS = [ + comnpay, + cheque, + balance, +] diff --git a/cotisations/payment_methods/balance/__init__.py b/cotisations/payment_methods/balance/__init__.py new file mode 100644 index 00000000..cacd73f7 --- /dev/null +++ b/cotisations/payment_methods/balance/__init__.py @@ -0,0 +1,27 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +This module contains a method to pay online using user balance. +""" +from . import models +NAME = "BALANCE" + +PaymentMethod = models.BalancePayment diff --git a/cotisations/payment_methods/balance/models.py b/cotisations/payment_methods/balance/models.py new file mode 100644 index 00000000..4e488405 --- /dev/null +++ b/cotisations/payment_methods/balance/models.py @@ -0,0 +1,120 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from django.db import models +from django.shortcuts import redirect +from django.urls import reverse +from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _l +from django.contrib import messages + + +from cotisations.models import Paiement +from cotisations.payment_methods.mixins import PaymentMethodMixin + + +class BalancePayment(PaymentMethodMixin, models.Model): + """ + The model allowing you to pay with a cheque. + """ + + class Meta: + verbose_name = _l("User Balance") + + payment = models.OneToOneField( + Paiement, + on_delete=models.CASCADE, + related_name='payment_method', + editable=False + ) + minimum_balance = models.DecimalField( + verbose_name=_l("Minimum balance"), + help_text=_l("The minimal amount of money allowed for the balance" + " at the end of a payment. You can specify negative " + "amount." + ), + max_digits=5, + decimal_places=2, + default=0, + ) + maximum_balance = models.DecimalField( + verbose_name=_l("Maximum balance"), + help_text=_l("The maximal amount of money allowed for the balance."), + max_digits=5, + decimal_places=2, + default=50, + blank=True, + null=True, + ) + credit_balance_allowed = models.BooleanField( + verbose_name=_l("Allow user to credit their balance"), + default=False, + ) + + def end_payment(self, invoice, request): + """Changes the user's balance to pay the invoice. If it is not + possible, shows an error and invalidates the invoice. + """ + user = invoice.user + total_price = invoice.prix_total() + if float(user.solde) - float(total_price) < self.minimum_balance: + invoice.valid = False + invoice.save() + messages.error( + request, + _("Your balance is too low for this operation.") + ) + return redirect(reverse( + 'users:profil', + kwargs={'userid': user.id} + )) + return invoice.paiement.end_payment( + invoice, + request, + use_payment_method=False + ) + + def valid_form(self, form): + """Checks that there is not already a balance payment method.""" + p = Paiement.objects.filter(is_balance=True) + if len(p) > 0: + form.add_error( + 'payment_method', + _("There is already a payment type for user balance") + ) + + def alter_payment(self, payment): + """Register the payment as a balance payment.""" + self.payment.is_balance = True + + def check_price(self, price, user, *args, **kwargs): + """Checks that the price meets the requirement to be paid with user + balance. + """ + return ( + float(user.solde) - float(price) >= self.minimum_balance, + _("Your balance is too low for this operation.") + ) + + def can_credit_balance(self, user_request): + return ( + len(Paiement.find_allowed_payments(user_request) + .exclude(is_balance=True)) > 0 + ) and self.credit_balance_allowed diff --git a/cotisations/payment_methods/cheque/__init__.py b/cotisations/payment_methods/cheque/__init__.py new file mode 100644 index 00000000..9eb17b09 --- /dev/null +++ b/cotisations/payment_methods/cheque/__init__.py @@ -0,0 +1,27 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +This module contains a method to pay online using cheque. +""" +from . import models, urls, views +NAME = "CHEQUE" + +PaymentMethod = models.ChequePayment diff --git a/cotisations/payment_methods/cheque/forms.py b/cotisations/payment_methods/cheque/forms.py new file mode 100644 index 00000000..37942687 --- /dev/null +++ b/cotisations/payment_methods/cheque/forms.py @@ -0,0 +1,31 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from django import forms + +from re2o.mixins import FormRevMixin +from cotisations.models import Facture as Invoice + + +class InvoiceForm(FormRevMixin, forms.ModelForm): + """A simple form to get the bank a the cheque number.""" + class Meta: + model = Invoice + fields = ['banque', 'cheque'] diff --git a/cotisations/payment_methods/cheque/models.py b/cotisations/payment_methods/cheque/models.py new file mode 100644 index 00000000..c2680e7a --- /dev/null +++ b/cotisations/payment_methods/cheque/models.py @@ -0,0 +1,54 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from django.db import models +from django.shortcuts import redirect +from django.urls import reverse +from django.utils.translation import ugettext_lazy as _l + +from cotisations.models import Paiement +from cotisations.payment_methods.mixins import PaymentMethodMixin + + +class ChequePayment(PaymentMethodMixin, models.Model): + """ + The model allowing you to pay with a cheque. + """ + + class Meta: + verbose_name = _l("Cheque") + + payment = models.OneToOneField( + Paiement, + on_delete=models.CASCADE, + related_name='payment_method', + editable=False + ) + + def end_payment(self, invoice, request): + """Invalidates the invoice then redirect the user towards a view asking + for informations to add to the invoice before validating it. + """ + invoice.valid = False + invoice.save() + return redirect(reverse( + 'cotisations:cheque:validate', + kwargs={'invoice_pk': invoice.pk} + )) diff --git a/cotisations/payment_methods/cheque/urls.py b/cotisations/payment_methods/cheque/urls.py new file mode 100644 index 00000000..54fe6a50 --- /dev/null +++ b/cotisations/payment_methods/cheque/urls.py @@ -0,0 +1,30 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from django.conf.urls import url +from . import views + +urlpatterns = [ + url( + r'^validate/(?P[0-9]+)$', + views.cheque, + name='validate' + ) +] diff --git a/cotisations/payment_methods/cheque/views.py b/cotisations/payment_methods/cheque/views.py new file mode 100644 index 00000000..4d164a79 --- /dev/null +++ b/cotisations/payment_methods/cheque/views.py @@ -0,0 +1,69 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Payment + +Here are defined some views dedicated to cheque payement. +""" + +from django.urls import reverse +from django.shortcuts import redirect, render, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.utils.translation import ugettext as _ + +from cotisations.models import Facture as Invoice +from cotisations.utils import find_payment_method + +from .models import ChequePayment +from .forms import InvoiceForm + + +@login_required +def cheque(request, invoice_pk): + """This view validate an invoice with the data from a cheque.""" + invoice = get_object_or_404(Invoice, pk=invoice_pk) + payment_method = find_payment_method(invoice.paiement) + if invoice.valid or not isinstance(payment_method, ChequePayment): + messages.error( + request, + _("You cannot pay this invoice with a cheque.") + ) + return redirect(reverse( + 'users:profil', + kwargs={'userid': request.user.pk} + )) + form = InvoiceForm(request.POST or None, instance=invoice) + if form.is_valid(): + form.instance.valid = True + form.save() + return form.instance.paiement.end_payment( + form.instance, + request, + use_payment_method=False + ) + return render( + request, + 'cotisations/payment.html', + { + 'form': form, + 'amount': invoice.prix_total() + } + ) diff --git a/cotisations/payment_methods/comnpay/__init__.py b/cotisations/payment_methods/comnpay/__init__.py new file mode 100644 index 00000000..7c80311d --- /dev/null +++ b/cotisations/payment_methods/comnpay/__init__.py @@ -0,0 +1,26 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +This module contains a method to pay online using comnpay. +""" +from . import models, urls, views +NAME = "COMNPAY" +PaymentMethod = models.ComnpayPayment diff --git a/cotisations/payment_utils/comnpay.py b/cotisations/payment_methods/comnpay/comnpay.py similarity index 99% rename from cotisations/payment_utils/comnpay.py rename to cotisations/payment_methods/comnpay/comnpay.py index 6c73463f..272ab928 100644 --- a/cotisations/payment_utils/comnpay.py +++ b/cotisations/payment_methods/comnpay/comnpay.py @@ -10,7 +10,7 @@ import hashlib from collections import OrderedDict -class Payment(): +class Transaction(): """ The class representing a transaction with all the functions used during the negociation """ diff --git a/cotisations/payment_methods/comnpay/models.py b/cotisations/payment_methods/comnpay/models.py new file mode 100644 index 00000000..ff6fed0d --- /dev/null +++ b/cotisations/payment_methods/comnpay/models.py @@ -0,0 +1,108 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from django.db import models +from django.shortcuts import render +from django.urls import reverse +from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _l + +from cotisations.models import Paiement +from cotisations.payment_methods.mixins import PaymentMethodMixin + +from re2o.aes_field import AESEncryptedField +from .comnpay import Transaction + + +class ComnpayPayment(PaymentMethodMixin, models.Model): + """ + The model allowing you to pay with COMNPAY. + """ + + class Meta: + verbose_name = "ComNpay" + + payment = models.OneToOneField( + Paiement, + on_delete=models.CASCADE, + related_name='payment_method', + editable=False + ) + payment_credential = models.CharField( + max_length=255, + default='', + blank=True, + verbose_name=_l("ComNpay VAD Number"), + ) + payment_pass = AESEncryptedField( + max_length=255, + null=True, + blank=True, + verbose_name=_l("ComNpay Secret Key"), + ) + minimum_payment = models.DecimalField( + verbose_name=_l("Minimum payment"), + help_text=_l("The minimal amount of money you have to use when paying" + " with ComNpay"), + max_digits=5, + decimal_places=2, + default=1, + ) + + def end_payment(self, invoice, request): + """ + Build a request to start the negociation with Comnpay by using + a facture id, the price and the secret transaction data stored in + the preferences. + """ + invoice.valid = False + invoice.save() + host = request.get_host() + p = Transaction( + str(self.payment_credential), + str(self.payment_pass), + 'https://' + host + reverse( + 'cotisations:comnpay:accept_payment', + kwargs={'factureid': invoice.id} + ), + 'https://' + host + reverse('cotisations:comnpay:refuse_payment'), + 'https://' + host + reverse('cotisations:comnpay:ipn'), + "", + "D" + ) + r = { + 'action': 'https://secure.homologation.comnpay.com', + 'method': 'POST', + 'content': p.buildSecretHTML( + _("Pay invoice no : ")+str(invoice.id), + invoice.prix_total(), + idTransaction=str(invoice.id) + ), + 'amount': invoice.prix_total(), + } + return render(request, 'cotisations/payment.html', r) + + def check_price(self, price, *args, **kwargs): + """Checks that the price meets the requirement to be paid with ComNpay. + """ + return ((price >= self.minimum_payment), + _('In order to pay your invoice with ComNpay' + ', the price must be grater than {} €') + .format(self.minimum_payment)) diff --git a/cotisations/payment_methods/comnpay/urls.py b/cotisations/payment_methods/comnpay/urls.py new file mode 100644 index 00000000..241d53dd --- /dev/null +++ b/cotisations/payment_methods/comnpay/urls.py @@ -0,0 +1,40 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from django.conf.urls import url +from . import views + +urlpatterns = [ + url( + r'^accept/(?P[0-9]+)$', + views.accept_payment, + name='accept_payment' + ), + url( + r'^refuse/$', + views.refuse_payment, + name='refuse_payment' + ), + url( + r'^ipn/$', + views.ipn, + name='ipn' + ), +] diff --git a/cotisations/payment.py b/cotisations/payment_methods/comnpay/views.py similarity index 50% rename from cotisations/payment.py rename to cotisations/payment_methods/comnpay/views.py index b03e1c55..89966b48 100644 --- a/cotisations/payment.py +++ b/cotisations/payment_methods/comnpay/views.py @@ -1,6 +1,26 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Payment -Here are defined some views dedicated to online payement. +Here are the views needed by comnpay """ from collections import OrderedDict @@ -14,24 +34,38 @@ from django.utils.datastructures import MultiValueDictKeyError from django.utils.translation import ugettext as _ from django.http import HttpResponse, HttpResponseBadRequest -from preferences.models import AssoOption -from .models import Facture -from .payment_utils.comnpay import Payment as ComnpayPayment +from cotisations.models import Facture +from .comnpay import Transaction +from .models import ComnpayPayment @csrf_exempt @login_required def accept_payment(request, factureid): """ - The view called when an online payment has been accepted. + The view where the user is redirected when a comnpay payment has been + accepted. """ - facture = get_object_or_404(Facture, id=factureid) - messages.success( - request, - _("The payment of %(amount)s € has been accepted.") % { - 'amount': facture.prix() - } - ) + invoice = get_object_or_404(Facture, id=factureid) + if invoice.valid: + messages.success( + request, + _("The payment of %(amount)s € has been accepted.") % { + 'amount': invoice.prix_total() + } + ) + # In case a cotisation was bought, inform the user, the + # cotisation time has been extended too + if any(purchase.type_cotisation + for purchase in invoice.vente_set.all()): + messages.success( + request, + _("The cotisation of %(member_name)s has been \ + extended to %(end_date)s.") % { + 'member_name': request.user.pseudo, + 'end_date': request.user.end_adhesion() + } + ) return redirect(reverse( 'users:profil', kwargs={'userid': request.user.id} @@ -42,7 +76,8 @@ def accept_payment(request, factureid): @login_required def refuse_payment(request): """ - The view called when an online payment has been refused. + The view where the user is redirected when a comnpay payment has been + refused. """ messages.error( request, @@ -59,37 +94,38 @@ def ipn(request): """ The view called by Comnpay server to validate the transaction. Verify that we can firmly save the user's action and notify - Comnpay with 400 response if not or with a 200 response if yes + Comnpay with 400 response if not or with a 200 response if yes. """ - p = ComnpayPayment() + p = Transaction() order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', ) try: data = OrderedDict([(f, request.POST[f]) for f in order]) except MultiValueDictKeyError: return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") - if not p.validSec(data, AssoOption.get_cached_value('payment_pass')): - return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") - - result = True if (request.POST['result'] == 'OK') else False - idTpe = request.POST['idTpe'] idTransaction = request.POST['idTransaction'] - - # Checking that the payment is actually for us. - if not idTpe == AssoOption.get_cached_value('payment_id'): - return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") - try: factureid = int(idTransaction) except ValueError: return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") facture = get_object_or_404(Facture, id=factureid) + payment_method = get_object_or_404( + ComnpayPayment, payment=facture.paiement) - # Checking that the payment is valid + if not p.validSec(data, payment_method.payment_pass): + return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") + + result = True if (request.POST['result'] == 'OK') else False + idTpe = request.POST['idTpe'] + + # Checking that the payment is actually for us. + if not idTpe == payment_method.payment_credential: + return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") + + # Checking that the payment is valid if not result: # Payment failed: Cancelling the invoice operation - facture.delete() # And send the response to Comnpay indicating we have well # received the failure information. return HttpResponse("HTTP/1.1 200 OK") @@ -100,42 +136,3 @@ def ipn(request): # Everything worked we send a reponse to Comnpay indicating that # it's ok for them to proceed return HttpResponse("HTTP/1.0 200 OK") - - -def comnpay(facture, request): - """ - Build a request to start the negociation with Comnpay by using - a facture id, the price and the secret transaction data stored in - the preferences. - """ - host = request.get_host() - p = ComnpayPayment( - str(AssoOption.get_cached_value('payment_id')), - str(AssoOption.get_cached_value('payment_pass')), - 'https://' + host + reverse( - 'cotisations:accept_payment', - kwargs={'factureid': facture.id} - ), - 'https://' + host + reverse('cotisations:refuse_payment'), - 'https://' + host + reverse('cotisations:ipn'), - "", - "D" - ) - r = { - 'action': 'https://secure.homologation.comnpay.com', - 'method': 'POST', - 'content': p.buildSecretHTML( - "Rechargement du solde", - facture.prix(), - idTransaction=str(facture.id) - ), - 'amount': facture.prix, - } - return r - - -# The payment systems supported by re2o -PAYMENT_SYSTEM = { - 'COMNPAY': comnpay, - 'NONE': None -} diff --git a/cotisations/payment_methods/forms.py b/cotisations/payment_methods/forms.py new file mode 100644 index 00000000..d4d55a74 --- /dev/null +++ b/cotisations/payment_methods/forms.py @@ -0,0 +1,115 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from django import forms +from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _l + +from . import PAYMENT_METHODS +from cotisations.utils import find_payment_method + +def payment_method_factory(payment, *args, creation=True, **kwargs): + """This function finds the right payment method form for a given payment. + + If the payment has a payment method, returns a ModelForm of it. Else if + it is the creation of the payment, a `PaymentMethodForm`. + Else `None`. + + Args: + payment: The payment + *args: arguments passed to the form + creation: Should be True if you are creating the payment + **kwargs: passed to the form + + Returns: + A form or None + """ + payment_method = kwargs.pop('instance', find_payment_method(payment)) + if payment_method is not None: + return forms.modelform_factory(type(payment_method), fields='__all__')( + *args, + instance=payment_method, + **kwargs + ) + elif creation: + return PaymentMethodForm(*args, **kwargs) + + +class PaymentMethodForm(forms.Form): + """A special form which allows you to add a payment method to a `Payment` + object. + """ + + payment_method = forms.ChoiceField( + label=_l("Special payment method"), + help_text=_l("Warning : You will not be able to change the payment " + "method later. But you will be allowed to edit the other " + "options." + ), + required=False + ) + + def __init__(self, *args, **kwargs): + super(PaymentMethodForm, self).__init__(*args, **kwargs) + prefix = kwargs.get('prefix', None) + self.fields['payment_method'].choices = [(i,p.NAME) for (i,p) in enumerate(PAYMENT_METHODS)] + self.fields['payment_method'].choices.insert(0, ('', _l('no'))) + self.fields['payment_method'].widget.attrs = { + 'id': 'paymentMethodSelect' + } + self.templates = [ + forms.modelform_factory(p.PaymentMethod, fields='__all__')(prefix=prefix) + for p in PAYMENT_METHODS + ] + + def clean(self): + """A classic `clean` method, except that it replaces + `self.payment_method` by the payment method object if one has been + found. Tries to call `payment_method.valid_form` if it exists. + """ + super(PaymentMethodForm, self).clean() + choice = self.cleaned_data['payment_method'] + if choice=='': + return + choice = int(choice) + model = PAYMENT_METHODS[choice].PaymentMethod + form = forms.modelform_factory(model, fields='__all__')(self.data, prefix=self.prefix) + self.payment_method = form.save(commit=False) + if hasattr(self.payment_method, 'valid_form'): + self.payment_method.valid_form(self) + return self.cleaned_data + + + + def save(self, payment, *args, **kwargs): + """Saves the payment method. + + Tries to call `payment_method.alter_payment` if it exists. + """ + commit = kwargs.pop('commit', True) + if not hasattr(self, 'payment_method'): + return None + self.payment_method.payment = payment + if hasattr(self.payment_method, 'alter_payment'): + self.payment_method.alter_payment(payment) + if commit: + payment.save() + self.payment_method.save() + return self.payment_method diff --git a/cotisations/payment_methods/mixins.py b/cotisations/payment_methods/mixins.py new file mode 100644 index 00000000..12503e05 --- /dev/null +++ b/cotisations/payment_methods/mixins.py @@ -0,0 +1,33 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +class PaymentMethodMixin: + """A simple mixin to avoid redefining end_payment if you don't need to""" + + def end_payment(self, invoice, request): + """Redefine this method in order to get a different ending to the + payment session if you whish. + + Must return a HttpResponse-like object. + """ + return self.payment.end_payment( + invoice, request, use_payment_method=False) diff --git a/cotisations/payment_methods/urls.py b/cotisations/payment_methods/urls.py new file mode 100644 index 00000000..20e50255 --- /dev/null +++ b/cotisations/payment_methods/urls.py @@ -0,0 +1,27 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from django.conf.urls import include, url +from . import comnpay, cheque + +urlpatterns = [ + url(r'^comnpay/', include(comnpay.urls, namespace='comnpay')), + url(r'^cheque/', include(cheque.urls, namespace='cheque')), +] diff --git a/cotisations/payment_utils/__init__.py b/cotisations/payment_utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/cotisations/templates/cotisations/aff_article.html b/cotisations/templates/cotisations/aff_article.html index eb4ec0a7..2afe726d 100644 --- a/cotisations/templates/cotisations/aff_article.html +++ b/cotisations/templates/cotisations/aff_article.html @@ -24,36 +24,38 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} {% load i18n %} +{% load logs_extra %} +{% load design %} - - - - - - - - - - - - {% for article in article_list %} +
{% trans "Article" %}{% trans "Price" %}{% trans "Cotisation type" %}{% trans "Duration (month)" %}{% trans "Concerned users" %}
+ - - - - - - + + + + + + + - {% endfor %} -
{{ article.name }}{{ article.prix }}{{ article.type_cotisation }}{{ article.duration }}{{ article.type_user }} - {% can_edit article %} - - - - {% acl_end %} - - - - {% trans "Article" %}{% trans "Price" %}{% trans "Cotisation type" %}{% trans "Duration (month)" %}{% trans "Concerned users" %}{% trans "Available for everyone" | tick %}
+ + {% for article in article_list %} + + {{ article.name }} + {{ article.prix }} + {{ article.type_cotisation }} + {{ article.duration }} + {{ article.type_user }} + {{ article.available_for_everyone }} + + {% can_edit article %} + + + + {% acl_end %} + {% history_button article %} + + + {% endfor %} + diff --git a/cotisations/templates/cotisations/aff_banque.html b/cotisations/templates/cotisations/aff_banque.html index d9ec8d1c..b5074c6c 100644 --- a/cotisations/templates/cotisations/aff_banque.html +++ b/cotisations/templates/cotisations/aff_banque.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} {% load i18n %} +{% load logs_extra %} @@ -36,14 +37,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %} diff --git a/cotisations/templates/cotisations/aff_cotisations.html b/cotisations/templates/cotisations/aff_cotisations.html index fdb575ed..6b5fa8fa 100644 --- a/cotisations/templates/cotisations/aff_cotisations.html +++ b/cotisations/templates/cotisations/aff_cotisations.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} {% load i18n %} +{% load logs_extra %}
{% if facture_list.paginator %} @@ -86,9 +87,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_end %}
  • - - {% trans "Historique" %} - + {% history_button facture text=True html_class=False%}
  • diff --git a/cotisations/templates/cotisations/aff_paiement.html b/cotisations/templates/cotisations/aff_paiement.html index 63a5cf35..46523928 100644 --- a/cotisations/templates/cotisations/aff_paiement.html +++ b/cotisations/templates/cotisations/aff_paiement.html @@ -24,28 +24,34 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} {% load i18n %} +{% load logs_extra %} +{% load design %} -
    {{ banque.name }} - {% can_edit banque %} - + {% can_edit banque %} + - {% acl_end %} - - - + {% acl_end %} + {% history_button banque %}
    - - - - - - - {% for paiement in paiement_list %} +
    {% trans "Payment method" %}
    + - - + + + + - {% endfor %} -
    {{ paiement.moyen }} - {% can_edit paiement %} - - - - {% acl_end %} - - - - {% trans "Payment type" %}{% trans "Is available for everyone" %}{% trans "Custom payment method" %}
    + + {% for paiement in paiement_list %} + + {{ paiement.moyen }} + {{ paiement.available_for_everyone|tick }} + + {{paiement.get_payment_method_name}} + + + {% can_edit paiement %} + + + + {% acl_end %} + {% history_button paiement %} + + + {% endfor %} + diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index dc8648ac..4ec05ec1 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -30,10 +30,27 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Invoices creation and edition" %}{% endblock %} {% block content %} -{% bootstrap_form_errors factureform %} +{% if title %} +

    {{title}}

    +{% else %} +

    {% trans "New invoice" %}

    +{% endif %} +{% if max_balance %} +

    {% trans "Maximum allowed balance : "%}{{max_balance}} €

    +{% endif %} +{% if balance is not None %} +

    + {% trans "Current balance :" %} {{ balance }} € +

    +{% endif %}
    {% csrf_token %} + {% bootstrap_form factureform %} + {% if payment_method %} + {% bootstrap_form payment_method %} +
    + {% endif %} {% if articlesformset %}

    {% trans "Invoice's articles" %}

    @@ -56,17 +73,17 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endblocktrans %}

    {% endif %} - {% bootstrap_form factureform %} {% bootstrap_button action_name button_type='submit' icon='star' %} -{% if articlesformset %} +{% if articlesformset or payment_method%} {% endif %} diff --git a/cotisations/templates/cotisations/new_facture.html b/cotisations/templates/cotisations/new_facture.html deleted file mode 100644 index 4dab92d3..00000000 --- a/cotisations/templates/cotisations/new_facture.html +++ /dev/null @@ -1,164 +0,0 @@ -{% extends "cotisations/sidebar.html" %} -{% comment %} -Re2o est un logiciel d'administration développé initiallement au rezometz. Il -se veut agnostique au réseau considéré, de manière à être installable en -quelques clics. - -Copyright © 2017 Gabriel Détraz -Copyright © 2017 Goulven Kermarec -Copyright © 2017 Augustin Lemesle - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -{% endcomment %} - -{% load bootstrap3 %} -{% load staticfiles%} -{% load i18n %} - -{% block title %}{% trans "Invoices creation and edition" %}{% endblock %} - -{% block content %} -{% bootstrap_form_errors factureform %} - -
    - {% csrf_token %} -

    {% trans "New invoice" %}

    -

    - {% blocktrans %} - User's balance : {{ user.solde }} € - {% endblocktrans %} -

    - {% bootstrap_form factureform %} - {{ venteform.management_form }} - -

    {% trans "Invoice's articles" %}

    -
    - {% for form in venteform.forms %} -
    - {% trans "Article" %} :   - {% bootstrap_form form label_class='sr-only' %} -   - -
    - {% endfor %} -
    - -

    - {% blocktrans %} - Total price : 0,00 € - {% endblocktrans %} -

    - {% trans "Create" as tr_create %} - {% bootstrap_button tr_create button_type='submit' icon='star' %} -
    - - - -{% endblock %} diff --git a/cotisations/templates/cotisations/new_facture_solde.html b/cotisations/templates/cotisations/new_facture_solde.html deleted file mode 100644 index dac68c54..00000000 --- a/cotisations/templates/cotisations/new_facture_solde.html +++ /dev/null @@ -1,158 +0,0 @@ -{% extends "cotisations/sidebar.html" %} -{% comment %} -Re2o est un logiciel d'administration développé initiallement au rezometz. Il -se veut agnostique au réseau considéré, de manière à être installable en -quelques clics. - -Copyright © 2017 Gabriel Détraz -Copyright © 2017 Goulven Kermarec -Copyright © 2017 Augustin Lemesle - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -{% endcomment %} - -{% load bootstrap3 %} -{% load staticfiles%} -{% load i18n %} - -{% block title %}{% trans "Invoices creation and edition" %}{% endblock %} - -{% block content %} -{% bootstrap_form_errors venteform.management_form %} - -
    - {% csrf_token %} -

    {% trans "New invoice" %}

    - {{ venteform.management_form }} - -

    {% trans "Invoice's articles" %}

    -
    - {% for form in venteform.forms %} -
    - {% trans "Article" %} :   - {% bootstrap_form form label_class='sr-only' %} -   - -
    - {% endfor %} -
    - -

    - {% blocktrans %} - Total price : 0,00 € - {% endblocktrans %} -

    - {% trans "Confirm" as tr_confirm %} - {% bootstrap_button tr_confirm button_type='submit' icon='star' %} -
    - - - -{% endblock %} - diff --git a/cotisations/templates/cotisations/payment.html b/cotisations/templates/cotisations/payment.html index a0a1abae..e1c8b0d0 100644 --- a/cotisations/templates/cotisations/payment.html +++ b/cotisations/templates/cotisations/payment.html @@ -32,11 +32,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

    {% blocktrans %} - Refill of {{ amount }} € + Pay {{ amount }} € {% endblocktrans %}

    -
    + {{ content | safe }} + {% if form %} + {% csrf_token %} + {% bootstrap_form form %} + {% endif %} {% trans "Pay" as tr_pay %} {% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %}
    diff --git a/cotisations/urls.py b/cotisations/urls.py index 6eafe721..470ccbfa 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -27,9 +27,8 @@ from __future__ import unicode_literals from django.conf.urls import url -import re2o from . import views -from . import payment +from . import payment_methods urlpatterns = [ url( @@ -122,41 +121,10 @@ urlpatterns = [ views.index_paiement, name='index-paiement' ), - url( - r'history/(?P\w+)/(?P[0-9]+)$', - re2o.views.history, - name='history', - kwargs={'application': 'cotisations'}, - ), url( r'^control/$', views.control, name='control' ), - url( - r'^new_facture_solde/(?P[0-9]+)$', - views.new_facture_solde, - name='new_facture_solde' - ), - url( - r'^recharge/$', - views.recharge, - name='recharge' - ), - url( - r'^payment/accept/(?P[0-9]+)$', - payment.accept_payment, - name='accept_payment' - ), - url( - r'^payment/refuse/$', - payment.refuse_payment, - name='refuse_payment' - ), - url( - r'^payment/ipn/$', - payment.ipn, - name='ipn' - ), url(r'^$', views.index, name='index'), -] +] + payment_methods.urls.urlpatterns diff --git a/cotisations/utils.py b/cotisations/utils.py new file mode 100644 index 00000000..f36b376f --- /dev/null +++ b/cotisations/utils.py @@ -0,0 +1,32 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Hugo Levy-Falk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +def find_payment_method(payment): + """Finds the payment method associated to the payment if it exists.""" + from cotisations.payment_methods import PAYMENT_METHODS + for method in PAYMENT_METHODS: + try: + o = method.PaymentMethod.objects.get(payment=payment) + return o + except method.PaymentMethod.DoesNotExist: + pass + return None diff --git a/cotisations/validators.py b/cotisations/validators.py new file mode 100644 index 00000000..fa8ea2cf --- /dev/null +++ b/cotisations/validators.py @@ -0,0 +1,21 @@ +from django.forms import ValidationError +from django.utils.translation import ugettext as _ + + +def check_no_balance(is_balance): + """This functions checks that no Paiement with is_balance=True exists + + Args: + is_balance: True if the model is balance. + + Raises: + ValidationError: if such a Paiement exists. + """ + from .models import Paiement + if not is_balance: + return + p = Paiement.objects.filter(is_balance=True) + if len(p) > 0: + raise ValidationError( + _("There are already payment method(s) for user balance") + ) diff --git a/cotisations/views.py b/cotisations/views.py index 47076c8f..8b9fe79e 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -5,6 +5,7 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle +# Copyright © 2018 Hugo Levy-Falk # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -31,7 +32,7 @@ from __future__ import unicode_literals import os from django.urls import reverse -from django.shortcuts import render, redirect +from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib import messages from django.db.models import ProtectedError @@ -56,11 +57,10 @@ from re2o.acl import ( can_delete_set, can_change, ) -from preferences.models import OptionalUser, AssoOption, GeneralOption +from preferences.models import AssoOption, GeneralOption from .models import Facture, Article, Vente, Paiement, Banque from .forms import ( - NewFactureForm, - EditFactureForm, + FactureForm, ArticleForm, DelArticleForm, PaiementForm, @@ -70,11 +70,11 @@ from .forms import ( NewFactureFormPdf, SelectUserArticleForm, SelectClubArticleForm, - CreditSoldeForm, RechargeForm ) -from . import payment as online_payment from .tex import render_invoice +from .payment_methods.forms import payment_method_factory +from .utils import find_payment_method @login_required @@ -84,29 +84,33 @@ def new_facture(request, user, userid): """ View called to create a new invoice. Currently, Send the list of available articles for the user along with - a formset of a new invoice (based on the `:forms:NewFactureForm()` form. + a formset of a new invoice (based on the `:forms:FactureForm()` form. A bit of JS is used in the template to add articles in a fancier way. If everything is correct, save each one of the articles, save the purchase object associated and finally the newly created invoice. - - TODO : The whole verification process should be moved to the model. This - function should only act as a dumb interface between the model and the - user. """ invoice = Facture(user=user) # The template needs the list of articles (for the JS part) article_list = Article.objects.filter( Q(type_user='All') | Q(type_user=request.user.class_name) ) - # Building the invocie form and the article formset - invoice_form = NewFactureForm(request.POST or None, instance=invoice) + # Building the invoice form and the article formset + invoice_form = FactureForm( + request.POST or None, + instance=invoice, + user=request.user, + creation=True + ) + if request.user.is_class_club: article_formset = formset_factory(SelectClubArticleForm)( - request.POST or None + request.POST or None, + form_kwargs={'user': request.user} ) else: article_formset = formset_factory(SelectUserArticleForm)( - request.POST or None + request.POST or None, + form_kwargs={'user': request.user} ) if invoice_form.is_valid() and article_formset.is_valid(): @@ -114,42 +118,15 @@ def new_facture(request, user, userid): articles = article_formset # Check if at leat one article has been selected if any(art.cleaned_data for art in articles): - user_balance = OptionalUser.get_cached_value('user_solde') - negative_balance = OptionalUser.get_cached_value('solde_negatif') - # If the paiement using balance has been activated, - # checking that the total price won't get the user under - # the authorized minimum (negative_balance) - if user_balance: - # TODO : change Paiement to Payment - if new_invoice_instance.paiement == ( - Paiement.objects.get_or_create(moyen='solde')[0] - ): - total_price = 0 - for art_item in articles: - if art_item.cleaned_data: - total_price += ( - art_item.cleaned_data['article'].prix * - art_item.cleaned_data['quantity'] - ) - if (float(user.solde) - float(total_price) - < negative_balance): - messages.error( - request, - _("Your balance is too low for this operation.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) - # Saving the invoice - new_invoice_instance.save() - # Building a purchase for each article sold + purchases = [] + total_price = 0 for art_item in articles: if art_item.cleaned_data: article = art_item.cleaned_data['article'] quantity = art_item.cleaned_data['quantity'] - new_purchase = Vente.objects.create( + total_price += article.prix*quantity + new_purchase = Vente( facture=new_invoice_instance, name=article.name, prix=article.prix, @@ -157,41 +134,42 @@ def new_facture(request, user, userid): duration=article.duration, number=quantity ) - new_purchase.save() - - # In case a cotisation was bought, inform the user, the - # cotisation time has been extended too - if any(art_item.cleaned_data['article'].type_cotisation - for art_item in articles if art_item.cleaned_data): - messages.success( - request, - _("The cotisation of %(member_name)s has been \ - extended to %(end_date)s.") % { - 'member_name': user.pseudo, - 'end_date': user.end_adhesion() - } - ) - # Else, only tell the invoice was created + purchases.append(new_purchase) + p = find_payment_method(new_invoice_instance.paiement) + if hasattr(p, 'check_price'): + price_ok, msg = p.check_price(total_price, user) + invoice_form.add_error(None, msg) else: - messages.success( - request, - _("The invoice has been created.") + price_ok = True + if price_ok: + new_invoice_instance.save() + for p in purchases: + p.facture = new_invoice_instance + p.save() + + return new_invoice_instance.paiement.end_payment( + new_invoice_instance, + request ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) - messages.error( - request, - _("You need to choose at least one article.") - ) + else: + messages.error( + request, + _("You need to choose at least one article.") + ) + p = Paiement.objects.filter(is_balance=True) + if len(p) and p[0].can_use_payment(request.user): + balance = user.solde + else: + balance = None return form( { 'factureform': invoice_form, - 'venteform': article_formset, - 'articlelist': article_list + 'articlesformset': article_formset, + 'articlelist': article_list, + 'balance': balance, + 'action_name': _('Create'), }, - 'cotisations/new_facture.html', request + 'cotisations/facture.html', request ) @@ -212,11 +190,13 @@ def new_facture_pdf(request): invoice_form = NewFactureFormPdf(request.POST or None) if request.user.is_class_club: articles_formset = formset_factory(SelectClubArticleForm)( - request.POST or None + request.POST or None, + form_kwargs={'user': request.user} ) else: articles_formset = formset_factory(SelectUserArticleForm)( - request.POST or None + request.POST or None, + form_kwargs={'user': request.user} ) if invoice_form.is_valid() and articles_formset.is_valid(): # Get the article list and build an list out of it @@ -251,13 +231,13 @@ def new_facture_pdf(request): 'email': AssoOption.get_cached_value('contact'), 'phone': AssoOption.get_cached_value('telephone'), 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) - }) + }) return form({ 'factureform': invoice_form, 'action_name': _("Create"), 'articlesformset': articles_formset, 'articles': articles - }, 'cotisations/facture.html', request) + }, 'cotisations/facture.html', request) # TODO : change facture to invoice @@ -300,7 +280,7 @@ def facture_pdf(request, facture, **_kwargs): 'email': AssoOption.get_cached_value('contact'), 'phone': AssoOption.get_cached_value('telephone'), 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) - }) + }) # TODO : change facture to invoice @@ -313,7 +293,7 @@ def edit_facture(request, facture, **_kwargs): can be set as desired. This is also the view used to invalidate an invoice. """ - invoice_form = EditFactureForm( + invoice_form = FactureForm( request.POST or None, instance=facture, user=request.user @@ -324,7 +304,7 @@ def edit_facture(request, facture, **_kwargs): fields=('name', 'number'), extra=0, max_num=len(purchases_objects) - ) + ) purchase_form = purchase_form_set( request.POST or None, queryset=purchases_objects @@ -341,7 +321,7 @@ def edit_facture(request, facture, **_kwargs): return form({ 'factureform': invoice_form, 'venteform': purchase_form - }, 'cotisations/edit_facture.html', request) + }, 'cotisations/edit_facture.html', request) # TODO : change facture to invoice @@ -361,40 +341,7 @@ def del_facture(request, facture, **_kwargs): return form({ 'objet': facture, 'objet_name': _("Invoice") - }, 'cotisations/delete.html', request) - - -# TODO : change solde to balance -@login_required -@can_create(Facture) -@can_edit(User) -def credit_solde(request, user, **_kwargs): - """ - View used to edit the balance of a user. - Can be use either to increase or decrease a user's balance. - """ - # TODO : change facture to invoice - invoice = CreditSoldeForm(request.POST or None) - if invoice.is_valid(): - invoice_instance = invoice.save(commit=False) - invoice_instance.user = user - invoice_instance.save() - new_purchase = Vente.objects.create( - facture=invoice_instance, - name="solde", - prix=invoice.cleaned_data['montant'], - number=1 - ) - new_purchase.save() - messages.success( - request, - _("Balance successfully updated.") - ) - return redirect(reverse('cotisations:index')) - return form({ - 'factureform': invoice, - 'action_name': _("Edit") - }, 'cotisations/facture.html', request) + }, 'cotisations/delete.html', request) @login_required @@ -419,8 +366,9 @@ def add_article(request): return redirect(reverse('cotisations:index-article')) return form({ 'factureform': article, - 'action_name': _("Add") - }, 'cotisations/facture.html', request) + 'action_name': _("Add"), + 'title': _("New article") + }, 'cotisations/facture.html', request) @login_required @@ -440,8 +388,9 @@ def edit_article(request, article_instance, **_kwargs): return redirect(reverse('cotisations:index-article')) return form({ 'factureform': article, - 'action_name': _('Edit') - }, 'cotisations/facture.html', request) + 'action_name': _('Edit'), + 'title': _("Edit article") + }, 'cotisations/facture.html', request) @login_required @@ -461,8 +410,9 @@ def del_article(request, instances): return redirect(reverse('cotisations:index-article')) return form({ 'factureform': article, - 'action_name': _("Delete") - }, 'cotisations/facture.html', request) + 'action_name': _("Delete"), + 'title': _("Delete article") + }, 'cotisations/facture.html', request) # TODO : change paiement to payment @@ -472,9 +422,15 @@ def add_paiement(request): """ View used to add a payment method. """ - payment = PaiementForm(request.POST or None) - if payment.is_valid(): - payment.save() + payment = PaiementForm(request.POST or None, prefix='payment') + payment_method = payment_method_factory( + payment.instance, + request.POST or None, + prefix='payment_method' + ) + if payment.is_valid() and payment_method.is_valid(): + payment = payment.save() + payment_method.save(payment) messages.success( request, _("The payment method has been successfully created.") @@ -482,8 +438,10 @@ def add_paiement(request): return redirect(reverse('cotisations:index-paiement')) return form({ 'factureform': payment, - 'action_name': _("Add") - }, 'cotisations/facture.html', request) + 'payment_method': payment_method, + 'action_name': _("Add"), + 'title': _("New payment method") + }, 'cotisations/facture.html', request) # TODO : chnage paiement to Payment @@ -493,19 +451,34 @@ def edit_paiement(request, paiement_instance, **_kwargs): """ View used to edit a payment method. """ - payment = PaiementForm(request.POST or None, instance=paiement_instance) - if payment.is_valid(): - if payment.changed_data: - payment.save() - messages.success( - request, - _("The payement method has been successfully edited.") - ) + payment = PaiementForm( + request.POST or None, + instance=paiement_instance, + prefix="payment" + ) + payment_method = payment_method_factory( + paiement_instance, + request.POST or None, + prefix='payment_method', + creation=False + ) + + if payment.is_valid() and \ + (payment_method is None or payment_method.is_valid()): + payment.save() + if payment_method is not None: + payment_method.save() + messages.success( + request, + _("The payement method has been successfully edited.") + ) return redirect(reverse('cotisations:index-paiement')) return form({ 'factureform': payment, - 'action_name': _("Edit") - }, 'cotisations/facture.html', request) + 'payment_method': payment_method, + 'action_name': _("Edit"), + 'title': _("Edit payment method") + }, 'cotisations/facture.html', request) # TODO : change paiement to payment @@ -539,8 +512,9 @@ def del_paiement(request, instances): return redirect(reverse('cotisations:index-paiement')) return form({ 'factureform': payment, - 'action_name': _("Delete") - }, 'cotisations/facture.html', request) + 'action_name': _("Delete"), + 'title': _("Delete payment method") + }, 'cotisations/facture.html', request) # TODO : change banque to bank @@ -560,8 +534,9 @@ def add_banque(request): return redirect(reverse('cotisations:index-banque')) return form({ 'factureform': bank, - 'action_name': _("Add") - }, 'cotisations/facture.html', request) + 'action_name': _("Add"), + 'title': _("New bank") + }, 'cotisations/facture.html', request) # TODO : change banque to bank @@ -582,8 +557,9 @@ def edit_banque(request, banque_instance, **_kwargs): return redirect(reverse('cotisations:index-banque')) return form({ 'factureform': bank, - 'action_name': _("Edit") - }, 'cotisations/facture.html', request) + 'action_name': _("Edit"), + 'title': _("Edit bank") + }, 'cotisations/facture.html', request) # TODO : chnage banque to bank @@ -617,8 +593,9 @@ def del_banque(request, instances): return redirect(reverse('cotisations:index-banque')) return form({ 'factureform': bank, - 'action_name': _("Delete") - }, 'cotisations/facture.html', request) + 'action_name': _("Delete"), + 'title': _("Delete bank") + }, 'cotisations/facture.html', request) # TODO : change facture to invoice @@ -659,7 +636,7 @@ def control(request): return render(request, 'cotisations/control.html', { 'facture_list': invoice_list, 'controlform': control_invoices_form - }) + }) @login_required @@ -672,7 +649,7 @@ def index_article(request): article_list = Article.objects.order_by('name') return render(request, 'cotisations/index_article.html', { 'article_list': article_list - }) + }) # TODO : change paiement to payment @@ -685,7 +662,7 @@ def index_paiement(request): payment_list = Paiement.objects.order_by('moyen') return render(request, 'cotisations/index_paiement.html', { 'paiement_list': payment_list - }) + }) # TODO : change banque to bank @@ -698,7 +675,7 @@ def index_banque(request): bank_list = Banque.objects.order_by('name') return render(request, 'cotisations/index_banque.html', { 'banque_list': bank_list - }) + }) @login_required @@ -719,156 +696,62 @@ def index(request): invoice_list = re2o_paginator(request, invoice_list, pagination_number) return render(request, 'cotisations/index.html', { 'facture_list': invoice_list - }) + }) -# TODO : merge this function with new_facture() which is nearly the same -# TODO : change facture to invoice +# TODO : change solde to balance @login_required -def new_facture_solde(request, userid): +@can_edit(User) +def credit_solde(request, user, **_kwargs): """ - View called to create a new invoice when using the balance to pay. - Currently, send the list of available articles for the user along with - a formset of a new invoice (based on the `:forms:NewFactureForm()` form. - A bit of JS is used in the template to add articles in a fancier way. - If everything is correct, save each one of the articles, save the - purchase object associated and finally the newly created invoice. - - TODO : The whole verification process should be moved to the model. This - function should only act as a dumb interface between the model and the - user. + View used to edit the balance of a user. + Can be use either to increase or decrease a user's balance. """ - user = request.user - invoice = Facture(user=user) - payment, _created = Paiement.objects.get_or_create(moyen='Solde') - invoice.paiement = payment - # The template needs the list of articles (for the JS part) - article_list = Article.objects.filter( - Q(type_user='All') | Q(type_user=request.user.class_name) - ) - if request.user.is_class_club: - article_formset = formset_factory(SelectClubArticleForm)( - request.POST or None - ) + try: + balance = find_payment_method(Paiement.objects.get(is_balance=True)) + except Paiement.DoesNotExist: + credit_allowed = False else: - article_formset = formset_factory(SelectUserArticleForm)( - request.POST or None + credit_allowed = ( + balance is not None + and balance.can_credit_balance(request.user) ) - - if article_formset.is_valid(): - articles = article_formset - # Check if at leat one article has been selected - if any(art.cleaned_data for art in articles): - user_balance = OptionalUser.get_cached_value('user_solde') - negative_balance = OptionalUser.get_cached_value('solde_negatif') - # If the paiement using balance has been activated, - # checking that the total price won't get the user under - # the authorized minimum (negative_balance) - if user_balance: - total_price = 0 - for art_item in articles: - if art_item.cleaned_data: - total_price += art_item.cleaned_data['article']\ - .prix*art_item.cleaned_data['quantity'] - if float(user.solde) - float(total_price) < negative_balance: - messages.error( - request, - _("The balance is too low for this operation.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) - # Saving the invoice - invoice.save() - - # Building a purchase for each article sold - for art_item in articles: - if art_item.cleaned_data: - article = art_item.cleaned_data['article'] - quantity = art_item.cleaned_data['quantity'] - new_purchase = Vente.objects.create( - facture=invoice, - name=article.name, - prix=article.prix, - type_cotisation=article.type_cotisation, - duration=article.duration, - number=quantity - ) - new_purchase.save() - - # In case a cotisation was bought, inform the user, the - # cotisation time has been extended too - if any(art_item.cleaned_data['article'].type_cotisation - for art_item in articles if art_item.cleaned_data): - messages.success( - request, - _("The cotisation of %(member_name)s has been successfully \ - extended to %(end_date)s.") % { - 'member_name': user.pseudo, - 'end_date': user.end_adhesion() - } - ) - # Else, only tell the invoice was created - else: - messages.success( - request, - _("The invoice has been successuflly created.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) + if not credit_allowed: messages.error( request, - _("You need to choose at least one article.") + _("You are not allowed to credit your balance.") ) return redirect(reverse( 'users:profil', - kwargs={'userid': userid} + kwargs={'userid': user.id} )) - return form({ - 'venteform': article_formset, - 'articlelist': article_list - }, 'cotisations/new_facture_solde.html', request) - - -# TODO : change recharge to refill -@login_required -def recharge(request): - """ - View used to refill the balance by using online payment. - """ - if AssoOption.get_cached_value('payment') == 'NONE': - messages.error( - request, - _("Online payment is disabled.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': request.user.id} - )) refill_form = RechargeForm(request.POST or None, user=request.user) if refill_form.is_valid(): - invoice = Facture(user=request.user) - payment, _created = Paiement.objects.get_or_create( - moyen='Rechargement en ligne' - ) - invoice.paiement = payment - invoice.valid = False - invoice.save() - purchase = Vente.objects.create( - facture=invoice, - name='solde', - prix=refill_form.cleaned_data['value'], - number=1 - ) - purchase.save() - content = online_payment.PAYMENT_SYSTEM[ - AssoOption.get_cached_value('payment') - ](invoice, request) - return render(request, 'cotisations/payment.html', content) + price = refill_form.cleaned_data['value'] + invoice = Facture(user=user) + invoice.paiement = refill_form.cleaned_data['payment'] + p = find_payment_method(invoice.paiement) + if hasattr(p, 'check_price'): + price_ok, msg = p.check_price(price, user) + refill_form.add_error(None, msg) + else: + price_ok = True + if price_ok: + invoice.valid = True + invoice.save() + Vente.objects.create( + facture=invoice, + name='solde', + prix=refill_form.cleaned_data['value'], + number=1 + ) + return invoice.paiement.end_payment(invoice, request) + p = get_object_or_404(Paiement, is_balance=True) return form({ - 'rechargeform': refill_form - }, 'cotisations/recharge.html', request) + 'factureform': refill_form, + 'balance': request.user.solde, + 'title': _("Refill your balance"), + 'action_name': _("Pay"), + 'max_balance': p.payment_method.maximum_balance, + }, 'cotisations/facture.html', request) diff --git a/logs/templates/logs/aff_stats_droits.html b/logs/templates/logs/aff_stats_droits.html deleted file mode 100644 index 6e424223..00000000 --- a/logs/templates/logs/aff_stats_droits.html +++ /dev/null @@ -1,86 +0,0 @@ -{% comment %} -Re2o est un logiciel d'administration développé initiallement au rezometz. Il -se veut agnostique au réseau considéré, de manière à être installable en -quelques clics. - -Copyright © 2017 Gabriel Détraz -Copyright © 2017 Goulven Kermarec -Copyright © 2017 Augustin Lemesle - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -{% endcomment %} - -{% load bootstrap3 %} -{% load acl %} - -{% for droit,users in stats_list.items %} -
    -
    -

    - - {{droit}} - {{users.count}} -

    -
    -
    -
    -
    - - - - - - - - - - - - {% for utilisateur in users %} - - - {% if utilisateur.is_adherent %} - - {% elif not utilisateur.end_adhesion %} - - {% else %} - - {% endif %} - - - {% if not utilisateur.last %} - - {% else %} - - {% endif %} - - - {% endfor %} -
    PseudoAdhésionDerniere connexionNombre d'actionsDate de la dernière action
    {{ utilisateur.pseudo }}

    Adhérent

    On ne s'en souvient plus...

    Plus depuis {{ utilisateur.end_adhesion }}

    {{ utilisateur.last_login }}{{ utilisateur.num }}

    Jamais

    {{utilisateur.last}}

    - {% if droit != 'Superuser' %} - - {% else %} - - {% endif %} - - -
    -
    -
    -
    -
    -{% endfor %} diff --git a/logs/templates/logs/sidebar.html b/logs/templates/logs/sidebar.html index 03a4bd73..0e3048e3 100644 --- a/logs/templates/logs/sidebar.html +++ b/logs/templates/logs/sidebar.html @@ -51,9 +51,5 @@ with this program; if not, write to the Free Software Foundation, Inc., Utilisateurs - - - Groupes de droit - {% acl_end %} {% endblock %} diff --git a/logs/templates/logs/stats_droits.html b/logs/templates/logs/stats_droits.html deleted file mode 100644 index 76c20331..00000000 --- a/logs/templates/logs/stats_droits.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends "logs/sidebar.html" %} -{% comment %} -Re2o est un logiciel d'administration développé initiallement au rezometz. Il -se veut agnostique au réseau considéré, de manière à être installable en -quelques clics. - -Copyright © 2017 Gabriel Détraz -Copyright © 2017 Goulven Kermarec -Copyright © 2017 Augustin Lemesle - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -{% endcomment %} - -{% load bootstrap3 %} - -{% block title %}Statistiques des droits{% endblock %} - -{% block content %} -

    Statistiques des droits

    - {% include "logs/aff_stats_droits.html" with stats_list=stats_list %} -
    -
    -
    - {% endblock %} diff --git a/logs/templatetags/logs_extra.py b/logs/templatetags/logs_extra.py index 97f60ec8..0620f8f4 100644 --- a/logs/templatetags/logs_extra.py +++ b/logs/templatetags/logs_extra.py @@ -33,3 +33,23 @@ register = template.Library() def classname(obj): """ Returns the object class name """ return obj.__class__.__name__ + + +@register.inclusion_tag('buttons/history.html') +def history_button(instance, text=False, html_class=True): + """Creates the correct history button for an instance. + + Args: + instance: The instance of which you want to get history buttons. + text: Flag stating if a 'History' text should be displayed. + html_class: Flag stating if the link should have the html classes + allowing it to be displayed as a button. + + """ + return { + 'application': instance._meta.app_label, + 'name': instance._meta.model_name, + 'id': instance.id, + 'text': text, + 'class': html_class, + } diff --git a/logs/urls.py b/logs/urls.py index 98dec281..9398cfe4 100644 --- a/logs/urls.py +++ b/logs/urls.py @@ -39,5 +39,9 @@ urlpatterns = [ url(r'^stats_models/$', views.stats_models, name='stats-models'), url(r'^stats_users/$', views.stats_users, name='stats-users'), url(r'^stats_actions/$', views.stats_actions, name='stats-actions'), - url(r'^stats_droits/$', views.stats_droits, name='stats-droits'), + url( + r'(?P\w+)/(?P\w+)/(?P[0-9]+)$', + views.history, + name='history', + ), ] diff --git a/logs/views.py b/logs/views.py index 1fdda9fb..d7ba59f3 100644 --- a/logs/views.py +++ b/logs/views.py @@ -2,9 +2,10 @@ # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # -# Copyright © 2017 Gabriel Détraz -# Copyright © 2017 Goulven Kermarec -# Copyright © 2017 Augustin Lemesle +# Copyright © 2018 Gabriel Détraz +# Copyright © 2018 Goulven Kermarec +# Copyright © 2018 Augustin Lemesle +# Copyright © 2018 Hugo Levy-Falk # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -36,12 +37,16 @@ nombre d'objets par models, nombre d'actions par user, etc """ from __future__ import unicode_literals +from itertools import chain from django.urls import reverse from django.shortcuts import render, redirect from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.db.models import Count, Max, F +from django.http import Http404 +from django.db.models import Count +from django.apps import apps +from django.utils.translation import ugettext as _ from reversion.models import Revision from reversion.models import Version, ContentType @@ -142,7 +147,7 @@ def index(request): 'comment': version.revision.comment, 'datetime': version.revision.date_created.strftime( '%d/%m/%y %H:%M:%S' - ), + ), 'username': version.revision.user.get_username() if version.revision.user else '?', @@ -173,7 +178,7 @@ def stats_logs(request): revisions = re2o_paginator(request, revisions, pagination_number) return render(request, 'logs/stats_logs.html', { 'revisions_list': revisions - }) + }) @login_required @@ -191,7 +196,7 @@ def revert_action(request, revision_id): return form({ 'objet': revision, 'objet_name': revision.__class__.__name__ - }, 'logs/delete.html', request) + }, 'logs/delete.html', request) @login_required @@ -455,25 +460,56 @@ def stats_actions(request): return render(request, 'logs/stats_users.html', {'stats_list': stats}) -@login_required -@can_view_app('users') -def stats_droits(request): - """Affiche la liste des droits et les users ayant chaque droit""" - stats_list = {} +def history(request, application, object_name, object_id): + """Render history for a model. - for droit in ListRight.objects.all().select_related('group_ptr'): - stats_list[droit] = droit.user_set.all().annotate( - num=Count('revision'), - last=Max('revision__date_created'), - ) + The model is determined using the `HISTORY_BIND` dictionnary if none is + found, raises a Http404. The view checks if the user is allowed to see the + history using the `can_view` method of the model. - stats_list['Superuser'] = User.objects.filter(is_superuser=True).annotate( - num=Count('revision'), - last=Max('revision__date_created'), - ) + Args: + request: The request sent by the user. + application: Name of the application. + object_name: Name of the model. + object_id: Id of the object you want to acces history. + Returns: + The rendered page of history if access is granted, else the user is + redirected to their profile page, with an error message. + + Raises: + Http404: This kind of models doesn't have history. + """ + try: + model = apps.get_model(application, object_name) + except LookupError: + raise Http404(_("No model found.")) + object_name_id = object_name + 'id' + kwargs = {object_name_id: object_id} + try: + instance = model.get_instance(**kwargs) + except model.DoesNotExist: + messages.error(request, _("No entry found.")) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) + can, msg = instance.can_view(request.user) + if not can: + messages.error(request, msg or _("You cannot acces to this menu")) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) + pagination_number = GeneralOption.get_cached_value('pagination_number') + reversions = Version.objects.get_for_object(instance) + if hasattr(instance, 'linked_objects'): + for related_object in chain(instance.linked_objects()): + reversions = (reversions | + Version.objects.get_for_object(related_object)) + reversions = re2o_paginator(request, reversions, pagination_number) return render( request, - 'logs/stats_droits.html', - {'stats_list': stats_list} + 're2o/history.html', + {'reversions': reversions, 'object': instance} ) diff --git a/machines/admin.py b/machines/admin.py index 0f85007c..eb765748 100644 --- a/machines/admin.py +++ b/machines/admin.py @@ -37,6 +37,7 @@ from .models import ( Ns, Vlan, Txt, + DName, Srv, Nas, Service, @@ -95,6 +96,10 @@ class TxtAdmin(VersionAdmin): """ Admin view of a TXT object """ pass +class DNameAdmin(VersionAdmin): + """ Admin view of a DName object """ + pass + class SrvAdmin(VersionAdmin): """ Admin view of a SRV object """ @@ -144,6 +149,7 @@ admin.site.register(SOA, SOAAdmin) admin.site.register(Mx, MxAdmin) admin.site.register(Ns, NsAdmin) admin.site.register(Txt, TxtAdmin) +admin.site.register(DName, DNameAdmin) admin.site.register(Srv, SrvAdmin) admin.site.register(IpList, IpListAdmin) admin.site.register(Interface, InterfaceAdmin) diff --git a/machines/forms.py b/machines/forms.py index 91df33d4..36cd64f8 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -51,6 +51,7 @@ from .models import ( SOA, Mx, Txt, + DName, Ns, Service, Vlan, @@ -410,6 +411,34 @@ class DelTxtForm(FormRevMixin, Form): self.fields['txt'].queryset = Txt.objects.all() +class DNameForm(FormRevMixin, ModelForm): + """Add a DNAME entry for a zone""" + class Meta: + model = DName + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(DNameForm, self).__init__(*args, prefix=prefix, **kwargs) + + +class DelDNameForm(FormRevMixin, Form): + """Delete a set of DNAME entries""" + dnames = forms.ModelMultipleChoiceField( + queryset=Txt.objects.none(), + label="Existing DNAME entries", + widget=forms.CheckboxSelectMultiple + ) + + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelDNameForm, self).__init__(*args, **kwargs) + if instances: + self.fields['dnames'].queryset = instances + else: + self.fields['dnames'].queryset = DName.objects.all() + + class SrvForm(FormRevMixin, ModelForm): """Ajout d'un srv pour une zone""" class Meta: diff --git a/machines/migrations/0083_remove_duplicate_rights.py b/machines/migrations/0083_remove_duplicate_rights.py new file mode 100644 index 00000000..05ad2938 --- /dev/null +++ b/machines/migrations/0083_remove_duplicate_rights.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + +def remove_permission_alias(apps, schema_editor): + Permission = apps.get_model('auth', 'Permission') + for codename in ['add_alias', 'change_alias', 'delete_alias']: + # Retrieve the wrong permission + try: + to_remove = Permission.objects.get( + codename=codename, + content_type__model='domain' + ) + except Permission.DoesNotExist: + # The permission is missing so no problem + pass + else: + to_remove.delete() + + +def remove_permission_text(apps, schema_editor): + Permission = apps.get_model('auth', 'Permission') + for codename in ['add_text', 'change_text', 'delete_text']: + # Retrieve the wrong permission + try: + to_remove = Permission.objects.get( + codename=codename, + content_type__model='txt' + ) + except Permission.DoesNotExist: + # The permission is missing so no problem + pass + else: + to_remove.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0082_auto_20180525_2209'), + ] + + operations = [ + migrations.RunPython(remove_permission_text), + migrations.RunPython(remove_permission_alias), + ] diff --git a/machines/migrations/0088_dname.py b/machines/migrations/0088_dname.py new file mode 100644 index 00000000..4cbeb492 --- /dev/null +++ b/machines/migrations/0088_dname.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-25 14:33 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import re2o.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0083_remove_duplicate_rights'), + ] + + operations = [ + migrations.CreateModel( + name='DName', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('alias', models.CharField(max_length=255)), + ('zone', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='machines.Extension')), + ], + options={ + 'permissions': (('view_dname', 'Can see a dname object'),), + 'verbose_name': 'DNAME entry', + 'verbose_name_plural': 'DNAME entries' + }, + bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model), + ), + ] diff --git a/machines/models.py b/machines/models.py index fd4999d6..590e3997 100644 --- a/machines/models.py +++ b/machines/models.py @@ -3,9 +3,10 @@ # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # -# Copyright © 2017 Gabriel Détraz +# Copyright © 2016-2018 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle +# Copyright © 2018 Charlie Jacomme # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -670,6 +671,27 @@ class Txt(RevMixin, AclMixin, models.Model): return str(self.field1).ljust(15) + " IN TXT " + str(self.field2) +class DName(RevMixin, AclMixin, models.Model): + """A DNAME entry for the DNS.""" + zone = models.ForeignKey('Extension', on_delete=models.PROTECT) + alias = models.CharField(max_length=255) + + class Meta: + permissions = ( + ("view_dname", "Can see a dname object"), + ) + verbose_name = "DNAME entry" + verbose_name_plural = "DNAME entries" + + def __str__(self): + return str(self.zone) + " : " + str(self.alias) + + @cached_property + def dns_entry(self): + """Returns the DNAME record for the DNS zone file.""" + return str(self.alias).ljust(15) + " IN DNAME " + str(self.zone) + + class Srv(RevMixin, AclMixin, models.Model): """ A SRV record """ PRETTY_NAME = "Enregistrement Srv" @@ -1687,6 +1709,18 @@ def text_post_delete(**_kwargs): regen('dns') +@receiver(post_save, sender=DName) +def dname_post_save(**_kwargs): + """Updates the DNS regen after modification of a DName object.""" + regen('dns') + + +@receiver(post_delete, sender=DName) +def dname_post_delete(**_kwargs): + """Updates the DNS regen after deletion of a DName object.""" + regen('dns') + + @receiver(post_save, sender=Srv) def srv_post_save(**_kwargs): """Regeneration dns après modification d'un SRV""" diff --git a/machines/templates/machines/aff_alias.html b/machines/templates/machines/aff_alias.html index f19b6482..184db6f4 100644 --- a/machines/templates/machines/aff_alias.html +++ b/machines/templates/machines/aff_alias.html @@ -23,24 +23,25 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} - - - - - - - - {% for alias in alias_list %} +
    Alias
    + - - + + - {% endfor %} -
    {{ alias }} - {% can_edit alias %} - {% include 'buttons/edit.html' with href='machines:edit-alias' id=alias.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='domain' id=alias.id %} - Alias
    + + {% for alias in alias_list %} + + {{ alias }} + + {% can_edit alias %} + {% include 'buttons/edit.html' with href='machines:edit-alias' id=alias.id %} + {% acl_end %} + {% history_button alias %} + + + {% endfor %} + diff --git a/cotisations/templates/cotisations/recharge.html b/machines/templates/machines/aff_dname.html similarity index 54% rename from cotisations/templates/cotisations/recharge.html rename to machines/templates/machines/aff_dname.html index 196de2ca..8ee3280e 100644 --- a/cotisations/templates/cotisations/recharge.html +++ b/machines/templates/machines/aff_dname.html @@ -1,12 +1,9 @@ -{% extends "cotisations/sidebar.html" %} {% comment %} Re2o est un logiciel d'administration développé initiallement au rezometz. Il se veut agnostique au réseau considéré, de manière à être installable en quelques clics. -Copyright © 2017 Gabriel Détraz -Copyright © 2017 Goulven Kermarec -Copyright © 2017 Augustin Lemesle +Copyright © 2018 Charlie Jacomme This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,23 +20,28 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} -{% load bootstrap3 %} -{% load staticfiles%} -{% load i18n %} +{% load acl %} + + + + + + + + + + {% for dname in dname_list %} + + + + + + {% endfor %} +
    Target zoneRecord
    {{ dname.zone }}{{ dname.dns_entry }} + {% can_edit dname %} + {% include 'buttons/edit.html' with href='machines:edit-dname' id=dname.id %} + {% acl_end %} + {% include 'buttons/history.html' with href='machines:history' name='dname' id=dname.id %} +
    -{% block title %}{% trans "Balance refill" %}{% endblock %} -{% block content %} -

    {% trans "Balance refill" %}

    -

    - {% blocktrans %} - Balance : {{ request.user.solde }} € - {% endblocktrans %} -

    -
    - {% csrf_token %} - {% bootstrap_form rechargeform %} - {% trans "Confirm" as tr_confirm %} - {% bootstrap_button tr_confirm button_type='submit' icon='piggy-bank' %} -
    -{% endblock %} diff --git a/machines/templates/machines/aff_extension.html b/machines/templates/machines/aff_extension.html index 44061b1b..ba444eca 100644 --- a/machines/templates/machines/aff_extension.html +++ b/machines/templates/machines/aff_extension.html @@ -23,6 +23,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} +{% load design %}
    @@ -30,28 +32,28 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - {% if ipv6_enabled %} - - {% endif %} - + + + {% if ipv6_enabled %} + + {% endif %} + {% for extension in extension_list %} - + - + - + {% if ipv6_enabled %} {% endif %} {% endfor %} diff --git a/machines/templates/machines/aff_iptype.html b/machines/templates/machines/aff_iptype.html index 1a76e303..fa2a2767 100644 --- a/machines/templates/machines/aff_iptype.html +++ b/machines/templates/machines/aff_iptype.html @@ -22,7 +22,10 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} +{% load design %} + {% load acl %} +{% load logs_extra %}
    Extension Droit infra pour utiliser ?Enregistrement SOAEnregistrement A originEnregistrement AAAA originEnregistrement SOAEnregistrement A originEnregistrement AAAA origin
    {{ extension.name }}{{ extension.need_infra }}{{ extension.need_infra|tick }} {{ extension.soa}}{{ extension.origin }}{{ extension.origin }}{{ extension.origin_v6 }} - {% can_edit extension %} + {% can_edit extension %} {% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='extension' id=extension.id %} + {% acl_end %} + {% history_button extension %}
    @@ -31,10 +34,10 @@ with this program; if not, write to the Free Software Foundation, Inc., - + - - + + @@ -42,16 +45,16 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - + + + - + {% endfor %} diff --git a/machines/templates/machines/aff_ipv6.html b/machines/templates/machines/aff_ipv6.html index b6efa3f0..d5323f61 100644 --- a/machines/templates/machines/aff_ipv6.html +++ b/machines/templates/machines/aff_ipv6.html @@ -23,29 +23,30 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} -
    Extension Nécessite l'autorisation infra Plage ipv4Préfixe v6Préfixe v6 Sur vlanOuverture ports par défaultOuverture ports par défault
    {{ type.type }} {{ type.extension }}{{ type.need_infra }}{{ type.domaine_ip_start }}-{{ type.domaine_ip_stop }}{{ type.prefix_v6 }}{{ type.need_infra|tick }}{{ type.domaine_ip_start }}-{{ type.domaine_ip_stop }}{{ type.prefix_v6 }} {{ type.vlan }}{{ type.ouverture_ports }}{{ type.ouverture_ports }} - {% can_edit type %} + {% can_edit type %} {% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='iptype' id=type.id %} + {% acl_end %} + {% history_button type %}
    - - - - - - - - {% for ipv6 in ipv6_list %} +
    Ipv6Slaac
    + - - - + + + - {% endfor %} -
    {{ ipv6.ipv6 }}{{ ipv6.slaac_ip }} - {% can_edit ipv6 %} - {% include 'buttons/edit.html' with href='machines:edit-ipv6list' id=ipv6.id %} - {% acl_end %} - {% can_delete ipv6 %} - {% include 'buttons/suppr.html' with href='machines:del-ipv6list' id=ipv6.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='ipv6list' id=ipv6.id %} - Ipv6Slaac
    + + {% for ipv6 in ipv6_list %} + + {{ ipv6.ipv6 }} + {{ ipv6.slaac_ip }} + + {% can_edit ipv6 %} + {% include 'buttons/edit.html' with href='machines:edit-ipv6list' id=ipv6.id %} + {% acl_end %} + {% can_delete ipv6 %} + {% include 'buttons/suppr.html' with href='machines:del-ipv6list' id=ipv6.id %} + {% acl_end %} + {% history_button ipv6 %} + + + {% endfor %} + diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html index 0020d8b3..0acababc 100644 --- a/machines/templates/machines/aff_machines.html +++ b/machines/templates/machines/aff_machines.html @@ -22,165 +22,166 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} -{% load acl %} +{% load acl %} +{% load logs_extra %}
    -{% if machines_list.paginator %} -{% include "pagination.html" with list=machines_list %} -{% endif %} + {% if machines_list.paginator %} + {% include "pagination.html" with list=machines_list %} + {% endif %} - - - - - - - - - - - - - - - - {% for machine in machines_list %} - - - - - {% for interface in machine.interface_set.all %} - - - - - - + + + {% endif %} + {% endfor %} + + + + {% endfor %} + +
    {% include "buttons/sort.html" with prefix='machine' col='name' text='Nom DNS' %}TypeMACIPActions
    - {{ machine.name|default:'Pas de nom' }} - - {{ machine.user }} - - - {% can_create Interface machine.id %} - {% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc='Ajouter une interface' %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='machine' id=machine.id %} - {% can_delete machine %} - {% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %} - {% acl_end %} -
    - {% if interface.domain.related_domain.all %} - {{ interface.domain }} - - {% else %} - {{ interface.domain }} - {% endif %} - - {{ interface.type }} - - {{ interface.mac_address }} - - IPv4 {{ interface.ipv4 }} -
    - {% if ipv6_enabled and interface.ipv6 != 'None'%} - IPv6 - - {% endif %} -
    -
    -
    -
    +
    +
      + {% for al in interface.domain.related_domain.all %} +
    • + + {{ al }} + + +
    • + {% endfor %} +
    +
    +
    - + -{% if machines_list.paginator %} -{% include "pagination.html" with list=machines_list %} -{% endif %} + {% if machines_list.paginator %} + {% include "pagination.html" with list=machines_list %} + {% endif %}
    diff --git a/machines/templates/machines/aff_machinetype.html b/machines/templates/machines/aff_machinetype.html index facad203..f6c99e49 100644 --- a/machines/templates/machines/aff_machinetype.html +++ b/machines/templates/machines/aff_machinetype.html @@ -23,26 +23,27 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} - - - - - - - - - {% for type in machinetype_list %} +
    Type de machineType d'ip correspondant
    + - - - + + + - {% endfor %} -
    {{ type.type }}{{ type.ip_type }} - {% can_edit type %} - {% include 'buttons/edit.html' with href='machines:edit-machinetype' id=type.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='machinetype' id=type.id %} - Type de machineType d'ip correspondant
    + + {% for type in machinetype_list %} + + {{ type.type }} + {{ type.ip_type }} + + {% can_edit type %} + {% include 'buttons/edit.html' with href='machines:edit-machinetype' id=type.id %} + {% acl_end %} + {% history_button type %} + + + {% endfor %} + diff --git a/machines/templates/machines/aff_mx.html b/machines/templates/machines/aff_mx.html index 4478cdab..1a4dac40 100644 --- a/machines/templates/machines/aff_mx.html +++ b/machines/templates/machines/aff_mx.html @@ -23,30 +23,31 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} - - - - - - - - - - - {% for mx in mx_list %} +
    Zone concernéePrioritéEnregistrement
    + - - - - + + + + + + + {% for mx in mx_list %} + + + + + - - {% endfor %} -
    {{ mx.zone }}{{ mx.priority }}{{ mx.name }} + Zone concernéePrioritéEnregistrement
    {{ mx.zone }}{{ mx.priority }}{{ mx.name }} {% can_edit mx %} {% include 'buttons/edit.html' with href='machines:edit-mx' id=mx.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='mx' id=mx.id %} -
    + {% acl_end %} + {% history_button mx %} + + + {% endfor %} + diff --git a/machines/templates/machines/aff_nas.html b/machines/templates/machines/aff_nas.html index 735a4ca8..78dc2d97 100644 --- a/machines/templates/machines/aff_nas.html +++ b/machines/templates/machines/aff_nas.html @@ -23,32 +23,34 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} +{% load design %} - - - - - - - - - - - - {% for nas in nas_list %} +
    NomType du nasType de machine reliées au nasMode d'accèsAutocapture mac
    + - - - - - - + + + + + + - {% endfor %} -
    {{ nas.name }}{{ nas.nas_type }}{{ nas.machine_type }}{{ nas.port_access_mode }}{{ nas.autocapture_mac }} - {% can_edit nas %} - {% include 'buttons/edit.html' with href='machines:edit-nas' id=nas.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='nas' id=nas.id %} - NomType du nasType de machine reliées au nasMode d'accèsAutocapture mac
    + + {% for nas in nas_list %} + + {{ nas.name }} + {{ nas.nas_type }} + {{ nas.machine_type }} + {{ nas.port_access_mode }} + {{ nas.autocapture_mac|tick }} + + {% can_edit nas %} + {% include 'buttons/edit.html' with href='machines:edit-nas' id=nas.id %} + {% acl_end %} + {% history_button nas %} + + + {% endfor %} + diff --git a/machines/templates/machines/aff_ns.html b/machines/templates/machines/aff_ns.html index 5ee87304..73423047 100644 --- a/machines/templates/machines/aff_ns.html +++ b/machines/templates/machines/aff_ns.html @@ -23,28 +23,29 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} - - - - - - - - - - {% for ns in ns_list %} +
    Zone concernéeInterface autoritaire de la zone
    + - - - + + + + + + {% for ns in ns_list %} + + + + - - {% endfor %} -
    {{ ns.zone }}{{ ns.ns }} + Zone concernéeInterface autoritaire de la zone
    {{ ns.zone }}{{ ns.ns }} {% can_edit ns %} {% include 'buttons/edit.html' with href='machines:edit-ns' id=ns.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='ns' id=ns.id %} -
    + {% acl_end %} + {% history_button ns %} + + + {% endfor %} + diff --git a/machines/templates/machines/aff_servers.html b/machines/templates/machines/aff_servers.html index f29b620f..90629e04 100644 --- a/machines/templates/machines/aff_servers.html +++ b/machines/templates/machines/aff_servers.html @@ -22,26 +22,28 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} - - - - - - - - - - - {% for server in servers_list %} - - - - - - - - - {% endfor %} -
    Nom du serviceServeurDernière régénérationRégénération nécessaireRégénération activée
    {{ server.service }}{{ server.server }}{{ server.last_regen }}{{ server.asked_regen }}{{ server.need_regen }} -
    +{% load design %} + + + + + + + + + + + + {% for server in servers_list %} + + + + + + + + + {% endfor %} +
    Nom du serviceServeurDernière régénérationRégénération nécessaireRégénération activée
    {{ server.service }}{{ server.server }}{{ server.last_regen }}{{ server.asked_regen| tick }}{{ server.need_regen | tick }} +
    diff --git a/machines/templates/machines/aff_service.html b/machines/templates/machines/aff_service.html index da80b4da..6ca6278b 100644 --- a/machines/templates/machines/aff_service.html +++ b/machines/templates/machines/aff_service.html @@ -23,31 +23,32 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} - - - - - - - - - - - - {% for service in service_list %} +
    Nom du serviceTemps minimum avant nouvelle régénérationTemps avant nouvelle génération obligatoire (max)Serveurs inclus
    + - - - - - + + + + + + - {% endfor %} -
    {{ service.service_type }}{{ service.min_time_regen }}{{ service.regular_time_regen }}{% for serv in service.servers.all %}{{ serv }}, {% endfor %} - {% can_edit service %} - {% include 'buttons/edit.html' with href='machines:edit-service' id=service.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='service' id=service.id %} - Nom du serviceTemps minimum avant nouvelle régénérationTemps avant nouvelle génération obligatoire (max)Serveurs inclus
    + + {% for service in service_list %} + + {{ service.service_type }} + {{ service.min_time_regen }} + {{ service.regular_time_regen }} + {% for serv in service.servers.all %}{{ serv }}, {% endfor %} + + {% can_edit service %} + {% include 'buttons/edit.html' with href='machines:edit-service' id=service.id %} + {% acl_end %} + {% history_button service %} + + + {% endfor %} + diff --git a/machines/templates/machines/aff_soa.html b/machines/templates/machines/aff_soa.html index 5352a739..e41e38fa 100644 --- a/machines/templates/machines/aff_soa.html +++ b/machines/templates/machines/aff_soa.html @@ -23,36 +23,37 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} - - - - - - - - - - - - - - {% for soa in soa_list %} +
    NomMailRefreshRetryExpireTTL
    + - - - - - - - + + + + + + + + + + {% for soa in soa_list %} + + + + + + + + - - {% endfor %} -
    {{ soa.name }}{{ soa.mail }}{{ soa.refresh }}{{ soa.retry }}{{ soa.expire }}{{ soa.ttl }} + NomMailRefreshRetryExpireTTL
    {{ soa.name }}{{ soa.mail }}{{ soa.refresh }}{{ soa.retry }}{{ soa.expire }}{{ soa.ttl }} {% can_edit soa %} {% include 'buttons/edit.html' with href='machines:edit-soa' id=soa.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='soa' id=soa.id %} -
    + {% acl_end %} + {% history_button soa %} + + + {% endfor %} + diff --git a/machines/templates/machines/aff_srv.html b/machines/templates/machines/aff_srv.html index e7886cf1..4c502dad 100644 --- a/machines/templates/machines/aff_srv.html +++ b/machines/templates/machines/aff_srv.html @@ -23,40 +23,41 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} - - - - - - - - - - - - - - - - {% for srv in srv_list %} +
    ServiceProtocoleExtensionTTLPrioritéPoidsPortCible
    + - - - - - - - - - + + + + + + + + + + - {% endfor %} -
    {{ srv.service }}{{ srv.protocole }}{{ srv.extension }}{{ srv.ttl }}{{ srv.priority }}{{ srv.weight }}{{ srv.port }}{{ srv.target }} - {% can_edit srv %} - {% include 'buttons/edit.html' with href='machines:edit-srv' id=srv.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='srv' id=srv.id %} - ServiceProtocoleExtensionTTLPrioritéPoidsPortCible
    + + {% for srv in srv_list %} + + {{ srv.service }} + {{ srv.protocole }} + {{ srv.extension }} + {{ srv.ttl }} + {{ srv.priority }} + {{ srv.weight }} + {{ srv.port }} + {{ srv.target }} + + {% can_edit srv %} + {% include 'buttons/edit.html' with href='machines:edit-srv' id=srv.id %} + {% acl_end %} + {% history_button srv %} + + + {% endfor %} + diff --git a/machines/templates/machines/aff_txt.html b/machines/templates/machines/aff_txt.html index 973fd6d9..27d78d11 100644 --- a/machines/templates/machines/aff_txt.html +++ b/machines/templates/machines/aff_txt.html @@ -23,28 +23,29 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} - - - - - - - - - - {% for txt in txt_list %} +
    Zone concernéeEnregistrement
    + - - - + + + + + + {% for txt in txt_list %} + + + + - - {% endfor %} -
    {{ txt.zone }}{{ txt.dns_entry }} + Zone concernéeEnregistrement
    {{ txt.zone }}{{ txt.dns_entry }} {% can_edit txt %} {% include 'buttons/edit.html' with href='machines:edit-txt' id=txt.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='txt' id=txt.id %} -
    + {% acl_end %} + {% history_button txt %} + + + {% endfor %} + diff --git a/machines/templates/machines/aff_vlan.html b/machines/templates/machines/aff_vlan.html index de49c7a6..cbc57133 100644 --- a/machines/templates/machines/aff_vlan.html +++ b/machines/templates/machines/aff_vlan.html @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %}
    @@ -30,22 +31,22 @@ with this program; if not, write to the Free Software Foundation, Inc., - + - + {% for vlan in vlan_list %} - + - {% endfor %} diff --git a/machines/templates/machines/index_extension.html b/machines/templates/machines/index_extension.html index 6ee1bdff..19a0fca4 100644 --- a/machines/templates/machines/index_extension.html +++ b/machines/templates/machines/index_extension.html @@ -61,6 +61,18 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_end %} Supprimer un enregistrement TXT {% include "machines/aff_txt.html" with txt_list=txt_list %} + +

    DNAME records

    + {% can_create DName %} + + {% trans "Add a DNAME record" %} + + {% acl_end %} + + {% trans "Delete DNAME records" %} + + {% include "machines/aff_dname.html" with dname_list=dname_list %} +

    Liste des enregistrements SRV

    {% can_create Srv %} Ajouter un enregistrement SRV diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 0e7082f8..0c5a478a 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -57,6 +57,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if txtform %} {% bootstrap_form_errors txtform %} {% endif %} +{% if dnameform %} + {% bootstrap_form_errors dnameform %} +{% endif %} {% if srvform %} {% bootstrap_form_errors srvform %} {% endif %} @@ -122,6 +125,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,

    Enregistrement TXT

    {% bootstrap_form txtform %} {% endif %} + {% if dnameform %} +

    DNAME record

    + {% bootstrap_form dnameform %} + {% endif %} {% if srvform %}

    Enregistrement SRV

    {% massive_bootstrap_form srvform 'target' %} diff --git a/machines/urls.py b/machines/urls.py index 9a5fa25e..bf3d63d8 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -27,7 +27,6 @@ The defined URLs for the Cotisations app from __future__ import unicode_literals from django.conf.urls import url -import re2o from . import views urlpatterns = [ @@ -74,6 +73,9 @@ urlpatterns = [ url(r'^add_txt/$', views.add_txt, name='add-txt'), url(r'^edit_txt/(?P[0-9]+)$', views.edit_txt, name='edit-txt'), url(r'^del_txt/$', views.del_txt, name='del-txt'), + url(r'^add_dname/$', views.add_dname, name='add-dname'), + url(r'^edit_dname/(?P[0-9]+)$', views.edit_dname, name='edit-dname'), + url(r'^del_dname/$', views.del_dname, name='del-dname'), url(r'^add_ns/$', views.add_ns, name='add-ns'), url(r'^edit_ns/(?P[0-9]+)$', views.edit_ns, name='edit-ns'), url(r'^del_ns/$', views.del_ns, name='del-ns'), @@ -119,10 +121,6 @@ urlpatterns = [ url(r'^edit_nas/(?P[0-9]+)$', views.edit_nas, name='edit-nas'), url(r'^del_nas/$', views.del_nas, name='del-nas'), url(r'^index_nas/$', views.index_nas, name='index-nas'), - url(r'history/(?P\w+)/(?P[0-9]+)$', - re2o.views.history, - name='history', - kwargs={'application': 'machines'}), url(r'^$', views.index, name='index'), url(r'^rest/mac-ip/$', views.mac_ip, name='mac-ip'), url(r'^rest/regen-achieved/$', diff --git a/machines/views.py b/machines/views.py index 75c2f483..71484952 100644 --- a/machines/views.py +++ b/machines/views.py @@ -3,10 +3,11 @@ # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # -# Copyright © 2017 Gabriel Détraz +# Copyright © 2016-2018 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle -# Copyright © 2017 Maël Kervella +# Copyright © 2017-2018 Maël Kervella +# Copyright © 2018 Charlie Jacomme # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -93,6 +94,8 @@ from .forms import ( DelNsForm, TxtForm, DelTxtForm, + DNameForm, + DelDNameForm, MxForm, DelMxForm, VlanForm, @@ -122,6 +125,7 @@ from .models import ( Vlan, Nas, Txt, + DName, Srv, OuverturePortList, OuverturePort, @@ -815,6 +819,63 @@ def del_ns(request, instances): request ) +@login_required +@can_create(DName) +def add_dname(request): + """ View used to add a DName object """ + dname = DNameForm(request.POST or None) + if dname.is_valid(): + dname.save() + messages.success(request, "This DNAME record has been added") + return redirect(reverse('machines:index-extension')) + return form( + {'dnameform': dname, 'action_name': "Create"}, + 'machines/machine.html', + request + ) + + +@login_required +@can_edit(DName) +def edit_dname(request, dname_instance, **_kwargs): + """ View used to edit a DName object """ + dname = DNameForm(request.POST or None, instance=dname_instance) + if dname.is_valid(): + if dname.changed_data: + dname.save() + messages.success(request, "DName successfully edited") + return redirect(reverse('machines:index-extension')) + return form( + {'dnameform': dname, 'action_name': "Edit"}, + 'machines/machine.html', + request + ) + + +@login_required +@can_delete_set(DName) +def del_dname(request, instances): + """ View used to delete a DName object """ + dname = DelDNameForm(request.POST or None, instances=instances) + if dname.is_valid(): + dname_dels = dname.cleaned_data['dname'] + for dname_del in dname_dels: + try: + dname_del.delete() + messages.success(request, + "The DNAME %s has been deleted" % dname_del) + except ProtectedError: + messages.error( + request, + "The DNAME %s can not be deleted" % dname_del + ) + return redirect(reverse('machines:index-extension')) + return form( + {'dnameform': dname, 'action_name': 'Delete'}, + 'machines/machine.html', + request + ) + @login_required @can_create(Txt) @@ -1272,7 +1333,7 @@ def index_nas(request): @login_required -@can_view_all(SOA, Mx, Ns, Txt, Srv, Extension) +@can_view_all(SOA, Mx, Ns, Txt, DName, Srv, Extension) def index_extension(request): """ View displaying the list of existing extensions, the list of existing SOA records, the list of existing MX records , the list of @@ -1292,6 +1353,7 @@ def index_extension(request): .select_related('zone') .select_related('ns__extension')) txt_list = Txt.objects.all().select_related('zone') + dname_list = DName.objects.all().select_related('zone') srv_list = (Srv.objects .all() .select_related('extension') @@ -1305,6 +1367,7 @@ def index_extension(request): 'mx_list': mx_list, 'ns_list': ns_list, 'txt_list': txt_list, + 'dname_list': dname_list, 'srv_list': srv_list } ) diff --git a/preferences/forms.py b/preferences/forms.py index afe111a2..477d4d57 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -38,6 +38,7 @@ from .models import ( Service ) + class EditOptionalUserForm(ModelForm): """Formulaire d'édition des options de l'user. (solde, telephone..)""" class Meta: @@ -54,13 +55,6 @@ class EditOptionalUserForm(ModelForm): self.fields['is_tel_mandatory'].label = ( 'Exiger un numéro de téléphone' ) - self.fields['user_solde'].label = ( - 'Activation du solde pour les utilisateurs' - ) - self.fields['max_solde'].label = 'Solde maximum' - self.fields['min_online_payment'].label = ( - 'Montant de rechargement minimum en ligne' - ) self.fields['self_adhesion'].label = 'Auto inscription' diff --git a/preferences/migrations/0035_optionaluser_allow_self_subscription.py b/preferences/migrations/0035_optionaluser_allow_self_subscription.py new file mode 100644 index 00000000..5fc45714 --- /dev/null +++ b/preferences/migrations/0035_optionaluser_allow_self_subscription.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-17 15:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0034_auto_20180416_1120'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='allow_self_subscription', + field=models.BooleanField(default=False, help_text="Autoriser les utilisateurs à cotiser par eux mêmes via les moyens de paiement permettant l'auto-cotisation."), + ), + ] diff --git a/preferences/migrations/0039_auto_20180115_0003.py b/preferences/migrations/0039_auto_20180115_0003.py index 3dbe2b4c..f8da5c27 100644 --- a/preferences/migrations/0039_auto_20180115_0003.py +++ b/preferences/migrations/0039_auto_20180115_0003.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals from django.db import migrations, models -import preferences.aes_field class Migration(migrations.Migration): diff --git a/preferences/migrations/0040_auto_20180129_1745.py b/preferences/migrations/0040_auto_20180129_1745.py index dc7800f4..7e657079 100644 --- a/preferences/migrations/0040_auto_20180129_1745.py +++ b/preferences/migrations/0040_auto_20180129_1745.py @@ -3,7 +3,10 @@ from __future__ import unicode_literals from django.db import migrations, models -import preferences.aes_field +try: + import preferences.aes_field as aes_field +except ImportError: + import re2o.aes_field as aes_field class Migration(migrations.Migration): @@ -16,7 +19,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='assooption', name='payment_pass', - field=preferences.aes_field.AESEncryptedField(blank=True, max_length=255, null=True), + field=aes_field.AESEncryptedField(blank=True, max_length=255, null=True), ), migrations.AlterField( model_name='assooption', diff --git a/preferences/migrations/0044_remove_payment_pass.py b/preferences/migrations/0044_remove_payment_pass.py new file mode 100644 index 00000000..23d45145 --- /dev/null +++ b/preferences/migrations/0044_remove_payment_pass.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-05 13:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0035_optionaluser_allow_self_subscription'), + ] + + operations = [ + migrations.RemoveField( + model_name='assooption', + name='payment_pass', + ), + ] + diff --git a/preferences/migrations/0045_remove_unused_payment_fields.py b/preferences/migrations/0045_remove_unused_payment_fields.py new file mode 100644 index 00000000..6944f58c --- /dev/null +++ b/preferences/migrations/0045_remove_unused_payment_fields.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-05 13:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0044_remove_payment_pass'), + ('cotisations', '0030_custom_payment'), + ] + + operations = [ + migrations.RemoveField( + model_name='assooption', + name='payment', + ), + migrations.RemoveField( + model_name='assooption', + name='payment_id', + ), + migrations.RemoveField( + model_name='optionaluser', + name='allow_self_subscription', + ), + migrations.RemoveField( + model_name='optionaluser', + name='max_solde', + ), + migrations.RemoveField( + model_name='optionaluser', + name='min_online_payment', + ), + migrations.RemoveField( + model_name='optionaluser', + name='solde_negatif', + ), + migrations.RemoveField( + model_name='optionaluser', + name='user_solde', + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 560fa30f..d39b9f8e 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -31,12 +31,9 @@ from django.db.models.signals import post_save from django.dispatch import receiver from django.core.cache import cache -import cotisations.models import machines.models from re2o.mixins import AclMixin -from .aes_field import AESEncryptedField - class PreferencesModel(models.Model): """ Base object for the Preferences objects @@ -67,22 +64,6 @@ class OptionalUser(AclMixin, PreferencesModel): PRETTY_NAME = "Options utilisateur" is_tel_mandatory = models.BooleanField(default=True) - user_solde = models.BooleanField(default=False) - solde_negatif = models.DecimalField( - max_digits=5, - decimal_places=2, - default=0 - ) - max_solde = models.DecimalField( - max_digits=5, - decimal_places=2, - default=50 - ) - min_online_payment = models.DecimalField( - max_digits=5, - decimal_places=2, - default=10 - ) gpg_fingerprint = models.BooleanField(default=True) all_can_create_club = models.BooleanField( default=False, @@ -108,14 +89,6 @@ class OptionalUser(AclMixin, PreferencesModel): ("view_optionaluser", "Peut voir les options de l'user"), ) - def clean(self): - """Creation du mode de paiement par solde""" - if self.user_solde: - p = cotisations.models.Paiement.objects.filter(moyen="Solde") - if not len(p): - c = cotisations.models.Paiement(moyen="Solde") - c.save() - @receiver(post_save, sender=OptionalUser) def optionaluser_post_save(**kwargs): @@ -294,25 +267,6 @@ class AssoOption(AclMixin, PreferencesModel): blank=True, null=True ) - PAYMENT = ( - ('NONE', 'NONE'), - ('COMNPAY', 'COMNPAY'), - ) - payment = models.CharField( - max_length=255, - choices=PAYMENT, - default='NONE', - ) - payment_id = models.CharField( - max_length=255, - default='', - blank=True - ) - payment_pass = AESEncryptedField( - max_length=255, - null=True, - blank=True, - ) description = models.TextField( null=True, blank=True, diff --git a/preferences/templates/preferences/aff_service.html b/preferences/templates/preferences/aff_service.html index b4d4894c..89cfc641 100644 --- a/preferences/templates/preferences/aff_service.html +++ b/preferences/templates/preferences/aff_service.html @@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} {% load acl %} +{% load logs_extra %}
    Id NomCommentaireCommentaire Ranges ip
    {{ vlan.vlan_id }} {{ vlan.name }}{{ vlan.comment }}{{ vlan.comment }} {% for range in vlan.iptype_set.all %}{{ range }}, {% endfor%} - {% can_edit vlan %} + + {% can_edit vlan %} {% include 'buttons/edit.html' with href='machines:edit-vlan' id=vlan.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='machines:history' name='vlan' id=vlan.id %} + {% acl_end %} + {% history_button vlan %}
    @@ -43,7 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% can_edit service%} {% include 'buttons/edit.html' with href='preferences:edit-service' id=service.id %} {% acl_end %} - {% include 'buttons/history.html' with href='preferences:history' name='service' id=service.id %} + {% history_button service %} {% endfor %} diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html old mode 100644 new mode 100755 index 99e3e14f..34417695 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -25,218 +25,201 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load acl %} +{% load design %} {% block title %}Création et modification des préférences{% endblock %} {% block content %} -

    Préférences utilisateur

    - - - Editer - -

    -

    -
    +

    Préférences utilisateur

    + + + Editer + +

    +

    +
    - - - - + + + + - - - {% if useroptions.user_solde %} - - - {% endif %} + + + + - - - - + + + + - {% if useroptions.user_solde %} - - - - - - - {% endif %} +
    Téléphone obligatoirement requis{{ useroptions.is_tel_mandatory }}Activation du solde pour les utilisateurs{{ useroptions.user_solde }}Téléphone obligatoirement requis{{ useroptions.is_tel_mandatory|tick }}Auto inscription{{ useroptions.self_adhesion|tick }}
    Champ gpg fingerprint{{ useroptions.gpg_fingerprint }}Solde négatif{{ useroptions.solde_negatif }}Champ gpg fingerprint{{ useroptions.gpg_fingerprint|tick }}Shell par défaut des utilisateurs{{ useroptions.shell_default }}
    Creations d'adhérents par tous{{ useroptions.all_can_create_adherent }}Creations de clubs par tous{{ useroptions.all_can_create_club }}Creations d'adhérents par tous{{ useroptions.all_can_create_adherent|tick }}Creations de clubs par tous{{ useroptions.all_can_create_club|tick }}
    Solde maximum{{ useroptions.max_solde }}Montant minimal de rechargement en ligne{{ useroptions.min_online_payment }}
    + + +

    Préférences machines

    + + + Editer + +

    +

    + - - - - - -
    Auto inscription{{ useroptions.self_adhesion }}Shell par défaut des utilisateurs{{ useroptions.shell_default }}
    -

    Préférences machines

    - - - Editer - -

    -

    - - - - - - + + + + - - - - - - - - - -
    Mot de passe par machine{{ machineoptions.password_machine }}Machines/interfaces autorisées par utilisateurs{{ machineoptions.max_lambdauser_interfaces }}Mot de passe par machine{{ machineoptions.password_machine|tick }}Machines/interfaces autorisées par utilisateurs{{ machineoptions.max_lambdauser_interfaces }}
    Alias dns autorisé par utilisateur{{ machineoptions.max_lambdauser_aliases }}Support de l'ipv6{{ machineoptions.ipv6_mode }}
    Creation de machines{{ machineoptions.create_machine }}
    -

    Préférences topologie

    - - - Editer - -

    -

    - - - - - - + + + + - - - - + + -
    Politique générale de placement de vlan{{ topologieoptions.radius_general_policy }} Ce réglage défini la politique vlan après acceptation radius : soit sur le vlan de la plage d'ip de la machine, soit sur un vlan prédéfini dans "Vlan où placer les machines après acceptation RADIUS"Alias dns autorisé par utilisateur{{ machineoptions.max_lambdauser_aliases }}Support de l'ipv6{{ machineoptions.ipv6_mode }}
    Vlan où placer les machines après acceptation RADIUS{{ topologieoptions.vlan_decision_ok }}Vlan où placer les machines après rejet RADIUS{{ topologieoptions.vlan_decision_nok }}Creation de machines{{ machineoptions.create_machine|tick }}
    -

    Préférences generales

    - - - Editer - -

    -

    - +
    +

    Préférences topologie

    + + + Editer + +

    +

    + - - - - + + + + - - - - + + + + + +
    Nom du site web{{ generaloptions.site_name }}Adresse mail d'expedition automatique{{ generaloptions.email_from }}Politique générale de placement de vlan{{ topologieoptions.radius_general_policy }} Ce réglage défini la politique vlan après acceptation radius : soit sur le vlan de la plage d'ip de la machine, soit sur un vlan prédéfini dans "Vlan où placer les machines après acceptation RADIUS"
    Affichage de résultats dans le champ de recherche{{ generaloptions.search_display_page }}Nombre d'items affichés en liste (taille normale){{ generaloptions.pagination_number }}Vlan où placer les machines après acceptation RADIUS{{ topologieoptions.vlan_decision_ok }}Vlan où placer les machines après rejet RADIUS{{ topologieoptions.vlan_decision_nok }}
    +

    Préférences generales

    + + + Editer + +

    +

    + + + + + + - - - - - - - - - - - - - - -
    Nom du site web{{ generaloptions.site_name }}Adresse mail d'expedition automatique{{ generaloptions.email_from }}
    Nombre d'items affichés en liste (taille élevée){{ generaloptions.pagination_large_number }}Temps avant expiration du lien de reinitialisation de mot de passe (en heures){{ generaloptions.req_expire_hrs }}
    Message global affiché sur le site{{ generaloptions.general_message }}Résumé des CGU{{ generaloptions.GTU_sum_up }}
    CGU{{generaloptions.GTU}} -
    -

    Données de l'association

    - - - Editer - -

    -

    - - - - - - + + + + - - - - + + + + - - - - + + + + + + + + +
    Nom{{ assooptions.name }}SIRET{{ assooptions.siret }}Affichage de résultats dans le champ de recherche{{ generaloptions.search_display_page }}Nombre d'items affichés en liste (taille normale){{ generaloptions.pagination_number }}
    Adresse{{ assooptions.adresse1 }}
    - {{ assooptions.adresse2 }}
    Contact mail{{ assooptions.contact }}Nombre d'items affichés en liste (taille élevée){{ generaloptions.pagination_large_number }}Temps avant expiration du lien de reinitialisation de mot de passe (en heures){{ generaloptions.req_expire_hrs }}
    Telephone{{ assooptions.telephone }}Pseudo d'usage{{ assooptions.pseudo }}Message global affiché sur le site{{ generaloptions.general_message }}Résumé des CGU{{ generaloptions.GTU_sum_up }}
    CGU{{generaloptions.GTU}} +
    +

    Données de l'association

    + + + Editer + +

    +

    + + + + + + - - - - + + + + - - + + + + + + + + + + -
    Nom{{ assooptions.name }}SIRET{{ assooptions.siret }}
    Objet utilisateur de l'association{{ assooptions.utilisateur_asso }}Moyen de paiement automatique{{ assooptions.payment }}Adresse{{ assooptions.adresse1 }}
    + {{ assooptions.adresse2 }}
    Contact mail{{ assooptions.contact }}
    Description de l'association{{ assooptions.description | safe }}Telephone{{ assooptions.telephone }}Pseudo d'usage{{ assooptions.pseudo }}
    Objet utilisateur de l'association{{ assooptions.utilisateur_asso }}Description de l'association{{ assooptions.description | safe }}
    -

    Messages personalisé dans les mails

    - - - Editer - -

    -

    - +
    +

    Messages personalisé dans les mails

    + + + Editer + +

    +

    + - - - - - - + + -
    Mail de bienvenue (Français){{ mailmessageoptions.welcome_mail_fr | safe }}
    Mail de bienvenue (Anglais){{ mailmessageoptions.welcome_mail_en | safe }}Mail de bienvenue (Français){{ mailmessageoptions.welcome_mail_fr | safe }}
    -

    Liste des services et préférences page d'accueil

    - {% can_create preferences.Service%} - Ajouter un service - {% acl_end %} - Supprimer un ou plusieurs service - {% include "preferences/aff_service.html" with service_list=service_list %} + + Mail de bienvenue (Anglais) + {{ mailmessageoptions.welcome_mail_en | safe }} + + +

    Liste des services et préférences page d'accueil

    +{% can_create preferences.Service%} + Ajouter un service +{% acl_end %} + Supprimer un ou plusieurs service +{% include "preferences/aff_service.html" with service_list=service_list %} - - - Editer - -

    + + + Editer + +

    - +
    - - - - + + + + - - + + -
    Url du compte twitter{{ homeoptions.twitter_url }}Nom utilisé pour afficher le compte{{ homeoptions.twitter_account_name }}Url du compte twitter{{ homeoptions.twitter_url }}Nom utilisé pour afficher le compte{{ homeoptions.twitter_account_name }}
    Url du compte facebook{{ homeoptions.facebook_url }}Url du compte facebook{{ homeoptions.facebook_url }}
    -
    -
    -
    + +
    +
    +
    {% endblock %} diff --git a/preferences/templatetags/__init__.py b/preferences/templatetags/__init__.py new file mode 100644 index 00000000..86d112b2 --- /dev/null +++ b/preferences/templatetags/__init__.py @@ -0,0 +1,19 @@ +#re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2017 Maël Kervella +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. diff --git a/preferences/urls.py b/preferences/urls.py index bca7bb1e..ec35cc41 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -27,7 +27,6 @@ from __future__ import unicode_literals from django.conf.urls import url -import re2o from . import views @@ -74,11 +73,5 @@ urlpatterns = [ name='edit-service' ), url(r'^del_services/$', views.del_services, name='del-services'), - url( - r'^history/(?P\w+)/(?P[0-9]+)$', - re2o.views.history, - name='history', - kwargs={'application': 'preferences'}, - ), url(r'^$', views.display_options, name='display-options'), ] diff --git a/preferences/aes_field.py b/re2o/aes_field.py similarity index 72% rename from preferences/aes_field.py rename to re2o/aes_field.py index 302aa82b..2720f5af 100644 --- a/preferences/aes_field.py +++ b/re2o/aes_field.py @@ -7,6 +7,7 @@ # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle # Copyright © 2018 Maël Kervella +# Copyright © 2018 Hugo Levy-Falk # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,10 +23,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -# App de gestion des machines pour re2o -# Gabriel Détraz, Augustin Lemesle -# Gplv2 -"""preferences.aes_field +""" Module defining a AESEncryptedField object that can be used in forms to handle the use of properly encrypting and decrypting AES keys """ @@ -36,6 +34,7 @@ from random import choice from Crypto.Cipher import AES from django.db import models +from django import forms from django.conf import settings EOD = '`%EofD%`' # This should be something that will not occur in strings @@ -66,18 +65,35 @@ def decrypt(key, s): return ss.split(bytes(EOD, 'utf-8'))[0] +class AESEncryptedFormField(forms.CharField): + widget = forms.PasswordInput(render_value=True) + + class AESEncryptedField(models.CharField): """ A Field that can be used in forms for adding the support of AES ecnrypted fields """ + def save_form_data(self, instance, data): - setattr(instance, self.name, - binascii.b2a_base64(encrypt(settings.AES_KEY, data))) + setattr(instance, self.name, binascii.b2a_base64( + encrypt(settings.AES_KEY, data)).decode('utf-8')) def to_python(self, value): if value is None: return None - return decrypt(settings.AES_KEY, - binascii.a2b_base64(value)).decode('utf-8') + try: + return decrypt(settings.AES_KEY, + binascii.a2b_base64(value)).decode('utf-8') + except Exception as e: + raise ValueError(value) + + def from_db_value(self, value, *args, **kwargs): + if value is None: + return value + try: + return decrypt(settings.AES_KEY, + binascii.a2b_base64(value)).decode('utf-8') + except Exception as e: + raise ValueError(value) def get_prep_value(self, value): if value is None: @@ -85,4 +101,9 @@ class AESEncryptedField(models.CharField): return binascii.b2a_base64(encrypt( settings.AES_KEY, value - )) + )).decode('utf-8') + + def formfield(self, **kwargs): + defaults = {'form_class': AESEncryptedFormField} + defaults.update(kwargs) + return super().formfield(**defaults) diff --git a/re2o/context_processors.py b/re2o/context_processors.py index a2e205c2..ceb03be2 100644 --- a/re2o/context_processors.py +++ b/re2o/context_processors.py @@ -21,8 +21,10 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Fonction de context, variables renvoyées à toutes les vues""" - from __future__ import unicode_literals + +import datetime + from django.contrib import messages from preferences.models import GeneralOption, OptionalMachine @@ -47,3 +49,12 @@ def context_user(request): 'name_website': GeneralOption.get_cached_value('site_name'), 'ipv6_enabled': OptionalMachine.get_cached_value('ipv6'), } + + +def date_now(request): + """Add the current date in the context for quick informations and + comparisons""" + return { + 'now_aware': datetime.datetime.now(datetime.timezone.utc), + 'now_naive': datetime.datetime.now() + } diff --git a/re2o/settings.py b/re2o/settings.py index 1117dd77..71bd266f 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -125,6 +125,7 @@ TEMPLATES = [ 'django.contrib.messages.context_processors.messages', 'django.template.context_processors.request', 're2o.context_processors.context_user', + 're2o.context_processors.date_now', ], }, }, diff --git a/re2o/templatetags/acl.py b/re2o/templatetags/acl.py index 11399633..fe13c5ac 100644 --- a/re2o/templatetags/acl.py +++ b/re2o/templatetags/acl.py @@ -74,83 +74,42 @@ import sys from django import template from django.template.base import Node, NodeList +from django.contrib.contenttypes.models import ContentType -import cotisations -import machines -import preferences -import topologie -import users register = template.Library() -MODEL_NAME = { - # cotisations - 'Facture': cotisations.models.Facture, - 'Vente': cotisations.models.Vente, - 'Article': cotisations.models.Article, - 'Banque': cotisations.models.Banque, - 'Paiement': cotisations.models.Paiement, - 'Cotisation': cotisations.models.Cotisation, - # machines - 'Machine': machines.models.Machine, - 'MachineType': machines.models.MachineType, - 'IpType': machines.models.IpType, - 'Vlan': machines.models.Vlan, - 'Nas': machines.models.Nas, - 'SOA': machines.models.SOA, - 'Extension': machines.models.Extension, - 'Mx': machines.models.Mx, - 'Ns': machines.models.Ns, - 'Txt': machines.models.Txt, - 'Srv': machines.models.Srv, - 'Interface': machines.models.Interface, - 'Domain': machines.models.Domain, - 'IpList': machines.models.IpList, - 'Ipv6List': machines.models.Ipv6List, - 'machines.Service': machines.models.Service, - 'Service_link': machines.models.Service_link, - 'OuverturePortList': machines.models.OuverturePortList, - 'OuverturePort': machines.models.OuverturePort, - # preferences - 'OptionalUser': preferences.models.OptionalUser, - 'OptionalMachine': preferences.models.OptionalMachine, - 'OptionalTopologie': preferences.models.OptionalTopologie, - 'GeneralOption': preferences.models.GeneralOption, - 'preferences.Service': preferences.models.Service, - 'AssoOption': preferences.models.AssoOption, - 'MailMessageOption': preferences.models.MailMessageOption, - # topologie - 'Stack': topologie.models.Stack, - 'Switch': topologie.models.Switch, - 'AccessPoint': topologie.models.AccessPoint, - 'ModelSwitch': topologie.models.ModelSwitch, - 'ConstructorSwitch': topologie.models.ConstructorSwitch, - 'Port': topologie.models.Port, - 'Room': topologie.models.Room, - 'Building': topologie.models.Building, - 'SwitchBay': topologie.models.SwitchBay, - 'PortProfile': topologie.models.PortProfile, - # users - 'User': users.models.User, - 'Adherent': users.models.Adherent, - 'Club': users.models.Club, - 'ServiceUser': users.models.ServiceUser, - 'School': users.models.School, - 'ListRight': users.models.ListRight, - 'ListShell': users.models.ListShell, - 'Ban': users.models.Ban, - 'Whitelist': users.models.Whitelist, -} - def get_model(model_name): """Retrieve the model object from its name""" + splitted = model_name.split('.') + if len(splitted) > 1: + try: + app_label, name = splitted + except ValueError: + raise template.TemplateSyntaxError( + "%r is an inconsistent model name" % model_name + ) + else: + app_label, name = None, splitted[0] try: - return MODEL_NAME[model_name] - except KeyError: + if app_label is not None: + content_type = ContentType.objects.get( + model=name.lower(), + app_label=app_label + ) + else: + content_type = ContentType.objects.get(model=name.lower()) + except ContentType.DoesNotExist: raise template.TemplateSyntaxError( "%r is not a valid model for an acl tag" % model_name ) + except ContentType.MultipleObjectsReturned: + raise template.TemplateSyntaxError( + "More than one model found for %r. Try with `app.model`." + % model_name + ) + return content_type.model_class() def get_callback(tag_name, obj=None): diff --git a/re2o/templatetags/design.py b/re2o/templatetags/design.py new file mode 100644 index 00000000..87a0e0f8 --- /dev/null +++ b/re2o/templatetags/design.py @@ -0,0 +1,39 @@ +#re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2017 Maël Kervella +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django import template +from django.utils.html import conditional_escape +from django.utils.safestring import mark_safe + +register = template.Library() + + +@register.filter(needs_autoescape=False) +def tick(valeur, autoescape=False): + + if isinstance(valeur,bool): + if valeur == True: + result = '' + else: + result = '' + return mark_safe(result) + + else: # if the value is not a boolean, display it as if tick was not called + return valeur diff --git a/re2o/urls.py b/re2o/urls.py index 3322e82b..87a5b346 100644 --- a/re2o/urls.py +++ b/re2o/urls.py @@ -50,6 +50,7 @@ from django.contrib.auth import views as auth_views from .views import index, about_page handler500 = 're2o.views.handler500' +handler404 = 're2o.views.handler404' urlpatterns = [ url(r'^$', index, name='index'), diff --git a/re2o/views.py b/re2o/views.py index d803505e..9a9dec78 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -26,33 +26,20 @@ les views from __future__ import unicode_literals -from itertools import chain import git -from reversion.models import Version -from django.http import Http404 -from django.urls import reverse -from django.shortcuts import render, redirect +from django.shortcuts import render from django.template.context_processors import csrf -from django.contrib.auth.decorators import login_required -from django.contrib import messages from django.conf import settings from django.utils.translation import ugettext as _ from django.views.decorators.cache import cache_page -import preferences from preferences.models import ( Service, - GeneralOption, AssoOption, HomeOption ) -import users -import cotisations -import topologie -import machines -from .utils import re2o_paginator from .contributors import CONTRIBUTORS @@ -73,119 +60,12 @@ def index(request): twitter_account_name = HomeOption.get_cached_value('twitter_account_name') asso_name = AssoOption.get_cached_value('pseudo') return form({ - 'services_urls': services, - 'twitter_url': twitter_url, - 'twitter_account_name' : twitter_account_name, - 'facebook_url': facebook_url, - 'asso_name': asso_name - }, 're2o/index.html', request) - - -#: Binding the corresponding char sequence of history url to re2o models. -HISTORY_BIND = { - 'users': { - 'user': users.models.User, - 'ban': users.models.Ban, - 'whitelist': users.models.Whitelist, - 'school': users.models.School, - 'listright': users.models.ListRight, - 'serviceuser': users.models.ServiceUser, - 'listshell': users.models.ListShell, - }, - 'preferences': { - 'service': preferences.models.Service, - }, - 'cotisations': { - 'facture': cotisations.models.Facture, - 'article': cotisations.models.Article, - 'paiement': cotisations.models.Paiement, - 'banque': cotisations.models.Banque, - }, - 'topologie': { - 'switch': topologie.models.Switch, - 'port': topologie.models.Port, - 'room': topologie.models.Room, - 'stack': topologie.models.Stack, - 'modelswitch': topologie.models.ModelSwitch, - 'constructorswitch': topologie.models.ConstructorSwitch, - 'accesspoint': topologie.models.AccessPoint, - 'switchbay': topologie.models.SwitchBay, - 'building': topologie.models.Building, - 'portprofile': topologie.models.PortProfile, - }, - 'machines': { - 'machine': machines.models.Machine, - 'interface': machines.models.Interface, - 'domain': machines.models.Domain, - 'machinetype': machines.models.MachineType, - 'iptype': machines.models.IpType, - 'extension': machines.models.Extension, - 'soa': machines.models.SOA, - 'mx': machines.models.Mx, - 'txt': machines.models.Txt, - 'srv': machines.models.Srv, - 'ns': machines.models.Ns, - 'service': machines.models.Service, - 'vlan': machines.models.Vlan, - 'nas': machines.models.Nas, - 'ipv6list': machines.models.Ipv6List, - }, -} - - -@login_required -def history(request, application, object_name, object_id): - """Render history for a model. - - The model is determined using the `HISTORY_BIND` dictionnary if none is - found, raises a Http404. The view checks if the user is allowed to see the - history using the `can_view` method of the model. - - Args: - request: The request sent by the user. - object_name: Name of the model. - object_id: Id of the object you want to acces history. - - Returns: - The rendered page of history if access is granted, else the user is - redirected to their profile page, with an error message. - - Raises: - Http404: This kind of models doesn't have history. - """ - try: - model = HISTORY_BIND[application][object_name] - except KeyError: - raise Http404(u"Il n'existe pas d'historique pour ce modèle.") - object_name_id = object_name + 'id' - kwargs = {object_name_id: object_id} - try: - instance = model.get_instance(**kwargs) - except model.DoesNotExist: - messages.error(request, u"Entrée inexistante") - return redirect(reverse( - 'users:profil', - kwargs={'userid': str(request.user.id)} - )) - can, msg = instance.can_view(request.user) - if not can: - messages.error(request, msg or "Vous ne pouvez pas accéder à ce menu") - return redirect(reverse( - 'users:profil', - kwargs={'userid': str(request.user.id)} - )) - pagination_number = GeneralOption.get_cached_value('pagination_number') - reversions = Version.objects.get_for_object(instance) - if hasattr(instance, 'linked_objects'): - for related_object in chain(instance.linked_objects()): - reversions = (reversions | - Version.objects.get_for_object(related_object)) - reversions = re2o_paginator(request, reversions, pagination_number) - return render( - request, - 're2o/history.html', - {'reversions': reversions, 'object': instance} - ) + 'services_urls': services, + 'twitter_url': twitter_url, + 'twitter_account_name': twitter_account_name, + 'facebook_url': facebook_url, + 'asso_name': asso_name + }, 're2o/index.html', request) @cache_page(7 * 24 * 60 * 60) @@ -230,3 +110,8 @@ def about_page(request): def handler500(request): """The handler view for a 500 error""" return render(request, 'errors/500.html') + + +def handler404(request): + """The handler view for a 404 error""" + return render(request, 'errors/404.html') diff --git a/static/css/base.css b/static/css/base.css index f6240970..2b44e95c 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -108,3 +108,9 @@ footer a { overflow-y: visible; } +/* Make modal wider on wide screens */ +@media (min-width: 1024px) { + .modal-dialog { + width: 1000px + } +} diff --git a/templates/buttons/history.html b/templates/buttons/history.html index 4b6bcc86..fa7835da 100644 --- a/templates/buttons/history.html +++ b/templates/buttons/history.html @@ -21,7 +21,8 @@ You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} - - +{% load i18n %} + + {% if text %}{% trans 'History' %}{% endif %} diff --git a/templates/errors/404.html b/templates/errors/404.html new file mode 100644 index 00000000..fdf5ec77 --- /dev/null +++ b/templates/errors/404.html @@ -0,0 +1,227 @@ +{% comment %} +Re2o est un logiciel d'administration développé initiallement au rezometz. Il +se veut agnostique au réseau considéré, de manière à être installable en +quelques clics. + +Copyright © 2018 Hugo Levy-Falk + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +{% endcomment %} + +{% load i18n %} +{% load staticfiles %} + + + + + + + + + + + + + + + 404, Page not Found + + + + + +

    {% trans "Yup, that's a 404 error."%} {% trans "(Go to a known place)"%}

    + + {%trans "Your browser does not support the HTML5 canvas tag."%} + + + + + diff --git a/topologie/forms.py b/topologie/forms.py index ba37d395..86b5c541 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -89,15 +89,7 @@ class EditPortForm(FormRevMixin, ModelForm): self.fields['machine_interface'].queryset = ( Interface.objects.all().select_related('domain__extension') ) - self.fields['related'].queryset = ( - Port.objects.all() - .prefetch_related(Prefetch( - 'switch__interface_set', - queryset=(Interface.objects - .select_related('ipv4__ip_type__extension') - .select_related('domain__extension')) - )) - ) + self.fields['related'].queryset = Port.objects.all().prefetch_related('switch__machine_ptr__interface_set__domain__extension') class AddPortForm(FormRevMixin, ModelForm): diff --git a/topologie/migrations/0030_auto_20171004_0235.py b/topologie/migrations/0030_auto_20171004_0235.py index 83f3b022..ac3ceeba 100644 --- a/topologie/migrations/0030_auto_20171004_0235.py +++ b/topologie/migrations/0030_auto_20171004_0235.py @@ -10,6 +10,7 @@ class Migration(migrations.Migration): dependencies = [ ('topologie', '0029_auto_20171002_0334'), + ('machines', '0049_vlan'), ] operations = [ diff --git a/topologie/models.py b/topologie/models.py index bbcab749..a0333d46 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -282,8 +282,12 @@ class Switch(AclMixin, Machine): """ Returns the 'main' interface of the switch """ return self.interface_set.first() + @cached_property + def get_name(self): + return self.name or self.main_interface().domain.name + def __str__(self): - return str(self.main_interface()) + return str(self.get_name) class ModelSwitch(AclMixin, RevMixin, models.Model): diff --git a/topologie/templates/topologie/aff_ap.html b/topologie/templates/topologie/aff_ap.html index fc38b4b1..18a419d3 100644 --- a/topologie/templates/topologie/aff_ap.html +++ b/topologie/templates/topologie/aff_ap.html @@ -23,52 +23,51 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %}
    - {% if ap_list.paginator %} - {% include "pagination.html" with list=ap_list %} - {% endif %} + {% if ap_list.paginator %} + {% include "pagination.html" with list=ap_list %} + {% endif %} - - - - - - - - - - - - {% for ap in ap_list %} - - - - - - - - - {% endfor %} -
    {% include "buttons/sort.html" with prefix='ap' col='name' text='Borne' %}{% include "buttons/sort.html" with prefix='ap' col='mac' text='Addresse mac' %}{% include "buttons/sort.html" with prefix='ap' col='ip' text='Ipv4' %}CommentaireLocalisation
    {{ap.interface_set.first}}{{ap.interface_set.first.mac_address}}{{ap.interface_set.first.ipv4}}{{ap.interface_set.first.details}}{{ap.location}} - - - - {% can_edit ap %} - - - - {% acl_end %} - {% can_delete ap %} - - - - {% acl_end %} -
    + + + + + + + + + + + + {% for ap in ap_list %} + + + + + + + + + {% endfor %} +
    {% include "buttons/sort.html" with prefix='ap' col='name' text='Borne' %}{% include "buttons/sort.html" with prefix='ap' col='mac' text='Addresse mac' %}{% include "buttons/sort.html" with prefix='ap' col='ip' text='Ipv4' %}CommentaireLocalisation
    {{ap.interface_set.first}}{{ap.interface_set.first.mac_address}}{{ap.interface_set.first.ipv4}}{{ap.interface_set.first.details}}{{ap.location}} + {% history_button ap %} + {% can_edit ap %} + + + + {% acl_end %} + {% can_delete ap %} + + + + {% acl_end %} +
    - {% if ap_list.paginator %} -{% include "pagination.html" with list=ap_list %} -{% endif %} + {% if ap_list.paginator %} + {% include "pagination.html" with list=ap_list %} + {% endif %}
    diff --git a/topologie/templates/topologie/aff_building.html b/topologie/templates/topologie/aff_building.html index 878c85e8..f0e02135 100644 --- a/topologie/templates/topologie/aff_building.html +++ b/topologie/templates/topologie/aff_building.html @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} {% if building_list.paginator %} {% include "pagination.html" with list=building_list %} @@ -34,27 +35,25 @@ with this program; if not, write to the Free Software Foundation, Inc., {% include "buttons/sort.html" with prefix='building' col='name' text='Bâtiment' %} - - {% for building in building_list %} - - {{building.name}} + + {% for building in building_list %} + + {{building.name}} - - - - {% can_edit building %} + {% history_button building %} + {% can_edit building %} - {% acl_end %} - {% can_delete building %} + {% acl_end %} + {% can_delete building %} - {% acl_end %} + {% acl_end %} - - {% endfor %} + + {% endfor %} {% if building_list.paginator %} diff --git a/topologie/templates/topologie/aff_chambres.html b/topologie/templates/topologie/aff_chambres.html index 3ca4651d..f71d6927 100644 --- a/topologie/templates/topologie/aff_chambres.html +++ b/topologie/templates/topologie/aff_chambres.html @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} {% if room_list.paginator %} {% include "pagination.html" with list=room_list %} @@ -35,28 +36,26 @@ with this program; if not, write to the Free Software Foundation, Inc., Commentaire - - {% for room in room_list %} - - {{room.name}} - {{room.details}} + + {% for room in room_list %} + + {{room.name}} + {{room.details}} - - - - {% can_edit room %} + {% history_button room %} + {% can_edit room %} - {% acl_end %} - {% can_delete room %} + {% acl_end %} + {% can_delete room %} - {% acl_end %} + {% acl_end %} - - {% endfor %} + + {% endfor %} {% if room_list.paginator %} diff --git a/topologie/templates/topologie/aff_constructor_switch.html b/topologie/templates/topologie/aff_constructor_switch.html index 76c74734..1b9f5d19 100644 --- a/topologie/templates/topologie/aff_constructor_switch.html +++ b/topologie/templates/topologie/aff_constructor_switch.html @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} {% if constructor_switch_list.paginator %} {% include "pagination.html" with list=constructor_switch_list %} @@ -34,27 +35,25 @@ with this program; if not, write to the Free Software Foundation, Inc., {% include "buttons/sort.html" with prefix='constructor-switch' col='name' text='Constructeur' %} - - {% for constructor_switch in constructor_switch_list %} - - {{constructor_switch}} + + {% for constructor_switch in constructor_switch_list %} + + {{constructor_switch}} - - - - {% can_edit constructor_switch %} + {% history_button constructor_switch %} + {% can_edit constructor_switch %} - {% acl_end %} - {% can_delete constructor_switch %} + {% acl_end %} + {% can_delete constructor_switch %} - {% acl_end %} + {% acl_end %} - - {% endfor %} + + {% endfor %} {% if constructor_switch_list.paginator %} diff --git a/topologie/templates/topologie/aff_model_switch.html b/topologie/templates/topologie/aff_model_switch.html index 6bb3e1f0..3a62024f 100644 --- a/topologie/templates/topologie/aff_model_switch.html +++ b/topologie/templates/topologie/aff_model_switch.html @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} {% if model_switch_list.paginator %} {% include "pagination.html" with list=model_switch_list %} @@ -33,30 +34,28 @@ with this program; if not, write to the Free Software Foundation, Inc., {% include "buttons/sort.html" with prefix='model-switch' col='reference' text='Référence' %} {% include "buttons/sort.html" with prefix='model-switch' col='constructor' text='Constructeur' %} - + - - {% for model_switch in model_switch_list %} - - {{model_switch.reference}} - {{model_switch.constructor}} - - - - - {% can_edit model_switch %} + + {% for model_switch in model_switch_list %} + + {{model_switch.reference}} + {{model_switch.constructor}} + + {% history_button model_switch %} + {% can_edit model_switch %} - {% acl_end %} - {% can_delete model_switch %} + {% acl_end %} + {% can_delete model_switch %} - {% acl_end %} + {% acl_end %} - - {% endfor %} + + {% endfor %} {% if model_switch_list.paginator %} diff --git a/topologie/templates/topologie/aff_port.html b/topologie/templates/topologie/aff_port.html index 86216f15..44e6ec82 100644 --- a/topologie/templates/topologie/aff_port.html +++ b/topologie/templates/topologie/aff_port.html @@ -23,10 +23,23 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %}
    - - +
    + + + + + + + + + + + + + {% for port in port_list %} @@ -49,16 +62,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% can_view port.machine_interface.machine.user %} {{ port.machine_interface }} - - {% acl_else %} - {{ port.machine_interface }} - {% acl_end %} - {% endif %} - - +
    {% include "buttons/sort.html" with prefix='port' col='port' text='Port' %}{% include "buttons/sort.html" with prefix='port' col='room' text='Room' %}{% include "buttons/sort.html" with prefix='port' col='interface' text='Interface machine' %}{% include "buttons/sort.html" with prefix='port' col='related' text='Related' %}{% include "buttons/sort.html" with prefix='port' col='radius' text='Radius' %}{% include "buttons/sort.html" with prefix='port' col='vlan' text='Vlan forcé' %}Détails
    {% include "buttons/sort.html" with prefix='port' col='port' text='Port' %} {% include "buttons/sort.html" with prefix='port' col='room' text='Room' %} - {% if port.related %} - {% can_view port.related.switch %} - + {% acl_end %} + {% endif %} + + {% if port.related %} + {% can_view port.related.switch %} + + {{ port.related }} + + {% acl_else %} {{ port.related }} {% acl_else %} diff --git a/topologie/templates/topologie/aff_repr_switch.html b/topologie/templates/topologie/aff_repr_switch.html index 236dee56..f9e6e648 100644 --- a/topologie/templates/topologie/aff_repr_switch.html +++ b/topologie/templates/topologie/aff_repr_switch.html @@ -27,90 +27,90 @@ with this program; if not, write to the Free Software Foundation, Inc.,
    - + - {% for port in port_list|slice:"::2" %} - - {% endfor %} + {% for port in port_list|slice:"::2" %} + + {% endfor %} - - + + - {% for port in port_list|slice:"::2" %} - {% if port.room %} - - {% elif port.machine_interface %} - + {% elif port.machine_interface %} + - {% elif port.related%} - + {% elif port.related%} + - {% else %} - - {% endif %} - {% endfor %} - - + {% acl_end %} + + {% else %} + + {% endif %} + {% endfor %} - + - {% for port in port_list|slice:"1::2" %} - - {% endfor %} + - - + {% for port in port_list|slice:"1::2" %} + + {% endfor %} - {% for port in port_list|slice:"1::2" %} - {% if port.room %} - - {% elif port.machine_interface %} - + + + {% for port in port_list|slice:"1::2" %} + {% if port.room %} + + {% elif port.machine_interface %} + - {% elif port.related%} - + {% elif port.related%} + - {% else %} - - {% endif %} - {% endfor %} - - + {% acl_end %} + + {% else %} + + {% endif %} + {% endfor %} + +
    {{ port.port }}{{ port.port }}
    - {{ port.room }} - - {% can_view port.machine_interface.machine.user %} - + {% for port in port_list|slice:"::2" %} + {% if port.room %} + + {{ port.room }} + + {% can_view port.machine_interface.machine.user %} + + {{ port.machine_interface }} + + {% acl_else %} {{ port.machine_interface }} - - {% acl_else %} - {{ port.machine_interface }} - {% acl_end %} - - {% can_view port.related.switch %} - + {% acl_end %} + + {% can_view port.related.switch %} + + {{ port.related }} + + {% acl_else %} {{ port.related }} - - {% acl_else %} - {{ port.related }} - {% acl_end %} - - Vide -
    + Vide +
    {{ port.port }}
    {{ port.port }} - {{ port.room }} - - {% can_view port.machine_interface.machine.user %} - +
    + {{ port.room }} + + {% can_view port.machine_interface.machine.user %} + + {{ port.machine_interface }} + + {% acl_else %} {{ port.machine_interface }} - - {% acl_else %} - {{ port.machine_interface }} - {% acl_end %} - - {% can_view port.related.switch %} - + {% acl_end %} + + {% can_view port.related.switch %} + + {{ port.related }} + + {% acl_else %} {{ port.related }} - - {% acl_else %} - {{ port.related }} - {% acl_end %} - - Vide -
    + Vide +
    diff --git a/topologie/templates/topologie/aff_stacks.html b/topologie/templates/topologie/aff_stacks.html index 969c1075..4dd0f6ea 100644 --- a/topologie/templates/topologie/aff_stacks.html +++ b/topologie/templates/topologie/aff_stacks.html @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} @@ -33,29 +34,27 @@ with this program; if not, write to the Free Software Foundation, Inc., - - {% for stack in stack_list %} - - - - - - + + + + + - - {% endfor %} + + {% endfor %}
    Membres
    {{ stack.name }}{{stack.stack_id}}{{stack.details}}{% for switch in stack.switch_set.all %}{{switch }} {% endfor %} - - - - {% can_edit stack %} - - - - {% acl_end %} - {% can_delete stack %} - - - - {% acl_end %} + + {% for stack in stack_list %} +
    {{ stack.name }}{{stack.stack_id}}{{stack.details}}{% for switch in stack.switch_set.all %}{{switch }} {% endfor %} + {% history_button stack %} + {% can_edit stack %} + + + + {% acl_end %} + {% can_delete stack %} + + + + {% acl_end %}
    diff --git a/topologie/templates/topologie/aff_switch.html b/topologie/templates/topologie/aff_switch.html index 799225f0..1f4dcbcf 100644 --- a/topologie/templates/topologie/aff_switch.html +++ b/topologie/templates/topologie/aff_switch.html @@ -23,59 +23,60 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %}
    -{% if switch_list.paginator %} -{% include "pagination.html" with list=switch_list %} -{% endif %} + {% if switch_list.paginator %} + {% include "pagination.html" with list=switch_list %} + {% endif %} - - +
    + + + + + + + + + + + + + + {% for switch in switch_list %} - - - - - - - - - + + + + + + + + + - - {% for switch in switch_list %} - - - - - - - - - - - - {% endfor %} -
    {% include "buttons/sort.html" with prefix='switch' col='dns' text='Dns' %}{% include "buttons/sort.html" with prefix='switch' col='ip' text='Ipv4' %}{% include "buttons/sort.html" with prefix='switch' col='loc' text='Emplacement' %}{% include "buttons/sort.html" with prefix='switch' col='ports' text='Ports' %}{% include "buttons/sort.html" with prefix='switch' col='stack' text='Stack' %}Id stackModèleDétails
    {% include "buttons/sort.html" with prefix='switch' col='dns' text='Dns' %}{% include "buttons/sort.html" with prefix='switch' col='ip' text='Ipv4' %}{% include "buttons/sort.html" with prefix='switch' col='loc' text='Emplacement' %}{% include "buttons/sort.html" with prefix='switch' col='ports' text='Ports' %}{% include "buttons/sort.html" with prefix='switch' col='stack' text='Stack' %}Id stackModèleDétails + + {{switch}} + + {{switch.interface_set.first.ipv4}}{{switch.switchbay}}{{switch.number}}{{switch.stack.name}}{{switch.stack_member_id}}{{switch.model}}{{switch.interface_set.first.details}} + {% history_button switch %} + {% can_edit switch %} + {% include 'buttons/edit.html' with href='topologie:edit-switch' id=switch.pk %} + {% acl_end %} + {% can_delete switch %} + {% include 'buttons/suppr.html' with href='machines:del-machine' id=switch.id %} + {% acl_end %} + {% can_create Port %} + {% include 'buttons/add.html' with href='topologie:create-ports' id=switch.pk desc='Création de ports'%} + {% acl_end %} +
    - - {{switch}} - - {{switch.interface_set.first.ipv4}}{{switch.switchbay}}{{switch.number}}{{switch.stack.name}}{{switch.stack_member_id}}{{switch.model}}{{switch.interface_set.first.details}} - {% include 'buttons/history.html' with href='topologie:history' name='switch' id=switch.pk%} - {% can_edit switch %} - {% include 'buttons/edit.html' with href='topologie:edit-switch' id=switch.pk %} - {% acl_end %} - {% can_delete switch %} - {% include 'buttons/suppr.html' with href='machines:del-machine' id=switch.id %} - {% acl_end %} - {% can_create Port %} - {% include 'buttons/add.html' with href='topologie:create-ports' id=switch.pk desc='Création de ports'%} - {% acl_end %} -
    + {% endfor %} +
    -{% if switch_list.paginator %} -{% include "pagination.html" with list=switch_list %} -{% endif %} + {% if switch_list.paginator %} + {% include "pagination.html" with list=switch_list %} + {% endif %}
    diff --git a/topologie/templates/topologie/aff_switch_bay.html b/topologie/templates/topologie/aff_switch_bay.html index 881a63e0..c73bdf5f 100644 --- a/topologie/templates/topologie/aff_switch_bay.html +++ b/topologie/templates/topologie/aff_switch_bay.html @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} {% if switch_bay_list.paginator %} {% include "pagination.html" with list=switch_bay_list %} @@ -32,35 +33,33 @@ with this program; if not, write to the Free Software Foundation, Inc., {% include "buttons/sort.html" with prefix='switch-bay' col='name' text='Baie' %} - {% include "buttons/sort.html" with prefix='switch-bay' col='building' text='Bâtiment' %} - Info particulières - Switchs de la baie + {% include "buttons/sort.html" with prefix='switch-bay' col='building' text='Bâtiment' %} + Info particulières + Switchs de la baie - - {% for switch_bay in switch_bay_list %} - - {{switch_bay.name}} - {{switch_bay.building}} - {{switch_bay.info}} - {% for switch in switch_bay.switch_set.all %}{{switch }} {% endfor %} - - - - - {% can_edit switch_bay %} + + {% for switch_bay in switch_bay_list %} + + {{switch_bay.name}} + {{switch_bay.building}} + {{switch_bay.info}} + {% for switch in switch_bay.switch_set.all %}{{switch }} {% endfor %} + + {% history_button switch_bay %} + {% can_edit switch_bay %} - {% acl_end %} - {% can_delete switch_bay %} + {% acl_end %} + {% can_delete switch_bay %} - {% acl_end %} + {% acl_end %} - - {% endfor %} + + {% endfor %} {% if switch_bay_list.paginator %} diff --git a/topologie/templates/topologie/edit_stack_sw.html b/topologie/templates/topologie/edit_stack_sw.html index 75faaf84..37a25cd0 100644 --- a/topologie/templates/topologie/edit_stack_sw.html +++ b/topologie/templates/topologie/edit_stack_sw.html @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load logs_extra %} @@ -31,27 +32,25 @@ with this program; if not, write to the Free Software Foundation, Inc., - - {% for stack in stack_list %} - - - - - + + + + - - {% endfor %} + + {% endfor %}
    ID Details
    {{stack.name}}{{stack.stack_id}}{{stack.details}} - - - - {% can_edit stack %} - + + {% for stack in stack_list %} +
    {{stack.name}}{{stack.stack_id}}{{stack.details}} + {% history_button stack %} + {% can_edit stack %} + - - {% acl_end %} - {% can_delete stack %} - - - - {% acl_end %} + + {% acl_end %} + {% can_delete stack %} + + + + {% acl_end %}
    diff --git a/topologie/urls.py b/topologie/urls.py index a827acf2..c314c800 100644 --- a/topologie/urls.py +++ b/topologie/urls.py @@ -30,7 +30,6 @@ from __future__ import unicode_literals from django.conf.urls import url -import re2o from . import views urlpatterns = [ @@ -51,10 +50,6 @@ urlpatterns = [ url(r'^switch/(?P[0-9]+)$', views.index_port, name='index-port'), - url(r'^history/(?P\w+)/(?P[0-9]+)$', - re2o.views.history, - name='history', - kwargs={'application': 'topologie'}), url(r'^edit_port/(?P[0-9]+)$', views.edit_port, name='edit-port'), url(r'^new_port/(?P[0-9]+)$', views.new_port, name='new-port'), url(r'^del_port/(?P[0-9]+)$', views.del_port, name='del-port'), diff --git a/users/forms.py b/users/forms.py index 8c6f5db9..670fbb98 100644 --- a/users/forms.py +++ b/users/forms.py @@ -501,7 +501,7 @@ class ShellForm(FormRevMixin, ModelForm): class ListRightForm(FormRevMixin, ModelForm): """Edition, d'un groupe , équivalent à un droit - Ne peremet pas d'editer le gid, car il sert de primary key""" + Ne permet pas d'editer le gid, car il sert de primary key""" permissions = forms.ModelMultipleChoiceField( Permission.objects.all().select_related('content_type'), widget=forms.CheckboxSelectMultiple, @@ -510,23 +510,24 @@ class ListRightForm(FormRevMixin, ModelForm): class Meta: model = ListRight - fields = ['name', 'unix_name', 'critical', 'permissions', 'details'] + fields = ('name', 'unix_name', 'critical', 'permissions', 'details') def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ListRightForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['unix_name'].label = 'Nom du droit/groupe' + self.fields['unix_name'].label = 'Nom UNIX du groupe' class NewListRightForm(ListRightForm): """Ajout d'un groupe/list de droit """ class Meta(ListRightForm.Meta): - fields = '__all__' + fields = ('name', 'unix_name', 'gid', 'critical', 'permissions', + 'details') def __init__(self, *args, **kwargs): super(NewListRightForm, self).__init__(*args, **kwargs) - self.fields['gid'].label = 'Gid, attention, cet attribut ne doit\ - pas être modifié après création' + self.fields['gid'].label = ("Gid, attention, cet attribut ne doit " + "pas être modifié après création") class DelListRightForm(Form): diff --git a/users/locale/fr/LC_MESSAGES/django.mo b/users/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 00000000..9808ced6 Binary files /dev/null and b/users/locale/fr/LC_MESSAGES/django.mo differ diff --git a/users/locale/fr/LC_MESSAGES/django.po b/users/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..2814cb83 --- /dev/null +++ b/users/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,123 @@ +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2018 Maël Kervella +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +msgid "" +msgstr "" +"Project-Id-Version: 2.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-28 00:06+0200\n" +"PO-Revision-Date: 2018-06-27 23:35+0200\n" +"Last-Translator: Maël Kervella \n" +"Language-Team: \n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: templates/users/aff_listright.html:37 +msgid "Superuser" +msgstr "Superuser" + +#: templates/users/aff_listright.html:39 +msgid "Django's specific pre-defined right that supersed any other rights." +msgstr "" +"Droit prédéfini spécifique à Django qui outrepasse tous les autres droits." + +#: templates/users/aff_listright.html:43 +msgid "Total: All permissions" +msgstr "Total: Toutes les permissions" + +#: templates/users/aff_listright.html:55 +msgid "Users in Superuser" +msgstr "Utilisateurs dans Superuser" + +#: templates/users/aff_listright.html:62 templates/users/aff_listright.html:167 +msgid "Username" +msgstr "Pseudo" + +#: templates/users/aff_listright.html:63 templates/users/aff_listright.html:168 +msgid "Membership" +msgstr "Adhésion" + +#: templates/users/aff_listright.html:64 templates/users/aff_listright.html:169 +msgid "Last seen" +msgstr "Dernière connexion" + +#: templates/users/aff_listright.html:65 templates/users/aff_listright.html:170 +msgid "Actions" +msgstr "Actions" + +#: templates/users/aff_listright.html:66 templates/users/aff_listright.html:171 +msgid "Last action" +msgstr "Dernière action" + +#: templates/users/aff_listright.html:74 templates/users/aff_listright.html:179 +msgid "Member" +msgstr "Adhérent" + +#: templates/users/aff_listright.html:76 templates/users/aff_listright.html:181 +msgid "No membership records" +msgstr "Aucune adhésion" + +#: templates/users/aff_listright.html:79 templates/users/aff_listright.html:184 +#, python-format +msgid "Not since %(end_date)s" +msgstr "Plus depuis %(end_date)s" + +#: templates/users/aff_listright.html:87 templates/users/aff_listright.html:192 +msgid "Never" +msgstr "Jamais" + +#: templates/users/aff_listright.html:122 +#, python-format +msgid "%(right_name)s (gid: %(right_gid)s)" +msgstr "%(right_name)s (gid: %(right_gid)s)" + +#: templates/users/aff_listright.html:131 +#, python-format +msgid "Total: %(perm_count)s permission" +msgid_plural "Total: %(perm_count)s permissions" +msgstr[0] "Total: %(perm_count)s permission" +msgstr[1] "Total: %(perm_count)s permissions" + +#: templates/users/aff_listright.html:157 +#, python-format +msgid "Users in %(right_name)s" +msgstr "Utilisateurs dans %(right_name)s" + +#: widgets.py:35 +msgid "Close" +msgstr "" + +#: widgets.py:36 +msgid "Today" +msgstr "" + +#: widgets.py:44 +msgid "Next" +msgstr "" + +#: widgets.py:45 +msgid "Previous" +msgstr "" + +#: widgets.py:46 +msgid "Wk" +msgstr "" diff --git a/users/management/commands/ldap_rebuild.py b/users/management/commands/ldap_rebuild.py new file mode 100644 index 00000000..9deecbd3 --- /dev/null +++ b/users/management/commands/ldap_rebuild.py @@ -0,0 +1,112 @@ +# Copyright © 2018 Maël Kervella +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +import subprocess +from base64 import decodebytes + +from django.core.management.base import BaseCommand, CommandError +from django.conf import settings + +from users.models import User, ListRight + + +def split_lines(lines): + """ + Split LDIF lines. They can span over multiple system lines if the + following system lines begins with a space. + """ + ret = [] + for line in lines.split(b'\n'): + if line.startswith(b' ') and len(ret) > 1: + ret[-1] += line[len(b' '):] + else: + ret.append(line) + return ret + + +def flush_ldap(binddn, bindpass, server, usersdn, groupsdn): + """ + Perform the python (and more understandable) equivalent of the following commands: + + ldapsearch -LLL -s one -D $binddn -w $bindpass -H $server -b $usersdn dn \ + | grep "dn: " | sed -e 's/dn: //g' \ + | ldapdelete -v -D $binddn -w $bindpass -H $server -- + ldapsearch -LLL -s one -D $binddn -w $bindpass -H $server -b $usersdn dn \ + | grep "dn:: " | sed -e 's/dn:: //g' \ + | while read x; do echo "$x" | base64 -d; echo ""; done \ + | ldapdelete -v -D $binddn -w $bindpass -H $server -- + ldapsearch -LLL -s one -D $binddn -w $bindpass -H $server -b $groupsdn dn \ + | grep "dn: " | sed -e 's/dn: //g' \ + | ldapdelete -v -D $binddn -w $bindpass -H $server -- + ldapsearch -LLL -s one -D $binddn -w $bindpass -H $server -b $groupsdn dn \ + | grep "dn:: " | sed -e 's/dn:: //g' \ + | while read x; do echo "$x" | base64 -d; echo ""; done \ + | ldapdelete -v -D $binddn -w $bindpass -H $server -- + """ + + to_remove = [] + + for lookup in (usersdn, groupsdn): + search_cmd = [ + 'ldapsearch', + '-LLL', + '-s', 'one', + '-D', binddn, + '-w', bindpass, + '-H', server, + '-b', lookup, + 'dn' + ] + for line in split_lines(subprocess.check_output(search_cmd)): + if line.startswith(b'dn: '): + to_remove.append(line[len(b'dn: '):]) + elif line.startswith(b'dn:: '): + # Non ASCII value ares are base64-encoded + to_remove.append(decodebytes(line[len(b'dn:: '):])) + + delete_cmd = [ + 'ldapdelete', + '-D', binddn, + '-w', bindpass, + '-H', server + ] + to_remove + subprocess.check_call(delete_cmd) + + +def sync_ldap(): + """Syncrhonize the whole LDAP with the DB.""" + for u in User.objects.all(): + u.ldap_sync() + for lr in ListRight.objects.all(): + lr.ldap_sync() + + +class Command(BaseCommand): + help = ('Destroy the current LDAP data and rebuild it from the DB data. ' + 'Use with caution.') + + def handle(self, *args, **options): + + usersdn = settings.LDAP['base_user_dn'] + groupsdn = settings.LDAP['base_usergroup_dn'] + binddn = settings.DATABASES['ldap']['USER'] + bindpass = settings.DATABASES['ldap']['PASSWORD'] + server = settings.DATABASES['ldap']['NAME'] + + flush_ldap(binddn, bindpass, server, usersdn, groupsdn) + self.stdout.write("LDAP emptied") + sync_ldap() + self.stdout.write("LDAP rebuilt") diff --git a/users/models.py b/users/models.py index d0e21997..067a0346 100644 --- a/users/models.py +++ b/users/models.py @@ -52,7 +52,7 @@ import datetime from django.db import models from django.db.models import Q from django import forms -from django.db.models.signals import post_save, post_delete +from django.db.models.signals import post_save, post_delete, m2m_changed from django.dispatch import receiver from django.utils.functional import cached_property from django.template import Context, loader @@ -321,6 +321,14 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, si il n'est pas défini""" return self.shell or OptionalUser.get_cached_value('shell_default') + @cached_property + def get_shadow_expire(self): + """Return the shadow_expire value for the user""" + if self.state == self.STATE_DISABLED: + return str(0) + else: + return None + def end_adhesion(self): """ Renvoie la date de fin d'adhésion d'un user. Examine les objets cotisation""" @@ -426,36 +434,31 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, @cached_property def solde(self): - """ Renvoie le solde d'un user. Vérifie que l'option solde est - activé, retourne 0 sinon. + """ Renvoie le solde d'un user. Somme les crédits de solde et retire les débit payés par solde""" - user_solde = OptionalUser.get_cached_value('user_solde') - if user_solde: - solde_objects = Paiement.objects.filter(moyen='Solde') - somme_debit = Vente.objects.filter( - facture__in=Facture.objects.filter( - user=self, - paiement__in=solde_objects, - valid=True - ) - ).aggregate( - total=models.Sum( - models.F('prix')*models.F('number'), - output_field=models.FloatField() - ) - )['total'] or 0 - somme_credit = Vente.objects.filter( - facture__in=Facture.objects.filter(user=self, valid=True), - name="solde" - ).aggregate( - total=models.Sum( - models.F('prix')*models.F('number'), - output_field=models.FloatField() - ) - )['total'] or 0 - return somme_credit - somme_debit - else: - return 0 + solde_objects = Paiement.objects.filter(is_balance=True) + somme_debit = Vente.objects.filter( + facture__in=Facture.objects.filter( + user=self, + paiement__in=solde_objects, + valid=True + ) + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] or 0 + somme_credit = Vente.objects.filter( + facture__in=Facture.objects.filter(user=self, valid=True), + name="solde" + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] or 0 + return somme_credit - somme_debit def user_interfaces(self, active=True): """ Renvoie toutes les interfaces dont les machines appartiennent à @@ -484,16 +487,19 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, interface.save() def archive(self): - """ Archive l'user : appelle unassign_ips() puis passe state à - ARCHIVE""" + """ Filling the user; no more active""" self.unassign_ips() - self.state = User.STATE_ARCHIVE def unarchive(self): - """ Désarchive l'user : réassigne ses ip et le passe en state - ACTIVE""" + """Unfilling the user""" self.assign_ips() - self.state = User.STATE_ACTIVE + + def state_sync(self): + """Archive, or unarchive, if the user was not active/or archived before""" + if self.__original_state != self.STATE_ACTIVE and self.state == self.STATE_ACTIVE: + self.unarchive() + elif self.__original_state != self.STATE_ARCHIVE and self.state == self.STATE_ARCHIVE: + self.archive() def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True, group_refresh=False): @@ -527,10 +533,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, user_ldap.sambat_nt_password = self.pwd_ntlm.upper() if self.get_shell: user_ldap.login_shell = str(self.get_shell) - if self.state == self.STATE_DISABLED: - user_ldap.shadowexpire = str(0) - else: - user_ldap.shadowexpire = None + user_ldap.shadowexpire = self.get_shadow_expire if access_refresh: user_ldap.dialupAccess = str(self.has_access()) if mac_refresh: @@ -538,7 +541,10 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, machine__user=self ).values_list('mac_address', flat=True).distinct()] if group_refresh: - for group in self.groups.all(): + # Need to refresh all groups because we don't know which groups + # were updated during edition of groups and the user may no longer + # be part of the updated group (case of group removal) + for group in Group.objects.all(): if hasattr(group, 'listright'): group.listright.ldap_sync() user_ldap.save() @@ -881,6 +887,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, 'force': self.can_change_force, 'selfpasswd': self.check_selfpasswd, } + self.__original_state = self.state def __str__(self): return self.pseudo @@ -1009,6 +1016,7 @@ def user_post_save(**kwargs): user = kwargs['instance'] if is_created: user.notif_inscription() + user.state_sync() user.ldap_sync( base=True, access_refresh=True, @@ -1018,6 +1026,16 @@ def user_post_save(**kwargs): regen('mailing') +@receiver(m2m_changed, sender=User.groups.through) +def user_group_relation_changed(**kwargs): + action = kwargs['action'] + if action in ('post_add', 'post_remove', 'post_clear'): + user = kwargs['instance'] + user.ldap_sync(base=False, + access_refresh=False, + mac_refresh=False, + group_refresh=True) + @receiver(post_delete, sender=Adherent) @receiver(post_delete, sender=Club) @receiver(post_delete, sender=User) @@ -1175,7 +1193,7 @@ class ListRight(RevMixin, AclMixin, Group): group_ldap = LdapUserGroup.objects.get(gid=self.gid) except LdapUserGroup.DoesNotExist: group_ldap = LdapUserGroup(gid=self.gid) - group_ldap.name = self.listright + group_ldap.name = self.unix_name group_ldap.members = [user.pseudo for user in self.user_set.all()] group_ldap.save() diff --git a/users/templates/users/aff_bans.html b/users/templates/users/aff_bans.html index f9b11455..7f25a2ab 100644 --- a/users/templates/users/aff_bans.html +++ b/users/templates/users/aff_bans.html @@ -22,42 +22,43 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} {% load acl %} +{% load logs_extra %} {% if ban_list.paginator %} {% include "pagination.html" with list=ban_list %} {% endif %} - - - - - - - - - - - {% for ban in ban_list %} - {% if ban.is_active %} - +
    {% include "buttons/sort.html" with prefix='ban' col="user" text="Utilisateur" %}Raison{% include "buttons/sort.html" with prefix='ban' col="start" text="Date de début" %}{% include "buttons/sort.html" with prefix='ban' col="end" text="Date de fin" %}
    + + + + + + + + + + {% for ban in ban_list %} + {% if ban.is_active %} + {% else %} - {% endif %} + {% endif %} {% endfor %} -
    {% include "buttons/sort.html" with prefix='ban' col="user" text="Utilisateur" %}Raison{% include "buttons/sort.html" with prefix='ban' col="start" text="Date de début" %}{% include "buttons/sort.html" with prefix='ban' col="end" text="Date de fin" %}
    {{ ban.user }} {{ ban.raison }} {{ ban.date_start }} {{ ban.date_end }} - {% can_delete ban %} - {% include 'buttons/suppr.html' with href='users:del-ban' id=ban.id %} - {% acl_end %} - {% can_edit ban %} + {% can_delete ban %} + {% include 'buttons/suppr.html' with href='users:del-ban' id=ban.id %} + {% acl_end %} + {% can_edit ban %} {% include 'buttons/edit.html' with href='users:edit-ban' id=ban.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='users:history' name='ban' id=ban.id %} + {% acl_end %} + {% history_button ban %}
    + {% if ban_list.paginator %} {% include "pagination.html" with list=ban_list %} diff --git a/users/templates/users/aff_clubs.html b/users/templates/users/aff_clubs.html index 4fb6bd0d..dfb82e10 100644 --- a/users/templates/users/aff_clubs.html +++ b/users/templates/users/aff_clubs.html @@ -28,36 +28,36 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} - - - - - - - - - - - - {% for club in clubs_list %} - {% can_view club %} - - - - - - - +
    {% include "buttons/sort.html" with prefix='club' col="surname" text="Nom" %}{% include "buttons/sort.html" with prefix='club' col="pseudo" text="Pseudo" %}{% include "buttons/sort.html" with prefix='club' col="room" text="Chambre" %}Fin de cotisation leConnexionProfil
    {{ club.surname }}{{ club.pseudo }}{{ club.room }}{% if club.is_adherent %}{{ club.end_adhesion }}{% else %}Non adhérent{% endif %}{% if club.has_access == True %} - Active - {% else %} - Désactivée - {% endif %} - -
    + + + + + + + + - {% acl_end %} - {% endfor %} -
    {% include "buttons/sort.html" with prefix='club' col="surname" text="Nom" %}{% include "buttons/sort.html" with prefix='club' col="pseudo" text="Pseudo" %}{% include "buttons/sort.html" with prefix='club' col="room" text="Chambre" %}Fin de cotisation leConnexionProfil
    + + {% for club in clubs_list %} + {% can_view club %} + + {{ club.surname }} + {{ club.pseudo }} + {{ club.room }} + {% if club.is_adherent %}{{ club.end_adhesion }}{% else %}Non adhérent{% endif %} + {% if club.has_access == True %} + Active + {% else %} + Désactivée + {% endif %} + + + + + {% acl_end %} + {% endfor %} + {% if clubs_list.paginator %} {% include "pagination.html" with list=clubs_list %} diff --git a/users/templates/users/aff_listright.html b/users/templates/users/aff_listright.html index 8906b38e..cf78e295 100644 --- a/users/templates/users/aff_listright.html +++ b/users/templates/users/aff_listright.html @@ -22,118 +22,190 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} - - - - - - - - - - - - {% if superuser_right %} - - - - - - - - - - - + + + + +{% endif %} - {% endif %} - {% for listright in listright_list %} - - - - - - - - - - - - {% endfor %} -
    DroitGidGroupe/permission critiqueInformationsDetails
    SuperuserTrue - - - Donne tous les droits sur Re2o. - -
    -
    -
    -
    -
      - {% for user in superuser_right %} -
    • - {{user}} - - - -
    • - {% endfor %} -
    -
    -
    + +
    +
    - {{ listright.name }} - {{ listright.gid }}{{ listright.critical }} - - {{ listright.details }} - {% include 'buttons/edit.html' with href='users:edit-listright' id=listright.id %} - {% include 'buttons/history.html' with href='users:history' name='listright' id=listright.id %} -
    -
    -
    -
    -
      - {% for perm in listright.permissions.all %} -
    • - {{perm.name}} -
    • - {% endfor %} -
    -
    -
    -
    -
    -
      - {% for user in listright.user_set.all %} -
    • - {{user}} - - - -
    • - {% endfor %} -
    -
    -
    +{% for right, users in rights.items %} +
    +
    +
    + {% if users %} + + {% else %} + + {% endif %} + {{users.count}} + + {% include 'buttons/edit.html' with href='users:edit-listright' id=right.id %} + {% history_button right %} +
    +

    + + {% blocktrans trimmed with right.name as right_name and right.gid as right_gid %} + {{ right_name }} (gid: {{ right_gid }}) + {% endblocktrans %} +

    + {{ right.details }} +
    +
    +
    +

    + {% blocktrans trimmed count right.permissions.count as perm_count%} + Total: {{ perm_count }} permission + {% plural %} + Total: {{ perm_count }} permissions + {% endblocktrans %} +

    +
      + {% for perm in right.permissions.all %} +
    • + {{perm.name}} +
    • + {% endfor %} +
    +
    +
    +
    + +{% if users %} +
    - - +
    +
    +
    + +{% endif %} +{% endfor %} diff --git a/users/templates/users/aff_rights.html b/users/templates/users/aff_rights.html index 98eb4674..26ed3c5d 100644 --- a/users/templates/users/aff_rights.html +++ b/users/templates/users/aff_rights.html @@ -22,20 +22,20 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} - - - - {% for right in right_list %} - - - {% endfor %} - - - {% for user_right in user_right_list %} +
    {{ right }}
    + - + {% for right in right_list %} + + + {% endfor %} - {% endfor %} -
    {{ user_right }}{{ right }}
    + + {% for user_right in user_right_list %} + + {{ user_right }} + + {% endfor %} + diff --git a/users/templates/users/aff_schools.html b/users/templates/users/aff_schools.html index 260c7ef3..1d32a6eb 100644 --- a/users/templates/users/aff_schools.html +++ b/users/templates/users/aff_schools.html @@ -22,18 +22,19 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} {% load acl %} +{% load logs_extra %}
    -{% if school_list.paginator %} -{% include "pagination.html" with list=school_list %} -{% endif %} + {% if school_list.paginator %} + {% include "pagination.html" with list=school_list %} + {% endif %} - + @@ -44,14 +45,14 @@ with this program; if not, write to the Free Software Foundation, Inc., {% can_edit school %} {% include 'buttons/edit.html' with href='users:edit-school' id=school.id %} {% acl_end %} - {% include 'buttons/history.html' with href='users:history' name='school' id=school.id %} + {% history_button school %} {% endfor %}
    {% include "buttons/sort.html" with prefix='school' col='name' text='Etablissement' %}{% include "buttons/sort.html" with prefix='school' col='name' text='Etablissement' %}
    -{% if school_list.paginator %} -{% include "pagination.html" with list=school_list %} -{% endif %} + {% if school_list.paginator %} + {% include "pagination.html" with list=school_list %} + {% endif %}
    diff --git a/users/templates/users/aff_serviceusers.html b/users/templates/users/aff_serviceusers.html index ddef0360..5f43b0d7 100644 --- a/users/templates/users/aff_serviceusers.html +++ b/users/templates/users/aff_serviceusers.html @@ -22,30 +22,31 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} {% load acl %} - - - - - - - - - - {% for serviceuser in serviceusers_list %} +{% load logs_extra %} +
    NomRôleCommentaire
    + - - - - + + + + - {% endfor %} -
    {{ serviceuser.pseudo }}{{ serviceuser.access_group }}{{ serviceuser.comment }} - {% can_delete serviceuser %} - {% include 'buttons/suppr.html' with href='users:del-serviceuser' id=serviceuser.id %} - {% acl_end %} - {% can_edit serviceuser %} - {% include 'buttons/edit.html' with href='users:edit-serviceuser' id=serviceuser.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='users:history' name='serviceuser' id=serviceuser.id %} - NomRôleCommentaire
    + + {% for serviceuser in serviceusers_list %} + + {{ serviceuser.pseudo }} + {{ serviceuser.access_group }} + {{ serviceuser.comment }} + + {% can_delete serviceuser %} + {% include 'buttons/suppr.html' with href='users:del-serviceuser' id=serviceuser.id %} + {% acl_end %} + {% can_edit serviceuser %} + {% include 'buttons/edit.html' with href='users:edit-serviceuser' id=serviceuser.id %} + {% acl_end %} + {% history_button serviceuser %} + + + {% endfor %} + diff --git a/users/templates/users/aff_shell.html b/users/templates/users/aff_shell.html index a660f88b..a968a312 100644 --- a/users/templates/users/aff_shell.html +++ b/users/templates/users/aff_shell.html @@ -22,26 +22,27 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} {% load acl %} - - - - - - - - {% for shell in shell_list %} +{% load logs_extra %} +
    Shell
    + - - + + - {% endfor %} -
    {{ shell.shell }} - {% can_delete shell %} - {% include 'buttons/suppr.html' with href='users:del-shell' id=shell.id %} - {% acl_end %} - {% can_edit shell %} - {% include 'buttons/edit.html' with href='users:edit-shell' id=shell.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='users:history' name='listshell' id=shell.id %} - Shell
    + + {% for shell in shell_list %} + + {{ shell.shell }} + + {% can_delete shell %} + {% include 'buttons/suppr.html' with href='users:del-shell' id=shell.id %} + {% acl_end %} + {% can_edit shell %} + {% include 'buttons/edit.html' with href='users:edit-shell' id=shell.id %} + {% acl_end %} + {% history_button shell %} + + + {% endfor %} + diff --git a/users/templates/users/aff_users.html b/users/templates/users/aff_users.html index d8c18890..42dc07d6 100644 --- a/users/templates/users/aff_users.html +++ b/users/templates/users/aff_users.html @@ -22,9 +22,9 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %}
    -{% if users_list.paginator %} -{% include "pagination.html" with list=users_list %} -{% endif %} + {% if users_list.paginator %} + {% include "pagination.html" with list=users_list %} + {% endif %} @@ -57,7 +57,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
    -{% if users_list.paginator %} -{% include "pagination.html" with list=users_list %} -{% endif %} + {% if users_list.paginator %} + {% include "pagination.html" with list=users_list %} + {% endif %}
    diff --git a/users/templates/users/aff_whitelists.html b/users/templates/users/aff_whitelists.html index 01a9efdd..98989093 100644 --- a/users/templates/users/aff_whitelists.html +++ b/users/templates/users/aff_whitelists.html @@ -26,38 +26,39 @@ with this program; if not, write to the Free Software Foundation, Inc., {% include "pagination.html" with list=white_list %} {% endif %} {% load acl %} - - - - - - - - - - - {% for whitelist in white_list %} - {% if whitelist.is_active %} - +{% load logs_extra %} +
    {% include "buttons/sort.html" with prefix='white' col="user" text="Utilisateur" %}Raison{% include "buttons/sort.html" with prefix='white' col="start" text="Date de début" %}{% include "buttons/sort.html" with prefix='white' col="end" text="Date de fin" %}
    + + + + + + + + + + {% for whitelist in white_list %} + {% if whitelist.is_active %} + {% else %} - {% endif %} + {% endif %} {% endfor %} -
    {% include "buttons/sort.html" with prefix='white' col="user" text="Utilisateur" %}Raison{% include "buttons/sort.html" with prefix='white' col="start" text="Date de début" %}{% include "buttons/sort.html" with prefix='white' col="end" text="Date de fin" %}
    {{ whitelist.user }} {{ whitelist.raison }} {{ whitelist.date_start }} {{ whitelist.date_end }} - {% can_delete whitelist %} + {% can_delete whitelist %} {% include 'buttons/suppr.html' with href='users:del-whitelist' id=whitelist.id %} - {% acl_end %} - {% can_edit whitelist %} + {% acl_end %} + {% can_edit whitelist %} {% include 'buttons/edit.html' with href='users:edit-whitelist' id=whitelist.id %} - {% acl_end %} - {% include 'buttons/history.html' with href='users:history' name='whitelist' id=whitelist.id %} + {% acl_end %} + {% history_button whitelist %}
    + {% if white_list.paginator %} {% include "pagination.html" with list=white_list %} diff --git a/users/templates/users/index_listright.html b/users/templates/users/index_listright.html index 57165792..3b8b3e60 100644 --- a/users/templates/users/index_listright.html +++ b/users/templates/users/index_listright.html @@ -34,9 +34,8 @@ with this program; if not, write to the Free Software Foundation, Inc., Ajouter un droit ou groupe {% acl_end %} Supprimer un ou plusieurs droits/groupes - {% include "users/aff_listright.html" with listright_list=listright_list %} -


    + {% include "users/aff_listright.html" %} {% endblock %} diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index abba61a2..9628097a 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -25,23 +25,104 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load acl %} +{% load logs_extra %} {% block title %}Profil{% endblock %} {% block content %} -

    {{ users.surname }} {{users.name}}

    -

    Vous êtes {% if users.end_adhesion != None %} -un {{ users.class_name | lower}}{% else %} -non adhérent{% endif %} et votre connexion est {% if users.has_access %} -active{% else %}désactivée{% endif %}.

    -{% if user_solde %} -

    Votre solde est de {{ user.solde }}€. -{% if allow_online_payment %} - - - Recharger - -{% endif %} -

    -{% endif %} + +
    +

    Bienvenue {{users.name}} {{ users.surname }}

    +
    +
    +
    + {% if solde_activated %} +
    + {% else %} +
    + {% endif %} +
    + {% if users.is_ban%} +
    +
    Votre compte est banni
    +
    + Fin du ban : {{user.end_ban|date:"d M Y"}} +
    +
    + {% elif not users.is_adherent%} +
    +
    Non Connécté
    + + {% else %} +
    +
    Connécté
    +
    + Fin de connexion: {{user.end_adhesion|date:"d M Y"}} +
    +
    + {% endif %} +
    +
    + {% if solde_activated %} +
    +
    +
    +
    +
    + + {{user.solde}} +
    + +
    +
    +
    +
    + {% endif %} + {% if solde_activated %} +
    + {% else %} +
    + {% endif %} +
    + {% if nb_machines %} +
    +
    + {{nb_machines}} + Machines + +
    + +
    + {% else %} +
    +
    Aucune machine
    + +
    + {% endif %} +
    +
    +
    +
    + +
    @@ -51,134 +132,131 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %}
    -
    - - - Editer - - - - Changer le mot de passe - - {% can_change User state %} - - - Changer le statut - - {% acl_end %} - {% can_change User groups %} - - - Gérer les groupes - - {% acl_end %} - - - Historique - +
    + + + Editer + + + + Changer le mot de passe + + {% can_change User state %} + + + Changer le statut + + {% acl_end %} + {% can_change User groups %} + + + Gérer les groupes + + {% acl_end %} + {% history_button users text=True %}
    -
    +
    - - - {% if users.is_class_club %} - - {% if users.club.mailing %} - - {% else %} - - {% endif %} - {% else %} - - - {% endif %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% if users.end_adhesion != None %} - - {% else %} - - {% endif %} - - {% if users.end_whitelist != None %} - - {% else %} - - {% endif %} - - - {% if users.end_ban != None %} - - {% else %} - - {% endif %} - - {% if users.state == 0 %} - - {% elif users.state == 1 %} - - {% else %} - - {% endif %} - - - - {% if users.has_access == True %} - - {% else %} - - {% endif %} - - {% if users.groups.all %} - - {% else %} - - {% endif %} - - - - - {% if users.shell %} - - - {% endif %} - -
    Mailing{{ users.pseudo }}(-admin)Mailing désactivéePrénom{{ users.name }}Nom{{ users.surname }}
    Pseudo{{ users.pseudo }}E-mail{{ users.email }}
    Chambre{{ users.room }}Téléphone{{ users.telephone }}
    École{{ users.school }}Commentaire{{ users.comment }}
    Date d'inscription{{ users.registered }}Dernière connexion{{ users.last_login }}
    Fin d'adhésion{{ users.end_adhesion }}Non adhérentAccès gracieux{{ users.end_whitelist }}Aucun
    Bannissement{{ users.end_ban }}Non banniStatutActifDésactivéArchivé
    Accès internetActif (jusqu'au {{ users.end_access }})DésactivéGroupes{{ users.groups.all|join:", "}}Aucun
    Solde{{ users.solde }} € - {% if allow_online_payment %} - - - Recharger - - {% endif %} - Shell{{ users.shell }}
    + + + {% if users.is_class_club %} + + {% if users.club.mailing %} + + {% else %} + + {% endif %} + {% else %} + + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if users.end_adhesion != None %} + + {% else %} + + {% endif %} + + {% if users.end_whitelist != None %} + + {% else %} + + {% endif %} + + + {% if users.end_ban != None %} + + {% else %} + + {% endif %} + + {% if users.state == 0 %} + + {% elif users.state == 1 %} + + {% else %} + + {% endif %} + + + + {% if users.has_access == True %} + + {% else %} + + {% endif %} + + {% if users.groups.all %} + + {% else %} + + {% endif %} + + + + + {% if users.shell %} + + + {% endif %} + +
    Mailing{{ users.pseudo }}(-admin)Mailing désactivéePrénom{{ users.name }}Nom{{ users.surname }}
    Pseudo{{ users.pseudo }}E-mail{{ users.email }}
    Chambre{{ users.room }}Téléphone{{ users.telephone }}
    École{{ users.school }}Commentaire{{ users.comment }}
    Date d'inscription{{ users.registered }}Dernière connexion{{ users.last_login }}
    Fin d'adhésion{{ users.end_adhesion }}Non adhérentAccès gracieux{{ users.end_whitelist }}Aucun
    Bannissement{{ users.end_ban }}Non banniStatutActifDésactivéArchivé
    Accès internetActif (jusqu'au {{ users.end_access }})DésactivéGroupes{{ users.groups.all|join:", "}}Aucun
    Solde{{ users.solde }} € + {% if user_solde %} + + + Recharger + + {% endif %} + Shell{{ users.shell }}
    @@ -195,46 +273,46 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %} Gérer admin et membres - +
    -
    +

    Administrateurs du club

    - - +
    + + + + + + + + {% for admin in users.club.administrators.all %} - - - + + + - - {% for admin in users.club.administrators.all %} - - - - - - {% endfor %} -
    NomPrenomPseudo
    NomPrenomPseudo{{ admin.surname }}{{ admin.name }}{{ admin.pseudo }}
    {{ admin.surname }}{{ admin.name }}{{ admin.pseudo }}
    + {% endfor %} +

    Membres

    - - +
    + + + + + + + + {% for admin in users.club.members.all %} - - - + + + - - {% for admin in users.club.members.all %} - - - - - - {% endfor %} -
    NomPrenomPseudo
    NomPrenomPseudo{{ admin.surname }}{{ admin.name }}{{ admin.pseudo }}
    {{ admin.surname }}{{ admin.name }}{{ admin.pseudo }}
    + {% endfor %} +
    @@ -247,12 +325,12 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %} Machines {{nb_machines}} -
    +
    @@ -283,12 +361,8 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %} Modifier le solde - {% endif%}{% acl_else %} - {% if user_solde %} - - - Ajouter une cotisation par solde{% endif %}{% acl_end %} - + {% endif%} + {% acl_end %}
    {% if facture_list %} @@ -305,12 +379,12 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %} Bannissements -
    +
    -
    - {% can_create Ban %} - - +
    + {% can_create Ban %} + + Ajouter un bannissement {% acl_end %} @@ -332,12 +406,12 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %}
    -
    +
    diff --git a/users/urls.py b/users/urls.py index 724601c3..6bb374c3 100644 --- a/users/urls.py +++ b/users/urls.py @@ -27,7 +27,6 @@ from __future__ import unicode_literals from django.conf.urls import url -import re2o from . import views urlpatterns = [ @@ -95,10 +94,6 @@ urlpatterns = [ url(r'^process/(?P[a-z0-9]{32})/$', views.process, name='process'), url(r'^reset_password/$', views.reset_password, name='reset-password'), url(r'^mass_archive/$', views.mass_archive, name='mass-archive'), - url(r'^history/(?P\w+)/(?P[0-9]+)$', - re2o.views.history, - name='history', - kwargs={'application': 'users'}), url(r'^$', views.index, name='index'), url(r'^index_clubs/$', views.index_clubs, name='index-clubs'), url(r'^rest/ml/std/$', diff --git a/users/views.py b/users/views.py index fcb44f65..a6444e33 100644 --- a/users/views.py +++ b/users/views.py @@ -39,7 +39,7 @@ from django.urls import reverse from django.shortcuts import get_object_or_404, render, redirect from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.db.models import ProtectedError +from django.db.models import ProtectedError, Count, Max from django.utils import timezone from django.db import transaction from django.http import HttpResponse @@ -49,7 +49,7 @@ from django.views.decorators.csrf import csrf_exempt from rest_framework.renderers import JSONRenderer from reversion import revisions as reversion -from cotisations.models import Facture +from cotisations.models import Facture, Paiement from machines.models import Machine from preferences.models import OptionalUser, GeneralOption, AssoOption from re2o.views import form @@ -67,6 +67,7 @@ from re2o.acl import ( can_view_all, can_change ) +from cotisations.utils import find_payment_method from .serializers import MailingSerializer, MailingMemberSerializer from .models import ( @@ -111,8 +112,7 @@ def new_user(request): GTU_sum_up = GeneralOption.get_cached_value('GTU_sum_up') GTU = GeneralOption.get_cached_value('GTU') if user.is_valid(): - user = user.save(commit=False) - user.save() + user = user.save() user.reset_passwd_mail(request) messages.success(request, "L'utilisateur %s a été crée, un mail\ pour l'initialisation du mot de passe a été envoyé" % user.pseudo) @@ -220,15 +220,10 @@ def edit_info(request, user, userid): @login_required @can_edit(User, 'state') def state(request, user, userid): - """ Changer l'etat actif/desactivé/archivé d'un user, - need droit bureau """ + """ Change the state (active/unactive/archived) of a user""" state_form = StateForm(request.POST or None, instance=user) if state_form.is_valid(): if state_form.changed_data: - if state_form.cleaned_data['state'] == User.STATE_ARCHIVE: - user.archive() - elif state_form.cleaned_data['state'] == User.STATE_ACTIVE: - user.unarchive() state_form.save() messages.success(request, "Etat changé avec succès") return redirect(reverse( @@ -246,7 +241,8 @@ def state(request, user, userid): @can_edit(User, 'groups') def groups(request, user, userid): """ View to edit the groups of a user """ - group_form = GroupForm(request.POST or None, instance=user, user=request.user) + group_form = GroupForm(request.POST or None, + instance=user, user=request.user) if group_form.is_valid(): if group_form.changed_data: group_form.save() @@ -404,23 +400,23 @@ def edit_ban(request, ban_instance, **_kwargs): request ) + @login_required @can_delete(Ban) def del_ban(request, ban, **_kwargs): - """ Supprime un banissement""" - if request.method == "POST": - ban.delete() - messages.success(request, "Le banissement a été supprimé") - return redirect(reverse( - 'users:profil', - kwargs={'userid': str(ban.user.id)} - )) - return form( - {'objet': ban, 'objet_name': 'ban'}, - 'users/delete.html', - request - ) - + """ Supprime un banissement""" + if request.method == "POST": + ban.delete() + messages.success(request, "Le banissement a été supprimé") + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(ban.user.id)} + )) + return form( + {'objet': ban, 'objet_name': 'ban'}, + 'users/delete.html', + request + ) @login_required @@ -481,19 +477,20 @@ def edit_whitelist(request, whitelist_instance, **_kwargs): @login_required @can_delete(Whitelist) def del_whitelist(request, whitelist, **_kwargs): - """ Supprime un acces gracieux""" - if request.method == "POST": - whitelist.delete() - messages.success(request, "L'accés gracieux a été supprimé") - return redirect(reverse( - 'users:profil', - kwargs={'userid': str(whitelist.user.id)} - )) - return form( - {'objet': whitelist, 'objet_name': 'whitelist'}, - 'users/delete.html', - request - ) + """ Supprime un acces gracieux""" + if request.method == "POST": + whitelist.delete() + messages.success(request, "L'accés gracieux a été supprimé") + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(whitelist.user.id)} + )) + return form( + {'objet': whitelist, 'objet_name': 'whitelist'}, + 'users/delete.html', + request + ) + @login_required @can_create(School) @@ -806,15 +803,30 @@ def index_shell(request): @can_view_all(ListRight) def index_listright(request): """ Affiche l'ensemble des droits""" - listright_list = ListRight.objects.order_by('unix_name')\ - .prefetch_related('permissions').prefetch_related('user_set') - superuser_right = User.objects.filter(is_superuser=True) + rights = {} + for right in (ListRight.objects + .order_by('name') + .prefetch_related('permissions') + .prefetch_related('user_set') + .prefetch_related('user_set__facture_set__vente_set__cotisation') + ): + rights[right] = (right.user_set + .annotate(action_number=Count('revision'), + last_seen=Max('revision__date_created'), + end_adhesion=Max('facture__vente__cotisation__date_end')) + ) + superusers = (User.objects + .filter(is_superuser=True) + .annotate(action_number=Count('revision'), + last_seen=Max('revision__date_created'), + end_adhesion=Max('facture__vente__cotisation__date_end')) + ) return render( request, 'users/index_listright.html', { - 'listright_list': listright_list, - 'superuser_right' : superuser_right, + 'rights': rights, + 'superusers' : superusers, } ) @@ -837,7 +849,7 @@ def mon_profil(request): return redirect(reverse( 'users:profil', kwargs={'userid': str(request.user.id)} - )) + )) @login_required @@ -881,20 +893,28 @@ def profil(request, users, **_kwargs): request.GET.get('order'), SortTable.USERS_INDEX_WHITE ) - user_solde = OptionalUser.get_cached_value('user_solde') - allow_online_payment = AssoOption.get_cached_value('payment') != 'NONE' + try: + balance = find_payment_method(Paiement.objects.get(is_balance=True)) + except Paiement.DoesNotExist: + user_solde = False + else: + user_solde = ( + balance is not None + and balance.can_credit_balance(request.user) + ) return render( request, 'users/profil.html', { 'users': users, 'machines_list': machines, - 'nb_machines' : nb_machines, + 'nb_machines': nb_machines, 'facture_list': factures, 'ban_list': bans, 'white_list': whitelists, 'user_solde': user_solde, - 'allow_online_payment': allow_online_payment, + 'solde_activated': Paiement.objects.filter(is_balance=True).exists(), + 'asso_name': AssoOption.objects.first().name } ) @@ -959,6 +979,7 @@ def process_passwd(request, req): class JSONResponse(HttpResponse): """ Framework Rest """ + def __init__(self, data, **kwargs): content = JSONRenderer().render(data) kwargs['content_type'] = 'application/json'