8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-25 22:22:26 +00:00

Conflicts fix switch_conf_json

# Conflicts:
#   re2o/templatetags/acl.py
#   re2o/views.py
#   topologie/templates/topologie/aff_port.html
This commit is contained in:
chirac 2018-07-29 20:02:20 +02:00
commit feddc3f69b
128 changed files with 4328 additions and 2861 deletions

View file

@ -1,7 +1,7 @@
## MR 160: Datepicker ## MR 160: Datepicker
Install libjs-jquery libjs-jquery-ui libjs-jquery-timepicker libjs-bootstrap javascript-common Install libjs-jquery libjs-jquery-ui libjs-jquery-timepicker libjs-bootstrap javascript-common
``` ```bash
apt-get -y install \ apt-get -y install \
libjs-jquery \ libjs-jquery \
libjs-jquery-ui \ libjs-jquery-ui \
@ -10,12 +10,12 @@ apt-get -y install \
javascript-common javascript-common
``` ```
Enable javascript-common conf Enable javascript-common conf
``` ```bash
a2enconf javascript-common a2enconf javascript-common
``` ```
Delete old jquery files : Delete old jquery files :
``` ```bash
rm -r static_files/js/jquery-ui-* rm -r static_files/js/jquery-ui-*
rm static_files/js/jquery-2.2.4.min.js rm static_files/js/jquery-2.2.4.min.js
rm static/css/jquery-ui-timepicker-addon.css rm static/css/jquery-ui-timepicker-addon.css
@ -42,6 +42,7 @@ Refactored install_re2o.sh script.
``` ```
install_re2o.sh help 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) * 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 ## MR 177: Add django-debug-toolbar support
Add the possibility to enable `django-debug-toolbar` in debug mode. First install the APT package: 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"] 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.

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
@ -190,6 +191,13 @@ class MxSerializer(NamespacedHMSerializer):
fields = ('zone', 'priority', 'name', 'api_url') 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): class NsSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Ns` objects. """Serialize `machines.models.Ns` objects.
""" """

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # 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/mx', views.MxViewSet)
router.register_viewset(r'machines/ns', views.NsViewSet) router.register_viewset(r'machines/ns', views.NsViewSet)
router.register_viewset(r'machines/txt', views.TxtViewSet) 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/srv', views.SrvViewSet)
router.register_viewset(r'machines/interface', views.InterfaceViewSet) router.register_viewset(r'machines/interface', views.InterfaceViewSet)
router.register_viewset(r'machines/ipv6list', views.Ipv6ListViewSet) router.register_viewset(r'machines/ipv6list', views.Ipv6ListViewSet)

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
@ -162,6 +163,12 @@ class TxtViewSet(viewsets.ReadOnlyModelViewSet):
queryset = machines.Txt.objects.all() queryset = machines.Txt.objects.all()
serializer_class = serializers.TxtSerializer 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): class SrvViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `machines.models.Srv` objects. """Exposes list and details of `machines.models.Srv` objects.

View file

@ -5,6 +5,7 @@
# Copyright © 2017 Gabriel Détraz # Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec # Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle # Copyright © 2017 Augustin Lemesle
# Copyright © 2018 Hugo Levy-Falk
# #
# This program is free software; you can redistribute it and/or modify # 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 # 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.core.validators import MinValueValidator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _l 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.field_permissions import FieldPermissionFormMixin
from re2o.mixins import FormRevMixin from re2o.mixins import FormRevMixin
from .models import Article, Paiement, Facture, Banque 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 Form used to manage and create an invoice and its fields.
cheque number.
""" """
def __init__(self, *args, **kwargs):
def __init__(self, *args, creation=False, **kwargs):
user = kwargs['user']
prefix = kwargs.pop('prefix', self.Meta.model.__name__) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs) super(FactureForm, 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")
self.fields['paiement'].empty_label = \ self.fields['paiement'].empty_label = \
_("Select a payment method") _("Select a payment method")
paiement_list = Paiement.objects.filter(type_paiement=1) self.fields['paiement'].queryset = Paiement.find_allowed_payments(user)
if paiement_list: if not creation:
self.fields['paiement'].widget\ self.fields['user'].label = _("Member")
.attrs['data-cheque'] = paiement_list.first().id self.fields['user'].empty_label = \
_("Select the proprietary member")
self.fields['valid'].label = _("Validated invoice")
else:
self.fields = {'paiement': self.fields['paiement']}
class Meta: class Meta:
model = Facture model = Facture
fields = ['paiement', 'banque', 'cheque'] fields = '__all__'
def clean(self): def clean(self):
cleaned_data = super(NewFactureForm, self).clean() cleaned_data = super(FactureForm, self).clean()
paiement = cleaned_data.get('paiement') paiement = cleaned_data.get('paiement')
cheque = cleaned_data.get('cheque')
banque = cleaned_data.get('banque')
if not paiement: if not paiement:
raise forms.ValidationError( raise forms.ValidationError(
_("A payment method must be specified.") _("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 return cleaned_data
class CreditSoldeForm(NewFactureForm): class SelectUserArticleForm(FormRevMixin, Form):
"""
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):
""" """
Form used to select an article during the creation of an invoice for a Form used to select an article during the creation of an invoice for a
member. member.
@ -127,6 +102,11 @@ class SelectUserArticleForm(
required=True 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): class SelectClubArticleForm(Form):
""" """
@ -146,6 +126,10 @@ class SelectClubArticleForm(Form):
required=True 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 # TODO : change Facture to Invoice
class NewFactureFormPdf(Form): 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): class ArticleForm(FormRevMixin, ModelForm):
""" """
Form used to create an article. Form used to create an article.
@ -231,17 +195,12 @@ class PaiementForm(FormRevMixin, ModelForm):
class Meta: class Meta:
model = Paiement model = Paiement
# TODO : change moyen to method and type_paiement to payment_type # 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): def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs) super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['moyen'].label = _("Payment method name") 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 # TODO : change paiement to payment
@ -304,56 +263,6 @@ class DelBanqueForm(FormRevMixin, Form):
self.fields['banques'].queryset = Banque.objects.all() 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 # TODO : Better name and docstring
class RechargeForm(FormRevMixin, Form): class RechargeForm(FormRevMixin, Form):
""" """
@ -364,34 +273,31 @@ class RechargeForm(FormRevMixin, Form):
min_value=0.01, min_value=0.01,
validators=[] validators=[]
) )
payment = forms.ModelChoiceField(
queryset=Paiement.objects.none(),
label=_l("Payment method")
)
def __init__(self, *args, **kwargs): def __init__(self, *args, user=None, **kwargs):
self.user = kwargs.pop('user') self.user = user
super(RechargeForm, self).__init__(*args, **kwargs) 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 the value is well inside the possible limits
""" """
value = self.cleaned_data['value'] value = self.cleaned_data['value']
if value < OptionalUser.get_cached_value('min_online_payment'): balance_method = get_object_or_404(balance.PaymentMethod)
raise forms.ValidationError( if balance_method.maximum_balance is not None and \
_("Requested amount is too small. Minimum amount possible : \ value + self.user.solde > balance_method.maximum_balance:
%(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'):
raise forms.ValidationError( raise forms.ValidationError(
_("Requested amount is too high. Your balance can't exceed \ _("Requested amount is too high. Your balance can't exceed \
%(max_online_balance)s .") % { %(max_online_balance)s .") % {
'max_online_balance': OptionalUser.get_cached_value( 'max_online_balance': balance_method.maximum_balance
'max_solde'
)
} }
) )
return value return self.cleaned_data

View file

@ -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',
),
]

View file

@ -6,6 +6,7 @@
# Copyright © 2017 Gabriel Détraz # Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec # Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle # Copyright © 2017 Augustin Lemesle
# Copyright © 2018 Hugo Levy-Falk
# #
# This program is free software; you can redistribute it and/or modify # 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 # 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 import timezone
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _l 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 machines.models import regen
from re2o.field_permissions import FieldPermissionModelMixin from re2o.field_permissions import FieldPermissionModelMixin
from re2o.mixins import AclMixin, RevMixin 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 # TODO : change facture to invoice
class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
@ -148,7 +155,7 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
models.F('prix')*models.F('number'), models.F('prix')*models.F('number'),
output_field=models.FloatField() output_field=models.FloatField()
) )
)['total'] )['total'] or 0
def name(self): def name(self):
""" """
@ -213,6 +220,22 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
_("You don't have the right to edit an invoice.") _("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): def __init__(self, *args, **kwargs):
super(Facture, self).__init__(*args, **kwargs) super(Facture, self).__init__(*args, **kwargs)
self.field_permissions = { self.field_permissions = {
@ -501,12 +524,17 @@ class Article(RevMixin, AclMixin, models.Model):
max_length=255, max_length=255,
verbose_name=_l("Type of cotisation") 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') unique_together = ('name', 'type_user')
class Meta: class Meta:
permissions = ( permissions = (
('view_article', _l("Can see an article's details")), ('view_article', _l("Can see an article's details")),
('buy_every_article', _l("Can buy every_article"))
) )
verbose_name = "Article" verbose_name = "Article"
verbose_name_plural = "Articles" verbose_name_plural = "Articles"
@ -524,6 +552,35 @@ class Article(RevMixin, AclMixin, models.Model):
def __str__(self): def __str__(self):
return self.name 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): 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. invoice. It's easier to know this information when doing the accouts.
It is represented by: It is represented by:
* a name * 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 # TODO : change moyen to method
moyen = models.CharField( moyen = models.CharField(
max_length=255, max_length=255,
verbose_name=_l("Method") verbose_name=_l("Method")
) )
type_paiement = models.IntegerField( available_for_everyone = models.BooleanField(
choices=PAYMENT_TYPES, default=False,
default=0, verbose_name=_l("Is available for every user")
verbose_name=_l("Payment type") )
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: class Meta:
permissions = ( permissions = (
('view_paiement', _l("Can see a payement's details")), ('view_paiement', _l("Can see a payement's details")),
('use_every_payment', _l("Can use every payement")),
) )
verbose_name = _l("Payment method") verbose_name = _l("Payment method")
verbose_name_plural = _l("Payment methods") verbose_name_plural = _l("Payment methods")
@ -593,16 +650,79 @@ class Paiement(RevMixin, AclMixin, models.Model):
""" """
self.moyen = self.moyen.title() 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 The general way of ending a payment.
method of type 'cheque' exists.
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: payment_method = find_payment_method(self)
raise ValidationError( if payment_method is not None and use_payment_method:
_("You cannot have multiple payment method of type cheque") 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): class Cotisation(RevMixin, AclMixin, models.Model):

View file

@ -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,
]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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']

View file

@ -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}
))

View file

@ -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<invoice_pk>[0-9]+)$',
views.cheque,
name='validate'
)
]

View file

@ -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()
}
)

View file

@ -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

View file

@ -10,7 +10,7 @@ import hashlib
from collections import OrderedDict from collections import OrderedDict
class Payment(): class Transaction():
""" The class representing a transaction with all the functions """ The class representing a transaction with all the functions
used during the negociation used during the negociation
""" """

View file

@ -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))

View file

@ -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<factureid>[0-9]+)$',
views.accept_payment,
name='accept_payment'
),
url(
r'^refuse/$',
views.refuse_payment,
name='refuse_payment'
),
url(
r'^ipn/$',
views.ipn,
name='ipn'
),
]

View file

@ -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 """Payment
Here are defined some views dedicated to online payement. Here are the views needed by comnpay
""" """
from collections import OrderedDict from collections import OrderedDict
@ -14,22 +34,36 @@ from django.utils.datastructures import MultiValueDictKeyError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.http import HttpResponse, HttpResponseBadRequest from django.http import HttpResponse, HttpResponseBadRequest
from preferences.models import AssoOption from cotisations.models import Facture
from .models import Facture from .comnpay import Transaction
from .payment_utils.comnpay import Payment as ComnpayPayment from .models import ComnpayPayment
@csrf_exempt @csrf_exempt
@login_required @login_required
def accept_payment(request, factureid): 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) invoice = get_object_or_404(Facture, id=factureid)
if invoice.valid:
messages.success( messages.success(
request, request,
_("The payment of %(amount)s € has been accepted.") % { _("The payment of %(amount)s € has been accepted.") % {
'amount': facture.prix() '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( return redirect(reverse(
@ -42,7 +76,8 @@ def accept_payment(request, factureid):
@login_required @login_required
def refuse_payment(request): 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( messages.error(
request, request,
@ -59,37 +94,38 @@ def ipn(request):
""" """
The view called by Comnpay server to validate the transaction. The view called by Comnpay server to validate the transaction.
Verify that we can firmly save the user's action and notify 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', ) order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', )
try: try:
data = OrderedDict([(f, request.POST[f]) for f in order]) data = OrderedDict([(f, request.POST[f]) for f in order])
except MultiValueDictKeyError: except MultiValueDictKeyError:
return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") 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'] 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: try:
factureid = int(idTransaction) factureid = int(idTransaction)
except ValueError: except ValueError:
return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") return HttpResponseBadRequest("HTTP/1.1 400 Bad Request")
facture = get_object_or_404(Facture, id=factureid) facture = get_object_or_404(Facture, id=factureid)
payment_method = get_object_or_404(
ComnpayPayment, payment=facture.paiement)
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 # Checking that the payment is valid
if not result: if not result:
# Payment failed: Cancelling the invoice operation # Payment failed: Cancelling the invoice operation
facture.delete()
# And send the response to Comnpay indicating we have well # And send the response to Comnpay indicating we have well
# received the failure information. # received the failure information.
return HttpResponse("HTTP/1.1 200 OK") return HttpResponse("HTTP/1.1 200 OK")
@ -100,42 +136,3 @@ def ipn(request):
# Everything worked we send a reponse to Comnpay indicating that # Everything worked we send a reponse to Comnpay indicating that
# it's ok for them to proceed # it's ok for them to proceed
return HttpResponse("HTTP/1.0 200 OK") 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
}

View file

@ -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

View file

@ -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)

View file

@ -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')),
]

View file

@ -24,6 +24,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load i18n %} {% load i18n %}
{% load logs_extra %}
{% load design %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -33,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>{% trans "Cotisation type" %}</th> <th>{% trans "Cotisation type" %}</th>
<th>{% trans "Duration (month)" %}</th> <th>{% trans "Duration (month)" %}</th>
<th>{% trans "Concerned users" %}</th> <th>{% trans "Concerned users" %}</th>
<th>{% trans "Available for everyone" | tick %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -43,15 +46,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ article.type_cotisation }}</td> <td>{{ article.type_cotisation }}</td>
<td>{{ article.duration }}</td> <td>{{ article.duration }}</td>
<td>{{ article.type_user }}</td> <td>{{ article.type_user }}</td>
<td>{{ article.available_for_everyone }}</td>
<td class="text-right"> <td class="text-right">
{% can_edit article %} {% can_edit article %}
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-article' article.id %}"> <a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-article' article.id %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
</a> </a>
{% acl_end %} {% acl_end %}
<a class="btn btn-info btn-sm" role="button" title="{% trans "Historique" %}" href="{% url 'cotisations:history' 'article' article.id %}"> {% history_button article %}
<i class="fa fa-history"></i>
</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load i18n %} {% load i18n %}
{% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -41,9 +42,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
</a> </a>
{% acl_end %} {% acl_end %}
<a class="btn btn-info btn-sm" role="button" title="{% trans "Historique" %}" href="{% url 'cotisations:history' 'banque' banque.id %}"> {% history_button banque %}
<i class="fa fa-history"></i>
</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load i18n %} {% load i18n %}
{% load logs_extra %}
<div class="table-responsive"> <div class="table-responsive">
{% if facture_list.paginator %} {% if facture_list.paginator %}
@ -86,9 +87,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</li> </li>
{% acl_end %} {% acl_end %}
<li> <li>
<a href="{% url 'cotisations:history' 'facture' facture.id %}"> {% history_button facture text=True html_class=False%}
<i class="fa fa-history"></i> {% trans "Historique" %}
</a>
</li> </li>
</ul> </ul>
</div> </div>

View file

@ -24,26 +24,32 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load i18n %} {% load i18n %}
{% load logs_extra %}
{% load design %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>{% trans "Payment method" %}</th> <th>{% trans "Payment type" %}</th>
<th>{% trans "Is available for everyone" %}</th>
<th>{% trans "Custom payment method" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
{% for paiement in paiement_list %} {% for paiement in paiement_list %}
<tr> <tr>
<td>{{ paiement.moyen }}</td> <td>{{ paiement.moyen }}</td>
<td>{{ paiement.available_for_everyone|tick }}</td>
<td>
{{paiement.get_payment_method_name}}
</td>
<td class="text-right"> <td class="text-right">
{% can_edit paiement %} {% can_edit paiement %}
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-paiement' paiement.id %}"> <a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-paiement' paiement.id %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
</a> </a>
{% acl_end %} {% acl_end %}
<a class="btn btn-info btn-sm" role="button" title="{% trans "Historique" %}" href="{% url 'cotisations:history' 'paiement' paiement.id %}"> {% history_button paiement %}
<i class="fa fa-history"></i>
</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -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 title %}{% trans "Invoices creation and edition" %}{% endblock %}
{% block content %} {% block content %}
{% bootstrap_form_errors factureform %} {% if title %}
<h3>{{title}}</h3>
{% else %}
<h3>{% trans "New invoice" %}</h3>
{% endif %}
{% if max_balance %}
<h4>{% trans "Maximum allowed balance : "%}{{max_balance}} €</h4>
{% endif %}
{% if balance is not None %}
<p>
{% trans "Current balance :" %} {{ balance }} €
</p>
{% endif %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form factureform %}
{% if payment_method %}
{% bootstrap_form payment_method %}
<div id="paymentMethod"></div>
{% endif %}
{% if articlesformset %} {% if articlesformset %}
<h3>{% trans "Invoice's articles" %}</h3> <h3>{% trans "Invoice's articles" %}</h3>
<div id="form_set" class="form-group"> <div id="form_set" class="form-group">
@ -56,14 +73,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endblocktrans %} {% endblocktrans %}
</p> </p>
{% endif %} {% endif %}
{% bootstrap_form factureform %}
{% bootstrap_button action_name button_type='submit' icon='star' %} {% bootstrap_button action_name button_type='submit' icon='star' %}
</form> </form>
{% if articlesformset %} {% if articlesformset or payment_method%}
<script type="text/javascript"> <script type="text/javascript">
{% if articlesformset %}
var prices = {}; var prices = {};
{% for article in articles %} {% for article in articlelist %}
prices[{{ article.id|escapejs }}] = {{ article.prix }}; prices[{{ article.id|escapejs }}] = {{ article.prix }};
{% endfor %} {% endfor %}
@ -134,6 +151,34 @@ with this program; if not, write to the Free Software Foundation, Inc.,
} }
update_price(); update_price();
}); });
{% endif %}
{% if payment_method.templates %}
var TEMPLATES = [
"",
{% for t in payment_method.templates %}
{% if t %}
`{% bootstrap_form t %}`,
{% else %}
"",
{% endif %}
{% endfor %}
];
function update_payment_method_form(){
var method = document.getElementById('paymentMethodSelect').value;
if(method==""){
method=0;
}
else{
method = Number(method);
method += 1;
}
console.log(method);
var html = TEMPLATES[method];
document.getElementById('paymentMethod').innerHTML = html;
}
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
{% endif %}
</script> </script>
{% endif %} {% endif %}

View file

@ -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 %}
<form class="form" method="post">
{% csrf_token %}
<h3>{% trans "New invoice" %}</h3>
<p>
{% blocktrans %}
User's balance : {{ user.solde }} €
{% endblocktrans %}
</p>
{% bootstrap_form factureform %}
{{ venteform.management_form }}
<!-- TODO: FIXME to include data-type="check" for right option in id_cheque select -->
<h3>{% trans "Invoice's articles" %}</h3>
<div id="form_set" class="form-group">
{% for form in venteform.forms %}
<div class='product_to_sell form-inline'>
{% trans "Article" %} : &nbsp;
{% bootstrap_form form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm" id="id_form-0-article-remove" type="button">
<span class="fa fa-times"></span>
</button>
</div>
{% endfor %}
</div>
<input class="btn btn-primary btn-sm" role="button" value="{% trans "Add an article"%}" id="add_one">
<p>
{% blocktrans %}
Total price : <span id="total_price">0,00</span>
{% endblocktrans %}
</p>
{% trans "Create" as tr_create %}
{% bootstrap_button tr_create button_type='submit' icon='star' %}
</form>
<script type="text/javascript">
var prices = {};
{% for article in articlelist %}
prices[{{ article.id|escapejs }}] = {{ article.prix }};
{% endfor %}
var template = `Article : &nbsp;
{% bootstrap_form venteform.empty_form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm"
id="id_form-__prefix__-article-remove" type="button">
<span class="fa fa-times"></span>
</button>`
function add_article(){
// Index start at 0 => new_index = number of items
var new_index =
document.getElementsByClassName('product_to_sell').length;
document.getElementById('id_form-TOTAL_FORMS').value ++;
var new_article = document.createElement('div');
new_article.className = 'product_to_sell form-inline';
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('form_set').appendChild(new_article);
add_listenner_for_id(new_index);
}
function update_price(){
var price = 0;
var product_count =
document.getElementsByClassName('product_to_sell').length;
var article, article_price, quantity;
for (i = 0; i < product_count; ++i){
article = document.getElementById(
'id_form-' + i.toString() + '-article').value;
if (article == '') {
continue;
}
article_price = prices[article];
quantity = document.getElementById(
'id_form-' + i.toString() + '-quantity').value;
price += article_price * quantity;
}
document.getElementById('total_price').innerHTML =
price.toFixed(2).toString().replace('.', ',');
}
function add_listenner_for_id(i){
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("onkeypress", update_price, true);
document.getElementById('id_form-' + i.toString() + '-quantity')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article-remove')
.addEventListener("click", function(event) {
var article = event.target.parentNode;
article.parentNode.removeChild(article);
document.getElementById('id_form-TOTAL_FORMS').value --;
update_price();
}
)
}
function set_cheque_info_visibility() {
var paiement = document.getElementById("id_Facture-paiement");
var visible = paiement.value == paiement.getAttribute('data-cheque');
p = document.getElementById("id_Facture-paiement");
var display = 'none';
if (visible) {
display = 'block';
}
document.getElementById("id_Facture-cheque")
.parentNode.style.display = display;
document.getElementById("id_Facture-banque")
.parentNode.style.display = display;
}
// Add events manager when DOM is fully loaded
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("add_one")
.addEventListener("click", add_article, true);
var product_count =
document.getElementsByClassName('product_to_sell').length;
for (i = 0; i < product_count; ++i){
add_listenner_for_id(i);
}
document.getElementById("id_Facture-paiement")
.addEventListener("change", set_cheque_info_visibility, true);
set_cheque_info_visibility();
update_price();
});
</script>
{% endblock %}

View file

@ -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 %}
<form class="form" method="post">
{% csrf_token %}
<h3>{% trans "New invoice" %}</h3>
{{ venteform.management_form }}
<!-- TODO: FIXME to include data-type="check" for right option in id_cheque select -->
<h3>{% trans "Invoice's articles" %}</h3>
<div id="form_set" class="form-group">
{% for form in venteform.forms %}
<div class='product_to_sell form-inline'>
{% trans "Article" %} : &nbsp;
{% bootstrap_form form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm" id="id_form-0-article-remove" type="button">
<span class="fa fa-times"></span>
</button>
</div>
{% endfor %}
</div>
<input class="btn btn-primary btn-sm" role="button" value="{% trans "Add an article"%}" id="add_one">
<p>
{% blocktrans %}
Total price : <span id="total_price">0,00</span>
{% endblocktrans %}
</p>
{% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type='submit' icon='star' %}
</form>
<script type="text/javascript">
var prices = {};
{% for article in articlelist %}
prices[{{ article.id|escapejs }}] = {{ article.prix }};
{% endfor %}
var template = `Article : &nbsp;
{% bootstrap_form venteform.empty_form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm" id="id_form-__prefix__-article-remove" type="button">
<span class="fa fa-times"></span>
</button>`
function add_article(){
// Index start at 0 => new_index = number of items
var new_index =
document.getElementsByClassName('product_to_sell').length;
document.getElementById('id_form-TOTAL_FORMS').value ++;
var new_article = document.createElement('div');
new_article.className = 'product_to_sell form-inline';
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('form_set').appendChild(new_article);
add_listenner_for_id(new_index);
}
function update_price(){
var price = 0;
var product_count =
document.getElementsByClassName('product_to_sell').length;
var article, article_price, quantity;
for (i = 0; i < product_count; ++i){
article = document.getElementById(
'id_form-' + i.toString() + '-article').value;
if (article == '') {
continue;
}
article_price = prices[article];
quantity = document.getElementById(
'id_form-' + i.toString() + '-quantity').value;
price += article_price * quantity;
}
document.getElementById('total_price').innerHTML =
price.toFixed(2).toString().replace('.', ',');
}
function add_listenner_for_id(i){
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("onkeypress", update_price, true);
document.getElementById('id_form-' + i.toString() + '-quantity')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article-remove')
.addEventListener("click", function(event) {
var article = event.target.parentNode;
article.parentNode.removeChild(article);
document.getElementById('id_form-TOTAL_FORMS').value --;
update_price();
}
)
}
function set_cheque_info_visibility() {
var paiement = document.getElementById("id_Facture-paiement");
var visible = paiement.value == paiement.getAttribute('data-cheque');
p = document.getElementById("id_Facture-paiement");
var display = 'none';
if (visible) {
display = 'block';
}
document.getElementById("id_Facture-cheque")
.parentNode.style.display = display;
document.getElementById("id_Facture-banque")
.parentNode.style.display = display;
}
// Add events manager when DOM is fully loaded
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("add_one")
.addEventListener("click", add_article, true);
var product_count =
document.getElementsByClassName('product_to_sell').length;
for (i = 0; i < product_count; ++i){
add_listenner_for_id(i);
}
document.getElementById("id_Facture-paiement")
.addEventListener("change", set_cheque_info_visibility, true);
set_cheque_info_visibility();
update_price();
});
</script>
{% endblock %}

View file

@ -32,11 +32,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %} {% block content %}
<h3> <h3>
{% blocktrans %} {% blocktrans %}
Refill of {{ amount }} € Pay {{ amount }} €
{% endblocktrans %} {% endblocktrans %}
</h3> </h3>
<form class="form" method="{{ method }}" action="{{ action }}"> <form class="form" method="{{ method | default:"post" }}" action="{{ action }}">
{{ content | safe }} {{ content | safe }}
{% if form %}
{% csrf_token %}
{% bootstrap_form form %}
{% endif %}
{% trans "Pay" as tr_pay %} {% trans "Pay" as tr_pay %}
{% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %} {% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %}
</form> </form>

View file

@ -27,9 +27,8 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
import re2o
from . import views from . import views
from . import payment from . import payment_methods
urlpatterns = [ urlpatterns = [
url( url(
@ -122,41 +121,10 @@ urlpatterns = [
views.index_paiement, views.index_paiement,
name='index-paiement' name='index-paiement'
), ),
url(
r'history/(?P<object_name>\w+)/(?P<object_id>[0-9]+)$',
re2o.views.history,
name='history',
kwargs={'application': 'cotisations'},
),
url( url(
r'^control/$', r'^control/$',
views.control, views.control,
name='control' name='control'
), ),
url(
r'^new_facture_solde/(?P<userid>[0-9]+)$',
views.new_facture_solde,
name='new_facture_solde'
),
url(
r'^recharge/$',
views.recharge,
name='recharge'
),
url(
r'^payment/accept/(?P<factureid>[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'), url(r'^$', views.index, name='index'),
] ] + payment_methods.urls.urlpatterns

32
cotisations/utils.py Normal file
View file

@ -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

21
cotisations/validators.py Normal file
View file

@ -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")
)

View file

@ -5,6 +5,7 @@
# Copyright © 2017 Gabriel Détraz # Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec # Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle # Copyright © 2017 Augustin Lemesle
# Copyright © 2018 Hugo Levy-Falk
# #
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@ -31,7 +32,7 @@ from __future__ import unicode_literals
import os import os
from django.urls import reverse 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.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.db.models import ProtectedError from django.db.models import ProtectedError
@ -56,11 +57,10 @@ from re2o.acl import (
can_delete_set, can_delete_set,
can_change, can_change,
) )
from preferences.models import OptionalUser, AssoOption, GeneralOption from preferences.models import AssoOption, GeneralOption
from .models import Facture, Article, Vente, Paiement, Banque from .models import Facture, Article, Vente, Paiement, Banque
from .forms import ( from .forms import (
NewFactureForm, FactureForm,
EditFactureForm,
ArticleForm, ArticleForm,
DelArticleForm, DelArticleForm,
PaiementForm, PaiementForm,
@ -70,11 +70,11 @@ from .forms import (
NewFactureFormPdf, NewFactureFormPdf,
SelectUserArticleForm, SelectUserArticleForm,
SelectClubArticleForm, SelectClubArticleForm,
CreditSoldeForm,
RechargeForm RechargeForm
) )
from . import payment as online_payment
from .tex import render_invoice from .tex import render_invoice
from .payment_methods.forms import payment_method_factory
from .utils import find_payment_method
@login_required @login_required
@ -84,29 +84,33 @@ def new_facture(request, user, userid):
""" """
View called to create a new invoice. View called to create a new invoice.
Currently, Send the list of available articles for the user along with 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. 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 If everything is correct, save each one of the articles, save the
purchase object associated and finally the newly created invoice. 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) invoice = Facture(user=user)
# The template needs the list of articles (for the JS part) # The template needs the list of articles (for the JS part)
article_list = Article.objects.filter( article_list = Article.objects.filter(
Q(type_user='All') | Q(type_user=request.user.class_name) Q(type_user='All') | Q(type_user=request.user.class_name)
) )
# Building the invocie form and the article formset # Building the invoice form and the article formset
invoice_form = NewFactureForm(request.POST or None, instance=invoice) invoice_form = FactureForm(
request.POST or None,
instance=invoice,
user=request.user,
creation=True
)
if request.user.is_class_club: if request.user.is_class_club:
article_formset = formset_factory(SelectClubArticleForm)( article_formset = formset_factory(SelectClubArticleForm)(
request.POST or None request.POST or None,
form_kwargs={'user': request.user}
) )
else: else:
article_formset = formset_factory(SelectUserArticleForm)( 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(): if invoice_form.is_valid() and article_formset.is_valid():
@ -114,42 +118,15 @@ def new_facture(request, user, userid):
articles = article_formset articles = article_formset
# Check if at leat one article has been selected # Check if at leat one article has been selected
if any(art.cleaned_data for art in articles): 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 # Building a purchase for each article sold
purchases = []
total_price = 0
for art_item in articles: for art_item in articles:
if art_item.cleaned_data: if art_item.cleaned_data:
article = art_item.cleaned_data['article'] article = art_item.cleaned_data['article']
quantity = art_item.cleaned_data['quantity'] quantity = art_item.cleaned_data['quantity']
new_purchase = Vente.objects.create( total_price += article.prix*quantity
new_purchase = Vente(
facture=new_invoice_instance, facture=new_invoice_instance,
name=article.name, name=article.name,
prix=article.prix, prix=article.prix,
@ -157,41 +134,42 @@ def new_facture(request, user, userid):
duration=article.duration, duration=article.duration,
number=quantity number=quantity
) )
new_purchase.save() purchases.append(new_purchase)
p = find_payment_method(new_invoice_instance.paiement)
# In case a cotisation was bought, inform the user, the if hasattr(p, 'check_price'):
# cotisation time has been extended too price_ok, msg = p.check_price(total_price, user)
if any(art_item.cleaned_data['article'].type_cotisation invoice_form.add_error(None, msg)
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
else: else:
messages.success( price_ok = True
request, if price_ok:
_("The invoice has been created.") 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( else:
'users:profil',
kwargs={'userid': userid}
))
messages.error( messages.error(
request, request,
_("You need to choose at least one article.") _("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( return form(
{ {
'factureform': invoice_form, 'factureform': invoice_form,
'venteform': article_formset, 'articlesformset': article_formset,
'articlelist': article_list '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) invoice_form = NewFactureFormPdf(request.POST or None)
if request.user.is_class_club: if request.user.is_class_club:
articles_formset = formset_factory(SelectClubArticleForm)( articles_formset = formset_factory(SelectClubArticleForm)(
request.POST or None request.POST or None,
form_kwargs={'user': request.user}
) )
else: else:
articles_formset = formset_factory(SelectUserArticleForm)( 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(): if invoice_form.is_valid() and articles_formset.is_valid():
# Get the article list and build an list out of it # Get the article list and build an list out of it
@ -313,7 +293,7 @@ def edit_facture(request, facture, **_kwargs):
can be set as desired. This is also the view used to invalidate can be set as desired. This is also the view used to invalidate
an invoice. an invoice.
""" """
invoice_form = EditFactureForm( invoice_form = FactureForm(
request.POST or None, request.POST or None,
instance=facture, instance=facture,
user=request.user user=request.user
@ -364,39 +344,6 @@ def del_facture(request, facture, **_kwargs):
}, 'cotisations/delete.html', request) }, '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)
@login_required @login_required
@can_create(Article) @can_create(Article)
def add_article(request): def add_article(request):
@ -419,7 +366,8 @@ def add_article(request):
return redirect(reverse('cotisations:index-article')) return redirect(reverse('cotisations:index-article'))
return form({ return form({
'factureform': article, 'factureform': article,
'action_name': _("Add") 'action_name': _("Add"),
'title': _("New article")
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -440,7 +388,8 @@ def edit_article(request, article_instance, **_kwargs):
return redirect(reverse('cotisations:index-article')) return redirect(reverse('cotisations:index-article'))
return form({ return form({
'factureform': article, 'factureform': article,
'action_name': _('Edit') 'action_name': _('Edit'),
'title': _("Edit article")
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -461,7 +410,8 @@ def del_article(request, instances):
return redirect(reverse('cotisations:index-article')) return redirect(reverse('cotisations:index-article'))
return form({ return form({
'factureform': article, 'factureform': article,
'action_name': _("Delete") 'action_name': _("Delete"),
'title': _("Delete article")
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -472,9 +422,15 @@ def add_paiement(request):
""" """
View used to add a payment method. View used to add a payment method.
""" """
payment = PaiementForm(request.POST or None) payment = PaiementForm(request.POST or None, prefix='payment')
if payment.is_valid(): payment_method = payment_method_factory(
payment.save() 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( messages.success(
request, request,
_("The payment method has been successfully created.") _("The payment method has been successfully created.")
@ -482,7 +438,9 @@ def add_paiement(request):
return redirect(reverse('cotisations:index-paiement')) return redirect(reverse('cotisations:index-paiement'))
return form({ return form({
'factureform': payment, 'factureform': payment,
'action_name': _("Add") 'payment_method': payment_method,
'action_name': _("Add"),
'title': _("New payment method")
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -493,10 +451,23 @@ def edit_paiement(request, paiement_instance, **_kwargs):
""" """
View used to edit a payment method. View used to edit a payment method.
""" """
payment = PaiementForm(request.POST or None, instance=paiement_instance) payment = PaiementForm(
if payment.is_valid(): request.POST or None,
if payment.changed_data: 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() payment.save()
if payment_method is not None:
payment_method.save()
messages.success( messages.success(
request, request,
_("The payement method has been successfully edited.") _("The payement method has been successfully edited.")
@ -504,7 +475,9 @@ def edit_paiement(request, paiement_instance, **_kwargs):
return redirect(reverse('cotisations:index-paiement')) return redirect(reverse('cotisations:index-paiement'))
return form({ return form({
'factureform': payment, 'factureform': payment,
'action_name': _("Edit") 'payment_method': payment_method,
'action_name': _("Edit"),
'title': _("Edit payment method")
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -539,7 +512,8 @@ def del_paiement(request, instances):
return redirect(reverse('cotisations:index-paiement')) return redirect(reverse('cotisations:index-paiement'))
return form({ return form({
'factureform': payment, 'factureform': payment,
'action_name': _("Delete") 'action_name': _("Delete"),
'title': _("Delete payment method")
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -560,7 +534,8 @@ def add_banque(request):
return redirect(reverse('cotisations:index-banque')) return redirect(reverse('cotisations:index-banque'))
return form({ return form({
'factureform': bank, 'factureform': bank,
'action_name': _("Add") 'action_name': _("Add"),
'title': _("New bank")
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -582,7 +557,8 @@ def edit_banque(request, banque_instance, **_kwargs):
return redirect(reverse('cotisations:index-banque')) return redirect(reverse('cotisations:index-banque'))
return form({ return form({
'factureform': bank, 'factureform': bank,
'action_name': _("Edit") 'action_name': _("Edit"),
'title': _("Edit bank")
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -617,7 +593,8 @@ def del_banque(request, instances):
return redirect(reverse('cotisations:index-banque')) return redirect(reverse('cotisations:index-banque'))
return form({ return form({
'factureform': bank, 'factureform': bank,
'action_name': _("Delete") 'action_name': _("Delete"),
'title': _("Delete bank")
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -722,153 +699,59 @@ def index(request):
}) })
# TODO : merge this function with new_facture() which is nearly the same # TODO : change solde to balance
# TODO : change facture to invoice
@login_required @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. View used to edit the balance of a user.
Currently, send the list of available articles for the user along with Can be use either to increase or decrease a user's balance.
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.
""" """
user = request.user try:
invoice = Facture(user=user) balance = find_payment_method(Paiement.objects.get(is_balance=True))
payment, _created = Paiement.objects.get_or_create(moyen='Solde') except Paiement.DoesNotExist:
invoice.paiement = payment credit_allowed = False
# 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
)
else: else:
article_formset = formset_factory(SelectUserArticleForm)( credit_allowed = (
request.POST or None balance is not None
and balance.can_credit_balance(request.user)
) )
if not credit_allowed:
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( messages.error(
request, request,
_("The balance is too low for this operation.") _("You are not allowed to credit your balance.")
) )
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': userid} kwargs={'userid': user.id}
))
# 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}
))
messages.error(
request,
_("You need to choose at least one article.")
)
return redirect(reverse(
'users:profil',
kwargs={'userid': userid}
)) ))
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) refill_form = RechargeForm(request.POST or None, user=request.user)
if refill_form.is_valid(): if refill_form.is_valid():
invoice = Facture(user=request.user) price = refill_form.cleaned_data['value']
payment, _created = Paiement.objects.get_or_create( invoice = Facture(user=user)
moyen='Rechargement en ligne' invoice.paiement = refill_form.cleaned_data['payment']
) p = find_payment_method(invoice.paiement)
invoice.paiement = payment if hasattr(p, 'check_price'):
invoice.valid = False 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() invoice.save()
purchase = Vente.objects.create( Vente.objects.create(
facture=invoice, facture=invoice,
name='solde', name='solde',
prix=refill_form.cleaned_data['value'], prix=refill_form.cleaned_data['value'],
number=1 number=1
) )
purchase.save() return invoice.paiement.end_payment(invoice, request)
content = online_payment.PAYMENT_SYSTEM[ p = get_object_or_404(Paiement, is_balance=True)
AssoOption.get_cached_value('payment')
](invoice, request)
return render(request, 'cotisations/payment.html', content)
return form({ return form({
'rechargeform': refill_form 'factureform': refill_form,
}, 'cotisations/recharge.html', request) 'balance': request.user.solde,
'title': _("Refill your balance"),
'action_name': _("Pay"),
'max_balance': p.payment_method.maximum_balance,
}, 'cotisations/facture.html', request)

View file

@ -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 %}
<div class="panel panel-default">
<div class="panel-heading clearfix" data-parent="#accordion" data-toggle="collapse" data-target="#collapse{{droit.id}}">
<h2 class="panel-title pull-left">
<i class="fa fa-address-book"></i>
{{droit}}
<span class="badge">{{users.count}}</span>
</h2>
</div>
<div class="panel-collapse collapse" id="collapse{{droit.id}}">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Pseudo</th>
<th>Adhésion</th>
<th>Derniere connexion</th>
<th>Nombre d'actions</th>
<th>Date de la dernière action</th>
<th></th>
</tr>
</thead>
{% for utilisateur in users %}
<tr>
<td>{{ utilisateur.pseudo }}</td>
{% if utilisateur.is_adherent %}
<td><p class="text-success">Adhérent</p></td>
{% elif not utilisateur.end_adhesion %}
<td><p class="text-warning">On ne s'en souvient plus...</p></td>
{% else %}
<td><p class="text-danger">Plus depuis {{ utilisateur.end_adhesion }}</p></td>
{% endif %}
<td>{{ utilisateur.last_login }}</td>
<td>{{ utilisateur.num }}</td>
{% if not utilisateur.last %}
<td><p class="text-danger">Jamais</p></td>
{% else %}
<td><p class="text-success">{{utilisateur.last}}</p></td>
{% endif %}
<td>
{% if droit != 'Superuser' %}
<a href="{% url 'users:del-group' utilisateur.id droit.id %}">
{% else %}
<a href="{% url 'users:del-superuser' utilisateur.id %}">
{% endif %}
<button type="button" class="btn btn-danger" aria-label="Left Align">
<span class="fa fa-user-times" aria-hidden="true"></span>
</button>
</a>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
{% endfor %}

View file

@ -51,9 +51,5 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-users"></i> <i class="fa fa-users"></i>
Utilisateurs Utilisateurs
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "logs:stats-droits" %}">
<i class="fa fa-balance-scale"></i>
Groupes de droit
</a>
{% acl_end %} {% acl_end %}
{% endblock %} {% endblock %}

View file

@ -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 %}
<h2>Statistiques des droits</h2>
{% include "logs/aff_stats_droits.html" with stats_list=stats_list %}
<br />
<br />
<br />
{% endblock %}

View file

@ -33,3 +33,23 @@ register = template.Library()
def classname(obj): def classname(obj):
""" Returns the object class name """ """ Returns the object class name """
return obj.__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,
}

View file

@ -39,5 +39,9 @@ urlpatterns = [
url(r'^stats_models/$', views.stats_models, name='stats-models'), url(r'^stats_models/$', views.stats_models, name='stats-models'),
url(r'^stats_users/$', views.stats_users, name='stats-users'), url(r'^stats_users/$', views.stats_users, name='stats-users'),
url(r'^stats_actions/$', views.stats_actions, name='stats-actions'), url(r'^stats_actions/$', views.stats_actions, name='stats-actions'),
url(r'^stats_droits/$', views.stats_droits, name='stats-droits'), url(
r'(?P<application>\w+)/(?P<object_name>\w+)/(?P<object_id>[0-9]+)$',
views.history,
name='history',
),
] ]

View file

@ -2,9 +2,10 @@
# se veut agnostique au réseau considéré, de manière à être installable en # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
# #
# Copyright © 2017 Gabriel Détraz # Copyright © 2018 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec # Copyright © 2018 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle # Copyright © 2018 Augustin Lemesle
# Copyright © 2018 Hugo Levy-Falk
# #
# This program is free software; you can redistribute it and/or modify # 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 # 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 __future__ import unicode_literals
from itertools import chain
from django.urls import reverse from django.urls import reverse
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required 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 Revision
from reversion.models import Version, ContentType from reversion.models import Version, ContentType
@ -455,25 +460,56 @@ def stats_actions(request):
return render(request, 'logs/stats_users.html', {'stats_list': stats}) return render(request, 'logs/stats_users.html', {'stats_list': stats})
@login_required def history(request, application, object_name, object_id):
@can_view_app('users') """Render history for a model.
def stats_droits(request):
"""Affiche la liste des droits et les users ayant chaque droit"""
stats_list = {}
for droit in ListRight.objects.all().select_related('group_ptr'): The model is determined using the `HISTORY_BIND` dictionnary if none is
stats_list[droit] = droit.user_set.all().annotate( found, raises a Http404. The view checks if the user is allowed to see the
num=Count('revision'), history using the `can_view` method of the model.
last=Max('revision__date_created'),
)
stats_list['Superuser'] = User.objects.filter(is_superuser=True).annotate( Args:
num=Count('revision'), request: The request sent by the user.
last=Max('revision__date_created'), 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( return render(
request, request,
'logs/stats_droits.html', 're2o/history.html',
{'stats_list': stats_list} {'reversions': reversions, 'object': instance}
) )

View file

@ -37,6 +37,7 @@ from .models import (
Ns, Ns,
Vlan, Vlan,
Txt, Txt,
DName,
Srv, Srv,
Nas, Nas,
Service, Service,
@ -95,6 +96,10 @@ class TxtAdmin(VersionAdmin):
""" Admin view of a TXT object """ """ Admin view of a TXT object """
pass pass
class DNameAdmin(VersionAdmin):
""" Admin view of a DName object """
pass
class SrvAdmin(VersionAdmin): class SrvAdmin(VersionAdmin):
""" Admin view of a SRV object """ """ Admin view of a SRV object """
@ -144,6 +149,7 @@ admin.site.register(SOA, SOAAdmin)
admin.site.register(Mx, MxAdmin) admin.site.register(Mx, MxAdmin)
admin.site.register(Ns, NsAdmin) admin.site.register(Ns, NsAdmin)
admin.site.register(Txt, TxtAdmin) admin.site.register(Txt, TxtAdmin)
admin.site.register(DName, DNameAdmin)
admin.site.register(Srv, SrvAdmin) admin.site.register(Srv, SrvAdmin)
admin.site.register(IpList, IpListAdmin) admin.site.register(IpList, IpListAdmin)
admin.site.register(Interface, InterfaceAdmin) admin.site.register(Interface, InterfaceAdmin)

View file

@ -51,6 +51,7 @@ from .models import (
SOA, SOA,
Mx, Mx,
Txt, Txt,
DName,
Ns, Ns,
Service, Service,
Vlan, Vlan,
@ -410,6 +411,34 @@ class DelTxtForm(FormRevMixin, Form):
self.fields['txt'].queryset = Txt.objects.all() 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): class SrvForm(FormRevMixin, ModelForm):
"""Ajout d'un srv pour une zone""" """Ajout d'un srv pour une zone"""
class Meta: class Meta:

View file

@ -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),
]

View file

@ -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),
),
]

View file

@ -3,9 +3,10 @@
# se veut agnostique au réseau considéré, de manière à être installable en # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
# #
# Copyright © 2017 Gabriel Détraz # Copyright © 2016-2018 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec # Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle # Copyright © 2017 Augustin Lemesle
# Copyright © 2018 Charlie Jacomme
# #
# This program is free software; you can redistribute it and/or modify # 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 # 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) 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): class Srv(RevMixin, AclMixin, models.Model):
""" A SRV record """ """ A SRV record """
PRETTY_NAME = "Enregistrement Srv" PRETTY_NAME = "Enregistrement Srv"
@ -1687,6 +1709,18 @@ def text_post_delete(**_kwargs):
regen('dns') 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) @receiver(post_save, sender=Srv)
def srv_post_save(**_kwargs): def srv_post_save(**_kwargs):
"""Regeneration dns après modification d'un SRV""" """Regeneration dns après modification d'un SRV"""

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -38,7 +39,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit alias %} {% can_edit alias %}
{% include 'buttons/edit.html' with href='machines:edit-alias' id=alias.id %} {% include 'buttons/edit.html' with href='machines:edit-alias' id=alias.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='domain' id=alias.id %} {% history_button alias %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -1,12 +1,9 @@
{% extends "cotisations/sidebar.html" %}
{% comment %} {% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il 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 se veut agnostique au réseau considéré, de manière à être installable en
quelques clics. quelques clics.
Copyright © 2017 Gabriel Détraz Copyright © 2018 Charlie Jacomme
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify 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 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. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load acl %}
{% load staticfiles%}
{% load i18n %} <table class="table table-striped">
<thead>
<tr>
<th>Target zone</th>
<th>Record</th>
<th></th>
</tr>
</thead>
{% for dname in dname_list %}
<tr>
<td>{{ dname.zone }}</td>
<td>{{ dname.dns_entry }}</td>
<td class="text-right">
{% 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 %}
</td>
</tr>
{% endfor %}
</table>
{% block title %}{% trans "Balance refill" %}{% endblock %}
{% block content %}
<h2>{% trans "Balance refill" %}</h2>
<h3>
{% blocktrans %}
Balance : <span class="label label-default">{{ request.user.solde }} €</span>
{% endblocktrans %}
</h3>
<form class="form" method="post">
{% csrf_token %}
{% bootstrap_form rechargeform %}
{% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type='submit' icon='piggy-bank' %}
</form>
{% endblock %}

View file

@ -23,6 +23,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
{% load design %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
@ -41,7 +43,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% for extension in extension_list %} {% for extension in extension_list %}
<tr> <tr>
<td>{{ extension.name }}</td> <td>{{ extension.name }}</td>
<td>{{ extension.need_infra }}</td> <td>{{ extension.need_infra|tick }}</td>
<td>{{ extension.soa}}</td> <td>{{ extension.soa}}</td>
<td>{{ extension.origin }}</td> <td>{{ extension.origin }}</td>
{% if ipv6_enabled %} {% if ipv6_enabled %}
@ -51,7 +53,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit extension %} {% can_edit extension %}
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %} {% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='extension' id=extension.id %} {% history_button extension %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -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. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %} {% endcomment %}
{% load design %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -42,7 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td>{{ type.type }}</td> <td>{{ type.type }}</td>
<td>{{ type.extension }}</td> <td>{{ type.extension }}</td>
<td>{{ type.need_infra }}</td> <td>{{ type.need_infra|tick }}</td>
<td>{{ type.domaine_ip_start }}-{{ type.domaine_ip_stop }}</td> <td>{{ type.domaine_ip_start }}-{{ type.domaine_ip_stop }}</td>
<td>{{ type.prefix_v6 }}</td> <td>{{ type.prefix_v6 }}</td>
<td>{{ type.vlan }}</td> <td>{{ type.vlan }}</td>
@ -51,7 +54,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit type %} {% can_edit type %}
{% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %} {% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='iptype' id=type.id %} {% history_button type %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -43,7 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_delete ipv6 %} {% can_delete ipv6 %}
{% include 'buttons/suppr.html' with href='machines:del-ipv6list' id=ipv6.id %} {% include 'buttons/suppr.html' with href='machines:del-ipv6list' id=ipv6.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='ipv6list' id=ipv6.id %} {% history_button ipv6 %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<div class="table-responsive"> <div class="table-responsive">
{% if machines_list.paginator %} {% if machines_list.paginator %}
@ -56,7 +57,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_create Interface machine.id %} {% can_create Interface machine.id %}
{% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc='Ajouter une interface' %} {% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc='Ajouter une interface' %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='machine' id=machine.id %} {% history_button machine %}
{% can_delete machine %} {% can_delete machine %}
{% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %} {% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %}
{% acl_end %} {% acl_end %}
@ -127,7 +128,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% acl_end %} {% acl_end %}
</ul> </ul>
</div> </div>
{% include 'buttons/history.html' with href='machines:history' name='interface' id=interface.id %} {% history_button interface %}
{% can_delete interface %} {% can_delete interface %}
{% include 'buttons/suppr.html' with href='machines:del-interface' id=interface.id %} {% include 'buttons/suppr.html' with href='machines:del-interface' id=interface.id %}
{% acl_end %} {% acl_end %}

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -40,7 +41,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit type %} {% can_edit type %}
{% include 'buttons/edit.html' with href='machines:edit-machinetype' id=type.id %} {% include 'buttons/edit.html' with href='machines:edit-machinetype' id=type.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='machinetype' id=type.id %} {% history_button type %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -43,7 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit mx %} {% can_edit mx %}
{% include 'buttons/edit.html' with href='machines:edit-mx' id=mx.id %} {% include 'buttons/edit.html' with href='machines:edit-mx' id=mx.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='mx' id=mx.id %} {% history_button mx %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -23,6 +23,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
{% load design %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -41,12 +43,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ nas.nas_type }}</td> <td>{{ nas.nas_type }}</td>
<td>{{ nas.machine_type }}</td> <td>{{ nas.machine_type }}</td>
<td>{{ nas.port_access_mode }}</td> <td>{{ nas.port_access_mode }}</td>
<td>{{ nas.autocapture_mac }}</td> <td>{{ nas.autocapture_mac|tick }}</td>
<td class="text-right"> <td class="text-right">
{% can_edit nas %} {% can_edit nas %}
{% include 'buttons/edit.html' with href='machines:edit-nas' id=nas.id %} {% include 'buttons/edit.html' with href='machines:edit-nas' id=nas.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='nas' id=nas.id %} {% history_button nas %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -41,7 +42,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit ns %} {% can_edit ns %}
{% include 'buttons/edit.html' with href='machines:edit-ns' id=ns.id %} {% include 'buttons/edit.html' with href='machines:edit-ns' id=ns.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='ns' id=ns.id %} {% history_button ns %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -22,6 +22,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %} {% endcomment %}
{% load design %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -37,8 +39,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ server.service }}</td> <td>{{ server.service }}</td>
<td>{{ server.server }}</td> <td>{{ server.server }}</td>
<td>{{ server.last_regen }}</td> <td>{{ server.last_regen }}</td>
<td>{{ server.asked_regen }}</td> <td>{{ server.asked_regen| tick }}</td>
<td>{{ server.need_regen }}</td> <td>{{ server.need_regen | tick }}</td>
<td class="text-right"> <td class="text-right">
</td> </td>
</tr> </tr>

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -45,7 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit service %} {% can_edit service %}
{% include 'buttons/edit.html' with href='machines:edit-service' id=service.id %} {% include 'buttons/edit.html' with href='machines:edit-service' id=service.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='service' id=service.id %} {% history_button service %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -49,7 +50,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit soa %} {% can_edit soa %}
{% include 'buttons/edit.html' with href='machines:edit-soa' id=soa.id %} {% include 'buttons/edit.html' with href='machines:edit-soa' id=soa.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='soa' id=soa.id %} {% history_button soa %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -53,7 +54,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit srv %} {% can_edit srv %}
{% include 'buttons/edit.html' with href='machines:edit-srv' id=srv.id %} {% include 'buttons/edit.html' with href='machines:edit-srv' id=srv.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='srv' id=srv.id %} {% history_button srv %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -41,7 +42,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit txt %} {% can_edit txt %}
{% include 'buttons/edit.html' with href='machines:edit-txt' id=txt.id %} {% include 'buttons/edit.html' with href='machines:edit-txt' id=txt.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='txt' id=txt.id %} {% history_button txt %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
@ -45,7 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit vlan %} {% can_edit vlan %}
{% include 'buttons/edit.html' with href='machines:edit-vlan' id=vlan.id %} {% include 'buttons/edit.html' with href='machines:edit-vlan' id=vlan.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='machines:history' name='vlan' id=vlan.id %} {% history_button vlan %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -61,6 +61,18 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-txt' %}"><i class="fa fa-trash"></i> Supprimer un enregistrement TXT</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-txt' %}"><i class="fa fa-trash"></i> Supprimer un enregistrement TXT</a>
{% include "machines/aff_txt.html" with txt_list=txt_list %} {% include "machines/aff_txt.html" with txt_list=txt_list %}
<h2>DNAME records</h2>
{% can_create DName %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-dname' %}">
<i class="fa fa-plus"></i> {% trans "Add a DNAME record" %}
</a>
{% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-dname' %}">
<i class="fa fa-trash"></i> {% trans "Delete DNAME records" %}
</a>
{% include "machines/aff_dname.html" with dname_list=dname_list %}
<h2>Liste des enregistrements SRV</h2> <h2>Liste des enregistrements SRV</h2>
{% can_create Srv %} {% can_create Srv %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-srv' %}"><i class="fa fa-plus"></i> Ajouter un enregistrement SRV</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-srv' %}"><i class="fa fa-plus"></i> Ajouter un enregistrement SRV</a>

View file

@ -57,6 +57,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if txtform %} {% if txtform %}
{% bootstrap_form_errors txtform %} {% bootstrap_form_errors txtform %}
{% endif %} {% endif %}
{% if dnameform %}
{% bootstrap_form_errors dnameform %}
{% endif %}
{% if srvform %} {% if srvform %}
{% bootstrap_form_errors srvform %} {% bootstrap_form_errors srvform %}
{% endif %} {% endif %}
@ -122,6 +125,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<h3>Enregistrement TXT</h3> <h3>Enregistrement TXT</h3>
{% bootstrap_form txtform %} {% bootstrap_form txtform %}
{% endif %} {% endif %}
{% if dnameform %}
<h3>DNAME record</h3>
{% bootstrap_form dnameform %}
{% endif %}
{% if srvform %} {% if srvform %}
<h3>Enregistrement SRV</h3> <h3>Enregistrement SRV</h3>
{% massive_bootstrap_form srvform 'target' %} {% massive_bootstrap_form srvform 'target' %}

View file

@ -27,7 +27,6 @@ The defined URLs for the Cotisations app
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
import re2o
from . import views from . import views
urlpatterns = [ urlpatterns = [
@ -74,6 +73,9 @@ urlpatterns = [
url(r'^add_txt/$', views.add_txt, name='add-txt'), url(r'^add_txt/$', views.add_txt, name='add-txt'),
url(r'^edit_txt/(?P<txtid>[0-9]+)$', views.edit_txt, name='edit-txt'), url(r'^edit_txt/(?P<txtid>[0-9]+)$', views.edit_txt, name='edit-txt'),
url(r'^del_txt/$', views.del_txt, name='del-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<dnameid>[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'^add_ns/$', views.add_ns, name='add-ns'),
url(r'^edit_ns/(?P<nsid>[0-9]+)$', views.edit_ns, name='edit-ns'), url(r'^edit_ns/(?P<nsid>[0-9]+)$', views.edit_ns, name='edit-ns'),
url(r'^del_ns/$', views.del_ns, name='del-ns'), url(r'^del_ns/$', views.del_ns, name='del-ns'),
@ -119,10 +121,6 @@ urlpatterns = [
url(r'^edit_nas/(?P<nasid>[0-9]+)$', views.edit_nas, name='edit-nas'), url(r'^edit_nas/(?P<nasid>[0-9]+)$', views.edit_nas, name='edit-nas'),
url(r'^del_nas/$', views.del_nas, name='del-nas'), url(r'^del_nas/$', views.del_nas, name='del-nas'),
url(r'^index_nas/$', views.index_nas, name='index-nas'), url(r'^index_nas/$', views.index_nas, name='index-nas'),
url(r'history/(?P<object_name>\w+)/(?P<object_id>[0-9]+)$',
re2o.views.history,
name='history',
kwargs={'application': 'machines'}),
url(r'^$', views.index, name='index'), url(r'^$', views.index, name='index'),
url(r'^rest/mac-ip/$', views.mac_ip, name='mac-ip'), url(r'^rest/mac-ip/$', views.mac_ip, name='mac-ip'),
url(r'^rest/regen-achieved/$', url(r'^rest/regen-achieved/$',

View file

@ -3,10 +3,11 @@
# se veut agnostique au réseau considéré, de manière à être installable en # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
# #
# Copyright © 2017 Gabriel Détraz # Copyright © 2016-2018 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec # Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle # 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 # 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 # it under the terms of the GNU General Public License as published by
@ -93,6 +94,8 @@ from .forms import (
DelNsForm, DelNsForm,
TxtForm, TxtForm,
DelTxtForm, DelTxtForm,
DNameForm,
DelDNameForm,
MxForm, MxForm,
DelMxForm, DelMxForm,
VlanForm, VlanForm,
@ -122,6 +125,7 @@ from .models import (
Vlan, Vlan,
Nas, Nas,
Txt, Txt,
DName,
Srv, Srv,
OuverturePortList, OuverturePortList,
OuverturePort, OuverturePort,
@ -815,6 +819,63 @@ def del_ns(request, instances):
request 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 @login_required
@can_create(Txt) @can_create(Txt)
@ -1272,7 +1333,7 @@ def index_nas(request):
@login_required @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): def index_extension(request):
""" View displaying the list of existing extensions, the list of """ View displaying the list of existing extensions, the list of
existing SOA records, the list of existing MX records , 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('zone')
.select_related('ns__extension')) .select_related('ns__extension'))
txt_list = Txt.objects.all().select_related('zone') txt_list = Txt.objects.all().select_related('zone')
dname_list = DName.objects.all().select_related('zone')
srv_list = (Srv.objects srv_list = (Srv.objects
.all() .all()
.select_related('extension') .select_related('extension')
@ -1305,6 +1367,7 @@ def index_extension(request):
'mx_list': mx_list, 'mx_list': mx_list,
'ns_list': ns_list, 'ns_list': ns_list,
'txt_list': txt_list, 'txt_list': txt_list,
'dname_list': dname_list,
'srv_list': srv_list 'srv_list': srv_list
} }
) )

View file

@ -38,6 +38,7 @@ from .models import (
Service Service
) )
class EditOptionalUserForm(ModelForm): class EditOptionalUserForm(ModelForm):
"""Formulaire d'édition des options de l'user. (solde, telephone..)""" """Formulaire d'édition des options de l'user. (solde, telephone..)"""
class Meta: class Meta:
@ -54,13 +55,6 @@ class EditOptionalUserForm(ModelForm):
self.fields['is_tel_mandatory'].label = ( self.fields['is_tel_mandatory'].label = (
'Exiger un numéro de téléphone' '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' self.fields['self_adhesion'].label = 'Auto inscription'

View file

@ -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."),
),
]

View file

@ -3,7 +3,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
import preferences.aes_field
class Migration(migrations.Migration): class Migration(migrations.Migration):

View file

@ -3,7 +3,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models 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): class Migration(migrations.Migration):
@ -16,7 +19,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='assooption', model_name='assooption',
name='payment_pass', 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( migrations.AlterField(
model_name='assooption', model_name='assooption',

View file

@ -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',
),
]

View file

@ -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',
),
]

View file

@ -31,12 +31,9 @@ from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.core.cache import cache from django.core.cache import cache
import cotisations.models
import machines.models import machines.models
from re2o.mixins import AclMixin from re2o.mixins import AclMixin
from .aes_field import AESEncryptedField
class PreferencesModel(models.Model): class PreferencesModel(models.Model):
""" Base object for the Preferences objects """ Base object for the Preferences objects
@ -67,22 +64,6 @@ class OptionalUser(AclMixin, PreferencesModel):
PRETTY_NAME = "Options utilisateur" PRETTY_NAME = "Options utilisateur"
is_tel_mandatory = models.BooleanField(default=True) 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) gpg_fingerprint = models.BooleanField(default=True)
all_can_create_club = models.BooleanField( all_can_create_club = models.BooleanField(
default=False, default=False,
@ -108,14 +89,6 @@ class OptionalUser(AclMixin, PreferencesModel):
("view_optionaluser", "Peut voir les options de l'user"), ("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) @receiver(post_save, sender=OptionalUser)
def optionaluser_post_save(**kwargs): def optionaluser_post_save(**kwargs):
@ -294,25 +267,6 @@ class AssoOption(AclMixin, PreferencesModel):
blank=True, blank=True,
null=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( description = models.TextField(
null=True, null=True,
blank=True, blank=True,

View file

@ -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. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -43,7 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit service%} {% can_edit service%}
{% include 'buttons/edit.html' with href='preferences:edit-service' id=service.id %} {% include 'buttons/edit.html' with href='preferences:edit-service' id=service.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='preferences:history' name='service' id=service.id %} {% history_button service %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load acl %} {% load acl %}
{% load design %}
{% block title %}Création et modification des préférences{% endblock %} {% block title %}Création et modification des préférences{% endblock %}
@ -39,39 +40,25 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Téléphone obligatoirement requis</th> <th>Téléphone obligatoirement requis</th>
<td>{{ useroptions.is_tel_mandatory }}</td> <td>{{ useroptions.is_tel_mandatory|tick }}</td>
<th>Activation du solde pour les utilisateurs</th> <th>Auto inscription</th>
<td>{{ useroptions.user_solde }}</td> <td>{{ useroptions.self_adhesion|tick }}</td>
</tr> </tr>
<tr> <tr>
<th>Champ gpg fingerprint</th> <th>Champ gpg fingerprint</th>
<td>{{ useroptions.gpg_fingerprint }}</td> <td>{{ useroptions.gpg_fingerprint|tick }}</td>
{% if useroptions.user_solde %}
<th>Solde négatif</th>
<td>{{ useroptions.solde_negatif }}</td>
{% endif %}
</tr>
<tr>
<th>Creations d'adhérents par tous</th>
<td>{{ useroptions.all_can_create_adherent }}</td>
<th>Creations de clubs par tous</th>
<td>{{ useroptions.all_can_create_club }}</td>
</tr>
{% if useroptions.user_solde %}
<tr>
<th>Solde maximum</th>
<td>{{ useroptions.max_solde }}</td>
<th>Montant minimal de rechargement en ligne</th>
<td>{{ useroptions.min_online_payment }}</td>
</tr>
{% endif %}
<tr>
<th>Auto inscription</th>
<td>{{ useroptions.self_adhesion }}</td>
<th>Shell par défaut des utilisateurs</th> <th>Shell par défaut des utilisateurs</th>
<td>{{ useroptions.shell_default }}</td> <td>{{ useroptions.shell_default }}</td>
</tr> </tr>
<tr>
<th>Creations d'adhérents par tous</th>
<td>{{ useroptions.all_can_create_adherent|tick }}</td>
<th>Creations de clubs par tous</th>
<td>{{ useroptions.all_can_create_club|tick }}</td>
</tr>
</table> </table>
<h4>Préférences machines</h4> <h4>Préférences machines</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
@ -82,7 +69,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Mot de passe par machine</th> <th>Mot de passe par machine</th>
<td>{{ machineoptions.password_machine }}</td> <td>{{ machineoptions.password_machine|tick }}</td>
<th>Machines/interfaces autorisées par utilisateurs</th> <th>Machines/interfaces autorisées par utilisateurs</th>
<td>{{ machineoptions.max_lambdauser_interfaces }}</td> <td>{{ machineoptions.max_lambdauser_interfaces }}</td>
</tr> </tr>
@ -94,7 +81,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
<tr> <tr>
<th>Creation de machines</th> <th>Creation de machines</th>
<td>{{ machineoptions.create_machine }}</td> <td>{{ machineoptions.create_machine|tick }}</td>
</tr> </tr>
</table> </table>
<h4>Préférences topologie</h4> <h4>Préférences topologie</h4>
@ -185,12 +172,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<th>Objet utilisateur de l'association</th> <th>Objet utilisateur de l'association</th>
<td>{{ assooptions.utilisateur_asso }}</td> <td>{{ assooptions.utilisateur_asso }}</td>
<th>Moyen de paiement automatique</th>
<td>{{ assooptions.payment }}</td>
</tr>
<tr>
<th>Description de l'association</th> <th>Description de l'association</th>
<td colspan="3">{{ assooptions.description | safe }}</td> <td>{{ assooptions.description | safe }}</td>
</tr> </tr>
</table> </table>

View file

@ -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.

View file

@ -27,7 +27,6 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
import re2o
from . import views from . import views
@ -74,11 +73,5 @@ urlpatterns = [
name='edit-service' name='edit-service'
), ),
url(r'^del_services/$', views.del_services, name='del-services'), url(r'^del_services/$', views.del_services, name='del-services'),
url(
r'^history/(?P<object_name>\w+)/(?P<object_id>[0-9]+)$',
re2o.views.history,
name='history',
kwargs={'application': 'preferences'},
),
url(r'^$', views.display_options, name='display-options'), url(r'^$', views.display_options, name='display-options'),
] ]

View file

@ -7,6 +7,7 @@
# Copyright © 2017 Goulven Kermarec # Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle # Copyright © 2017 Augustin Lemesle
# Copyright © 2018 Maël Kervella # Copyright © 2018 Maël Kervella
# Copyright © 2018 Hugo Levy-Falk
# #
# This program is free software; you can redistribute it and/or modify # 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 # 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., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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 Module defining a AESEncryptedField object that can be used in forms
to handle the use of properly encrypting and decrypting AES keys 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 Crypto.Cipher import AES
from django.db import models from django.db import models
from django import forms
from django.conf import settings from django.conf import settings
EOD = '`%EofD%`' # This should be something that will not occur in strings 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] return ss.split(bytes(EOD, 'utf-8'))[0]
class AESEncryptedFormField(forms.CharField):
widget = forms.PasswordInput(render_value=True)
class AESEncryptedField(models.CharField): class AESEncryptedField(models.CharField):
""" A Field that can be used in forms for adding the support """ A Field that can be used in forms for adding the support
of AES ecnrypted fields """ of AES ecnrypted fields """
def save_form_data(self, instance, data): def save_form_data(self, instance, data):
setattr(instance, self.name, setattr(instance, self.name, binascii.b2a_base64(
binascii.b2a_base64(encrypt(settings.AES_KEY, data))) encrypt(settings.AES_KEY, data)).decode('utf-8'))
def to_python(self, value): def to_python(self, value):
if value is None: if value is None:
return None return None
try:
return decrypt(settings.AES_KEY, return decrypt(settings.AES_KEY,
binascii.a2b_base64(value)).decode('utf-8') 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): def get_prep_value(self, value):
if value is None: if value is None:
@ -85,4 +101,9 @@ class AESEncryptedField(models.CharField):
return binascii.b2a_base64(encrypt( return binascii.b2a_base64(encrypt(
settings.AES_KEY, settings.AES_KEY,
value value
)) )).decode('utf-8')
def formfield(self, **kwargs):
defaults = {'form_class': AESEncryptedFormField}
defaults.update(kwargs)
return super().formfield(**defaults)

View file

@ -21,8 +21,10 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Fonction de context, variables renvoyées à toutes les vues""" """Fonction de context, variables renvoyées à toutes les vues"""
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime
from django.contrib import messages from django.contrib import messages
from preferences.models import GeneralOption, OptionalMachine from preferences.models import GeneralOption, OptionalMachine
@ -47,3 +49,12 @@ def context_user(request):
'name_website': GeneralOption.get_cached_value('site_name'), 'name_website': GeneralOption.get_cached_value('site_name'),
'ipv6_enabled': OptionalMachine.get_cached_value('ipv6'), '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()
}

View file

@ -125,6 +125,7 @@ TEMPLATES = [
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'django.template.context_processors.request', 'django.template.context_processors.request',
're2o.context_processors.context_user', 're2o.context_processors.context_user',
're2o.context_processors.date_now',
], ],
}, },
}, },

View file

@ -74,83 +74,42 @@ import sys
from django import template from django import template
from django.template.base import Node, NodeList 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() 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): def get_model(model_name):
"""Retrieve the model object from its name""" """Retrieve the model object from its name"""
splitted = model_name.split('.')
if len(splitted) > 1:
try: try:
return MODEL_NAME[model_name] app_label, name = splitted
except KeyError: except ValueError:
raise template.TemplateSyntaxError(
"%r is an inconsistent model name" % model_name
)
else:
app_label, name = None, splitted[0]
try:
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( raise template.TemplateSyntaxError(
"%r is not a valid model for an acl tag" % model_name "%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): def get_callback(tag_name, obj=None):

View file

@ -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 = '<i style="color: #1ECA18;" class="fas fa-check"></i>'
else:
result = '<i style="color: #D10115;" class="fas fa-times"></i>'
return mark_safe(result)
else: # if the value is not a boolean, display it as if tick was not called
return valeur

View file

@ -50,6 +50,7 @@ from django.contrib.auth import views as auth_views
from .views import index, about_page from .views import index, about_page
handler500 = 're2o.views.handler500' handler500 = 're2o.views.handler500'
handler404 = 're2o.views.handler404'
urlpatterns = [ urlpatterns = [
url(r'^$', index, name='index'), url(r'^$', index, name='index'),

View file

@ -26,33 +26,20 @@ les views
from __future__ import unicode_literals from __future__ import unicode_literals
from itertools import chain
import git import git
from reversion.models import Version
from django.http import Http404 from django.shortcuts import render
from django.urls import reverse
from django.shortcuts import render, redirect
from django.template.context_processors import csrf 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.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
import preferences
from preferences.models import ( from preferences.models import (
Service, Service,
GeneralOption,
AssoOption, AssoOption,
HomeOption HomeOption
) )
import users
import cotisations
import topologie
import machines
from .utils import re2o_paginator
from .contributors import CONTRIBUTORS from .contributors import CONTRIBUTORS
@ -81,113 +68,6 @@ def index(request):
}, 're2o/index.html', request) }, '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}
)
@cache_page(7 * 24 * 60 * 60) @cache_page(7 * 24 * 60 * 60)
def about_page(request): def about_page(request):
""" The view for the about page. """ The view for the about page.
@ -230,3 +110,8 @@ def about_page(request):
def handler500(request): def handler500(request):
"""The handler view for a 500 error""" """The handler view for a 500 error"""
return render(request, 'errors/500.html') return render(request, 'errors/500.html')
def handler404(request):
"""The handler view for a 404 error"""
return render(request, 'errors/404.html')

View file

@ -108,3 +108,9 @@ footer a {
overflow-y: visible; overflow-y: visible;
} }
/* Make modal wider on wide screens */
@media (min-width: 1024px) {
.modal-dialog {
width: 1000px
}
}

View file

@ -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., with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %} {% endcomment %}
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url href name id %}"> {% load i18n %}
<i class="fa fa-history"></i> <a {% if class%}class="btn btn-info btn-sm"{% endif %} role="button" title="{% trans 'History' %}" href="{% url 'logs:history' application name id %}">
<i class="fa fa-history"></i> {% if text %}{% trans 'History' %}{% endif %}
</a> </a>

227
templates/errors/404.html Normal file
View file

@ -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 %}
<!DOCTYPE html>
<html>
<head prefix="og: http://ogp.me/ns#">
<meta property="og:title" content="Re2o" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{{ request.scheme }}://{{ request.get_host }}/" />
<meta property="og:image" content="{% static 'images/logo_re2o.svg' %}"/>
<meta property="og:image:type" content="image/svg"/>
<meta property="og:image:alt" content="The Re2o logo"/>
<meta property="og:description" content="Site de gestion de réseau supporté par FedeRez." />
<meta charset="utf-8">
<link rel="shortcut icon" type="image/svg" href="{% static 'images/logo_re2o.svg' %}">
<title>404, Page not Found</title>
<script src="/javascript/jquery/jquery.min.js"></script>
<script>
var snake = [{x:0,y:0,vx:1,vy:0}];
var bonus = [];
var lost = false;
var grid = 20;
var score = 0;
function update_snake() {
var l = snake.length;
var c = document.getElementById("myCanvas");
var width = c.width;
var height = c.height;
var last_case = {
x:snake[l-1].x,
y:snake[l-1].y,
vx:snake[l-1].vx,
vy:snake[l-1].vy
};
for(var i=l-1; i>=0; --i){
if(i == 0)
{
var m = bonus.length;
var remove = -1;
for(var j=0; j<m; ++j)
{
if((bonus[j].x == snake[i].x) && (bonus[j].y == snake[i].y))
{
remove = j;
}
}
if(remove >= 0){
bonus.splice(remove, 1);
snake.push(last_case);
score += 1;
}
}
if((i > 0) && (snake[i].x == snake[0].x) && (snake[i].y == snake[0].y))
{
lost = true;
}
snake[i].x = (snake[i].x + snake[i].vx * grid + width)%width;
snake[i].y = (snake[i].y + snake[i].vy * grid + height)%height;
if(i>0)
{
snake[i].vx = snake[i-1].vx;
snake[i].vy = snake[i-1].vy;
}
}
}
function draw_snake() {
var l = snake.length;
var c = document.getElementById("myCanvas");
if(c.getContext) {
var ctx = c.getContext("2d");
for(var i=0; i<l; ++i){
ctx.fillStyle = "#2980b9";
ctx.fillRect(snake[i].x, snake[i].y, grid, grid);
}
}
}
function draw_bonus() {
var l = bonus.length;
var ctx = document.getElementById("myCanvas").getContext("2d");
for(var i=0; i<l; ++i)
{
ctx.beginPath();
var x = bonus[i].x;
var y = bonus[i].y;
ctx.beginPath();
ctx.arc(x+grid/2, y+grid/2, grid/2, 0, 2 * Math.PI, false);
ctx.fillStyle = '#2ecc71';
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = '#27ae60';
ctx.stroke();
}
}
function draw_score(){
var ctx = document.getElementById('myCanvas').getContext('2d');
ctx.font = '50px serif';
ctx.fillStyle = '#2ecc71';
ctx.fillText("{% trans "Score :"%} " + score, 10, 60);
}
function draw_lost(){
var c = document.getElementById("myCanvas");
var ctx = c.getContext('2d');
ctx.fillStyle = '#2ecc71';
ctx.font = '50px serif';
ctx.fillText("{% trans "YOU LOST" %}", c.width/2, c.height/2);
}
function update_bonus() {
var c = document.getElementById("myCanvas");
var width = c.width;
var height = c.height;
var x = (Math.floor(Math.random() * width / grid))*grid;
var y = (Math.floor(Math.random() * height / grid))*grid;
bonus.push({x:x, y:y});
}
function draw() {
var c = document.getElementById("myCanvas");
var width = c.width;
var height = c.height;
var ctx = c.getContext("2d");
ctx.clearRect(0, 0, width, height);
if(!lost){
draw_snake();
draw_bonus();
draw_score();
}
else
{
draw_score();
draw_lost();
}
}
function on_keydown(e) {
if(e.which == 37) { // left
snake[0].vx = -1;
snake[0].vy = 0;
}
else if(e.which == 38) { // up
snake[0].vx = 0;
snake[0].vy = -1;
}
else if(e.which == 39) { // right
snake[0].vx = 1;
snake[0].vy = 0;
}
else if(e.which == 40) { // down
snake[0].vx = 0;
snake[0].vy = 1;
}
}
$("html").keydown(on_keydown);
window.setInterval(draw, 100);
window.setInterval(update_snake, 100);
window.setInterval(update_bonus, 3000);
</script>
<style>
html {
background: #34495e;
}
h1 {
display:block;
text-align: center;
background: #2c3e50;
padding: 1em;
width: 80%;
margin: auto;
color: #ecf0f1;
margin-bottom: 1em;
margin-top: 1em;
}
a
{
font-size: x-small;
color: #ecf0f1;
}
#myCanvas
{
width:80%;
display:block;
margin-left:auto;
margin-right:auto;
height:50%;
}
</style>
</head>
<body>
<h1>{% trans "Yup, that's a 404 error."%} <a href="/">{% trans "(Go to a known place)"%}</a></h1>
<canvas id="myCanvas" width="800px" height="300px" style="border:1px solid #d3d3d3;">
{%trans "Your browser does not support the HTML5 canvas tag."%}
</canvas>
</body>
</html>

View file

@ -89,15 +89,7 @@ class EditPortForm(FormRevMixin, ModelForm):
self.fields['machine_interface'].queryset = ( self.fields['machine_interface'].queryset = (
Interface.objects.all().select_related('domain__extension') Interface.objects.all().select_related('domain__extension')
) )
self.fields['related'].queryset = ( self.fields['related'].queryset = Port.objects.all().prefetch_related('switch__machine_ptr__interface_set__domain__extension')
Port.objects.all()
.prefetch_related(Prefetch(
'switch__interface_set',
queryset=(Interface.objects
.select_related('ipv4__ip_type__extension')
.select_related('domain__extension'))
))
)
class AddPortForm(FormRevMixin, ModelForm): class AddPortForm(FormRevMixin, ModelForm):

View file

@ -10,6 +10,7 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('topologie', '0029_auto_20171002_0334'), ('topologie', '0029_auto_20171002_0334'),
('machines', '0049_vlan'),
] ]
operations = [ operations = [

View file

@ -282,8 +282,12 @@ class Switch(AclMixin, Machine):
""" Returns the 'main' interface of the switch """ """ Returns the 'main' interface of the switch """
return self.interface_set.first() return self.interface_set.first()
@cached_property
def get_name(self):
return self.name or self.main_interface().domain.name
def __str__(self): def __str__(self):
return str(self.main_interface()) return str(self.get_name)
class ModelSwitch(AclMixin, RevMixin, models.Model): class ModelSwitch(AclMixin, RevMixin, models.Model):

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
<div class="table-responsive"> <div class="table-responsive">
{% if ap_list.paginator %} {% if ap_list.paginator %}
@ -49,9 +50,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ap.interface_set.first.details}}</td> <td>{{ap.interface_set.first.details}}</td>
<td>{{ap.location}}</td> <td>{{ap.location}}</td>
<td class="text-right"> <td class="text-right">
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'accesspoint' ap.pk %}"> {% history_button ap %}
<i class="fa fa-history"></i>
</a>
{% can_edit ap %} {% can_edit ap %}
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'topologie:edit-ap' ap.id %}"> <a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'topologie:edit-ap' ap.id %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load logs_extra %}
{% if building_list.paginator %} {% if building_list.paginator %}
{% include "pagination.html" with list=building_list %} {% include "pagination.html" with list=building_list %}
@ -39,9 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td>{{building.name}}</td> <td>{{building.name}}</td>
<td class="text-right"> <td class="text-right">
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'building' building.pk %}"> {% history_button building %}
<i class="fa fa-history"></i>
</a>
{% can_edit building %} {% can_edit building %}
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'topologie:edit-building' building.id %}"> <a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'topologie:edit-building' building.id %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>

Some files were not shown because too many files have changed in this diff Show more