8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-12-24 07:53:47 +00:00

Release : 2.6.1

This commit is contained in:
Hugo LEVY-FALK 2018-08-28 13:46:05 +02:00
commit ef4e430eba
240 changed files with 12666 additions and 3917 deletions

View file

@ -120,3 +120,33 @@ Don't forget to run migrations, several settings previously in the `preferences`
in their own Payment models. in their own Payment models.
To have a closer look on how the payments works, please go to the wiki. To have a closer look on how the payments works, please go to the wiki.
## MR 182: Add role models
Adds the Role model.
You need to ensure that your database character set is utf-8.
```sql
ALTER DATABASE re2o CHARACTER SET utf8;
```
## MR 247: Fix des comptes mails
Fix several issues with email accounts, you need to collect the static files.
```bash
./manage.py collectstatic
```
## MR 203 Add custom invoices
The custom invoices are now stored in database. You need to migrate your database :
```bash
python3 manage.py migrate
```
On some database engines (postgreSQL) you also need to update the id sequences:
```bash
python3 manage.py sqlsequencereset cotisations | python3 manage.py dbshell
```

View file

@ -338,6 +338,7 @@ class OptionalMachineSerializer(NamespacedHMSerializer):
class OptionalTopologieSerializer(NamespacedHMSerializer): class OptionalTopologieSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.OptionalTopologie` objects. """Serialize `preferences.models.OptionalTopologie` objects.
""" """
class Meta: class Meta:
model = preferences.OptionalTopologie model = preferences.OptionalTopologie
fields = ('radius_general_policy', 'vlan_decision_ok', fields = ('radius_general_policy', 'vlan_decision_ok',
@ -469,10 +470,10 @@ class SwitchPortSerializer(NamespacedHMSerializer):
class Meta: class Meta:
model = topologie.Port model = topologie.Port
fields = ('switch', 'port', 'room', 'machine_interface', 'related', fields = ('switch', 'port', 'room', 'machine_interface', 'related',
'radius', 'vlan_force', 'details', 'api_url') 'custom_profile', 'state', 'details', 'api_url')
extra_kwargs = { extra_kwargs = {
'related': {'view_name': 'switchport-detail'}, 'related': {'view_name': 'switchport-detail'},
'api_url': {'view_name': 'switchport-detail'} 'api_url': {'view_name': 'switchport-detail'},
} }
@ -484,6 +485,18 @@ class RoomSerializer(NamespacedHMSerializer):
fields = ('name', 'details', 'api_url') fields = ('name', 'details', 'api_url')
class PortProfileSerializer(NamespacedHMSerializer):
vlan_untagged = VlanSerializer(read_only=True)
class Meta:
model = topologie.PortProfile
fields = ('name', 'profil_default', 'vlan_untagged', 'vlan_tagged',
'radius_type', 'radius_mode', 'speed', 'mac_limit',
'flow_control', 'dhcp_snooping', 'dhcpv6_snooping',
'arp_protect', 'ra_guard', 'loop_protect', 'vlan_untagged',
'vlan_tagged')
# USERS # USERS
@ -534,11 +547,20 @@ class AdherentSerializer(NamespacedHMSerializer):
fields = ('name', 'surname', 'pseudo', 'email', 'local_email_redirect', fields = ('name', 'surname', 'pseudo', 'email', 'local_email_redirect',
'local_email_enabled', 'school', 'shell', 'comment', 'local_email_enabled', 'school', 'shell', 'comment',
'state', 'registered', 'telephone', 'room', 'solde', 'state', 'registered', 'telephone', 'room', 'solde',
'access', 'end_access', 'uid', 'api_url') 'access', 'end_access', 'uid', 'api_url','gid')
extra_kwargs = { extra_kwargs = {
'shell': {'view_name': 'shell-detail'} 'shell': {'view_name': 'shell-detail'}
} }
class HomeCreationSerializer(NamespacedHMSerializer):
"""Serialize 'users.models.User' minimal infos to create home
"""
uid = serializers.IntegerField(source='uid_number')
gid = serializers.IntegerField(source='gid_number')
class Meta:
model = users.User
fields = ('pseudo', 'uid', 'gid')
class ServiceUserSerializer(NamespacedHMSerializer): class ServiceUserSerializer(NamespacedHMSerializer):
"""Serialize `users.models.ServiceUser` objects. """Serialize `users.models.ServiceUser` objects.
@ -599,7 +621,7 @@ class WhitelistSerializer(NamespacedHMSerializer):
class EMailAddressSerializer(NamespacedHMSerializer): class EMailAddressSerializer(NamespacedHMSerializer):
"""Serialize `users.models.EMailAddress` objects. """Serialize `users.models.EMailAddress` objects.
""" """
user = serializers.CharField(source='user.pseudo', read_only=True)
class Meta: class Meta:
model = users.EMailAddress model = users.EMailAddress
fields = ('user', 'local_part', 'complete_email_address', 'api_url') fields = ('user', 'local_part', 'complete_email_address', 'api_url')
@ -635,9 +657,42 @@ class LocalEmailUsersSerializer(NamespacedHMSerializer):
class Meta: class Meta:
model = users.User model = users.User
fields = ('local_email_enabled', 'local_email_redirect', fields = ('local_email_enabled', 'local_email_redirect',
'email_address') 'email_address', 'email')
#Firewall
class FirewallPortListSerializer(serializers.ModelSerializer):
class Meta:
model = machines.OuverturePort
fields = ('begin', 'end', 'protocole', 'io', 'show_port')
class FirewallOuverturePortListSerializer(serializers.ModelSerializer):
tcp_ports_in = FirewallPortListSerializer(many=True, read_only=True)
udp_ports_in = FirewallPortListSerializer(many=True, read_only=True)
tcp_ports_out = FirewallPortListSerializer(many=True, read_only=True)
udp_ports_out = FirewallPortListSerializer(many=True, read_only=True)
class Meta:
model = machines.OuverturePortList
fields = ('tcp_ports_in', 'udp_ports_in', 'tcp_ports_out', 'udp_ports_out')
class SubnetPortsOpenSerializer(serializers.ModelSerializer):
ouverture_ports = FirewallOuverturePortListSerializer(read_only=True)
class Meta:
model = machines.IpType
fields = ('type', 'domaine_ip_start', 'domaine_ip_stop', 'complete_prefixv6', 'ouverture_ports')
class InterfacePortsOpenSerializer(serializers.ModelSerializer):
port_lists = FirewallOuverturePortListSerializer(read_only=True, many=True)
ipv4 = serializers.CharField(source='ipv4.ipv4', read_only=True)
ipv6 = Ipv6ListSerializer(many=True, read_only=True)
class Meta:
model = machines.Interface
fields = ('port_lists', 'ipv4', 'ipv6')
# DHCP # DHCP
@ -679,7 +734,7 @@ class NSRecordSerializer(NsSerializer):
"""Serialize `machines.models.Ns` objects with the data needed to """Serialize `machines.models.Ns` objects with the data needed to
generate a NS DNS record. generate a NS DNS record.
""" """
target = serializers.CharField(source='ns.name', read_only=True) target = serializers.CharField(source='ns', read_only=True)
class Meta(NsSerializer.Meta): class Meta(NsSerializer.Meta):
fields = ('target',) fields = ('target',)
@ -689,7 +744,7 @@ class MXRecordSerializer(MxSerializer):
"""Serialize `machines.models.Mx` objects with the data needed to """Serialize `machines.models.Mx` objects with the data needed to
generate a MX DNS record. generate a MX DNS record.
""" """
target = serializers.CharField(source='name.name', read_only=True) target = serializers.CharField(source='name', read_only=True)
class Meta(MxSerializer.Meta): class Meta(MxSerializer.Meta):
fields = ('target', 'priority') fields = ('target', 'priority')
@ -761,13 +816,12 @@ class CNAMERecordSerializer(serializers.ModelSerializer):
"""Serialize `machines.models.Domain` objects with the data needed to """Serialize `machines.models.Domain` objects with the data needed to
generate a CNAME DNS record. generate a CNAME DNS record.
""" """
alias = serializers.CharField(source='cname.name', read_only=True) alias = serializers.CharField(source='cname', read_only=True)
hostname = serializers.CharField(source='name', read_only=True) hostname = serializers.CharField(source='name', read_only=True)
extension = serializers.CharField(source='extension.name', read_only=True)
class Meta: class Meta:
model = machines.Domain model = machines.Domain
fields = ('alias', 'hostname', 'extension') fields = ('alias', 'hostname')
class DNSZonesSerializer(serializers.ModelSerializer): class DNSZonesSerializer(serializers.ModelSerializer):
@ -792,6 +846,25 @@ class DNSZonesSerializer(serializers.ModelSerializer):
'aaaa_records', 'cname_records', 'sshfp_records') 'aaaa_records', 'cname_records', 'sshfp_records')
class DNSReverseZonesSerializer(serializers.ModelSerializer):
"""Serialize the data about DNS Zones.
"""
soa = SOARecordSerializer(source='extension.soa')
extension = serializers.CharField(source='extension.name', read_only=True)
cidrs = serializers.ListField(child=serializers.CharField(), source='ip_set_cidrs_as_str', read_only=True)
ns_records = NSRecordSerializer(many=True, source='extension.ns_set')
mx_records = MXRecordSerializer(many=True, source='extension.mx_set')
txt_records = TXTRecordSerializer(many=True, source='extension.txt_set')
ptr_records = ARecordSerializer(many=True, source='get_associated_ptr_records')
ptr_v6_records = AAAARecordSerializer(many=True, source='get_associated_ptr_v6_records')
class Meta:
model = machines.IpType
fields = ('type', 'extension', 'soa', 'ns_records', 'mx_records',
'txt_records', 'ptr_records', 'ptr_v6_records', 'cidrs',
'prefix_v6', 'prefix_v6_length')
# MAILING # MAILING
@ -799,7 +872,7 @@ class MailingMemberSerializer(UserSerializer):
"""Serialize the data about a mailing member. """Serialize the data about a mailing member.
""" """
class Meta(UserSerializer.Meta): class Meta(UserSerializer.Meta):
fields = ('name', 'pseudo', 'email') fields = ('name', 'pseudo', 'get_mail')
class MailingSerializer(ClubSerializer): class MailingSerializer(ClubSerializer):
"""Serialize the data about a mailing. """Serialize the data about a mailing.

View file

@ -81,10 +81,12 @@ router.register_viewset(r'topologie/modelswitch', views.ModelSwitchViewSet)
router.register_viewset(r'topologie/constructorswitch', views.ConstructorSwitchViewSet) router.register_viewset(r'topologie/constructorswitch', views.ConstructorSwitchViewSet)
router.register_viewset(r'topologie/switchbay', views.SwitchBayViewSet) router.register_viewset(r'topologie/switchbay', views.SwitchBayViewSet)
router.register_viewset(r'topologie/building', views.BuildingViewSet) router.register_viewset(r'topologie/building', views.BuildingViewSet)
router.register_viewset(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport') router.register(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport')
router.register_viewset(r'topologie/room', views.RoomViewSet) router.register_viewset(r'topologie/room', views.RoomViewSet)
router.register(r'topologie/portprofile', views.PortProfileViewSet)
# USERS # USERS
router.register_viewset(r'users/user', views.UserViewSet) router.register_viewset(r'users/user', views.UserViewSet)
router.register_viewset(r'users/homecreation', views.HomeCreationViewSet)
router.register_viewset(r'users/club', views.ClubViewSet) router.register_viewset(r'users/club', views.ClubViewSet)
router.register_viewset(r'users/adherent', views.AdherentViewSet) router.register_viewset(r'users/adherent', views.AdherentViewSet)
router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet) router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet)
@ -100,8 +102,12 @@ router.register_viewset(r'services/regen', views.ServiceRegenViewSet, base_name=
router.register_view(r'dhcp/hostmacip', views.HostMacIpView), router.register_view(r'dhcp/hostmacip', views.HostMacIpView),
# LOCAL EMAILS # LOCAL EMAILS
router.register_view(r'localemail/users', views.LocalEmailUsersView), router.register_view(r'localemail/users', views.LocalEmailUsersView),
# Firewall
router.register_view(r'firewall/subnet-ports', views.SubnetPortsOpenView),
router.register_view(r'firewall/interface-ports', views.InterfacePortsOpenView),
# DNS # DNS
router.register_view(r'dns/zones', views.DNSZonesView), router.register_view(r'dns/zones', views.DNSZonesView),
router.register_view(r'dns/reverse-zones', views.DNSReverseZonesView),
# MAILING # MAILING
router.register_view(r'mailing/standard', views.StandardMailingView), router.register_view(r'mailing/standard', views.StandardMailingView),
router.register_view(r'mailing/club', views.ClubMailingView), router.register_view(r'mailing/club', views.ClubMailingView),

View file

@ -403,6 +403,12 @@ class RoomViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.RoomSerializer serializer_class = serializers.RoomSerializer
class PortProfileViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `topologie.models.PortProfile` objects.
"""
queryset = topologie.PortProfile.objects.all()
serializer_class = serializers.PortProfileSerializer
# USER # USER
@ -412,6 +418,11 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
queryset = users.User.objects.all() queryset = users.User.objects.all()
serializer_class = serializers.UserSerializer serializer_class = serializers.UserSerializer
class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users` objects to create homes.
"""
queryset = users.User.objects.all()
serializer_class = serializers.HomeCreationSerializer
class ClubViewSet(viewsets.ReadOnlyModelViewSet): class ClubViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `users.models.Club` objects. """Exposes list and details of `users.models.Club` objects.
@ -532,6 +543,16 @@ class HostMacIpView(generics.ListAPIView):
serializer_class = serializers.HostMacIpSerializer serializer_class = serializers.HostMacIpSerializer
#Firewall
class SubnetPortsOpenView(generics.ListAPIView):
queryset = machines.IpType.objects.all()
serializer_class = serializers.SubnetPortsOpenSerializer
class InterfacePortsOpenView(generics.ListAPIView):
queryset = machines.Interface.objects.filter(port_lists__isnull=False).distinct()
serializer_class = serializers.InterfacePortsOpenSerializer
# DNS # DNS
@ -549,6 +570,15 @@ class DNSZonesView(generics.ListAPIView):
.all()) .all())
serializer_class = serializers.DNSZonesSerializer serializer_class = serializers.DNSZonesSerializer
class DNSReverseZonesView(generics.ListAPIView):
"""Exposes the detailed information about each extension (hostnames,
IPs, DNS records, etc.) in order to build the DNS zone files.
"""
queryset = (machines.IpType.objects.all())
serializer_class = serializers.DNSReverseZonesSerializer
# MAILING # MAILING

View file

@ -42,4 +42,5 @@ def can_view(user):
if can: if can:
return can, None return can, None
else: else:
return can, _("You don't have the rights to see this application.") return can, _("You don't have the right to view this application.")

View file

@ -30,6 +30,7 @@ from django.contrib import admin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
from .models import CustomInvoice
class FactureAdmin(VersionAdmin): class FactureAdmin(VersionAdmin):
@ -37,6 +38,11 @@ class FactureAdmin(VersionAdmin):
pass pass
class CustomInvoiceAdmin(VersionAdmin):
"""Admin class for custom invoices."""
pass
class VenteAdmin(VersionAdmin): class VenteAdmin(VersionAdmin):
"""Class admin d'une vente, tous les champs (facture related)""" """Class admin d'une vente, tous les champs (facture related)"""
pass pass
@ -69,3 +75,4 @@ admin.site.register(Banque, BanqueAdmin)
admin.site.register(Paiement, PaiementAdmin) admin.site.register(Paiement, PaiementAdmin)
admin.site.register(Vente, VenteAdmin) admin.site.register(Vente, VenteAdmin)
admin.site.register(Cotisation, CotisationAdmin) admin.site.register(Cotisation, CotisationAdmin)
admin.site.register(CustomInvoice, CustomInvoiceAdmin)

View file

@ -40,13 +40,13 @@ from django import forms
from django.db.models import Q from django.db.models import Q
from django.forms import ModelForm, Form 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_lazy as _l from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
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, CustomInvoice
from .payment_methods import balance from .payment_methods import balance
@ -84,71 +84,36 @@ class FactureForm(FieldPermissionFormMixin, FormRevMixin, ModelForm):
return cleaned_data return cleaned_data
class SelectUserArticleForm(FormRevMixin, Form): class SelectArticleForm(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.
""" """
article = forms.ModelChoiceField( article = forms.ModelChoiceField(
queryset=Article.objects.filter( queryset=Article.objects.none(),
Q(type_user='All') | Q(type_user='Adherent') label=_("Article"),
),
label=_l("Article"),
required=True required=True
) )
quantity = forms.IntegerField( quantity = forms.IntegerField(
label=_l("Quantity"), label=_("Quantity"),
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
required=True required=True
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
user = kwargs.pop('user') user = kwargs.pop('user')
super(SelectUserArticleForm, self).__init__(*args, **kwargs) target_user = kwargs.pop('target_user')
self.fields['article'].queryset = Article.find_allowed_articles(user) super(SelectArticleForm, self).__init__(*args, **kwargs)
self.fields['article'].queryset = Article.find_allowed_articles(user, target_user)
class SelectClubArticleForm(Form): class CustomInvoiceForm(FormRevMixin, ModelForm):
""" """
Form used to select an article during the creation of an invoice for a Form used to create a custom invoice.
club.
""" """
article = forms.ModelChoiceField( class Meta:
queryset=Article.objects.filter( model = CustomInvoice
Q(type_user='All') | Q(type_user='Club') fields = '__all__'
),
label=_l("Article"),
required=True
)
quantity = forms.IntegerField(
label=_l("Quantity"),
validators=[MinValueValidator(1)],
required=True
)
def __init__(self, user, *args, **kwargs):
super(SelectClubArticleForm, self).__init__(*args, **kwargs)
self.fields['article'].queryset = Article.find_allowed_articles(user)
# TODO : change Facture to Invoice
class NewFactureFormPdf(Form):
"""
Form used to create a custom PDF invoice.
"""
paid = forms.BooleanField(label=_l("Paid"), required=False)
# TODO : change dest field to recipient
dest = forms.CharField(
required=True,
max_length=255,
label=_l("Recipient")
)
# TODO : change chambre field to address
chambre = forms.CharField(
required=False,
max_length=10,
label=_l("Address")
)
class ArticleForm(FormRevMixin, ModelForm): class ArticleForm(FormRevMixin, ModelForm):
@ -172,7 +137,7 @@ class DelArticleForm(FormRevMixin, Form):
""" """
articles = forms.ModelMultipleChoiceField( articles = forms.ModelMultipleChoiceField(
queryset=Article.objects.none(), queryset=Article.objects.none(),
label=_l("Existing articles"), label=_("Available articles"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -212,7 +177,7 @@ class DelPaiementForm(FormRevMixin, Form):
# TODO : change paiement to payment # TODO : change paiement to payment
paiements = forms.ModelMultipleChoiceField( paiements = forms.ModelMultipleChoiceField(
queryset=Paiement.objects.none(), queryset=Paiement.objects.none(),
label=_l("Existing payment method"), label=_("Available payment methods"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -250,7 +215,7 @@ class DelBanqueForm(FormRevMixin, Form):
# TODO : change banque to bank # TODO : change banque to bank
banques = forms.ModelMultipleChoiceField( banques = forms.ModelMultipleChoiceField(
queryset=Banque.objects.none(), queryset=Banque.objects.none(),
label=_l("Existing banks"), label=_("Available banks"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -269,21 +234,21 @@ class RechargeForm(FormRevMixin, Form):
Form used to refill a user's balance Form used to refill a user's balance
""" """
value = forms.FloatField( value = forms.FloatField(
label=_l("Amount"), label=_("Amount"),
min_value=0.01, min_value=0.01,
validators=[] validators=[]
) )
payment = forms.ModelChoiceField( payment = forms.ModelChoiceField(
queryset=Paiement.objects.none(), queryset=Paiement.objects.none(),
label=_l("Payment method") label=_("Payment method")
) )
def __init__(self, *args, user=None, **kwargs): def __init__(self, *args, user=None, user_source=None, **kwargs):
self.user = user self.user = user
super(RechargeForm, self).__init__(*args, **kwargs) super(RechargeForm, self).__init__(*args, **kwargs)
self.fields['payment'].empty_label = \ self.fields['payment'].empty_label = \
_("Select a payment method") _("Select a payment method")
self.fields['payment'].queryset = Paiement.find_allowed_payments(user) self.fields['payment'].queryset = Paiement.find_allowed_payments(user_source).exclude(is_balance=True)
def clean(self): def clean(self):
""" """
@ -301,3 +266,4 @@ class RechargeForm(FormRevMixin, Form):
} }
) )
return self.cleaned_data return self.cleaned_data

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-08-11 23:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0030_custom_payment'),
]
operations = [
migrations.AddField(
model_name='comnpaypayment',
name='production',
field=models.BooleanField(default=True, verbose_name='Production mode enabled (production url, instead of homologation)'),
),
]

View file

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-07-21 20:01
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.contrib.auth.management import create_permissions
import re2o.field_permissions
import re2o.mixins
def reattribute_ids(apps, schema_editor):
Facture = apps.get_model('cotisations', 'Facture')
BaseInvoice = apps.get_model('cotisations', 'BaseInvoice')
for f in Facture.objects.all():
base = BaseInvoice.objects.create(id=f.pk)
base.date = f.date
base.save()
f.baseinvoice_ptr = base
f.save()
def update_rights(apps, schema_editor):
Permission = apps.get_model('auth', 'Permission')
# creates needed permissions
app = apps.get_app_config('cotisations')
app.models_module = True
create_permissions(app)
app.models_module = False
former = Permission.objects.get(codename='change_facture_pdf')
new_1 = Permission.objects.get(codename='add_custominvoice')
new_2 = Permission.objects.get(codename='change_custominvoice')
new_3 = Permission.objects.get(codename='view_custominvoice')
new_4 = Permission.objects.get(codename='delete_custominvoice')
for group in former.group_set.all():
group.permissions.remove(former)
group.permissions.add(new_1)
group.permissions.add(new_2)
group.permissions.add(new_3)
group.permissions.add(new_4)
group.save()
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0031_comnpaypayment_production'),
]
operations = [
migrations.CreateModel(
name='BaseInvoice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField(auto_now_add=True, verbose_name='Date')),
],
bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, re2o.field_permissions.FieldPermissionModelMixin, models.Model),
),
migrations.CreateModel(
name='CustomInvoice',
fields=[
('baseinvoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.BaseInvoice')),
('recipient', models.CharField(max_length=255, verbose_name='Recipient')),
('payment', models.CharField(max_length=255, verbose_name='Payment type')),
('address', models.CharField(max_length=255, verbose_name='Address')),
('paid', models.BooleanField(verbose_name='Paid')),
],
bases=('cotisations.baseinvoice',),
options={'permissions': (('view_custominvoice', 'Can view a custom invoice'),)},
),
migrations.AddField(
model_name='facture',
name='baseinvoice_ptr',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='cotisations.BaseInvoice', null=True),
preserve_default=False,
),
migrations.RunPython(reattribute_ids),
migrations.AlterField(
model_name='vente',
name='facture',
field=models.ForeignKey(on_delete=models.CASCADE, verbose_name='Invoice', to='cotisations.BaseInvoice')
),
migrations.RemoveField(
model_name='facture',
name='id',
),
migrations.RemoveField(
model_name='facture',
name='date',
),
migrations.AlterField(
model_name='facture',
name='baseinvoice_ptr',
field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.BaseInvoice'),
),
migrations.RunPython(update_rights),
migrations.AlterModelOptions(
name='facture',
options={'permissions': (('change_facture_control', 'Can change the "controlled" state'), ('view_facture', "Can see an invoice's details"), ('change_all_facture', 'Can edit all the previous invoices')), 'verbose_name': 'Invoice', 'verbose_name_plural': 'Invoices'},
),
]

View file

@ -0,0 +1,181 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-08-18 11:19
from __future__ import unicode_literals
import cotisations.validators
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import re2o.aes_field
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0032_custom_invoice'),
]
operations = [
migrations.AlterModelOptions(
name='article',
options={'permissions': (('view_article', 'Can view an article object'), ('buy_every_article', 'Can buy every article')), 'verbose_name': 'article', 'verbose_name_plural': 'articles'},
),
migrations.AlterModelOptions(
name='balancepayment',
options={'verbose_name': 'user balance'},
),
migrations.AlterModelOptions(
name='banque',
options={'permissions': (('view_banque', 'Can view a bank object'),), 'verbose_name': 'bank', 'verbose_name_plural': 'banks'},
),
migrations.AlterModelOptions(
name='cotisation',
options={'permissions': (('view_cotisation', 'Can view a subscription object'), ('change_all_cotisation', 'Can edit the previous subscriptions')), 'verbose_name': 'subscription', 'verbose_name_plural': 'subscriptions'},
),
migrations.AlterModelOptions(
name='custominvoice',
options={'permissions': (('view_custominvoice', 'Can view a custom invoice object'),)},
),
migrations.AlterModelOptions(
name='facture',
options={'permissions': (('change_facture_control', 'Can edit the "controlled" state'), ('view_facture', 'Can view an invoice object'), ('change_all_facture', 'Can edit all the previous invoices')), 'verbose_name': 'invoice', 'verbose_name_plural': 'invoices'},
),
migrations.AlterModelOptions(
name='paiement',
options={'permissions': (('view_paiement', 'Can view a payment method object'), ('use_every_payment', 'Can use every payment method')), 'verbose_name': 'payment method', 'verbose_name_plural': 'payment methods'},
),
migrations.AlterModelOptions(
name='vente',
options={'permissions': (('view_vente', 'Can view a purchase object'), ('change_all_vente', 'Can edit all the previous purchases')), 'verbose_name': 'purchase', 'verbose_name_plural': 'purchases'},
),
migrations.AlterField(
model_name='article',
name='available_for_everyone',
field=models.BooleanField(default=False, verbose_name='is available for every user'),
),
migrations.AlterField(
model_name='article',
name='duration',
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='duration (in months)'),
),
migrations.AlterField(
model_name='article',
name='name',
field=models.CharField(max_length=255, verbose_name='designation'),
),
migrations.AlterField(
model_name='article',
name='prix',
field=models.DecimalField(decimal_places=2, max_digits=5, verbose_name='unit price'),
),
migrations.AlterField(
model_name='article',
name='type_cotisation',
field=models.CharField(blank=True, choices=[('Connexion', 'Connection'), ('Adhesion', 'Membership'), ('All', 'Both of them')], default=None, max_length=255, null=True, verbose_name='subscription type'),
),
migrations.AlterField(
model_name='article',
name='type_user',
field=models.CharField(choices=[('Adherent', 'Member'), ('Club', 'Club'), ('All', 'Both of them')], default='All', max_length=255, verbose_name='type of users concerned'),
),
migrations.AlterField(
model_name='banque',
name='name',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='comnpaypayment',
name='payment_credential',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='ComNpay VAT Number'),
),
migrations.AlterField(
model_name='comnpaypayment',
name='payment_pass',
field=re2o.aes_field.AESEncryptedField(blank=True, max_length=255, null=True, verbose_name='ComNpay secret key'),
),
migrations.AlterField(
model_name='comnpaypayment',
name='production',
field=models.BooleanField(default=True, verbose_name='Production mode enabled (production URL, instead of homologation)'),
),
migrations.AlterField(
model_name='cotisation',
name='date_end',
field=models.DateTimeField(verbose_name='end date'),
),
migrations.AlterField(
model_name='cotisation',
name='date_start',
field=models.DateTimeField(verbose_name='start date'),
),
migrations.AlterField(
model_name='cotisation',
name='type_cotisation',
field=models.CharField(choices=[('Connexion', 'Connection'), ('Adhesion', 'Membership'), ('All', 'Both of them')], default='All', max_length=255, verbose_name='subscription type'),
),
migrations.AlterField(
model_name='cotisation',
name='vente',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='cotisations.Vente', verbose_name='purchase'),
),
migrations.AlterField(
model_name='facture',
name='cheque',
field=models.CharField(blank=True, max_length=255, verbose_name='cheque number'),
),
migrations.AlterField(
model_name='facture',
name='control',
field=models.BooleanField(default=False, verbose_name='controlled'),
),
migrations.AlterField(
model_name='facture',
name='valid',
field=models.BooleanField(default=True, verbose_name='validated'),
),
migrations.AlterField(
model_name='paiement',
name='available_for_everyone',
field=models.BooleanField(default=False, verbose_name='is available for every user'),
),
migrations.AlterField(
model_name='paiement',
name='is_balance',
field=models.BooleanField(default=False, editable=False, help_text='There should be only one balance payment method.', validators=[cotisations.validators.check_no_balance], verbose_name='is user balance'),
),
migrations.AlterField(
model_name='paiement',
name='moyen',
field=models.CharField(max_length=255, verbose_name='method'),
),
migrations.AlterField(
model_name='vente',
name='duration',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='duration (in months)'),
),
migrations.AlterField(
model_name='vente',
name='facture',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cotisations.BaseInvoice', verbose_name='invoice'),
),
migrations.AlterField(
model_name='vente',
name='name',
field=models.CharField(max_length=255, verbose_name='article'),
),
migrations.AlterField(
model_name='vente',
name='number',
field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='amount'),
),
migrations.AlterField(
model_name='vente',
name='prix',
field=models.DecimalField(decimal_places=2, max_digits=5, verbose_name='price'),
),
migrations.AlterField(
model_name='vente',
name='type_cotisation',
field=models.CharField(blank=True, choices=[('Connexion', 'Connection'), ('Adhesion', 'Membership'), ('All', 'Both of them')], max_length=255, null=True, verbose_name='subscription type'),
),
]

View file

@ -41,8 +41,7 @@ from django.dispatch import receiver
from django.forms import ValidationError from django.forms import ValidationError
from django.core.validators import MinValueValidator 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_lazy as _
from django.utils.translation import ugettext_lazy as _l
from django.urls import reverse from django.urls import reverse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.contrib import messages from django.contrib import messages
@ -55,80 +54,11 @@ from cotisations.utils import find_payment_method
from cotisations.validators import check_no_balance from cotisations.validators import check_no_balance
# TODO : change facture to invoice class BaseInvoice(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
"""
The model for an invoice. It reprensents the fact that a user paid for
something (it can be multiple article paid at once).
An invoice is linked to :
* one or more purchases (one for each article sold that time)
* a user (the one who bought those articles)
* a payment method (the one used by the user)
* (if applicable) a bank
* (if applicable) a cheque number.
Every invoice is dated throught the 'date' value.
An invoice has a 'controlled' value (default : False) which means that
someone with high enough rights has controlled that invoice and taken it
into account. It also has a 'valid' value (default : True) which means
that someone with high enough rights has decided that this invoice was not
valid (thus it's like the user never paid for his articles). It may be
necessary in case of non-payment.
"""
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
# TODO : change paiement to payment
paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
# TODO : change banque to bank
banque = models.ForeignKey(
'Banque',
on_delete=models.PROTECT,
blank=True,
null=True
)
# TODO : maybe change to cheque nummber because not evident
cheque = models.CharField(
max_length=255,
blank=True,
verbose_name=_l("Cheque number")
)
date = models.DateTimeField( date = models.DateTimeField(
auto_now_add=True, auto_now_add=True,
verbose_name=_l("Date") verbose_name=_("Date")
) )
# TODO : change name to validity for clarity
valid = models.BooleanField(
default=True,
verbose_name=_l("Validated")
)
# TODO : changed name to controlled for clarity
control = models.BooleanField(
default=False,
verbose_name=_l("Controlled")
)
class Meta:
abstract = False
permissions = (
# TODO : change facture to invoice
('change_facture_control',
_l("Can change the \"controlled\" state")),
# TODO : seems more likely to be call create_facture_pdf
# or create_invoice_pdf
('change_facture_pdf',
_l("Can create a custom PDF invoice")),
('view_facture',
_l("Can see an invoice's details")),
('change_all_facture',
_l("Can edit all the previous invoices")),
)
verbose_name = _l("Invoice")
verbose_name_plural = _l("Invoices")
def linked_objects(self):
"""Return linked objects : machine and domain.
Usefull in history display"""
return self.vente_set.all()
# TODO : change prix to price # TODO : change prix to price
def prix(self): def prix(self):
@ -167,6 +97,74 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
).values_list('name', flat=True)) ).values_list('name', flat=True))
return name return name
# TODO : change facture to invoice
class Facture(BaseInvoice):
"""
The model for an invoice. It reprensents the fact that a user paid for
something (it can be multiple article paid at once).
An invoice is linked to :
* one or more purchases (one for each article sold that time)
* a user (the one who bought those articles)
* a payment method (the one used by the user)
* (if applicable) a bank
* (if applicable) a cheque number.
Every invoice is dated throught the 'date' value.
An invoice has a 'controlled' value (default : False) which means that
someone with high enough rights has controlled that invoice and taken it
into account. It also has a 'valid' value (default : True) which means
that someone with high enough rights has decided that this invoice was not
valid (thus it's like the user never paid for his articles). It may be
necessary in case of non-payment.
"""
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
# TODO : change paiement to payment
paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
# TODO : change banque to bank
banque = models.ForeignKey(
'Banque',
on_delete=models.PROTECT,
blank=True,
null=True
)
# TODO : maybe change to cheque nummber because not evident
cheque = models.CharField(
max_length=255,
blank=True,
verbose_name=_("cheque number")
)
# TODO : change name to validity for clarity
valid = models.BooleanField(
default=True,
verbose_name=_("validated")
)
# TODO : changed name to controlled for clarity
control = models.BooleanField(
default=False,
verbose_name=_("controlled")
)
class Meta:
abstract = False
permissions = (
# TODO : change facture to invoice
('change_facture_control',
_("Can edit the \"controlled\" state")),
('view_facture',
_("Can view an invoice object")),
('change_all_facture',
_("Can edit all the previous invoices")),
)
verbose_name = _("invoice")
verbose_name_plural = _("invoices")
def linked_objects(self):
"""Return linked objects : machine and domain.
Usefull in history display"""
return self.vente_set.all()
def can_edit(self, user_request, *args, **kwargs): def can_edit(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.change_facture'): if not user_request.has_perm('cotisations.change_facture'):
return False, _("You don't have the right to edit an invoice.") return False, _("You don't have the right to edit an invoice.")
@ -196,7 +194,7 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
def can_view(self, user_request, *_args, **_kwargs): def can_view(self, user_request, *_args, **_kwargs):
if not user_request.has_perm('cotisations.view_facture') and \ if not user_request.has_perm('cotisations.view_facture') and \
self.user != user_request: self.user != user_request:
return False, _("You don't have the right to see someone else's " return False, _("You don't have the right to view someone else's "
"invoices history.") "invoices history.")
elif not self.valid: elif not self.valid:
return False, _("The invoice has been invalidated.") return False, _("The invoice has been invalidated.")
@ -212,14 +210,6 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
_("You don't have the right to edit the \"controlled\" state.") _("You don't have the right to edit the \"controlled\" state.")
) )
@staticmethod
def can_change_pdf(user_request, *_args, **_kwargs):
""" Returns True if the user can change this invoice """
return (
user_request.has_perm('cotisations.change_facture_pdf'),
_("You don't have the right to edit an invoice.")
)
@staticmethod @staticmethod
def can_create(user_request, *_args, **_kwargs): def can_create(user_request, *_args, **_kwargs):
"""Check if a user can create an invoice. """Check if a user can create an invoice.
@ -231,8 +221,8 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
if user_request.has_perm('cotisations.add_facture'): if user_request.has_perm('cotisations.add_facture'):
return True, None return True, None
if len(Paiement.find_allowed_payments(user_request)) <= 0: if len(Paiement.find_allowed_payments(user_request)) <= 0:
return False, _("There are no payment types which you can use.") return False, _("There are no payment method which you can use.")
if len(Article.find_allowed_articles(user_request)) <= 0: if len(Article.find_allowed_articles(user_request, user_request)) <= 0:
return False, _("There are no article that you can buy.") return False, _("There are no article that you can buy.")
return True, None return True, None
@ -265,6 +255,28 @@ def facture_post_delete(**kwargs):
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
class CustomInvoice(BaseInvoice):
class Meta:
permissions = (
('view_custominvoice', _("Can view a custom invoice object")),
)
recipient = models.CharField(
max_length=255,
verbose_name=_("Recipient")
)
payment = models.CharField(
max_length=255,
verbose_name=_("Payment type")
)
address = models.CharField(
max_length=255,
verbose_name=_("Address")
)
paid = models.BooleanField(
verbose_name=_("Paid")
)
# TODO : change Vente to Purchase # TODO : change Vente to Purchase
class Vente(RevMixin, AclMixin, models.Model): class Vente(RevMixin, AclMixin, models.Model):
""" """
@ -281,38 +293,38 @@ class Vente(RevMixin, AclMixin, models.Model):
# TODO : change this to English # TODO : change this to English
COTISATION_TYPE = ( COTISATION_TYPE = (
('Connexion', _l("Connexion")), ('Connexion', _("Connection")),
('Adhesion', _l("Membership")), ('Adhesion', _("Membership")),
('All', _l("Both of them")), ('All', _("Both of them")),
) )
# TODO : change facture to invoice # TODO : change facture to invoice
facture = models.ForeignKey( facture = models.ForeignKey(
'Facture', 'BaseInvoice',
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_l("Invoice") verbose_name=_("invoice")
) )
# TODO : change number to amount for clarity # TODO : change number to amount for clarity
number = models.IntegerField( number = models.IntegerField(
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
verbose_name=_l("Amount") verbose_name=_("amount")
) )
# TODO : change this field for a ForeinKey to Article # TODO : change this field for a ForeinKey to Article
name = models.CharField( name = models.CharField(
max_length=255, max_length=255,
verbose_name=_l("Article") verbose_name=_("article")
) )
# TODO : change prix to price # TODO : change prix to price
# TODO : this field is not needed if you use Article ForeignKey # TODO : this field is not needed if you use Article ForeignKey
prix = models.DecimalField( prix = models.DecimalField(
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
verbose_name=_l("Price")) verbose_name=_("price"))
# TODO : this field is not needed if you use Article ForeignKey # TODO : this field is not needed if you use Article ForeignKey
duration = models.PositiveIntegerField( duration = models.PositiveIntegerField(
blank=True, blank=True,
null=True, null=True,
verbose_name=_l("Duration (in whole month)") verbose_name=_("duration (in months)")
) )
# TODO : this field is not needed if you use Article ForeignKey # TODO : this field is not needed if you use Article ForeignKey
type_cotisation = models.CharField( type_cotisation = models.CharField(
@ -320,16 +332,16 @@ class Vente(RevMixin, AclMixin, models.Model):
blank=True, blank=True,
null=True, null=True,
max_length=255, max_length=255,
verbose_name=_l("Type of cotisation") verbose_name=_("subscription type")
) )
class Meta: class Meta:
permissions = ( permissions = (
('view_vente', _l("Can see a purchase's details")), ('view_vente', _("Can view a purchase object")),
('change_all_vente', _l("Can edit all the previous purchases")), ('change_all_vente', _("Can edit all the previous purchases")),
) )
verbose_name = _l("Purchase") verbose_name = _("purchase")
verbose_name_plural = _l("Purchases") verbose_name_plural = _("purchases")
# TODO : change prix_total to total_price # TODO : change prix_total to total_price
def prix_total(self): def prix_total(self):
@ -355,6 +367,10 @@ class Vente(RevMixin, AclMixin, models.Model):
cotisation_type defined (which means the article sold represents cotisation_type defined (which means the article sold represents
a cotisation) a cotisation)
""" """
try:
invoice = self.facture.facture
except Facture.DoesNotExist:
return
if not hasattr(self, 'cotisation') and self.type_cotisation: if not hasattr(self, 'cotisation') and self.type_cotisation:
cotisation = Cotisation(vente=self) cotisation = Cotisation(vente=self)
cotisation.type_cotisation = self.type_cotisation cotisation.type_cotisation = self.type_cotisation
@ -362,7 +378,7 @@ class Vente(RevMixin, AclMixin, models.Model):
end_cotisation = Cotisation.objects.filter( end_cotisation = Cotisation.objects.filter(
vente__in=Vente.objects.filter( vente__in=Vente.objects.filter(
facture__in=Facture.objects.filter( facture__in=Facture.objects.filter(
user=self.facture.user user=invoice.user
).exclude(valid=False)) ).exclude(valid=False))
).filter( ).filter(
Q(type_cotisation='All') | Q(type_cotisation='All') |
@ -371,9 +387,9 @@ class Vente(RevMixin, AclMixin, models.Model):
date_start__lt=date_start date_start__lt=date_start
).aggregate(Max('date_end'))['date_end__max'] ).aggregate(Max('date_end'))['date_end__max']
elif self.type_cotisation == "Adhesion": elif self.type_cotisation == "Adhesion":
end_cotisation = self.facture.user.end_adhesion() end_cotisation = invoice.user.end_adhesion()
else: else:
end_cotisation = self.facture.user.end_connexion() end_cotisation = invoice.user.end_connexion()
date_start = date_start or timezone.now() date_start = date_start or timezone.now()
end_cotisation = end_cotisation or date_start end_cotisation = end_cotisation or date_start
date_max = max(end_cotisation, date_start) date_max = max(end_cotisation, date_start)
@ -392,7 +408,7 @@ class Vente(RevMixin, AclMixin, models.Model):
# Checking that if a cotisation is specified, there is also a duration # Checking that if a cotisation is specified, there is also a duration
if self.type_cotisation and not self.duration: if self.type_cotisation and not self.duration:
raise ValidationError( raise ValidationError(
_("A cotisation should always have a duration.") _("Duration must be specified for a subscription.")
) )
self.update_cotisation() self.update_cotisation()
super(Vente, self).save(*args, **kwargs) super(Vente, self).save(*args, **kwargs)
@ -428,7 +444,7 @@ class Vente(RevMixin, AclMixin, models.Model):
def can_view(self, user_request, *_args, **_kwargs): def can_view(self, user_request, *_args, **_kwargs):
if (not user_request.has_perm('cotisations.view_vente') and if (not user_request.has_perm('cotisations.view_vente') and
self.facture.user != user_request): self.facture.user != user_request):
return False, _("You don't have the right to see someone " return False, _("You don't have the right to view someone "
"else's purchase history.") "else's purchase history.")
else: else:
return True, None return True, None
@ -445,6 +461,10 @@ def vente_post_save(**kwargs):
LDAP user when a purchase has been saved. LDAP user when a purchase has been saved.
""" """
purchase = kwargs['instance'] purchase = kwargs['instance']
try:
purchase.facture.facture
except Facture.DoesNotExist:
return
if hasattr(purchase, 'cotisation'): if hasattr(purchase, 'cotisation'):
purchase.cotisation.vente = purchase purchase.cotisation.vente = purchase
purchase.cotisation.save() purchase.cotisation.save()
@ -462,8 +482,12 @@ def vente_post_delete(**kwargs):
Synchronise the LDAP user after a purchase has been deleted. Synchronise the LDAP user after a purchase has been deleted.
""" """
purchase = kwargs['instance'] purchase = kwargs['instance']
try:
invoice = purchase.facture.facture
except Facture.DoesNotExist:
return
if purchase.type_cotisation: if purchase.type_cotisation:
user = purchase.facture.user user = invoice.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
@ -483,38 +507,38 @@ class Article(RevMixin, AclMixin, models.Model):
# TODO : Either use TYPE or TYPES in both choices but not both # TODO : Either use TYPE or TYPES in both choices but not both
USER_TYPES = ( USER_TYPES = (
('Adherent', _l("Member")), ('Adherent', _("Member")),
('Club', _l("Club")), ('Club', _("Club")),
('All', _l("Both of them")), ('All', _("Both of them")),
) )
COTISATION_TYPE = ( COTISATION_TYPE = (
('Connexion', _l("Connexion")), ('Connexion', _("Connection")),
('Adhesion', _l("Membership")), ('Adhesion', _("Membership")),
('All', _l("Both of them")), ('All', _("Both of them")),
) )
name = models.CharField( name = models.CharField(
max_length=255, max_length=255,
verbose_name=_l("Designation") verbose_name=_("designation")
) )
# TODO : change prix to price # TODO : change prix to price
prix = models.DecimalField( prix = models.DecimalField(
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
verbose_name=_l("Unitary price") verbose_name=_("unit price")
) )
duration = models.PositiveIntegerField( duration = models.PositiveIntegerField(
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(0)], validators=[MinValueValidator(0)],
verbose_name=_l("Duration (in whole month)") verbose_name=_("duration (in months)")
) )
type_user = models.CharField( type_user = models.CharField(
choices=USER_TYPES, choices=USER_TYPES,
default='All', default='All',
max_length=255, max_length=255,
verbose_name=_l("Type of users concerned") verbose_name=_("type of users concerned")
) )
type_cotisation = models.CharField( type_cotisation = models.CharField(
choices=COTISATION_TYPE, choices=COTISATION_TYPE,
@ -522,31 +546,31 @@ class Article(RevMixin, AclMixin, models.Model):
blank=True, blank=True,
null=True, null=True,
max_length=255, max_length=255,
verbose_name=_l("Type of cotisation") verbose_name=_("subscription type")
) )
available_for_everyone = models.BooleanField( available_for_everyone = models.BooleanField(
default=False, default=False,
verbose_name=_l("Is available for every user") verbose_name=_("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', _("Can view an article object")),
('buy_every_article', _l("Can buy every_article")) ('buy_every_article', _("Can buy every article"))
) )
verbose_name = "Article" verbose_name = "article"
verbose_name_plural = "Articles" verbose_name_plural = "articles"
def clean(self): def clean(self):
if self.name.lower() == 'solde': if self.name.lower() == 'solde':
raise ValidationError( raise ValidationError(
_("Solde is a reserved article name") _("Balance is a reserved article name.")
) )
if self.type_cotisation and not self.duration: if self.type_cotisation and not self.duration:
raise ValidationError( raise ValidationError(
_("Duration must be specified for a cotisation") _("Duration must be specified for a subscription.")
) )
def __str__(self): def __str__(self):
@ -567,19 +591,28 @@ class Article(RevMixin, AclMixin, models.Model):
self.available_for_everyone self.available_for_everyone
or user.has_perm('cotisations.buy_every_article') or user.has_perm('cotisations.buy_every_article')
or user.has_perm('cotisations.add_facture'), or user.has_perm('cotisations.add_facture'),
_("You cannot buy this Article.") _("You can't buy this article.")
) )
@classmethod @classmethod
def find_allowed_articles(cls, user): def find_allowed_articles(cls, user, target_user):
"""Finds every allowed articles for an user. """Finds every allowed articles for an user, on a target user.
Args: Args:
user: The user requesting articles. user: The user requesting articles.
target_user: The user to sell articles
""" """
if target_user.is_class_club:
objects_pool = cls.objects.filter(
Q(type_user='All') | Q(type_user='Club')
)
else:
objects_pool = cls.objects.filter(
Q(type_user='All') | Q(type_user='Adherent')
)
if user.has_perm('cotisations.buy_every_article'): if user.has_perm('cotisations.buy_every_article'):
return cls.objects.all() return objects_pool
return cls.objects.filter(available_for_everyone=True) return objects_pool.filter(available_for_everyone=True)
class Banque(RevMixin, AclMixin, models.Model): class Banque(RevMixin, AclMixin, models.Model):
@ -593,15 +626,14 @@ class Banque(RevMixin, AclMixin, models.Model):
name = models.CharField( name = models.CharField(
max_length=255, max_length=255,
verbose_name=_l("Name")
) )
class Meta: class Meta:
permissions = ( permissions = (
('view_banque', _l("Can see a bank's details")), ('view_banque', _("Can view a bank object")),
) )
verbose_name = _l("Bank") verbose_name = _("bank")
verbose_name_plural = _l("Banks") verbose_name_plural = _("banks")
def __str__(self): def __str__(self):
return self.name return self.name
@ -619,33 +651,33 @@ class Paiement(RevMixin, AclMixin, models.Model):
# 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=_("method")
) )
available_for_everyone = models.BooleanField( available_for_everyone = models.BooleanField(
default=False, default=False,
verbose_name=_l("Is available for every user") verbose_name=_("is available for every user")
) )
is_balance = models.BooleanField( is_balance = models.BooleanField(
default=False, default=False,
editable=False, editable=False,
verbose_name=_l("Is user balance"), verbose_name=_("is user balance"),
help_text=_l("There should be only one balance payment method."), help_text=_("There should be only one balance payment method."),
validators=[check_no_balance] validators=[check_no_balance]
) )
class Meta: class Meta:
permissions = ( permissions = (
('view_paiement', _l("Can see a payement's details")), ('view_paiement', _("Can view a payment method object")),
('use_every_payment', _l("Can use every payement")), ('use_every_payment', _("Can use every payment method")),
) )
verbose_name = _l("Payment method") verbose_name = _("payment method")
verbose_name_plural = _l("Payment methods") verbose_name_plural = _("payment methods")
def __str__(self): def __str__(self):
return self.moyen return self.moyen
def clean(self): def clean(self):
""" """l
Override of the herited clean function to get a correct name Override of the herited clean function to get a correct name
""" """
self.moyen = self.moyen.title() self.moyen = self.moyen.title()
@ -673,8 +705,8 @@ class Paiement(RevMixin, AclMixin, models.Model):
if any(sell.type_cotisation for sell in invoice.vente_set.all()): if any(sell.type_cotisation for sell in invoice.vente_set.all()):
messages.success( messages.success(
request, request,
_("The cotisation of %(member_name)s has been \ _("The subscription of %(member_name)s was extended to"
extended to %(end_date)s.") % { " %(end_date)s.") % {
'member_name': invoice.user.pseudo, 'member_name': invoice.user.pseudo,
'end_date': invoice.user.end_adhesion() 'end_date': invoice.user.end_adhesion()
} }
@ -683,7 +715,7 @@ class Paiement(RevMixin, AclMixin, models.Model):
else: else:
messages.success( messages.success(
request, request,
_("The invoice has been created.") _("The invoice was created.")
) )
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
@ -704,7 +736,7 @@ class Paiement(RevMixin, AclMixin, models.Model):
self.available_for_everyone self.available_for_everyone
or user.has_perm('cotisations.use_every_payment') or user.has_perm('cotisations.use_every_payment')
or user.has_perm('cotisations.add_facture'), or user.has_perm('cotisations.add_facture'),
_("You cannot use this Payment.") _("You can't use this payment method.")
) )
@classmethod @classmethod
@ -722,7 +754,7 @@ class Paiement(RevMixin, AclMixin, models.Model):
p = find_payment_method(self) p = find_payment_method(self)
if p is not None: if p is not None:
return p._meta.verbose_name return p._meta.verbose_name
return _("No custom payment method") return _("No custom payment method.")
class Cotisation(RevMixin, AclMixin, models.Model): class Cotisation(RevMixin, AclMixin, models.Model):
@ -738,9 +770,9 @@ class Cotisation(RevMixin, AclMixin, models.Model):
""" """
COTISATION_TYPE = ( COTISATION_TYPE = (
('Connexion', _l("Connexion")), ('Connexion', _("Connection")),
('Adhesion', _l("Membership")), ('Adhesion', _("Membership")),
('All', _l("Both of them")), ('All', _("Both of them")),
) )
# TODO : change vente to purchase # TODO : change vente to purchase
@ -748,34 +780,36 @@ class Cotisation(RevMixin, AclMixin, models.Model):
'Vente', 'Vente',
on_delete=models.CASCADE, on_delete=models.CASCADE,
null=True, null=True,
verbose_name=_l("Purchase") verbose_name=_("purchase")
) )
type_cotisation = models.CharField( type_cotisation = models.CharField(
choices=COTISATION_TYPE, choices=COTISATION_TYPE,
max_length=255, max_length=255,
default='All', default='All',
verbose_name=_l("Type of cotisation") verbose_name=_("subscription type")
) )
date_start = models.DateTimeField( date_start = models.DateTimeField(
verbose_name=_l("Starting date") verbose_name=_("start date")
) )
date_end = models.DateTimeField( date_end = models.DateTimeField(
verbose_name=_l("Ending date") verbose_name=_("end date")
) )
class Meta: class Meta:
permissions = ( permissions = (
('view_cotisation', _l("Can see a cotisation's details")), ('view_cotisation', _("Can view a subscription object")),
('change_all_cotisation', _l("Can edit the previous cotisations")), ('change_all_cotisation', _("Can edit the previous subscriptions")),
) )
verbose_name = _("subscription")
verbose_name_plural = _("subscriptions")
def can_edit(self, user_request, *_args, **_kwargs): def can_edit(self, user_request, *_args, **_kwargs):
if not user_request.has_perm('cotisations.change_cotisation'): if not user_request.has_perm('cotisations.change_cotisation'):
return False, _("You don't have the right to edit a cotisation.") return False, _("You don't have the right to edit a subscription.")
elif not user_request.has_perm('cotisations.change_all_cotisation') \ elif not user_request.has_perm('cotisations.change_all_cotisation') \
and (self.vente.facture.control or and (self.vente.facture.control or
not self.vente.facture.valid): not self.vente.facture.valid):
return False, _("You don't have the right to edit a cotisation " return False, _("You don't have the right to edit a subscription "
"already controlled or invalidated.") "already controlled or invalidated.")
else: else:
return True, None return True, None
@ -783,9 +817,9 @@ class Cotisation(RevMixin, AclMixin, models.Model):
def can_delete(self, user_request, *_args, **_kwargs): def can_delete(self, user_request, *_args, **_kwargs):
if not user_request.has_perm('cotisations.delete_cotisation'): if not user_request.has_perm('cotisations.delete_cotisation'):
return False, _("You don't have the right to delete a " return False, _("You don't have the right to delete a "
"cotisation.") "subscription.")
if self.vente.facture.control or not self.vente.facture.valid: if self.vente.facture.control or not self.vente.facture.valid:
return False, _("You don't have the right to delete a cotisation " return False, _("You don't have the right to delete a subscription "
"already controlled or invalidated.") "already controlled or invalidated.")
else: else:
return True, None return True, None
@ -793,8 +827,8 @@ class Cotisation(RevMixin, AclMixin, models.Model):
def can_view(self, user_request, *_args, **_kwargs): def can_view(self, user_request, *_args, **_kwargs):
if not user_request.has_perm('cotisations.view_cotisation') and\ if not user_request.has_perm('cotisations.view_cotisation') and\
self.vente.facture.user != user_request: self.vente.facture.user != user_request:
return False, _("You don't have the right to see someone else's " return False, _("You don't have the right to view someone else's "
"cotisation history.") "subscription history.")
else: else:
return True, None return True, None
@ -822,3 +856,4 @@ def cotisation_post_delete(**_kwargs):
""" """
regen('mac_ip_list') regen('mac_ip_list')
regen('mailing') regen('mailing')

View file

@ -21,8 +21,7 @@
from django.db import models from django.db import models
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _l
from django.contrib import messages from django.contrib import messages
@ -36,7 +35,7 @@ class BalancePayment(PaymentMethodMixin, models.Model):
""" """
class Meta: class Meta:
verbose_name = _l("User Balance") verbose_name = _("user balance")
payment = models.OneToOneField( payment = models.OneToOneField(
Paiement, Paiement,
@ -45,8 +44,8 @@ class BalancePayment(PaymentMethodMixin, models.Model):
editable=False editable=False
) )
minimum_balance = models.DecimalField( minimum_balance = models.DecimalField(
verbose_name=_l("Minimum balance"), verbose_name=_("Minimum balance"),
help_text=_l("The minimal amount of money allowed for the balance" help_text=_("The minimal amount of money allowed for the balance"
" at the end of a payment. You can specify negative " " at the end of a payment. You can specify negative "
"amount." "amount."
), ),
@ -55,8 +54,8 @@ class BalancePayment(PaymentMethodMixin, models.Model):
default=0, default=0,
) )
maximum_balance = models.DecimalField( maximum_balance = models.DecimalField(
verbose_name=_l("Maximum balance"), verbose_name=_("Maximum balance"),
help_text=_l("The maximal amount of money allowed for the balance."), help_text=_("The maximal amount of money allowed for the balance."),
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
default=50, default=50,
@ -64,7 +63,7 @@ class BalancePayment(PaymentMethodMixin, models.Model):
null=True, null=True,
) )
credit_balance_allowed = models.BooleanField( credit_balance_allowed = models.BooleanField(
verbose_name=_l("Allow user to credit their balance"), verbose_name=_("Allow user to credit their balance"),
default=False, default=False,
) )
@ -97,7 +96,7 @@ class BalancePayment(PaymentMethodMixin, models.Model):
if len(p) > 0: if len(p) > 0:
form.add_error( form.add_error(
'payment_method', 'payment_method',
_("There is already a payment type for user balance") _("There is already a payment method for user balance.")
) )
def alter_payment(self, payment): def alter_payment(self, payment):
@ -118,3 +117,4 @@ class BalancePayment(PaymentMethodMixin, models.Model):
len(Paiement.find_allowed_payments(user_request) len(Paiement.find_allowed_payments(user_request)
.exclude(is_balance=True)) > 0 .exclude(is_balance=True)) > 0
) and self.credit_balance_allowed ) and self.credit_balance_allowed

View file

@ -21,7 +21,7 @@
from django.db import models from django.db import models
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext_lazy as _l from django.utils.translation import ugettext_lazy as _
from cotisations.models import Paiement from cotisations.models import Paiement
from cotisations.payment_methods.mixins import PaymentMethodMixin from cotisations.payment_methods.mixins import PaymentMethodMixin
@ -33,7 +33,7 @@ class ChequePayment(PaymentMethodMixin, models.Model):
""" """
class Meta: class Meta:
verbose_name = _l("Cheque") verbose_name = _("Cheque")
payment = models.OneToOneField( payment = models.OneToOneField(
Paiement, Paiement,
@ -52,3 +52,4 @@ class ChequePayment(PaymentMethodMixin, models.Model):
'cotisations:cheque:validate', 'cotisations:cheque:validate',
kwargs={'invoice_pk': invoice.pk} kwargs={'invoice_pk': invoice.pk}
)) ))

View file

@ -44,7 +44,7 @@ def cheque(request, invoice_pk):
if invoice.valid or not isinstance(payment_method, ChequePayment): if invoice.valid or not isinstance(payment_method, ChequePayment):
messages.error( messages.error(
request, request,
_("You cannot pay this invoice with a cheque.") _("You can't pay this invoice with a cheque.")
) )
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
@ -67,3 +67,4 @@ def cheque(request, invoice_pk):
'amount': invoice.prix_total() 'amount': invoice.prix_total()
} }
) )

View file

@ -21,8 +21,7 @@
from django.db import models from django.db import models
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _l
from cotisations.models import Paiement from cotisations.models import Paiement
from cotisations.payment_methods.mixins import PaymentMethodMixin from cotisations.payment_methods.mixins import PaymentMethodMixin
@ -37,7 +36,7 @@ class ComnpayPayment(PaymentMethodMixin, models.Model):
""" """
class Meta: class Meta:
verbose_name = "ComNpay" verbose_name = _("ComNpay")
payment = models.OneToOneField( payment = models.OneToOneField(
Paiement, Paiement,
@ -49,22 +48,32 @@ class ComnpayPayment(PaymentMethodMixin, models.Model):
max_length=255, max_length=255,
default='', default='',
blank=True, blank=True,
verbose_name=_l("ComNpay VAD Number"), verbose_name=_("ComNpay VAT Number"),
) )
payment_pass = AESEncryptedField( payment_pass = AESEncryptedField(
max_length=255, max_length=255,
null=True, null=True,
blank=True, blank=True,
verbose_name=_l("ComNpay Secret Key"), verbose_name=_("ComNpay secret key"),
) )
minimum_payment = models.DecimalField( minimum_payment = models.DecimalField(
verbose_name=_l("Minimum payment"), verbose_name=_("Minimum payment"),
help_text=_l("The minimal amount of money you have to use when paying" help_text=_("The minimal amount of money you have to use when paying"
" with ComNpay"), " with ComNpay"),
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
default=1, default=1,
) )
production = models.BooleanField(
default=True,
verbose_name=_("Production mode enabled (production URL, instead of homologation)"),
)
def return_url_comnpay(self):
if self.production:
return 'https://secure.comnpay.com'
else:
return 'https://secure.homologation.comnpay.com'
def end_payment(self, invoice, request): def end_payment(self, invoice, request):
""" """
@ -87,11 +96,12 @@ class ComnpayPayment(PaymentMethodMixin, models.Model):
"", "",
"D" "D"
) )
r = { r = {
'action': 'https://secure.homologation.comnpay.com', 'action': self.return_url_comnpay(),
'method': 'POST', 'method': 'POST',
'content': p.buildSecretHTML( 'content': p.buildSecretHTML(
_("Pay invoice no : ")+str(invoice.id), _("Pay invoice number ")+str(invoice.id),
invoice.prix_total(), invoice.prix_total(),
idTransaction=str(invoice.id) idTransaction=str(invoice.id)
), ),
@ -103,6 +113,6 @@ class ComnpayPayment(PaymentMethodMixin, models.Model):
"""Checks that the price meets the requirement to be paid with ComNpay. """Checks that the price meets the requirement to be paid with ComNpay.
""" """
return ((price >= self.minimum_payment), return ((price >= self.minimum_payment),
_('In order to pay your invoice with ComNpay' _("In order to pay your invoice with ComNpay, the price must"
', the price must be grater than {}') " be greater than {} €.").format(self.minimum_payment))
.format(self.minimum_payment))

View file

@ -50,7 +50,7 @@ def accept_payment(request, factureid):
if invoice.valid: if invoice.valid:
messages.success( messages.success(
request, request,
_("The payment of %(amount)shas been accepted.") % { _("The payment of %(amount)swas accepted.") % {
'amount': invoice.prix_total() 'amount': invoice.prix_total()
} }
) )
@ -60,8 +60,8 @@ def accept_payment(request, factureid):
for purchase in invoice.vente_set.all()): for purchase in invoice.vente_set.all()):
messages.success( messages.success(
request, request,
_("The cotisation of %(member_name)s has been \ _("The subscription of %(member_name)s was extended to"
extended to %(end_date)s.") % { " %(end_date)s.") % {
'member_name': request.user.pseudo, 'member_name': request.user.pseudo,
'end_date': request.user.end_adhesion() 'end_date': request.user.end_adhesion()
} }
@ -81,7 +81,7 @@ def refuse_payment(request):
""" """
messages.error( messages.error(
request, request,
_("The payment has been refused.") _("The payment was refused.")
) )
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
@ -136,3 +136,4 @@ 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")

View file

@ -19,8 +19,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.
from django import forms from django import forms
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _l
from . import PAYMENT_METHODS from . import PAYMENT_METHODS
from cotisations.utils import find_payment_method from cotisations.utils import find_payment_method
@ -58,8 +57,8 @@ class PaymentMethodForm(forms.Form):
""" """
payment_method = forms.ChoiceField( payment_method = forms.ChoiceField(
label=_l("Special payment method"), label=_("Special payment method"),
help_text=_l("Warning : You will not be able to change the payment " help_text=_("Warning: you will not be able to change the payment "
"method later. But you will be allowed to edit the other " "method later. But you will be allowed to edit the other "
"options." "options."
), ),
@ -70,7 +69,7 @@ class PaymentMethodForm(forms.Form):
super(PaymentMethodForm, self).__init__(*args, **kwargs) super(PaymentMethodForm, self).__init__(*args, **kwargs)
prefix = kwargs.get('prefix', None) 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 = [(i,p.NAME) for (i,p) in enumerate(PAYMENT_METHODS)]
self.fields['payment_method'].choices.insert(0, ('', _l('no'))) self.fields['payment_method'].choices.insert(0, ('', _('no')))
self.fields['payment_method'].widget.attrs = { self.fields['payment_method'].widget.attrs = {
'id': 'paymentMethodSelect' 'id': 'paymentMethodSelect'
} }

View file

@ -32,10 +32,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<th>{% trans "Article" %}</th> <th>{% trans "Article" %}</th>
<th>{% trans "Price" %}</th> <th>{% trans "Price" %}</th>
<th>{% trans "Cotisation type" %}</th> <th>{% trans "Subscription type" %}</th>
<th>{% trans "Duration (month)" %}</th> <th>{% trans "Duration (in months)" %}</th>
<th>{% trans "Concerned users" %}</th> <th>{% trans "Concerned users" %}</th>
<th>{% trans "Available for everyone" | tick %}</th> <th>{% trans "Available for everyone" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -46,7 +46,7 @@ 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>{{ article.available_for_everyone | tick }}</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 %}">

View file

@ -26,7 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load i18n %} {% load i18n %}
{% load logs_extra %} {% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>{% trans "Bank" %}</th> <th>{% trans "Bank" %}</th>
@ -38,13 +38,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ banque.name }}</td> <td>{{ banque.name }}</td>
<td class="text-right"> <td class="text-right">
{% can_edit banque %} {% can_edit banque %}
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-banque' banque.id %}"> {% include 'buttons/edit.html' with href='cotisations:edit-banque' id=banque.id %}
<i class="fa fa-edit"></i>
</a>
{% acl_end %} {% acl_end %}
{% history_button banque %} {% history_button banque %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -49,7 +49,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% include 'buttons/sort.html' with prefix='cotis' col='date' text=tr_date %} {% include 'buttons/sort.html' with prefix='cotis' col='date' text=tr_date %}
</th> </th>
<th> <th>
{% trans "Invoice id" as tr_invoice_id %} {% trans "Invoice ID" as tr_invoice_id %}
{% include 'buttons/sort.html' with prefix='cotis' col='id' text=tr_invoice_id %} {% include 'buttons/sort.html' with prefix='cotis' col='id' text=tr_invoice_id %}
</th> </th>
<th></th> <th></th>
@ -65,32 +65,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ facture.date }}</td> <td>{{ facture.date }}</td>
<td>{{ facture.id }}</td> <td>{{ facture.id }}</td>
<td> <td>
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" id="editinvoice" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{% trans "Edit" %}<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="editinvoice">
{% can_edit facture %} {% can_edit facture %}
<li> {% include 'buttons/edit.html' with href='cotisations:edit-facture' id=facture.id %}
<a href="{% url 'cotisations:edit-facture' facture.id %}">
<i class="fa fa-dollar-sign"></i> {% trans "Edit" %}
</a>
</li>
{% acl_else %} {% acl_else %}
<li>{% trans "Controlled invoice" %}</li> {% trans "Controlled invoice" %}
{% acl_end %} {% acl_end %}
{% can_delete facture %} {% can_delete facture %}
<li> {% include 'buttons/suppr.html' with href='cotisations:del-facture' id=facture.id %}
<a href="{% url 'cotisations:del-facture' facture.id %}">
<i class="fa fa-trash"></i> {% trans "Delete" %}
</a>
</li>
{% acl_end %} {% acl_end %}
<li> {% history_button facture %}
{% history_button facture text=True html_class=False%}
</li>
</ul>
</div>
</td> </td>
<td> <td>
{% if facture.valid %} {% if facture.valid %}
@ -109,3 +92,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% include 'pagination.html' with list=facture_list %} {% include 'pagination.html' with list=facture_list %}
{% endif %} {% endif %}
</div> </div>

View file

@ -0,0 +1,89 @@
{% 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 acl %}
{% load logs_extra %}
{% load design %}
<div class="table-responsive">
{% if custom_invoice_list.paginator %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>
{% trans "Recipient" as tr_recip %}
{% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_user %}
</th>
<th>{% trans "Designation" %}</th>
<th>{% trans "Total price" %}</th>
<th>
{% trans "Payment method" as tr_payment_method %}
{% include 'buttons/sort.html' with prefix='invoice' col='payement' text=tr_payment_method %}
</th>
<th>
{% trans "Date" as tr_date %}
{% include 'buttons/sort.html' with prefix='invoice' col='date' text=tr_date %}
</th>
<th>
{% trans "Invoice ID" as tr_invoice_id %}
{% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_invoice_id %}
</th>
<th>
{% trans "Paid" as tr_invoice_paid%}
{% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_paid %}
</th>
<th></th>
<th></th>
</tr>
</thead>
{% for invoice in custom_invoice_list %}
<tr>
<td>{{ invoice.recipient }}</td>
<td>{{ invoice.name }}</td>
<td>{{ invoice.prix_total }}</td>
<td>{{ invoice.payment }}</td>
<td>{{ invoice.date }}</td>
<td>{{ invoice.id }}</td>
<td>{{ invoice.paid|tick }}</td>
<td>
{% can_edit invoice %}
{% include 'buttons/edit.html' with href='cotisations:edit-custom-invoice' id=invoice.id %}
{% acl_end %}
{% can_delete invoice %}
{% include 'buttons/suppr.html' with href='cotisations:del-custom-invoice' id=invoice.id %}
{% acl_end %}
{% history_button invoice %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:custom-invoice-pdf' invoice.id %}">
<i class="fa fa-file-pdf"></i> {% trans "PDF" %}
</a>
</td>
</tr>
{% endfor %}
</table>
{% if custom_invoice_list.paginator %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% endif %}
</div>

View file

@ -41,7 +41,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ paiement.moyen }}</td> <td>{{ paiement.moyen }}</td>
<td>{{ paiement.available_for_everyone|tick }}</td> <td>{{ paiement.available_for_everyone|tick }}</td>
<td> <td>
{{paiement.get_payment_method_name}} {{ paiement.get_payment_method_name }}
</td> </td>
<td class="text-right"> <td class="text-right">
{% can_edit paiement %} {% can_edit paiement %}

View file

@ -30,17 +30,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "Invoice control" %}{% endblock %} {% block title %}{% trans "Invoice control" %}{% endblock %}
{% block content %} {% block content %}
<h2>{% trans "Invoice control and validation" %}</h2> <h2>{% trans "Invoice control and validation" %}</h2>
{% if facture_list.paginator %} {% if facture_list.paginator %}
{% include 'pagination.html' with list=facture_list %} {% include 'pagination.html' with list=facture_list %}
{% endif %} {% endif %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
{{ controlform.management_form }} {{ controlform.management_form }}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>{% trans "Profil" %}</th> <th>{% trans "Profile" %}</th>
<th> <th>
{% trans "Last name" as tr_last_name %} {% trans "Last name" as tr_last_name %}
{% include 'buttons/sort.html' with prefix='control' col='name' text=tr_last_name %} {% include 'buttons/sort.html' with prefix='control' col='name' text=tr_last_name %}
@ -50,11 +53,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% include 'buttons/sort.html' with prefix='control' col='surname' text=tr_first_name %} {% include 'buttons/sort.html' with prefix='control' col='surname' text=tr_first_name %}
</th> </th>
<th> <th>
{% trans "Invoice id" as tr_invoice_id %} {% trans "Invoice ID" as tr_invoice_id %}
{% include 'buttons/sort.html' with prefix='control' col='id' text=tr_invoice_id %} {% include 'buttons/sort.html' with prefix='control' col='id' text=tr_invoice_id %}
</th> </th>
<th> <th>
{% trans "User id" as tr_user_id %} {% trans "User ID" as tr_user_id %}
{% include 'buttons/sort.html' with prefix='control' col='user-id' text=tr_user_id %} {% include 'buttons/sort.html' with prefix='control' col='user-id' text=tr_user_id %}
</th> </th>
<th>{% trans "Designation" %}</th> <th>{% trans "Designation" %}</th>
@ -65,7 +68,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</th> </th>
<th> <th>
{% trans "Date" as tr_date %} {% trans "Date" as tr_date %}
{% include 'buttons/sort.html' with prefix='control' col='date' text=tr_date %}i {% include 'buttons/sort.html' with prefix='control' col='date' text=tr_date %}
</th> </th>
<th> <th>
{% trans "Validated" as tr_validated %} {% trans "Validated" as tr_validated %}
@ -109,3 +112,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if facture_list.paginator %} {% if facture_list.paginator %}
{% include 'pagination.html' with list=facture_list %} {% include 'pagination.html' with list=facture_list %}
{% endif %} {% endif %}

View file

@ -26,18 +26,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Deletion of cotisations" %}{% endblock %} {% block title %}{% trans "Deletion of subscriptions" %}{% endblock %}
{% block content %} {% block content %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
<h4> <h4>
{% blocktrans %} {% blocktrans %}Warning: are you sure you really want to delete this {{ object_name }} object ( {{ objet }} )?{% endblocktrans %}
Warning. Are you sure you really want te delete this {{ object_name }} object ( {{ objet }} ) ?
{% endblocktrans %}
</h4> </h4>
{% trans "Confirm" as tr_confirm %} {% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type='submit' icon='trash' %} {% bootstrap_button tr_confirm button_type='submit' icon='trash' %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load massive_bootstrap_form %} {% load massive_bootstrap_form %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Invoices creation and edition" %}{% endblock %} {% block title %}{% trans "Creation and editing of invoices" %}{% endblock %}
{% block content %} {% block content %}
{% bootstrap_form_errors factureform %} {% bootstrap_form_errors factureform %}
@ -62,3 +62,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</form> </form>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,22 @@
=== English version below ===
Bonjour {{name}},
Nous vous remercions pour votre achat auprès de {{asso_name}} et nous vous en joignons la facture.
En cas de question, nhésitez pas à nous contacter par mail à {{contact_mail}}.
Cordialement,
Léquipe de {{asso_name}}
=== English version ===
Dear {{name}},
Thank you for your purchase. Here is your invoice.
Should you need extra information, you can email us at {{contact_mail}}.
Best regards,
{{ asso_name }}'s team

View file

@ -27,20 +27,21 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load staticfiles%} {% load staticfiles%}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Invoices creation and edition" %}{% endblock %} {% block title %}{% trans "Creation and editing of invoices" %}{% endblock %}
{% block content %} {% block content %}
{% if title %} {% if title %}
<h3>{{title}}</h3> <h3>{{ title }}</h3>
{% else %} {% else %}
<h3>{% trans "New invoice" %}</h3> <h3>{% trans "New invoice" %}</h3>
{% endif %} {% endif %}
{% if max_balance %} {% if max_balance %}
<h4>{% trans "Maximum allowed balance : "%}{{max_balance}} €</h4> <h4>{% blocktrans %}Maximum allowed balance: {{ max_balance }} €{% endblocktrans %}</h4>
{% endif %} {% endif %}
{% if balance is not None %} {% if balance is not None %}
<p> <p>
{% trans "Current balance :" %} {{ balance }} € {% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}
</p> </p>
{% endif %} {% endif %}
@ -68,9 +69,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div> </div>
<input class="btn btn-primary btn-sm" role="button" value="{% trans "Add an article"%}" id="add_one"> <input class="btn btn-primary btn-sm" role="button" value="{% trans "Add an article"%}" id="add_one">
<p> <p>
{% blocktrans %} {% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
Total price : <span id="total_price">0,00</span>
{% endblocktrans %}
</p> </p>
{% endif %} {% endif %}
{% bootstrap_button action_name button_type='submit' icon='star' %} {% bootstrap_button action_name button_type='submit' icon='star' %}
@ -183,3 +182,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -29,7 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "Invoices" %}{% endblock %} {% block title %}{% trans "Invoices" %}{% endblock %}
{% block content %} {% block content %}
<h2>{% trans "Cotisations" %}</h2> <h2>{% trans "Subscriptions" %}</h2>
{% include 'cotisations/aff_cotisations.html' with facture_list=facture_list %} {% include 'cotisations/aff_cotisations.html' with facture_list=facture_list %}
{% endblock %} {% endblock %}

View file

@ -37,7 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</a> </a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-article' %}"> <a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-article' %}">
<i class="fa fa-trash"></i> {% trans "Delete article types" %} <i class="fa fa-trash"></i> {% trans "Delete one or several article types" %}
</a> </a>
{% include 'cotisations/aff_article.html' with article_list=article_list %} {% include 'cotisations/aff_article.html' with article_list=article_list %}
{% endblock %} {% endblock %}

View file

@ -37,7 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</a> </a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-banque' %}"> <a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-banque' %}">
<i class="fa fa-trash"></i> {% trans "Delete banks" %} <i class="fa fa-trash"></i> {% trans "Delete one or several banks" %}
</a> </a>
{% include 'cotisations/aff_banque.html' with banque_list=banque_list %} {% include 'cotisations/aff_banque.html' with banque_list=banque_list %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,36 @@
{% 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 acl %}
{% load i18n %}
{% block title %}{% trans "Custom invoices" %}{% endblock %}
{% block content %}
<h2>{% trans "Custom invoices list" %}</h2>
{% can_create CustomInvoice %}
{% include "buttons/add.html" with href='cotisations:new-custom-invoice'%}
{% acl_end %}
{% include 'cotisations/aff_custom_invoice.html' with custom_invoice_list=custom_invoice_list %}
{% endblock %}

View file

@ -27,17 +27,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Payments" %}{% endblock %} {% block title %}{% trans "Payment methods" %}{% endblock %}
{% block content %} {% block content %}
<h2>{% trans "Payment types list" %}</h2> <h2>{% trans "List of payment methods" %}</h2>
{% can_create Paiement %} {% can_create Paiement %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:add-paiement' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:add-paiement' %}">
<i class="fa fa-cart-plus"></i> {% trans "Add a payment type" %} <i class="fa fa-cart-plus"></i> {% trans "Add a payment method" %}
</a> </a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-paiement' %}"> <a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-paiement' %}">
<i class="fa fa-trash"></i> {% trans "Delete payment types" %} <i class="fa fa-trash"></i> {% trans "Delete one or several payment methods" %}
</a> </a>
{% include 'cotisations/aff_paiement.html' with paiement_list=paiement_list %} {% include 'cotisations/aff_paiement.html' with paiement_list=paiement_list %}
{% endblock %} {% endblock %}

View file

@ -31,11 +31,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %} {% block content %}
<h3> <h3>
{% blocktrans %} {% blocktrans %}Pay {{ amount }} €{% endblocktrans %}
Pay {{ amount }} €
{% endblocktrans %}
</h3> </h3>
<form class="form" method="{{ method | default:"post" }}" action="{{ action }}"> <form class="form" method="{{ method|default:"post" }}" action="{{ action }}">
{{ content | safe }} {{ content | safe }}
{% if form %} {% if form %}
{% csrf_token %} {% csrf_token %}
@ -45,3 +43,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %} {% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -27,8 +27,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load i18n %} {% load i18n %}
{% block sidebar %} {% block sidebar %}
{% can_change Facture pdf %} {% can_create CustomInvoice %}
<a class="list-group-item list-group-item-success" href="{% url "cotisations:new-facture-pdf" %}"> <a class="list-group-item list-group-item-success" href="{% url "cotisations:new-custom-invoice" %}">
<i class="fa fa-plus"></i> {% trans "Create an invoice" %} <i class="fa fa-plus"></i> {% trans "Create an invoice" %}
</a> </a>
<a class="list-group-item list-group-item-warning" href="{% url "cotisations:control" %}"> <a class="list-group-item list-group-item-warning" href="{% url "cotisations:control" %}">
@ -40,6 +40,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-list-ul"></i> {% trans "Invoices" %} <i class="fa fa-list-ul"></i> {% trans "Invoices" %}
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all CustomInvoice %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-custom-invoice" %}">
<i class="fa fa-list-ul"></i> {% trans "Custom invoices" %}
</a>
{% acl_end %}
{% can_view_all Article %} {% can_view_all Article %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}"> <a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
<i class="fa fa-list-ul"></i> {% trans "Available articles" %} <i class="fa fa-list-ul"></i> {% trans "Available articles" %}
@ -56,3 +61,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</a> </a>
{% acl_end %} {% acl_end %}
{% endblock %} {% endblock %}

View file

@ -1,3 +1,4 @@
# 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.
@ -24,6 +25,7 @@ Module in charge of rendering some LaTex templates.
Used to generated PDF invoice. Used to generated PDF invoice.
""" """
import tempfile import tempfile
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
import os import os
@ -61,18 +63,24 @@ def render_invoice(_request, ctx={}):
return r return r
def render_tex(_request, template, ctx={}): def create_pdf(template, ctx={}):
""" """Creates and returns a PDF from a LaTeX template using pdflatex.
Creates a PDF from a LaTex templates using pdflatex.
Writes it in a temporary directory and send back an HTTP response for It create a temporary file for the PDF then read it to return its content.
accessing this file.
Args:
template: Path to the LaTeX template.
ctx: Dict with the context for rendering the template.
Returns:
The content of the temporary PDF file generated.
""" """
context = Context(ctx) context = Context(ctx)
template = get_template(template) template = get_template(template)
rendered_tpl = template.render(context).encode('utf-8') rendered_tpl = template.render(context).encode('utf-8')
with tempfile.TemporaryDirectory() as tempdir: with tempfile.TemporaryDirectory() as tempdir:
for i in range(2): for _ in range(2):
process = Popen( process = Popen(
['pdflatex', '-output-directory', tempdir], ['pdflatex', '-output-directory', tempdir],
stdin=PIPE, stdin=PIPE,
@ -81,6 +89,25 @@ def render_tex(_request, template, ctx={}):
process.communicate(rendered_tpl) process.communicate(rendered_tpl)
with open(os.path.join(tempdir, 'texput.pdf'), 'rb') as f: with open(os.path.join(tempdir, 'texput.pdf'), 'rb') as f:
pdf = f.read() pdf = f.read()
return pdf
def render_tex(_request, template, ctx={}):
"""Creates a PDF from a LaTex templates using pdflatex.
Calls `create_pdf` and send back an HTTP response for
accessing this file.
Args:
_request: Unused, but allow using this function as a Django view.
template: Path to the LaTeX template.
ctx: Dict with the context for rendering the template.
Returns:
An HttpResponse with type `application/pdf` containing the PDF file.
"""
pdf = create_pdf(template, ctx)
r = HttpResponse(content_type='application/pdf') r = HttpResponse(content_type='application/pdf')
r.write(pdf) r.write(pdf)
return r return r

View file

@ -52,9 +52,29 @@ urlpatterns = [
name='facture-pdf' name='facture-pdf'
), ),
url( url(
r'^new_facture_pdf/$', r'^index_custom_invoice/$',
views.new_facture_pdf, views.index_custom_invoice,
name='new-facture-pdf' name='index-custom-invoice'
),
url(
r'^new_custom_invoice/$',
views.new_custom_invoice,
name='new-custom-invoice'
),
url(
r'^edit_custom_invoice/(?P<custominvoiceid>[0-9]+)$',
views.edit_custom_invoice,
name='edit-custom-invoice'
),
url(
r'^custom_invoice_pdf/(?P<custominvoiceid>[0-9]+)$',
views.custom_invoice_pdf,
name='custom-invoice-pdf',
),
url(
r'^del_custom_invoice/(?P<custominvoiceid>[0-9]+)$',
views.del_custom_invoice,
name='del-custom-invoice'
), ),
url( url(
r'^credit_solde/(?P<userid>[0-9]+)$', r'^credit_solde/(?P<userid>[0-9]+)$',

View file

@ -19,6 +19,16 @@
# 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.
import os
from django.template.loader import get_template
from django.core.mail import EmailMessage
from .tex import create_pdf
from preferences.models import AssoOption, GeneralOption
from re2o.settings import LOGO_PATH
from re2o import settings
def find_payment_method(payment): def find_payment_method(payment):
"""Finds the payment method associated to the payment if it exists.""" """Finds the payment method associated to the payment if it exists."""
@ -30,3 +40,56 @@ def find_payment_method(payment):
except method.PaymentMethod.DoesNotExist: except method.PaymentMethod.DoesNotExist:
pass pass
return None return None
def send_mail_invoice(invoice):
"""Creates the pdf of the invoice and sends it by email to the client"""
purchases_info = []
for purchase in invoice.vente_set.all():
purchases_info.append({
'name': purchase.name,
'price': purchase.prix,
'quantity': purchase.number,
'total_price': purchase.prix_total
})
ctx = {
'paid': True,
'fid': invoice.id,
'DATE': invoice.date,
'recipient_name': "{} {}".format(
invoice.user.name,
invoice.user.surname
),
'address': invoice.user.room,
'article': purchases_info,
'total': invoice.prix_total(),
'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'),
'line2': AssoOption.get_cached_value('adresse2'),
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
}
pdf = create_pdf('cotisations/factures.tex', ctx)
template = get_template('cotisations/email_invoice')
ctx = {
'name': "{} {}".format(
invoice.user.name,
invoice.user.surname
),
'contact_mail': AssoOption.get_cached_value('contact'),
'asso_name': AssoOption.get_cached_value('name')
}
mail = EmailMessage(
'Votre facture / Your invoice',
template.render(ctx),
GeneralOption.get_cached_value('email_from'),
[invoice.user.email],
attachments=[('invoice.pdf', pdf, 'application/pdf')]
)
mail.send()

View file

@ -17,5 +17,6 @@ def check_no_balance(is_balance):
p = Paiement.objects.filter(is_balance=True) p = Paiement.objects.filter(is_balance=True)
if len(p) > 0: if len(p) > 0:
raise ValidationError( raise ValidationError(
_("There are already payment method(s) for user balance") _("There is already a payment method for user balance.")
) )

View file

@ -58,7 +58,15 @@ from re2o.acl import (
can_change, can_change,
) )
from preferences.models import AssoOption, GeneralOption from preferences.models import AssoOption, GeneralOption
from .models import Facture, Article, Vente, Paiement, Banque from .models import (
Facture,
Article,
Vente,
Paiement,
Banque,
CustomInvoice,
BaseInvoice
)
from .forms import ( from .forms import (
FactureForm, FactureForm,
ArticleForm, ArticleForm,
@ -67,14 +75,13 @@ from .forms import (
DelPaiementForm, DelPaiementForm,
BanqueForm, BanqueForm,
DelBanqueForm, DelBanqueForm,
NewFactureFormPdf, SelectArticleForm,
SelectUserArticleForm, RechargeForm,
SelectClubArticleForm, CustomInvoiceForm
RechargeForm
) )
from .tex import render_invoice from .tex import render_invoice
from .payment_methods.forms import payment_method_factory from .payment_methods.forms import payment_method_factory
from .utils import find_payment_method from .utils import find_payment_method, send_mail_invoice
@login_required @login_required
@ -102,15 +109,9 @@ def new_facture(request, user, userid):
creation=True creation=True
) )
if request.user.is_class_club: article_formset = formset_factory(SelectArticleForm)(
article_formset = formset_factory(SelectClubArticleForm)(
request.POST or None, request.POST or None,
form_kwargs={'user': request.user} form_kwargs={'user': request.user, 'target_user': user}
)
else:
article_formset = formset_factory(SelectUserArticleForm)(
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():
@ -147,6 +148,8 @@ def new_facture(request, user, userid):
p.facture = new_invoice_instance p.facture = new_invoice_instance
p.save() p.save()
send_mail_invoice(new_invoice_instance)
return new_invoice_instance.paiement.end_payment( return new_invoice_instance.paiement.end_payment(
new_invoice_instance, new_invoice_instance,
request request
@ -161,6 +164,7 @@ def new_facture(request, user, userid):
balance = user.solde balance = user.solde
else: else:
balance = None balance = None
return form( return form(
{ {
'factureform': invoice_form, 'factureform': invoice_form,
@ -175,10 +179,10 @@ def new_facture(request, user, userid):
# TODO : change facture to invoice # TODO : change facture to invoice
@login_required @login_required
@can_change(Facture, 'pdf') @can_create(CustomInvoice)
def new_facture_pdf(request): def new_custom_invoice(request):
""" """
View used to generate a custom PDF invoice. It's mainly used to View used to generate a custom invoice. It's mainly used to
get invoices that are not taken into account, for the administrative get invoices that are not taken into account, for the administrative
point of view. point of view.
""" """
@ -187,56 +191,39 @@ def new_facture_pdf(request):
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 invocie form and the article formset
invoice_form = NewFactureFormPdf(request.POST or None) invoice_form = CustomInvoiceForm(request.POST or None)
if request.user.is_class_club:
articles_formset = formset_factory(SelectClubArticleForm)( article_formset = formset_factory(SelectArticleForm)(
request.POST or None, request.POST or None,
form_kwargs={'user': request.user} form_kwargs={'user': request.user, 'target_user': user}
) )
else:
articles_formset = formset_factory(SelectUserArticleForm)( if invoice_form.is_valid() and articles_formset.is_valid():
request.POST or None, new_invoice_instance = invoice_form.save()
form_kwargs={'user': request.user} for art_item in articles_formset:
) if art_item.cleaned_data:
if invoice_form.is_valid() and articles_formset.is_valid(): article = art_item.cleaned_data['article']
# Get the article list and build an list out of it quantity = art_item.cleaned_data['quantity']
# contiaining (article_name, article_price, quantity, total_price) Vente.objects.create(
articles_info = [] facture=new_invoice_instance,
for articles_form in articles_formset: name=article.name,
if articles_form.cleaned_data: prix=article.prix,
article = articles_form.cleaned_data['article'] type_cotisation=article.type_cotisation,
quantity = articles_form.cleaned_data['quantity'] duration=article.duration,
articles_info.append({ number=quantity
'name': article.name, )
'price': article.prix, messages.success(
'quantity': quantity, request,
'total_price': article.prix * quantity _("The custom invoice was created.")
}) )
paid = invoice_form.cleaned_data['paid'] return redirect(reverse('cotisations:index-custom-invoice'))
recipient = invoice_form.cleaned_data['dest']
address = invoice_form.cleaned_data['chambre']
total_price = sum(a['total_price'] for a in articles_info)
return render_invoice(request, {
'DATE': timezone.now(),
'recipient_name': recipient,
'address': address,
'article': articles_info,
'total': total_price,
'paid': paid,
'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'),
'line2': AssoOption.get_cached_value('adresse2'),
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
})
return form({ return form({
'factureform': invoice_form, 'factureform': invoice_form,
'action_name': _("Create"), 'action_name': _("Create"),
'articlesformset': articles_formset, 'articlesformset': articles_formset,
'articles': articles 'articlelist': articles
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -289,7 +276,7 @@ def facture_pdf(request, facture, **_kwargs):
def edit_facture(request, facture, **_kwargs): def edit_facture(request, facture, **_kwargs):
""" """
View used to edit an existing invoice. View used to edit an existing invoice.
Articles can be added or remove to the invoice and quantity Articles can be added or removed to the invoice and quantity
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.
""" """
@ -315,7 +302,7 @@ def edit_facture(request, facture, **_kwargs):
purchase_form.save() purchase_form.save()
messages.success( messages.success(
request, request,
_("The invoice has been successfully edited.") _("The invoice was edited.")
) )
return redirect(reverse('cotisations:index')) return redirect(reverse('cotisations:index'))
return form({ return form({
@ -335,7 +322,7 @@ def del_facture(request, facture, **_kwargs):
facture.delete() facture.delete()
messages.success( messages.success(
request, request,
_("The invoice has been successfully deleted.") _("The invoice was deleted.")
) )
return redirect(reverse('cotisations:index')) return redirect(reverse('cotisations:index'))
return form({ return form({
@ -344,6 +331,100 @@ def del_facture(request, facture, **_kwargs):
}, 'cotisations/delete.html', request) }, 'cotisations/delete.html', request)
@login_required
@can_edit(CustomInvoice)
def edit_custom_invoice(request, invoice, **kwargs):
# Building the invocie form and the article formset
invoice_form = CustomInvoiceForm(
request.POST or None,
instance=invoice
)
purchases_objects = Vente.objects.filter(facture=invoice)
purchase_form_set = modelformset_factory(
Vente,
fields=('name', 'number'),
extra=0,
max_num=len(purchases_objects)
)
purchase_form = purchase_form_set(
request.POST or None,
queryset=purchases_objects
)
if invoice_form.is_valid() and purchase_form.is_valid():
if invoice_form.changed_data:
invoice_form.save()
purchase_form.save()
messages.success(
request,
_("The invoice was edited.")
)
return redirect(reverse('cotisations:index-custom-invoice'))
return form({
'factureform': invoice_form,
'venteform': purchase_form
}, 'cotisations/edit_facture.html', request)
@login_required
@can_view(CustomInvoice)
def custom_invoice_pdf(request, invoice, **_kwargs):
"""
View used to generate a PDF file from an existing invoice in database
Creates a line for each Purchase (thus article sold) and generate the
invoice with the total price, the payment method, the address and the
legal information for the user.
"""
# TODO : change vente to purchase
purchases_objects = Vente.objects.all().filter(facture=invoice)
# Get the article list and build an list out of it
# contiaining (article_name, article_price, quantity, total_price)
purchases_info = []
for purchase in purchases_objects:
purchases_info.append({
'name': purchase.name,
'price': purchase.prix,
'quantity': purchase.number,
'total_price': purchase.prix_total
})
return render_invoice(request, {
'paid': invoice.paid,
'fid': invoice.id,
'DATE': invoice.date,
'recipient_name': invoice.recipient,
'address': invoice.address,
'article': purchases_info,
'total': invoice.prix_total(),
'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'),
'line2': AssoOption.get_cached_value('adresse2'),
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
})
# TODO : change facture to invoice
@login_required
@can_delete(CustomInvoice)
def del_custom_invoice(request, invoice, **_kwargs):
"""
View used to delete an existing invocie.
"""
if request.method == "POST":
invoice.delete()
messages.success(
request,
_("The invoice was deleted.")
)
return redirect(reverse('cotisations:index-custom-invoice'))
return form({
'objet': invoice,
'objet_name': _("Invoice")
}, 'cotisations/delete.html', request)
@login_required @login_required
@can_create(Article) @can_create(Article)
def add_article(request): def add_article(request):
@ -361,7 +442,7 @@ def add_article(request):
article.save() article.save()
messages.success( messages.success(
request, request,
_("The article has been successfully created.") _("The article was created.")
) )
return redirect(reverse('cotisations:index-article')) return redirect(reverse('cotisations:index-article'))
return form({ return form({
@ -383,7 +464,7 @@ def edit_article(request, article_instance, **_kwargs):
article.save() article.save()
messages.success( messages.success(
request, request,
_("The article has been successfully edited.") _("The article was edited.")
) )
return redirect(reverse('cotisations:index-article')) return redirect(reverse('cotisations:index-article'))
return form({ return form({
@ -405,7 +486,7 @@ def del_article(request, instances):
article_del.delete() article_del.delete()
messages.success( messages.success(
request, request,
_("The article(s) have been successfully deleted.") _("The articles were deleted.")
) )
return redirect(reverse('cotisations:index-article')) return redirect(reverse('cotisations:index-article'))
return form({ return form({
@ -433,7 +514,7 @@ def add_paiement(request):
payment_method.save(payment) payment_method.save(payment)
messages.success( messages.success(
request, request,
_("The payment method has been successfully created.") _("The payment method was created.")
) )
return redirect(reverse('cotisations:index-paiement')) return redirect(reverse('cotisations:index-paiement'))
return form({ return form({
@ -469,8 +550,7 @@ def edit_paiement(request, paiement_instance, **_kwargs):
if payment_method is not None: if payment_method is not None:
payment_method.save() payment_method.save()
messages.success( messages.success(
request, request,_("The payment method was edited.")
_("The payement method has been successfully edited.")
) )
return redirect(reverse('cotisations:index-paiement')) return redirect(reverse('cotisations:index-paiement'))
return form({ return form({
@ -496,8 +576,7 @@ def del_paiement(request, instances):
payment_del.delete() payment_del.delete()
messages.success( messages.success(
request, request,
_("The payment method %(method_name)s has been \ _("The payment method %(method_name)s was deleted.") % {
successfully deleted.") % {
'method_name': payment_del 'method_name': payment_del
} }
) )
@ -529,7 +608,7 @@ def add_banque(request):
bank.save() bank.save()
messages.success( messages.success(
request, request,
_("The bank has been successfully created.") _("The bank was created.")
) )
return redirect(reverse('cotisations:index-banque')) return redirect(reverse('cotisations:index-banque'))
return form({ return form({
@ -552,7 +631,7 @@ def edit_banque(request, banque_instance, **_kwargs):
bank.save() bank.save()
messages.success( messages.success(
request, request,
_("The bank has been successfully edited") _("The bank was edited.")
) )
return redirect(reverse('cotisations:index-banque')) return redirect(reverse('cotisations:index-banque'))
return form({ return form({
@ -577,8 +656,7 @@ def del_banque(request, instances):
bank_del.delete() bank_del.delete()
messages.success( messages.success(
request, request,
_("The bank %(bank_name)s has been successfully \ _("The bank %(bank_name)s was deleted.") % {
deleted.") % {
'bank_name': bank_del 'bank_name': bank_del
} }
) )
@ -678,8 +756,31 @@ def index_banque(request):
}) })
@login_required
@can_view_all(CustomInvoice)
def index_custom_invoice(request):
"""View used to display every custom invoice."""
pagination_number = GeneralOption.get_cached_value('pagination_number')
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set')
custom_invoice_list = SortTable.sort(
custom_invoice_list,
request.GET.get('col'),
request.GET.get('order'),
SortTable.COTISATIONS_CUSTOM
)
custom_invoice_list = re2o_paginator(
request,
custom_invoice_list,
pagination_number,
)
return render(request, 'cotisations/index_custom_invoice.html', {
'custom_invoice_list': custom_invoice_list
})
@login_required @login_required
@can_view_all(Facture) @can_view_all(Facture)
@can_view_all(CustomInvoice)
def index(request): def index(request):
""" """
View used to display the list of all exisitng invoices. View used to display the list of all exisitng invoices.
@ -695,7 +796,7 @@ def index(request):
) )
invoice_list = re2o_paginator(request, invoice_list, pagination_number) invoice_list = re2o_paginator(request, invoice_list, pagination_number)
return render(request, 'cotisations/index.html', { return render(request, 'cotisations/index.html', {
'facture_list': invoice_list 'facture_list': invoice_list,
}) })
@ -726,7 +827,7 @@ def credit_solde(request, user, **_kwargs):
kwargs={'userid': user.id} kwargs={'userid': user.id}
)) ))
refill_form = RechargeForm(request.POST or None, user=request.user) refill_form = RechargeForm(request.POST or None, user=user, user_source=request.user)
if refill_form.is_valid(): if refill_form.is_valid():
price = refill_form.cleaned_data['value'] price = refill_form.cleaned_data['value']
invoice = Facture(user=user) invoice = Facture(user=user)
@ -746,12 +847,16 @@ def credit_solde(request, user, **_kwargs):
prix=refill_form.cleaned_data['value'], prix=refill_form.cleaned_data['value'],
number=1 number=1
) )
send_mail_invoice(invoice)
return invoice.paiement.end_payment(invoice, request) return invoice.paiement.end_payment(invoice, request)
p = get_object_or_404(Paiement, is_balance=True) p = get_object_or_404(Paiement, is_balance=True)
return form({ return form({
'factureform': refill_form, 'factureform': refill_form,
'balance': request.user.solde, 'balance': user.solde,
'title': _("Refill your balance"), 'title': _("Refill your balance"),
'action_name': _("Pay"), 'action_name': _("Pay"),
'max_balance': p.payment_method.maximum_balance, 'max_balance': p.payment_method.maximum_balance,
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)

View file

@ -63,6 +63,7 @@ from preferences.models import OptionalTopologie
options, created = OptionalTopologie.objects.get_or_create() options, created = OptionalTopologie.objects.get_or_create()
VLAN_NOK = options.vlan_decision_nok.vlan_id VLAN_NOK = options.vlan_decision_nok.vlan_id
VLAN_OK = options.vlan_decision_ok.vlan_id VLAN_OK = options.vlan_decision_ok.vlan_id
RADIUS_POLICY = options.radius_general_policy
#: Serveur radius de test (pas la prod) #: Serveur radius de test (pas la prod)
TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False)) TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False))
@ -347,7 +348,7 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
if not nas_machine: if not nas_machine:
return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK) return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK)
sw_name = str(nas_machine) sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine)))
port = (Port.objects port = (Port.objects
.filter( .filter(
@ -355,27 +356,47 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
port=port_number port=port_number
) )
.first()) .first())
# Si le port est inconnu, on place sur le vlan defaut # Si le port est inconnu, on place sur le vlan defaut
# Aucune information particulière ne permet de déterminer quelle
# politique à appliquer sur ce port
if not port: if not port:
return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK) return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK)
# Si un vlan a été précisé, on l'utilise pour VLAN_OK # On récupère le profil du port
if port.vlan_force: port_profile = port.get_port_profile
DECISION_VLAN = int(port.vlan_force.vlan_id)
# Si un vlan a été précisé dans la config du port,
# on l'utilise pour VLAN_OK
if port_profile.vlan_untagged:
DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id)
extra_log = u"Force sur vlan " + str(DECISION_VLAN) extra_log = u"Force sur vlan " + str(DECISION_VLAN)
else: else:
DECISION_VLAN = VLAN_OK DECISION_VLAN = VLAN_OK
if port.radius == 'NO': # Si le port est désactivé, on rejette sur le vlan de déconnexion
if not port.state:
return (sw_name, port.room, u'Port desactivé', VLAN_NOK)
# Si radius est désactivé, on laisse passer
if port_profile.radius_type == 'NO':
return (sw_name, return (sw_name,
"", "",
u"Pas d'authentification sur ce port" + extra_log, u"Pas d'authentification sur ce port" + extra_log,
DECISION_VLAN) DECISION_VLAN)
if port.radius == 'BLOQ': # Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment
return (sw_name, port.room, u'Port desactive', VLAN_NOK) # Par conséquent, on laisse passer sur le bon vlan
if nas_type.port_access_mode == '802.1X' and port_profile.radius_type == '802.1X':
room = port.room or "Chambre/local inconnu"
return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN)
if port.radius == 'STRICT': # Sinon, cela veut dire qu'on fait de l'auth radius par mac
# Si le port est en mode strict, on vérifie que tous les users
# rattachés à ce port sont bien à jour de cotisation. Sinon on rejette (anti squattage)
# Il n'est pas possible de se connecter sur une prise strict sans adhérent à jour de cotis
# dedans
if port_profile.radius_mode == 'STRICT':
room = port.room room = port.room
if not room: if not room:
return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK) return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK)
@ -390,7 +411,8 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
return (sw_name, room, u'Chambre resident desactive', VLAN_NOK) return (sw_name, room, u'Chambre resident desactive', VLAN_NOK)
# else: user OK, on passe à la verif MAC # else: user OK, on passe à la verif MAC
if port.radius == 'COMMON' or port.radius == 'STRICT': # Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd
if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT':
# Authentification par mac # Authentification par mac
interface = (Interface.objects interface = (Interface.objects
.filter(mac_address=mac_address) .filter(mac_address=mac_address)
@ -399,15 +421,19 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
.first()) .first())
if not interface: if not interface:
room = port.room room = port.room
# On essaye de register la mac # On essaye de register la mac, si l'autocapture a été activée
# Sinon on rejette sur vlan_nok
if not nas_type.autocapture_mac: if not nas_type.autocapture_mac:
return (sw_name, "", u'Machine inconnue', VLAN_NOK) return (sw_name, "", u'Machine inconnue', VLAN_NOK)
# On ne peut autocapturer que si on connait la chambre et donc l'user correspondant
elif not room: elif not room:
return (sw_name, return (sw_name,
"Inconnue", "Inconnue",
u'Chambre et machine inconnues', u'Chambre et machine inconnues',
VLAN_NOK) VLAN_NOK)
else: else:
# Si la chambre est vide (local club, prises en libre services)
# Impossible d'autocapturer
if not room_user: if not room_user:
room_user = User.objects.filter( room_user = User.objects.filter(
Q(club__room=port.room) | Q(adherent__room=port.room) Q(club__room=port.room) | Q(adherent__room=port.room)
@ -418,6 +444,8 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
u'Machine et propriétaire de la chambre ' u'Machine et propriétaire de la chambre '
'inconnus', 'inconnus',
VLAN_NOK) VLAN_NOK)
# Si il y a plus d'un user dans la chambre, impossible de savoir à qui
# Ajouter la machine
elif room_user.count() > 1: elif room_user.count() > 1:
return (sw_name, return (sw_name,
room, room,
@ -425,19 +453,24 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
'dans la chambre/local -> ajout de mac ' 'dans la chambre/local -> ajout de mac '
'automatique impossible', 'automatique impossible',
VLAN_NOK) VLAN_NOK)
# Si l'adhérent de la chambre n'est pas à jour de cotis, pas d'autocapture
elif not room_user.first().has_access(): elif not room_user.first().has_access():
return (sw_name, return (sw_name,
room, room,
u'Machine inconnue et adhérent non cotisant', u'Machine inconnue et adhérent non cotisant',
VLAN_NOK) VLAN_NOK)
# Sinon on capture et on laisse passer sur le bon vlan
else: else:
result, reason = (room_user interface, reason = (room_user
.first() .first()
.autoregister_machine( .autoregister_machine(
mac_address, mac_address,
nas_type nas_type
)) ))
if result: if interface:
## Si on choisi de placer les machines sur le vlan correspondant à leur type :
if RADIUS_POLICY == 'MACHINE':
DECISION_VLAN = interface.type.ip_type.vlan.vlan_id
return (sw_name, return (sw_name,
room, room,
u'Access Ok, Capture de la mac: ' + extra_log, u'Access Ok, Capture de la mac: ' + extra_log,
@ -449,6 +482,9 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
reason + str(mac_address) reason + str(mac_address)
), ),
VLAN_NOK) VLAN_NOK)
# L'interface a été trouvée, on vérifie qu'elle est active, sinon on reject
# Si elle n'a pas d'ipv4, on lui en met une
# Enfin on laisse passer sur le vlan pertinent
else: else:
room = port.room room = port.room
if not interface.is_active: if not interface.is_active:
@ -456,7 +492,10 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
room, room,
u'Machine non active / adherent non cotisant', u'Machine non active / adherent non cotisant',
VLAN_NOK) VLAN_NOK)
elif not interface.ipv4: ## Si on choisi de placer les machines sur le vlan correspondant à leur type :
if RADIUS_POLICY == 'MACHINE':
DECISION_VLAN = interface.type.ip_type.vlan.vlan_id
if not interface.ipv4:
interface.assign_ipv4() interface.assign_ipv4()
return (sw_name, return (sw_name,
room, room,

View file

@ -9,7 +9,7 @@
python re2o { python re2o {
module = auth module = auth
python_path = /etc/freeradius/3.0:/usr/lib/python2.7/:/usr/lib/python2.7/dist-packages/:/usr/local/lib/python2.7/site-packages/:/usr/local/lib/python2.7/dist-packages/ python_path = /etc/freeradius/3.0:/usr/lib/python2.7:/usr/lib/python2.7/dist-packages:/usr/local/lib/python2.7/site-packages:/usr/lib/python2.7/lib-dynload:/usr/local/lib/python2.7/dist-packages
mod_instantiate = ${.module} mod_instantiate = ${.module}
func_instantiate = instantiate func_instantiate = instantiate

View file

@ -1157,6 +1157,7 @@ olcDbIndex: dc eq
olcDbIndex: entryCSN eq olcDbIndex: entryCSN eq
olcDbIndex: entryUUID eq olcDbIndex: entryUUID eq
olcDbIndex: radiusCallingStationId eq olcDbIndex: radiusCallingStationId eq
olcSizeLimit: 50000
structuralObjectClass: olcHdbConfig structuralObjectClass: olcHdbConfig
entryUUID: fc8fa138-514b-1034-9c36-0faf5bc7ead5 entryUUID: fc8fa138-514b-1034-9c36-0faf5bc7ead5
creatorsName: cn=admin,cn=config creatorsName: cn=admin,cn=config

View file

@ -25,6 +25,7 @@
Here are defined some functions to check acl on the application. Here are defined some functions to check acl on the application.
""" """
from django.utils.translation import ugettext as _
def can_view(user): def can_view(user):
@ -38,4 +39,6 @@ def can_view(user):
viewing is granted and msg is a message (can be None). viewing is granted and msg is a message (can be None).
""" """
can = user.has_module_perms('admin') can = user.has_module_perms('admin')
return can, None if can else "Vous ne pouvez pas voir cette application." return can, None if can else _("You don't have the right to view this"
" application.")

Binary file not shown.

View file

@ -0,0 +1,338 @@
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2018 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-15 20:12+0200\n"
"PO-Revision-Date: 2018-06-23 16:01+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: acl.py:42
msgid "You don't have the right to view this application."
msgstr "Vous n'avez pas le droit de voir cette application."
#: templates/logs/aff_stats_logs.html:36
msgid "Edited object"
msgstr "Objet modifié"
#: templates/logs/aff_stats_logs.html:37
#: templates/logs/aff_stats_models.html:32
msgid "Object type"
msgstr "Type d'objet"
#: templates/logs/aff_stats_logs.html:38
msgid "Edited by"
msgstr "Modifié par"
#: templates/logs/aff_stats_logs.html:40
msgid "Date of editing"
msgstr "Date de modification"
#: templates/logs/aff_stats_logs.html:42
msgid "Comment"
msgstr "Commentaire"
#: templates/logs/aff_stats_logs.html:58 templates/logs/aff_summary.html:62
#: templates/logs/aff_summary.html:85 templates/logs/aff_summary.html:104
#: templates/logs/aff_summary.html:123 templates/logs/aff_summary.html:142
msgid "Cancel"
msgstr "Annuler"
#: templates/logs/aff_stats_models.html:29
#, python-format
msgid "Statistics of the set %(key)s"
msgstr "Statistiques de l'ensemble %(key)s"
#: templates/logs/aff_stats_models.html:33
msgid "Number of stored entries"
msgstr "Nombre d'entrées enregistrées"
#: templates/logs/aff_stats_users.html:31
#, python-format
msgid "Statistics per %(key_dict)s of %(key)s"
msgstr "Statistiques par %(key_dict)s de %(key)s"
#: templates/logs/aff_stats_users.html:34
#, python-format
msgid "Number of %(key)s per %(key_dict)s"
msgstr "Nombre de %(key)s par %(key_dict)s"
#: templates/logs/aff_stats_users.html:35
msgid "Rank"
msgstr "Rang"
#: templates/logs/aff_summary.html:37
msgid "Date"
msgstr "Date"
#: templates/logs/aff_summary.html:39
msgid "Editing"
msgstr "Modification"
#: templates/logs/aff_summary.html:48
#, python-format
msgid "%(username)s has banned"
msgstr "%(username)s a banni"
#: templates/logs/aff_summary.html:52 templates/logs/aff_summary.html:75
msgid "No reason"
msgstr "Aucun motif"
#: templates/logs/aff_summary.html:71
#, python-format
msgid "%(username)s has graciously authorised"
msgstr "%(username)s a autorisé gracieusement"
#: templates/logs/aff_summary.html:94
#, python-format
msgid "%(username)s has updated"
msgstr "%(username)s a mis à jour"
#: templates/logs/aff_summary.html:113
#, python-format
msgid "%(username)s has sold %(number)sx %(name)s to"
msgstr "%(username)s a vendu %(number)sx %(name)s à"
#: templates/logs/aff_summary.html:116
#, python-format
msgid "+%(duration)s months"
msgstr "+%(duration)s mois"
#: templates/logs/aff_summary.html:132
#, python-format
msgid "%(username)s has edited an interface of"
msgstr "%(username)s a modifié une interface de"
#: templates/logs/delete.html:29
msgid "Deletion of actions"
msgstr "Suppression d'actions"
#: templates/logs/delete.html:35
#, python-format
msgid ""
"Warning: are you sure you want to delete this action %(objet_name)s "
"( %(objet)s )?"
msgstr ""
"Attention: voulez-vous vraiment supprimer cette action %(objet_name)s "
"( %(objet)s ) ?"
#: templates/logs/delete.html:36
msgid "Confirm"
msgstr "Confirmer"
#: templates/logs/index.html:29 templates/logs/stats_general.html:29
#: templates/logs/stats_logs.html:29 templates/logs/stats_models.html:29
#: templates/logs/stats_users.html:29
msgid "Statistics"
msgstr "Statistiques"
#: templates/logs/index.html:32 templates/logs/stats_logs.html:32 views.py:403
msgid "Actions performed"
msgstr "Actions effectuées"
#: templates/logs/sidebar.html:33
msgid "Summary"
msgstr "Résumé"
#: templates/logs/sidebar.html:37
msgid "Events"
msgstr "Évènements"
#: templates/logs/sidebar.html:41
msgid "General"
msgstr "Général"
#: templates/logs/sidebar.html:45
msgid "Database"
msgstr "Base de données"
#: templates/logs/sidebar.html:49
msgid "Wiring actions"
msgstr "Actions de câblage"
#: templates/logs/sidebar.html:53 views.py:325
msgid "Users"
msgstr "Utilisateurs"
#: templates/logs/stats_general.html:32
msgid "General statistics"
msgstr "Statistiques générales"
#: templates/logs/stats_models.html:32
msgid "Database statistics"
msgstr "Statistiques sur la base de données"
#: templates/logs/stats_users.html:32
msgid "Statistics about users"
msgstr "Statistiques sur les utilisateurs"
#: views.py:191
msgid "Nonexistent revision."
msgstr "Révision inexistante."
#: views.py:194
msgid "The action was deleted."
msgstr "L'action a été supprimée."
#: views.py:227
msgid "Category"
msgstr "Catégorie"
#: views.py:228
msgid "Number of users (members and clubs)"
msgstr "Nombre d'utilisateurs (adhérents et clubs)"
#: views.py:229
msgid "Number of members"
msgstr "Nombre d'adhérents"
#: views.py:230
msgid "Number of clubs"
msgstr "Nombre de clubs"
#: views.py:234
msgid "Activated users"
msgstr "Utilisateurs activés"
#: views.py:242
msgid "Disabled users"
msgstr "Utilisateurs désactivés"
#: views.py:250
msgid "Archived users"
msgstr "Utilisateurs archivés"
#: views.py:258
msgid "Contributing members"
msgstr "Adhérents cotisants"
#: views.py:264
msgid "Users benefiting from a connection"
msgstr "Utilisateurs bénéficiant d'une connexion"
#: views.py:270
msgid "Banned users"
msgstr "Utilisateurs bannis"
#: views.py:276
msgid "Users benefiting from a free connection"
msgstr "Utilisateurs bénéficiant d'une connexion gratuite"
#: views.py:282
msgid "Active interfaces (with access to the network)"
msgstr "Interfaces actives (ayant accès au réseau)"
#: views.py:292
msgid "Active interfaces assigned IPv4"
msgstr "Interfaces actives assignées IPv4"
#: views.py:305
msgid "IP range"
msgstr "Plage d'IP"
#: views.py:306
msgid "VLAN"
msgstr "VLAN"
#: views.py:307
msgid "Total number of IP addresses"
msgstr "Nombre total d'adresses IP"
#: views.py:308
msgid "Number of assigned IP addresses"
msgstr "Nombre d'adresses IP non assignées"
#: views.py:309
msgid "Number of IP address assigned to an activated machine"
msgstr "Nombre d'adresses IP assignées à une machine activée"
#: views.py:310
msgid "Number of nonassigned IP addresses"
msgstr "Nombre d'adresses IP non assignées"
#: views.py:337
msgid "Subscriptions"
msgstr "Cotisations"
#: views.py:359 views.py:420
msgid "Machines"
msgstr "Machines"
#: views.py:386
msgid "Topology"
msgstr "Topologie"
#: views.py:405
msgid "Number of actions"
msgstr "Nombre d'actions"
#: views.py:419 views.py:437 views.py:442 views.py:447 views.py:462
msgid "User"
msgstr "Utilisateur"
#: views.py:423
msgid "Invoice"
msgstr "Facture"
#: views.py:426
msgid "Ban"
msgstr "Bannissement"
#: views.py:429
msgid "Whitelist"
msgstr "Accès gracieux"
#: views.py:432
msgid "Rights"
msgstr "Droits"
#: views.py:436
msgid "School"
msgstr "Établissement"
#: views.py:441
msgid "Payment method"
msgstr "Moyen de paiement"
#: views.py:446
msgid "Bank"
msgstr "Banque"
#: views.py:463
msgid "Action"
msgstr "Action"
#: views.py:494
msgid "No model found."
msgstr "Aucun modèle trouvé."
#: views.py:500
msgid "Nonexistent entry."
msgstr "Entrée inexistante."
#: views.py:507
msgid "You don't have the right to access this menu."
msgstr "Vous n'avez pas le droit d'accéder à ce menu."

View file

@ -22,7 +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 %}
{% for stats in stats_list %} {% for stats in stats_list %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -39,4 +39,5 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endfor %} {% endfor %}

View file

@ -28,15 +28,18 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load logs_extra %} {% load logs_extra %}
{% load acl %} {% load acl %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Objet modifié</th> <th>{% trans "Edited object" %}</th>
<th>Type de l'objet</th> <th>{% trans "Object type" %}</th>
<th>{% include "buttons/sort.html" with prefix='logs' col='author' text='Modification par' %}</th> {% trans "Edited by" as tr_edited_by %}
<th>{% include "buttons/sort.html" with prefix='logs' col='date' text='Date de modification' %}</th> <th>{% include "buttons/sort.html" with prefix='logs' col='author' text=tr_edited_by %}</th>
<th>Commentaire</th> {% trans "Date of editing" as tr_date_of_editing %}
<th>{% include "buttons/sort.html" with prefix='logs' col='date' text=tr_date_of_editing %}</th>
<th>{% trans "Comment" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -52,15 +55,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td> <td>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'logs:revert-action' revision.id %}"> <a class="btn btn-danger btn-sm" role="button" href="{% url 'logs:revert-action' revision.id %}">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
Annuler {% trans "Cancel" %}
</a> </a>
</td> </td>
{% acl_end %} {% acl_end %}
</tr> </tr>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</table> </table>
{% if revisions_list.paginator %} {% if revisions_list.paginator %}
{% include "pagination.html" with list=revisions_list %} {% include "pagination.html" with list=revisions_list %}
{% endif %} {% endif %}

View file

@ -22,13 +22,15 @@ 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 %}
{% for key, stats in stats_list.items %} {% load i18n %}
{% for key, stats in stats_list.items %}
<table class="table table-striped"> <table class="table table-striped">
<h4>Statistiques de l'ensemble {{ key }}</h4> <h4>{% blocktrans %}Statistics of the set {{ key }}{% endblocktrans %}</h4>
<thead> <thead>
<tr> <tr>
<th>Type d'objet</th> <th>{% trans "Object type" %}</th>
<th>Nombre d'entrée stockées</th> <th>{% trans "Number of stored entries" %}</th>
</tr> </tr>
</thead> </thead>
{% for key, stat in stats.items %} {% for key, stat in stats.items %}
@ -38,4 +40,5 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endfor %} {% endfor %}

View file

@ -22,15 +22,17 @@ 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 %}
{% for key_dict, stats_dict in stats_list.items %} {% load i18n %}
{% for key_dict, stats_dict in stats_list.items %}
{% for key, stats in stats_dict.items %} {% for key, stats in stats_dict.items %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<h4>Statistiques par {{ key_dict }} de {{ key }}</h4> <h4>{% blocktrans %}Statistics per {{ key_dict }} of {{ key }}{% endblocktrans %}</h4>
<tr> <tr>
<th>{{ key_dict }}</th> <th>{{ key_dict }}</th>
<th>Nombre de {{ key }} par {{ key_dict }}</th> <th>{% blocktrans %}Number of {{ key }} per {{ key_dict }}{% endblocktrans %}</th>
<th>Rang</th> <th>{% trans "Rank" %}</th>
</tr> </tr>
</thead> </thead>
{% for stat in stats %} {% for stat in stats %}
@ -42,4 +44,5 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}

View file

@ -28,11 +28,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load logs_extra %} {% load logs_extra %}
{% load acl %} {% load acl %}
<table class="table table-striped">
{% load i18n %}
<table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>{% include "buttons/sort.html" with prefix='sum' col='date' text='Date' %}</th> {% trans "Date" as tr_date %}
<th>Modification</th> <th>{% include "buttons/sort.html" with prefix='sum' col='date' text=tr_date %}</th>
<th>{% trans "Editing" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -41,11 +45,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr class="danger"> <tr class="danger">
<td>{{ v.datetime }}</td> <td>{{ v.datetime }}</td>
<td> <td>
{{ v.username }} a banni {% blocktrans with username=v.username %}{{ username }} has banned{% endblocktrans %}
<a href="{% url 'users:profil' v.version.object.user_id %}">{{ v.version.object.user.get_username }}</a> <a href="{% url 'users:profil' v.version.object.user_id %}">{{ v.version.object.user.get_username }}</a>
(<i> (<i>
{% if v.version.object.raison == '' %} {% if v.version.object.raison == '' %}
Aucune raison {% trans "No reason" %}
{% else %} {% else %}
{{ v.version.object.raison }} {{ v.version.object.raison }}
{% endif %} {% endif %}
@ -55,7 +59,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td> <td>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'logs:revert-action' v.rev_id %}"> <a class="btn btn-danger btn-sm" role="button" href="{% url 'logs:revert-action' v.rev_id %}">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
Annuler {% trans "Cancel" %}
</a> </a>
</td> </td>
{% acl_end %} {% acl_end %}
@ -64,11 +68,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr class="success"> <tr class="success">
<td>{{ v.datetime }}</td> <td>{{ v.datetime }}</td>
<td> <td>
{{ v.username }} a autorisé gracieusement {% blocktrans with username=v.username %}{{ username }} has graciously authorised{% endblocktrans %}
<a href="{% url 'users:profil' v.version.object.user_id %}">{{ v.version.object.user.get_username }}</a> <a href="{% url 'users:profil' v.version.object.user_id %}">{{ v.version.object.user.get_username }}</a>
(<i> (<i>
{% if v.version.object.raison == '' %} {% if v.version.object.raison == '' %}
Aucune raison {% trans "No reason" %}
{% else %} {% else %}
{{ v.version.object.raison }} {{ v.version.object.raison }}
{% endif %} {% endif %}
@ -78,7 +82,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td> <td>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'logs:revert-action' v.rev_id %}"> <a class="btn btn-danger btn-sm" role="button" href="{% url 'logs:revert-action' v.rev_id %}">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
Annuler {% trans "Cancel" %}
</a> </a>
</td> </td>
{% acl_end %} {% acl_end %}
@ -87,7 +91,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td>{{ v.datetime }}</td> <td>{{ v.datetime }}</td>
<td> <td>
{{ v.username }} a mis à jour {% blocktrans with username=v.username %}{{ username }} has updated{% endblocktrans %}
<a href="{% url 'users:profil' v.version.object.id %}">{{ v.version.object.get_username }}</a> <a href="{% url 'users:profil' v.version.object.id %}">{{ v.version.object.get_username }}</a>
{% if v.comment != '' %} {% if v.comment != '' %}
(<i>{{ v.comment }}</i>) (<i>{{ v.comment }}</i>)
@ -97,7 +101,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td> <td>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'logs:revert-action' v.rev_id %}"> <a class="btn btn-danger btn-sm" role="button" href="{% url 'logs:revert-action' v.rev_id %}">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
Annuler {% trans "Cancel" %}
</a> </a>
</td> </td>
{% acl_end %} {% acl_end %}
@ -106,17 +110,22 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td>{{ v.datetime }}</td> <td>{{ v.datetime }}</td>
<td> <td>
{{ v.username }} a vendu {{ v.version.object.number }}x {{ v.version.object.name }} à {% blocktrans with username=v.username number=v.version.object.number name=v.version.object.name %}{{ username }} has sold {{ number }}x {{ name }}{% endblocktrans %}
<a href="{% url 'users:profil' v.version.object.facture.user_id %}">{{ v.version.object.facture.user.get_username }}</a> {% with invoice=v.version.object.facture %}
{% if invoice|is_facture %}
{% trans " to" %}
<a href="{% url 'users:profil' v.version.object.facture.facture.user_id %}">{{ v.version.object.facture.facture.user.get_username }}</a>
{% if v.version.object.iscotisation %} {% if v.version.object.iscotisation %}
(<i>+{{ v.version.object.duration }} mois</i>) (<i>{% blocktrans with duration=v.version.object.duration %}+{{ duration }} months{% endblocktrans %}</i>)
{% endif %} {% endif %}
{% endif %}
{% endwith %}
</td> </td>
{% can_edit_history %} {% can_edit_history %}
<td> <td>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'logs:revert-action' v.rev_id %}"> <a class="btn btn-danger btn-sm" role="button" href="{% url 'logs:revert-action' v.rev_id %}">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
Annuler {% trans "Cancel" %}
</a> </a>
</td> </td>
{% acl_end %} {% acl_end %}
@ -125,7 +134,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td>{{ v.datetime }}</td> <td>{{ v.datetime }}</td>
<td> <td>
{{ v.username }} a modifié une interface de {% blocktrans with username=v.username %}{{ username }} has edited an interface of{% endblocktrans %}
<a href="{% url 'users:profil' v.version.object.machine.user_id %}">{{ v.version.object.machine.user.get_username }}</a> <a href="{% url 'users:profil' v.version.object.machine.user_id %}">{{ v.version.object.machine.user.get_username }}</a>
{% if v.comment != '' %} {% if v.comment != '' %}
(<i>{{ v.comment }}</i>) (<i>{{ v.comment }}</i>)
@ -135,15 +144,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td> <td>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'logs:revert-action' v.rev_id %}"> <a class="btn btn-danger btn-sm" role="button" href="{% url 'logs:revert-action' v.rev_id %}">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
Annuler {% trans "Cancel" %}
</a> </a>
</td> </td>
{% acl_end %} {% acl_end %}
</tr> </tr>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</table> </table>
{% if versions_list.paginator %} {% if versions_list.paginator %}
{% include "pagination.html" with list=versions_list %} {% include "pagination.html" with list=versions_list %}
{% endif %} {% endif %}

View file

@ -24,17 +24,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %}
{% block title %}Supression d'action{% endblock %} {% block title %}{% trans "Deletion of actions" %}{% endblock %}
{% block content %} {% block content %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
<h4>Attention, voulez-vous vraiment annuler cette action {{ objet_name }} ( {{ objet }} ) ?</h4> <h4>{% blocktrans %}Warning: are you sure you want to delete this action {{ objet_name }} ( {{ objet }} )?{% endblocktrans %}</h4>
{% bootstrap_button "Confirmer" button_type="submit" icon="trash" %} {% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type="submit" icon="trash" %}
</form> </form>
<br /> <br />
<br /> <br />
<br /> <br />
{% endblock %} {% endblock %}

View file

@ -24,13 +24,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %}
{% block title %}Statistiques{% endblock %} {% block title %}{%trans "Statistics" %}{% endblock %}
{% block content %} {% block content %}
<h2>Actions effectuées</h2> <h2>{% trans "Actions performed" %}</h2>
{% include "logs/aff_summary.html" with versions_list=versions_list %} {% include "logs/aff_summary.html" with versions_list=versions_list %}
<br /> <br />
<br /> <br />
<br /> <br />
{% endblock %} {% endblock %}

View file

@ -24,32 +24,34 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load i18n %}
{% block sidebar %} {% block sidebar %}
{% can_view_app logs %} {% can_view_app logs %}
<a class="list-group-item list-group-item-info" href="{% url "logs:index" %}"> <a class="list-group-item list-group-item-info" href="{% url "logs:index" %}">
<i class="fa fa-clipboard-list"></i> <i class="fa fa-clipboard-list"></i>
Résumé {% trans "Summary" %}
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "logs:stats-logs" %}"> <a class="list-group-item list-group-item-info" href="{% url "logs:stats-logs" %}">
<i class="fa fa-calendar-alt"></i> <i class="fa fa-calendar-alt"></i>
Évènements {% trans "Events" %}
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "logs:stats-general" %}"> <a class="list-group-item list-group-item-info" href="{% url "logs:stats-general" %}">
<i class="fa fa-chart-area"></i> <i class="fa fa-chart-area"></i>
Général {% trans "General" %}
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "logs:stats-models" %}"> <a class="list-group-item list-group-item-info" href="{% url "logs:stats-models" %}">
<i class="fa fa-database"></i> <i class="fa fa-database"></i>
Base de données {% trans "Database" %}
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "logs:stats-actions" %}"> <a class="list-group-item list-group-item-info" href="{% url "logs:stats-actions" %}">
<i class="fa fa-plug"></i> <i class="fa fa-plug"></i>
Actions de cablage {% trans "Wiring actions" %}
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "logs:stats-users" %}"> <a class="list-group-item list-group-item-info" href="{% url "logs:stats-users" %}">
<i class="fa fa-users"></i> <i class="fa fa-users"></i>
Utilisateurs {% trans "Users" %}
</a> </a>
{% acl_end %} {% acl_end %}
{% endblock %} {% endblock %}

View file

@ -24,13 +24,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %}
{% block title %}Statistiques générales{% endblock %} {% block title %}{% trans "Statistics" %}{% endblock %}
{% block content %} {% block content %}
<h2>Statistiques générales</h2> <h2>{% trans "General statistics" %}</h2>
{% include "logs/aff_stats_general.html" with stats_list=stats_list %} {% include "logs/aff_stats_general.html" with stats_list=stats_list %}
<br /> <br />
<br /> <br />
<br /> <br />
{% endblock %} {% endblock %}

View file

@ -24,13 +24,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %}
{% block title %}Statistiques{% endblock %} {% block title %}{% trans "Statistics" %}{% endblock %}
{% block content %} {% block content %}
<h2>Actions effectuées</h2> <h2>{% trans "Actions performed" %}</h2>
{% include "logs/aff_stats_logs.html" with revisions_list=revisions_list %} {% include "logs/aff_stats_logs.html" with revisions_list=revisions_list %}
<br /> <br />
<br /> <br />
<br /> <br />
{% endblock %} {% endblock %}

View file

@ -24,13 +24,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %}
{% block title %}Statistiques des objets base de données{% endblock %} {% block title %}{% trans "Statistics" %}{% endblock %}
{% block content %} {% block content %}
<h2>Statistiques bdd</h2> <h2>{% trans "Database statistics" %}</h2>
{% include "logs/aff_stats_models.html" with stats_list=stats_list %} {% include "logs/aff_stats_models.html" with stats_list=stats_list %}
<br /> <br />
<br /> <br />
<br /> <br />
{% endblock %} {% endblock %}

View file

@ -24,13 +24,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %}
{% block title %}Statistiques par utilisateur{% endblock %} {% block title %}{% trans "Statistics" %}{% endblock %}
{% block content %} {% block content %}
<h2>Statistiques par utilisateur</h2> <h2>{% trans "Statistics about users" %}</h2>
{% include "logs/aff_stats_users.html" with stats_list=stats_list %} {% include "logs/aff_stats_users.html" with stats_list=stats_list %}
<br /> <br />
<br /> <br />
<br /> <br />
{% endblock %} {% endblock %}

View file

@ -19,7 +19,7 @@
# #
# You should have received a copy of the GNU General Public License along # 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 021}10-1301 USA.
"""logs.templatetags.logs_extra """logs.templatetags.logs_extra
A templatetag to get the class name for a given object A templatetag to get the class name for a given object
""" """
@ -34,6 +34,10 @@ def classname(obj):
""" Returns the object class name """ """ Returns the object class name """
return obj.__class__.__name__ return obj.__class__.__name__
@register.filter
def is_facture(baseinvoice):
"""Returns True if a baseinvoice has a `Facture` child."""
return hasattr(baseinvoice, 'facture')
@register.inclusion_tag('buttons/history.html') @register.inclusion_tag('buttons/history.html')
def history_button(instance, text=False, html_class=True): def history_button(instance, text=False, html_class=True):

View file

@ -188,10 +188,10 @@ def revert_action(request, revision_id):
try: try:
revision = Revision.objects.get(id=revision_id) revision = Revision.objects.get(id=revision_id)
except Revision.DoesNotExist: except Revision.DoesNotExist:
messages.error(request, u"Revision inexistante") messages.error(request, _("Nonexistent revision."))
if request.method == "POST": if request.method == "POST":
revision.revert() revision.revert()
messages.success(request, "L'action a été supprimée") messages.success(request, _("The action was deleted."))
return redirect(reverse('logs:index')) return redirect(reverse('logs:index'))
return form({ return form({
'objet': revision, 'objet': revision,
@ -224,14 +224,14 @@ def stats_general(request):
stats = [ stats = [
[ # First set of data (about users) [ # First set of data (about users)
[ # Headers [ # Headers
"Categorie", _("Category"),
"Nombre d'utilisateurs (total club et adhérents)", _("Number of users (members and clubs)"),
"Nombre d'adhérents", _("Number of members"),
"Nombre de clubs" _("Number of clubs")
], ],
{ # Data { # Data
'active_users': [ 'active_users': [
"Users actifs", _("Activated users"),
User.objects.filter(state=User.STATE_ACTIVE).count(), User.objects.filter(state=User.STATE_ACTIVE).count(),
(Adherent.objects (Adherent.objects
.filter(state=Adherent.STATE_ACTIVE) .filter(state=Adherent.STATE_ACTIVE)
@ -239,7 +239,7 @@ def stats_general(request):
Club.objects.filter(state=Club.STATE_ACTIVE).count() Club.objects.filter(state=Club.STATE_ACTIVE).count()
], ],
'inactive_users': [ 'inactive_users': [
"Users désactivés", _("Disabled users"),
User.objects.filter(state=User.STATE_DISABLED).count(), User.objects.filter(state=User.STATE_DISABLED).count(),
(Adherent.objects (Adherent.objects
.filter(state=Adherent.STATE_DISABLED) .filter(state=Adherent.STATE_DISABLED)
@ -247,7 +247,7 @@ def stats_general(request):
Club.objects.filter(state=Club.STATE_DISABLED).count() Club.objects.filter(state=Club.STATE_DISABLED).count()
], ],
'archive_users': [ 'archive_users': [
"Users archivés", _("Archived users"),
User.objects.filter(state=User.STATE_ARCHIVE).count(), User.objects.filter(state=User.STATE_ARCHIVE).count(),
(Adherent.objects (Adherent.objects
.filter(state=Adherent.STATE_ARCHIVE) .filter(state=Adherent.STATE_ARCHIVE)
@ -255,31 +255,31 @@ def stats_general(request):
Club.objects.filter(state=Club.STATE_ARCHIVE).count() Club.objects.filter(state=Club.STATE_ARCHIVE).count()
], ],
'adherent_users': [ 'adherent_users': [
"Cotisant à l'association", _("Contributing members"),
_all_adherent.count(), _all_adherent.count(),
_all_adherent.exclude(adherent__isnull=True).count(), _all_adherent.exclude(adherent__isnull=True).count(),
_all_adherent.exclude(club__isnull=True).count() _all_adherent.exclude(club__isnull=True).count()
], ],
'connexion_users': [ 'connexion_users': [
"Utilisateurs bénéficiant d'une connexion", _("Users benefiting from a connection"),
_all_has_access.count(), _all_has_access.count(),
_all_has_access.exclude(adherent__isnull=True).count(), _all_has_access.exclude(adherent__isnull=True).count(),
_all_has_access.exclude(club__isnull=True).count() _all_has_access.exclude(club__isnull=True).count()
], ],
'ban_users': [ 'ban_users': [
"Utilisateurs bannis", _("Banned users"),
_all_baned.count(), _all_baned.count(),
_all_baned.exclude(adherent__isnull=True).count(), _all_baned.exclude(adherent__isnull=True).count(),
_all_baned.exclude(club__isnull=True).count() _all_baned.exclude(club__isnull=True).count()
], ],
'whitelisted_user': [ 'whitelisted_user': [
"Utilisateurs bénéficiant d'une connexion gracieuse", _("Users benefiting from a free connection"),
_all_whitelisted.count(), _all_whitelisted.count(),
_all_whitelisted.exclude(adherent__isnull=True).count(), _all_whitelisted.exclude(adherent__isnull=True).count(),
_all_whitelisted.exclude(club__isnull=True).count() _all_whitelisted.exclude(club__isnull=True).count()
], ],
'actives_interfaces': [ 'actives_interfaces': [
"Interfaces actives (ayant accès au reseau)", _("Active interfaces (with access to the network)"),
_all_active_interfaces_count.count(), _all_active_interfaces_count.count(),
(_all_active_interfaces_count (_all_active_interfaces_count
.exclude(machine__user__adherent__isnull=True) .exclude(machine__user__adherent__isnull=True)
@ -289,7 +289,7 @@ def stats_general(request):
.count()) .count())
], ],
'actives_assigned_interfaces': [ 'actives_assigned_interfaces': [
"Interfaces actives et assignées ipv4", _("Active interfaces assigned IPv4"),
_all_active_assigned_interfaces_count.count(), _all_active_assigned_interfaces_count.count(),
(_all_active_assigned_interfaces_count (_all_active_assigned_interfaces_count
.exclude(machine__user__adherent__isnull=True) .exclude(machine__user__adherent__isnull=True)
@ -302,12 +302,12 @@ def stats_general(request):
], ],
[ # Second set of data (about ip adresses) [ # Second set of data (about ip adresses)
[ # Headers [ # Headers
"Range d'ip", _("IP range"),
"Vlan", _("VLAN"),
"Nombre d'ip totales", _("Total number of IP addresses"),
"Ip assignées", _("Number of assigned IP addresses"),
"Ip assignées à une machine active", _("Number of IP address assigned to an activated machine"),
"Ip non assignées" _("Number of nonassigned IP addresses")
], ],
ip_dict # Data already prepared ip_dict # Data already prepared
] ]
@ -322,79 +322,87 @@ def stats_models(request):
nombre d'users, d'écoles, de droits, de bannissements, nombre d'users, d'écoles, de droits, de bannissements,
de factures, de ventes, de banque, de machines, etc""" de factures, de ventes, de banque, de machines, etc"""
stats = { stats = {
'Users': { _("Users"): {
'users': [User.PRETTY_NAME, User.objects.count()], 'users': [User._meta.verbose_name, User.objects.count()],
'adherents': [Adherent.PRETTY_NAME, Adherent.objects.count()], 'adherents': [Adherent._meta.verbose_name, Adherent.objects.count()],
'clubs': [Club.PRETTY_NAME, Club.objects.count()], 'clubs': [Club._meta.verbose_name, Club.objects.count()],
'serviceuser': [ServiceUser.PRETTY_NAME, 'serviceuser': [ServiceUser._meta.verbose_name,
ServiceUser.objects.count()], ServiceUser.objects.count()],
'school': [School.PRETTY_NAME, School.objects.count()], 'school': [School._meta.verbose_name, School.objects.count()],
'listright': [ListRight.PRETTY_NAME, ListRight.objects.count()], 'listright': [ListRight._meta.verbose_name, ListRight.objects.count()],
'listshell': [ListShell.PRETTY_NAME, ListShell.objects.count()], 'listshell': [ListShell._meta.verbose_name, ListShell.objects.count()],
'ban': [Ban.PRETTY_NAME, Ban.objects.count()], 'ban': [Ban._meta.verbose_name, Ban.objects.count()],
'whitelist': [Whitelist.PRETTY_NAME, Whitelist.objects.count()] 'whitelist': [Whitelist._meta.verbose_name, Whitelist.objects.count()]
}, },
'Cotisations': { _("Subscriptions"): {
'factures': [ 'factures': [
Facture._meta.verbose_name.title(), Facture._meta.verbose_name,
Facture.objects.count() Facture.objects.count()
], ],
'vente': [ 'vente': [
Vente._meta.verbose_name.title(), Vente._meta.verbose_name,
Vente.objects.count() Vente.objects.count()
], ],
'cotisation': [ 'cotisation': [
Cotisation._meta.verbose_name.title(), Cotisation._meta.verbose_name,
Cotisation.objects.count() Cotisation.objects.count()
], ],
'article': [ 'article': [
Article._meta.verbose_name.title(), Article._meta.verbose_name,
Article.objects.count() Article.objects.count()
], ],
'banque': [ 'banque': [
Banque._meta.verbose_name.title(), Banque._meta.verbose_name,
Banque.objects.count() Banque.objects.count()
], ],
}, },
'Machines': { _("Machines"): {
'machine': [Machine.PRETTY_NAME, Machine.objects.count()], 'machine': [Machine._meta.verbose_name,
'typemachine': [MachineType.PRETTY_NAME, Machine.objects.count()],
'typemachine': [MachineType._meta.verbose_name,
MachineType.objects.count()], MachineType.objects.count()],
'typeip': [IpType.PRETTY_NAME, IpType.objects.count()], 'typeip': [IpType._meta.verbose_name,
'extension': [Extension.PRETTY_NAME, Extension.objects.count()], IpType.objects.count()],
'interface': [Interface.PRETTY_NAME, Interface.objects.count()], 'extension': [Extension._meta.verbose_name,
'alias': [Domain.PRETTY_NAME, Extension.objects.count()],
'interface': [Interface._meta.verbose_name,
Interface.objects.count()],
'alias': [Domain._meta.verbose_name,
Domain.objects.exclude(cname=None).count()], Domain.objects.exclude(cname=None).count()],
'iplist': [IpList.PRETTY_NAME, IpList.objects.count()], 'iplist': [IpList._meta.verbose_name,
'service': [Service.PRETTY_NAME, Service.objects.count()], IpList.objects.count()],
'service': [Service._meta.verbose_name,
Service.objects.count()],
'ouvertureportlist': [ 'ouvertureportlist': [
OuverturePortList.PRETTY_NAME, OuverturePortList._meta.verbose_name,
OuverturePortList.objects.count() OuverturePortList.objects.count()
], ],
'vlan': [Vlan.PRETTY_NAME, Vlan.objects.count()], 'vlan': [Vlan._meta.verbose_name, Vlan.objects.count()],
'SOA': [SOA.PRETTY_NAME, SOA.objects.count()], 'SOA': [SOA._meta.verbose_name, SOA.objects.count()],
'Mx': [Mx.PRETTY_NAME, Mx.objects.count()], 'Mx': [Mx._meta.verbose_name, Mx.objects.count()],
'Ns': [Ns.PRETTY_NAME, Ns.objects.count()], 'Ns': [Ns._meta.verbose_name, Ns.objects.count()],
'nas': [Nas.PRETTY_NAME, Nas.objects.count()], 'nas': [Nas._meta.verbose_name, Nas.objects.count()],
}, },
'Topologie': { _("Topology"): {
'switch': [Switch.PRETTY_NAME, Switch.objects.count()], 'switch': [Switch._meta.verbose_name,
'bornes': [AccessPoint.PRETTY_NAME, AccessPoint.objects.count()], Switch.objects.count()],
'port': [Port.PRETTY_NAME, Port.objects.count()], 'bornes': [AccessPoint._meta.verbose_name,
'chambre': [Room.PRETTY_NAME, Room.objects.count()], AccessPoint.objects.count()],
'stack': [Stack.PRETTY_NAME, Stack.objects.count()], 'port': [Port._meta.verbose_name, Port.objects.count()],
'chambre': [Room._meta.verbose_name, Room.objects.count()],
'stack': [Stack._meta.verbose_name, Stack.objects.count()],
'modelswitch': [ 'modelswitch': [
ModelSwitch.PRETTY_NAME, ModelSwitch._meta.verbose_name,
ModelSwitch.objects.count() ModelSwitch.objects.count()
], ],
'constructorswitch': [ 'constructorswitch': [
ConstructorSwitch.PRETTY_NAME, ConstructorSwitch._meta.verbose_name,
ConstructorSwitch.objects.count() ConstructorSwitch.objects.count()
], ],
}, },
'Actions effectuées sur la base': _("Actions performed"):
{ {
'revision': ["Nombre d'actions", Revision.objects.count()], 'revision': [_("Number of actions"), Revision.objects.count()],
}, },
} }
return render(request, 'logs/stats_models.html', {'stats_list': stats}) return render(request, 'logs/stats_models.html', {'stats_list': stats})
@ -408,35 +416,35 @@ def stats_users(request):
de moyens de paiements par user, de banque par user, de moyens de paiements par user, de banque par user,
de bannissement par user, etc""" de bannissement par user, etc"""
stats = { stats = {
'Utilisateur': { _("User"): {
'Machines': User.objects.annotate( _("Machines"): User.objects.annotate(
num=Count('machine') num=Count('machine')
).order_by('-num')[:10], ).order_by('-num')[:10],
'Facture': User.objects.annotate( _("Invoice"): User.objects.annotate(
num=Count('facture') num=Count('facture')
).order_by('-num')[:10], ).order_by('-num')[:10],
'Bannissement': User.objects.annotate( _("Ban"): User.objects.annotate(
num=Count('ban') num=Count('ban')
).order_by('-num')[:10], ).order_by('-num')[:10],
'Accès gracieux': User.objects.annotate( _("Whitelist"): User.objects.annotate(
num=Count('whitelist') num=Count('whitelist')
).order_by('-num')[:10], ).order_by('-num')[:10],
'Droits': User.objects.annotate( _("Rights"): User.objects.annotate(
num=Count('groups') num=Count('groups')
).order_by('-num')[:10], ).order_by('-num')[:10],
}, },
'Etablissement': { _("School"): {
'Utilisateur': School.objects.annotate( _("User"): School.objects.annotate(
num=Count('user') num=Count('user')
).order_by('-num')[:10], ).order_by('-num')[:10],
}, },
'Moyen de paiement': { _("Payment method"): {
'Utilisateur': Paiement.objects.annotate( _("User"): Paiement.objects.annotate(
num=Count('facture') num=Count('facture')
).order_by('-num')[:10], ).order_by('-num')[:10],
}, },
'Banque': { _("Bank"): {
'Utilisateur': Banque.objects.annotate( _("User"): Banque.objects.annotate(
num=Count('facture') num=Count('facture')
).order_by('-num')[:10], ).order_by('-num')[:10],
}, },
@ -451,8 +459,8 @@ def stats_actions(request):
utilisateurs. utilisateurs.
Affiche le nombre de modifications aggrégées par utilisateurs""" Affiche le nombre de modifications aggrégées par utilisateurs"""
stats = { stats = {
'Utilisateur': { _("User"): {
'Action': User.objects.annotate( _("Action"): User.objects.annotate(
num=Count('revision') num=Count('revision')
).order_by('-num')[:40], ).order_by('-num')[:40],
}, },
@ -489,14 +497,14 @@ def history(request, application, object_name, object_id):
try: try:
instance = model.get_instance(**kwargs) instance = model.get_instance(**kwargs)
except model.DoesNotExist: except model.DoesNotExist:
messages.error(request, _("No entry found.")) messages.error(request, _("Nonexistent entry."))
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': str(request.user.id)} kwargs={'userid': str(request.user.id)}
)) ))
can, msg = instance.can_view(request.user) can, msg = instance.can_view(request.user)
if not can: if not can:
messages.error(request, msg or _("You cannot acces to this menu")) messages.error(request, msg or _("You don't have the right to access this menu."))
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': str(request.user.id)} kwargs={'userid': str(request.user.id)}
@ -513,3 +521,4 @@ def history(request, application, object_name, object_id):
're2o/history.html', 're2o/history.html',
{'reversions': reversions, 'object': instance} {'reversions': reversions, 'object': instance}
) )

View file

@ -25,6 +25,7 @@
Here are defined some functions to check acl on the application. Here are defined some functions to check acl on the application.
""" """
from django.utils.translation import ugettext as _
def can_view(user): def can_view(user):
@ -38,4 +39,6 @@ def can_view(user):
viewing is granted and msg is a message (can be None). viewing is granted and msg is a message (can be None).
""" """
can = user.has_module_perms('machines') can = user.has_module_perms('machines')
return can, None if can else "Vous ne pouvez pas voir cette application." return can, None if can else _("You don't have the right to view this"
" application.")

View file

@ -42,6 +42,7 @@ from .models import (
SshFp, SshFp,
Nas, Nas,
Service, Service,
Role,
OuverturePort, OuverturePort,
Ipv6List, Ipv6List,
OuverturePortList, OuverturePortList,
@ -146,6 +147,11 @@ class ServiceAdmin(VersionAdmin):
""" Admin view of a ServiceAdmin object """ """ Admin view of a ServiceAdmin object """
list_display = ('service_type', 'min_time_regen', 'regular_time_regen') list_display = ('service_type', 'min_time_regen', 'regular_time_regen')
class RoleAdmin(VersionAdmin):
""" Admin view of a RoleAdmin object """
pass
admin.site.register(Machine, MachineAdmin) admin.site.register(Machine, MachineAdmin)
admin.site.register(MachineType, MachineTypeAdmin) admin.site.register(MachineType, MachineTypeAdmin)
@ -162,6 +168,7 @@ admin.site.register(IpList, IpListAdmin)
admin.site.register(Interface, InterfaceAdmin) admin.site.register(Interface, InterfaceAdmin)
admin.site.register(Domain, DomainAdmin) admin.site.register(Domain, DomainAdmin)
admin.site.register(Service, ServiceAdmin) admin.site.register(Service, ServiceAdmin)
admin.site.register(Role, RoleAdmin)
admin.site.register(Vlan, VlanAdmin) admin.site.register(Vlan, VlanAdmin)
admin.site.register(Ipv6List, Ipv6ListAdmin) admin.site.register(Ipv6List, Ipv6ListAdmin)
admin.site.register(Nas, NasAdmin) admin.site.register(Nas, NasAdmin)

View file

@ -37,6 +37,7 @@ from __future__ import unicode_literals
from django.forms import ModelForm, Form from django.forms import ModelForm, Form
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _
from re2o.field_permissions import FieldPermissionFormMixin from re2o.field_permissions import FieldPermissionFormMixin
from re2o.mixins import FormRevMixin from re2o.mixins import FormRevMixin
@ -53,6 +54,7 @@ from .models import (
Txt, Txt,
DName, DName,
Ns, Ns,
Role,
Service, Service,
Vlan, Vlan,
Srv, Srv,
@ -73,7 +75,7 @@ class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
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(EditMachineForm, self).__init__(*args, prefix=prefix, **kwargs) super(EditMachineForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['name'].label = 'Nom de la machine' self.fields['name'].label = _("Machine name")
class NewMachineForm(EditMachineForm): class NewMachineForm(EditMachineForm):
@ -92,12 +94,11 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
prefix = kwargs.pop('prefix', self.Meta.model.__name__) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
user = kwargs.get('user') user = kwargs.get('user')
super(EditInterfaceForm, self).__init__(*args, prefix=prefix, **kwargs) super(EditInterfaceForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['mac_address'].label = 'Adresse mac' self.fields['mac_address'].label = _("MAC address")
self.fields['type'].label = 'Type de machine' self.fields['type'].label = _("Machine type")
self.fields['type'].empty_label = "Séléctionner un type de machine" self.fields['type'].empty_label = _("Select a machine type")
if "ipv4" in self.fields: if "ipv4" in self.fields:
self.fields['ipv4'].empty_label = ("Assignation automatique de " self.fields['ipv4'].empty_label = _("Automatic IPv4 assignment")
"l'ipv4")
self.fields['ipv4'].queryset = IpList.objects.filter( self.fields['ipv4'].queryset = IpList.objects.filter(
interface__isnull=True interface__isnull=True
) )
@ -168,7 +169,7 @@ class DelAliasForm(FormRevMixin, Form):
"""Suppression d'un ou plusieurs objets alias""" """Suppression d'un ou plusieurs objets alias"""
alias = forms.ModelMultipleChoiceField( alias = forms.ModelMultipleChoiceField(
queryset=Domain.objects.all(), queryset=Domain.objects.all(),
label="Alias actuels", label=_("Current aliases"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -189,15 +190,15 @@ class MachineTypeForm(FormRevMixin, ModelForm):
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(MachineTypeForm, self).__init__(*args, prefix=prefix, **kwargs) super(MachineTypeForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['type'].label = 'Type de machine à ajouter' self.fields['type'].label = _("Machine type to add")
self.fields['ip_type'].label = "Type d'ip relié" self.fields['ip_type'].label = _("Related IP type")
class DelMachineTypeForm(FormRevMixin, Form): class DelMachineTypeForm(FormRevMixin, Form):
"""Suppression d'un ou plusieurs machinetype""" """Suppression d'un ou plusieurs machinetype"""
machinetypes = forms.ModelMultipleChoiceField( machinetypes = forms.ModelMultipleChoiceField(
queryset=MachineType.objects.none(), queryset=MachineType.objects.none(),
label="Types de machines actuelles", label=_("Current machine types"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -215,20 +216,21 @@ class IpTypeForm(FormRevMixin, ModelForm):
stop après creation""" stop après creation"""
class Meta: class Meta:
model = IpType model = IpType
fields = ['type', 'extension', 'need_infra', 'domaine_ip_start', fields = '__all__'
'domaine_ip_stop', 'prefix_v6', 'vlan', 'ouverture_ports']
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(IpTypeForm, self).__init__(*args, prefix=prefix, **kwargs) super(IpTypeForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['type'].label = 'Type ip à ajouter' self.fields['type'].label = _("IP type to add")
class EditIpTypeForm(IpTypeForm): class EditIpTypeForm(IpTypeForm):
"""Edition d'un iptype. Pas d'edition du rangev4 possible, car il faudrait """Edition d'un iptype. Pas d'edition du rangev4 possible, car il faudrait
synchroniser les objets iplist""" synchroniser les objets iplist"""
class Meta(IpTypeForm.Meta): class Meta(IpTypeForm.Meta):
fields = ['extension', 'type', 'need_infra', 'prefix_v6', 'vlan', fields = ['extension', 'type', 'need_infra', 'domaine_ip_network', 'domaine_ip_netmask',
'prefix_v6', 'prefix_v6_length',
'vlan', 'reverse_v4', 'reverse_v6',
'ouverture_ports'] 'ouverture_ports']
@ -236,7 +238,7 @@ class DelIpTypeForm(FormRevMixin, Form):
"""Suppression d'un ou plusieurs iptype""" """Suppression d'un ou plusieurs iptype"""
iptypes = forms.ModelMultipleChoiceField( iptypes = forms.ModelMultipleChoiceField(
queryset=IpType.objects.none(), queryset=IpType.objects.none(),
label="Types d'ip actuelles", label=_("Current IP types"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -258,17 +260,17 @@ class ExtensionForm(FormRevMixin, ModelForm):
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(ExtensionForm, self).__init__(*args, prefix=prefix, **kwargs) super(ExtensionForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['name'].label = 'Extension à ajouter' self.fields['name'].label = _("Extension to add")
self.fields['origin'].label = 'Enregistrement A origin' self.fields['origin'].label = _("A record origin")
self.fields['origin_v6'].label = 'Enregistrement AAAA origin' self.fields['origin_v6'].label = _("AAAA record origin")
self.fields['soa'].label = 'En-tête SOA à utiliser' self.fields['soa'].label = _("SOA record to use")
class DelExtensionForm(FormRevMixin, Form): class DelExtensionForm(FormRevMixin, Form):
"""Suppression d'une ou plusieurs extensions""" """Suppression d'une ou plusieurs extensions"""
extensions = forms.ModelMultipleChoiceField( extensions = forms.ModelMultipleChoiceField(
queryset=Extension.objects.none(), queryset=Extension.objects.none(),
label="Extensions actuelles", label=_("Current extensions"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -307,7 +309,7 @@ class DelSOAForm(FormRevMixin, Form):
"""Suppression d'un ou plusieurs SOA""" """Suppression d'un ou plusieurs SOA"""
soa = forms.ModelMultipleChoiceField( soa = forms.ModelMultipleChoiceField(
queryset=SOA.objects.none(), queryset=SOA.objects.none(),
label="SOA actuels", label=_("Current SOA records"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -338,7 +340,7 @@ class DelMxForm(FormRevMixin, Form):
"""Suppression d'un ou plusieurs MX""" """Suppression d'un ou plusieurs MX"""
mx = forms.ModelMultipleChoiceField( mx = forms.ModelMultipleChoiceField(
queryset=Mx.objects.none(), queryset=Mx.objects.none(),
label="MX actuels", label=_("Current MX records"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -371,7 +373,7 @@ class DelNsForm(FormRevMixin, Form):
"""Suppresion d'un ou plusieurs NS""" """Suppresion d'un ou plusieurs NS"""
ns = forms.ModelMultipleChoiceField( ns = forms.ModelMultipleChoiceField(
queryset=Ns.objects.none(), queryset=Ns.objects.none(),
label="Enregistrements NS actuels", label=_("Current NS records"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -399,7 +401,7 @@ class DelTxtForm(FormRevMixin, Form):
"""Suppression d'un ou plusieurs TXT""" """Suppression d'un ou plusieurs TXT"""
txt = forms.ModelMultipleChoiceField( txt = forms.ModelMultipleChoiceField(
queryset=Txt.objects.none(), queryset=Txt.objects.none(),
label="Enregistrements Txt actuels", label=_("Current TXT records"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -427,7 +429,7 @@ class DelDNameForm(FormRevMixin, Form):
"""Delete a set of DNAME entries""" """Delete a set of DNAME entries"""
dnames = forms.ModelMultipleChoiceField( dnames = forms.ModelMultipleChoiceField(
queryset=Txt.objects.none(), queryset=Txt.objects.none(),
label="Existing DNAME entries", label=_("Current DNAME records"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -455,7 +457,7 @@ class DelSrvForm(FormRevMixin, Form):
"""Suppression d'un ou plusieurs Srv""" """Suppression d'un ou plusieurs Srv"""
srv = forms.ModelMultipleChoiceField( srv = forms.ModelMultipleChoiceField(
queryset=Srv.objects.none(), queryset=Srv.objects.none(),
label="Enregistrements Srv actuels", label=_("Current SRV records"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -484,7 +486,7 @@ class DelNasForm(FormRevMixin, Form):
"""Suppression d'un ou plusieurs nas""" """Suppression d'un ou plusieurs nas"""
nas = forms.ModelMultipleChoiceField( nas = forms.ModelMultipleChoiceField(
queryset=Nas.objects.none(), queryset=Nas.objects.none(),
label="Enregistrements Nas actuels", label=_("Current NAS devices"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -497,6 +499,38 @@ class DelNasForm(FormRevMixin, Form):
self.fields['nas'].queryset = Nas.objects.all() self.fields['nas'].queryset = Nas.objects.all()
class RoleForm(FormRevMixin, ModelForm):
"""Add and edit role."""
class Meta:
model = Role
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(RoleForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['servers'].queryset = (Interface.objects.all()
.select_related(
'domain__extension'
))
class DelRoleForm(FormRevMixin, Form):
"""Deletion of one or several roles."""
role = forms.ModelMultipleChoiceField(
queryset=Role.objects.none(),
label=_("Current roles"),
widget=forms.CheckboxSelectMultiple
)
def __init__(self, *args, **kwargs):
instances = kwargs.pop('instances', None)
super(DelRoleForm, self).__init__(*args, **kwargs)
if instances:
self.fields['role'].queryset = instances
else:
self.fields['role'].queryset = Role.objects.all()
class ServiceForm(FormRevMixin, ModelForm): class ServiceForm(FormRevMixin, ModelForm):
"""Ajout et edition d'une classe de service : dns, dhcp, etc""" """Ajout et edition d'une classe de service : dns, dhcp, etc"""
class Meta: class Meta:
@ -525,7 +559,7 @@ class DelServiceForm(FormRevMixin, Form):
"""Suppression d'un ou plusieurs service""" """Suppression d'un ou plusieurs service"""
service = forms.ModelMultipleChoiceField( service = forms.ModelMultipleChoiceField(
queryset=Service.objects.none(), queryset=Service.objects.none(),
label="Services actuels", label=_("Current services"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -553,7 +587,7 @@ class DelVlanForm(FormRevMixin, Form):
"""Suppression d'un ou plusieurs vlans""" """Suppression d'un ou plusieurs vlans"""
vlan = forms.ModelMultipleChoiceField( vlan = forms.ModelMultipleChoiceField(
queryset=Vlan.objects.none(), queryset=Vlan.objects.none(),
label="Vlan actuels", label=_("Current VLANs"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -611,3 +645,4 @@ class SshFpForm(FormRevMixin, ModelForm):
prefix=prefix, prefix=prefix,
**kwargs **kwargs
) )

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-23 14:07
from __future__ import unicode_literals
from django.db import migrations, models
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
('machines', '0085_sshfingerprint'),
]
operations = [
migrations.CreateModel(
name='Role',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role_type', models.CharField(max_length=255, unique=True)),
('servers', models.ManyToManyField(to='machines.Interface')),
('specific_role', models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursif-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'Radius server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gatewaw')], max_length=32, null=True))
],
options={'permissions': (('view_role', 'Can view a role.'),), 'verbose_name': 'Server role'},
bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model),
),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-25 15:06
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0086_role'),
]
operations = [
migrations.AddField(
model_name='iptype',
name='dnssec_reverse_v4',
field=models.BooleanField(default=False, help_text='Activer DNSSEC sur le reverse DNS IPv4'),
),
migrations.AddField(
model_name='iptype',
name='dnssec_reverse_v6',
field=models.BooleanField(default=False, help_text='Activer DNSSEC sur le reverse DNS IPv6'),
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-07-16 18:46
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0087_dnssec'),
]
operations = [
migrations.AddField(
model_name='iptype',
name='prefix_v6_length',
field=models.IntegerField(default=64, validators=[django.core.validators.MaxValueValidator(128), django.core.validators.MinValueValidator(0)]),
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-08-05 09:48
from __future__ import unicode_literals
from django.db import migrations
import macaddress.fields
class Migration(migrations.Migration):
dependencies = [
('machines', '0088_iptype_prefix_v6_length'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='mac_address',
field=macaddress.fields.MACAddressField(integer=False, max_length=17),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-08-05 12:59
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0089_auto_20180805_1148'),
]
operations = [
migrations.AlterField(
model_name='ipv6list',
name='ipv6',
field=models.GenericIPAddressField(protocol='IPv6'),
),
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-08-06 21:10
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0090_auto_20180805_1459'),
]
operations = [
migrations.AddField(
model_name='iptype',
name='domaine_ip_netmask',
field=models.IntegerField(default=24, help_text='Netmask for the ipv4 range domain', validators=[django.core.validators.MaxValueValidator(31), django.core.validators.MinValueValidator(8)]),
),
migrations.AddField(
model_name='iptype',
name='domaine_ip_network',
field=models.GenericIPAddressField(blank=True, help_text='Network containing the ipv4 range domain ip start/stop. Optional', null=True, protocol='IPv4'),
),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-08-07 07:26
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('machines', '0091_auto_20180806_2310'),
]
operations = [
migrations.RenameField(
model_name='iptype',
old_name='dnssec_reverse_v4',
new_name='reverse_v4',
),
migrations.RenameField(
model_name='iptype',
old_name='dnssec_reverse_v6',
new_name='reverse_v6',
),
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-08-07 09:15
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('machines', '0092_auto_20180807_0926'),
]
operations = [
migrations.AlterField(
model_name='mx',
name='name',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='machines.Domain'),
),
migrations.AlterField(
model_name='mx',
name='priority',
field=models.PositiveIntegerField(),
),
]

View file

@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-08-15 17:18
from __future__ import unicode_literals
import datetime
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('machines', '0093_auto_20180807_1115'),
]
operations = [
migrations.AlterModelOptions(
name='dname',
options={'permissions': (('view_dname', 'Can view a DNAME record object'),), 'verbose_name': 'DNAME record', 'verbose_name_plural': 'DNAME records'},
),
migrations.AlterModelOptions(
name='domain',
options={'permissions': (('view_domain', 'Can view a domain object'),), 'verbose_name': 'domain', 'verbose_name_plural': 'domains'},
),
migrations.AlterModelOptions(
name='extension',
options={'permissions': (('view_extension', 'Can view an extension object'), ('use_all_extension', 'Can use all extensions')), 'verbose_name': 'DNS extension', 'verbose_name_plural': 'DNS extensions'},
),
migrations.AlterModelOptions(
name='interface',
options={'permissions': (('view_interface', 'Can view an interface object'), ('change_interface_machine', 'Can change the owner of an interface')), 'verbose_name': 'interface', 'verbose_name_plural': 'interfaces'},
),
migrations.AlterModelOptions(
name='iplist',
options={'permissions': (('view_iplist', 'Can view an IPv4 addresses list object'),), 'verbose_name': 'IPv4 addresses list', 'verbose_name_plural': 'IPv4 addresses lists'},
),
migrations.AlterModelOptions(
name='iptype',
options={'permissions': (('view_iptype', 'Can view an IP type object'), ('use_all_iptype', 'Can use all IP types')), 'verbose_name': 'IP type', 'verbose_name_plural': 'IP types'},
),
migrations.AlterModelOptions(
name='ipv6list',
options={'permissions': (('view_ipv6list', 'Can view an IPv6 addresses list object'), ('change_ipv6list_slaac_ip', 'Can change the SLAAC value of an IPv6 addresses list')), 'verbose_name': 'IPv6 addresses list', 'verbose_name_plural': 'IPv6 addresses lists'},
),
migrations.AlterModelOptions(
name='machine',
options={'permissions': (('view_machine', 'Can view a machine object'), ('change_machine_user', 'Can change the user of a machine')), 'verbose_name': 'machine', 'verbose_name_plural': 'machines'},
),
migrations.AlterModelOptions(
name='machinetype',
options={'permissions': (('view_machinetype', 'Can view a machine type object'), ('use_all_machinetype', 'Can use all machine types')), 'verbose_name': 'machine type', 'verbose_name_plural': 'machine types'},
),
migrations.AlterModelOptions(
name='mx',
options={'permissions': (('view_mx', 'Can view an MX record object'),), 'verbose_name': 'MX record', 'verbose_name_plural': 'MX records'},
),
migrations.AlterModelOptions(
name='nas',
options={'permissions': (('view_nas', 'Can view a NAS device object'),), 'verbose_name': 'NAS device', 'verbose_name_plural': 'NAS devices'},
),
migrations.AlterModelOptions(
name='ns',
options={'permissions': (('view_ns', 'Can view an NS record object'),), 'verbose_name': 'NS record', 'verbose_name_plural': 'NS records'},
),
migrations.AlterModelOptions(
name='ouvertureport',
options={'verbose_name': 'ports openings'},
),
migrations.AlterModelOptions(
name='ouvertureportlist',
options={'permissions': (('view_ouvertureportlist', 'Can view a ports opening list object'),), 'verbose_name': 'ports opening list', 'verbose_name_plural': 'ports opening lists'},
),
migrations.AlterModelOptions(
name='role',
options={'permissions': (('view_role', 'Can view a role object'),), 'verbose_name': 'server role', 'verbose_name_plural': 'server roles'},
),
migrations.AlterModelOptions(
name='service',
options={'permissions': (('view_service', 'Can view a service object'),), 'verbose_name': 'service to generate (DHCP, DNS, ...)', 'verbose_name_plural': 'services to generate (DHCP, DNS, ...)'},
),
migrations.AlterModelOptions(
name='service_link',
options={'permissions': (('view_service_link', 'Can view a service server link object'),), 'verbose_name': 'link between service and server', 'verbose_name_plural': 'links between service and server'},
),
migrations.AlterModelOptions(
name='soa',
options={'permissions': (('view_soa', 'Can view an SOA record object'),), 'verbose_name': 'SOA record', 'verbose_name_plural': 'SOA records'},
),
migrations.AlterModelOptions(
name='srv',
options={'permissions': (('view_srv', 'Can view an SRV record object'),), 'verbose_name': 'SRV record', 'verbose_name_plural': 'SRV records'},
),
migrations.AlterModelOptions(
name='sshfp',
options={'permissions': (('view_sshfp', 'Can view an SSHFP record object'),), 'verbose_name': 'SSHFP record', 'verbose_name_plural': 'SSHFP records'},
),
migrations.AlterModelOptions(
name='txt',
options={'permissions': (('view_txt', 'Can view a TXT record object'),), 'verbose_name': 'TXT record', 'verbose_name_plural': 'TXT records'},
),
migrations.AlterModelOptions(
name='vlan',
options={'permissions': (('view_vlan', 'Can view a VLAN object'),), 'verbose_name': 'VLAN', 'verbose_name_plural': 'VLANs'},
),
migrations.AlterField(
model_name='domain',
name='name',
field=models.CharField(help_text='Mandatory and unique, must not contain dots.', max_length=255),
),
migrations.AlterField(
model_name='extension',
name='name',
field=models.CharField(help_text='Zone name, must begin with a dot (.example.org)', max_length=255, unique=True),
),
migrations.AlterField(
model_name='extension',
name='origin',
field=models.ForeignKey(blank=True, help_text='A record associated with the zone', null=True, on_delete=django.db.models.deletion.PROTECT, to='machines.IpList'),
),
migrations.AlterField(
model_name='extension',
name='origin_v6',
field=models.GenericIPAddressField(blank=True, help_text='AAAA record associated with the zone', null=True, protocol='IPv6'),
),
migrations.AlterField(
model_name='iptype',
name='domaine_ip_netmask',
field=models.IntegerField(default=24, help_text="Netmask for the domain's IPv4 range", validators=[django.core.validators.MaxValueValidator(31), django.core.validators.MinValueValidator(8)]),
),
migrations.AlterField(
model_name='iptype',
name='domaine_ip_network',
field=models.GenericIPAddressField(blank=True, help_text="Network containing the domain's IPv4 range (optional)", null=True, protocol='IPv4'),
),
migrations.AlterField(
model_name='iptype',
name='reverse_v4',
field=models.BooleanField(default=False, help_text='Enable reverse DNS for IPv4'),
),
migrations.AlterField(
model_name='iptype',
name='reverse_v6',
field=models.BooleanField(default=False, help_text='Enable reverse DNS for IPv6'),
),
migrations.AlterField(
model_name='machine',
name='name',
field=models.CharField(blank=True, help_text='Optional', max_length=255, null=True),
),
migrations.AlterField(
model_name='ouvertureportlist',
name='name',
field=models.CharField(help_text='Name of the ports configuration', max_length=255),
),
migrations.AlterField(
model_name='role',
name='specific_role',
field=models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursif-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'RADIUS server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gateway')], max_length=32, null=True),
),
migrations.AlterField(
model_name='service',
name='min_time_regen',
field=models.DurationField(default=datetime.timedelta(0, 60), help_text='Minimal time before regeneration of the service.'),
),
migrations.AlterField(
model_name='service',
name='regular_time_regen',
field=models.DurationField(default=datetime.timedelta(0, 3600), help_text='Maximal time before regeneration of the service.'),
),
migrations.AlterField(
model_name='soa',
name='expire',
field=models.PositiveIntegerField(default=3600000, help_text='Seconds before the secondary DNS stop answering requests in case of primary DNS timeout'),
),
migrations.AlterField(
model_name='soa',
name='mail',
field=models.EmailField(help_text='Contact email address for the zone', max_length=254),
),
migrations.AlterField(
model_name='soa',
name='refresh',
field=models.PositiveIntegerField(default=86400, help_text='Seconds before the secondary DNS have to ask the primary DNS serial to detect a modification'),
),
migrations.AlterField(
model_name='soa',
name='retry',
field=models.PositiveIntegerField(default=7200, help_text='Seconds before the secondary DNS ask the serial again in case of a primary DNS timeout'),
),
migrations.AlterField(
model_name='soa',
name='ttl',
field=models.PositiveIntegerField(default=172800, help_text='Time to Live'),
),
migrations.AlterField(
model_name='srv',
name='port',
field=models.PositiveIntegerField(help_text='TCP/UDP port', validators=[django.core.validators.MaxValueValidator(65535)]),
),
migrations.AlterField(
model_name='srv',
name='priority',
field=models.PositiveIntegerField(default=0, help_text='Priority of the target server (positive integer value, the lower it is, the more the server will be used if available)', validators=[django.core.validators.MaxValueValidator(65535)]),
),
migrations.AlterField(
model_name='srv',
name='target',
field=models.ForeignKey(help_text='Target server', on_delete=django.db.models.deletion.PROTECT, to='machines.Domain'),
),
migrations.AlterField(
model_name='srv',
name='ttl',
field=models.PositiveIntegerField(default=172800, help_text='Time to Live'),
),
migrations.AlterField(
model_name='srv',
name='weight',
field=models.PositiveIntegerField(default=0, help_text='Relative weight for records with the same priority (integer value between 0 and 65535)', validators=[django.core.validators.MaxValueValidator(65535)]),
),
]

File diff suppressed because it is too large Load diff

View file

@ -23,12 +23,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load i18n %}
{% load logs_extra %} {% load logs_extra %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Alias</th> <th>{% trans "Aliases" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>

View file

@ -22,12 +22,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Target zone</th> <th>{% trans "Target zone" %}</th>
<th>Record</th> <th>{% trans "Record" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -43,6 +44,5 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -25,17 +25,18 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load design %} {% load design %}
{% load i18n %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Extension</th> <th>{% trans "Extension" %}</th>
<th>Droit infra pour utiliser ?</th> <th>{% trans "'infra' right required" %}</th>
<th>Enregistrement SOA</th> <th>{% trans "SOA record" %}</th>
<th>Enregistrement A origin</th> <th>{% trans "A record origin" %}</th>
{% if ipv6_enabled %} {% if ipv6_enabled %}
<th>Enregistrement AAAA origin</th> <th>{% trans "AAAA record origin" %}</th>
{% endif %} {% endif %}
<th></th> <th></th>
</tr> </tr>
@ -44,7 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td>{{ extension.name }}</td> <td>{{ extension.name }}</td>
<td>{{ extension.need_infra|tick }}</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 %}
<td>{{ extension.origin_v6 }}</td> <td>{{ extension.origin_v6 }}</td>
@ -59,3 +60,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>
</div> </div>

View file

@ -26,18 +26,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load i18n %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Type d'ip</th> <th>{% trans "IP type" %}</th>
<th>Extension</th> <th>{% trans "Extension" %}</th>
<th>Nécessite l'autorisation infra</th> <th>{% trans "'infra' right required" %}</th>
<th>Plage ipv4</th> <th>{% trans "IPv4 range" %}</th>
<th>Préfixe v6</th> <th>{% trans "v6 prefix" %}</th>
<th>Sur vlan</th> <th>{% trans "DNSSEC reverse v4/v6" %}</th>
<th>Ouverture ports par défault</th> <th>{% trans "On VLAN(s)" %}</th>
<th></th> <th>{% trans "Default ports opening" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -46,8 +48,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ type.type }}</td> <td>{{ type.type }}</td>
<td>{{ type.extension }}</td> <td>{{ type.extension }}</td>
<td>{{ type.need_infra|tick }}</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 }}{% if type.ip_network %}<b><u> on </b></u>{{ type.ip_network }}{% endif %}</td>
<td>{{ type.prefix_v6 }}</td> <td>{{ type.prefix_v6 }}/{{ type.prefix_v6_length }}</td>
<td>{{ type.reverse_v4|tick }}/{{ type.reverse_v6|tick }}</td>
<td>{{ type.vlan }}</td> <td>{{ type.vlan }}</td>
<td>{{ type.ouverture_ports }}</td> <td>{{ type.ouverture_ports }}</td>
<td class="text-right"> <td class="text-right">
@ -60,3 +63,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>
</div> </div>

View file

@ -24,12 +24,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Ipv6</th> <th>{% trans "IPv6 addresses" %}</th>
<th>Slaac</th> <th>{% trans "SLAAC" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>

View file

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load i18n %}
<div class="table-responsive"> <div class="table-responsive">
{% if machines_list.paginator %} {% if machines_list.paginator %}
@ -39,23 +40,27 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<col width="144px"> <col width="144px">
</colgroup> </colgroup>
<thead> <thead>
<th>{% include "buttons/sort.html" with prefix='machine' col='name' text='Nom DNS' %}</th> {% trans "DNS name" as tr_dns_name %}
<th>Type</th> <th>{% include "buttons/sort.html" with prefix='machine' col='name' text=tr_dns_name %}</th>
<th>MAC</th> <th>{% trans "Type" %}</th>
<th>IP</th> <th>{% trans "MAC address" %}</th>
<th>Actions</th> <th>{% trans "IP address" %}</th>
<th>{% trans "Actions" %}</th>
<tbody> <tbody>
{% for machine in machines_list %} {% for machine in machines_list %}
<tr class="info"> <tr class="info">
<td colspan="4"> <td colspan="4">
<b>{{ machine.name|default:'<i>Pas de nom</i>' }}</b> <i class="fa-angle-right"></i> {% trans "No name" as tr_no_name %}
<a href="{% url 'users:profil' userid=machine.user.id %}" title="Voir le profil"> {% trans "View the profile" as tr_view_the_profile %}
<b>{{ machine.name|default:tr_no_name }}</b> <i class="fa-angle-right"></i>
<a href="{% url 'users:profil' userid=machine.user.id %}" title=tr_view_the_profile>
<i class="fa fa-user"></i> {{ machine.user }} <i class="fa fa-user"></i> {{ machine.user }}
</a> </a>
</td> </td>
<td class="text-right"> <td class="text-right">
{% 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' %} {% trans "Create an interface" as tr_create_an_interface %}
{% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc=tr_create_an_interface %}
{% acl_end %} {% acl_end %}
{% history_button machine %} {% history_button machine %}
{% can_delete machine %} {% can_delete machine %}
@ -68,8 +73,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td> <td>
{% if interface.domain.related_domain.all %} {% if interface.domain.related_domain.all %}
{{ interface.domain }} {{ interface.domain }}
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#collapseDomain_{{interface.id}}" aria-expanded="true" aria-controls="collapseDomain_{{interface.id}}"> <button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#collapseDomain_{{ interface.id }}" aria-expanded="true" aria-controls="collapseDomain_{{ interface.id }}">
Afficher les alias {% trans "Display the aliases" %}
</button> </button>
{% else %} {% else %}
{{ interface.domain }} {{ interface.domain }}
@ -86,8 +91,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<br> <br>
{% if ipv6_enabled and interface.ipv6 != 'None'%} {% if ipv6_enabled and interface.ipv6 != 'None'%}
<b>IPv6</b> <b>IPv6</b>
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#collapseIpv6_{{interface.id}}" aria-expanded="true" aria-controls="collapseIpv6_{{interface.id}}"> <button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#collapseIpv6_{{ interface.id }}" aria-expanded="true" aria-controls="collapseIpv6_{{ interface.id }}">
Afficher l'IPV6 {% trans "Display the IPv6 address" %}
</button> </button>
{% endif %} {% endif %}
</td> </td>
@ -97,39 +102,44 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<button class="btn btn-primary btn-sm dropdown-toggle" type="button" id="editioninterface" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> <button class="btn btn-primary btn-sm dropdown-toggle" type="button" id="editioninterface" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-edit"></i> <span class="caret"></span> <i class="fa fa-edit"></i> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu pull-right" aria-labelledby="editioninterface"> <ul class="dropdown-menu" aria-labelledby="editioninterface">
{% can_edit interface %} {% can_edit interface %}
<li> <li>
<a href="{% url 'machines:edit-interface' interface.id %}"> <a href="{% url 'machines:edit-interface' interface.id %}">
<i class="fa fa-edit"></i> Editer <i class="fa fa-edit"></i>
{% trans " Edit"%}
</a> </a>
</li> </li>
{% acl_end %} {% acl_end %}
{% can_create Domain interface.id %} {% can_create Domain interface.id %}
<li> <li>
<a href="{% url 'machines:index-alias' interface.id %}"> <a href="{% url 'machines:index-alias' interface.id %}">
<i class="fa fa-edit"></i> Gerer les alias <i class="fa fa-edit"></i>
{% trans " Manage the aliases" %}
</a> </a>
</li> </li>
{% acl_end %} {% acl_end %}
{% can_create Ipv6List interface.id %} {% can_create Ipv6List interface.id %}
<li> <li>
<a href="{% url 'machines:index-ipv6' interface.id %}"> <a href="{% url 'machines:index-ipv6' interface.id %}">
<i class="fa fa-edit"></i> Gerer les ipv6 <i class="fa fa-edit"></i>
{% trans " Manage the IPv6 addresses" %}
</a> </a>
</li> </li>
{% acl_end %} {% acl_end %}
{% can_create SshFp interface.machine.id %} {% can_create SshFp interface.machine.id %}
<li> <li>
<a href="{% url 'machines:index-sshfp' interface.machine.id %}"> <a href="{% url 'machines:index-sshfp' interface.machine.id %}">
<i class="fa fa-edit"></i> Manage the SSH fingerprints <i class="fa fa-edit"></i>
{% trans " Manage the SSH fingerprints" %}
</a> </a>
</li> </li>
{% acl_end %} {% acl_end %}
{% can_create OuverturePortList %} {% can_create OuverturePortList %}
<li> <li>
<a href="{% url 'machines:port-config' interface.id%}"> <a href="{% url 'machines:port-config' interface.id%}">
<i class="fa fa-edit"></i> Gerer la configuration des ports <i class="fa fa-edit"></i>
{% trans " Manage the ports configuration" %}
</a> </a>
</li> </li>
{% acl_end %} {% acl_end %}
@ -142,7 +152,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div> </div>
</td> </td>
</tr> </tr>
{% if ipv6_enabled and interface.ipv6 != 'None'%} {% if ipv6_enabled and interface.ipv6 != 'None'%}
<tr> <tr>
<td colspan=5 style="border-top: none; padding: 1px;"> <td colspan=5 style="border-top: none; padding: 1px;">
@ -150,16 +159,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<ul class="list-group" style="margin-bottom: 0px;"> <ul class="list-group" style="margin-bottom: 0px;">
{% for ipv6 in interface.ipv6.all %} {% for ipv6 in interface.ipv6.all %}
<li class="list-group-item col-xs-6 col-sm-6 col-md-6" style="border: none;"> <li class="list-group-item col-xs-6 col-sm-6 col-md-6" style="border: none;">
{{ipv6}} {{ ipv6 }}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
</td> </td>
<tr> </tr>
{% endif %} {% endif %}
{% if interface.domain.related_domain.all %} {% if interface.domain.related_domain.all %}
<tr> <tr>
<td colspan=5 style="border-top: none; padding: 1px;"> <td colspan=5 style="border-top: none; padding: 1px;">
@ -176,7 +183,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</ul> </ul>
</div> </div>
</td> </td>
<tr> </tr>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<tr> <tr>
@ -184,19 +191,18 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</thead>
</table> </table>
<script> <script>
$("#machines_table").ready( function() { $("#machines_table").ready( function() {
var alias_div = [{% for machine in machines_list %}{% for interface in machine.interface_set.all %}{% if interface.domain.related_domain.all %}$("#collapseDomain_{{interface.id}}"), {% endif %}{% endfor %}{% endfor %}]; var alias_div = [{% for machine in machines_list %}{% for interface in machine.interface_set.all %}{% if interface.domain.related_domain.all %}$("#collapseDomain_{{ interface.id }}"), {% endif %}{% endfor %}{% endfor %}];
for (var i=0 ; i<alias_div.length ; i++) { for (var i=0 ; i<alias_div.length ; i++) {
alias_div[i].collapse('hide'); alias_div[i].collapse('hide');
} }
} ); } );
$("#machines_table").ready( function() { $("#machines_table").ready( function() {
var ipv6_div = [{% for machine in machines_list %}{% for interface in machine.interface_set.all %}{% if interface.ipv6.all %}$("#collapseIpv6_{{interface.id}}"), {% endif %}{% endfor %}{% endfor %}]; var ipv6_div = [{% for machine in machines_list %}{% for interface in machine.interface_set.all %}{% if interface.ipv6.all %}$("#collapseIpv6_{{ interface.id }}"), {% endif %}{% endfor %}{% endfor %}];
for (var i=0 ; i<ipv6_div.length ; i++) { for (var i=0 ; i<ipv6_div.length ; i++) {
ipv6_div[i].collapse('hide'); ipv6_div[i].collapse('hide');
} }
@ -207,3 +213,4 @@ $("#machines_table").ready( function() {
{% include "pagination.html" with list=machines_list %} {% include "pagination.html" with list=machines_list %}
{% endif %} {% endif %}
</div> </div>

View file

@ -24,12 +24,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Type de machine</th> <th>{% trans "Machine type" %}</th>
<th>Type d'ip correspondant</th> <th>{% trans "Matching IP type" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>

View file

@ -24,14 +24,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Zone concernée</th> <th>{% trans "Concerned zone" %}</th>
<th>Priorité</th> <th>{% trans "Priority" %}</th>
<th>Enregistrement</th> <th>{% trans "Record" %}</th>
<th></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -50,4 +50,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>

View file

@ -25,15 +25,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load design %} {% load design %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Nom</th> <th>{% trans "Name" %}</th>
<th>Type du nas</th> <th>{% trans "NAS device type" %}</th>
<th>Type de machine reliées au nas</th> <th>{% trans "Machine type linked to the NAS device" %}</th>
<th>Mode d'accès</th> <th>{% trans "Access mode" %}</th>
<th>Autocapture mac</th> <th>{% trans "MAC address auto capture" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>

View file

@ -24,13 +24,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Zone concernée</th> <th>{% trans "Concerned zone" %}</th>
<th>Interface autoritaire de la zone</th> <th>{% trans "Authoritarian interface for the concerned zone" %}</th>
<th></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -48,4 +48,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>

View file

@ -0,0 +1,54 @@
{% 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 acl %}
{% load i18n %}
{% load logs_extra %}
<table class="table table-striped">
<thead>
<tr>
<th>{% trans "Role name" %}</th>
<th>{% trans "Specific role" %}</th>
<th>{% trans "Servers" %}</th>
<th></th>
<th></th>
</tr>
</thead>
{% for role in role_list %}
<tr>
<td>{{ role.role_type }}</td>
<td>{{ role.specific_role }}</td>
<td>{% for serv in role.servers.all %}{{ serv }}, {% endfor %}</td>
<td class="text-right">
{% can_edit role %}
{% include 'buttons/edit.html' with href='machines:edit-role' id=role.id %}
{% acl_end %}
{% history_button role %}
</td>
</tr>
{% endfor %}
</table>

View file

@ -23,15 +23,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load design %} {% load design %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Nom du service</th> <th>{% trans "Service name" %}</th>
<th>Serveur</th> <th>{% trans "Server" %}</th>
<th>Dernière régénération</th> <th>{% trans "Last regeneration" %}</th>
<th>Régénération nécessaire</th> <th>{% trans "Regeneration required" %}</th>
<th>Régénération activée</th> <th>{% trans "Regeneration activated" %}</th>
</tr> </tr>
</thead> </thead>
{% for server in servers_list %} {% for server in servers_list %}
@ -39,8 +40,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| tick }}</td> <td>{{ server.asked_regen|tick }}</td>
<td>{{ server.need_regen | tick }}</td> <td>{{ server.need_regen|tick }}</td>
<td class="text-right"> <td class="text-right">
</td> </td>
</tr> </tr>

View file

@ -24,15 +24,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Nom du service</th> <th>{% trans "Service name" %}</th>
<th>Temps minimum avant nouvelle régénération</th> <th>{% trans "Minimal time before regeneration" %}</th>
<th>Temps avant nouvelle génération obligatoire (max)</th> <th>{% trans "Maximal time before regeneration" %}</th>
<th>Serveurs inclus</th> <th>{% trans "Included servers" %}</th>
<th></th> <th>{% trans "Ask for regeneration" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -42,6 +43,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ service.min_time_regen }}</td> <td>{{ service.min_time_regen }}</td>
<td>{{ service.regular_time_regen }}</td> <td>{{ service.regular_time_regen }}</td>
<td>{% for serv in service.servers.all %}{{ serv }}, {% endfor %}</td> <td>{% for serv in service.servers.all %}{{ serv }}, {% endfor %}</td>
<td><a role="button" class="btn btn-danger" href="{% url 'machines:regen-service' service.id %}"><i class="fa fa-sync"></i></a></td>
<td class="text-right"> <td class="text-right">
{% 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 %}

View file

@ -24,17 +24,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Nom</th> <th>{% trans "Name" %}</th>
<th>Mail</th> <th>{% trans "Mail" %}</th>
<th>Refresh</th> <th>{% trans "Refresh" %}</th>
<th>Retry</th> <th>{% trans "Retry" %}</th>
<th>Expire</th> <th>{% trans "Expire" %}</th>
<th>TTL</th> <th>{% trans "TTL" %}</th>
<th></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -56,4 +56,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>

View file

@ -24,19 +24,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Service</th> <th>{% trans "Service" %}</th>
<th>Protocole</th> <th>{% trans "Protocol" %}</th>
<th>Extension</th> <th>{% trans "Extension" %}</th>
<th>TTL</th> <th>{% trans "TTL" %}</th>
<th>Priorité</th> <th>{% trans "Priority" %}</th>
<th>Poids</th> <th>{% trans "Weight" %}</th>
<th>Port</th> <th>{% trans "Port" %}</th>
<th>Cible</th> <th>{% trans "Target" %}</th>
<th></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -60,4 +60,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>

View file

@ -21,15 +21,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load i18n %}
{% load logs_extra %} {% load logs_extra %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped long_text"> <table class="table table-striped long_text">
<thead> <thead>
<tr> <tr>
<th class="long_text">SSH public key</th> <th class="long_text">{% trans "SSH public key" %}</th>
<th>Algorithm used</th> <th>{% trans "Algorithm used" %}</th>
<th>Comment</th> <th>{% trans "Comment" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -42,10 +43,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_edit sshfp %} {% can_edit sshfp %}
{% include 'buttons/edit.html' with href='machines:edit-sshfp' id=sshfp.id %} {% include 'buttons/edit.html' with href='machines:edit-sshfp' id=sshfp.id %}
{% acl_end %} {% acl_end %}
{% history_button sshfp %}
{% can_delete sshfp %} {% can_delete sshfp %}
{% include 'buttons/suppr.html' with href='machines:del-sshfp' id=sshfp.id %} {% include 'buttons/suppr.html' with href='machines:del-sshfp' id=sshfp.id %}
{% acl_end %} {% acl_end %}
{% history_button sshfp %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -24,13 +24,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Zone concernée</th> <th>{% trans "Concerned zone" %}</th>
<th>Enregistrement</th> <th>{% trans "Record" %}</th>
<th></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -48,4 +48,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>

View file

@ -24,15 +24,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load i18n %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Id</th> <th>{% trans "ID" %}</th>
<th>Nom</th> <th>{% trans "Name" %}</th>
<th>Commentaire</th> <th>{% trans "Comment" %}</th>
<th>Ranges ip</th> <th>{% trans "IP ranges" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -52,3 +53,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>
</div> </div>

View file

@ -24,17 +24,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %}
{% block title %}Création et modification de machines{% endblock %} {% block title %}{% trans "Creation and editing of machines" %}{% endblock %}
{% block content %} {% block content %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
<h4>Attention, voulez-vous vraiment supprimer cet objet {{ objet_name }} ( {{ objet }} ) ?</h4> <h4>{% blocktrans %}Warning: are you sure you want to delete this object {{ objet_name }} ( {{ objet }} )?{% endblocktrans %}</h4>
{% bootstrap_button "Confirmer" button_type="submit" icon="trash" %} {% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type="submit" icon="trash" %}
</form> </form>
<br /> <br />
<br /> <br />
<br /> <br />
{% endblock %} {% endblock %}

View file

@ -24,8 +24,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %}
{% block title %}Création et modification de machines{% endblock %} {% block title %}{% trans "Machines" %}{% endblock %}
{% block content %} {% block content %}
{% bootstrap_form_errors port_list %} {% bootstrap_form_errors port_list %}
@ -46,10 +47,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div> </div>
<p> <p>
<input class="btn btn-primary btn-sm" role="button" value="Ajouter un port" id="add_one"> {% trans "Add a port" as value %}
<input class="btn btn-primary btn-sm" role="button" value=value id="add_one">
</p> </p>
{% trans "Create or edit" as tr_create_or_edit %}
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} {% bootstrap_button tr_create_or_edit button_type="submit" icon="star" %}
</form> </form>
<script type="text/javascript"> <script type="text/javascript">
var template = `{{ports.empty_form}}`; var template = `{{ports.empty_form}}`;
@ -67,3 +69,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</script> </script>
{% endblock %} {% endblock %}

View file

@ -24,11 +24,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %}
{% block title %}Machines{% endblock %} {% block title %}{% trans "Machines" %}{% endblock %}
{% block content %} {% block content %}
<h2>Machines</h2> <h2>{% trans "Machines" %}</h2>
{% include "machines/aff_machines.html" with machines_list=machines_list %} {% include "machines/aff_machines.html" with machines_list=machines_list %}
<br /> <br />
<br /> <br />

View file

@ -24,13 +24,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %}
{% block title %}Machines{% endblock %} {% block title %}{% trans "Machines" %}{% endblock %}
{% block content %} {% block content %}
<h2>Liste des alias de l'interface</h2> <h2>{% trans "List of the aliases of the interface" %}</h2>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-alias' interface_id %}"><i class="fa fa-plus"></i> Ajouter un alias</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-alias' interface_id %}"><i class="fa fa-plus"></i>{% trans " Add an alias" %}</a>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-alias' interface_id %}"><i class="fa fa-trash"></i> Supprimer un ou plusieurs alias</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-alias' interface_id %}"><i class="fa fa-trash"></i>{% trans " Delete one or several aliases" %}</a>
{% include "machines/aff_alias.html" with alias_list=alias_list %} {% include "machines/aff_alias.html" with alias_list=alias_list %}
<br /> <br />
<br /> <br />

View file

@ -28,57 +28,60 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% load i18n %} {% load i18n %}
{% block title %}Machines{% endblock %} {% block title %}{% trans "Machines" %}{% endblock %}
{% block content %} {% block content %}
<h2>Liste des extensions</h2> <h2>{% trans "List of extensions" %}</h2>
{% can_create Extension %} {% can_create Extension %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-extension' %}"><i class="fa fa-plus"></i> Ajouter une extension</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-extension' %}"><i class="fa fa-plus"></i>{% trans " Add an extension" %}</a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-extension' %}"><i class="fa fa-trash"></i> Supprimer une ou plusieurs extensions</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-extension' %}"><i class="fa fa-trash"></i>{% trans " Delete one or several extensions" %}</a>
{% include "machines/aff_extension.html" with extension_list=extension_list %} {% include "machines/aff_extension.html" with extension_list=extension_list %}
<h2>Liste des enregistrements SOA</h2> <h2>{% trans "List of SOA records" %}</h2>
{% can_create SOA %} {% can_create SOA %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-soa' %}"><i class="fa fa-plus"></i> Ajouter un enregistrement SOA</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-soa' %}"><i class="fa fa-plus"></i>{% trans " Add an SOA record" %}</a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-soa' %}"><i class="fa fa-trash"></i> Supprimer un enregistrement SOA</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-soa' %}"><i class="fa fa-trash"></i>{% trans " Delete one or several SOA records" %}</a>
{% include "machines/aff_soa.html" with soa_list=soa_list %} {% include "machines/aff_soa.html" with soa_list=soa_list %}
<h2>Liste des enregistrements MX</h2>
<h2>{% trans "List of MX records" %}</h2>
{% can_create Mx %} {% can_create Mx %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-mx' %}"><i class="fa fa-plus"></i> Ajouter un enregistrement MX</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-mx' %}"><i class="fa fa-plus"></i>{% trans " Add an MX record" %}</a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-mx' %}"><i class="fa fa-trash"></i> Supprimer un enregistrement MX</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-mx' %}"><i class="fa fa-trash"></i>{% trans " Delete one or several MX records" %}</a>
{% include "machines/aff_mx.html" with mx_list=mx_list %} {% include "machines/aff_mx.html" with mx_list=mx_list %}
<h2>Liste des enregistrements NS</h2>
<h2>{% trans "List of NS records" %}</h2>
{% can_create Ns %} {% can_create Ns %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-ns' %}"><i class="fa fa-plus"></i> Ajouter un enregistrement NS</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-ns' %}"><i class="fa fa-plus"></i>{% trans " Add an NS record" %}</a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-ns' %}"><i class="fa fa-trash"></i> Supprimer un enregistrement NS</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-ns' %}"><i class="fa fa-trash"></i>{% trans " Delete one or several NS records" %}</a>
{% include "machines/aff_ns.html" with ns_list=ns_list %} {% include "machines/aff_ns.html" with ns_list=ns_list %}
<h2>Liste des enregistrements TXT</h2>
<h2>{% trans "List of TXT records" %}</h2>
{% can_create Txt %} {% can_create Txt %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-txt' %}"><i class="fa fa-plus"></i> Ajouter un enregistrement TXT</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-txt' %}"><i class="fa fa-plus"></i>{% trans " Add a TXT record" %}</a>
{% 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>{% trans " Delete one or several TXT records" %}</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> <h2>{% trans "List of DNAME records" %}</h2>
{% can_create DName %} {% can_create DName %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-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" %} <i class="fa fa-plus"></i> {% trans " Add a DNAME record" %}
</a> </a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-dname' %}"> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-dname' %}">
<i class="fa fa-trash"></i> {% trans "Delete DNAME records" %} <i class="fa fa-trash"></i> {% trans " Delete one or several DNAME records" %}
</a> </a>
{% include "machines/aff_dname.html" with dname_list=dname_list %} {% include "machines/aff_dname.html" with dname_list=dname_list %}
<h2>Liste des enregistrements SRV</h2> <h2>{% trans "List of SRV records" %}</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>{% trans " Add an SRV record" %}</a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-srv' %}"><i class="fa fa-trash"></i> Supprimer un enregistrement SRV</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-srv' %}"><i class="fa fa-trash"></i>{% trans " Delete one or several SRV records" %}</a>
{% include "machines/aff_srv.html" with srv_list=srv_list %} {% include "machines/aff_srv.html" with srv_list=srv_list %}
<br /> <br />
<br /> <br />

View file

@ -26,15 +26,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load acl %} {% load acl %}
{% load i18n %}
{% block title %}Ip{% endblock %} {% block title %}{% trans "Machines" %}{% endblock %}
{% block content %} {% block content %}
<h2>Liste des types d'ip</h2> <h2>{% trans "List of IP types" %}</h2>
{% can_create IpType %} {% can_create IpType %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-iptype' %}"><i class="fa fa-plus"></i> Ajouter un type d'ip</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-iptype' %}"><i class="fa fa-plus"></i>{% trans " Add an IP type" %}</a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-iptype' %}"><i class="fa fa-trash"></i> Supprimer un ou plusieurs types d'ip</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-iptype' %}"><i class="fa fa-trash"></i>{% trans " Delete one or several IP types" %}</a>
{% include "machines/aff_iptype.html" with iptype_list=iptype_list %} {% include "machines/aff_iptype.html" with iptype_list=iptype_list %}
<br /> <br />
<br /> <br />

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