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:
commit
093050e245
73 changed files with 2153 additions and 187 deletions
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
20
preferences/migrations/0035_optionaluser_mail_extension.py
Normal file
20
preferences/migrations/0035_optionaluser_mail_extension.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
20
preferences/migrations/0036_optionaluser_mail_accounts.py
Normal file
20
preferences/migrations/0036_optionaluser_mail_accounts.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
20
preferences/migrations/0037_optionaluser_max_mail_alias.py
Normal file
20
preferences/migrations/0037_optionaluser_max_mail_alias.py
Normal 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"),
|
||||||
|
),
|
||||||
|
]
|
28
preferences/migrations/0038_mailcontact.py
Normal file
28
preferences/migrations/0038_mailcontact.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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"""
|
||||||
|
|
45
preferences/templates/preferences/aff_mailcontact.html
Normal file
45
preferences/templates/preferences/aff_mailcontact.html
Normal 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>
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
0
printer/__init__.py
Normal file
3
printer/admin.py
Normal file
3
printer/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
5
printer/apps.py
Normal file
5
printer/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PrinterConfig(AppConfig):
|
||||||
|
name = 'printer'
|
37
printer/forms.py
Normal file
37
printer/forms.py
Normal 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',
|
||||||
|
]
|
||||||
|
|
26
printer/migrations/0001_initial.py
Normal file
26
printer/migrations/0001_initial.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
48
printer/migrations/0002_auto_20180628_2032.py
Normal file
48
printer/migrations/0002_auto_20180628_2032.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
0
printer/migrations/__init__.py
Normal file
0
printer/migrations/__init__.py
Normal file
116
printer/models.py
Normal file
116
printer/models.py
Normal 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
13
printer/settings.py
Normal 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']
|
12
printer/templates/printer/echec.html
Normal file
12
printer/templates/printer/echec.html
Normal 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 %}
|
87
printer/templates/printer/newjob.html
Normal file
87
printer/templates/printer/newjob.html
Normal 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 %}
|
||||||
|
|
12
printer/templates/printer/success.html
Normal file
12
printer/templates/printer/success.html
Normal 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
3
printer/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
17
printer/urls.py
Normal file
17
printer/urls.py
Normal 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
72
printer/validators.py
Normal 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
55
printer/views.py
Normal 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
|
||||||
|
)
|
52
re2o/templates/re2o/contact.html
Normal file
52
re2o/templates/re2o/contact.html
Normal 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 %}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
44
topologie/migrations/0061_portprofile.py
Normal file
44
topologie/migrations/0061_portprofile.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
25
topologie/migrations/0062_auto_20180627_0123.py
Normal file
25
topologie/migrations/0062_auto_20180627_0123.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
21
topologie/migrations/0063_port_custom_profil.py
Normal file
21
topologie/migrations/0063_port_custom_profil.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
53
topologie/migrations/0064_createprofil.py
Normal file
53
topologie/migrations/0064_createprofil.py
Normal 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),
|
||||||
|
]
|
23
topologie/migrations/0065_auto_20180630_1703.py
Normal file
23
topologie/migrations/0065_auto_20180630_1703.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
25
topologie/migrations/0066_auto_20180630_1855.py
Normal file
25
topologie/migrations/0066_auto_20180630_1855.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
75
topologie/migrations/0067_auto_20180701_0016.py
Normal file
75
topologie/migrations/0067_auto_20180701_0016.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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"""
|
||||||
|
|
|
@ -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 %}">
|
||||||
|
|
85
topologie/templates/topologie/aff_port_profile.html
Normal file
85
topologie/templates/topologie/aff_port_profile.html
Normal 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>
|
|
@ -72,5 +72,4 @@ Topologie des Switchs
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
43
topologie/templates/topologie/index_portprofile.html
Normal file
43
topologie/templates/topologie/index_portprofile.html
Normal 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 %}
|
|
@ -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
|
||||||
|
|
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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...
|
||||||
|
|
|
@ -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']
|
||||||
|
|
46
users/migrations/0073_auto_20180629_1614.py
Normal file
46
users/migrations/0073_auto_20180629_1614.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
41
users/migrations/0074_auto_20180629_1717.py
Normal file
41
users/migrations/0074_auto_20180629_1717.py
Normal 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),
|
||||||
|
]
|
163
users/models.py
163
users/models.py
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
|
@ -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 />
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue