8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-23 03:43:12 +00:00

Merge branch 'crans' of https://gitlab.federez.net/federez/re2o into crans

This commit is contained in:
root 2018-07-01 13:55:52 +00:00
commit 093050e245
73 changed files with 2153 additions and 187 deletions

View file

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

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
@ -43,6 +44,7 @@ class ExpiringTokenAuthentication(TokenAuthentication):
) )
utc_now = datetime.datetime.now(datetime.timezone.utc) utc_now = datetime.datetime.now(datetime.timezone.utc)
if token.created < utc_now - token_duration: if token.created < utc_now - token_duration:
raise ValueError('boom')
raise exceptions.AuthenticationFailed(_('Token has expired')) raise exceptions.AuthenticationFailed(_('Token has expired'))
return (token.user, token) return (token.user, token)

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
@ -484,10 +485,12 @@ class UserSerializer(NamespacedHMSerializer):
""" """
access = serializers.BooleanField(source='has_access') access = serializers.BooleanField(source='has_access')
uid = serializers.IntegerField(source='uid_number') uid = serializers.IntegerField(source='uid_number')
email = serializers.CharField(source='get_mail')
class Meta: class Meta:
model = users.User model = users.User
fields = ('name', 'pseudo', 'email', 'school', 'shell', 'comment', fields = ('name', 'pseudo', 'email', 'school', 'shell', 'comment',
'external_mail', 'redirection', 'internal_address',
'state', 'registered', 'telephone', 'solde', 'access', 'state', 'registered', 'telephone', 'solde', 'access',
'end_access', 'uid', 'class_name', 'api_url') 'end_access', 'uid', 'class_name', 'api_url')
extra_kwargs = { extra_kwargs = {
@ -501,10 +504,12 @@ class ClubSerializer(NamespacedHMSerializer):
name = serializers.CharField(source='surname') name = serializers.CharField(source='surname')
access = serializers.BooleanField(source='has_access') access = serializers.BooleanField(source='has_access')
uid = serializers.IntegerField(source='uid_number') uid = serializers.IntegerField(source='uid_number')
email = serializers.CharField(source='get_mail')
class Meta: class Meta:
model = users.Club model = users.Club
fields = ('name', 'pseudo', 'email', 'school', 'shell', 'comment', fields = ('name', 'pseudo', 'email', 'school', 'shell', 'comment',
'external_mail', 'redirection', 'internal_address',
'state', 'registered', 'telephone', 'solde', 'room', 'state', 'registered', 'telephone', 'solde', 'room',
'access', 'end_access', 'administrators', 'members', 'access', 'end_access', 'administrators', 'members',
'mailing', 'uid', 'api_url') 'mailing', 'uid', 'api_url')
@ -518,10 +523,12 @@ class AdherentSerializer(NamespacedHMSerializer):
""" """
access = serializers.BooleanField(source='has_access') access = serializers.BooleanField(source='has_access')
uid = serializers.IntegerField(source='uid_number') uid = serializers.IntegerField(source='uid_number')
email = serializers.CharField(source='get_mail')
class Meta: class Meta:
model = users.Adherent model = users.Adherent
fields = ('name', 'surname', 'pseudo', 'email', 'school', 'shell', fields = ('name', 'surname', 'pseudo', 'email', 'redirection', 'internal_address',
'external_mail', 'school', 'shell',
'comment', 'state', 'registered', 'telephone', 'room', 'comment', 'state', 'registered', 'telephone', 'room',
'solde', 'access', 'end_access', 'uid', 'api_url') 'solde', 'access', 'end_access', 'uid', 'api_url')
extra_kwargs = { extra_kwargs = {
@ -585,6 +592,15 @@ class WhitelistSerializer(NamespacedHMSerializer):
fields = ('user', 'raison', 'date_start', 'date_end', 'active', 'api_url') fields = ('user', 'raison', 'date_start', 'date_end', 'active', 'api_url')
class MailAliasSerializer(NamespacedHMSerializer):
"""Serialize `users.models.MailAlias` objects.
"""
class Meta:
model = users.MailAlias
fields = ('user', 'valeur', 'complete_mail')
# SERVICE REGEN # SERVICE REGEN

View file

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

View file

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

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
@ -91,6 +92,7 @@ router.register_viewset(r'users/listright', views.ListRightViewSet)
router.register_viewset(r'users/shell', views.ShellViewSet, base_name='shell') router.register_viewset(r'users/shell', views.ShellViewSet, base_name='shell')
router.register_viewset(r'users/ban', views.BanViewSet) router.register_viewset(r'users/ban', views.BanViewSet)
router.register_viewset(r'users/whitelist', views.WhitelistViewSet) router.register_viewset(r'users/whitelist', views.WhitelistViewSet)
router.register_viewset(r'users/mailalias', views.MailAliasViewSet)
# SERVICE REGEN # SERVICE REGEN
router.register_viewset(r'services/regen', views.ServiceRegenViewSet, base_name='serviceregen') router.register_viewset(r'services/regen', views.ServiceRegenViewSet, base_name='serviceregen')
# DHCP # DHCP

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
@ -461,6 +462,13 @@ class WhitelistViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.WhitelistSerializer serializer_class = serializers.WhitelistSerializer
class MailAliasViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `users.models.MailAlias` objects.
"""
queryset = users.MailAlias.objects.all()
serializer_class = serializers.MailAliasSerializer
# SERVICE REGEN # SERVICE REGEN

View file

@ -355,27 +355,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_profil = port.get_port_profil
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_profil.vlan_untagged:
DECISION_VLAN = int(port_profil.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_profil.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_profil.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_profil.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 +410,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_profil.radius_mode == 'COMMON' or port_profil.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 +420,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 +443,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,11 +452,13 @@ 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 result, reason = (room_user
.first() .first()
@ -449,6 +478,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:

View file

@ -266,7 +266,6 @@ class ExtensionForm(FormRevMixin, ModelForm):
self.fields['origin'].label = 'Enregistrement A origin' self.fields['origin'].label = 'Enregistrement A origin'
self.fields['origin_v6'].label = 'Enregistrement AAAA origin' self.fields['origin_v6'].label = 'Enregistrement AAAA origin'
self.fields['soa'].label = 'En-tête SOA à utiliser' self.fields['soa'].label = 'En-tête SOA à utiliser'
self.fielss['mail_extension'].label = 'Utilisable comme extension mail'
class DelExtensionForm(FormRevMixin, Form): class DelExtensionForm(FormRevMixin, Form):

View file

@ -641,10 +641,6 @@ class Extension(RevMixin, AclMixin, models.Model):
'SOA', 'SOA',
on_delete=models.CASCADE on_delete=models.CASCADE
) )
mail_extension = models.BooleanField(
default=False,
help_text="Determine si l'extension peut être utilisée comme extension mail interne"
)
class Meta: class Meta:
permissions = ( permissions = (

View file

@ -34,6 +34,7 @@ from .models import (
OptionalTopologie, OptionalTopologie,
GeneralOption, GeneralOption,
Service, Service,
MailContact,
AssoOption, AssoOption,
MailMessageOption, MailMessageOption,
HomeOption HomeOption
@ -65,6 +66,11 @@ class ServiceAdmin(VersionAdmin):
pass pass
class MailContactAdmin(VersionAdmin):
"""Class admin gestion des adresses mail de contact"""
pass
class AssoOptionAdmin(VersionAdmin): class AssoOptionAdmin(VersionAdmin):
"""Class admin options de l'asso""" """Class admin options de l'asso"""
pass pass
@ -86,5 +92,6 @@ admin.site.register(OptionalTopologie, OptionalTopologieAdmin)
admin.site.register(GeneralOption, GeneralOptionAdmin) admin.site.register(GeneralOption, GeneralOptionAdmin)
admin.site.register(HomeOption, HomeOptionAdmin) admin.site.register(HomeOption, HomeOptionAdmin)
admin.site.register(Service, ServiceAdmin) admin.site.register(Service, ServiceAdmin)
admin.site.register(MailContact, MailContactAdmin)
admin.site.register(AssoOption, AssoOptionAdmin) admin.site.register(AssoOption, AssoOptionAdmin)
admin.site.register(MailMessageOption, MailMessageOptionAdmin) admin.site.register(MailMessageOption, MailMessageOptionAdmin)

View file

@ -35,7 +35,8 @@ from .models import (
AssoOption, AssoOption,
MailMessageOption, MailMessageOption,
HomeOption, HomeOption,
Service Service,
MailContact
) )
class EditOptionalUserForm(ModelForm): class EditOptionalUserForm(ModelForm):
@ -233,3 +234,30 @@ class DelServiceForm(Form):
self.fields['services'].queryset = instances self.fields['services'].queryset = instances
else: else:
self.fields['services'].queryset = Service.objects.all() self.fields['services'].queryset = Service.objects.all()
class MailContactForm(ModelForm):
"""Edition, ajout d'adresse de contact"""
class Meta:
model = MailContact
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(MailContactForm, self).__init__(*args, prefix=prefix, **kwargs)
class DelMailContactForm(Form):
"""Suppression d'adresse de contact"""
mailcontacts = forms.ModelMultipleChoiceField(
queryset=MailContact.objects.none(),
label="Enregistrements adresses actuels",
widget=forms.CheckboxSelectMultiple
)
def __init__(self, *args, **kwargs):
instances = kwargs.pop('instances', None)
super(DelMailContactForm, self).__init__(*args, **kwargs)
if instances:
self.fields['mailcontacts'].queryset = instances
else:
self.fields['mailcontacts'].queryset = MailContact.objects.all()

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-26 19:31
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0034_auto_20180416_1120'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='mail_extension',
field=models.CharField(default='@example.org', help_text='Extension principale pour les mails internes', max_length=32),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-29 16:01
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0035_optionaluser_mail_extension'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='mail_accounts',
field=models.BooleanField(default=False, help_text='Activation des comptes mails pour les utilisateurs'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-30 12:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0036_optionaluser_mail_accounts'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='max_mail_alias',
field=models.IntegerField(default=15, help_text="Nombre maximal d'alias pour un utilisateur lambda"),
),
]

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-30 15:27
from __future__ import unicode_literals
from django.db import migrations, models
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
('preferences', '0037_optionaluser_max_mail_alias'),
]
operations = [
migrations.CreateModel(
name='MailContact',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('address', models.EmailField(default='contact@example.org', help_text='Adresse mail de contact', max_length=254)),
('commentary', models.CharField(blank=True, help_text="Description de l'utilisation de l'adresse mail associée", max_length=256, null=True)),
],
options={
'permissions': (('view_mailcontact', 'Peut voir les mails de contact'),),
},
bases=(re2o.mixins.AclMixin, models.Model),
),
]

View file

@ -31,6 +31,7 @@ from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.core.cache import cache from django.core.cache import cache
from django.forms import ValidationError
import cotisations.models import cotisations.models
import machines.models import machines.models
from re2o.mixins import AclMixin from re2o.mixins import AclMixin
@ -102,6 +103,19 @@ class OptionalUser(AclMixin, PreferencesModel):
blank=True, blank=True,
null=True null=True
) )
mail_accounts = models.BooleanField(
default=False,
help_text="Activation des comptes mails pour les utilisateurs"
)
mail_extension = models.CharField(
max_length = 32,
default = "@example.org",
help_text="Extension principale pour les mails internes",
)
max_mail_alias = models.IntegerField(
default = 15,
help_text = "Nombre maximal d'alias pour un utilisateur lambda"
)
class Meta: class Meta:
permissions = ( permissions = (
@ -109,12 +123,17 @@ class OptionalUser(AclMixin, PreferencesModel):
) )
def clean(self): def clean(self):
"""Creation du mode de paiement par solde""" """Clean du model:
Creation du mode de paiement par solde
Vérifie que l'extension mail commence bien par @
"""
if self.user_solde: if self.user_solde:
p = cotisations.models.Paiement.objects.filter(moyen="Solde") p = cotisations.models.Paiement.objects.filter(moyen="Solde")
if not len(p): if not len(p):
c = cotisations.models.Paiement(moyen="Solde") c = cotisations.models.Paiement(moyen="Solde")
c.save() c.save()
if self.mail_extension[0] != "@":
raise ValidationError("L'extension mail doit commencer par un @")
@receiver(post_save, sender=OptionalUser) @receiver(post_save, sender=OptionalUser)
@ -273,6 +292,33 @@ class Service(AclMixin, models.Model):
def __str__(self): def __str__(self):
return str(self.name) return str(self.name)
class MailContact(AclMixin, models.Model):
"""Addresse mail de contact associée à un commentaire descriptif"""
address = models.EmailField(
default = "contact@example.org",
help_text = "Adresse mail de contact"
)
commentary = models.CharField(
blank = True,
null = True,
help_text = "Description de l'utilisation de l'adresse mail associée",
max_length = 256
)
@cached_property
def get_name(self):
return self.address.split("@")[0]
class Meta:
permissions = (
("view_mailcontact", "Peut voir les mails de contact"),
)
def __str__(self):
return(self.address)
class AssoOption(AclMixin, PreferencesModel): class AssoOption(AclMixin, PreferencesModel):
"""Options générales de l'asso : siret, addresse, nom, etc""" """Options générales de l'asso : siret, addresse, nom, etc"""

View file

@ -0,0 +1,45 @@
{% 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 %}
<table class="table table-striped">
<thead>
<tr>
<th>Adresse</th>
<th>Commentaire</th>
<th></th>
</tr>
</thead>
{% for mailcontact in mailcontact_list %}
<tr>
<td>{{ mailcontact.address }}</td>
<td>{{ mailcontact.commentary }}</td>
<td class="text-right">
{% can_edit mailcontact %}
{% include 'buttons/edit.html' with href='preferences:edit-mailcontact' id=mailcontact.id %}
{% acl_end %}
{% include 'buttons/history.html' with href='preferences:history' name='mailcontact' id=mailcontact.id %}
</td>
</tr>
{% endfor %}
</table>

View file

@ -36,20 +36,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</a> </a>
<p> <p>
</p> </p>
<h5>Généralités</h5>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Téléphone obligatoirement requis</th> <th>Téléphone obligatoirement requis</th>
<td>{{ useroptions.is_tel_mandatory }}</td> <td>{{ useroptions.is_tel_mandatory }}</td>
<th>Activation du solde pour les utilisateurs</th> <th>Auto inscription</th>
<td>{{ useroptions.user_solde }}</td> <td>{{ useroptions.self_adhesion }}</td>
</tr> </tr>
<tr> <tr>
<th>Champ gpg fingerprint</th> <th>Champ gpg fingerprint</th>
<td>{{ useroptions.gpg_fingerprint }}</td> <td>{{ useroptions.gpg_fingerprint }}</td>
{% if useroptions.user_solde %} <th>Shell par défaut des utilisateurs</th>
<th>Solde négatif</th> <td>{{ useroptions.shell_default }}</td>
<td>{{ useroptions.solde_negatif }}</td>
{% endif %}
</tr> </tr>
<tr> <tr>
<th>Creations d'adhérents par tous</th> <th>Creations d'adhérents par tous</th>
@ -57,21 +56,37 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>Creations de clubs par tous</th> <th>Creations de clubs par tous</th>
<td>{{ useroptions.all_can_create_club }}</td> <td>{{ useroptions.all_can_create_club }}</td>
</tr> </tr>
{% if useroptions.user_solde %} </table>
<h5>{% if useroptions.user_solde %}<span class="label label-success">Gestion du solde{% else %}<span class="label label-danger">Gesion du solde{% endif%}</span></h5>
<table class="table table-striped">
<tr>
<th>Activation du solde pour les utilisateurs</th>
<td>{{ useroptions.user_solde }}</td>
<th>Solde négatif</th>
<td>{{ useroptions.solde_negatif }}</td>
</tr>
<tr> <tr>
<th>Solde maximum</th> <th>Solde maximum</th>
<td>{{ useroptions.max_solde }}</td> <td>{{ useroptions.max_solde }}</td>
<th>Montant minimal de rechargement en ligne</th> <th>Montant minimal de rechargement en ligne</th>
<td>{{ useroptions.min_online_payment }}</td> <td>{{ useroptions.min_online_payment }}</td>
</tr> </tr>
{% endif %} </table>
<h5>{% if useroptions.mail_accounts %}<span class="label label-success">Comptes mails{% else %}<span class="label label-danger">Comptes mails{% endif%}</span></h5>
<table class="table table-striped">
<tr> <tr>
<th>Auto inscription</th> <th>Gestion des comptes mails</th>
<td>{{ useroptions.self_adhesion }}</td> <td>{{ useroptions.mail_accounts }}</td>
<th>Shell par défaut des utilisateurs</th> <th>Extension mail interne</th>
<td>{{ useroptions.shell_default }}</td> <td>{{ useroptions.mail_extension }}</td>
</tr>
<tr>
<th>Nombre d'alias maximum</th>
<td>{{ useroption.max_mail_alias }}<td>
</tr> </tr>
</table> </table>
<h4>Préférences machines</h4> <h4>Préférences machines</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
@ -215,9 +230,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_create preferences.Service%} {% can_create preferences.Service%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-service' %}"><i class="fa fa-plus"></i> Ajouter un service</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-service' %}"><i class="fa fa-plus"></i> Ajouter un service</a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-services' %}"><i class="fa fa-trash"></i> Supprimer un ou plusieurs service</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-service' %}"><i class="fa fa-trash"></i> Supprimer un ou plusieurs services</a>
{% include "preferences/aff_service.html" with service_list=service_list %} {% include "preferences/aff_service.html" with service_list=service_list %}
<h2>Liste des adresses mail de contact</h2>
{% can_create preferences.MailContact%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-mailcontact' %}"><i class="fa fa-plus"></i>Ajouter une adresse</a>
{% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-mailcontact' %}"><i class="fa fa-trash"></i>Supprimer une ou plusieurs adresses</a>
{% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'HomeOption' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'HomeOption' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
Editer Editer

View file

@ -73,7 +73,14 @@ urlpatterns = [
views.edit_service, views.edit_service,
name='edit-service' name='edit-service'
), ),
url(r'^del_services/$', views.del_services, name='del-services'), url(r'^del_service/$', views.del_service, name='del-service'),
url(r'^add_mailcontact/$', views.add_mailcontact, name='add-mailcontact'),
url(
r'^edit_mailcontact/(?P<mailcontactid>[0-9]+)$',
views.edit_mailcontact,
name='edit-mailcontact'
),
url(r'^del_mailcontact/$', views.del_mailcontact, name='del-mailcontact'),
url( url(
r'^history/(?P<object_name>\w+)/(?P<object_id>[0-9]+)$', r'^history/(?P<object_name>\w+)/(?P<object_id>[0-9]+)$',
re2o.views.history, re2o.views.history,

View file

@ -42,9 +42,10 @@ from reversion import revisions as reversion
from re2o.views import form from re2o.views import form
from re2o.acl import can_create, can_edit, can_delete_set, can_view_all from re2o.acl import can_create, can_edit, can_delete_set, can_view_all
from .forms import ServiceForm, DelServiceForm from .forms import ServiceForm, DelServiceForm, MailContactForm, DelMailContactForm
from .models import ( from .models import (
Service, Service,
MailContact,
OptionalUser, OptionalUser,
OptionalMachine, OptionalMachine,
AssoOption, AssoOption,
@ -71,6 +72,7 @@ def display_options(request):
homeoptions, _created = HomeOption.objects.get_or_create() homeoptions, _created = HomeOption.objects.get_or_create()
mailmessageoptions, _created = MailMessageOption.objects.get_or_create() mailmessageoptions, _created = MailMessageOption.objects.get_or_create()
service_list = Service.objects.all() service_list = Service.objects.all()
mailcontact_list = MailContact.objects.all()
return form({ return form({
'useroptions': useroptions, 'useroptions': useroptions,
'machineoptions': machineoptions, 'machineoptions': machineoptions,
@ -79,7 +81,8 @@ def display_options(request):
'assooptions': assooptions, 'assooptions': assooptions,
'homeoptions': homeoptions, 'homeoptions': homeoptions,
'mailmessageoptions': mailmessageoptions, 'mailmessageoptions': mailmessageoptions,
'service_list': service_list 'service_list': service_list,
'mailcontact_list': mailcontact_list
}, 'preferences/display_preferences.html', request) }, 'preferences/display_preferences.html', request)
@ -169,7 +172,7 @@ def edit_service(request, service_instance, **_kwargs):
@login_required @login_required
@can_delete_set(Service) @can_delete_set(Service)
def del_services(request, instances): def del_service(request, instances):
"""Suppression d'un service de la page d'accueil""" """Suppression d'un service de la page d'accueil"""
services = DelServiceForm(request.POST or None, instances=instances) services = DelServiceForm(request.POST or None, instances=instances)
if services.is_valid(): if services.is_valid():
@ -179,7 +182,7 @@ def del_services(request, instances):
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
services_del.delete() services_del.delete()
reversion.set_user(request.user) reversion.set_user(request.user)
messages.success(request, "Le service a été supprimée") messages.success(request, "Le service a été supprimé")
except ProtectedError: except ProtectedError:
messages.error(request, "Erreur le service\ messages.error(request, "Erreur le service\
suivant %s ne peut être supprimé" % services_del) suivant %s ne peut être supprimé" % services_del)
@ -189,3 +192,75 @@ def del_services(request, instances):
'preferences/preferences.html', 'preferences/preferences.html',
request request
) )
@login_required
@can_create(MailContact)
def add_mailcontact(request):
"""Ajout d'une adresse de contact"""
mailcontact = MailContactForm(
request.POST or None,
request.FILES or None
)
if mailcontact.is_valid():
with transaction.atomic(), reversion.create_revision():
mailcontact.save()
reversion.set_user(request.user)
reversion.set_comment("Création")
messages.success(request, "Cette adresse a été ajoutée")
return redirect(reverse('preferences:display-options'))
return form(
{'preferenceform': mailcontact, 'action_name': 'Ajouter'},
'preferences/preferences.html',
request
)
@login_required
@can_edit(MailContact)
def edit_mailcontact(request, mailcontact_instance, **_kwargs):
"""Edition des adresses de contacte affichées"""
mailcontact = MailContactForm(
request.POST or None,
request.FILES or None,
instance=mailcontact_instance
)
if mailcontact.is_valid():
with transaction.atomic(), reversion.create_revision():
mailcontact.save()
reversion.set_user(request.user)
reversion.set_comment("Modification")
messages.success(request, "Adresse modifiée")
return redirect(reverse('preferences:display-options'))
return form(
{'preferenceform': mailcontact, 'action_name': 'Editer'},
'preferences/preferences.html',
request
)
@login_required
@can_delete_set(MailContact)
def del_mailcontact(request, instances):
"""Suppression d'une adresse de contact"""
mailcontacts = DelMailContactForm(
request.POST or None,
instances=instances
)
if mailcontacts.is_valid():
mailcontacts_dels = mailcontacts.cleaned_data['mailcontacts']
for mailcontacts_del in mailcontacts_dels:
try:
with transaction.atomic(), reversion.create_revision():
mailcontacts_del.delete()
reversion.set_user(request.user)
messages.success(request, "L'adresse a été supprimée")
except ProtectedError:
messages.error(request, "Erreur le service\
suivant %s ne peut être supprimé" % mailcontacts_del)
return redirect(reverse('preferences:display-options'))
return form(
{'preferenceform': mailcontacts, 'action_name': 'Supprimer'},
'preferences/preferences.html',
request
)

0
printer/__init__.py Normal file
View file

3
printer/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
printer/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class PrinterConfig(AppConfig):
name = 'printer'

37
printer/forms.py Normal file
View file

@ -0,0 +1,37 @@
# -*- mode: python; coding: utf-8 -*-
"""printer.forms
Form to add, edit, cancel printer jobs.
Author : Maxime Bombar <bombar@crans.org>.
Date : 29/06/2018
"""
from django import forms
from django.forms import (
Form,
ModelForm,
)
import itertools
from re2o.mixins import FormRevMixin
from .models import (
JobWithOptions,
)
class JobWithOptionsForm(FormRevMixin, ModelForm):
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(JobWithOptionsForm, self).__init__(*args, prefix=prefix, **kwargs)
class Meta:
model = JobWithOptions
fields = [
'file',
'color',
'disposition',
'count',
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-28 18:30
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Dummy',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-28 18:32
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import printer.models
import printer.validators
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('printer', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='JobWithOptions',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to=printer.models.user_printing_path, validators=[printer.validators.FileValidator(allowed_types=['application/pdf'], max_size=10485760)])),
('starttime', models.DateTimeField(auto_now_add=True)),
('endtime', models.DateTimeField(null=True)),
('status', models.CharField(choices=[('Printable', 'Printable'), ('Running', 'Running'), ('Cancelled', 'Cancelled'), ('Finished', 'Finished')], max_length=255)),
('price', models.IntegerField(default=0)),
('format', models.CharField(choices=[('A4', 'A4'), ('A3', 'A4')], default='A4', max_length=255)),
('color', models.CharField(choices=[('Greyscale', 'Greyscale'), ('Color', 'Color')], default='Greyscale', max_length=255)),
('disposition', models.CharField(choices=[('TwoSided', 'Two sided'), ('OneSided', 'One sided'), ('Booklet', 'Booklet')], default='TwoSided', max_length=255)),
('count', models.PositiveIntegerField(default=1)),
('stapling', models.CharField(choices=[('None', 'None'), ('TopLeft', 'One top left'), ('TopRight', 'One top right'), ('LeftSided', 'Two left sided'), ('RightSided', 'Two right sided')], default='None', max_length=255)),
('perforation', models.CharField(choices=[('None', 'None'), ('TwoLeftSidedHoles', 'Two left sided holes'), ('TwoRightSidedHoles', 'Two right sided holes'), ('TwoTopHoles', 'Two top holes'), ('TwoBottomHoles', 'Two bottom holes'), ('FourLeftSidedHoles', 'Four left sided holes'), ('FourRightSidedHoles', 'Four right sided holes')], default='None', max_length=255)),
('printAs', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='print_as_user', to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
bases=(re2o.mixins.RevMixin, models.Model),
),
migrations.RemoveField(
model_name='dummy',
name='user',
),
migrations.DeleteModel(
name='Dummy',
),
]

View file

116
printer/models.py Normal file
View file

@ -0,0 +1,116 @@
# -*- mode: python; coding: utf-8 -*-
"""printer.models
Models of the printer application
Author : Maxime Bombar <bombar@crans.org>.
Date : 29/06/2018
"""
from __future__ import unicode_literals
from django.db import models
from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.template.defaultfilters import filesizeformat
from re2o.mixins import RevMixin
import users.models
from .validators import (
FileValidator,
)
from .settings import (
MAX_PRINTFILE_SIZE,
ALLOWED_TYPES,
)
"""
- ```user_printing_path``` is a function that returns the path of the uploaded file, used with the FileField.
- ```Job``` is the main model of a printer job. His parent is the ```user``` model.
"""
def user_printing_path(instance, filename):
# File will be uploaded to MEDIA_ROOT/printings/user_<id>/<filename>
return 'printings/user_{0}/{1}'.format(instance.user.id, filename)
class JobWithOptions(RevMixin, models.Model):
"""
This is the main model of printer application :
- ```user``` is a ForeignKey to the User Application
- ```file``` is the file to print
- ```starttime``` is the time when the job was launched
- ```endtime``` is the time when the job was stopped.
A job is stopped when it is either finished or cancelled.
- ```status``` can be running, finished or cancelled.
- ```club``` is blank in general. If the job was launched as a club then
it is the id of the club.
- ```price``` is the total price of this printing.
Printing Options :
- ```format``` is the paper format. Example: A4.
- ```color``` is the colorization option. Either Color or Greyscale.
- ```disposition``` is the paper disposition.
- ```count``` is the number of copies to be printed.
- ```stapling``` is the stapling options.
- ```perforations``` is the perforation options.
Parent class : User
"""
STATUS_AVAILABLE = (
('Printable', 'Printable'),
('Running', 'Running'),
('Cancelled', 'Cancelled'),
('Finished', 'Finished')
)
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
file = models.FileField(upload_to=user_printing_path, validators=[FileValidator(allowed_types=ALLOWED_TYPES, max_size=MAX_PRINTFILE_SIZE)])
starttime = models.DateTimeField(auto_now_add=True)
endtime = models.DateTimeField(null=True)
status = models.CharField(max_length=255, choices=STATUS_AVAILABLE)
printAs = models.ForeignKey('users.User', on_delete=models.PROTECT, related_name='print_as_user', null=True)
price = models.IntegerField(default=0)
FORMAT_AVAILABLE = (
('A4', 'A4'),
('A3', 'A4'),
)
COLOR_CHOICES = (
('Greyscale', 'Greyscale'),
('Color', 'Color')
)
DISPOSITIONS_AVAILABLE = (
('TwoSided', 'Two sided'),
('OneSided', 'One sided'),
('Booklet', 'Booklet')
)
STAPLING_OPTIONS = (
('None', 'None'),
('TopLeft', 'One top left'),
('TopRight', 'One top right'),
('LeftSided', 'Two left sided'),
('RightSided', 'Two right sided')
)
PERFORATION_OPTIONS = (
('None', 'None'),
('TwoLeftSidedHoles', 'Two left sided holes'),
('TwoRightSidedHoles', 'Two right sided holes'),
('TwoTopHoles', 'Two top holes'),
('TwoBottomHoles', 'Two bottom holes'),
('FourLeftSidedHoles', 'Four left sided holes'),
('FourRightSidedHoles', 'Four right sided holes')
)
format = models.CharField(max_length=255, choices=FORMAT_AVAILABLE, default='A4')
color = models.CharField(max_length=255, choices=COLOR_CHOICES, default='Greyscale')
disposition = models.CharField(max_length=255, choices=DISPOSITIONS_AVAILABLE, default='TwoSided')
count = models.PositiveIntegerField(default=1)
stapling = models.CharField(max_length=255, choices=STAPLING_OPTIONS, default='None')
perforation = models.CharField(max_length=255, choices=PERFORATION_OPTIONS, default='None')

13
printer/settings.py Normal file
View file

@ -0,0 +1,13 @@
"""printer.settings
Define variables, to be changed into a configuration table.
"""
MAX_PRINTFILE_SIZE = 25 * 1024 * 1024 # 25 MB
ALLOWED_TYPES = ['application/pdf']

View file

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load static %}
{% block title %}Printing interface{% endblock %}
{% block content %}
<h3>{% trans "Failure" %}</h3>
{% endblock %}

View file

@ -0,0 +1,87 @@
{% extends "base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load static %}
{% block title %}Printing interface{% endblock %}
{% block content %}
<form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %}
<h3>{% trans "Printing Menu" %}</h3>
{{ jobform.management_form }}
{% bootstrap_formset_errors jobform %}
<div id="form_set" class="form-group">
{% for job in jobform.forms %}
<div class='file_to_print form-inline'>
{% bootstrap_form job label_class='sr-only' %}
<button class="btn btn-danger btn-sm" id="id_form-0-job-remove" type="button">
<span class="fa fa-times"></span>
</button>
</div>
{% endfor %}
</div>
<input class="btn btn-primary btn-sm" role="button" value="{% trans "Add a file"%}" id="add_one">
{% bootstrap_button action_name button_type="submit" icon="star" %}
</form>
<script type="text/javascript">
var template = `{% bootstrap_form jobform.empty_form label_class='sr-only' %}
<button class="btn btn-danger btn-sm"
id="id_form-__prefix__-job-remove" type="button">
<span class="fa fa-times"></span>
</button>`
function add_job() {
var new_index =
document.getElementsByClassName('file_to_print').length;
document.getElementById('id_form-TOTAL_FORMS').value ++;
var new_job = document.createElement('div');
new_job.className = 'file_to_print form-inline';
new_job.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('form_set').appendChild(new_job);
add_listener_for_id(new_index);
}
function del_job(event){
var job = event.target.parentNode;
job.parentNode.removeChild(job);
document.getElementById('id_form-TOTAL_FORMS').value --;
}
function add_listener_for_id(i){
document.getElementById('id_form-' + i.toString() + '-job-remove')
.addEventListener("click", function(event){
var job = event.target.parentNode;
job.parentNode.removeChild(job);
document.getElementById('id_form-TOTAL_FORMS').value --;
}
)
}
// Add events manager when DOM is fully loaded
document.addEventListener(
"DOMContentLoaded",
function() {
document.getElementById("add_one")
.addEventListener("click", add_job, true);
document.getElementById('id_form-0-job-remove')
.addEventListener("click", function(event){
var job = event.target.parentNode;
job.parentNode.removeChild(job);
document.getElementById('id_form-TOTAL_FORMS').value --;
}
)
}
);
</script>
{% endblock %}

View file

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load static %}
{% block title %}Printing interface{% endblock %}
{% block content %}
<h3>{% trans "Success" %}</h3>
{% endblock %}

3
printer/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

17
printer/urls.py Normal file
View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""printer.urls
The defined URLs for the printer app
Author : Maxime Bombar <bombar@crans.org>.
Date : 29/06/2018
"""
from __future__ import unicode_literals
from django.conf.urls import url
import re2o
from . import views
urlpatterns = [
url(r'^new_job/$', views.new_job, name="new-job"),
url(r'^success/$', views.success, name="success"),
]

72
printer/validators.py Normal file
View file

@ -0,0 +1,72 @@
# -*- mode: python; coding: utf-8 -*-
"""printer.validators
Custom validators useful for printer application.
Author : Maxime Bombar <bombar@crans.org>.
Date : 29/06/2018
"""
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from django.template.defaultfilters import filesizeformat
from django.utils.deconstruct import deconstructible
import mimetypes
@deconstructible
class FileValidator(object):
"""
Custom validator for files. It checks the size and mimetype.
Parameters:
* ```allowed_types``` is an iterable of allowed mimetypes. Example: ['application/pdf'] for a pdf file.
* ```max_size``` is the maximum size allowed in bytes. Example: 25*1024*1024 for 25 MB.
Usage example:
class UploadModel(models.Model):
file = fileField(..., validators=FileValidator(allowed_types = ['application/pdf'], max_size=25*1024*1024))
"""
def __init__(self, *args, **kwargs):
"""
Initialize the custom validator.
By default, all types and size are allowed.
"""
self.allowed_types = kwargs.pop('allowed_types', None)
self.max_size = kwargs.pop('max_size', None)
def __call__(self, value):
"""
Check the type and size.
"""
type_message = _("MIME type '%(type)s' is not valid. Please, use one of these types: %(allowed_types)s.")
type_code = 'invalidType'
oversized_message = _('The current file size is %(size)s. The maximum file size is %(max_size)s.')
oversized_code = 'oversized'
mimetype = mimetypes.guess_type(value.name)[0]
if self.allowed_types and not (mimetype in self.allowed_types):
type_params = {
'type': mimetype,
'allowed_types': ', '.join(self.allowed_types),
}
raise ValidationError(type_message, code=type_code, params=type_params)
filesize = len(value)
if self.max_size and filesize > self.max_size:
oversized_params = {
'size': '{}'.format(filesizeformat(filesize)),
'max_size': '{}'.format(filesizeformat(self.max_size)),
}
raise ValidationError(oversized_message, code=oversized_code, params=oversized_params)

55
printer/views.py Normal file
View file

@ -0,0 +1,55 @@
# -*- mode: python; coding: utf-8 -*-
"""printer.views
The views for the printer app
Author : Maxime Bombar <bombar@crans.org>.
Date : 29/06/2018
"""
from __future__ import unicode_literals
from django.urls import reverse
from django.shortcuts import render, redirect
from django.forms import modelformset_factory, formset_factory
from django.contrib.auth.decorators import login_required
from re2o.views import form
from users.models import User
from . import settings
from .forms import (
JobWithOptionsForm,
)
@login_required
def new_job(request):
"""
View to create a new printing job
"""
job_formset = formset_factory(JobWithOptionsForm)(
request.POST or None, request.FILES or None,
)
if job_formset.is_valid():
for job in job_formset:
job = job.save(commit=False)
job.user=request.user
job.status='Printable'
job.save()
return redirect(reverse(
'printer:success',
))
return form(
{
'jobform': job_formset,
'action_name': "Print",
},
'printer/newjob.html',
request
)
def success(request):
return form(
{},
'printer/success.html',
request
)

View file

@ -0,0 +1,52 @@
{% extends "re2o/sidebar.html" %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load bootstrap3 %}
{% load i18n %}
{% block title %}{% trans "Contact" %}{% endblock %}
{% block content %}
<h2>{% blocktrans %}Contacter l'association {{asso_name}}{% endblocktrans %}</h2>
</br>
{% for contact in contacts %}
<div class="panel panel-info">
<div class="panel-heading"><h4>{{ contact.get_name }}</h4></div>
<div class="panel-body">
<div class="row">
<div class="col-sm-9">{{ contact.commentary}}</div>
<div class="col-sm-3"><a href="mailto:{{ contact.address }}">{{ contact.address }}</a></div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}

View file

@ -121,6 +121,7 @@ MODEL_NAME = {
'OptionalTopologie': preferences.models.OptionalTopologie, 'OptionalTopologie': preferences.models.OptionalTopologie,
'GeneralOption': preferences.models.GeneralOption, 'GeneralOption': preferences.models.GeneralOption,
'preferences.Service': preferences.models.Service, 'preferences.Service': preferences.models.Service,
'preferences.MailContact': preferences.models.MailContact,
'AssoOption': preferences.models.AssoOption, 'AssoOption': preferences.models.AssoOption,
'MailMessageOption': preferences.models.MailMessageOption, 'MailMessageOption': preferences.models.MailMessageOption,
# topologie # topologie
@ -133,9 +134,9 @@ MODEL_NAME = {
'Room': topologie.models.Room, 'Room': topologie.models.Room,
'Building': topologie.models.Building, 'Building': topologie.models.Building,
'SwitchBay': topologie.models.SwitchBay, 'SwitchBay': topologie.models.SwitchBay,
'PortProfile': topologie.models.PortProfile,
# users # users
'User': users.models.User, 'User': users.models.User,
'Mail': users.models.Mail,
'MailAlias': users.models.MailAlias, 'MailAlias': users.models.MailAlias,
'Adherent': users.models.Adherent, 'Adherent': users.models.Adherent,
'Club': users.models.Club, 'Club': users.models.Club,

View file

@ -71,6 +71,7 @@ urlpatterns = [
r'^preferences/', r'^preferences/',
include('preferences.urls', namespace='preferences') include('preferences.urls', namespace='preferences')
), ),
url(r'^printer/', include('printer.urls', namespace='printer')),
] ]
# Add debug_toolbar URLs if activated # Add debug_toolbar URLs if activated
if 'debug_toolbar' in settings.INSTALLED_APPS: if 'debug_toolbar' in settings.INSTALLED_APPS:

View file

@ -43,6 +43,7 @@ from django.views.decorators.cache import cache_page
import preferences import preferences
from preferences.models import ( from preferences.models import (
Service, Service,
MailContact,
GeneralOption, GeneralOption,
AssoOption, AssoOption,
HomeOption HomeOption
@ -86,6 +87,7 @@ HISTORY_BIND = {
'users': { 'users': {
'user': users.models.User, 'user': users.models.User,
'ban': users.models.Ban, 'ban': users.models.Ban,
'mailalias': users.models.MailAlias,
'whitelist': users.models.Whitelist, 'whitelist': users.models.Whitelist,
'school': users.models.School, 'school': users.models.School,
'listright': users.models.ListRight, 'listright': users.models.ListRight,
@ -94,6 +96,7 @@ HISTORY_BIND = {
}, },
'preferences': { 'preferences': {
'service': preferences.models.Service, 'service': preferences.models.Service,
'mailcontact': preferences.models.MailContact,
}, },
'cotisations': { 'cotisations': {
'facture': cotisations.models.Facture, 'facture': cotisations.models.Facture,
@ -111,6 +114,7 @@ HISTORY_BIND = {
'accesspoint': topologie.models.AccessPoint, 'accesspoint': topologie.models.AccessPoint,
'switchbay': topologie.models.SwitchBay, 'switchbay': topologie.models.SwitchBay,
'building': topologie.models.Building, 'building': topologie.models.Building,
'portprofile': topologie.models.PortProfile,
}, },
'machines': { 'machines': {
'machine': machines.models.Machine, 'machine': machines.models.Machine,
@ -229,6 +233,21 @@ def about_page(request):
} }
) )
def contact_page(request):
"""The view for the contact page
Send all the objects MailContact
"""
address = MailContact.objects.all()
return render(
request,
"re2o/contact.html",
{
'contacts': address,
'asso_name': AssoOption.objects.first().name
}
)
def handler500(request): def handler500(request):
"""The handler view for a 500 error""" """The handler view for a 500 error"""

View file

@ -262,9 +262,9 @@ def search_single_word(word, filters, user,
) | Q( ) | Q(
related__switch__interface__domain__name__icontains=word related__switch__interface__domain__name__icontains=word
) | Q( ) | Q(
radius__icontains=word custom_profile__name__icontains=word
) | Q( ) | Q(
vlan_force__name__icontains=word custom_profile__profil_default__icontains=word
) | Q( ) | Q(
details__icontains=word details__icontains=word
) )

View file

@ -108,7 +108,6 @@ footer a {
overflow-y: visible; overflow-y: visible;
} }
/* For tables with long text in cells */ /* For tables with long text in cells */
.table.long_text{ .table.long_text{
@ -124,3 +123,42 @@ td.long_text{
th.long_text{ th.long_text{
width: 60%; width: 60%;
} }
/* style for the user page */
.dashboard_container{
margin-top: 30px;
margin-bottom: 20px;
}
.panel-heading.dashboard{
text-align: center;
}
.panel-body.dashboard{
text-align: center;
height: 60px;
vertical-align:middle;
}
#grad_red {
background: red; /* For browsers that do not support gradients */
background: linear-gradient(#ff6363, #fefefe); /* Standard syntax (must be last) */
}
#grad_green {
background: green; /* For browsers that do not support gradients */
background: linear-gradient(#C8DD58,#4FB64A); /* Standard syntax (must be last) */
}
#grad_grey {
background: gray; /* For browsers that do not support gradients */
background: linear-gradient(#d4d4ff, #fefefe); /* Standard syntax (must be last) */
}
#grad_machines{
background: green;
background: linear-gradient(#c266e0,#fefefe)
}

View file

@ -112,6 +112,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</ul> </ul>
</li> </li>
{% acl_end %} {% acl_end %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><i class="glyphicon glyphicon-print"></i> Printer<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url "printer:new-job" %}"><i class="fa fa-print"></i> {% trans "Print" %}</a></li>
</ul>
</li>
{% can_view_app logs %} {% can_view_app logs %}
<li><a href="{% url "logs:index" %}"><i class="fa fa-chart-area"></i> {% trans "Statistics" %}</a></li> <li><a href="{% url "logs:index" %}"><i class="fa fa-chart-area"></i> {% trans "Statistics" %}</a></li>
{% acl_end %} {% acl_end %}
@ -124,8 +130,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</a> </a>
</li> </li>
{% acl_end %} {% acl_end %}
<li> <li class="dropdown">
<a href="{% url 'about' %}"><i class="fa fa-info-circle"></i> {% trans "About" %}</a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><i class="fas fa-info"></i> {% trans "Info" %}<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'about' %}"><i class="fa fa-info-circle"></i> {% trans "About" %}</a></li>
<li><a href="{% url 'contact' %}"><i class="fas fa-at"></i> {% trans "Contact" %}</a></li>
</ul>
</li> </li>
{% if not request.user.is_authenticated %} {% if not request.user.is_authenticated %}
{% if var_sa %} {% if var_sa %}

View file

@ -38,7 +38,8 @@ from .models import (
ConstructorSwitch, ConstructorSwitch,
AccessPoint, AccessPoint,
SwitchBay, SwitchBay,
Building Building,
PortProfile,
) )
@ -86,6 +87,9 @@ class BuildingAdmin(VersionAdmin):
"""Administration d'un batiment""" """Administration d'un batiment"""
pass pass
class PortProfileAdmin(VersionAdmin):
"""Administration of a port profile"""
pass
admin.site.register(Port, PortAdmin) admin.site.register(Port, PortAdmin)
admin.site.register(AccessPoint, AccessPointAdmin) admin.site.register(AccessPoint, AccessPointAdmin)
@ -96,3 +100,4 @@ admin.site.register(ModelSwitch, ModelSwitchAdmin)
admin.site.register(ConstructorSwitch, ConstructorSwitchAdmin) admin.site.register(ConstructorSwitch, ConstructorSwitchAdmin)
admin.site.register(Building, BuildingAdmin) admin.site.register(Building, BuildingAdmin)
admin.site.register(SwitchBay, SwitchBayAdmin) admin.site.register(SwitchBay, SwitchBayAdmin)
admin.site.register(PortProfile, PortProfileAdmin)

View file

@ -35,6 +35,7 @@ from __future__ import unicode_literals
from django import forms from django import forms
from django.forms import ModelForm from django.forms import ModelForm
from django.db.models import Prefetch from django.db.models import Prefetch
from django.utils.translation import ugettext_lazy as _
from machines.models import Interface from machines.models import Interface
from machines.forms import ( from machines.forms import (
@ -53,6 +54,7 @@ from .models import (
AccessPoint, AccessPoint,
SwitchBay, SwitchBay,
Building, Building,
PortProfile,
) )
@ -78,8 +80,8 @@ class EditPortForm(FormRevMixin, ModelForm):
optimiser le temps de chargement avec select_related (vraiment optimiser le temps de chargement avec select_related (vraiment
lent sans)""" lent sans)"""
class Meta(PortForm.Meta): class Meta(PortForm.Meta):
fields = ['room', 'related', 'machine_interface', 'radius', fields = ['room', 'related', 'machine_interface', 'custom_profile',
'vlan_force', 'details'] 'state', 'details']
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__)
@ -107,8 +109,8 @@ class AddPortForm(FormRevMixin, ModelForm):
'room', 'room',
'machine_interface', 'machine_interface',
'related', 'related',
'radius', 'custom_profile',
'vlan_force', 'state',
'details' 'details'
] ]
@ -262,3 +264,16 @@ class EditBuildingForm(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(EditBuildingForm, self).__init__(*args, prefix=prefix, **kwargs) super(EditBuildingForm, self).__init__(*args, prefix=prefix, **kwargs)
class EditPortProfileForm(FormRevMixin, ModelForm):
"""Form to edit a port profile"""
class Meta:
model = PortProfile
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditPortProfileForm, self).__init__(*args,
prefix=prefix,
**kwargs)

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-26 16:37
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
('machines', '0082_auto_20180525_2209'),
('topologie', '0060_server'),
]
operations = [
migrations.CreateModel(
name='PortProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('profil_default', models.CharField(blank=True, choices=[('room', 'room'), ('nothing', 'nothing'), ('accespoint', 'accesspoint'), ('uplink', 'uplink'), ('asso_machine', 'asso_machine')], max_length=32, null=True, unique=True, verbose_name='profil default')),
('radius_type', models.CharField(choices=[('NO', 'NO'), ('802.1X', '802.1X'), ('MAC-radius', 'MAC-radius')], max_length=32, verbose_name='RADIUS type')),
('radius_mode', models.CharField(choices=[('STRICT', 'STRICT'), ('COMMON', 'COMMON')], default='COMMON', max_length=32, verbose_name='RADIUS mode')),
('speed', models.CharField(choices=[('10-half', '10-half'), ('100-half', '100-half'), ('10-full', '10-full'), ('100-full', '100-full'), ('1000-full', '1000-full'), ('auto', 'auto'), ('auto-10', 'auto-10'), ('auto-100', 'auto-100')], default='auto', help_text='Mode de transmission et vitesse du port', max_length=32, verbose_name='Speed')),
('mac_limit', models.IntegerField(blank=True, help_text='Limit du nombre de mac sur le port', null=True, verbose_name='Mac limit')),
('flow_control', models.BooleanField(default=False, help_text='Gestion des débits', verbose_name='Flow control')),
('dhcp_snooping', models.BooleanField(default=False, help_text='Protection dhcp pirate', verbose_name='Dhcp snooping')),
('dhcpv6_snooping', models.BooleanField(default=False, help_text='Protection dhcpv6 pirate', verbose_name='Dhcpv6 snooping')),
('arp_protect', models.BooleanField(default=False, help_text="Verification assignation de l'IP par dhcp", verbose_name='Arp protect')),
('ra_guard', models.BooleanField(default=False, help_text='Protection contre ra pirate', verbose_name='Ra guard')),
('loop_protect', models.BooleanField(default=False, help_text='Protection contre les boucles', verbose_name='Loop Protect')),
('vlan_tagged', models.ManyToManyField(blank=True, related_name='vlan_tagged', to='machines.Vlan', verbose_name='VLAN(s) tagged')),
('vlan_untagged', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vlan_untagged', to='machines.Vlan', verbose_name='VLAN untagged')),
],
options={
'verbose_name': 'Port profile',
'permissions': (('view_port_profile', 'Can view a port profile object'),),
'verbose_name_plural': 'Port profiles',
},
bases=(re2o.mixins.AclMixin, re2o.mixins.RevMixin, models.Model),
),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-26 23:23
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('topologie', '0061_portprofile'),
]
operations = [
migrations.AlterField(
model_name='portprofile',
name='radius_mode',
field=models.CharField(choices=[('STRICT', 'STRICT'), ('COMMON', 'COMMON')], default='COMMON', help_text="En cas d'auth par mac, auth common ou strcit sur le port", max_length=32, verbose_name='RADIUS mode'),
),
migrations.AlterField(
model_name='portprofile',
name='radius_type',
field=models.CharField(choices=[('NO', 'NO'), ('802.1X', '802.1X'), ('MAC-radius', 'MAC-radius')], help_text="Choix du type d'authentification radius : non actif, mac ou 802.1X", max_length=32, verbose_name='RADIUS type'),
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-28 07:49
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('topologie', '0062_auto_20180627_0123'),
]
operations = [
migrations.AddField(
model_name='port',
name='custom_profil',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='topologie.PortProfile'),
),
]

View file

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-12-31 19:53
from __future__ import unicode_literals
from django.db import migrations
def transfer_profil(apps, schema_editor):
db_alias = schema_editor.connection.alias
port = apps.get_model("topologie", "Port")
profil = apps.get_model("topologie", "PortProfile")
vlan = apps.get_model("machines", "Vlan")
port_list = port.objects.using(db_alias).all()
profil_nothing = profil.objects.using(db_alias).create(name='nothing', profil_default='nothing', radius_type='NO')
profil_uplink = profil.objects.using(db_alias).create(name='uplink', profil_default='uplink', radius_type='NO')
profil_machine = profil.objects.using(db_alias).create(name='asso_machine', profil_default='asso_machine', radius_type='NO')
profil_room = profil.objects.using(db_alias).create(name='room', profil_default='room', radius_type='NO')
profil_borne = profil.objects.using(db_alias).create(name='accesspoint', profil_default='accesspoint', radius_type='NO')
for vlan_instance in vlan.objects.using(db_alias).all():
if port.objects.using(db_alias).filter(vlan_force=vlan_instance):
custom_profil = profil.objects.using(db_alias).create(name='vlan-force-' + str(vlan_instance.vlan_id), radius_type='NO', vlan_untagged=vlan_instance)
port.objects.using(db_alias).filter(vlan_force=vlan_instance).update(custom_profil=custom_profil)
if port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').count() > port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').count() and port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').count() > port.objects.using(db_alias).filter(room__isnull=False).filter(radius='COMMON').count():
profil_room.radius_type = 'MAC-radius'
profil_room.radius_mode = 'STRICT'
common_profil = profil.objects.using(db_alias).create(name='mac-radius-common', radius_type='MAC-radius', radius_mode='COMMON')
no_rad_profil = profil.objects.using(db_alias).create(name='no-radius', radius_type='NO')
port.objects.using(db_alias).filter(room__isnull=False).filter(radius='COMMON').update(custom_profil=common_profil)
port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').update(custom_profil=no_rad_profil)
elif port.objects.using(db_alias).filter(room__isnull=False).filter(radius='COMMON').count() > port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').count() and port.objects.using(db_alias).filter(room__isnull=False).filter(radius='COMMON').count() > port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').count():
profil_room.radius_type = 'MAC-radius'
profil_room.radius_mode = 'COMMON'
strict_profil = profil.objects.using(db_alias).create(name='mac-radius-strict', radius_type='MAC-radius', radius_mode='STRICT')
no_rad_profil = profil.objects.using(db_alias).create(name='no-radius', radius_type='NO')
port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').update(custom_profil=strict_profil)
port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').update(custom_profil=no_rad_profil)
else:
strict_profil = profil.objects.using(db_alias).create(name='mac-radius-strict', radius_type='MAC-radius', radius_mode='STRICT')
common_profil = profil.objects.using(db_alias).create(name='mac-radius-common', radius_type='MAC-radius', radius_mode='COMMON')
port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').update(custom_profil=strict_profil)
port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').update(custom_profil=common_profil)
profil_room.save()
class Migration(migrations.Migration):
dependencies = [
('topologie', '0063_port_custom_profil'),
]
operations = [
migrations.RunPython(transfer_profil),
]

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-30 15:03
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('topologie', '0064_createprofil'),
]
operations = [
migrations.RemoveField(
model_name='port',
name='radius',
),
migrations.RemoveField(
model_name='port',
name='vlan_force',
),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-30 16:55
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('topologie', '0065_auto_20180630_1703'),
]
operations = [
migrations.AddField(
model_name='port',
name='state',
field=models.BooleanField(default=True, help_text='Etat du port Actif', verbose_name='Etat du port Actif'),
),
migrations.AlterField(
model_name='portprofile',
name='profil_default',
field=models.CharField(blank=True, choices=[('room', 'room'), ('accespoint', 'accesspoint'), ('uplink', 'uplink'), ('asso_machine', 'asso_machine'), ('nothing', 'nothing')], max_length=32, null=True, unique=True, verbose_name='profil default'),
),
]

View file

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-30 22:16
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('topologie', '0066_auto_20180630_1855'),
]
operations = [
migrations.RenameField(
model_name='port',
old_name='custom_profil',
new_name='custom_profile',
),
migrations.AlterField(
model_name='port',
name='state',
field=models.BooleanField(default=True, help_text='Port state Active', verbose_name='Port State Active'),
),
migrations.AlterField(
model_name='portprofile',
name='arp_protect',
field=models.BooleanField(default=False, help_text='Check if ip is dhcp assigned', verbose_name='Arp protect'),
),
migrations.AlterField(
model_name='portprofile',
name='dhcp_snooping',
field=models.BooleanField(default=False, help_text='Protect against rogue dhcp', verbose_name='Dhcp snooping'),
),
migrations.AlterField(
model_name='portprofile',
name='dhcpv6_snooping',
field=models.BooleanField(default=False, help_text='Protect against rogue dhcpv6', verbose_name='Dhcpv6 snooping'),
),
migrations.AlterField(
model_name='portprofile',
name='flow_control',
field=models.BooleanField(default=False, help_text='Flow control', verbose_name='Flow control'),
),
migrations.AlterField(
model_name='portprofile',
name='loop_protect',
field=models.BooleanField(default=False, help_text='Protect again loop', verbose_name='Loop Protect'),
),
migrations.AlterField(
model_name='portprofile',
name='mac_limit',
field=models.IntegerField(blank=True, help_text='Limit of mac-address on this port', null=True, verbose_name='Mac limit'),
),
migrations.AlterField(
model_name='portprofile',
name='ra_guard',
field=models.BooleanField(default=False, help_text='Protect against rogue ra', verbose_name='Ra guard'),
),
migrations.AlterField(
model_name='portprofile',
name='radius_mode',
field=models.CharField(choices=[('STRICT', 'STRICT'), ('COMMON', 'COMMON')], default='COMMON', help_text='In case of mac-auth : mode common or strict on this port', max_length=32, verbose_name='RADIUS mode'),
),
migrations.AlterField(
model_name='portprofile',
name='radius_type',
field=models.CharField(choices=[('NO', 'NO'), ('802.1X', '802.1X'), ('MAC-radius', 'MAC-radius')], help_text='Type of radius auth : inactive, mac-address or 802.1X', max_length=32, verbose_name='RADIUS type'),
),
migrations.AlterField(
model_name='portprofile',
name='speed',
field=models.CharField(choices=[('10-half', '10-half'), ('100-half', '100-half'), ('10-full', '10-full'), ('100-full', '100-full'), ('1000-full', '1000-full'), ('auto', 'auto'), ('auto-10', 'auto-10'), ('auto-100', 'auto-100')], default='auto', help_text='Port speed limit', max_length=32, verbose_name='Speed'),
),
]

View file

@ -46,6 +46,7 @@ from django.dispatch import receiver
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import IntegrityError from django.db import IntegrityError
from django.db import transaction from django.db import transaction
from django.utils.translation import ugettext_lazy as _
from reversion import revisions as reversion from reversion import revisions as reversion
from machines.models import Machine, regen from machines.models import Machine, regen
@ -361,12 +362,6 @@ class Port(AclMixin, RevMixin, models.Model):
de forcer un port sur un vlan particulier. S'additionne à la politique de forcer un port sur un vlan particulier. S'additionne à la politique
RADIUS""" RADIUS"""
PRETTY_NAME = "Port de switch" PRETTY_NAME = "Port de switch"
STATES = (
('NO', 'NO'),
('STRICT', 'STRICT'),
('BLOQ', 'BLOQ'),
('COMMON', 'COMMON'),
)
switch = models.ForeignKey( switch = models.ForeignKey(
'Switch', 'Switch',
@ -392,13 +387,17 @@ class Port(AclMixin, RevMixin, models.Model):
blank=True, blank=True,
related_name='related_port' related_name='related_port'
) )
radius = models.CharField(max_length=32, choices=STATES, default='NO') custom_profile = models.ForeignKey(
vlan_force = models.ForeignKey( 'PortProfile',
'machines.Vlan', on_delete=models.PROTECT,
on_delete=models.SET_NULL,
blank=True, blank=True,
null=True null=True
) )
state = models.BooleanField(
default=True,
help_text='Port state Active',
verbose_name=_("Port State Active")
)
details = models.CharField(max_length=255, blank=True) details = models.CharField(max_length=255, blank=True)
class Meta: class Meta:
@ -407,6 +406,34 @@ class Port(AclMixin, RevMixin, models.Model):
("view_port", "Peut voir un objet port"), ("view_port", "Peut voir un objet port"),
) )
@cached_property
def get_port_profil(self):
"""Return the config profil for this port
:returns: the profile of self (port)"""
def profil_or_nothing(profil):
port_profil = PortProfile.objects.filter(profil_default=profil).first()
if port_profil:
return port_profil
else:
nothing = PortProfile.objects.filter(profil_default='nothing').first()
if not nothing:
nothing = PortProfile.objects.create(profil_default='nothing', name='nothing', radius_type='NO')
return nothing
if self.custom_profile:
return self.custom_profile
elif self.related:
return profil_or_nothing('uplink')
elif self.machine_interface:
if hasattr(self.machine_interface.machine, 'accesspoint'):
return profil_or_nothing('access_point')
else:
return profil_or_nothing('asso_machine')
elif self.room:
return profil_or_nothing('room')
else:
return profil_or_nothing('nothing')
@classmethod @classmethod
def get_instance(cls, portid, *_args, **kwargs): def get_instance(cls, portid, *_args, **kwargs):
return (cls.objects return (cls.objects
@ -484,6 +511,135 @@ class Room(AclMixin, RevMixin, models.Model):
return self.name return self.name
class PortProfile(AclMixin, RevMixin, models.Model):
"""Contains the information of the ports' configuration for a switch"""
TYPES = (
('NO', 'NO'),
('802.1X', '802.1X'),
('MAC-radius', 'MAC-radius'),
)
MODES = (
('STRICT', 'STRICT'),
('COMMON', 'COMMON'),
)
SPEED = (
('10-half', '10-half'),
('100-half', '100-half'),
('10-full', '10-full'),
('100-full', '100-full'),
('1000-full', '1000-full'),
('auto', 'auto'),
('auto-10', 'auto-10'),
('auto-100', 'auto-100'),
)
PROFIL_DEFAULT= (
('room', 'room'),
('accespoint', 'accesspoint'),
('uplink', 'uplink'),
('asso_machine', 'asso_machine'),
('nothing', 'nothing'),
)
name = models.CharField(max_length=255, verbose_name=_("Name"))
profil_default = models.CharField(
max_length=32,
choices=PROFIL_DEFAULT,
blank=True,
null=True,
unique=True,
verbose_name=_("profil default")
)
vlan_untagged = models.ForeignKey(
'machines.Vlan',
related_name='vlan_untagged',
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_("VLAN untagged")
)
vlan_tagged = models.ManyToManyField(
'machines.Vlan',
related_name='vlan_tagged',
blank=True,
verbose_name=_("VLAN(s) tagged")
)
radius_type = models.CharField(
max_length=32,
choices=TYPES,
help_text="Type of radius auth : inactive, mac-address or 802.1X",
verbose_name=_("RADIUS type")
)
radius_mode = models.CharField(
max_length=32,
choices=MODES,
default='COMMON',
help_text="In case of mac-auth : mode common or strict on this port",
verbose_name=_("RADIUS mode")
)
speed = models.CharField(
max_length=32,
choices=SPEED,
default='auto',
help_text='Port speed limit',
verbose_name=_("Speed")
)
mac_limit = models.IntegerField(
null=True,
blank=True,
help_text='Limit of mac-address on this port',
verbose_name=_("Mac limit")
)
flow_control = models.BooleanField(
default=False,
help_text='Flow control',
verbose_name=_("Flow control")
)
dhcp_snooping = models.BooleanField(
default=False,
help_text='Protect against rogue dhcp',
verbose_name=_("Dhcp snooping")
)
dhcpv6_snooping = models.BooleanField(
default=False,
help_text='Protect against rogue dhcpv6',
verbose_name=_("Dhcpv6 snooping")
)
arp_protect = models.BooleanField(
default=False,
help_text='Check if ip is dhcp assigned',
verbose_name=_("Arp protect")
)
ra_guard = models.BooleanField(
default=False,
help_text='Protect against rogue ra',
verbose_name=_("Ra guard")
)
loop_protect = models.BooleanField(
default=False,
help_text='Protect again loop',
verbose_name=_("Loop Protect")
)
class Meta:
permissions = (
("view_port_profile", _("Can view a port profile object")),
)
verbose_name = _("Port profile")
verbose_name_plural = _("Port profiles")
security_parameters_fields = ['loop_protect', 'ra_guard', 'arp_protect', 'dhcpv6_snooping', 'dhcp_snooping', 'flow_control']
@cached_property
def security_parameters_enabled(self):
return [parameter for parameter in self.security_parameters_fields if getattr(self, parameter)]
@cached_property
def security_parameters_as_str(self):
return ','.join(self.security_parameters_enabled)
def __str__(self):
return self.name
@receiver(post_save, sender=AccessPoint) @receiver(post_save, sender=AccessPoint)
def ap_post_save(**_kwargs): def ap_post_save(**_kwargs):
"""Regeneration des noms des bornes vers le controleur""" """Regeneration des noms des bornes vers le controleur"""

View file

@ -32,8 +32,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>{% include "buttons/sort.html" with prefix='port' col='room' text='Room' %}</th> <th>{% include "buttons/sort.html" with prefix='port' col='room' text='Room' %}</th>
<th>{% include "buttons/sort.html" with prefix='port' col='interface' text='Interface machine' %}</th> <th>{% include "buttons/sort.html" with prefix='port' col='interface' text='Interface machine' %}</th>
<th>{% include "buttons/sort.html" with prefix='port' col='related' text='Related' %}</th> <th>{% include "buttons/sort.html" with prefix='port' col='related' text='Related' %}</th>
<th>{% include "buttons/sort.html" with prefix='port' col='radius' text='Radius' %}</th> <th>Etat du port</th>
<th>{% include "buttons/sort.html" with prefix='port' col='vlan' text='Vlan forcé' %}</th> <th>Profil du port</th>
<th>Détails</th> <th>Détails</th>
<th></th> <th></th>
</tr> </tr>
@ -66,8 +66,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% acl_end %} {% acl_end %}
{% endif %} {% endif %}
</td> </td>
<td>{{ port.radius }}</td> <td>{% if port.state %} <i class="text-success">Actif</i>{% else %}<i class="text-danger">Désactivé</i>{% endif %}</td>
<td>{% if not port.vlan_force %}Aucun{% else %}{{ port.vlan_force }}{% endif %}</td> <td>{% if not port.custom_profile %}<u>Par défaut</u> : {% endif %}{{port.get_port_profil}}</td>
<td>{{ port.details }}</td> <td>{{ port.details }}</td>
<td class="text-right"> <td class="text-right">
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'port' port.pk %}"> <a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'port' port.pk %}">

View file

@ -0,0 +1,85 @@
{% 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 Gabriel Détraz
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 %}
<div class="table-responsive">
{% if port_profile_list.paginator %}
{% include "pagination.html" with list=port_profile_list %}
{% endif %}
<thead>
<table class="table table-striped">
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Default for" %}</th>
<th>{% trans "VLANs" %}</th>
<th>{% trans "RADIUS settings" %}</th>
<th>{% trans "Speed" %}</th>
<th>{% trans "Mac address limit" %}</th>
<th>{% trans "Security" %}</th>
<th></th>
</tr>
</thead>
{% for port_profile in port_profile_list %}
<tr>
<td>{{port_profile.name}}</td>
<td>{{port_profile.profil_default}}</td>
<td>
{% if port_profile.vlan_untagged %}
<b>Untagged : </b>{{port_profile.vlan_untagged}}
<br>
{% endif %}
{% if port_profile.vlan_tagged.all %}
<b>Tagged : </b>{{port_profile.vlan_tagged.all|join:", "}}
{% endif %}
</td>
<td>
<b>Type : </b>{{port_profile.radius_type}}
{% if port_profile.radius_type == "MAC-radius" %}
<br>
<b>Mode : </b>{{port_profile.radius_mode}}</td>
{% endif %}
<td>{{port_profile.speed}}</td>
<td>{{port_profile.mac_limit}}</td>
<td>{{port_profile.security_parameters_enabled|join:"<br>"}}</td>
<td class="text-right">
{% include 'buttons/history.html' with href='topologie:history' name='portprofile' id=port_profile.pk %}
{% can_edit port_profile %}
{% include 'buttons/edit.html' with href='topologie:edit-port-profile' id=port_profile.pk %}
{% acl_end %}
{% can_delete port_profile %}
{% include 'buttons/suppr.html' with href='topologie:del-port-profile' id=port_profile.pk %}
{% acl_end %}
</td>
</tr>
{% endfor %}
</table>
{% if port_profile_list.paginator %}
{% include "pagination.html" with list=port_profile_list %}
{% endif %}
</div>

View file

@ -72,5 +72,4 @@ Topologie des Switchs
<br /> <br />
<br /> <br />
{% endblock %} {% endblock %}

View file

@ -0,0 +1,43 @@
{% extends "topologie/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 © 2018 Gabriel Détraz
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load bootstrap3 %}
{% load acl %}
{% load i18n %}
{% block title %}Switchs{% endblock %}
{% block content %}
<h2>{% trans "Port profiles" %}</h2>
{% can_create PortProfile %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'topologie:new-port-profile' %}"><i class="fa fa-plus"></i>{% trans " Add a port profile" %}</a>
<hr>
{% acl_end %}
{% include "topologie/aff_port_profile.html" with port_profile_list=port_profile_list %}
<br />
<br />
<br />
{% endblock %}

View file

@ -33,6 +33,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-microchip"></i> <i class="fa fa-microchip"></i>
Switchs Switchs
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "topologie:index-port-profile" %}">
<i class="fa fa-cogs"></i>
Config des ports switchs
</a>
<a class="list-group-item list-group-item-info" href="{% url "topologie:index-ap" %}"> <a class="list-group-item list-group-item-info" href="{% url "topologie:index-ap" %}">
<i class="fa fa-wifi"></i> <i class="fa fa-wifi"></i>
Bornes WiFi Bornes WiFi

View file

@ -113,4 +113,16 @@ urlpatterns = [
url(r'^del_building/(?P<buildingid>[0-9]+)$', url(r'^del_building/(?P<buildingid>[0-9]+)$',
views.del_building, views.del_building,
name='del-building'), name='del-building'),
url(r'^index_port_profile/$',
views.index_port_profile,
name='index-port-profile'),
url(r'^new_port_profile/$',
views.new_port_profile,
name='new-port-profile'),
url(r'^edit_port_profile/(?P<portprofileid>[0-9]+)$',
views.edit_port_profile,
name='edit-port-profile'),
url(r'^del_port_profile/(?P<portprofileid>[0-9]+)$',
views.del_port_profile,
name='del-port-profile'),
] ]

View file

@ -47,6 +47,7 @@ from django.template.loader import get_template
from django.template import Context, Template, loader from django.template import Context, Template, loader
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext as _
import tempfile import tempfile
@ -80,6 +81,7 @@ from .models import (
SwitchBay, SwitchBay,
Building, Building,
Server, Server,
PortProfile,
) )
from .forms import ( from .forms import (
EditPortForm, EditPortForm,
@ -94,7 +96,8 @@ from .forms import (
AddAccessPointForm, AddAccessPointForm,
EditAccessPointForm, EditAccessPointForm,
EditSwitchBayForm, EditSwitchBayForm,
EditBuildingForm EditBuildingForm,
EditPortProfileForm,
) )
from subprocess import ( from subprocess import (
@ -124,10 +127,12 @@ def index(request):
request.GET.get('order'), request.GET.get('order'),
SortTable.TOPOLOGIE_INDEX SortTable.TOPOLOGIE_INDEX
) )
pagination_number = GeneralOption.get_cached_value('pagination_number') pagination_number = GeneralOption.get_cached_value('pagination_number')
switch_list = re2o_paginator(request, switch_list, pagination_number) switch_list = re2o_paginator(request, switch_list, pagination_number)
if any(service_link.need_regen() for service_link in Service_link.objects.filter(service__service_type='graph_topo')): if any(service_link.need_regen for service_link in Service_link.objects.filter(service__service_type='graph_topo')):
make_machine_graph() make_machine_graph()
for service_link in Service_link.objects.filter(service__service_type='graph_topo'): for service_link in Service_link.objects.filter(service__service_type='graph_topo'):
service_link.done_regen() service_link.done_regen()
@ -141,6 +146,19 @@ def index(request):
) )
@login_required
@can_view_all(PortProfile)
def index_port_profile(request):
pagination_number = GeneralOption.get_cached_value('pagination_number')
port_profile_list = PortProfile.objects.all().select_related('vlan_untagged')
port_profile_list = re2o_paginator(request, port_profile_list, pagination_number)
return render(
request,
'topologie/index_portprofile.html',
{'port_profile_list': port_profile_list}
)
@login_required @login_required
@can_view_all(Port) @can_view_all(Port)
@can_view(Switch) @can_view(Switch)
@ -955,6 +973,59 @@ def del_constructor_switch(request, constructor_switch, **_kwargs):
}, 'topologie/delete.html', request) }, 'topologie/delete.html', request)
@login_required
@can_create(PortProfile)
def new_port_profile(request):
"""Create a new port profile"""
port_profile = EditPortProfileForm(request.POST or None)
if port_profile.is_valid():
port_profile.save()
messages.success(request, _("Port profile created"))
return redirect(reverse('topologie:index'))
return form(
{'topoform': port_profile, 'action_name': _("Create")},
'topologie/topo.html',
request
)
@login_required
@can_edit(PortProfile)
def edit_port_profile(request, port_profile, **_kwargs):
"""Edit a port profile"""
port_profile = EditPortProfileForm(request.POST or None, instance=port_profile)
if port_profile.is_valid():
if port_profile.changed_data:
port_profile.save()
messages.success(request, _("Port profile modified"))
return redirect(reverse('topologie:index'))
return form(
{'topoform': port_profile, 'action_name': _("Edit")},
'topologie/topo.html',
request
)
@login_required
@can_delete(PortProfile)
def del_port_profile(request, port_profile, **_kwargs):
"""Delete a port profile"""
if request.method == 'POST':
try:
port_profile.delete()
messages.success(request,
_("The port profile was successfully deleted"))
except ProtectedError:
messages.success(request,
_("Impossible to delete the port profile"))
return redirect(reverse('topologie:index'))
return form(
{'objet': port_profile, 'objet_name': _("Port profile")},
'topologie/delete.html',
request
)
def make_machine_graph(): def make_machine_graph():
""" """
Create the graph of switchs, machines and access points. Create the graph of switchs, machines and access points.

View file

@ -34,7 +34,6 @@ from reversion.admin import VersionAdmin
from .models import ( from .models import (
User, User,
Mail,
MailAlias, MailAlias,
ServiceUser, ServiceUser,
School, School,
@ -110,6 +109,11 @@ class BanAdmin(VersionAdmin):
pass pass
class MailAliasAdmin(VersionAdmin):
"""Gestion des alias mail"""
pass
class WhitelistAdmin(VersionAdmin): class WhitelistAdmin(VersionAdmin):
"""Gestion des whitelist""" """Gestion des whitelist"""
pass pass
@ -127,7 +131,7 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
list_display = ( list_display = (
'pseudo', 'pseudo',
'surname', 'surname',
'email', 'external_mail',
'school', 'school',
'is_admin', 'is_admin',
'shell' 'shell'
@ -141,7 +145,7 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
'Personal info', 'Personal info',
{ {
'fields': 'fields':
('surname', 'email', 'school', 'shell', 'uid_number') ('surname', 'external_mail', 'school', 'shell', 'uid_number')
} }
), ),
('Permissions', {'fields': ('is_admin', )}), ('Permissions', {'fields': ('is_admin', )}),
@ -156,7 +160,7 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
'fields': ( 'fields': (
'pseudo', 'pseudo',
'surname', 'surname',
'email', 'external_mail',
'school', 'school',
'is_admin', 'is_admin',
'password1', 'password1',
@ -213,6 +217,7 @@ admin.site.register(School, SchoolAdmin)
admin.site.register(ListRight, ListRightAdmin) admin.site.register(ListRight, ListRightAdmin)
admin.site.register(ListShell, ListShellAdmin) admin.site.register(ListShell, ListShellAdmin)
admin.site.register(Ban, BanAdmin) admin.site.register(Ban, BanAdmin)
admin.site.register(MailAlias, MailAliasAdmin)
admin.site.register(Whitelist, WhitelistAdmin) admin.site.register(Whitelist, WhitelistAdmin)
admin.site.register(Request, RequestAdmin) admin.site.register(Request, RequestAdmin)
# Now register the new UserAdmin... # Now register the new UserAdmin...

View file

@ -140,7 +140,7 @@ class UserCreationForm(FormRevMixin, forms.ModelForm):
class Meta: class Meta:
model = Adherent model = Adherent
fields = ('pseudo', 'surname', 'email') fields = ('pseudo', 'surname')
def clean_password2(self): def clean_password2(self):
"""Verifie que password1 et 2 sont identiques""" """Verifie que password1 et 2 sont identiques"""
@ -220,7 +220,7 @@ class UserChangeForm(FormRevMixin, forms.ModelForm):
class Meta: class Meta:
model = Adherent model = Adherent
fields = ('pseudo', 'password', 'surname', 'email') fields = ('pseudo', 'password', 'surname')
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__)
@ -306,14 +306,12 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
self.fields['room'].label = 'Chambre' self.fields['room'].label = 'Chambre'
self.fields['room'].empty_label = "Pas de chambre" self.fields['room'].empty_label = "Pas de chambre"
self.fields['school'].empty_label = "Séléctionner un établissement" self.fields['school'].empty_label = "Séléctionner un établissement"
class Meta: class Meta:
model = Adherent model = Adherent
fields = [ fields = [
'name', 'name',
'surname', 'surname',
'pseudo', 'pseudo',
'email',
'school', 'school',
'comment', 'comment',
'room', 'room',
@ -365,7 +363,6 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
fields = [ fields = [
'surname', 'surname',
'pseudo', 'pseudo',
'email',
'school', 'school',
'comment', 'comment',
'room', 'room',
@ -597,9 +594,23 @@ class MailAliasForm(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(MailAliasForm, self).__init__(*args, prefix=prefix, **kwargs) super(MailAliasForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['valeur'].label = 'nom de l\'adresse mail' self.fields['valeur'].label = "Prefixe de l'alias mail. Ne peut contenir de @"
self.fields['extension'].label = 'extension de l\'adresse mail'
class Meta: class Meta:
model = MailAlias model = MailAlias
exclude = ['mail'] exclude = ['user']
class MailForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
"""Creation, edition des paramètres mail"""
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(MailForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['external_mail'].label = 'Adresse mail externe'
if 'redirection' in self.fields:
self.fields['redirection'].label = 'Activation de la redirection vers l\'adress externe'
if 'internal_address' in self.fields:
self.fields['internal_address'].label = 'Adresse mail interne'
class Meta:
model = User
fields = ['external_mail', 'redirection', 'internal_address']

View file

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-29 14:14
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
('users', '0072_auto_20180426_2021'),
]
operations = [
migrations.CreateModel(
name='MailAlias',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('valeur', models.CharField(help_text="username de l'adresse mail", max_length=64, unique=True)),
],
bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model),
),
migrations.RenameField(
model_name='user',
old_name='email',
new_name='external_mail',
),
migrations.AddField(
model_name='user',
name='internal_address',
field=models.BooleanField(default=False, help_text="Activer ou non l'utilisation de l'adresse mail interne"),
),
migrations.AddField(
model_name='user',
name='redirection',
field=models.BooleanField(default=False, help_text='Activer ou non la redirection du mail interne vers le mail externe'),
),
migrations.AddField(
model_name='mailalias',
name='user',
field=models.ForeignKey(blank=True, help_text='Utilisateur associé', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View file

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-29 15:17
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0073_auto_20180629_1614'),
]
def transfer_pseudo(apps, schema_editor):
db_alias = schema_editor.connection.alias
users = apps.get_model("users", "User")
mailalias = apps.get_model("users", "MailAlias")
users_list = users.objects.using(db_alias).all()
for user in users_list:
mailalias.objects.using(db_alias).create(valeur=user.pseudo, user=user)
def untransfer_pseudo(apps, schema_editor):
db_alias = schema_editor.connection.alias
mailalias = apps.get_model("users", "MailAlias")
mailalias.objects.using(db_alias).delete()
operations = [
migrations.AlterField(
model_name='mailalias',
name='user',
field=models.ForeignKey(help_text='Utilisateur associé', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='mailalias',
name='valeur',
field=models.CharField(help_text="Valeur de l'alias mail", max_length=128, unique=True),
),
migrations.RunPython(transfer_pseudo, untransfer_pseudo),
]

View file

@ -51,7 +51,7 @@ import datetime
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django import forms from django.forms import ValidationError
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.functional import cached_property from django.utils.functional import cached_property
@ -79,7 +79,7 @@ from re2o.field_permissions import FieldPermissionModelMixin
from re2o.mixins import AclMixin, RevMixin from re2o.mixins import AclMixin, RevMixin
from cotisations.models import Cotisation, Facture, Paiement, Vente from cotisations.models import Cotisation, Facture, Paiement, Vente
from machines.models import Domain, Interface, Machine, regen, Extension from machines.models import Domain, Interface, Machine, regen
from preferences.models import GeneralOption, AssoOption, OptionalUser from preferences.models import GeneralOption, AssoOption, OptionalUser
from preferences.models import OptionalMachine, MailMessageOption from preferences.models import OptionalMachine, MailMessageOption
@ -97,7 +97,7 @@ def linux_user_validator(login):
""" Retourne une erreur de validation si le login ne respecte """ Retourne une erreur de validation si le login ne respecte
pas les contraintes unix (maj, min, chiffres ou tiret)""" pas les contraintes unix (maj, min, chiffres ou tiret)"""
if not linux_user_check(login): if not linux_user_check(login):
raise forms.ValidationError( raise ValidationError(
", ce pseudo ('%(label)s') contient des carractères interdits", ", ce pseudo ('%(label)s') contient des carractères interdits",
params={'label': login}, params={'label': login},
) )
@ -134,7 +134,7 @@ class UserManager(BaseUserManager):
self, self,
pseudo, pseudo,
surname, surname,
email, external_mail,
password=None, password=None,
su=False su=False
): ):
@ -148,7 +148,7 @@ class UserManager(BaseUserManager):
pseudo=pseudo, pseudo=pseudo,
surname=surname, surname=surname,
name=surname, name=surname,
email=self.normalize_email(email), external_mail=external_mail,
) )
user.set_password(password) user.set_password(password)
@ -157,19 +157,19 @@ class UserManager(BaseUserManager):
user.save(using=self._db) user.save(using=self._db)
return user return user
def create_user(self, pseudo, surname, email, password=None): def create_user(self, pseudo, surname, external_mail, password=None):
""" """
Creates and saves a User with the given pseudo, name, surname, email, Creates and saves a User with the given pseudo, name, surname, email,
and password. and password.
""" """
return self._create_user(pseudo, surname, email, password, False) return self._create_user(pseudo, surname, external_mail, password, False)
def create_superuser(self, pseudo, surname, email, password): def create_superuser(self, pseudo, surname, external_mail, password):
""" """
Creates and saves a superuser with the given pseudo, name, surname, Creates and saves a superuser with the given pseudo, name, surname,
email, and password. email, and password.
""" """
return self._create_user(pseudo, surname, email, password, True) return self._create_user(pseudo, surname, external_mail, password, True)
class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
@ -194,13 +194,15 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
help_text="Doit contenir uniquement des lettres, chiffres, ou tirets", help_text="Doit contenir uniquement des lettres, chiffres, ou tirets",
validators=[linux_user_validator] validators=[linux_user_validator]
) )
email = models.EmailField() external_mail = models.EmailField()
""" redirection = models.BooleanField(
email= models.OneToOneField( default=False,
Mail, help_text='Activer ou non la redirection du mail interne vers le mail externe'
on_delete=models.PROTECT )
internal_address = models.BooleanField(
default=False,
help_text='Activer ou non l\'utilisation de l\'adresse mail interne'
) )
"""
school = models.ForeignKey( school = models.ForeignKey(
'School', 'School',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -233,7 +235,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
) )
USERNAME_FIELD = 'pseudo' USERNAME_FIELD = 'pseudo'
REQUIRED_FIELDS = ['surname', 'email'] REQUIRED_FIELDS = ['surname', 'external_mail']
objects = UserManager() objects = UserManager()
@ -527,7 +529,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
user_ldap.sn = self.pseudo user_ldap.sn = self.pseudo
user_ldap.dialupAccess = str(self.has_access()) user_ldap.dialupAccess = str(self.has_access())
user_ldap.home_directory = '/home/' + self.pseudo user_ldap.home_directory = '/home/' + self.pseudo
user_ldap.mail = self.email user_ldap.mail = self.get_mail()
user_ldap.given_name = self.surname.lower() + '_'\ user_ldap.given_name = self.surname.lower() + '_'\
+ self.name.lower()[:3] + self.name.lower()[:3]
user_ldap.gid = LDAP['user_gid'] user_ldap.gid = LDAP['user_gid']
@ -680,10 +682,10 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
""" """
Return the mail address choosen by the user Return the mail address choosen by the user
""" """
if not self.mail.internal_activated: if not OptionalUser.get_cached_value('mail_accounts') or not self.internal_address or self.redirection:
return(self.mail.external) return str(self.external_mail)
else: else:
return(self.mail.mailalias_set.first()) return str(self.mailalias_set.get(valeur=self.pseudo))
def get_next_domain_name(self): def get_next_domain_name(self):
"""Look for an available name for a new interface for """Look for an available name for a new interface for
@ -803,6 +805,32 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
"Droit requis pour changer le shell" "Droit requis pour changer le shell"
) )
@staticmethod
def can_change_redirection(user_request, *_args, **_kwargs):
""" Check if a user can change redirection.
:param user_request: The user who request
:returns: a message and a boolean which is True if the user has
the right to change a redirection
"""
return (
OptionalUser.get_cached_value('mail_accounts'),
"La gestion des comptes mails doit être activée"
)
@staticmethod
def can_change_internal_address(user_request, *_args, **_kwargs):
""" Check if a user can change internal address .
:param user_request: The user who request
:returns: a message and a boolean which is True if the user has
the right to change internal address
"""
return (
OptionalUser.get_cached_value('mail_accounts'),
"La gestion des comptes mails doit être activée"
)
@staticmethod @staticmethod
def can_change_force(user_request, *_args, **_kwargs): def can_change_force(user_request, *_args, **_kwargs):
""" Check if a user can change a force """ Check if a user can change a force
@ -897,12 +925,19 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
'shell': self.can_change_shell, 'shell': self.can_change_shell,
'force': self.can_change_force, 'force': self.can_change_force,
'selfpasswd': self.check_selfpasswd, 'selfpasswd': self.check_selfpasswd,
'redirection': self.can_change_redirection,
'internal_address' : self.can_change_internal_address,
} }
def clean(self, *args, **kwargs):
"""Check if this pseudo is already used by any mailalias.
Better than raising an error in post-save and catching it"""
if MailAlias.objects.filter(valeur=self.pseudo).exclude(user=self):
raise ValidationError("Ce pseudo est déjà utilisé")
def __str__(self): def __str__(self):
return self.pseudo return self.pseudo
class Adherent(User): class Adherent(User):
""" A class representing a member (it's a user with special """ A class representing a member (it's a user with special
informations) """ informations) """
@ -1021,9 +1056,11 @@ class Club(User):
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def user_post_save(**kwargs): def user_post_save(**kwargs):
""" Synchronisation post_save : envoie le mail de bienvenue si creation """ Synchronisation post_save : envoie le mail de bienvenue si creation
Synchronise le pseudo, en créant un alias mail correspondant
Synchronise le ldap""" Synchronise le ldap"""
is_created = kwargs['created'] is_created = kwargs['created']
user = kwargs['instance'] user = kwargs['instance']
mail_alias, created = MailAlias.objects.get_or_create(valeur=user.pseudo, user=user)
if is_created: if is_created:
user.notif_inscription() user.notif_inscription()
user.ldap_sync( user.ldap_sync(
@ -1594,62 +1631,53 @@ class LdapServiceUserGroup(ldapdb.models.Model):
return self.name return self.name
class Mail(RevMixin, AclMixin, models.Model):
"""
Mail account of a user
Compte mail d'un utilisateur
"""
external_mail = models.EmailField(help_text="Mail externe")
user = models.ForeignKey(
'User',
on_delete=models.CASCADE,
help_text="Object mail d'un User"
)
redirection = models.BooleanField(
default=False
)
internal_address = models.BooleanField(
default=False
)
def __str__(self):
return self.mail
class MailAlias(RevMixin, AclMixin, models.Model): class MailAlias(RevMixin, AclMixin, models.Model):
""" """
Define a alias for a user Mail Define a alias for a user Mail
Définit un aliase pour un Mail d'utilisateur Définit un alias pour un Mail d'utilisateur
""" """
mail = models.ForeignKey( user = models.ForeignKey(
'Mail', User,
on_delete=models.CASCADE, on_delete=models.CASCADE,
help_text="Objects Mail associé" help_text="Utilisateur associé",
) )
valeur = models.CharField( valeur = models.CharField(
max_length=64, unique=True,
help_text="username de l'adresse mail" max_length=128,
help_text="Valeur de l'alias mail"
) )
extension = models.ForeignKey(
'Extension',
on_delete=models.CASCADE,
help_text="Extension mail interne"
)
class Meta:
unique_together = ('valeur', 'extension',)
def __str__(self): def __str__(self):
return self.valeur + "@" + self.extension return self.complete_mail
@cached_property
def complete_mail(self):
return self.valeur + OptionalUser.get_cached_value('mail_extension')
@staticmethod
def can_create(user_request, userid, *_args, **_kwargs):
"""Check if an user can create an mailalias object.
:param user_request: The user who wants to create a mailalias object.
:return: a message and a boolean which is True if the user can create
an user or if the `options.all_can_create` is set.
"""
if not user_request.has_perm('users.add_mailalias'):
if int(user_request.id) != int(userid):
return False, 'Vous n\'avez pas le droit d\'ajouter un alias à une autre personne'
elif user_request.mailalias_set.all().count() >= OptionalUser.get_cached_value('max_mail_alias'):
return False, "Vous avez atteint la limite de {} alias".format(OptionalUser.get_cached_value('max_mail_alias'))
else:
return True, None
return True, None
def can_view(self, user_request, *_args, **_kwargs): def can_view(self, user_request, *_args, **_kwargs):
""" """
Check if the user can view the aliases Check if the user can view the alias
""" """
if user_request.has_perm('users.view_mailalias') or user.request == self.mail.user: if user_request.has_perm('users.view_mailalias') or user.request == self.user:
return True, None return True, None
else: else:
return False, "Vous n'avais pas les droits suffisants et n'êtes pas propriétaire de ces alias" return False, "Vous n'avais pas les droits suffisants et n'êtes pas propriétaire de ces alias"
@ -1662,8 +1690,8 @@ class MailAlias(RevMixin, AclMixin, models.Model):
if user_request.has_perm('users.delete_mailalias'): if user_request.has_perm('users.delete_mailalias'):
return True, None return True, None
else: else:
if user_request == self.mail.user: if user_request == self.user:
if self.id != 0: if self.valeur != self.user.pseudo:
return True, None return True, None
else: else:
return False, "Vous ne pouvez pas supprimer l'alias lié à votre pseudo" return False, "Vous ne pouvez pas supprimer l'alias lié à votre pseudo"
@ -1678,13 +1706,16 @@ class MailAlias(RevMixin, AclMixin, models.Model):
if user_request.has_perm('users.change_mailalias'): if user_request.has_perm('users.change_mailalias'):
return True, None return True, None
else: else:
if user_request == self.mail.user: if user_request == self.user:
if self.id != 0: if self.valeur != self.user.pseudo:
return True, None return True, None
else: else:
return False, "Vous ne pouvez pas modifier l'alias lié à votre pseudo" return False, "Vous ne pouvez pas modifier l'alias lié à votre pseudo"
else: else:
return False, "Vous n'avez pas les droits suffisants et n'êtes pas propriétairs de cet alias" return False, "Vous n'avez pas les droits suffisants et n'êtes pas propriétairs de cet alias"
def clean(self, *args, **kwargs):
if "@" in self.valeur:
raise ValidationError("Cet alias ne peut contenir un @")
super(MailAlias, self).clean(*args, **kwargs)

View file

@ -25,7 +25,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if alias_list.paginator %} {% if alias_list.paginator %}
{% include "pagination.html" with list=alias_list %} {% include "pagination.html" with list=alias_list %}
{% endif %} {% endif %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -37,12 +36,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ alias }}</td> <td>{{ alias }}</td>
<td class="text-right"> <td class="text-right">
{% can_delete alias %} {% can_delete alias %}
{% include 'buttons/suppr.html' with href='users:del-alias' id=alias.id %} {% include 'buttons/suppr.html' with href='users:del-mailalias' id=alias.id %}
{% acl_end %} {% acl_end %}
{% can_edit alias %} {% can_edit alias %}
{% include 'buttons/edit.html' with href='users:edit-alias' id=alias.id %} {% include 'buttons/edit.html' with href='users:edit-mailalias' id=alias.id %}
{% acl_end %} {% acl_end %}
{% include 'buttons/history.html' with href='users:history' name='alias' id=alias.id %} {% include 'buttons/history.html' with href='users:history' name='mailalias' id=alias.id %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -27,21 +27,99 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %} {% load acl %}
{% block title %}Profil{% endblock %} {% block title %}Profil{% endblock %}
{% block content %} {% block content %}
<h2>{{ users.surname }} {{users.name}}</h2> <div align="center">
<p>Vous êtes {% if users.end_adhesion != None %}<span class="label label-success"> <h2>Bienvenue {{users.name}} {{ users.surname }}</h2>
un {{ users.class_name | lower}}</span>{% else %}<span class="label label-danger"> </div>
non adhérent</span>{% endif %} et votre connexion est {% if users.has_access %} <div class="dashboard_container">
<span class="label label-success">active</span>{% else %}<span class="label label-danger">désactivée</span>{% endif %}.</p> <div class="row">
{% if user_solde %} {% if solde_activated %}
<p>Votre solde est de <span class="badge">{{ user.solde }}€</span>. <div class="col-sm-6 col-md-4">
{% if allow_online_payment %} {% else %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:recharge' %}"> <div class="col-sm-6 col-md-6">
{% endif %}
<div class="col-12">
{% if users.is_ban%}
<div class="panel panel-danger">
<div class="panel-heading dashboard">Votre compte est banni</div>
<div class="panel-body dashboard">
<i class="text-danger">Fin du ban : {{user.end_ban|date:"d M Y"}}</i>
</div>
</div>
{% elif not users.is_connected%}
<div class="panel panel-danger">
<div class="panel-heading dashboard">Pas d'accès à internet</div>
<div class="panel-body dashboard">
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:credit-solde' users.id %}">
<i class="fas fa-sign-in-alt"></i>
Payer ma connexion
</a>
</div>
</div>
{% else %}
<div class="panel panel-success">
<div class="panel-heading dashboard">Connecté</div>
<div class="panel-body dashboard">
<i class="text-success">Fin de connexion: {{user.end_adhesion|date:"d M Y"}}</i>
</div>
</div>
{% endif %}
</div>
</div>
{% if solde_activated %}
<div class="col-sm-6 col-md-4">
<div class="col-12">
<div class="panel panel-info">
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse" data-target="#collapse4">
{{user.solde}} <i class="fas fa-euro-sign"></i>
</div>
<div class="panel-body dashboard">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:credit-solde' users.id %}">
<i class="fa fa-euro-sign"></i> <i class="fa fa-euro-sign"></i>
Recharger Modifier le solde
</a> </a>
{% endif %} </div>
</p> </div>
{% endif %} </div>
</div>
{% endif %}
{% if solde_activated %}
<div class="col-sm-6 col-md-4">
{% else %}
<div class="col-sm-6 col-md-6">
{% endif %}
<div class="col-12">
{% if nb_machines %}
<div class="panel panel-info">
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse" data-target="#collapse3">
<span class="badge">{{nb_machines}}</span>
Machines
<i class="fa fa-desktop"></i>
</div>
<div class="panel-body dashboard">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
<i class="fa fa-desktop"></i>
Ajouter une machine
</a>
</div>
</div>
{% else %}
<div class="panel panel-warning">
<div class="panel-heading dashboard">Aucune machine</div>
<div class="panel-body dashboard">
<a class="btn btn-warning btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
<i class="fa fa-desktop"></i>
Ajouter une machine
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="panel-group" id="accordion"> <div class="panel-group" id="accordion">
<div class="panel panel-default"> <div class="panel panel-default">
@ -50,7 +128,7 @@ non adhérent</span>{% endif %} et votre connexion est {% if users.has_access %}
<i class="fa fa-user"></i> Informations détaillées <i class="fa fa-user"></i> Informations détaillées
</h3> </h3>
</div> </div>
<div class="panel-collapse collapse in" id="collapse1"> <div class="panel-collapse collapse" id="collapse1">
<div class="panel-body"> <div class="panel-body">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
@ -349,6 +427,65 @@ non adhérent</span>{% endif %} et votre connexion est {% if users.has_access %}
</div> </div>
</div> </div>
</div> </div>
<div class="panel panel-default">
<div class="panel-heading clearfix" data-parent="#accordion" data-toggle="collapse" data-target="#collapse7">
<h3 class="panel-title pull-left">
<i class="fa fa-envelope"></i>
Paramètres mail
</h3>
</div>
<div id="collapse7" class="panel-collapse collapse">
<div class="panel-body">
{% can_edit users %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-mail' users.id %}">
<i class="fa fa-plus-square"></i>
Modifier les options mail
</a>
{% acl_end %}
{% if mail_accounts %}
<div class="table-responsive">
<table class="table">
<tr>
<th>Adresse mail externe</th>
<th>Compte mail {{ asso_name }}</th>
<th>Adresse mail de contact</th>
</tr>
<tr>
<td>{{ users.external_mail }}</td>
<td>{{ users.internal_address|yesno:"Activé,Désactivé" }}</td>
<td>{{ users.get_mail }}</td>
</table>
<div class="alert alert-info" role="alert">
Vous pouvez bénéficier d'une adresse mail {{ asso_name }}.
Vous pouvez également la rediriger vers une adresse externe en modifiant les options mail.
</div>
</div>
{% if users.internal_address %}
{% can_create MailAlias users.id %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-mailalias' users.id %}">
<i class="fa fa-plus-square"></i>
Ajouter un alias mail
</a>
{% acl_end %}
{% if alias_list %}
{% include "users/aff_mailalias.html" with alias_list=alias_list %}
{% endif %}
{% endif %}
{% else %}
<div class="table-responsive">
<table class="table">
<tr>
<th>Adresse mail</th>
</tr>
<tr>
<td>{{ users.external_mail }}</td>
</tr>
</table>
</div>
{% endif %}
</div>
</div>
</div>
</div> </div>
<br /> <br />
<br /> <br />

View file

@ -68,6 +68,7 @@ urlpatterns = [
url(r'^add_mailalias/(?P<userid>[0-9]+)$', views.add_mailalias, name='add-mailalias'), url(r'^add_mailalias/(?P<userid>[0-9]+)$', views.add_mailalias, name='add-mailalias'),
url(r'^edit_mailalias/(?P<mailaliasid>[0-9]+)$', views.edit_mailalias, name='edit-mailalias'), url(r'^edit_mailalias/(?P<mailaliasid>[0-9]+)$', views.edit_mailalias, name='edit-mailalias'),
url(r'^del-mailalias/(?P<mailaliasid>[0-9]+)$', views.del_mailalias, name='del-mailalias'), url(r'^del-mailalias/(?P<mailaliasid>[0-9]+)$', views.del_mailalias, name='del-mailalias'),
url(r'^edit_mail/(?P<userid>[0-9]+)$', views.edit_mail, name='edit-mail'),
url(r'^add_school/$', views.add_school, name='add-school'), url(r'^add_school/$', views.add_school, name='add-school'),
url(r'^edit_school/(?P<schoolid>[0-9]+)$', url(r'^edit_school/(?P<schoolid>[0-9]+)$',
views.edit_school, views.edit_school,

View file

@ -80,10 +80,13 @@ from .models import (
Adherent, Adherent,
Club, Club,
ListShell, ListShell,
MailAlias,
) )
from .forms import ( from .forms import (
BanForm, BanForm,
WhitelistForm, WhitelistForm,
MailAliasForm,
MailForm,
DelSchoolForm, DelSchoolForm,
DelListRightForm, DelListRightForm,
NewListRightForm, NewListRightForm,
@ -111,8 +114,8 @@ def new_user(request):
GTU_sum_up = GeneralOption.get_cached_value('GTU_sum_up') GTU_sum_up = GeneralOption.get_cached_value('GTU_sum_up')
GTU = GeneralOption.get_cached_value('GTU') GTU = GeneralOption.get_cached_value('GTU')
if user.is_valid(): if user.is_valid():
user = user.save(commit=False) #user = user.save(commit=False)
user.save() user = user.save()
user.reset_passwd_mail(request) user.reset_passwd_mail(request)
messages.success(request, "L'utilisateur %s a été crée, un mail\ messages.success(request, "L'utilisateur %s a été crée, un mail\
pour l'initialisation du mot de passe a été envoyé" % user.pseudo) pour l'initialisation du mot de passe a été envoyé" % user.pseudo)
@ -500,19 +503,18 @@ def del_whitelist(request, whitelist, **_kwargs):
@can_edit(User) @can_edit(User)
def add_mailalias(request, user, userid): def add_mailalias(request, user, userid):
""" Créer un alias """ """ Créer un alias """
mailalias_instance = MailAlias(mail=user.mail) mailalias_instance = MailAlias(user=user)
whitelist = WhitelistForm( mailalias = MailAliasForm(
request.POST or None, request.POST or None,
instance=whitelist_instance instance=mailalias_instance
) )
if whitelist.is_valid(): if mailalias.is_valid():
whitelist.save() mailalias.save()
messages.success(request, "Alias créé") messages.success(request, "Alias créé")
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': str(userid)} kwargs={'userid': str(userid)}
)) ))
return form( return form(
{'userform': mailalias, 'action_name': 'Ajouter un alias mail'}, {'userform': mailalias, 'action_name': 'Ajouter un alias mail'},
'users/user.html', 'users/user.html',
@ -527,11 +529,14 @@ def edit_mailalias(request, mailalias_instance, **_kwargs):
request.POST or None, request.POST or None,
instance=mailalias_instance instance=mailalias_instance
) )
if whitelist.is_valid(): if mailalias.is_valid():
if whitelist.changed_data: if mailalias.changed_data:
whitelist.save() mailalias.save()
messages.success(request, "Alias modifiée") messages.success(request, "Alias modifiée")
return redirect(reverse('users:index')) return redirect(reverse(
'users:profil',
kwargs={'userid': str(mailalias_instance.user.id)}
))
return form( return form(
{'userform': mailalias, 'action_name': 'Editer un alias mail'}, {'userform': mailalias, 'action_name': 'Editer un alias mail'},
'users/user.html', 'users/user.html',
@ -547,7 +552,7 @@ def del_mailalias(request, mailalias, **_kwargs):
messages.success(request, "L'alias a été supprimé") messages.success(request, "L'alias a été supprimé")
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': str(mailalias.mail.user.id)} kwargs={'userid': str(mailalias.user.id)}
)) ))
return form( return form(
{'objet': mailalias, 'objet_name': 'mailalias'}, {'objet': mailalias, 'objet_name': 'mailalias'},
@ -555,6 +560,29 @@ def del_mailalias(request, mailalias, **_kwargs):
request request
) )
@login_required
@can_edit(User)
def edit_mail(request, user_instance, **_kwargs):
""" Editer un compte mail"""
mail = MailForm(
request.POST or None,
instance=user_instance,
user=request.user
)
if mail.is_valid():
if mail.changed_data:
mail.save()
messages.success(request, "Option mail modifiée")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(user_instance.id)}
))
return form(
{'userform': mail, 'action_name': 'Editer les options mail'},
'users/user.html',
request
)
@login_required @login_required
@can_create(School) @can_create(School)
def add_school(request): def add_school(request):
@ -920,7 +948,7 @@ def profil(request, users, **_kwargs):
) )
nb_machines = machines.count() nb_machines = machines.count()
machines = re2o_paginator(request, machines, pagination_large_number) machines = re2o_paginator(request, machines, pagination_large_number)
factures = Facture.objects.filter(user=users) factures = Facture.objects.filter(user=users).select_related('paiement').select_related('user')
factures = SortTable.sort( factures = SortTable.sort(
factures, factures,
request.GET.get('col'), request.GET.get('col'),
@ -955,6 +983,10 @@ def profil(request, users, **_kwargs):
'white_list': whitelists, 'white_list': whitelists,
'user_solde': user_solde, 'user_solde': user_solde,
'allow_online_payment': allow_online_payment, 'allow_online_payment': allow_online_payment,
'solde_activated': OptionalUser.objects.first().user_solde,
'asso_name': AssoOption.objects.first().name,
'alias_list': users.mailalias_set.all(),
'mail_accounts': OptionalUser.objects.first().mail_accounts
} }
) )