diff --git a/api/serializers.py b/api/serializers.py index 1b02c577..f2604068 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -29,20 +29,20 @@ from machines.models import ( IpType, Extension, IpList, - MachineType, Domain, Txt, Mx, Srv, Service_link, Ns, - OuverturePortList, OuverturePort, Ipv6List ) class ServiceLinkSerializer(serializers.ModelSerializer): + """ Serializer for the ServiceLink objects """ + name = serializers.CharField(source='service.service_type') class Meta: @@ -51,6 +51,8 @@ class ServiceLinkSerializer(serializers.ModelSerializer): class MailingSerializer(serializers.ModelSerializer): + """ Serializer to build Mailing objects """ + name = serializers.CharField(source='pseudo') class Meta: @@ -59,20 +61,27 @@ class MailingSerializer(serializers.ModelSerializer): class MailingMemberSerializer(serializers.ModelSerializer): + """ Serializer fot the Adherent objects (who belong to a + Mailing) """ + class Meta: model = Adherent - fields = ('email', 'name', 'surname', 'pseudo',) + fields = ('email',) class IpTypeField(serializers.RelatedField): - """Serialisation d'une iptype, renvoie son evaluation str""" + """ Serializer for an IpType object field """ + def to_representation(self, value): return value.type + def to_internal_value(self, data): + pass + class IpListSerializer(serializers.ModelSerializer): - """Serialisation d'une iplist, ip_type etant une foreign_key, - on evalue sa methode str""" + """ Serializer for an Ipv4List obejct using the IpType serialization """ + ip_type = IpTypeField(read_only=True) class Meta: @@ -81,16 +90,19 @@ class IpListSerializer(serializers.ModelSerializer): class Ipv6ListSerializer(serializers.ModelSerializer): + """ Serializer for an Ipv6List object """ + class Meta: model = Ipv6List fields = ('ipv6', 'slaac_ip') class InterfaceSerializer(serializers.ModelSerializer): - """Serialisation d'une interface, ipv4, domain et extension sont - des foreign_key, on les override et on les evalue avec des fonctions - get_...""" + """ Serializer for an Interface object. Use SerializerMethodField + to get ForeignKey values """ + ipv4 = IpListSerializer(read_only=True) + # TODO : use serializer.RelatedField to avoid duplicate code mac_address = serializers.SerializerMethodField('get_macaddress') domain = serializers.SerializerMethodField('get_dns') extension = serializers.SerializerMethodField('get_interface_extension') @@ -99,20 +111,29 @@ class InterfaceSerializer(serializers.ModelSerializer): model = Interface fields = ('ipv4', 'mac_address', 'domain', 'extension') - def get_dns(self, obj): + @staticmethod + def get_dns(obj): + """ The name of the associated DNS object """ return obj.domain.name - def get_interface_extension(self, obj): + @staticmethod + def get_interface_extension(obj): + """ The name of the associated Interface object """ return obj.domain.extension.name - def get_macaddress(self, obj): + @staticmethod + def get_macaddress(obj): + """ The string representation of the associated MAC address """ return str(obj.mac_address) class FullInterfaceSerializer(serializers.ModelSerializer): - """Serialisation complete d'une interface avec les ipv6 en plus""" + """ Serializer for an Interface obejct. Use SerializerMethodField + to get ForeignKey values """ + ipv4 = IpListSerializer(read_only=True) ipv6 = Ipv6ListSerializer(read_only=True, many=True) + # TODO : use serializer.RelatedField to avoid duplicate code mac_address = serializers.SerializerMethodField('get_macaddress') domain = serializers.SerializerMethodField('get_dns') extension = serializers.SerializerMethodField('get_interface_extension') @@ -121,26 +142,36 @@ class FullInterfaceSerializer(serializers.ModelSerializer): model = Interface fields = ('ipv4', 'ipv6', 'mac_address', 'domain', 'extension') - def get_dns(self, obj): + @staticmethod + def get_dns(obj): + """ The name of the associated DNS object """ return obj.domain.name - def get_interface_extension(self, obj): + @staticmethod + def get_interface_extension(obj): + """ The name of the associated Extension object """ return obj.domain.extension.name - def get_macaddress(self, obj): + @staticmethod + def get_macaddress(obj): + """ The string representation of the associated MAC address """ return str(obj.mac_address) class ExtensionNameField(serializers.RelatedField): - """Evaluation str d'un objet extension (.example.org)""" + """ Serializer for Extension object field """ + def to_representation(self, value): return value.name + def to_internal_value(self, data): + pass + class TypeSerializer(serializers.ModelSerializer): - """Serialisation d'un iptype : extension et la liste des - ouvertures de port son evalués en get_... etant des - foreign_key ou des relations manytomany""" + """ Serializer for an IpType object. Use SerializerMethodField to + get ForeignKey values """ + extension = ExtensionNameField(read_only=True) ouverture_ports_tcp_in = serializers\ .SerializerMethodField('get_port_policy_input_tcp') @@ -158,7 +189,10 @@ class TypeSerializer(serializers.ModelSerializer): 'ouverture_ports_tcp_in', 'ouverture_ports_tcp_out', 'ouverture_ports_udp_in', 'ouverture_ports_udp_out',) - def get_port_policy(self, obj, protocole, io): + @staticmethod + def get_port_policy(obj, protocole, io): + """ Generic utility function to get the policy for a given + port, protocole and IN or OUT """ if obj.ouverture_ports is None: return [] return map( @@ -196,14 +230,20 @@ class ExtensionSerializer(serializers.ModelSerializer): model = Extension fields = ('name', 'origin', 'origin_v6', 'zone_entry', 'soa') - def get_origin_ip(self, obj): + @staticmethod + def get_origin_ip(obj): + """ The IP of the associated origin for the zone """ return obj.origin.ipv4 - def get_zone_name(self, obj): + @staticmethod + def get_zone_name(obj): + """ The name of the associated zone """ return str(obj.dns_entry) - def get_soa_data(self, obj): - return { 'mail': obj.soa.dns_soa_mail, 'param': obj.soa.dns_soa_param } + @staticmethod + def get_soa_data(obj): + """ The representation of the associated SOA """ + return {'mail': obj.soa.dns_soa_mail, 'param': obj.soa.dns_soa_param} class MxSerializer(serializers.ModelSerializer): @@ -217,13 +257,19 @@ class MxSerializer(serializers.ModelSerializer): model = Mx fields = ('zone', 'priority', 'name', 'mx_entry') - def get_entry_name(self, obj): + @staticmethod + def get_entry_name(obj): + """ The name of the DNS MX entry """ return str(obj.name) - def get_zone_name(self, obj): + @staticmethod + def get_zone_name(obj): + """ The name of the associated zone of the MX record """ return obj.zone.name - def get_mx_name(self, obj): + @staticmethod + def get_mx_name(obj): + """ The string representation of the entry to add to the DNS """ return str(obj.dns_entry) @@ -237,10 +283,14 @@ class TxtSerializer(serializers.ModelSerializer): model = Txt fields = ('zone', 'txt_entry', 'field1', 'field2') - def get_zone_name(self, obj): + @staticmethod + def get_zone_name(obj): + """ The name of the associated zone """ return str(obj.zone.name) - def get_txt_name(self, obj): + @staticmethod + def get_txt_name(obj): + """ The string representation of the entry to add to the DNS """ return str(obj.dns_entry) @@ -263,10 +313,14 @@ class SrvSerializer(serializers.ModelSerializer): 'srv_entry' ) - def get_extension_name(self, obj): + @staticmethod + def get_extension_name(obj): + """ The name of the associated extension """ return str(obj.extension.name) - def get_srv_name(self, obj): + @staticmethod + def get_srv_name(obj): + """ The string representation of the entry to add to the DNS """ return str(obj.dns_entry) @@ -281,13 +335,19 @@ class NsSerializer(serializers.ModelSerializer): model = Ns fields = ('zone', 'ns', 'ns_entry') - def get_zone_name(self, obj): + @staticmethod + def get_zone_name(obj): + """ The name of the associated zone """ return obj.zone.name - def get_domain_name(self, obj): + @staticmethod + def get_domain_name(obj): + """ The name of the associated NS target """ return str(obj.ns) - def get_text_name(self, obj): + @staticmethod + def get_text_name(obj): + """ The string representation of the entry to add to the DNS """ return str(obj.dns_entry) @@ -302,13 +362,19 @@ class DomainSerializer(serializers.ModelSerializer): model = Domain fields = ('name', 'extension', 'cname', 'cname_entry') - def get_zone_name(self, obj): + @staticmethod + def get_zone_name(obj): + """ The name of the associated zone """ return obj.extension.name - def get_alias_name(self, obj): + @staticmethod + def get_alias_name(obj): + """ The name of the associated alias """ return str(obj.cname) - def get_cname_name(self, obj): + @staticmethod + def get_cname_name(obj): + """ The name of the associated CNAME target """ return str(obj.dns_entry) @@ -322,13 +388,19 @@ class ServicesSerializer(serializers.ModelSerializer): model = Service_link fields = ('server', 'service', 'need_regen') - def get_server_name(self, obj): + @staticmethod + def get_server_name(obj): + """ The name of the associated server """ return str(obj.server.domain.name) - def get_service_name(self, obj): + @staticmethod + def get_service_name(obj): + """ The name of the service name """ return str(obj.service) - def get_regen_status(self, obj): + @staticmethod + def get_regen_status(obj): + """ The string representation of the regen status """ return obj.need_regen() @@ -337,24 +409,38 @@ class OuverturePortsSerializer(serializers.Serializer): ipv4 = serializers.SerializerMethodField() ipv6 = serializers.SerializerMethodField() + def create(self, validated_data): + """ Creates a new object based on the un-serialized data. + Used to implement an abstract inherited method """ + pass + + def update(self, instance, validated_data): + """ Updates an object based on the un-serialized data. + Used to implement an abstract inherited method """ + pass + + @staticmethod def get_ipv4(): - return {i.ipv4.ipv4: - { - "tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()], - "tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()], - "udp_in":[j.udp_ports_in() for j in i.port_lists.all()], - "udp_out":[j.udp_ports_out() for j in i.port_lists.all()], + """ The representation of the policy for the IPv4 addresses """ + return { + i.ipv4.ipv4: { + "tcp_in": [j.tcp_ports_in() for j in i.port_lists.all()], + "tcp_out": [j.tcp_ports_out()for j in i.port_lists.all()], + "udp_in": [j.udp_ports_in() for j in i.port_lists.all()], + "udp_out": [j.udp_ports_out() for j in i.port_lists.all()], } - for i in Interface.objects.all() if i.ipv4 + for i in Interface.objects.all() if i.ipv4 } + @staticmethod def get_ipv6(): - return {i.ipv6: - { - "tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()], - "tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()], - "udp_in":[j.udp_ports_in() for j in i.port_lists.all()], - "udp_out":[j.udp_ports_out() for j in i.port_lists.all()], + """ The representation of the policy for the IPv6 addresses """ + return { + i.ipv6: { + "tcp_in": [j.tcp_ports_in() for j in i.port_lists.all()], + "tcp_out": [j.tcp_ports_out()for j in i.port_lists.all()], + "udp_in": [j.udp_ports_in() for j in i.port_lists.all()], + "udp_out": [j.udp_ports_out() for j in i.port_lists.all()], } - for i in Interface.objects.all() if i.ipv6 + for i in Interface.objects.all() if i.ipv6 } diff --git a/api/tests.py b/api/tests.py index 21fa6d24..bfcda28f 100644 --- a/api/tests.py +++ b/api/tests.py @@ -19,7 +19,10 @@ # 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. +"""api.tests +The tests for the API module. +""" -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/api/urls.py b/api/urls.py index f9838d38..9b77f308 100644 --- a/api/urls.py +++ b/api/urls.py @@ -32,7 +32,10 @@ from . import views urlpatterns = [ # Services url(r'^services/$', views.services), - url(r'^services/(?P\w+)/(?P\w+)/regen/$', views.services_server_service_regen), + url( + r'^services/(?P\w+)/(?P\w+)/regen/$', + views.services_server_service_regen + ), url(r'^services/(?P\w+)/$', views.services_server), # DNS @@ -56,7 +59,13 @@ urlpatterns = [ # Mailings url(r'^mailing/standard/$', views.mailing_standard), - url(r'^mailing/standard/(?P\w+)/members/$', views.mailing_standard_ml_members), + url( + r'^mailing/standard/(?P\w+)/members/$', + views.mailing_standard_ml_members + ), url(r'^mailing/club/$', views.mailing_club), - url(r'^mailing/club/(?P\w+)/members/$', views.mailing_club_ml_members), + url( + r'^mailing/club/(?P\w+)/members/$', + views.mailing_club_ml_members + ), ] diff --git a/api/utils.py b/api/utils.py index 024c9a39..d65cff44 100644 --- a/api/utils.py +++ b/api/utils.py @@ -26,6 +26,7 @@ Set of various and usefull functions for the API app from rest_framework.renderers import JSONRenderer from django.http import HttpResponse + class JSONResponse(HttpResponse): """A JSON response that can be send as an HTTP response. Usefull in case of REST API. @@ -51,23 +52,23 @@ class JSONResponse(HttpResponse): class JSONError(JSONResponse): """A JSON response when the request failed. """ - + def __init__(self, error_msg, data=None, **kwargs): """Initialise a JSONError object. - + Args: error_msg: A message explaining where the error is. data: An optional field for further data to send along. - + Creates: - A JSONResponse containing a field `status` set to `error` and a field - `reason` containing `error_msg`. If `data` argument has been given, - a field `data` containing it is added to the JSON response. + A JSONResponse containing a field `status` set to `error` and a + field `reason` containing `error_msg`. If `data` argument has been + given, a field `data` containing it is added to the JSON response. """ - + response = { - 'status' : 'error', - 'reason' : error_msg + 'status': 'error', + 'reason': error_msg } if data is not None: response['data'] = data @@ -77,22 +78,22 @@ class JSONError(JSONResponse): class JSONSuccess(JSONResponse): """A JSON response when the request suceeded. """ - + def __init__(self, data=None, **kwargs): """Initialise a JSONSucess object. - + Args: error_msg: A message explaining where the error is. data: An optional field for further data to send along. - + Creates: - A JSONResponse containing a field `status` set to `sucess`. If `data` - argument has been given, a field `data` containing it is added to the - JSON response. + A JSONResponse containing a field `status` set to `sucess`. If + `data` argument has been given, a field `data` containing it is + added to the JSON response. """ - + response = { - 'status' : 'success', + 'status': 'success', } if data is not None: response['data'] = data @@ -103,12 +104,20 @@ def accept_method(methods): """Decorator to set a list of accepted request method. Check if the method used is accepted. If not, send a NotAllowed response. """ + def decorator(view): + """The decorator to use on a specific view + """ def wrapper(request, *args, **kwargs): + """The wrapper used for a specific request + """ if request.method in methods: return view(request, *args, **kwargs) else: - return JSONError('Invalid request method. Request methods authorize are '+str(methods)) + return JSONError( + 'Invalid request method. Request methods authorize are ' + + str(methods) + ) return view(request, *args, **kwargs) return wrapper return decorator diff --git a/api/views.py b/api/views.py index 3e4b90ed..eda4dd59 100644 --- a/api/views.py +++ b/api/views.py @@ -27,13 +27,42 @@ HTML pages such as the login and index pages for a better integration. from django.contrib.auth.decorators import login_required, permission_required from django.views.decorators.csrf import csrf_exempt -from re2o.utils import all_has_access, all_active_assigned_interfaces - +from re2o.utils import ( + all_has_access, + all_active_assigned_interfaces, + filter_active_interfaces +) from users.models import Club -from machines.models import (Service_link, Service, Interface, Domain, - OuverturePortList) +from machines.models import ( + Service_link, + Service, + Interface, + Domain, + IpType, + Mx, + Ns, + Txt, + Srv, + Extension, + OuverturePortList, + OuverturePort +) -from .serializers import * +from .serializers import ( + ServicesSerializer, + ServiceLinkSerializer, + FullInterfaceSerializer, + DomainSerializer, + TypeSerializer, + MxSerializer, + NsSerializer, + TxtSerializer, + SrvSerializer, + ExtensionSerializer, + InterfaceSerializer, + MailingMemberSerializer, + MailingSerializer +) from .utils import JSONError, JSONSuccess, accept_method @@ -41,21 +70,26 @@ from .utils import JSONError, JSONSuccess, accept_method @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def services(request): +def services(_request): """The list of the different services and servers couples Return: GET: A JSONSuccess response with a field `data` containing: - * a list of dictionnaries (one for each service-server couple) containing: + * a list of dictionnaries (one for each service-server couple) + containing: * a field `server`: the server name * a field `service`: the service name * a field `need_regen`: does the service need a regeneration ? """ - service_link = Service_link.objects.all().select_related('server__domain').select_related('service') + + service_link = (Service_link.objects.all() + .select_related('server__domain') + .select_related('service')) seria = ServicesSerializer(service_link, many=True) return JSONSuccess(seria.data) + @csrf_exempt @login_required @permission_required('machines.serveur') @@ -72,6 +106,7 @@ def services_server_service_regen(request, server_name, service_name): POST: An empty JSONSuccess response. """ + query = Service_link.objects.filter( service__in=Service.objects.filter(service_type=service_name), server__in=Interface.objects.filter( @@ -80,7 +115,7 @@ def services_server_service_regen(request, server_name, service_name): ) if not query: return JSONError("This service is not active for this server") - + service = query.first() if request.method == 'GET': return JSONSuccess({'need_regen': service.need_regen()}) @@ -93,7 +128,7 @@ def services_server_service_regen(request, server_name, service_name): @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def services_server(request, server_name): +def services_server(_request, server_name): """The list of services attached to a specific server Returns: @@ -102,6 +137,7 @@ def services_server(request, server_name): * a list of dictionnaries (one for each service) containing: * a field `name`: the name of a service """ + query = Service_link.objects.filter( server__in=Interface.objects.filter( domain__in=Domain.objects.filter(name=server_name) @@ -109,9 +145,9 @@ def services_server(request, server_name): ) if not query: return JSONError("This service is not active for this server") - - services = query.all() - seria = ServiceLinkSerializer(services, many=True) + + services_objects = query.all() + seria = ServiceLinkSerializer(services_objects, many=True) return JSONSuccess(seria.data) @@ -119,7 +155,7 @@ def services_server(request, server_name): @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def dns_mac_ip_dns(request): +def dns_mac_ip_dns(_request): """The list of all active interfaces with all the associated infos (MAC, IP, IpType, DNS name and associated zone extension) @@ -135,8 +171,10 @@ def dns_mac_ip_dns(request): * a field `ip_type`: the name of the IpType of this interface * a field `mac_address`: the MAC of this interface * a field `domain`: the DNS name for this interface - * a field `extension`: the extension for the DNS zone of this interface + * a field `extension`: the extension for the DNS zone of this + interface """ + interfaces = all_active_assigned_interfaces(full=True) seria = FullInterfaceSerializer(interfaces, many=True) return JSONSuccess(seria.data) @@ -146,7 +184,7 @@ def dns_mac_ip_dns(request): @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def dns_alias(request): +def dns_alias(_request): """The list of all the alias used and the DNS info associated Returns: @@ -154,11 +192,23 @@ def dns_alias(request): A JSON Success response with a field `data` containing: * a list of dictionnaries (one for each alias) containing: * a field `name`: the alias used - * a field `cname`: the target of the alias (real name of the interface) - * a field `cname_entry`: the entry to write in the DNS to have the alias - * a field `extension`: the extension for the DNS zone of this interface + * a field `cname`: the target of the alias (real name of the + interface) + * a field `cname_entry`: the entry to write in the DNS to have + the alias + * a field `extension`: the extension for the DNS zone of this + interface """ - alias = Domain.objects.filter(interface_parent=None).filter(cname__in=Domain.objects.filter(interface_parent__in=Interface.objects.exclude(ipv4=None))).select_related('extension').select_related('cname__extension') + + alias = (Domain.objects + .filter(interface_parent=None) + .filter( + cname__in=Domain.objects.filter( + interface_parent__in=Interface.objects.exclude(ipv4=None) + ) + ) + .select_related('extension') + .select_related('cname__extension')) seria = DomainSerializer(alias, many=True) return JSONSuccess(seria.data) @@ -167,7 +217,7 @@ def dns_alias(request): @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def accesspoint_ip_dns(request): +def accesspoint_ip_dns(_request): """The list of all active interfaces with all the associated infos (MAC, IP, IpType, DNS name and associated zone extension) @@ -185,10 +235,12 @@ def accesspoint_ip_dns(request): * a field `ip_type`: the name of the IpType of this interface * a field `mac_address`: the MAC of this interface * a field `domain`: the DNS name for this interface - * a field `extension`: the extension for the DNS zone of this interface + * a field `extension`: the extension for the DNS zone of this + interface """ - interfaces = all_active_assigned_interfaces(full=True)\ - .filter(machine__accesspoint__isnull=False) + + interfaces = (all_active_assigned_interfaces(full=True) + .filter(machine__accesspoint__isnull=False)) seria = FullInterfaceSerializer(interfaces, many=True) return JSONSuccess(seria.data) @@ -197,7 +249,7 @@ def accesspoint_ip_dns(request): @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def dns_corresp(request): +def dns_corresp(_request): """The list of the IpTypes possible with the infos about each Returns: @@ -208,12 +260,14 @@ def dns_corresp(request): * a field `extension`: the DNS extension associated * a field `domain_ip_start`: the first ip to use for this type * a field `domain_ip_stop`: the last ip to use for this type - * a field `prefix_v6`: `null` if IPv6 is deactivated else the prefix to use + * a field `prefix_v6`: `null` if IPv6 is deactivated else the + prefix to use * a field `ouverture_ports_tcp_in`: the policy for TCP IN ports * a field `ouverture_ports_tcp_out`: the policy for TCP OUT ports * a field `ouverture_ports_udp_in`: the policy for UDP IN ports * a field `ouverture_ports_udp_out`: the policy for UDP OUT ports """ + ip_type = IpType.objects.all().select_related('extension') seria = TypeSerializer(ip_type, many=True) return JSONSuccess(seria.data) @@ -223,7 +277,7 @@ def dns_corresp(request): @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def dns_mx(request): +def dns_mx(_request): """The list of MX record to add to the DNS Returns: @@ -233,9 +287,13 @@ def dns_mx(request): * a field `zone`: the extension for the concerned zone * a field `priority`: the priority to use * a field `name`: the name of the target - * a field `mx_entry`: the full entry to add in the DNS for this MX record + * a field `mx_entry`: the full entry to add in the DNS for this + MX record """ - mx = Mx.objects.all().select_related('zone').select_related('name__extension') + + mx = (Mx.objects.all() + .select_related('zone') + .select_related('name__extension')) seria = MxSerializer(mx, many=True) return JSONSuccess(seria.data) @@ -244,7 +302,7 @@ def dns_mx(request): @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def dns_ns(request): +def dns_ns(_request): """The list of NS record to add to the DNS Returns: @@ -253,9 +311,18 @@ def dns_ns(request): * a list of dictionnaries (one for each NS record) containing: * a field `zone`: the extension for the concerned zone * a field `ns`: the DNS name for the NS server targeted - * a field `ns_entry`: the full entry to add in the DNS for this NS record + * a field `ns_entry`: the full entry to add in the DNS for this + NS record """ - ns = Ns.objects.exclude(ns__in=Domain.objects.filter(interface_parent__in=Interface.objects.filter(ipv4=None))).select_related('zone').select_related('ns__extension') + + ns = (Ns.objects + .exclude( + ns__in=Domain.objects.filter( + interface_parent__in=Interface.objects.filter(ipv4=None) + ) + ) + .select_related('zone') + .select_related('ns__extension')) seria = NsSerializer(ns, many=True) return JSONSuccess(seria.data) @@ -264,7 +331,7 @@ def dns_ns(request): @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def dns_txt(request): +def dns_txt(_request): """The list of TXT record to add to the DNS Returns: @@ -274,8 +341,10 @@ def dns_txt(request): * a field `zone`: the extension for the concerned zone * a field `field1`: the first field in the record (target) * a field `field2`: the second field in the record (value) - * a field `txt_entry`: the full entry to add in the DNS for this TXT record + * a field `txt_entry`: the full entry to add in the DNS for this + TXT record """ + txt = Txt.objects.all().select_related('zone') seria = TxtSerializer(txt, many=True) return JSONSuccess(seria.data) @@ -285,7 +354,7 @@ def dns_txt(request): @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def dns_srv(request): +def dns_srv(_request): """The list of SRV record to add to the DNS Returns: @@ -300,9 +369,13 @@ def dns_srv(request): * a field `weight`: the weight for same priority entries * a field `port`: the port targeted * a field `target`: the interface targeted by this service - * a field `srv_entry`: the full entry to add in the DNS for this SRV record + * a field `srv_entry`: the full entry to add in the DNS for this + SRV record """ - srv = Srv.objects.all().select_related('extension').select_related('target__extension') + + srv = (Srv.objects.all() + .select_related('extension') + .select_related('target__extension')) seria = SrvSerializer(srv, many=True) return JSONSuccess(seria.data) @@ -311,8 +384,8 @@ def dns_srv(request): @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def dns_zones(request): - """The list of the zones managed +def dns_zones(_request): + """The list of the zones managed Returns: GET: @@ -320,21 +393,27 @@ def dns_zones(request): * a list of dictionnaries (one for each zone) containing: * a field `name`: the extension for the zone * a field `origin`: the server IPv4 for the orgin of the zone - * a field `origin_v6`: `null` if ipv6 is deactivated else the server IPv6 for the origin of the zone + * a field `origin_v6`: `null` if ipv6 is deactivated else the + server IPv6 for the origin of the zone * a field `soa` containing: - * a field `mail` containing the mail to contact in case of problem with the zone - * a field `param` containing the full soa paramters to use in the DNS for this zone - * a field `zone_entry`: the full entry to add in the DNS for the origin of the zone + * a field `mail` containing the mail to contact in case of + problem with the zone + * a field `param` containing the full soa paramters to use + in the DNS for this zone + * a field `zone_entry`: the full entry to add in the DNS for the + origin of the zone """ + zones = Extension.objects.all().select_related('origin') seria = ExtensionSerializer(zones, many=True) return JSONSuccess(seria.data) + @csrf_exempt @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def firewall_ouverture_ports(request): +def firewall_ouverture_ports(_request): """The list of the ports authorized to be openned by the firewall Returns: @@ -359,37 +438,73 @@ def firewall_ouverture_ports(request): * a field `udp_out` containing: * a list of port number where ipv6 udp out should be ok """ - r = {'ipv4':{}, 'ipv6':{}} - for o in OuverturePortList.objects.all().prefetch_related('ouvertureport_set').prefetch_related('interface_set', 'interface_set__ipv4'): + + r = {'ipv4': {}, 'ipv6': {}} + for o in (OuverturePortList.objects.all() + .prefetch_related('ouvertureport_set') + .prefetch_related('interface_set', 'interface_set__ipv4')): pl = { - "tcp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.IN))), - "tcp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.OUT))), - "udp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.IN))), - "udp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.OUT))), + "tcp_in": set(map( + str, + o.ouvertureport_set.filter( + protocole=OuverturePort.TCP, + io=OuverturePort.IN + ) + )), + "tcp_out": set(map( + str, + o.ouvertureport_set.filter( + protocole=OuverturePort.TCP, + io=OuverturePort.OUT + ) + )), + "udp_in": set(map( + str, + o.ouvertureport_set.filter( + protocole=OuverturePort.UDP, + io=OuverturePort.IN + ) + )), + "udp_out": set(map( + str, + o.ouvertureport_set.filter( + protocole=OuverturePort.UDP, + io=OuverturePort.OUT + ) + )), } for i in filter_active_interfaces(o.interface_set): if i.may_have_port_open(): d = r['ipv4'].get(i.ipv4.ipv4, {}) - d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) - d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"]) - d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) - d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) + d["tcp_in"] = (d.get("tcp_in", set()) + .union(pl["tcp_in"])) + d["tcp_out"] = (d.get("tcp_out", set()) + .union(pl["tcp_out"])) + d["udp_in"] = (d.get("udp_in", set()) + .union(pl["udp_in"])) + d["udp_out"] = (d.get("udp_out", set()) + .union(pl["udp_out"])) r['ipv4'][i.ipv4.ipv4] = d if i.ipv6(): for ipv6 in i.ipv6(): d = r['ipv6'].get(ipv6.ipv6, {}) - d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) - d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"]) - d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) - d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) + d["tcp_in"] = (d.get("tcp_in", set()) + .union(pl["tcp_in"])) + d["tcp_out"] = (d.get("tcp_out", set()) + .union(pl["tcp_out"])) + d["udp_in"] = (d.get("udp_in", set()) + .union(pl["udp_in"])) + d["udp_out"] = (d.get("udp_out", set()) + .union(pl["udp_out"])) r['ipv6'][ipv6.ipv6] = d return JSONSuccess(r) + @csrf_exempt @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def dhcp_mac_ip(request): +def dhcp_mac_ip(_request): """The list of all active interfaces with all the associated infos (MAC, IP, IpType, DNS name and associated zone extension) @@ -402,8 +517,10 @@ def dhcp_mac_ip(request): * a field `ip_type`: the name of the IpType of this interface * a field `mac_address`: the MAC of this interface * a field `domain`: the DNS name for this interface - * a field `extension`: the extension for the DNS zone of this interface + * a field `extension`: the extension for the DNS zone of this + interface """ + interfaces = all_active_assigned_interfaces() seria = InterfaceSerializer(interfaces, many=True) return JSONSuccess(seria.data) @@ -413,7 +530,7 @@ def dhcp_mac_ip(request): @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def mailing_standard(request): +def mailing_standard(_request): """All the available standard mailings. Returns: @@ -422,15 +539,17 @@ def mailing_standard(request): * a list of dictionnaries (one for each mailing) containing: * a field `name`: the name of a mailing """ + return JSONSuccess([ {'name': 'adherents'} ]) + @csrf_exempt @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def mailing_standard_ml_members(request): +def mailing_standard_ml_members(_request, ml_name): """All the members of a specific standard mailing Returns: @@ -442,6 +561,7 @@ def mailing_standard_ml_members(request): * a field `surname`: the surname of the member * a field `pseudo`: the pseudo of the member """ + # All with active connextion if ml_name == 'adherents': members = all_has_access().values('email').distinct() @@ -456,7 +576,7 @@ def mailing_standard_ml_members(request): @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def mailing_club(request): +def mailing_club(_request): """All the available club mailings. Returns: @@ -465,15 +585,17 @@ def mailing_club(request): * a list of dictionnaries (one for each mailing) containing: * a field `name` indicating the name of a mailing """ + clubs = Club.objects.filter(mailing=True).values('pseudo') seria = MailingSerializer(clubs, many=True) return JSONSuccess(seria.data) + @csrf_exempt @login_required @permission_required('machines.serveur') @accept_method(['GET']) -def mailing_club_ml_members(request): +def mailing_club_ml_members(_request, ml_name): """All the members of a specific club mailing Returns: @@ -485,6 +607,7 @@ def mailing_club_ml_members(request): * a field `surname`: the surname of the member * a field `pseudo`: the pseudo of the member """ + try: club = Club.objects.get(mailing=True, pseudo=ml_name) except Club.DoesNotExist: diff --git a/cotisations/__init__.py b/cotisations/__init__.py index df6e4256..ee5a305d 100644 --- a/cotisations/__init__.py +++ b/cotisations/__init__.py @@ -20,5 +20,8 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""cotisations +The app in charge of all the members's cotisations +""" from .acl import * diff --git a/cotisations/acl.py b/cotisations/acl.py index 4e147a82..aa98c32a 100644 --- a/cotisations/acl.py +++ b/cotisations/acl.py @@ -27,6 +27,7 @@ Here are defined some functions to check acl on the application. """ from django.utils.translation import ugettext as _ + def can_view(user): """Check if an user can view the application. @@ -38,4 +39,7 @@ def can_view(user): viewing is granted and msg is a message (can be None). """ can = user.has_module_perms('cotisations') - return can, None if can else _("You don't have the rights to see this application.") + if can: + return can, None + else: + return can, _("You don't have the rights to see this application.") diff --git a/cotisations/admin.py b/cotisations/admin.py index 8186e4e3..587bc066 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -20,6 +20,9 @@ # 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. +"""cotisations.admin +The objects, fields and datastructures visible in the Django admin view +""" from __future__ import unicode_literals diff --git a/cotisations/forms.py b/cotisations/forms.py index d0a891bd..a6647a45 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -20,7 +20,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Forms for the 'cotisation' app of re2o. It highly depends on +Forms for the 'cotisation' app of re2o. It highly depends on :cotisations:models and is mainly used by :cotisations:views. The following forms are mainly used to create, edit or delete @@ -38,16 +38,15 @@ from __future__ import unicode_literals from django import forms from django.db.models import Q from django.forms import ModelForm, Form -from django.core.validators import MinValueValidator,MaxValueValidator +from django.core.validators import MinValueValidator from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _l -from .models import Article, Paiement, Facture, Banque from preferences.models import OptionalUser -from users.models import User - from re2o.field_permissions import FieldPermissionFormMixin -from re2o.mixins import FormRevMixin +from re2o.mixins import FormRevMixin +from .models import Article, Paiement, Facture, Banque + class NewFactureForm(FormRevMixin, ModelForm): """ @@ -109,12 +108,16 @@ class CreditSoldeForm(NewFactureForm): montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True) -class SelectUserArticleForm(FormRevMixin, Form): +class SelectUserArticleForm( + FormRevMixin, Form): """ - Form used to select an article during the creation of an invoice for a member. + Form used to select an article during the creation of an invoice for a + member. """ article = forms.ModelChoiceField( - queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Adherent')), + queryset=Article.objects.filter( + Q(type_user='All') | Q(type_user='Adherent') + ), label=_l("Article"), required=True ) @@ -127,10 +130,13 @@ class SelectUserArticleForm(FormRevMixin, Form): class SelectClubArticleForm(Form): """ - Form used to select an article during the creation of an invoice for a club. + Form used to select an article during the creation of an invoice for a + club. """ article = forms.ModelChoiceField( - queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Club')), + queryset=Article.objects.filter( + Q(type_user='All') | Q(type_user='Club') + ), label=_l("Article"), required=True ) @@ -140,6 +146,7 @@ class SelectClubArticleForm(Form): required=True ) + # TODO : change Facture to Invoice class NewFactureFormPdf(Form): """ @@ -147,9 +154,18 @@ class NewFactureFormPdf(Form): """ paid = forms.BooleanField(label=_l("Paid"), required=False) # TODO : change dest field to recipient - dest = forms.CharField(required=True, max_length=255, label=_l("Recipient")) + dest = forms.CharField( + required=True, + max_length=255, + label=_l("Recipient") + ) # TODO : change chambre field to address - chambre = forms.CharField(required=False, max_length=10, label=_l("Address")) + chambre = forms.CharField( + required=False, + max_length=10, + label=_l("Address") + ) + # TODO : change Facture to Invoice class EditFactureForm(FieldPermissionFormMixin, NewFactureForm): @@ -295,6 +311,11 @@ class NewFactureSoldeForm(NewFactureForm): """ def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(NewFactureSoldeForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) self.fields['cheque'].required = False self.fields['banque'].required = False self.fields['cheque'].label = _('Cheque number') @@ -313,7 +334,6 @@ class NewFactureSoldeForm(NewFactureForm): # TODO : change paiement to payment and baque to bank fields = ['paiement', 'banque'] - def clean(self): cleaned_data = super(NewFactureSoldeForm, self).clean() # TODO : change paiement to payment @@ -342,7 +362,7 @@ class RechargeForm(FormRevMixin, Form): value = forms.FloatField( label=_l("Amount"), min_value=0.01, - validators = [] + validators=[] ) def __init__(self, *args, **kwargs): @@ -350,6 +370,10 @@ class RechargeForm(FormRevMixin, Form): super(RechargeForm, self).__init__(*args, **kwargs) def clean_value(self): + """ + Returns a cleaned vlaue from the received form by validating + the value is well inside the possible limits + """ value = self.cleaned_data['value'] if value < OptionalUser.get_cached_value('min_online_payment'): raise forms.ValidationError( @@ -360,7 +384,8 @@ class RechargeForm(FormRevMixin, Form): ) } ) - if value + self.user.solde > OptionalUser.get_cached_value('max_solde'): + if value + self.user.solde > \ + OptionalUser.get_cached_value('max_solde'): raise forms.ValidationError( _("Requested amount is too high. Your balance can't exceed \ %(max_online_balance)s €.") % { diff --git a/cotisations/migrations/0029_auto_20180414_2056.py b/cotisations/migrations/0029_auto_20180414_2056.py new file mode 100644 index 00000000..f0cb63fd --- /dev/null +++ b/cotisations/migrations/0029_auto_20180414_2056.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-04-14 18:56 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0028_auto_20171231_0007'), + ] + + operations = [ + migrations.AlterModelOptions( + name='article', + options={'permissions': (('view_article', "Can see an article's details"),), 'verbose_name': 'Article', 'verbose_name_plural': 'Articles'}, + ), + migrations.AlterModelOptions( + name='banque', + options={'permissions': (('view_banque', "Can see a bank's details"),), 'verbose_name': 'Bank', 'verbose_name_plural': 'Banks'}, + ), + migrations.AlterModelOptions( + name='cotisation', + options={'permissions': (('view_cotisation', "Can see a cotisation's details"), ('change_all_cotisation', 'Can edit the previous cotisations'))}, + ), + migrations.AlterModelOptions( + name='facture', + options={'permissions': (('change_facture_control', 'Can change the "controlled" state'), ('change_facture_pdf', 'Can create a custom PDF invoice'), ('view_facture', "Can see an invoice's details"), ('change_all_facture', 'Can edit all the previous invoices')), 'verbose_name': 'Invoice', 'verbose_name_plural': 'Invoices'}, + ), + migrations.AlterModelOptions( + name='paiement', + options={'permissions': (('view_paiement', "Can see a payement's details"),), 'verbose_name': 'Payment method', 'verbose_name_plural': 'Payment methods'}, + ), + migrations.AlterModelOptions( + name='vente', + options={'permissions': (('view_vente', "Can see a purchase's details"), ('change_all_vente', 'Can edit all the previous purchases')), 'verbose_name': 'Purchase', 'verbose_name_plural': 'Purchases'}, + ), + migrations.AlterField( + model_name='article', + name='duration', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Duration (in whole month)'), + ), + migrations.AlterField( + model_name='article', + name='name', + field=models.CharField(max_length=255, verbose_name='Designation'), + ), + migrations.AlterField( + model_name='article', + name='prix', + field=models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Unitary price'), + ), + migrations.AlterField( + model_name='article', + name='type_cotisation', + field=models.CharField(blank=True, choices=[('Connexion', 'Connexion'), ('Adhesion', 'Membership'), ('All', 'Both of them')], default=None, max_length=255, null=True, verbose_name='Type of cotisation'), + ), + migrations.AlterField( + model_name='article', + name='type_user', + field=models.CharField(choices=[('Adherent', 'Member'), ('Club', 'Club'), ('All', 'Both of them')], default='All', max_length=255, verbose_name='Type of users concerned'), + ), + migrations.AlterField( + model_name='banque', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='cotisation', + name='date_end', + field=models.DateTimeField(verbose_name='Ending date'), + ), + migrations.AlterField( + model_name='cotisation', + name='date_start', + field=models.DateTimeField(verbose_name='Starting date'), + ), + migrations.AlterField( + model_name='cotisation', + name='type_cotisation', + field=models.CharField(choices=[('Connexion', 'Connexion'), ('Adhesion', 'Membership'), ('All', 'Both of them')], default='All', max_length=255, verbose_name='Type of cotisation'), + ), + migrations.AlterField( + model_name='cotisation', + name='vente', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='cotisations.Vente', verbose_name='Purchase'), + ), + migrations.AlterField( + model_name='facture', + name='cheque', + field=models.CharField(blank=True, max_length=255, verbose_name='Cheque number'), + ), + migrations.AlterField( + model_name='facture', + name='control', + field=models.BooleanField(default=False, verbose_name='Controlled'), + ), + migrations.AlterField( + model_name='facture', + name='date', + field=models.DateTimeField(auto_now_add=True, verbose_name='Date'), + ), + migrations.AlterField( + model_name='facture', + name='valid', + field=models.BooleanField(default=True, verbose_name='Validated'), + ), + migrations.AlterField( + model_name='paiement', + name='moyen', + field=models.CharField(max_length=255, verbose_name='Method'), + ), + migrations.AlterField( + model_name='paiement', + name='type_paiement', + field=models.IntegerField(choices=[(0, 'Standard'), (1, 'Cheque')], default=0, verbose_name='Payment type'), + ), + migrations.AlterField( + model_name='vente', + name='duration', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Duration (in whole month)'), + ), + migrations.AlterField( + model_name='vente', + name='facture', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cotisations.Facture', verbose_name='Invoice'), + ), + migrations.AlterField( + model_name='vente', + name='name', + field=models.CharField(max_length=255, verbose_name='Article'), + ), + migrations.AlterField( + model_name='vente', + name='number', + field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='Amount'), + ), + migrations.AlterField( + model_name='vente', + name='prix', + field=models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Price'), + ), + migrations.AlterField( + model_name='vente', + name='type_cotisation', + field=models.CharField(blank=True, choices=[('Connexion', 'Connexion'), ('Adhesion', 'Membership'), ('All', 'Both of them')], max_length=255, null=True, verbose_name='Type of cotisation'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index 83f81d5b..c4c6d4af 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -34,17 +34,16 @@ from __future__ import unicode_literals from dateutil.relativedelta import relativedelta from django.db import models -from django.db.models import Q +from django.db.models import Q, Max from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.forms import ValidationError from django.core.validators import MinValueValidator -from django.db.models import Max from django.utils import timezone -from machines.models import regen from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _l +from machines.models import regen from re2o.field_permissions import FieldPermissionModelMixin from re2o.mixins import AclMixin, RevMixin @@ -54,7 +53,7 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): """ The model for an invoice. It reprensents the fact that a user paid for something (it can be multiple article paid at once). - + An invoice is linked to : * one or more purchases (one for each article sold that time) * a user (the one who bought those articles) @@ -105,11 +104,16 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): abstract = False permissions = ( # TODO : change facture to invoice - ('change_facture_control', _l("Can change the \"controlled\" state")), - # TODO : seems more likely to be call create_facture_pdf or create_invoice_pdf - ('change_facture_pdf', _l("Can create a custom PDF invoice")), - ('view_facture', _l("Can see an invoice's details")), - ('change_all_facture', _l("Can edit all the previous invoices")), + ('change_facture_control', + _l("Can change the \"controlled\" state")), + # TODO : seems more likely to be call create_facture_pdf + # or create_invoice_pdf + ('change_facture_pdf', + _l("Can create a custom PDF invoice")), + ('view_facture', + _l("Can see an invoice's details")), + ('change_all_facture', + _l("Can edit all the previous invoices")), ) verbose_name = _l("Invoice") verbose_name_plural = _l("Invoices") @@ -159,11 +163,14 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): def can_edit(self, user_request, *args, **kwargs): if not user_request.has_perm('cotisations.change_facture'): return False, _("You don't have the right to edit an invoice.") - elif not user_request.has_perm('cotisations.change_all_facture') and not self.user.can_edit(user_request, *args, **kwargs)[0]: - return False, _("You don't have the right to edit this user's invoices.") - elif not user_request.has_perm('cotisations.change_all_facture') and\ - (self.control or not self.valid): - return False, _("You don't have the right to edit an invoice already controlled or invalidated.") + elif not user_request.has_perm('cotisations.change_all_facture') and \ + not self.user.can_edit(user_request, *args, **kwargs)[0]: + return False, _("You don't have the right to edit this user's " + "invoices.") + elif not user_request.has_perm('cotisations.change_all_facture') and \ + (self.control or not self.valid): + return False, _("You don't have the right to edit an invoice " + "already controlled or invalidated.") else: return True, None @@ -171,33 +178,45 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): if not user_request.has_perm('cotisations.delete_facture'): return False, _("You don't have the right to delete an invoice.") if not self.user.can_edit(user_request, *args, **kwargs)[0]: - return False, _("You don't have the right to delete this user's invoices.") + return False, _("You don't have the right to delete this user's " + "invoices.") if self.control or not self.valid: - return False, _("You don't have the right to delete an invoice already controlled or invalidated.") + return False, _("You don't have the right to delete an invoice " + "already controlled or invalidated.") else: return True, None - def can_view(self, user_request, *args, **kwargs): - if not user_request.has_perm('cotisations.view_facture') and\ - self.user != user_request: - return False, _("You don't have the right to see someone else's invoices history.") + def can_view(self, user_request, *_args, **_kwargs): + if not user_request.has_perm('cotisations.view_facture') and \ + self.user != user_request: + return False, _("You don't have the right to see someone else's " + "invoices history.") elif not self.valid: return False, _("The invoice has been invalidated.") else: return True, None @staticmethod - def can_change_control(user_request, *args, **kwargs): - return user_request.has_perm('cotisations.change_facture_control'), _("You don't have the right to edit the \"controlled\" state.") + def can_change_control(user_request, *_args, **_kwargs): + """ Returns True if the user can change the 'controlled' status of + this invoice """ + return ( + user_request.has_perm('cotisations.change_facture_control'), + _("You don't have the right to edit the \"controlled\" state.") + ) @staticmethod - def can_change_pdf(user_request, *args, **kwargs): - return user_request.has_perm('cotisations.change_facture_pdf'), _("You don't have the right to edit an invoice.") + def can_change_pdf(user_request, *_args, **_kwargs): + """ Returns True if the user can change this invoice """ + return ( + user_request.has_perm('cotisations.change_facture_pdf'), + _("You don't have the right to edit an invoice.") + ) def __init__(self, *args, **kwargs): super(Facture, self).__init__(*args, **kwargs) self.field_permissions = { - 'control' : self.can_change_control, + 'control': self.can_change_control, } def __str__(self): @@ -205,7 +224,7 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @receiver(post_save, sender=Facture) -def facture_post_save(sender, **kwargs): +def facture_post_save(**kwargs): """ Synchronise the LDAP user after an invoice has been saved. """ @@ -215,7 +234,7 @@ def facture_post_save(sender, **kwargs): @receiver(post_delete, sender=Facture) -def facture_post_delete(sender, **kwargs): +def facture_post_delete(**kwargs): """ Synchronise the LDAP user after an invoice has been deleted. """ @@ -228,7 +247,7 @@ class Vente(RevMixin, AclMixin, models.Model): """ The model defining a purchase. It consist of one type of article being sold. In particular there may be multiple purchases in a single invoice. - + It's reprensentated by: * an amount (the number of items sold) * an invoice (whose the purchase is part of) @@ -289,7 +308,6 @@ class Vente(RevMixin, AclMixin, models.Model): verbose_name = _l("Purchase") verbose_name_plural = _l("Purchases") - # TODO : change prix_total to total_price def prix_total(self): """ @@ -323,11 +341,13 @@ class Vente(RevMixin, AclMixin, models.Model): facture__in=Facture.objects.filter( user=self.facture.user ).exclude(valid=False)) - ).filter(Q(type_cotisation='All') | Q(type_cotisation=self.type_cotisation) + ).filter( + Q(type_cotisation='All') | + Q(type_cotisation=self.type_cotisation) ).filter( date_start__lt=date_start ).aggregate(Max('date_end'))['date_end__max'] - elif self.type_cotisation=="Adhesion": + elif self.type_cotisation == "Adhesion": end_cotisation = self.facture.user.end_adhesion() else: end_cotisation = self.facture.user.end_connexion() @@ -357,11 +377,16 @@ class Vente(RevMixin, AclMixin, models.Model): def can_edit(self, user_request, *args, **kwargs): if not user_request.has_perm('cotisations.change_vente'): return False, _("You don't have the right to edit the purchases.") - elif not user_request.has_perm('cotisations.change_all_facture') and not self.facture.user.can_edit(user_request, *args, **kwargs)[0]: - return False, _("You don't have the right to edit this user's purchases.") - elif not user_request.has_perm('cotisations.change_all_vente') and\ - (self.facture.control or not self.facture.valid): - return False, _("You don't have the right to edit a purchase already controlled or invalidated.") + elif (not user_request.has_perm('cotisations.change_all_facture') and + not self.facture.user.can_edit( + user_request, *args, **kwargs + )[0]): + return False, _("You don't have the right to edit this user's " + "purchases.") + elif (not user_request.has_perm('cotisations.change_all_vente') and + (self.facture.control or not self.facture.valid)): + return False, _("You don't have the right to edit a purchase " + "already controlled or invalidated.") else: return True, None @@ -369,16 +394,19 @@ class Vente(RevMixin, AclMixin, models.Model): if not user_request.has_perm('cotisations.delete_vente'): return False, _("You don't have the right to delete a purchase.") if not self.facture.user.can_edit(user_request, *args, **kwargs)[0]: - return False, _("You don't have the right to delete this user's purchases.") + return False, _("You don't have the right to delete this user's " + "purchases.") if self.facture.control or not self.facture.valid: - return False, _("You don't have the right to delete a purchase already controlled or invalidated.") + return False, _("You don't have the right to delete a purchase " + "already controlled or invalidated.") else: return True, None - def can_view(self, user_request, *args, **kwargs): - if not user_request.has_perm('cotisations.view_vente') and\ - self.facture.user != user_request: - return False, _("You don't have the right to see someone else's purchase history.") + def can_view(self, user_request, *_args, **_kwargs): + if (not user_request.has_perm('cotisations.view_vente') and + self.facture.user != user_request): + return False, _("You don't have the right to see someone " + "else's purchase history.") else: return True, None @@ -388,7 +416,7 @@ class Vente(RevMixin, AclMixin, models.Model): # TODO : change vente to purchase @receiver(post_save, sender=Vente) -def vente_post_save(sender, **kwargs): +def vente_post_save(**kwargs): """ Creates a 'cotisation' related object if needed and synchronise the LDAP user when a purchase has been saved. @@ -406,7 +434,7 @@ def vente_post_save(sender, **kwargs): # TODO : change vente to purchase @receiver(post_delete, sender=Vente) -def vente_post_delete(sender, **kwargs): +def vente_post_delete(**kwargs): """ Synchronise the LDAP user after a purchase has been deleted. """ @@ -418,12 +446,14 @@ def vente_post_delete(sender, **kwargs): class Article(RevMixin, AclMixin, models.Model): """ - The definition of an article model. It represents an type of object that can be sold to the user. - + The definition of an article model. It represents a type of object + that can be sold to the user. + It's represented by: * a name * a price - * a cotisation type (indicating if this article reprensents a cotisation or not) + * a cotisation type (indicating if this article reprensents a + cotisation or not) * a duration (if it is a cotisation) * a type of user (indicating what kind of user can buy this article) """ @@ -513,8 +543,8 @@ class Banque(RevMixin, AclMixin, models.Model): permissions = ( ('view_banque', _l("Can see a bank's details")), ) - verbose_name=_l("Bank") - verbose_name_plural=_l("Banks") + verbose_name = _l("Bank") + verbose_name_plural = _l("Banks") def __str__(self): return self.name @@ -530,7 +560,7 @@ class Paiement(RevMixin, AclMixin, models.Model): * a type (used for the type 'cheque' which implies the use of a bank and an account number in related models) """ - + PAYMENT_TYPES = ( (0, _l("Standard")), (1, _l("Cheque")), @@ -619,27 +649,32 @@ class Cotisation(RevMixin, AclMixin, models.Model): ('change_all_cotisation', _l("Can edit the previous cotisations")), ) - def can_edit(self, user_request, *args, **kwargs): + def can_edit(self, user_request, *_args, **_kwargs): if not user_request.has_perm('cotisations.change_cotisation'): return False, _("You don't have the right to edit a cotisation.") - elif not user_request.has_perm('cotisations.change_all_cotisation') and\ - (self.vente.facture.control or not self.vente.facture.valid): - return False, _("You don't have the right to edit a cotisation already controlled or invalidated.") + elif not user_request.has_perm('cotisations.change_all_cotisation') \ + and (self.vente.facture.control or + not self.vente.facture.valid): + return False, _("You don't have the right to edit a cotisation " + "already controlled or invalidated.") else: return True, None - def can_delete(self, user_request, *args, **kwargs): + def can_delete(self, user_request, *_args, **_kwargs): if not user_request.has_perm('cotisations.delete_cotisation'): - return False, _("You don't have the right to delete a cotisation.") + return False, _("You don't have the right to delete a " + "cotisation.") if self.vente.facture.control or not self.vente.facture.valid: - return False, _("You don't have the right to delete a cotisation already controlled or invalidated.") + return False, _("You don't have the right to delete a cotisation " + "already controlled or invalidated.") else: return True, None - def can_view(self, user_request, *args, **kwargs): + def can_view(self, user_request, *_args, **_kwargs): if not user_request.has_perm('cotisations.view_cotisation') and\ - self.vente.facture.user != user_request: - return False, _("You don't have the right to see someone else's cotisation history.") + self.vente.facture.user != user_request: + return False, _("You don't have the right to see someone else's " + "cotisation history.") else: return True, None @@ -648,7 +683,7 @@ class Cotisation(RevMixin, AclMixin, models.Model): @receiver(post_save, sender=Cotisation) -def cotisation_post_save(sender, **kwargs): +def cotisation_post_save(**_kwargs): """ Mark some services as needing a regeneration after the edition of a cotisation. Indeed the membership status may have changed. @@ -659,13 +694,11 @@ def cotisation_post_save(sender, **kwargs): regen('mailing') -# TODO : should be name cotisation_post_delete @receiver(post_delete, sender=Cotisation) -def vente_post_delete(sender, **kwargs): +def cotisation_post_delete(**_kwargs): """ Mark some services as needing a regeneration after the deletion of a cotisation. Indeed the membership status may have changed. """ - cotisation = kwargs['instance'] regen('mac_ip_list') regen('mailing') diff --git a/cotisations/payment.py b/cotisations/payment.py index 652037fb..b03e1c55 100644 --- a/cotisations/payment.py +++ b/cotisations/payment.py @@ -2,6 +2,9 @@ Here are defined some views dedicated to online payement. """ + +from collections import OrderedDict + from django.urls import reverse from django.shortcuts import redirect, get_object_or_404 from django.contrib.auth.decorators import login_required @@ -11,12 +14,11 @@ from django.utils.datastructures import MultiValueDictKeyError from django.utils.translation import ugettext as _ from django.http import HttpResponse, HttpResponseBadRequest -from collections import OrderedDict - from preferences.models import AssoOption from .models import Facture from .payment_utils.comnpay import Payment as ComnpayPayment + @csrf_exempt @login_required def accept_payment(request, factureid): @@ -30,7 +32,10 @@ def accept_payment(request, factureid): 'amount': facture.prix() } ) - return redirect(reverse('users:profil', kwargs={'userid':request.user.id})) + return redirect(reverse( + 'users:profil', + kwargs={'userid': request.user.id} + )) @csrf_exempt @@ -43,7 +48,11 @@ def refuse_payment(request): request, _("The payment has been refused.") ) - return redirect(reverse('users:profil', kwargs={'userid':request.user.id})) + return redirect(reverse( + 'users:profil', + kwargs={'userid': request.user.id} + )) + @csrf_exempt def ipn(request): @@ -105,7 +114,7 @@ def comnpay(facture, request): str(AssoOption.get_cached_value('payment_pass')), 'https://' + host + reverse( 'cotisations:accept_payment', - kwargs={'factureid':facture.id} + kwargs={'factureid': facture.id} ), 'https://' + host + reverse('cotisations:refuse_payment'), 'https://' + host + reverse('cotisations:ipn'), @@ -113,20 +122,20 @@ def comnpay(facture, request): "D" ) r = { - 'action' : 'https://secure.homologation.comnpay.com', - 'method' : 'POST', - 'content' : p.buildSecretHTML( + 'action': 'https://secure.homologation.comnpay.com', + 'method': 'POST', + 'content': p.buildSecretHTML( "Rechargement du solde", facture.prix(), idTransaction=str(facture.id) ), - 'amount' : facture.prix, + 'amount': facture.prix, } return r # The payment systems supported by re2o PAYMENT_SYSTEM = { - 'COMNPAY' : comnpay, - 'NONE' : None + 'COMNPAY': comnpay, + 'NONE': None } diff --git a/cotisations/payment_utils/comnpay.py b/cotisations/payment_utils/comnpay.py index 6c1701d3..6c73463f 100644 --- a/cotisations/payment_utils/comnpay.py +++ b/cotisations/payment_utils/comnpay.py @@ -1,21 +1,22 @@ +"""cotisations.payment_utils.comnpay +The module in charge of handling the negociation with Comnpay +for online payment +""" + import time from random import randrange import base64 import hashlib from collections import OrderedDict -from itertools import chain + class Payment(): + """ The class representing a transaction with all the functions + used during the negociation + """ - vad_number = "" - secret_key = "" - urlRetourOK = "" - urlRetourNOK = "" - urlIPN = "" - source = "" - typeTr = "D" - - def __init__(self, vad_number = "", secret_key = "", urlRetourOK = "", urlRetourNOK = "", urlIPN = "", source="", typeTr="D"): + def __init__(self, vad_number="", secret_key="", urlRetourOK="", + urlRetourNOK="", urlIPN="", source="", typeTr="D"): self.vad_number = vad_number self.secret_key = secret_key self.urlRetourOK = urlRetourOK @@ -23,46 +24,63 @@ class Payment(): self.urlIPN = urlIPN self.source = source self.typeTr = typeTr + self.idTransaction = "" - def buildSecretHTML(self, produit="Produit", montant="0.00", idTransaction=""): + def buildSecretHTML(self, produit="Produit", montant="0.00", + idTransaction=""): + """ Build an HTML hidden form with the different parameters for the + transaction + """ if idTransaction == "": - self.idTransaction = str(time.time())+self.vad_number+str(randrange(999)) + self.idTransaction = str(time.time()) + self.idTransaction += self.vad_number + self.idTransaction += str(randrange(999)) else: self.idTransaction = idTransaction - array_tpe = OrderedDict( - montant= str(montant), - idTPE= self.vad_number, - idTransaction= self.idTransaction, - devise= "EUR", - lang= 'fr', - nom_produit= produit, - source= self.source, - urlRetourOK= self.urlRetourOK, - urlRetourNOK= self.urlRetourNOK, - typeTr= str(self.typeTr) + array_tpe = OrderedDict( + montant=str(montant), + idTPE=self.vad_number, + idTransaction=self.idTransaction, + devise="EUR", + lang='fr', + nom_produit=produit, + source=self.source, + urlRetourOK=self.urlRetourOK, + urlRetourNOK=self.urlRetourNOK, + typeTr=str(self.typeTr) ) - if self.urlIPN!="": + if self.urlIPN != "": array_tpe['urlIPN'] = self.urlIPN - array_tpe['key'] = self.secret_key; - strWithKey = base64.b64encode(bytes('|'.join(array_tpe.values()), 'utf-8')) + array_tpe['key'] = self.secret_key + strWithKey = base64.b64encode(bytes( + '|'.join(array_tpe.values()), + 'utf-8' + )) del array_tpe["key"] array_tpe['sec'] = hashlib.sha512(strWithKey).hexdigest() ret = "" for key in array_tpe: - ret += '' + ret += ''.format( + k=key, + v=array_tpe[key] + ) return ret - def validSec(self, values, secret_key): + @staticmethod + def validSec(values, secret_key): + """ Check if the secret value is correct """ if "sec" in values: sec = values['sec'] del values["sec"] - strWithKey = hashlib.sha512(base64.b64encode(bytes('|'.join(values.values()) +"|"+secret_key, 'utf-8'))).hexdigest() + strWithKey = hashlib.sha512(base64.b64encode(bytes( + '|'.join(values.values()) + "|" + secret_key, + 'utf-8' + ))).hexdigest() return strWithKey.upper() == sec.upper() else: return False - diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index d708b407..dc8648ac 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -34,6 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} + {% if articlesformset %}

{% trans "Invoice's articles" %}

{{ articlesformset.management_form }} @@ -54,11 +55,12 @@ with this program; if not, write to the Free Software Foundation, Inc., Total price : 0,00 € {% endblocktrans %}

+ {% endif %} {% bootstrap_form factureform %} {% bootstrap_button action_name button_type='submit' icon='star' %} - +{% if articlesformset %} - +{% endif %} {% endblock %} diff --git a/cotisations/tests.py b/cotisations/tests.py index 21fa6d24..46b9d708 100644 --- a/cotisations/tests.py +++ b/cotisations/tests.py @@ -19,7 +19,10 @@ # 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. +"""cotisations.tests +The tests for the Cotisations module. +""" -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/cotisations/tex.py b/cotisations/tex.py index fb50d05b..f456fe8a 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -24,40 +24,44 @@ Module in charge of rendering some LaTex templates. Used to generated PDF invoice. """ +import tempfile +from subprocess import Popen, PIPE +import os +from datetime import datetime + from django.template.loader import get_template from django.template import Context from django.http import HttpResponse from django.conf import settings from django.utils.text import slugify -import tempfile -from subprocess import Popen, PIPE -import os - TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-') CACHE_PREFIX = getattr(settings, 'TEX_CACHE_PREFIX', 'render-tex') CACHE_TIMEOUT = getattr(settings, 'TEX_CACHE_TIMEOUT', 86400) # 1 day -def render_invoice(request, ctx={}): +def render_invoice(_request, ctx={}): """ Render an invoice using some available information such as the current date, the user, the articles, the prices, ... """ filename = '_'.join([ - 'invoice', - slugify(ctx['asso_name']), - slugify(ctx['recipient_name']), - str(ctx['DATE'].year), - str(ctx['DATE'].month), - str(ctx['DATE'].day), + 'invoice', + slugify(ctx.get('asso_name', "")), + slugify(ctx.get('recipient_name', "")), + str(ctx.get('DATE', datetime.now()).year), + str(ctx.get('DATE', datetime.now()).month), + str(ctx.get('DATE', datetime.now()).day), ]) - r = render_tex(request, 'cotisations/factures.tex', ctx) - r['Content-Disposition'] = ''.join(['attachment; filename="',filename,'.pdf"']) + r = render_tex(_request, 'cotisations/factures.tex', ctx) + r['Content-Disposition'] = 'attachment; filename="{name}.pdf"'.format( + name=filename + ) return r -def render_tex(request, template, ctx={}): + +def render_tex(_request, template, ctx={}): """ Creates a PDF from a LaTex templates using pdflatex. Writes it in a temporary directory and send back an HTTP response for @@ -66,13 +70,13 @@ def render_tex(request, template, ctx={}): context = Context(ctx) template = get_template(template) rendered_tpl = template.render(context).encode('utf-8') - + with tempfile.TemporaryDirectory() as tempdir: for i in range(2): process = Popen( ['pdflatex', '-output-directory', tempdir], - stdin = PIPE, - stdout = PIPE, + stdin=PIPE, + stdout=PIPE, ) process.communicate(rendered_tpl) with open(os.path.join(tempdir, 'texput.pdf'), 'rb') as f: diff --git a/cotisations/urls.py b/cotisations/urls.py index 0040e48c..6eafe721 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -19,6 +19,9 @@ # 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. +"""cotisations.urls +The defined URLs for the Cotisations app +""" from __future__ import unicode_literals @@ -29,107 +32,131 @@ from . import views from . import payment urlpatterns = [ - url(r'^new_facture/(?P[0-9]+)$', + url( + r'^new_facture/(?P[0-9]+)$', views.new_facture, name='new-facture' - ), - url(r'^edit_facture/(?P[0-9]+)$', + ), + url( + r'^edit_facture/(?P[0-9]+)$', views.edit_facture, name='edit-facture' - ), - url(r'^del_facture/(?P[0-9]+)$', + ), + url( + r'^del_facture/(?P[0-9]+)$', views.del_facture, name='del-facture' - ), - url(r'^facture_pdf/(?P[0-9]+)$', + ), + url( + r'^facture_pdf/(?P[0-9]+)$', views.facture_pdf, name='facture-pdf' - ), - url(r'^new_facture_pdf/$', + ), + url( + r'^new_facture_pdf/$', views.new_facture_pdf, name='new-facture-pdf' - ), - url(r'^credit_solde/(?P[0-9]+)$', + ), + url( + r'^credit_solde/(?P[0-9]+)$', views.credit_solde, name='credit-solde' - ), - url(r'^add_article/$', + ), + url( + r'^add_article/$', views.add_article, name='add-article' - ), - url(r'^edit_article/(?P[0-9]+)$', + ), + url( + r'^edit_article/(?P[0-9]+)$', views.edit_article, name='edit-article' - ), - url(r'^del_article/$', + ), + url( + r'^del_article/$', views.del_article, name='del-article' - ), - url(r'^add_paiement/$', + ), + url( + r'^add_paiement/$', views.add_paiement, name='add-paiement' - ), - url(r'^edit_paiement/(?P[0-9]+)$', + ), + url( + r'^edit_paiement/(?P[0-9]+)$', views.edit_paiement, name='edit-paiement' - ), - url(r'^del_paiement/$', + ), + url( + r'^del_paiement/$', views.del_paiement, name='del-paiement' - ), - url(r'^add_banque/$', + ), + url( + r'^add_banque/$', views.add_banque, name='add-banque' - ), - url(r'^edit_banque/(?P[0-9]+)$', + ), + url( + r'^edit_banque/(?P[0-9]+)$', views.edit_banque, name='edit-banque' - ), - url(r'^del_banque/$', + ), + url( + r'^del_banque/$', views.del_banque, name='del-banque' - ), - url(r'^index_article/$', + ), + url( + r'^index_article/$', views.index_article, name='index-article' - ), - url(r'^index_banque/$', + ), + url( + r'^index_banque/$', views.index_banque, name='index-banque' - ), - url(r'^index_paiement/$', + ), + url( + r'^index_paiement/$', views.index_paiement, name='index-paiement' - ), + ), url( r'history/(?P\w+)/(?P[0-9]+)$', re2o.views.history, name='history', - kwargs={'application':'cotisations'}, + kwargs={'application': 'cotisations'}, ), - url(r'^control/$', + url( + r'^control/$', views.control, name='control' - ), - url(r'^new_facture_solde/(?P[0-9]+)$', + ), + url( + r'^new_facture_solde/(?P[0-9]+)$', views.new_facture_solde, name='new_facture_solde' - ), - url(r'^recharge/$', + ), + url( + r'^recharge/$', views.recharge, name='recharge' - ), - url(r'^payment/accept/(?P[0-9]+)$', + ), + url( + r'^payment/accept/(?P[0-9]+)$', payment.accept_payment, name='accept_payment' - ), - url(r'^payment/refuse/$', + ), + url( + r'^payment/refuse/$', payment.refuse_payment, name='refuse_payment' - ), - url(r'^payment/ipn/$', + ), + url( + r'^payment/ipn/$', payment.ipn, name='ipn' - ), + ), url(r'^$', views.index, name='index'), ] diff --git a/cotisations/views.py b/cotisations/views.py index bb2540ae..78ee0d74 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -23,22 +23,23 @@ # App de gestion des users pour re2o # Goulven Kermarec, Gabriel Détraz # Gplv2 +"""cotisations.views +The different views used in the Cotisations module +""" + from __future__ import unicode_literals import os from django.urls import reverse from django.shortcuts import render, redirect -from django.core.validators import MaxValueValidator -from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.decorators import login_required from django.contrib import messages from django.db.models import ProtectedError -from django.db import transaction from django.db.models import Q from django.forms import modelformset_factory, formset_factory from django.utils import timezone from django.utils.translation import ugettext as _ -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.debug import sensitive_variables + # Import des models, forms et fonctions re2o from reversion import revisions as reversion from users.models import User @@ -70,14 +71,12 @@ from .forms import ( SelectUserArticleForm, SelectClubArticleForm, CreditSoldeForm, - NewFactureSoldeForm, RechargeForm ) from . import payment as online_payment from .tex import render_invoice - @login_required @can_create(Facture) @can_edit(User) @@ -102,9 +101,13 @@ def new_facture(request, user, userid): # Building the invocie form and the article formset invoice_form = NewFactureForm(request.POST or None, instance=invoice) if request.user.is_class_club: - article_formset = formset_factory(SelectClubArticleForm)(request.POST or None) + article_formset = formset_factory(SelectClubArticleForm)( + request.POST or None + ) else: - article_formset = formset_factory(SelectUserArticleForm)(request.POST or None) + article_formset = formset_factory(SelectUserArticleForm)( + request.POST or None + ) if invoice_form.is_valid() and article_formset.is_valid(): new_invoice_instance = invoice_form.save(commit=False) @@ -118,15 +121,18 @@ def new_facture(request, user, userid): # the authorized minimum (negative_balance) if user_balance: # TODO : change Paiement to Payment - if new_invoice_instance.paiement == Paiement.objects.get_or_create( - moyen='solde' - )[0]: + if new_invoice_instance.paiement == ( + Paiement.objects.get_or_create(moyen='solde')[0] + ): total_price = 0 for art_item in articles: if art_item.cleaned_data: - total_price += art_item.cleaned_data['article']\ - .prix*art_item.cleaned_data['quantity'] - if float(user.solde) - float(total_price) < negative_balance: + total_price += ( + art_item.cleaned_data['article'].prix * + art_item.cleaned_data['quantity'] + ) + if (float(user.solde) - float(total_price) + < negative_balance): messages.error( request, _("Your balance is too low for this operation.") @@ -194,7 +200,7 @@ def new_facture(request, user, userid): @can_change(Facture, 'pdf') def new_facture_pdf(request): """ - View used to generate a custom PDF invoice. It's mainly used to + View used to generate a custom PDF invoice. It's mainly used to get invoices that are not taken into account, for the administrative point of view. """ @@ -205,9 +211,13 @@ def new_facture_pdf(request): # Building the invocie form and the article formset invoice_form = NewFactureFormPdf(request.POST or None) if request.user.is_class_club: - articles_formset = formset_factory(SelectClubArticleForm)(request.POST or None) + articles_formset = formset_factory(SelectClubArticleForm)( + request.POST or None + ) else: - articles_formset = formset_factory(SelectUserArticleForm)(request.POST or None) + articles_formset = formset_factory(SelectUserArticleForm)( + request.POST or None + ) if invoice_form.is_valid() and articles_formset.is_valid(): # Get the article list and build an list out of it # contiaining (article_name, article_price, quantity, total_price) @@ -253,7 +263,7 @@ def new_facture_pdf(request): # TODO : change facture to invoice @login_required @can_view(Facture) -def facture_pdf(request, facture, factureid): +def facture_pdf(request, facture, **_kwargs): """ View used to generate a PDF file from an existing invoice in database Creates a line for each Purchase (thus article sold) and generate the @@ -296,14 +306,18 @@ def facture_pdf(request, facture, factureid): # TODO : change facture to invoice @login_required @can_edit(Facture) -def edit_facture(request, facture, factureid): +def edit_facture(request, facture, **_kwargs): """ View used to edit an existing invoice. Articles can be added or remove to the invoice and quantity can be set as desired. This is also the view used to invalidate an invoice. """ - invoice_form = EditFactureForm(request.POST or None, instance=facture, user=request.user) + invoice_form = EditFactureForm( + request.POST or None, + instance=facture, + user=request.user + ) purchases_objects = Vente.objects.filter(facture=facture) purchase_form_set = modelformset_factory( Vente, @@ -311,7 +325,10 @@ def edit_facture(request, facture, factureid): extra=0, max_num=len(purchases_objects) ) - purchase_form = purchase_form_set(request.POST or None, queryset=purchases_objects) + purchase_form = purchase_form_set( + request.POST or None, + queryset=purchases_objects + ) if invoice_form.is_valid() and purchase_form.is_valid(): if invoice_form.changed_data: invoice_form.save() @@ -330,7 +347,7 @@ def edit_facture(request, facture, factureid): # TODO : change facture to invoice @login_required @can_delete(Facture) -def del_facture(request, facture, factureid): +def del_facture(request, facture, **_kwargs): """ View used to delete an existing invocie. """ @@ -351,7 +368,7 @@ def del_facture(request, facture, factureid): @login_required @can_create(Facture) @can_edit(User) -def credit_solde(request, user, userid): +def credit_solde(request, user, **_kwargs): """ View used to edit the balance of a user. Can be use either to increase or decrease a user's balance. @@ -375,7 +392,7 @@ def credit_solde(request, user, userid): ) return redirect(reverse('cotisations:index')) return form({ - 'factureform': facture, + 'factureform': invoice, 'action_name': _("Edit") }, 'cotisations/facture.html', request) @@ -385,7 +402,7 @@ def credit_solde(request, user, userid): def add_article(request): """ View used to add an article. - + .. note:: If a purchase has already been sold, the price are calculated once and for all. That means even if the price of an article is edited later, it won't change the invoice. That is really important to keep @@ -408,7 +425,7 @@ def add_article(request): @login_required @can_edit(Article) -def edit_article(request, article_instance, articleid): +def edit_article(request, article_instance, **_kwargs): """ View used to edit an article. """ @@ -472,7 +489,7 @@ def add_paiement(request): # TODO : chnage paiement to Payment @login_required @can_edit(Paiement) -def edit_paiement(request, paiement_instance, paiementid): +def edit_paiement(request, paiement_instance, **_kwargs): """ View used to edit a payment method. """ @@ -550,7 +567,7 @@ def add_banque(request): # TODO : change banque to bank @login_required @can_edit(Banque) -def edit_banque(request, banque_instance, banqueid): +def edit_banque(request, banque_instance, **_kwargs): """ View used to edit a bank. """ @@ -613,7 +630,8 @@ def control(request): View used to control the invoices all at once. """ pagination_number = GeneralOption.get_cached_value('pagination_number') - invoice_list = Facture.objects.select_related('user').select_related('paiement') + invoice_list = (Facture.objects.select_related('user'). + select_related('paiement')) invoice_list = SortTable.sort( invoice_list, request.GET.get('col'), @@ -725,9 +743,13 @@ def new_facture_solde(request, userid): Q(type_user='All') | Q(type_user=request.user.class_name) ) if request.user.is_class_club: - article_formset = formset_factory(SelectClubArticleForm)(request.POST or None) + article_formset = formset_factory(SelectClubArticleForm)( + request.POST or None + ) else: - article_formset = formset_factory(SelectUserArticleForm)(request.POST or None) + article_formset = formset_factory(SelectUserArticleForm)( + request.POST or None + ) if article_formset.is_valid(): articles = article_formset @@ -826,7 +848,9 @@ def recharge(request): refill_form = RechargeForm(request.POST or None, user=request.user) if refill_form.is_valid(): invoice = Facture(user=request.user) - payment, _created = Paiement.objects.get_or_create(moyen='Rechargement en ligne') + payment, _created = Paiement.objects.get_or_create( + moyen='Rechargement en ligne' + ) invoice.paiement = payment invoice.valid = False invoice.save() @@ -837,7 +861,9 @@ def recharge(request): number=1 ) purchase.save() - content = online_payment.PAYMENT_SYSTEM[AssoOption.get_cached_value('payment')](invoice, request) + content = online_payment.PAYMENT_SYSTEM[ + AssoOption.get_cached_value('payment') + ](invoice, request) return render(request, 'cotisations/payment.html', content) return form({ 'rechargeform': refill_form diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 5d3195da..aafa0a50 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -1,4 +1,4 @@ -# ⁻*- mode: python; coding: utf-8 -*- +# -*- mode: python; coding: utf-8 -*- # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. @@ -35,13 +35,18 @@ https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_pyth Inspiré du travail de Daniel Stan au Crans """ +import os +import sys import logging -import netaddr -import radiusd # Module magique freeradius (radiusd.py is dummy) -import binascii -import hashlib -import os, sys +import radiusd # Module magique freeradius (radiusd.py is dummy) +from django.core.wsgi import get_wsgi_application +from django.db.models import Q + +from machines.models import Interface, IpList, Nas, Domain +from topologie.models import Port, Switch +from users.models import User +from preferences.models import OptionalTopologie proj_path = "/var/www/re2o/" # This is so Django knows where to find stuff. @@ -52,28 +57,17 @@ sys.path.append(proj_path) os.chdir(proj_path) # This is so models get loaded. -from django.core.wsgi import get_wsgi_application application = get_wsgi_application() -import argparse - -from django.db.models import Q -from machines.models import Interface, IpList, Nas, Domain -from topologie.models import Room, Port, Switch -from users.models import User -from preferences.models import OptionalTopologie - options, created = OptionalTopologie.objects.get_or_create() VLAN_NOK = options.vlan_decision_nok.vlan_id VLAN_OK = options.vlan_decision_ok.vlan_id - #: Serveur radius de test (pas la prod) TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False)) -## -*- Logging -*- - +# Logging class RadiusdHandler(logging.Handler): """Handler de logs pour freeradius""" @@ -87,6 +81,7 @@ class RadiusdHandler(logging.Handler): rad_sig = radiusd.L_DBG radiusd.radlog(rad_sig, record.msg) + # Initialisation d'un logger (pour logguer unifié) logger = logging.getLogger('auth.py') logger.setLevel(logging.DEBUG) @@ -95,10 +90,11 @@ handler = RadiusdHandler() handler.setFormatter(formatter) logger.addHandler(handler) + def radius_event(fun): """Décorateur pour les fonctions d'interfaces avec radius. - Une telle fonction prend un uniquement argument, qui est une liste de tuples - (clé, valeur) et renvoie un triplet dont les composantes sont : + Une telle fonction prend un uniquement argument, qui est une liste de + tuples (clé, valeur) et renvoie un triplet dont les composantes sont : * le code de retour (voir radiusd.RLM_MODULE_* ) * un tuple de couples (clé, valeur) pour les valeurs de réponse (accès ok et autres trucs du genre) @@ -109,7 +105,8 @@ def radius_event(fun): tuples en entrée en un dictionnaire.""" def new_f(auth_data): - if type(auth_data) == dict: + """ The function transforming the tuples as dict """ + if isinstance(auth_data, dict): data = auth_data else: data = dict() @@ -118,8 +115,8 @@ def radius_event(fun): # Ex: Calling-Station-Id: "une_adresse_mac" data[key] = value.replace('"', '') try: - # TODO s'assurer ici que les tuples renvoyés sont bien des (str,str) - # rlm_python ne digère PAS les unicodes + # TODO s'assurer ici que les tuples renvoyés sont bien des + # (str,str) : rlm_python ne digère PAS les unicodes return fun(data) except Exception as err: logger.error('Failed %r on data %r' % (err, auth_data)) @@ -128,7 +125,6 @@ def radius_event(fun): return new_f - @radius_event def instantiate(*_): """Utile pour initialiser les connexions ldap une première fois (otherwise, @@ -137,12 +133,15 @@ def instantiate(*_): if TEST_SERVER: logger.info(u'DBG_FREERADIUS is enabled') + @radius_event def authorize(data): """On test si on connait le calling nas: - - si le nas est inconnue, on suppose que c'est une requète 802.1X, on la traite + - si le nas est inconnue, on suppose que c'est une requète 802.1X, on la + traite - si le nas est connu, on applique 802.1X si le mode est activé - - si le nas est connu et si il s'agit d'un nas auth par mac, on repond accept en authorize + - si le nas est connu et si il s'agit d'un nas auth par mac, on repond + accept en authorize """ # Pour les requetes proxifiees, on split nas = data.get('NAS-IP-Address', data.get('NAS-Identifier', None)) @@ -155,30 +154,40 @@ def authorize(data): user = data.get('User-Name', '').decode('utf-8', errors='replace') user = user.split('@', 1)[0] mac = data.get('Calling-Station-Id', '') - result, log, password = check_user_machine_and_register(nas_type, user, mac) + result, log, password = check_user_machine_and_register( + nas_type, + user, + mac + ) logger.info(log.encode('utf-8')) logger.info(user.encode('utf-8')) if not result: return radiusd.RLM_MODULE_REJECT else: - return (radiusd.RLM_MODULE_UPDATED, - (), - ( - (str("NT-Password"), str(password)), - ), + return ( + radiusd.RLM_MODULE_UPDATED, + (), + ( + (str("NT-Password"), str(password)), + ), ) else: - return (radiusd.RLM_MODULE_UPDATED, - (), - ( - ("Auth-Type", "Accept"), - ), + return ( + radiusd.RLM_MODULE_UPDATED, + (), + ( + ("Auth-Type", "Accept"), + ), ) + @radius_event def post_auth(data): + """ Function called after the user is authenticated + """ + nas = data.get('NAS-IP-Address', data.get('NAS-Identifier', None)) nas_instance = find_nas_from_request(nas) # Toutes les reuquètes non proxifiées @@ -187,61 +196,89 @@ def post_auth(data): return radiusd.RLM_MODULE_OK nas_type = Nas.objects.filter(nas_type=nas_instance.type).first() if not nas_type: - logger.info(u"Type de nas non enregistré dans la bdd!".encode('utf-8')) + logger.info( + u"Type de nas non enregistré dans la bdd!".encode('utf-8') + ) return radiusd.RLM_MODULE_OK mac = data.get('Calling-Station-Id', None) - # Switch et bornes héritent de machine et peuvent avoir plusieurs interfaces filles + # Switch et bornes héritent de machine et peuvent avoir plusieurs + # interfaces filles nas_machine = nas_instance.machine # Si il s'agit d'un switch if hasattr(nas_machine, 'switch'): port = data.get('NAS-Port-Id', data.get('NAS-Port', None)) - #Pour les infrastructures possédant des switchs Juniper : - #On vérifie si le switch fait partie d'un stack Juniper + # Pour les infrastructures possédant des switchs Juniper : + # On vérifie si le switch fait partie d'un stack Juniper instance_stack = nas_machine.switch.stack if instance_stack: # Si c'est le cas, on resélectionne le bon switch dans la stack id_stack_member = port.split("-")[1].split('/')[0] - nas_machine = Switch.objects.filter(stack=instance_stack).filter(stack_member_id=id_stack_member).prefetch_related('interface_set__domain__extension').first() - # On récupère le numéro du port sur l'output de freeradius. La ligne suivante fonctionne pour cisco, HP et Juniper + nas_machine = (Switch.objects + .filter(stack=instance_stack) + .filter(stack_member_id=id_stack_member) + .prefetch_related( + 'interface_set__domain__extension' + ) + .first()) + # On récupère le numéro du port sur l'output de freeradius. + # La ligne suivante fonctionne pour cisco, HP et Juniper port = port.split(".")[0].split('/')[-1][-2:] out = decide_vlan_and_register_switch(nas_machine, nas_type, port, mac) sw_name, room, reason, vlan_id = out - log_message = '(fil) %s -> %s [%s%s]' % \ - (sw_name + u":" + port + u"/" + unicode(room), mac, vlan_id, (reason and u': ' + reason).encode('utf-8')) + log_message = '(fil) %s -> %s [%s%s]' % ( + sw_name + u":" + port + u"/" + str(room), + mac, + vlan_id, + (reason and u': ' + reason).encode('utf-8') + ) logger.info(log_message) # Filaire - return (radiusd.RLM_MODULE_UPDATED, + return ( + radiusd.RLM_MODULE_UPDATED, ( ("Tunnel-Type", "VLAN"), ("Tunnel-Medium-Type", "IEEE-802"), ("Tunnel-Private-Group-Id", '%d' % int(vlan_id)), ), () - ) + ) else: return radiusd.RLM_MODULE_OK + +# TODO : remove this function @radius_event def dummy_fun(_): """Do nothing, successfully. (C'est pour avoir un truc à mettre)""" return radiusd.RLM_MODULE_OK + def detach(_=None): """Appelé lors du déchargement du module (enfin, normalement)""" - print "*** goodbye from auth.py ***" + print("*** goodbye from auth.py ***") return radiusd.RLM_MODULE_OK + def find_nas_from_request(nas_id): - nas = Interface.objects.filter(Q(domain=Domain.objects.filter(name=nas_id)) | Q(ipv4=IpList.objects.filter(ipv4=nas_id))).select_related('type').select_related('machine__switch__stack') + """ Get the nas object from its ID """ + nas = (Interface.objects + .filter( + Q(domain=Domain.objects.filter(name=nas_id)) | + Q(ipv4=IpList.objects.filter(ipv4=nas_id)) + ) + .select_related('type') + .select_related('machine__switch__stack')) return nas.first() + def check_user_machine_and_register(nas_type, username, mac_address): - """ Verifie le username et la mac renseignee. L'enregistre si elle est inconnue. + """Verifie le username et la mac renseignee. L'enregistre si elle est + inconnue. Renvoie le mot de passe ntlm de l'user si tout est ok Utilise pour les authentifications en 802.1X""" interface = Interface.objects.filter(mac_address=mac_address).first() @@ -252,7 +289,10 @@ def check_user_machine_and_register(nas_type, username, mac_address): return (False, u"Adhérent non cotisant", '') if interface: if interface.machine.user != user: - return (False, u"Machine enregistrée sur le compte d'un autre user...", '') + return (False, + u"Machine enregistrée sur le compte d'un autre " + "user...", + '') elif not interface.is_active: return (False, u"Machine desactivée", '') elif not interface.ipv4: @@ -264,7 +304,9 @@ def check_user_machine_and_register(nas_type, username, mac_address): if nas_type.autocapture_mac: result, reason = user.autoregister_machine(mac_address, nas_type) if result: - return (True, u'Access Ok, Capture de la mac...', user.pwd_ntlm) + return (True, + u'Access Ok, Capture de la mac...', + user.pwd_ntlm) else: return (False, u'Erreur dans le register mac %s' % reason, '') else: @@ -273,8 +315,10 @@ def check_user_machine_and_register(nas_type, username, mac_address): return (False, u"Machine inconnue", '') -def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, mac_address): - """Fonction de placement vlan pour un switch en radius filaire auth par mac. +def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, + mac_address): + """Fonction de placement vlan pour un switch en radius filaire auth par + mac. Plusieurs modes : - nas inconnu, port inconnu : on place sur le vlan par defaut VLAN_OK - pas de radius sur le port : VLAN_OK @@ -292,7 +336,8 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, mac_addr - interface inconnue : - register mac désactivé : VLAN_NOK - register mac activé : - - dans la chambre associé au port, pas d'user ou non à jour : VLAN_NOK + - dans la chambre associé au port, pas d'user ou non à + jour : VLAN_NOK - user à jour, autocapture de la mac et VLAN_OK """ # Get port from switch and port number @@ -303,8 +348,13 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, mac_addr sw_name = str(nas_machine) - port = Port.objects.filter(switch=Switch.objects.filter(machine_ptr=nas_machine), port=port_number).first() - #Si le port est inconnu, on place sur le vlan defaut + port = (Port.objects + .filter( + switch=Switch.objects.filter(machine_ptr=nas_machine), + port=port_number + ) + .first()) + # Si le port est inconnu, on place sur le vlan defaut if not port: return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK) @@ -316,7 +366,10 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, mac_addr DECISION_VLAN = VLAN_OK if port.radius == 'NO': - return (sw_name, "", u"Pas d'authentification sur ce port" + extra_log, DECISION_VLAN) + return (sw_name, + "", + u"Pas d'authentification sur ce port" + extra_log, + DECISION_VLAN) if port.radius == 'BLOQ': return (sw_name, port.room, u'Port desactive', VLAN_NOK) @@ -326,7 +379,9 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, mac_addr if not room: return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK) - room_user = User.objects.filter(Q(club__room=port.room) | Q(adherent__room=port.room)) + room_user = User.objects.filter( + Q(club__room=port.room) | Q(adherent__room=port.room) + ) if not room_user: return (sw_name, room, u'Chambre non cotisante', VLAN_NOK) for user in room_user: @@ -336,35 +391,78 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, mac_addr if port.radius == 'COMMON' or port.radius == 'STRICT': # Authentification par mac - interface = Interface.objects.filter(mac_address=mac_address).select_related('machine__user').select_related('ipv4').first() + interface = (Interface.objects + .filter(mac_address=mac_address) + .select_related('machine__user') + .select_related('ipv4') + .first()) if not interface: room = port.room # On essaye de register la mac if not nas_type.autocapture_mac: return (sw_name, "", u'Machine inconnue', VLAN_NOK) elif not room: - return (sw_name, "Inconnue", u'Chambre et machine inconnues', VLAN_NOK) + return (sw_name, + "Inconnue", + u'Chambre et machine inconnues', + VLAN_NOK) else: if not room_user: - room_user = User.objects.filter(Q(club__room=port.room) | Q(adherent__room=port.room)) + room_user = User.objects.filter( + Q(club__room=port.room) | Q(adherent__room=port.room) + ) if not room_user: - return (sw_name, room, u'Machine et propriétaire de la chambre inconnus', VLAN_NOK) + return (sw_name, + room, + u'Machine et propriétaire de la chambre ' + 'inconnus', + VLAN_NOK) elif room_user.count() > 1: - return (sw_name, room, u'Machine inconnue, il y a au moins 2 users dans la chambre/local -> ajout de mac automatique impossible', VLAN_NOK) + return (sw_name, + room, + u'Machine inconnue, il y a au moins 2 users ' + 'dans la chambre/local -> ajout de mac ' + 'automatique impossible', + VLAN_NOK) elif not room_user.first().has_access(): - return (sw_name, room, u'Machine inconnue et adhérent non cotisant', VLAN_NOK) + return (sw_name, + room, + u'Machine inconnue et adhérent non cotisant', + VLAN_NOK) else: - result, reason = room_user.first().autoregister_machine(mac_address, nas_type) + result, reason = (room_user + .first() + .autoregister_machine( + mac_address, + nas_type + )) if result: - return (sw_name, room, u'Access Ok, Capture de la mac...' + extra_log, DECISION_VLAN) + return (sw_name, + room, + u'Access Ok, Capture de la mac: ' + extra_log, + DECISION_VLAN) else: - return (sw_name, room, u'Erreur dans le register mac %s' % reason + unicode(mac_address), VLAN_NOK) + return (sw_name, + room, + u'Erreur dans le register mac %s' % ( + reason + str(mac_address) + ), + VLAN_NOK) else: room = port.room if not interface.is_active: - return (sw_name, room, u'Machine non active / adherent non cotisant', VLAN_NOK) + return (sw_name, + room, + u'Machine non active / adherent non cotisant', + VLAN_NOK) elif not interface.ipv4: interface.assign_ipv4() - return (sw_name, room, u"Ok, Reassignation de l'ipv4" + extra_log, DECISION_VLAN) + return (sw_name, + room, + u"Ok, Reassignation de l'ipv4" + extra_log, + DECISION_VLAN) else: - return (sw_name, room, u'Machine OK' + extra_log, DECISION_VLAN) + return (sw_name, + room, + u'Machine OK' + extra_log, + DECISION_VLAN) diff --git a/logs/__init__.py b/logs/__init__.py index df6e4256..20338670 100644 --- a/logs/__init__.py +++ b/logs/__init__.py @@ -20,5 +20,8 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""logs +The app in charge of the stats and logs of what's happening in re2o +""" from .acl import * diff --git a/logs/acl.py b/logs/acl.py index 4a1417f6..1ec227d3 100644 --- a/logs/acl.py +++ b/logs/acl.py @@ -26,6 +26,7 @@ Here are defined some functions to check acl on the application. """ + def can_view(user): """Check if an user can view the application. diff --git a/logs/admin.py b/logs/admin.py deleted file mode 100644 index bcdc4f1d..00000000 --- a/logs/admin.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- mode: python; coding: utf-8 -*- -# Re2o est un logiciel d'administration développé initiallement au rezometz. Il -# se veut agnostique au réseau considéré, de manière à être installable en -# quelques clics. -# -# Copyright © 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. - -from __future__ import unicode_literals - -from django.contrib import admin - -# Register your models here. diff --git a/logs/models.py b/logs/models.py deleted file mode 100644 index 934704ba..00000000 --- a/logs/models.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- mode: python; coding: utf-8 -*- -# Re2o est un logiciel d'administration développé initiallement au rezometz. Il -# se veut agnostique au réseau considéré, de manière à être installable en -# quelques clics. -# -# Copyright © 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. - -from __future__ import unicode_literals - -from django.db import models - -# Create your models here. diff --git a/logs/templatetags/__init__.py b/logs/templatetags/__init__.py index fc1be5d7..cd256e09 100644 --- a/logs/templatetags/__init__.py +++ b/logs/templatetags/__init__.py @@ -20,4 +20,3 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - diff --git a/logs/templatetags/logs_extra.py b/logs/templatetags/logs_extra.py index 7bb238ba..97f60ec8 100644 --- a/logs/templatetags/logs_extra.py +++ b/logs/templatetags/logs_extra.py @@ -20,12 +20,16 @@ # 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. +"""logs.templatetags.logs_extra +A templatetag to get the class name for a given object +""" from django import template register = template.Library() + @register.filter def classname(obj): + """ Returns the object class name """ return obj.__class__.__name__ - diff --git a/logs/tests.py b/logs/tests.py index 21fa6d24..65679fa3 100644 --- a/logs/tests.py +++ b/logs/tests.py @@ -19,7 +19,10 @@ # 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. +"""logs.tests +The tests for the Logs module. +""" -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/logs/views.py b/logs/views.py index e07a2bb9..afb0a118 100644 --- a/logs/views.py +++ b/logs/views.py @@ -46,8 +46,6 @@ from django.db.models import Count, Max from reversion.models import Revision from reversion.models import Version, ContentType -from time import time - from users.models import ( User, ServiceUser, @@ -109,15 +107,6 @@ from re2o.acl import ( from re2o.utils import all_active_assigned_interfaces_count from re2o.utils import all_active_interfaces_count, SortTable -STATS_DICT = { - 0: ["Tout", 36], - 1: ["1 mois", 1], - 2: ["2 mois", 2], - 3: ["6 mois", 6], - 4: ["1 an", 12], - 5: ["2 an", 24], -} - @login_required @can_view_app('logs') @@ -227,66 +216,99 @@ def stats_general(request): _all_baned = all_baned() _all_whitelisted = all_whitelisted() _all_active_interfaces_count = all_active_interfaces_count() - _all_active_assigned_interfaces_count = all_active_assigned_interfaces_count() + _all_active_assigned_interfaces_count = \ + all_active_assigned_interfaces_count() stats = [ - [["Categorie", "Nombre d'utilisateurs (total club et adhérents)", "Nombre d'adhérents", "Nombre de clubs"], { - 'active_users': [ - "Users actifs", - User.objects.filter(state=User.STATE_ACTIVE).count(), - Adherent.objects.filter(state=Adherent.STATE_ACTIVE).count(), - Club.objects.filter(state=Club.STATE_ACTIVE).count()], - 'inactive_users': [ - "Users désactivés", - User.objects.filter(state=User.STATE_DISABLED).count(), - Adherent.objects.filter(state=Adherent.STATE_DISABLED).count(), - Club.objects.filter(state=Club.STATE_DISABLED).count()], - 'archive_users': [ - "Users archivés", - User.objects.filter(state=User.STATE_ARCHIVE).count(), - Adherent.objects.filter(state=Adherent.STATE_ARCHIVE).count(), - Club.objects.filter(state=Club.STATE_ARCHIVE).count()], - 'adherent_users': [ - "Cotisant à l'association", - _all_adherent.count(), - _all_adherent.exclude(adherent__isnull=True).count(), - _all_adherent.exclude(club__isnull=True).count()], - 'connexion_users': [ - "Utilisateurs bénéficiant d'une connexion", - _all_has_access.count(), - _all_has_access.exclude(adherent__isnull=True).count(), - _all_has_access.exclude(club__isnull=True).count()], - 'ban_users': [ - "Utilisateurs bannis", - _all_baned.count(), - _all_baned.exclude(adherent__isnull=True).count(), - _all_baned.exclude(club__isnull=True).count()], - 'whitelisted_user': [ - "Utilisateurs bénéficiant d'une connexion gracieuse", - _all_whitelisted.count(), - _all_whitelisted.exclude(adherent__isnull=True).count(), - _all_whitelisted.exclude(club__isnull=True).count()], - 'actives_interfaces': [ - "Interfaces actives (ayant accès au reseau)", - _all_active_interfaces_count.count(), - _all_active_interfaces_count.exclude( - machine__user__adherent__isnull=True - ).count(), - _all_active_interfaces_count.exclude( - machine__user__club__isnull=True - ).count()], - 'actives_assigned_interfaces': [ - "Interfaces actives et assignées ipv4", - _all_active_assigned_interfaces_count.count(), - _all_active_assigned_interfaces_count.exclude( - machine__user__adherent__isnull=True - ).count(), - _all_active_assigned_interfaces_count.exclude( - machine__user__club__isnull=True - ).count()] - }], - [["Range d'ip", "Vlan", "Nombre d'ip totales", "Ip assignées", - "Ip assignées à une machine active", "Ip non assignées"], ip_dict] + [ # First set of data (about users) + [ # Headers + "Categorie", + "Nombre d'utilisateurs (total club et adhérents)", + "Nombre d'adhérents", + "Nombre de clubs" + ], + { # Data + 'active_users': [ + "Users actifs", + User.objects.filter(state=User.STATE_ACTIVE).count(), + (Adherent.objects + .filter(state=Adherent.STATE_ACTIVE) + .count()), + Club.objects.filter(state=Club.STATE_ACTIVE).count() + ], + 'inactive_users': [ + "Users désactivés", + User.objects.filter(state=User.STATE_DISABLED).count(), + (Adherent.objects + .filter(state=Adherent.STATE_DISABLED) + .count()), + Club.objects.filter(state=Club.STATE_DISABLED).count() + ], + 'archive_users': [ + "Users archivés", + User.objects.filter(state=User.STATE_ARCHIVE).count(), + (Adherent.objects + .filter(state=Adherent.STATE_ARCHIVE) + .count()), + Club.objects.filter(state=Club.STATE_ARCHIVE).count() + ], + 'adherent_users': [ + "Cotisant à l'association", + _all_adherent.count(), + _all_adherent.exclude(adherent__isnull=True).count(), + _all_adherent.exclude(club__isnull=True).count() + ], + 'connexion_users': [ + "Utilisateurs bénéficiant d'une connexion", + _all_has_access.count(), + _all_has_access.exclude(adherent__isnull=True).count(), + _all_has_access.exclude(club__isnull=True).count() + ], + 'ban_users': [ + "Utilisateurs bannis", + _all_baned.count(), + _all_baned.exclude(adherent__isnull=True).count(), + _all_baned.exclude(club__isnull=True).count() + ], + 'whitelisted_user': [ + "Utilisateurs bénéficiant d'une connexion gracieuse", + _all_whitelisted.count(), + _all_whitelisted.exclude(adherent__isnull=True).count(), + _all_whitelisted.exclude(club__isnull=True).count() + ], + 'actives_interfaces': [ + "Interfaces actives (ayant accès au reseau)", + _all_active_interfaces_count.count(), + (_all_active_interfaces_count + .exclude(machine__user__adherent__isnull=True) + .count()), + (_all_active_interfaces_count + .exclude(machine__user__club__isnull=True) + .count()) + ], + 'actives_assigned_interfaces': [ + "Interfaces actives et assignées ipv4", + _all_active_assigned_interfaces_count.count(), + (_all_active_assigned_interfaces_count + .exclude(machine__user__adherent__isnull=True) + .count()), + (_all_active_assigned_interfaces_count + .exclude(machine__user__club__isnull=True) + .count()) + ] + } + ], + [ # Second set of data (about ip adresses) + [ # Headers + "Range d'ip", + "Vlan", + "Nombre d'ip totales", + "Ip assignées", + "Ip assignées à une machine active", + "Ip non assignées" + ], + ip_dict # Data already prepared ] + ] return render(request, 'logs/stats_general.html', {'stats_list': stats}) @@ -313,11 +335,26 @@ def stats_models(request): 'whitelist': [Whitelist.PRETTY_NAME, Whitelist.objects.count()] }, 'Cotisations': { - 'factures': [Facture._meta.verbose_name.title(), Facture.objects.count()], - 'vente': [Vente._meta.verbose_name.title(), Vente.objects.count()], - 'cotisation': [Cotisation._meta.verbose_name.title(), Cotisation.objects.count()], - 'article': [Article._meta.verbose_name.title(), Article.objects.count()], - 'banque': [Banque._meta.verbose_name.title(), Banque.objects.count()], + 'factures': [ + Facture._meta.verbose_name.title(), + Facture.objects.count() + ], + 'vente': [ + Vente._meta.verbose_name.title(), + Vente.objects.count() + ], + 'cotisation': [ + Cotisation._meta.verbose_name.title(), + Cotisation.objects.count() + ], + 'article': [ + Article._meta.verbose_name.title(), + Article.objects.count() + ], + 'banque': [ + Banque._meta.verbose_name.title(), + Banque.objects.count() + ], }, 'Machines': { 'machine': [Machine.PRETTY_NAME, Machine.objects.count()], @@ -370,12 +407,6 @@ def stats_users(request): nombre de machines par user, d'etablissements par user, de moyens de paiements par user, de banque par user, de bannissement par user, etc""" - onglet = request.GET.get('onglet') - try: - _search_field = STATS_DICT[onglet] - except KeyError: - _search_field = STATS_DICT[0] - onglet = 0 stats = { 'Utilisateur': { 'Machines': User.objects.annotate( @@ -410,11 +441,7 @@ def stats_users(request): ).order_by('-num')[:10], }, } - return render(request, 'logs/stats_users.html', { - 'stats_list': stats, - 'stats_dict': STATS_DICT, - 'active_field': onglet - }) + return render(request, 'logs/stats_users.html', {'stats_list': stats}) @login_required @@ -432,14 +459,21 @@ def stats_actions(request): } return render(request, 'logs/stats_users.html', {'stats_list': stats}) + @login_required @can_view_app('users') def stats_droits(request): """Affiche la liste des droits et les users ayant chaque droit""" - depart=time() - stats_list={} - - for droit in ListRight.objects.all().select_related('group_ptr'): - stats_list[droit]=droit.user_set.all().annotate(num=Count('revision'),last=Max('revision__date_created')) + stats_list = {} - return render(request, 'logs/stats_droits.html', {'stats_list': stats_list}) + for droit in ListRight.objects.all().select_related('group_ptr'): + stats_list[droit] = droit.user_set.all().annotate( + num=Count('revision'), + last=Max('revision__date_created') + ) + + return render( + request, + 'logs/stats_droits.html', + {'stats_list': stats_list} + ) diff --git a/machines/__init__.py b/machines/__init__.py index df6e4256..f874399a 100644 --- a/machines/__init__.py +++ b/machines/__init__.py @@ -20,5 +20,8 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""machines +The app in charge of everything related to the machines, the interface, ... +""" from .acl import * diff --git a/machines/acl.py b/machines/acl.py index f77a93c7..1b74760c 100644 --- a/machines/acl.py +++ b/machines/acl.py @@ -26,6 +26,7 @@ Here are defined some functions to check acl on the application. """ + def can_view(user): """Check if an user can view the application. diff --git a/machines/admin.py b/machines/admin.py index 9a2d5133..0f85007c 100644 --- a/machines/admin.py +++ b/machines/admin.py @@ -20,6 +20,9 @@ # 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. +"""machines.admin +The objects, fields and datastructures visible in the Django admin view +""" from __future__ import unicode_literals @@ -44,74 +47,92 @@ from .models import ( class MachineAdmin(VersionAdmin): + """ Admin view of a Machine object """ pass class Ipv6ListAdmin(VersionAdmin): + """ Admin view of a Ipv6List object """ pass class IpTypeAdmin(VersionAdmin): + """ Admin view of a IpType object """ pass class MachineTypeAdmin(VersionAdmin): + """ Admin view of a MachineType object """ pass class VlanAdmin(VersionAdmin): + """ Admin view of a Vlan object """ pass class ExtensionAdmin(VersionAdmin): + """ Admin view of a Extension object """ pass class SOAAdmin(VersionAdmin): + """ Admin view of a SOA object """ pass class MxAdmin(VersionAdmin): + """ Admin view of a MX object """ pass class NsAdmin(VersionAdmin): + """ Admin view of a NS object """ pass class TxtAdmin(VersionAdmin): + """ Admin view of a TXT object """ pass class SrvAdmin(VersionAdmin): + """ Admin view of a SRV object """ pass class NasAdmin(VersionAdmin): + """ Admin view of a Nas object """ pass class IpListAdmin(VersionAdmin): + """ Admin view of a Ipv4List object """ pass class OuverturePortAdmin(VersionAdmin): + """ Admin view of a OuverturePort object """ pass class OuverturePortListAdmin(VersionAdmin): + """ Admin view of a OuverturePortList object """ pass class InterfaceAdmin(VersionAdmin): - list_display = ('machine','type','mac_address','ipv4','details') + """ Admin view of a Interface object """ + list_display = ('machine', 'type', 'mac_address', 'ipv4', 'details') class DomainAdmin(VersionAdmin): + """ Admin view of a Domain object """ list_display = ('interface_parent', 'name', 'extension', 'cname') class ServiceAdmin(VersionAdmin): + """ Admin view of a ServiceAdmin object """ list_display = ('service_type', 'min_time_regen', 'regular_time_regen') @@ -133,5 +154,3 @@ admin.site.register(Ipv6List, Ipv6ListAdmin) admin.site.register(Nas, NasAdmin) admin.site.register(OuverturePort, OuverturePortAdmin) admin.site.register(OuverturePortList, OuverturePortListAdmin) - - diff --git a/machines/forms.py b/machines/forms.py index 6ece03e8..a838719f 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -94,7 +94,8 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): self.fields['type'].label = 'Type de machine' self.fields['type'].empty_label = "Séléctionner un type de machine" if "ipv4" in self.fields: - self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4" + self.fields['ipv4'].empty_label = ("Assignation automatique de " + "l'ipv4") self.fields['ipv4'].queryset = IpList.objects.filter( interface__isnull=True ) @@ -136,10 +137,10 @@ class AliasForm(FormRevMixin, ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) user = kwargs.pop('user') super(AliasForm, self).__init__(*args, prefix=prefix, **kwargs) - can_use_all, reason = Extension.can_use_all(user) + can_use_all, _reason = Extension.can_use_all(user) if not can_use_all: self.fields['extension'].queryset = Extension.objects.filter( - need_infra=False + need_infra=False ) @@ -328,6 +329,7 @@ class MxForm(FormRevMixin, ModelForm): interface_parent=None ).select_related('extension') + class DelMxForm(FormRevMixin, Form): """Suppression d'un ou plusieurs MX""" mx = forms.ModelMultipleChoiceField( @@ -472,10 +474,14 @@ class ServiceForm(FormRevMixin, ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ServiceForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['servers'].queryset = Interface.objects.all()\ - .select_related('domain__extension') + self.fields['servers'].queryset = (Interface.objects.all() + .select_related( + 'domain__extension' + )) def save(self, commit=True): + # TODO : None of the parents of ServiceForm use the commit + # parameter in .save() instance = super(ServiceForm, self).save(commit=False) if commit: instance.save() diff --git a/machines/migrations/0078_auto_20180415_1252.py b/machines/migrations/0078_auto_20180415_1252.py new file mode 100644 index 00000000..909ae847 --- /dev/null +++ b/machines/migrations/0078_auto_20180415_1252.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-04-15 10:52 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0077_auto_20180409_2243'), + ] + + operations = [ + migrations.AlterField( + model_name='srv', + name='priority', + field=models.PositiveIntegerField(default=0, help_text="La priorité du serveur cible (valeur entière non négative, plus elle est faible, plus ce serveur sera utilisé s'il est disponible)", validators=[django.core.validators.MaxValueValidator(65535)]), + ), + ] diff --git a/machines/models.py b/machines/models.py index 5af11b36..4282d1f5 100644 --- a/machines/models.py +++ b/machines/models.py @@ -20,14 +20,17 @@ # 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. +"""machines.models +The models definitions for the Machines app +""" from __future__ import unicode_literals from datetime import timedelta import re -from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress from ipaddress import IPv6Address from itertools import chain +from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress from django.db import models from django.db.models.signals import post_save, post_delete @@ -40,7 +43,7 @@ from django.core.validators import MaxValueValidator from macaddress.fields import MACAddressField from re2o.field_permissions import FieldPermissionModelMixin -from re2o.mixins import AclMixin, RevMixin +from re2o.mixins import AclMixin, RevMixin import users.models import preferences.models @@ -63,23 +66,30 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): class Meta: permissions = ( ("view_machine", "Peut voir un objet machine quelquonque"), - ("change_machine_user", "Peut changer le propriétaire d'une machine"), + ("change_machine_user", + "Peut changer le propriétaire d'une machine"), ) - def get_instance(machineid, *args, **kwargs): + @classmethod + def get_instance(cls, machineid, *_args, **_kwargs): """Get the Machine instance with machineid. :param userid: The id :return: The user """ - return Machine.objects.get(pk=machineid) + return cls.objects.get(pk=machineid) def linked_objects(self): """Return linked objects : machine and domain. Usefull in history display""" - return chain(self.interface_set.all(), Domain.objects.filter(interface_parent__in=self.interface_set.all())) + return chain( + self.interface_set.all(), + Domain.objects.filter( + interface_parent__in=self.interface_set.all() + ) + ) @staticmethod - def can_change_user(user_request, *args, **kwargs): + def can_change_user(user_request, *_args, **_kwargs): """Checks if an user is allowed to change the user who owns a Machine. @@ -90,18 +100,22 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): A tuple with a boolean stating if edition is allowed and an explanation message. """ - return user_request.has_perm('machines.change_machine_user'), "Vous ne pouvez pas modifier l'utilisateur de la machine." + return (user_request.has_perm('machines.change_machine_user'), + "Vous ne pouvez pas modifier l'utilisateur de la machine.") - def can_view_all(user_request, *args, **kwargs): + @staticmethod + def can_view_all(user_request, *_args, **_kwargs): """Vérifie qu'on peut bien afficher l'ensemble des machines, droit particulier correspondant :param user_request: instance user qui fait l'edition :return: True ou False avec la raison de l'échec le cas échéant""" if not user_request.has_perm('machines.view_machine'): - return False, u"Vous ne pouvez pas afficher l'ensemble des machines sans permission" + return False, (u"Vous ne pouvez pas afficher l'ensemble des " + "machines sans permission") return True, None - def can_create(user_request, userid, *args, **kwargs): + @staticmethod + def can_create(user_request, userid, *_args, **_kwargs): """Vérifie qu'un user qui fait la requète peut bien créer la machine et n'a pas atteint son quota, et crée bien une machine à lui :param user_request: Utilisateur qui fait la requête @@ -111,17 +125,21 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): user = users.models.User.objects.get(pk=userid) except users.models.User.DoesNotExist: return False, u"Utilisateur inexistant" - max_lambdauser_interfaces = preferences.models.OptionalMachine.get_cached_value('max_lambdauser_interfaces') + max_lambdauser_interfaces = (preferences.models.OptionalMachine + .get_cached_value( + 'max_lambdauser_interfaces' + )) if not user_request.has_perm('machines.add_machine'): - if not preferences.models.OptionalMachine.get_cached_value('create_machine'): + if not (preferences.models.OptionalMachine + .get_cached_value('create_machine')): return False, u"Vous ne pouvez pas ajouter une machine" if user != user_request: - return False, u"Vous ne pouvez pas ajouter une machine à un\ - autre user que vous sans droit" + return False, (u"Vous ne pouvez pas ajouter une machine à un " + "autre user que vous sans droit") if user.user_interfaces().count() >= max_lambdauser_interfaces: - return False, u"Vous avez atteint le maximum d'interfaces\ - autorisées que vous pouvez créer vous même (%s) "\ - % max_lambdauser_interfaces + return False, (u"Vous avez atteint le maximum d'interfaces " + "autorisées que vous pouvez créer vous même " + "(%s) " % max_lambdauser_interfaces) return True, None def can_edit(self, user_request, *args, **kwargs): @@ -131,9 +149,15 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): :param user_request: instance user qui fait l'edition :return: True ou False avec la raison le cas échéant""" if self.user != user_request: - if not user_request.has_perm('machines.change_interface') or not self.user.can_edit(self.user, user_request, *args, **kwargs)[0]: - return False, u"Vous ne pouvez pas éditer une machine\ - d'un autre user que vous sans droit" + if (not user_request.has_perm('machines.change_interface') or + not self.user.can_edit( + self.user, + user_request, + *args, + **kwargs + )[0]): + return False, (u"Vous ne pouvez pas éditer une machine " + "d'un autre user que vous sans droit") return True, None def can_delete(self, user_request, *args, **kwargs): @@ -143,26 +167,33 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): :param user_request: instance user qui fait l'edition :return: True ou False avec la raison de l'échec le cas échéant""" if self.user != user_request: - if not user_request.has_perm('machines.change_interface') or not self.user.can_edit(self.user, user_request, *args, **kwargs)[0]: - return False, u"Vous ne pouvez pas éditer une machine\ - d'un autre user que vous sans droit" + if (not user_request.has_perm('machines.change_interface') or + not self.user.can_edit( + self.user, + user_request, + *args, + **kwargs + )[0]): + return False, (u"Vous ne pouvez pas éditer une machine " + "d'un autre user que vous sans droit") return True, None - def can_view(self, user_request, *args, **kwargs): + def can_view(self, user_request, *_args, **_kwargs): """Vérifie qu'on peut bien voir cette instance particulière (soit machine de soi, soit droit particulier :param self: instance machine à éditer :param user_request: instance user qui fait l'edition :return: True ou False avec la raison de l'échec le cas échéant""" - if not user_request.has_perm('machines.view_machine') and self.user != user_request: - return False, u"Vous n'avez pas droit de voir les machines autre\ - que les vôtres" + if (not user_request.has_perm('machines.view_machine') and + self.user != user_request): + return False, (u"Vous n'avez pas droit de voir les machines autre " + "que les vôtres") return True, None def __init__(self, *args, **kwargs): super(Machine, self).__init__(*args, **kwargs) self.field_permissions = { - 'user' : self.can_change_user, + 'user': self.can_change_user, } def __str__(self): @@ -184,7 +215,8 @@ class MachineType(RevMixin, AclMixin, models.Model): class Meta: permissions = ( ("view_machinetype", "Peut voir un objet machinetype"), - ("use_all_machinetype", "Peut utiliser n'importe quel type de machine"), + ("use_all_machinetype", + "Peut utiliser n'importe quel type de machine"), ) def all_interfaces(self): @@ -192,7 +224,8 @@ class MachineType(RevMixin, AclMixin, models.Model): machinetype""" return Interface.objects.filter(type=self) - def can_use_all(user_request, *args, **kwargs): + @staticmethod + def can_use_all(user_request, *_args, **_kwargs): """Check if an user can use every MachineType. Args: @@ -202,7 +235,8 @@ class MachineType(RevMixin, AclMixin, models.Model): message is acces is not allowed. """ if not user_request.has_perm('machines.use_all_machinetype'): - return False, u"Vous n'avez pas le droit d'utiliser tout types de machines" + return False, (u"Vous n'avez pas le droit d'utiliser tout types " + "de machines") return True, None def __str__(self): @@ -300,7 +334,11 @@ class IpType(RevMixin, AclMixin, models.Model): if not self.prefix_v6: return else: - for ipv6 in Ipv6List.objects.filter(interface__in=Interface.objects.filter(type__in=MachineType.objects.filter(ip_type=self))): + for ipv6 in Ipv6List.objects.filter( + interface__in=Interface.objects.filter( + type__in=MachineType.objects.filter(ip_type=self) + ) + ): ipv6.check_and_replace_prefix(prefix=self.prefix_v6) def clean(self): @@ -329,8 +367,10 @@ class IpType(RevMixin, AclMixin, models.Model): self.clean() super(IpType, self).save(*args, **kwargs) - def can_use_all(user_request, *args, **kwargs): - """Superdroit qui permet d'utiliser toutes les extensions sans restrictions + @staticmethod + def can_use_all(user_request, *_args, **_kwargs): + """Superdroit qui permet d'utiliser toutes les extensions sans + restrictions :param user_request: instance user qui fait l'edition :return: True ou False avec la raison de l'échec le cas échéant""" return user_request.has_perm('machines.use_all_iptype'), None @@ -409,17 +449,17 @@ class SOA(RevMixin, AclMixin, models.Model): help_text='Email du contact pour la zone' ) refresh = models.PositiveIntegerField( - default=86400, # 24 hours + default=86400, # 24 hours help_text='Secondes avant que les DNS secondaires doivent demander le\ serial du DNS primaire pour détecter une modification' ) retry = models.PositiveIntegerField( - default=7200, # 2 hours + default=7200, # 2 hours help_text='Secondes avant que les DNS secondaires fassent une nouvelle\ demande de serial en cas de timeout du DNS primaire' ) expire = models.PositiveIntegerField( - default=3600000, # 1000 hours + default=3600000, # 1000 hours help_text='Secondes après lesquelles les DNS secondaires arrêtent de\ de répondre aux requêtes en cas de timeout du DNS primaire' ) @@ -469,8 +509,10 @@ class SOA(RevMixin, AclMixin, models.Model): extensions . /!\ Ne jamais supprimer ou renommer cette fonction car elle est utilisée dans les migrations de la BDD. """ - return cls.objects.get_or_create(name="SOA to edit", mail="postmaser@example.com")[0].pk - + return cls.objects.get_or_create( + name="SOA to edit", + mail="postmaser@example.com" + )[0].pk class Extension(RevMixin, AclMixin, models.Model): @@ -521,8 +563,10 @@ class Extension(RevMixin, AclMixin, models.Model): entry += "@ IN AAAA " + str(self.origin_v6) return entry - def can_use_all(user_request, *args, **kwargs): - """Superdroit qui permet d'utiliser toutes les extensions sans restrictions + @staticmethod + def can_use_all(user_request, *_args, **_kwargs): + """Superdroit qui permet d'utiliser toutes les extensions sans + restrictions :param user_request: instance user qui fait l'edition :return: True ou False avec la raison de l'échec le cas échéant""" return user_request.has_perm('machines.use_all_extension'), None @@ -555,7 +599,10 @@ class Mx(RevMixin, AclMixin, models.Model): def dns_entry(self): """Renvoie l'entrée DNS complète pour un MX à mettre dans les fichiers de zones""" - return "@ IN MX " + str(self.priority).ljust(3) + " " + str(self.name) + return "@ IN MX {prior} {name}".format( + prior=str(self.priority).ljust(3), + name=str(self.name) + ) def __str__(self): return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name) @@ -606,12 +653,13 @@ class Txt(RevMixin, AclMixin, models.Model): class Srv(RevMixin, AclMixin, models.Model): + """ A SRV record """ PRETTY_NAME = "Enregistrement Srv" TCP = 'TCP' UDP = 'UDP' - service = models.CharField(max_length=31) + service = models.CharField(max_length=31) protocole = models.CharField( max_length=3, choices=( @@ -628,9 +676,9 @@ class Srv(RevMixin, AclMixin, models.Model): priority = models.PositiveIntegerField( default=0, validators=[MaxValueValidator(65535)], - help_text="La priorité du serveur cible (valeur entière non négative,\ - plus elle est faible, plus ce serveur sera utilisé s'il est disponible)" - + help_text=("La priorité du serveur cible (valeur entière non " + "négative, plus elle est faible, plus ce serveur sera " + "utilisé s'il est disponible)") ) weight = models.PositiveIntegerField( default=0, @@ -667,7 +715,7 @@ class Srv(RevMixin, AclMixin, models.Model): str(self.port) + ' ' + str(self.target) + '.' -class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model): +class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): """ Une interface. Objet clef de l'application machine : - une address mac unique. Possibilité de la rendre unique avec le typemachine @@ -692,7 +740,8 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model): class Meta: permissions = ( ("view_interface", "Peut voir un objet interface"), - ("change_interface_machine", "Peut changer le propriétaire d'une interface"), + ("change_interface_machine", + "Peut changer le propriétaire d'une interface"), ) @cached_property @@ -719,7 +768,10 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model): prefix_v6 = self.type.ip_type.prefix_v6 if not prefix_v6: return None - return IPv6Address(IPv6Address(prefix_v6).exploded[:20] + IPv6Address(self.id).exploded[20:]) + return IPv6Address( + IPv6Address(prefix_v6).exploded[:20] + + IPv6Address(self.id).exploded[20:] + ) def sync_ipv6_dhcpv6(self): """Affecte une ipv6 dhcpv6 calculée à partir de l'id de la machine""" @@ -741,7 +793,9 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model): ipv6_slaac = self.ipv6_slaac if not ipv6_slaac: return - ipv6_object = Ipv6List.objects.filter(interface=self, slaac_ip=True).first() + ipv6_object = (Ipv6List.objects + .filter(interface=self, slaac_ip=True) + .first()) if not ipv6_object: ipv6_object = Ipv6List(interface=self, slaac_ip=True) if ipv6_object.ipv6 != str(ipv6_slaac): @@ -750,19 +804,24 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model): def sync_ipv6(self): """Cree et met à jour l'ensemble des ipv6 en fonction du mode choisi""" - if preferences.models.OptionalMachine.get_cached_value('ipv6_mode') == 'SLAAC': + if (preferences.models.OptionalMachine + .get_cached_value('ipv6_mode') == 'SLAAC'): self.sync_ipv6_slaac() - elif preferences.models.OptionalMachine.get_cached_value('ipv6_mode') == 'DHCPV6': + elif (preferences.models.OptionalMachine + .get_cached_value('ipv6_mode') == 'DHCPV6'): self.sync_ipv6_dhcpv6() else: return def ipv6(self): """ Renvoie le queryset de la liste des ipv6 - On renvoie l'ipv6 slaac que si le mode slaac est activé (et non dhcpv6)""" - if preferences.models.OptionalMachine.get_cached_value('ipv6_mode') == 'SLAAC': + On renvoie l'ipv6 slaac que si le mode slaac est activé + (et non dhcpv6)""" + if (preferences.models.OptionalMachine + .get_cached_value('ipv6_mode') == 'SLAAC'): return self.ipv6list.all() - elif preferences.models.OptionalMachine.get_cached_value('ipv6_mode') == 'DHCPV6': + elif (preferences.models.OptionalMachine + .get_cached_value('ipv6_mode') == 'DHCPV6'): return self.ipv6list.filter(slaac_ip=False) else: return None @@ -789,7 +848,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model): # instance. # But in our case, it's impossible to create a type value so we raise # the error. - if not hasattr(self, 'type') : + if not hasattr(self, 'type'): raise ValidationError("Le type d'ip choisi n'est pas valide") self.filter_macaddress() self.mac_address = str(EUI(self.mac_address)) or None @@ -825,7 +884,8 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model): correspondent pas") super(Interface, self).save(*args, **kwargs) - def can_create(user_request, machineid, *args, **kwargs): + @staticmethod + def can_create(user_request, machineid, *_args, **_kwargs): """Verifie que l'user a les bons droits infra pour créer une interface, ou bien que la machine appartient bien à l'user :param macineid: Id de la machine parente de l'interface @@ -836,21 +896,29 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model): except Machine.DoesNotExist: return False, u"Machine inexistante" if not user_request.has_perm('machines.add_interface'): - if not preferences.models.OptionalMachine.get_cached_value('create_machine'): + if not (preferences.models.OptionalMachine + .get_cached_value('create_machine')): return False, u"Vous ne pouvez pas ajouter une machine" - max_lambdauser_interfaces = preferences.models.OptionalMachine.get_cached_value('max_lambdauser_interfaces') + max_lambdauser_interfaces = (preferences.models.OptionalMachine + .get_cached_value( + 'max_lambdauser_interfaces' + )) if machine.user != user_request: return False, u"Vous ne pouvez pas ajouter une interface à une\ machine d'un autre user que vous sans droit" - if machine.user.user_interfaces().count() >= max_lambdauser_interfaces: + if (machine.user.user_interfaces().count() >= + max_lambdauser_interfaces): return False, u"Vous avez atteint le maximum d'interfaces\ autorisées que vous pouvez créer vous même (%s) "\ % max_lambdauser_interfaces return True, None @staticmethod - def can_change_machine(user_request, *args, **kwargs): - return user_request.has_perm('machines.change_interface_machine'), "Droit requis pour changer la machine" + def can_change_machine(user_request, *_args, **_kwargs): + """Check if a user can change the machine associated with an + Interface object """ + return (user_request.has_perm('machines.change_interface_machine'), + "Droit requis pour changer la machine") def can_edit(self, user_request, *args, **kwargs): """Verifie que l'user a les bons droits infra pour editer @@ -859,9 +927,14 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model): :param user_request: Utilisateur qui fait la requête :return: soit True, soit False avec la raison de l'échec""" if self.machine.user != user_request: - if not user_request.has_perm('machines.change_interface') or not self.machine.user.can_edit(user_request, *args, **kwargs)[0]: - return False, u"Vous ne pouvez pas éditer une machine\ - d'un autre user que vous sans droit" + if (not user_request.has_perm('machines.change_interface') or + not self.machine.user.can_edit( + user_request, + *args, + **kwargs + )[0]): + return False, (u"Vous ne pouvez pas éditer une machine " + "d'un autre user que vous sans droit") return True, None def can_delete(self, user_request, *args, **kwargs): @@ -871,26 +944,32 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model): :param user_request: Utilisateur qui fait la requête :return: soit True, soit False avec la raison de l'échec""" if self.machine.user != user_request: - if not user_request.has_perm('machines.change_interface') or not self.machine.user.can_edit(user_request, *args, **kwargs)[0]: - return False, u"Vous ne pouvez pas éditer une machine\ - d'un autre user que vous sans droit" + if (not user_request.has_perm('machines.change_interface') or + not self.machine.user.can_edit( + user_request, + *args, + **kwargs + )[0]): + return False, (u"Vous ne pouvez pas éditer une machine " + "d'un autre user que vous sans droit") return True, None - def can_view(self, user_request, *args, **kwargs): + def can_view(self, user_request, *_args, **_kwargs): """Vérifie qu'on peut bien voir cette instance particulière avec droit view objet ou qu'elle appartient à l'user :param self: instance interface à voir :param user_request: instance user qui fait l'edition :return: True ou False avec la raison de l'échec le cas échéant""" - if not user_request.has_perm('machines.view_interface') and self.machine.user != user_request: - return False, u"Vous n'avez pas le droit de voir des machines autre\ - que les vôtres" + if (not user_request.has_perm('machines.view_interface') and + self.machine.user != user_request): + return False, (u"Vous n'avez pas le droit de voir des machines " + "autre que les vôtres") return True, None def __init__(self, *args, **kwargs): super(Interface, self).__init__(*args, **kwargs) self.field_permissions = { - 'machine' : self.can_change_machine, + 'machine': self.can_change_machine, } def __str__(self): @@ -915,22 +994,29 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model): class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): + """ A list of IPv6 """ PRETTY_NAME = 'Enregistrements Ipv6 des machines' ipv6 = models.GenericIPAddressField( protocol='IPv6', unique=True ) - interface = models.ForeignKey('Interface', on_delete=models.CASCADE, related_name='ipv6list') + interface = models.ForeignKey( + 'Interface', + on_delete=models.CASCADE, + related_name='ipv6list' + ) slaac_ip = models.BooleanField(default=False) class Meta: permissions = ( ("view_ipv6list", "Peut voir un objet ipv6"), - ("change_ipv6list_slaac_ip", "Peut changer la valeur slaac sur une ipv6"), + ("change_ipv6list_slaac_ip", + "Peut changer la valeur slaac sur une ipv6"), ) - def can_create(user_request, interfaceid, *args, **kwargs): + @staticmethod + def can_create(user_request, interfaceid, *_args, **_kwargs): """Verifie que l'user a les bons droits infra pour créer une ipv6, ou possède l'interface associée :param interfaceid: Id de l'interface associée à cet objet domain @@ -947,8 +1033,10 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return True, None @staticmethod - def can_change_slaac_ip(user_request, *args, **kwargs): - return user_request.has_perm('machines.change_ipv6list_slaac_ip'), "Droit requis pour changer la valeur slaac ip" + def can_change_slaac_ip(user_request, *_args, **_kwargs): + """ Check if a user can change the slaac value """ + return (user_request.has_perm('machines.change_ipv6list_slaac_ip'), + "Droit requis pour changer la valeur slaac ip") def can_edit(self, user_request, *args, **kwargs): """Verifie que l'user a les bons droits infra pour editer @@ -957,9 +1045,14 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): :param user_request: Utilisateur qui fait la requête :return: soit True, soit False avec la raison de l'échec""" if self.interface.machine.user != user_request: - if not user_request.has_perm('machines.change_ipv6list') or not self.interface.machine.user.can_edit(user_request, *args, **kwargs)[0]: - return False, u"Vous ne pouvez pas éditer une machine\ - d'un autre user que vous sans droit" + if (not user_request.has_perm('machines.change_ipv6list') or + not self.interface.machine.user.can_edit( + user_request, + *args, + **kwargs + )[0]): + return False, (u"Vous ne pouvez pas éditer une machine " + "d'un autre user que vous sans droit") return True, None def can_delete(self, user_request, *args, **kwargs): @@ -969,26 +1062,32 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): :param user_request: Utilisateur qui fait la requête :return: soit True, soit False avec la raison de l'échec""" if self.interface.machine.user != user_request: - if not user_request.has_perm('machines.change_ipv6list') or not self.interface.machine.user.can_edit(user_request, *args, **kwargs)[0]: - return False, u"Vous ne pouvez pas éditer une machine\ - d'un autre user que vous sans droit" + if (not user_request.has_perm('machines.change_ipv6list') or + not self.interface.machine.user.can_edit( + user_request, + *args, + **kwargs + )[0]): + return False, (u"Vous ne pouvez pas éditer une machine " + "d'un autre user que vous sans droit") return True, None - def can_view(self, user_request, *args, **kwargs): + def can_view(self, user_request, *_args, **_kwargs): """Vérifie qu'on peut bien voir cette instance particulière avec droit view objet ou qu'elle appartient à l'user :param self: instance interface à voir :param user_request: instance user qui fait l'edition :return: True ou False avec la raison de l'échec le cas échéant""" - if not user_request.has_perm('machines.view_ipv6list') and self.interface.machine.user != user_request: - return False, u"Vous n'avez pas le droit de voir des machines autre\ - que les vôtres" + if (not user_request.has_perm('machines.view_ipv6list') and + self.interface.machine.user != user_request): + return False, (u"Vous n'avez pas le droit de voir des machines " + "autre que les vôtres") return True, None def __init__(self, *args, **kwargs): super(Ipv6List, self).__init__(*args, **kwargs) self.field_permissions = { - 'slaac_ip' : self.can_change_slaac_ip, + 'slaac_ip': self.can_change_slaac_ip, } def check_and_replace_prefix(self, prefix=None): @@ -996,17 +1095,27 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): prefix_v6 = prefix or self.interface.type.ip_type.prefix_v6 if not prefix_v6: return - if IPv6Address(self.ipv6).exploded[:20] != IPv6Address(prefix_v6).exploded[:20]: - self.ipv6 = IPv6Address(IPv6Address(prefix_v6).exploded[:20] + IPv6Address(self.ipv6).exploded[20:]) + if (IPv6Address(self.ipv6).exploded[:20] != + IPv6Address(prefix_v6).exploded[:20]): + self.ipv6 = IPv6Address( + IPv6Address(prefix_v6).exploded[:20] + + IPv6Address(self.ipv6).exploded[20:] + ) self.save() def clean(self, *args, **kwargs): - if self.slaac_ip and Ipv6List.objects.filter(interface=self.interface, slaac_ip=True).exclude(id=self.id): + if self.slaac_ip and (Ipv6List.objects + .filter(interface=self.interface, slaac_ip=True) + .exclude(id=self.id)): raise ValidationError("Une ip slaac est déjà enregistrée") prefix_v6 = self.interface.type.ip_type.prefix_v6 if prefix_v6: - if IPv6Address(self.ipv6).exploded[:20] != IPv6Address(prefix_v6).exploded[:20]: - raise ValidationError("Le prefixv6 est incorrect et ne correspond pas au type associé à la machine") + if (IPv6Address(self.ipv6).exploded[:20] != + IPv6Address(prefix_v6).exploded[:20]): + raise ValidationError( + "Le prefixv6 est incorrect et ne correspond pas au type " + "associé à la machine" + ) super(Ipv6List, self).clean(*args, **kwargs) def save(self, *args, **kwargs): @@ -1072,7 +1181,7 @@ class Domain(RevMixin, AclMixin, models.Model): if self.cname == self: raise ValidationError("On ne peut créer un cname sur lui même") HOSTNAME_LABEL_PATTERN = re.compile( - "(?!-)[A-Z\d-]+(?= max_lambdauser_aliases: - return False, u"Vous avez atteint le maximum d'alias\ - autorisés que vous pouvez créer vous même (%s) "\ - % max_lambdauser_aliases + return False, (u"Vous avez atteint le maximum d'alias " + "autorisés que vous pouvez créer vous même " + "(%s) " % max_lambdauser_aliases) return True, None - def can_edit(self, user_request, *args, **kwargs): + def can_edit(self, user_request, *_args, **_kwargs): """Verifie que l'user a les bons droits pour editer cette instance domain :param self: Instance domain à editer :param user_request: Utilisateur qui fait la requête :return: soit True, soit False avec la raison de l'échec""" - if not user_request.has_perm('machines.change_domain') and\ - self.get_source_interface.machine.user != user_request: - return False, u"Vous ne pouvez pas editer un alias à une machine\ - d'un autre user que vous sans droit" + if (not user_request.has_perm('machines.change_domain') and + self.get_source_interface.machine.user != user_request): + return False, (u"Vous ne pouvez pas editer un alias à une machine " + "d'un autre user que vous sans droit") return True, None - def can_delete(self, user_request, *args, **kwargs): + def can_delete(self, user_request, *_args, **_kwargs): """Verifie que l'user a les bons droits delete object pour del cette instance domain, ou qu'elle lui appartient :param self: Instance domain à del :param user_request: Utilisateur qui fait la requête :return: soit True, soit False avec la raison de l'échec""" - if not user_request.has_perm('machines.delete_domain') and\ - self.get_source_interface.machine.user != user_request: - return False, u"Vous ne pouvez pas supprimer un alias à une machine\ - d'un autre user que vous sans droit" + if (not user_request.has_perm('machines.delete_domain') and + self.get_source_interface.machine.user != user_request): + return False, (u"Vous ne pouvez pas supprimer un alias à une " + "machine d'un autre user que vous sans droit") return True, None - def can_view(self, user_request, *args, **kwargs): + def can_view(self, user_request, *_args, **_kwargs): """Vérifie qu'on peut bien voir cette instance particulière avec droit view objet ou qu'elle appartient à l'user :param self: instance domain à voir :param user_request: instance user qui fait l'edition :return: True ou False avec la raison de l'échec le cas échéant""" - if not user_request.has_perm('machines.view_domain') and\ - self.get_source_interface.machine.user != user_request: - return False, u"Vous n'avez pas le droit de voir des machines autre\ - que les vôtres" + if (not user_request.has_perm('machines.view_domain') and + self.get_source_interface.machine.user != user_request): + return False, (u"Vous n'avez pas le droit de voir des machines " + "autre que les vôtres") return True, None def __str__(self): @@ -1177,6 +1294,7 @@ class Domain(RevMixin, AclMixin, models.Model): class IpList(RevMixin, AclMixin, models.Model): + """ A list of IPv4 """ PRETTY_NAME = "Addresses ipv4" ipv4 = models.GenericIPAddressField(protocol='IPv4', unique=True) @@ -1307,15 +1425,15 @@ class OuverturePortList(RevMixin, AclMixin, models.Model): ("view_ouvertureportlist", "Peut voir un objet ouvertureport"), ) - def can_delete(self, user_request, *args, **kwargs): + def can_delete(self, user_request, *_args, **_kwargs): """Verifie que l'user a les bons droits bureau pour delete cette instance ouvertureportlist :param self: Instance ouvertureportlist à delete :param user_request: Utilisateur qui fait la requête :return: soit True, soit False avec la raison de l'échec""" if not user_request.has_perm('machines.delete_ouvertureportlist'): - return False, u"Vous n'avez pas le droit de supprimer une ouverture\ - de port" + return False, (u"Vous n'avez pas le droit de supprimer une " + "ouverture de port") if self.interface_set.all(): return False, u"Cette liste de ports est utilisée" return True, None @@ -1401,7 +1519,7 @@ class OuverturePort(RevMixin, AclMixin, models.Model): @receiver(post_save, sender=Machine) -def machine_post_save(sender, **kwargs): +def machine_post_save(**kwargs): """Synchronisation ldap et régen parefeu/dhcp lors de la modification d'une machine""" user = kwargs['instance'].user @@ -1411,7 +1529,7 @@ def machine_post_save(sender, **kwargs): @receiver(post_delete, sender=Machine) -def machine_post_delete(sender, **kwargs): +def machine_post_delete(**kwargs): """Synchronisation ldap et régen parefeu/dhcp lors de la suppression d'une machine""" machine = kwargs['instance'] @@ -1422,7 +1540,7 @@ def machine_post_delete(sender, **kwargs): @receiver(post_save, sender=Interface) -def interface_post_save(sender, **kwargs): +def interface_post_save(**kwargs): """Synchronisation ldap et régen parefeu/dhcp lors de la modification d'une interface""" interface = kwargs['instance'] @@ -1435,7 +1553,7 @@ def interface_post_save(sender, **kwargs): @receiver(post_delete, sender=Interface) -def interface_post_delete(sender, **kwargs): +def interface_post_delete(**kwargs): """Synchronisation ldap et régen parefeu/dhcp lors de la suppression d'une interface""" interface = kwargs['instance'] @@ -1444,7 +1562,7 @@ def interface_post_delete(sender, **kwargs): @receiver(post_save, sender=IpType) -def iptype_post_save(sender, **kwargs): +def iptype_post_save(**kwargs): """Generation des objets ip après modification d'un range ip""" iptype = kwargs['instance'] iptype.gen_ip_range() @@ -1452,7 +1570,7 @@ def iptype_post_save(sender, **kwargs): @receiver(post_save, sender=MachineType) -def machine_post_save(sender, **kwargs): +def machinetype_post_save(**kwargs): """Mise à jour des interfaces lorsque changement d'attribution d'une machinetype (changement iptype parent)""" machinetype = kwargs['instance'] @@ -1461,85 +1579,84 @@ def machine_post_save(sender, **kwargs): @receiver(post_save, sender=Domain) -def domain_post_save(sender, **kwargs): +def domain_post_save(**_kwargs): """Regeneration dns après modification d'un domain object""" regen('dns') @receiver(post_delete, sender=Domain) -def domain_post_delete(sender, **kwargs): +def domain_post_delete(**_kwargs): """Regeneration dns après suppression d'un domain object""" regen('dns') @receiver(post_save, sender=Extension) -def extension_post_save(sender, **kwargs): +def extension_post_save(**_kwargs): """Regeneration dns après modification d'une extension""" regen('dns') @receiver(post_delete, sender=Extension) -def extension_post_selete(sender, **kwargs): +def extension_post_selete(**_kwargs): """Regeneration dns après suppression d'une extension""" regen('dns') @receiver(post_save, sender=SOA) -def soa_post_save(sender, **kwargs): +def soa_post_save(**_kwargs): """Regeneration dns après modification d'un SOA""" regen('dns') @receiver(post_delete, sender=SOA) -def soa_post_delete(sender, **kwargs): +def soa_post_delete(**_kwargs): """Regeneration dns après suppresson d'un SOA""" regen('dns') @receiver(post_save, sender=Mx) -def mx_post_save(sender, **kwargs): +def mx_post_save(**_kwargs): """Regeneration dns après modification d'un MX""" regen('dns') @receiver(post_delete, sender=Mx) -def mx_post_delete(sender, **kwargs): +def mx_post_delete(**_kwargs): """Regeneration dns après suppresson d'un MX""" regen('dns') @receiver(post_save, sender=Ns) -def ns_post_save(sender, **kwargs): +def ns_post_save(**_kwargs): """Regeneration dns après modification d'un NS""" regen('dns') @receiver(post_delete, sender=Ns) -def ns_post_delete(sender, **kwargs): +def ns_post_delete(**_kwargs): """Regeneration dns après modification d'un NS""" regen('dns') @receiver(post_save, sender=Txt) -def text_post_save(sender, **kwargs): +def text_post_save(**_kwargs): """Regeneration dns après modification d'un TXT""" regen('dns') @receiver(post_delete, sender=Txt) -def text_post_delete(sender, **kwargs): +def text_post_delete(**_kwargs): """Regeneration dns après modification d'un TX""" regen('dns') @receiver(post_save, sender=Srv) -def srv_post_save(sender, **kwargs): +def srv_post_save(**_kwargs): """Regeneration dns après modification d'un SRV""" regen('dns') @receiver(post_delete, sender=Srv) -def text_post_delete(sender, **kwargs): +def srv_post_delete(**_kwargs): """Regeneration dns après modification d'un SRV""" regen('dns') - diff --git a/machines/serializers.py b/machines/serializers.py index 42ca679d..9476e9d0 100644 --- a/machines/serializers.py +++ b/machines/serializers.py @@ -21,7 +21,11 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -#Augustin Lemesle +# Augustin Lemesle +"""machines.serializers +Serializers for the Machines app +""" + from rest_framework import serializers from machines.models import ( @@ -29,28 +33,30 @@ from machines.models import ( IpType, Extension, IpList, - MachineType, Domain, Txt, Mx, Srv, Service_link, Ns, - OuverturePortList, OuverturePort, Ipv6List ) class IpTypeField(serializers.RelatedField): - """Serialisation d'une iptype, renvoie son evaluation str""" + """ Serializer for an IpType object field """ + def to_representation(self, value): return value.type + def to_internal_value(self, data): + pass + class IpListSerializer(serializers.ModelSerializer): - """Serialisation d'une iplist, ip_type etant une foreign_key, - on evalue sa methode str""" + """ Serializer for an Ipv4List obejct using the IpType serialization """ + ip_type = IpTypeField(read_only=True) class Meta: @@ -59,16 +65,19 @@ class IpListSerializer(serializers.ModelSerializer): class Ipv6ListSerializer(serializers.ModelSerializer): + """ Serializer for an Ipv6List object """ + class Meta: model = Ipv6List fields = ('ipv6', 'slaac_ip') class InterfaceSerializer(serializers.ModelSerializer): - """Serialisation d'une interface, ipv4, domain et extension sont - des foreign_key, on les override et on les evalue avec des fonctions - get_...""" + """ Serializer for an Interface object. Use SerializerMethodField + to get ForeignKey values """ + ipv4 = IpListSerializer(read_only=True) + # TODO : use serializer.RelatedField to avoid duplicate code mac_address = serializers.SerializerMethodField('get_macaddress') domain = serializers.SerializerMethodField('get_dns') extension = serializers.SerializerMethodField('get_interface_extension') @@ -77,20 +86,29 @@ class InterfaceSerializer(serializers.ModelSerializer): model = Interface fields = ('ipv4', 'mac_address', 'domain', 'extension') - def get_dns(self, obj): + @staticmethod + def get_dns(obj): + """ The name of the associated DNS object """ return obj.domain.name - def get_interface_extension(self, obj): + @staticmethod + def get_interface_extension(obj): + """ The name of the associated Interface object """ return obj.domain.extension.name - def get_macaddress(self, obj): + @staticmethod + def get_macaddress(obj): + """ The string representation of the associated MAC address """ return str(obj.mac_address) class FullInterfaceSerializer(serializers.ModelSerializer): - """Serialisation complete d'une interface avec les ipv6 en plus""" + """ Serializer for an Interface obejct. Use SerializerMethodField + to get ForeignKey values """ + ipv4 = IpListSerializer(read_only=True) ipv6 = Ipv6ListSerializer(read_only=True, many=True) + # TODO : use serializer.RelatedField to avoid duplicate code mac_address = serializers.SerializerMethodField('get_macaddress') domain = serializers.SerializerMethodField('get_dns') extension = serializers.SerializerMethodField('get_interface_extension') @@ -99,26 +117,36 @@ class FullInterfaceSerializer(serializers.ModelSerializer): model = Interface fields = ('ipv4', 'ipv6', 'mac_address', 'domain', 'extension') - def get_dns(self, obj): + @staticmethod + def get_dns(obj): + """ The name of the associated DNS object """ return obj.domain.name - def get_interface_extension(self, obj): + @staticmethod + def get_interface_extension(obj): + """ The name of the associated Extension object """ return obj.domain.extension.name - def get_macaddress(self, obj): + @staticmethod + def get_macaddress(obj): + """ The string representation of the associated MAC address """ return str(obj.mac_address) class ExtensionNameField(serializers.RelatedField): - """Evaluation str d'un objet extension (.example.org)""" + """ Serializer for Extension object field """ + def to_representation(self, value): return value.name + def to_internal_value(self, data): + pass + class TypeSerializer(serializers.ModelSerializer): - """Serialisation d'un iptype : extension et la liste des - ouvertures de port son evalués en get_... etant des - foreign_key ou des relations manytomany""" + """ Serializer for an IpType object. Use SerializerMethodField to + get ForeignKey values. Infos about the general port policy is added """ + extension = ExtensionNameField(read_only=True) ouverture_ports_tcp_in = serializers\ .SerializerMethodField('get_port_policy_input_tcp') @@ -136,7 +164,10 @@ class TypeSerializer(serializers.ModelSerializer): 'ouverture_ports_tcp_in', 'ouverture_ports_tcp_out', 'ouverture_ports_udp_in', 'ouverture_ports_udp_out',) - def get_port_policy(self, obj, protocole, io): + @staticmethod + def get_port_policy(obj, protocole, io): + """ Generic utility function to get the policy for a given + port, protocole and IN or OUT """ if obj.ouverture_ports is None: return [] return map( @@ -174,14 +205,20 @@ class ExtensionSerializer(serializers.ModelSerializer): model = Extension fields = ('name', 'origin', 'origin_v6', 'zone_entry', 'soa') - def get_origin_ip(self, obj): - return getattr(obj.origin, 'ipv4', None) + @staticmethod + def get_origin_ip(obj): + """ The IP of the associated origin for the zone """ + return obj.origin.ipv4 - def get_zone_name(self, obj): + @staticmethod + def get_zone_name(obj): + """ The name of the associated zone """ return str(obj.dns_entry) - def get_soa_data(self, obj): - return { 'mail': obj.soa.dns_soa_mail, 'param': obj.soa.dns_soa_param } + @staticmethod + def get_soa_data(obj): + """ The representation of the associated SOA """ + return {'mail': obj.soa.dns_soa_mail, 'param': obj.soa.dns_soa_param} class MxSerializer(serializers.ModelSerializer): @@ -195,13 +232,19 @@ class MxSerializer(serializers.ModelSerializer): model = Mx fields = ('zone', 'priority', 'name', 'mx_entry') - def get_entry_name(self, obj): + @staticmethod + def get_entry_name(obj): + """ The name of the DNS MX entry """ return str(obj.name) - def get_zone_name(self, obj): + @staticmethod + def get_zone_name(obj): + """ The name of the associated zone of the MX record """ return obj.zone.name - def get_mx_name(self, obj): + @staticmethod + def get_mx_name(obj): + """ The string representation of the entry to add to the DNS """ return str(obj.dns_entry) @@ -215,10 +258,14 @@ class TxtSerializer(serializers.ModelSerializer): model = Txt fields = ('zone', 'txt_entry', 'field1', 'field2') - def get_zone_name(self, obj): + @staticmethod + def get_zone_name(obj): + """ The name of the associated zone """ return str(obj.zone.name) - def get_txt_name(self, obj): + @staticmethod + def get_txt_name(obj): + """ The string representation of the entry to add to the DNS """ return str(obj.dns_entry) @@ -241,10 +288,14 @@ class SrvSerializer(serializers.ModelSerializer): 'srv_entry' ) - def get_extension_name(self, obj): + @staticmethod + def get_extension_name(obj): + """ The name of the associated extension """ return str(obj.extension.name) - def get_srv_name(self, obj): + @staticmethod + def get_srv_name(obj): + """ The string representation of the entry to add to the DNS """ return str(obj.dns_entry) @@ -259,13 +310,19 @@ class NsSerializer(serializers.ModelSerializer): model = Ns fields = ('zone', 'ns', 'ns_entry') - def get_zone_name(self, obj): + @staticmethod + def get_zone_name(obj): + """ The name of the associated zone """ return obj.zone.name - def get_domain_name(self, obj): + @staticmethod + def get_domain_name(obj): + """ The name of the associated NS target """ return str(obj.ns) - def get_text_name(self, obj): + @staticmethod + def get_text_name(obj): + """ The string representation of the entry to add to the DNS """ return str(obj.dns_entry) @@ -280,13 +337,19 @@ class DomainSerializer(serializers.ModelSerializer): model = Domain fields = ('name', 'extension', 'cname', 'cname_entry') - def get_zone_name(self, obj): + @staticmethod + def get_zone_name(obj): + """ The name of the associated zone """ return obj.extension.name - def get_alias_name(self, obj): + @staticmethod + def get_alias_name(obj): + """ The name of the associated alias """ return str(obj.cname) - def get_cname_name(self, obj): + @staticmethod + def get_cname_name(obj): + """ The name of the associated CNAME target """ return str(obj.dns_entry) @@ -300,13 +363,19 @@ class ServiceServersSerializer(serializers.ModelSerializer): model = Service_link fields = ('server', 'service', 'need_regen') - def get_server_name(self, obj): + @staticmethod + def get_server_name(obj): + """ The name of the associated server """ return str(obj.server.domain.name) - def get_service_name(self, obj): + @staticmethod + def get_service_name(obj): + """ The name of the service name """ return str(obj.service) - def get_regen_status(self, obj): + @staticmethod + def get_regen_status(obj): + """ The string representation of the regen status """ return obj.need_regen() @@ -315,24 +384,38 @@ class OuverturePortsSerializer(serializers.Serializer): ipv4 = serializers.SerializerMethodField() ipv6 = serializers.SerializerMethodField() + def create(self, validated_data): + """ Creates a new object based on the un-serialized data. + Used to implement an abstract inherited method """ + pass + + def update(self, instance, validated_data): + """ Updates an object based on the un-serialized data. + Used to implement an abstract inherited method """ + pass + + @staticmethod def get_ipv4(): - return {i.ipv4.ipv4: - { - "tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()], - "tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()], - "udp_in":[j.udp_ports_in() for j in i.port_lists.all()], - "udp_out":[j.udp_ports_out() for j in i.port_lists.all()], + """ The representation of the policy for the IPv4 addresses """ + return { + i.ipv4.ipv4: { + "tcp_in": [j.tcp_ports_in() for j in i.port_lists.all()], + "tcp_out": [j.tcp_ports_out()for j in i.port_lists.all()], + "udp_in": [j.udp_ports_in() for j in i.port_lists.all()], + "udp_out": [j.udp_ports_out() for j in i.port_lists.all()], } - for i in Interface.objects.all() if i.ipv4 + for i in Interface.objects.all() if i.ipv4 } + @staticmethod def get_ipv6(): - return {i.ipv6: - { - "tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()], - "tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()], - "udp_in":[j.udp_ports_in() for j in i.port_lists.all()], - "udp_out":[j.udp_ports_out() for j in i.port_lists.all()], + """ The representation of the policy for the IPv6 addresses """ + return { + i.ipv6: { + "tcp_in": [j.tcp_ports_in() for j in i.port_lists.all()], + "tcp_out": [j.tcp_ports_out()for j in i.port_lists.all()], + "udp_in": [j.udp_ports_in() for j in i.port_lists.all()], + "udp_out": [j.udp_ports_out() for j in i.port_lists.all()], } - for i in Interface.objects.all() if i.ipv6 + for i in Interface.objects.all() if i.ipv6 } diff --git a/machines/tests.py b/machines/tests.py index dedc0021..2074f683 100644 --- a/machines/tests.py +++ b/machines/tests.py @@ -20,7 +20,10 @@ # 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. +"""machines.tests +The tests for the API module. +""" -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/machines/urls.py b/machines/urls.py index e3454097..9a5fa25e 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -20,6 +20,9 @@ # 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. +"""machines.urls +The defined URLs for the Cotisations app +""" from __future__ import unicode_literals @@ -28,21 +31,39 @@ import re2o from . import views urlpatterns = [ - url(r'^new_machine/(?P[0-9]+)$', views.new_machine, name='new-machine'), - url(r'^edit_interface/(?P[0-9]+)$', views.edit_interface, name='edit-interface'), - url(r'^del_machine/(?P[0-9]+)$', views.del_machine, name='del-machine'), - url(r'^new_interface/(?P[0-9]+)$', views.new_interface, name='new-interface'), - url(r'^del_interface/(?P[0-9]+)$', views.del_interface, name='del-interface'), + url(r'^new_machine/(?P[0-9]+)$', + views.new_machine, + name='new-machine'), + url(r'^edit_interface/(?P[0-9]+)$', + views.edit_interface, + name='edit-interface'), + url(r'^del_machine/(?P[0-9]+)$', + views.del_machine, + name='del-machine'), + url(r'^new_interface/(?P[0-9]+)$', + views.new_interface, + name='new-interface'), + url(r'^del_interface/(?P[0-9]+)$', + views.del_interface, + name='del-interface'), url(r'^add_machinetype/$', views.add_machinetype, name='add-machinetype'), - url(r'^edit_machinetype/(?P[0-9]+)$', views.edit_machinetype, name='edit-machinetype'), + url(r'^edit_machinetype/(?P[0-9]+)$', + views.edit_machinetype, + name='edit-machinetype'), url(r'^del_machinetype/$', views.del_machinetype, name='del-machinetype'), - url(r'^index_machinetype/$', views.index_machinetype, name='index-machinetype'), + url(r'^index_machinetype/$', + views.index_machinetype, + name='index-machinetype'), url(r'^add_iptype/$', views.add_iptype, name='add-iptype'), - url(r'^edit_iptype/(?P[0-9]+)$', views.edit_iptype, name='edit-iptype'), + url(r'^edit_iptype/(?P[0-9]+)$', + views.edit_iptype, + name='edit-iptype'), url(r'^del_iptype/$', views.del_iptype, name='del-iptype'), url(r'^index_iptype/$', views.index_iptype, name='index-iptype'), url(r'^add_extension/$', views.add_extension, name='add-extension'), - url(r'^edit_extension/(?P[0-9]+)$', views.edit_extension, name='edit-extension'), + url(r'^edit_extension/(?P[0-9]+)$', + views.edit_extension, + name='edit-extension'), url(r'^del_extension/$', views.del_extension, name='del-extension'), url(r'^add_soa/$', views.add_soa, name='add-soa'), url(r'^edit_soa/(?P[0-9]+)$', views.edit_soa, name='edit-soa'), @@ -60,16 +81,34 @@ urlpatterns = [ url(r'^edit_srv/(?P[0-9]+)$', views.edit_srv, name='edit-srv'), url(r'^del_srv/$', views.del_srv, name='del-srv'), url(r'^index_extension/$', views.index_extension, name='index-extension'), - url(r'^add_alias/(?P[0-9]+)$', views.add_alias, name='add-alias'), - url(r'^edit_alias/(?P[0-9]+)$', views.edit_alias, name='edit-alias'), - url(r'^del_alias/(?P[0-9]+)$', views.del_alias, name='del-alias'), - url(r'^index_alias/(?P[0-9]+)$', views.index_alias, name='index-alias'), - url(r'^new_ipv6list/(?P[0-9]+)$', views.new_ipv6list, name='new-ipv6list'), - url(r'^edit_ipv6list/(?P[0-9]+)$', views.edit_ipv6list, name='edit-ipv6list'), - url(r'^del_ipv6list/(?P[0-9]+)$', views.del_ipv6list, name='del-ipv6list'), - url(r'^index_ipv6/(?P[0-9]+)$', views.index_ipv6, name='index-ipv6'), + url(r'^add_alias/(?P[0-9]+)$', + views.add_alias, + name='add-alias'), + url(r'^edit_alias/(?P[0-9]+)$', + views.edit_alias, + name='edit-alias'), + url(r'^del_alias/(?P[0-9]+)$', + views.del_alias, + name='del-alias'), + url(r'^index_alias/(?P[0-9]+)$', + views.index_alias, + name='index-alias'), + url(r'^new_ipv6list/(?P[0-9]+)$', + views.new_ipv6list, + name='new-ipv6list'), + url(r'^edit_ipv6list/(?P[0-9]+)$', + views.edit_ipv6list, + name='edit-ipv6list'), + url(r'^del_ipv6list/(?P[0-9]+)$', + views.del_ipv6list, + name='del-ipv6list'), + url(r'^index_ipv6/(?P[0-9]+)$', + views.index_ipv6, + name='index-ipv6'), url(r'^add_service/$', views.add_service, name='add-service'), - url(r'^edit_service/(?P[0-9]+)$', views.edit_service, name='edit-service'), + url(r'^edit_service/(?P[0-9]+)$', + views.edit_service, + name='edit-service'), url(r'^del_service/$', views.del_service, name='del-service'), url(r'^index_service/$', views.index_service, name='index-service'), url(r'^add_vlan/$', views.add_vlan, name='add-vlan'), @@ -80,15 +119,15 @@ urlpatterns = [ url(r'^edit_nas/(?P[0-9]+)$', views.edit_nas, name='edit-nas'), url(r'^del_nas/$', views.del_nas, name='del-nas'), url(r'^index_nas/$', views.index_nas, name='index-nas'), - url( - r'history/(?P\w+)/(?P[0-9]+)$', + url(r'history/(?P\w+)/(?P[0-9]+)$', re2o.views.history, name='history', - kwargs={'application':'machines'}, - ), + kwargs={'application': 'machines'}), url(r'^$', views.index, name='index'), url(r'^rest/mac-ip/$', views.mac_ip, name='mac-ip'), - url(r'^rest/regen-achieved/$', views.regen_achieved, name='regen-achieved'), + url(r'^rest/regen-achieved/$', + views.regen_achieved, + name='regen-achieved'), url(r'^rest/mac-ip-dns/$', views.mac_ip_dns, name='mac-ip-dns'), url(r'^rest/alias/$', views.alias, name='alias'), url(r'^rest/corresp/$', views.corresp, name='corresp'), @@ -97,12 +136,21 @@ urlpatterns = [ url(r'^rest/txt/$', views.txt, name='txt'), url(r'^rest/srv/$', views.srv, name='srv'), url(r'^rest/zones/$', views.zones, name='zones'), - url(r'^rest/service_servers/$', views.service_servers, name='service-servers'), - url(r'^rest/ouverture_ports/$', views.ouverture_ports, name='ouverture-ports'), + url(r'^rest/service_servers/$', + views.service_servers, + name='service-servers'), + url(r'^rest/ouverture_ports/$', + views.ouverture_ports, + name='ouverture-ports'), url(r'index_portlist/$', views.index_portlist, name='index-portlist'), - url(r'^edit_portlist/(?P[0-9]+)$', views.edit_portlist, name='edit-portlist'), - url(r'^del_portlist/(?P[0-9]+)$', views.del_portlist, name='del-portlist'), + url(r'^edit_portlist/(?P[0-9]+)$', + views.edit_portlist, + name='edit-portlist'), + url(r'^del_portlist/(?P[0-9]+)$', + views.del_portlist, + name='del-portlist'), url(r'^add_portlist/$', views.add_portlist, name='add-portlist'), - url(r'^port_config/(?P[0-9]+)$', views.configure_ports, name='port-config'), - - ] + url(r'^port_config/(?P[0-9]+)$', + views.configure_ports, + name='port-config'), +] diff --git a/machines/views.py b/machines/views.py index ee23504d..c3033049 100644 --- a/machines/views.py +++ b/machines/views.py @@ -25,24 +25,42 @@ # App de gestion des machines pour re2o # Gabriel Détraz, Augustin Lemesle # Gplv2 +"""machines.views +The views for the Machines app +""" from __future__ import unicode_literals from django.urls import reverse from django.http import HttpResponse from django.shortcuts import render, redirect -from django.shortcuts import get_object_or_404 -from django.template.context_processors import csrf -from django.template import Context, RequestContext, loader from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.db.models import ProtectedError, F -from django.forms import ValidationError, modelformset_factory -from django.contrib.auth import authenticate, login +from django.forms import modelformset_factory from django.views.decorators.csrf import csrf_exempt from rest_framework.renderers import JSONRenderer -from machines.serializers import ( FullInterfaceSerializer, + +from users.models import User +from preferences.models import GeneralOption +from re2o.utils import ( + all_active_assigned_interfaces, + filter_active_interfaces, + SortTable, + re2o_paginator, +) +from re2o.acl import ( + can_create, + can_edit, + can_delete, + can_view_all, + can_delete_set, +) +from re2o.views import form + +from .serializers import ( + FullInterfaceSerializer, InterfaceSerializer, TypeSerializer, DomainSerializer, @@ -52,12 +70,8 @@ from machines.serializers import ( FullInterfaceSerializer, ExtensionSerializer, ServiceServersSerializer, NsSerializer, - OuverturePortsSerializer ) -from reversion import revisions as reversion -from reversion.models import Version -import re from .forms import ( NewMachineForm, EditMachineForm, @@ -67,8 +81,6 @@ from .forms import ( DelMachineTypeForm, ExtensionForm, DelExtensionForm, -) -from .forms import ( EditIpTypeForm, IpTypeForm, DelIpTypeForm, @@ -92,13 +104,13 @@ from .forms import ( SrvForm, DelSrvForm, Ipv6ListForm, + EditOuverturePortListForm, + EditOuverturePortConfigForm ) -from .forms import EditOuverturePortListForm, EditOuverturePortConfigForm from .models import ( IpType, Machine, Interface, - IpList, MachineType, Extension, SOA, @@ -115,60 +127,46 @@ from .models import ( OuverturePort, Ipv6List, ) -from users.models import User -from preferences.models import GeneralOption, OptionalMachine -from re2o.utils import ( - all_active_assigned_interfaces, - all_has_access, - filter_active_interfaces, - SortTable, - re2o_paginator, -) -from re2o.acl import ( - can_create, - can_edit, - can_delete, - can_view, - can_view_all, - can_delete_set, -) -from re2o.views import form -def f_type_id( is_type_tt ): + +def f_type_id(is_type_tt): """ The id that will be used in HTML to store the value of the field type. Depends on the fact that type is generate using typeahead or not """ return 'id_Interface-type_hidden' if is_type_tt else 'id_Interface-type' -def generate_ipv4_choices( form ) : + +def generate_ipv4_choices(form_obj): """ Generate the parameter choices for the massive_bootstrap_form tag """ - f_ipv4 = form.fields['ipv4'] + f_ipv4 = form_obj.fields['ipv4'] used_mtype_id = [] choices = '{"":[{key:"",value:"Choisissez d\'abord un type de machine"},' mtype_id = -1 - for ip in f_ipv4.queryset.annotate(mtype_id=F('ip_type__machinetype__id'))\ - .order_by('mtype_id', 'id') : - if mtype_id != ip.mtype_id : + for ip in (f_ipv4.queryset + .annotate(mtype_id=F('ip_type__machinetype__id')) + .order_by('mtype_id', 'id')): + if mtype_id != ip.mtype_id: mtype_id = ip.mtype_id used_mtype_id.append(mtype_id) choices += '],"{t}":[{{key:"",value:"{v}"}},'.format( - t = mtype_id, - v = f_ipv4.empty_label or '""' + t=mtype_id, + v=f_ipv4.empty_label or '""' ) choices += '{{key:{k},value:"{v}"}},'.format( - k = ip.id, - v = ip.ipv4 + k=ip.id, + v=ip.ipv4 ) - for t in form.fields['type'].queryset.exclude(id__in=used_mtype_id) : + for t in form_obj.fields['type'].queryset.exclude(id__in=used_mtype_id): choices += '], "'+str(t.id)+'": [' choices += '{key: "", value: "' + str(f_ipv4.empty_label) + '"},' choices += ']}' return choices -def generate_ipv4_engine( is_type_tt ) : + +def generate_ipv4_engine(is_type_tt): """ Generate the parameter engine for the massive_bootstrap_form tag """ return ( @@ -179,10 +177,11 @@ def generate_ipv4_engine( is_type_tt ) : 'identify: function( obj ) {{ return obj.key; }}' '}} )' ).format( - type_id = f_type_id( is_type_tt ) + type_id=f_type_id(is_type_tt) ) -def generate_ipv4_match_func( is_type_tt ) : + +def generate_ipv4_match_func(is_type_tt): """ Generate the parameter match_func for the massive_bootstrap_form tag """ return ( @@ -196,17 +195,18 @@ def generate_ipv4_match_func( is_type_tt ) : '}}' '}}' ).format( - type_id = f_type_id( is_type_tt ) + type_id=f_type_id(is_type_tt) ) -def generate_ipv4_mbf_param( form, is_type_tt ): + +def generate_ipv4_mbf_param(form_obj, is_type_tt): """ Generate all the parameters to use with the massive_bootstrap_form tag """ - i_choices = { 'ipv4': generate_ipv4_choices( form ) } - i_engine = { 'ipv4': generate_ipv4_engine( is_type_tt ) } - i_match_func = { 'ipv4': generate_ipv4_match_func( is_type_tt ) } - i_update_on = { 'ipv4': [f_type_id( is_type_tt )] } - i_gen_select = { 'ipv4': False } + i_choices = {'ipv4': generate_ipv4_choices(form_obj)} + i_engine = {'ipv4': generate_ipv4_engine(is_type_tt)} + i_match_func = {'ipv4': generate_ipv4_match_func(is_type_tt)} + i_update_on = {'ipv4': [f_type_id(is_type_tt)]} + i_gen_select = {'ipv4': False} i_mbf_param = { 'choices': i_choices, 'engine': i_engine, @@ -216,10 +216,11 @@ def generate_ipv4_mbf_param( form, is_type_tt ): } return i_mbf_param + @login_required @can_create(Machine) @can_edit(User) -def new_machine(request, user, userid): +def new_machine(request, user, **_kwargs): """ Fonction de creation d'une machine. Cree l'objet machine, le sous objet interface et l'objet domain à partir de model forms. Trop complexe, devrait être simplifié""" @@ -231,21 +232,21 @@ def new_machine(request, user, userid): ) domain = DomainForm(request.POST or None, user=user) if machine.is_valid() and interface.is_valid(): - new_machine = machine.save(commit=False) - new_machine.user = user - new_interface = interface.save(commit=False) - domain.instance.interface_parent = new_interface + new_machine_obj = machine.save(commit=False) + new_machine_obj.user = user + new_interface_obj = interface.save(commit=False) + domain.instance.interface_parent = new_interface_obj if domain.is_valid(): new_domain = domain.save(commit=False) - new_machine.save() - new_interface.machine = new_machine - new_interface.save() - new_domain.interface_parent = new_interface + new_machine_obj.save() + new_interface_obj.machine = new_machine_obj + new_interface_obj.save() + new_domain.interface_parent = new_interface_obj new_domain.save() messages.success(request, "La machine a été créée") return redirect(reverse( 'users:profil', - kwargs={'userid':str(user.id)} + kwargs={'userid': str(user.id)} )) i_mbf_param = generate_ipv4_mbf_param(interface, False) return form( @@ -254,95 +255,121 @@ def new_machine(request, user, userid): 'interfaceform': interface, 'domainform': domain, 'i_mbf_param': i_mbf_param, - 'action_name' : 'Créer une machine' + 'action_name': 'Créer une machine' }, 'machines/machine.html', request ) + @login_required @can_edit(Interface) -def edit_interface(request, interface_instance, interfaceid): - """ Edition d'une interface. Distingue suivant les droits les valeurs de interfaces et machines que l'user peut modifier - infra permet de modifier le propriétaire""" +def edit_interface(request, interface_instance, **_kwargs): + """ Edition d'une interface. Distingue suivant les droits les valeurs + de interfaces et machines que l'user peut modifier infra permet de + modifier le propriétaire""" machine_form = EditMachineForm( request.POST or None, instance=interface_instance.machine, user=request.user ) - interface_form = EditInterfaceForm(request.POST or None, instance=interface_instance, user=request.user) - domain_form = DomainForm(request.POST or None, instance=interface_instance.domain) - if machine_form.is_valid() and interface_form.is_valid() and domain_form.is_valid(): - new_machine = machine_form.save(commit=False) - new_interface = interface_form.save(commit=False) - new_domain = domain_form.save(commit=False) + interface_form = EditInterfaceForm( + request.POST or None, + instance=interface_instance, + user=request.user + ) + domain_form = DomainForm( + request.POST or None, + instance=interface_instance.domain + ) + if (machine_form.is_valid() and + interface_form.is_valid() and + domain_form.is_valid()): + new_machine_obj = machine_form.save(commit=False) + new_interface_obj = interface_form.save(commit=False) + new_domain_obj = domain_form.save(commit=False) if machine_form.changed_data: - new_machine.save() + new_machine_obj.save() if interface_form.changed_data: - new_interface.save() + new_interface_obj.save() if domain_form.changed_data: - new_domain.save() + new_domain_obj.save() messages.success(request, "La machine a été modifiée") return redirect(reverse( 'users:profil', - kwargs={'userid':str(interface_instance.machine.user.id)} - )) - i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) - return form({ - 'machineform': machine_form, - 'interfaceform': interface_form, - 'domainform': domain_form, - 'i_mbf_param': i_mbf_param, - 'action_name' : 'Editer une interface' - }, 'machines/machine.html', request) + kwargs={'userid': str(interface_instance.machine.user.id)} + )) + i_mbf_param = generate_ipv4_mbf_param(interface_form, False) + return form( + { + 'machineform': machine_form, + 'interfaceform': interface_form, + 'domainform': domain_form, + 'i_mbf_param': i_mbf_param, + 'action_name': 'Editer une interface' + }, + 'machines/machine.html', + request + ) + @login_required @can_delete(Machine) -def del_machine(request, machine, machineid): +def del_machine(request, machine, **_kwargs): """ Supprime une machine, interfaces en mode cascade""" if request.method == "POST": machine.delete() messages.success(request, "La machine a été détruite") return redirect(reverse( 'users:profil', - kwargs={'userid':str(machine.user.id)} - )) - return form({'objet': machine, 'objet_name': 'machine'}, 'machines/delete.html', request) + kwargs={'userid': str(machine.user.id)} + )) + return form( + {'objet': machine, 'objet_name': 'machine'}, + 'machines/delete.html', + request + ) + @login_required @can_create(Interface) @can_edit(Machine) -def new_interface(request, machine, machineid): +def new_interface(request, machine, **_kwargs): """ Ajoute une interface et son domain associé à une machine existante""" interface_form = AddInterfaceForm(request.POST or None, user=request.user) domain_form = DomainForm(request.POST or None) if interface_form.is_valid(): - new_interface = interface_form.save(commit=False) - domain_form.instance.interface_parent = new_interface - new_interface.machine = machine + new_interface_obj = interface_form.save(commit=False) + domain_form.instance.interface_parent = new_interface_obj + new_interface_obj.machine = machine if domain_form.is_valid(): - new_domain = domain_form.save(commit=False) - new_interface.save() - new_domain.interface_parent = new_interface - new_domain.save() + new_domain_obj = domain_form.save(commit=False) + new_interface_obj.save() + new_domain_obj.interface_parent = new_interface_obj + new_domain_obj.save() messages.success(request, "L'interface a été ajoutée") return redirect(reverse( 'users:profil', - kwargs={'userid':str(machine.user.id)} - )) - i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) - return form({ - 'interfaceform': interface_form, - 'domainform': domain_form, - 'i_mbf_param': i_mbf_param, - 'action_name' : 'Créer une interface' - }, 'machines/machine.html', request) + kwargs={'userid': str(machine.user.id)} + )) + i_mbf_param = generate_ipv4_mbf_param(interface_form, False) + return form( + { + 'interfaceform': interface_form, + 'domainform': domain_form, + 'i_mbf_param': i_mbf_param, + 'action_name': 'Créer une interface' + }, + 'machines/machine.html', + request + ) + @login_required @can_delete(Interface) -def del_interface(request, interface, interfaceid): +def del_interface(request, interface, **_kwargs): """ Supprime une interface. Domain objet en mode cascade""" if request.method == "POST": machine = interface.machine @@ -352,44 +379,67 @@ def del_interface(request, interface, interfaceid): messages.success(request, "L'interface a été détruite") return redirect(reverse( 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - return form({'objet': interface, 'objet_name': 'interface'}, 'machines/delete.html', request) + kwargs={'userid': str(request.user.id)} + )) + return form( + {'objet': interface, 'objet_name': 'interface'}, + 'machines/delete.html', + request + ) + @login_required @can_create(Ipv6List) @can_edit(Interface) -def new_ipv6list(request, interface, interfaceid): +def new_ipv6list(request, interface, **_kwargs): """Nouvelle ipv6""" ipv6list_instance = Ipv6List(interface=interface) - ipv6 = Ipv6ListForm(request.POST or None, instance=ipv6list_instance, user=request.user) + ipv6 = Ipv6ListForm( + request.POST or None, + instance=ipv6list_instance, + user=request.user + ) if ipv6.is_valid(): ipv6.save() messages.success(request, "Ipv6 ajoutée") return redirect(reverse( 'machines:index-ipv6', - kwargs={'interfaceid':str(interface.id)} - )) - return form({'ipv6form': ipv6, 'action_name' : 'Créer'}, 'machines/machine.html', request) + kwargs={'interfaceid': str(interface.id)} + )) + return form( + {'ipv6form': ipv6, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(Ipv6List) -def edit_ipv6list(request, ipv6list_instance, ipv6listid): +def edit_ipv6list(request, ipv6list_instance, **_kwargs): """Edition d'une ipv6""" - ipv6 = Ipv6ListForm(request.POST or None, instance=ipv6list_instance, user=request.user) + ipv6 = Ipv6ListForm( + request.POST or None, + instance=ipv6list_instance, + user=request.user + ) if ipv6.is_valid(): if ipv6.changed_data: ipv6.save() messages.success(request, "Ipv6 modifiée") return redirect(reverse( 'machines:index-ipv6', - kwargs={'interfaceid':str(ipv6list_instance.interface.id)} - )) - return form({'ipv6form': ipv6, 'action_name' : 'Editer'}, 'machines/machine.html', request) + kwargs={'interfaceid': str(ipv6list_instance.interface.id)} + )) + return form( + {'ipv6form': ipv6, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_delete(Ipv6List) -def del_ipv6list(request, ipv6list, ipv6listid): +def del_ipv6list(request, ipv6list, **_kwargs): """ Supprime une ipv6""" if request.method == "POST": interfaceid = ipv6list.interface.id @@ -397,26 +447,38 @@ def del_ipv6list(request, ipv6list, ipv6listid): messages.success(request, "L'ipv6 a été détruite") return redirect(reverse( 'machines:index-ipv6', - kwargs={'interfaceid':str(interfaceid)} - )) - return form({'objet': ipv6list, 'objet_name': 'ipv6'}, 'machines/delete.html', request) + kwargs={'interfaceid': str(interfaceid)} + )) + return form( + {'objet': ipv6list, 'objet_name': 'ipv6'}, + 'machines/delete.html', + request + ) + @login_required @can_create(IpType) def add_iptype(request): - """ Ajoute un range d'ip. Intelligence dans le models, fonction views minimaliste""" + """ Ajoute un range d'ip. Intelligence dans le models, fonction views + minimaliste""" iptype = IpTypeForm(request.POST or None) if iptype.is_valid(): iptype.save() messages.success(request, "Ce type d'ip a été ajouté") return redirect(reverse('machines:index-iptype')) - return form({'iptypeform': iptype, 'action_name' : 'Créer'}, 'machines/machine.html', request) + return form( + {'iptypeform': iptype, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(IpType) -def edit_iptype(request, iptype_instance, iptypeid): - """ Edition d'un range. Ne permet pas de le redimensionner pour éviter l'incohérence""" +def edit_iptype(request, iptype_instance, **_kwargs): + """ Edition d'un range. Ne permet pas de le redimensionner pour éviter + l'incohérence""" iptype = EditIpTypeForm(request.POST or None, instance=iptype_instance) if iptype.is_valid(): @@ -424,7 +486,12 @@ def edit_iptype(request, iptype_instance, iptypeid): iptype.save() messages.success(request, "Type d'ip modifié") return redirect(reverse('machines:index-iptype')) - return form({'iptypeform': iptype, 'action_name' : 'Editer'}, 'machines/machine.html', request) + return form( + {'iptypeform': iptype, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_delete_set(IpType) @@ -438,35 +505,59 @@ def del_iptype(request, instances): iptype_del.delete() messages.success(request, "Le type d'ip a été supprimé") except ProtectedError: - messages.error(request, "Le type d'ip %s est affectée à au moins une machine, vous ne pouvez pas le supprimer" % iptype_del) + messages.error( + request, + ("Le type d'ip %s est affectée à au moins une machine, " + "vous ne pouvez pas le supprimer" % iptype_del) + ) return redirect(reverse('machines:index-iptype')) - return form({'iptypeform': iptype, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) + return form( + {'iptypeform': iptype, 'action_name': 'Supprimer'}, + 'machines/machine.html', + request + ) + @login_required @can_create(MachineType) def add_machinetype(request): - + """ View used to add a Machinetype object """ machinetype = MachineTypeForm(request.POST or None) if machinetype.is_valid(): machinetype.save() messages.success(request, "Ce type de machine a été ajouté") return redirect(reverse('machines:index-machinetype')) - return form({'machinetypeform': machinetype, 'action_name' : 'Créer'}, 'machines/machine.html', request) + return form( + {'machinetypeform': machinetype, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(MachineType) -def edit_machinetype(request, machinetype_instance, machinetypeid): - machinetype = MachineTypeForm(request.POST or None, instance=machinetype_instance) +def edit_machinetype(request, machinetype_instance, **_kwargs): + """ View used to edit a MachineType object """ + machinetype = MachineTypeForm( + request.POST or None, + instance=machinetype_instance + ) if machinetype.is_valid(): if machinetype.changed_data: machinetype.save() messages.success(request, "Type de machine modifié") return redirect(reverse('machines:index-machinetype')) - return form({'machinetypeform': machinetype, 'action_name' : 'Editer'}, 'machines/machine.html', request) + return form( + {'machinetypeform': machinetype, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_delete_set(MachineType) def del_machinetype(request, instances): + """ View used to delete a MachineType object """ machinetype = DelMachineTypeForm(request.POST or None, instances=instances) if machinetype.is_valid(): machinetype_dels = machinetype.cleaned_data['machinetypes'] @@ -475,34 +566,60 @@ def del_machinetype(request, instances): machinetype_del.delete() messages.success(request, "Le type de machine a été supprimé") except ProtectedError: - messages.error(request, "Le type de machine %s est affectée à au moins une machine, vous ne pouvez pas le supprimer" % machinetype_del) + messages.error( + request, + ("Le type de machine %s est affectée à au moins une " + "machine, vous ne pouvez pas le supprimer" + % machinetype_del) + ) return redirect(reverse('machines:index-machinetype')) - return form({'machinetypeform': machinetype, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) + return form( + {'machinetypeform': machinetype, 'action_name': 'Supprimer'}, + 'machines/machine.html', + request + ) + @login_required @can_create(Extension) def add_extension(request): + """ View used to add an Extension object """ extension = ExtensionForm(request.POST or None) if extension.is_valid(): extension.save() messages.success(request, "Cette extension a été ajoutée") return redirect(reverse('machines:index-extension')) - return form({'extensionform': extension, 'action_name' : 'Créer'}, 'machines/machine.html', request) + return form( + {'extensionform': extension, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(Extension) -def edit_extension(request, extension_instance, extensionid): - extension = ExtensionForm(request.POST or None, instance=extension_instance) +def edit_extension(request, extension_instance, **_kwargs): + """ View used to edit an Extension object """ + extension = ExtensionForm( + request.POST or None, + instance=extension_instance + ) if extension.is_valid(): if extension.changed_data: extension.save() messages.success(request, "Extension modifiée") return redirect(reverse('machines:index-extension')) - return form({'extensionform': extension, 'action_name' : 'Editer'}, 'machines/machine.html', request) + return form( + {'extensionform': extension, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_delete_set(Extension) def del_extension(request, instances): + """ View used to delete an Extension object """ extension = DelExtensionForm(request.POST or None, instances=instances) if extension.is_valid(): extension_dels = extension.cleaned_data['extensions'] @@ -511,34 +628,57 @@ def del_extension(request, instances): extension_del.delete() messages.success(request, "L'extension a été supprimée") except ProtectedError: - messages.error(request, "L'extension %s est affectée à au moins un type de machine, vous ne pouvez pas la supprimer" % extension_del) + messages.error( + request, + ("L'extension %s est affectée à au moins un type de " + "machine, vous ne pouvez pas la supprimer" + % extension_del) + ) return redirect(reverse('machines:index-extension')) - return form({'extensionform': extension, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) + return form( + {'extensionform': extension, 'action_name': 'Supprimer'}, + 'machines/machine.html', + request + ) + @login_required @can_create(SOA) def add_soa(request): + """ View used to add a SOA object """ soa = SOAForm(request.POST or None) if soa.is_valid(): soa.save() messages.success(request, "Cet enregistrement SOA a été ajouté") return redirect(reverse('machines:index-extension')) - return form({'soaform': soa, 'action_name' : 'Créer'}, 'machines/machine.html', request) + return form( + {'soaform': soa, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(SOA) -def edit_soa(request, soa_instance, soaid): +def edit_soa(request, soa_instance, **_kwargs): + """ View used to edit a SOA object """ soa = SOAForm(request.POST or None, instance=soa_instance) if soa.is_valid(): if soa.changed_data: soa.save() messages.success(request, "SOA modifié") return redirect(reverse('machines:index-extension')) - return form({'soaform': soa, 'action_name' : 'Editer'}, 'machines/machine.html', request) + return form( + {'soaform': soa, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_delete_set(SOA) def del_soa(request, instances): + """ View used to delete a SOA object """ soa = DelSOAForm(request.POST or None, instances=instances) if soa.is_valid(): soa_dels = soa.cleaned_data['soa'] @@ -547,34 +687,56 @@ def del_soa(request, instances): soa_del.delete() messages.success(request, "Le SOA a été supprimée") except ProtectedError: - messages.error(request, "Erreur le SOA suivant %s ne peut être supprimé" % soa_del) + messages.error( + request, + ("Erreur le SOA suivant %s ne peut être supprimé" + % soa_del) + ) return redirect(reverse('machines:index-extension')) - return form({'soaform': soa, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) + return form( + {'soaform': soa, 'action_name': 'Supprimer'}, + 'machines/machine.html', + request + ) + @login_required @can_create(Mx) def add_mx(request): + """ View used to add a MX object """ mx = MxForm(request.POST or None) if mx.is_valid(): mx.save() messages.success(request, "Cet enregistrement mx a été ajouté") return redirect(reverse('machines:index-extension')) - return form({'mxform': mx, 'action_name' : 'Créer'}, 'machines/machine.html', request) + return form( + {'mxform': mx, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(Mx) -def edit_mx(request, mx_instance, mxid): +def edit_mx(request, mx_instance, **_kwargs): + """ View used to edit a MX object """ mx = MxForm(request.POST or None, instance=mx_instance) if mx.is_valid(): if mx.changed_data: mx.save() messages.success(request, "Mx modifié") return redirect(reverse('machines:index-extension')) - return form({'mxform': mx, 'action_name' : 'Editer'}, 'machines/machine.html', request) + return form( + {'mxform': mx, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_delete_set(Mx) def del_mx(request, instances): + """ View used to delete a MX object """ mx = DelMxForm(request.POST or None, instances=instances) if mx.is_valid(): mx_dels = mx.cleaned_data['mx'] @@ -583,34 +745,56 @@ def del_mx(request, instances): mx_del.delete() messages.success(request, "L'mx a été supprimée") except ProtectedError: - messages.error(request, "Erreur le Mx suivant %s ne peut être supprimé" % mx_del) + messages.error( + request, + ("Erreur le Mx suivant %s ne peut être supprimé" + % mx_del) + ) return redirect(reverse('machines:index-extension')) - return form({'mxform': mx, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) + return form( + {'mxform': mx, 'action_name': 'Supprimer'}, + 'machines/machine.html', + request + ) + @login_required @can_create(Ns) def add_ns(request): + """ View used to add a NS object """ ns = NsForm(request.POST or None) if ns.is_valid(): ns.save() messages.success(request, "Cet enregistrement ns a été ajouté") return redirect(reverse('machines:index-extension')) - return form({'nsform': ns, 'action_name' : 'Créer'}, 'machines/machine.html', request) + return form( + {'nsform': ns, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(Ns) -def edit_ns(request, ns_instance, nsid): +def edit_ns(request, ns_instance, **_kwargs): + """ View used to edit a NS object """ ns = NsForm(request.POST or None, instance=ns_instance) if ns.is_valid(): if ns.changed_data: ns.save() messages.success(request, "Ns modifié") return redirect(reverse('machines:index-extension')) - return form({'nsform': ns, 'action_name' : 'Editer'}, 'machines/machine.html', request) + return form( + {'nsform': ns, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_delete_set(Ns) def del_ns(request, instances): + """ View used to delete a NS object """ ns = DelNsForm(request.POST or None, instances=instances) if ns.is_valid(): ns_dels = ns.cleaned_data['ns'] @@ -619,34 +803,56 @@ def del_ns(request, instances): ns_del.delete() messages.success(request, "Le ns a été supprimée") except ProtectedError: - messages.error(request, "Erreur le Ns suivant %s ne peut être supprimé" % ns_del) + messages.error( + request, + ("Erreur le Ns suivant %s ne peut être supprimé" + % ns_del) + ) return redirect(reverse('machines:index-extension')) - return form({'nsform': ns, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) + return form( + {'nsform': ns, 'action_name': 'Supprimer'}, + 'machines/machine.html', + request + ) + @login_required @can_create(Txt) def add_txt(request): + """ View used to add a TXT object """ txt = TxtForm(request.POST or None) if txt.is_valid(): txt.save() messages.success(request, "Cet enregistrement text a été ajouté") return redirect(reverse('machines:index-extension')) - return form({'txtform': txt, 'action_name' : 'Créer'}, 'machines/machine.html', request) + return form( + {'txtform': txt, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(Txt) -def edit_txt(request, txt_instance, txtid): +def edit_txt(request, txt_instance, **_kwargs): + """ View used to edit a TXT object """ txt = TxtForm(request.POST or None, instance=txt_instance) if txt.is_valid(): if txt.changed_data: txt.save() messages.success(request, "Txt modifié") return redirect(reverse('machines:index-extension')) - return form({'txtform': txt, 'action_name' : 'Editer'}, 'machines/machine.html', request) + return form( + {'txtform': txt, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_delete_set(Txt) def del_txt(request, instances): + """ View used to delete a TXT object """ txt = DelTxtForm(request.POST or None, instances=instances) if txt.is_valid(): txt_dels = txt.cleaned_data['txt'] @@ -655,34 +861,56 @@ def del_txt(request, instances): txt_del.delete() messages.success(request, "Le txt a été supprimé") except ProtectedError: - messages.error(request, "Erreur le Txt suivant %s ne peut être supprimé" % txt_del) + messages.error( + request, + ("Erreur le Txt suivant %s ne peut être supprimé" + % txt_del) + ) return redirect(reverse('machines:index-extension')) - return form({'txtform': txt, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) + return form( + {'txtform': txt, 'action_name': 'Supprimer'}, + 'machines/machine.html', + request + ) + @login_required @can_create(Srv) def add_srv(request): + """ View used to add a SRV object """ srv = SrvForm(request.POST or None) if srv.is_valid(): srv.save() messages.success(request, "Cet enregistrement srv a été ajouté") return redirect(reverse('machines:index-extension')) - return form({'srvform': srv, 'action_name' : 'Créer'}, 'machines/machine.html', request) + return form( + {'srvform': srv, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(Srv) -def edit_srv(request, srv_instance, srvid): +def edit_srv(request, srv_instance, **_kwargs): + """ View used to edit a SRV object """ srv = SrvForm(request.POST or None, instance=srv_instance) if srv.is_valid(): if srv.changed_data: srv.save() messages.success(request, "Srv modifié") return redirect(reverse('machines:index-extension')) - return form({'srvform': srv, 'action_name' : 'Editer'}, 'machines/machine.html', request) + return form( + {'srvform': srv, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_delete_set(Srv) def del_srv(request, instances): + """ View used to delete a SRV object """ srv = DelSrvForm(request.POST or None, instances=instances) if srv.is_valid(): srv_dels = srv.cleaned_data['srv'] @@ -691,14 +919,24 @@ def del_srv(request, instances): srv_del.delete() messages.success(request, "L'srv a été supprimée") except ProtectedError: - messages.error(request, "Erreur le Srv suivant %s ne peut être supprimé" % srv_del) + messages.error( + request, + ("Erreur le Srv suivant %s ne peut être supprimé" + % srv_del) + ) return redirect(reverse('machines:index-extension')) - return form({'srvform': srv, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) + return form( + {'srvform': srv, 'action_name': 'Supprimer'}, + 'machines/machine.html', + request + ) + @login_required @can_create(Domain) @can_edit(Interface) def add_alias(request, interface, interfaceid): + """ View used to add an Alias object """ alias = AliasForm(request.POST or None, user=request.user) if alias.is_valid(): alias = alias.save(commit=False) @@ -707,67 +945,109 @@ def add_alias(request, interface, interfaceid): messages.success(request, "Cet alias a été ajouté") return redirect(reverse( 'machines:index-alias', - kwargs={'interfaceid':str(interfaceid)} - )) - return form({'aliasform': alias, 'action_name' : 'Créer'}, 'machines/machine.html', request) + kwargs={'interfaceid': str(interfaceid)} + )) + return form( + {'aliasform': alias, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(Domain) -def edit_alias(request, domain_instance, domainid): - alias = AliasForm(request.POST or None, instance=domain_instance, user=request.user) +def edit_alias(request, domain_instance, **_kwargs): + """ View used to edit an Alias object """ + alias = AliasForm( + request.POST or None, + instance=domain_instance, + user=request.user + ) if alias.is_valid(): if alias.changed_data: domain_instance = alias.save() messages.success(request, "Alias modifié") return redirect(reverse( 'machines:index-alias', - kwargs={'interfaceid':str(domain_instance.cname.interface_parent.id)} - )) - return form({'aliasform': alias, 'action_name' : 'Editer'}, 'machines/machine.html', request) + kwargs={ + 'interfaceid': str(domain_instance.cname.interface_parent.id) + } + )) + return form( + {'aliasform': alias, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(Interface) def del_alias(request, interface, interfaceid): + """ View used to delete an Alias object """ alias = DelAliasForm(request.POST or None, interface=interface) if alias.is_valid(): alias_dels = alias.cleaned_data['alias'] for alias_del in alias_dels: try: alias_del.delete() - messages.success(request, "L'alias %s a été supprimé" % alias_del) + messages.success( + request, + "L'alias %s a été supprimé" % alias_del + ) except ProtectedError: - messages.error(request, "Erreur l'alias suivant %s ne peut être supprimé" % alias_del) + messages.error( + request, + ("Erreur l'alias suivant %s ne peut être supprimé" + % alias_del) + ) return redirect(reverse( 'machines:index-alias', - kwargs={'interfaceid':str(interfaceid)} - )) - return form({'aliasform': alias, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) + kwargs={'interfaceid': str(interfaceid)} + )) + return form( + {'aliasform': alias, 'action_name': 'Supprimer'}, + 'machines/machine.html', + request + ) @login_required @can_create(Service) def add_service(request): + """ View used to add a Service object """ service = ServiceForm(request.POST or None) if service.is_valid(): service.save() messages.success(request, "Cet enregistrement service a été ajouté") return redirect(reverse('machines:index-service')) - return form({'serviceform': service, 'action_name' : 'Créer'}, 'machines/machine.html', request) + return form( + {'serviceform': service, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(Service) -def edit_service(request, service_instance, serviceid): +def edit_service(request, service_instance, **_kwargs): + """ View used to edit a Service object """ service = ServiceForm(request.POST or None, instance=service_instance) if service.is_valid(): if service.changed_data: service.save() messages.success(request, "Service modifié") return redirect(reverse('machines:index-service')) - return form({'serviceform': service, 'action_name' : 'Editer'}, 'machines/machine.html', request) + return form( + {'serviceform': service, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_delete_set(Service) def del_service(request, instances): + """ View used to delete a Service object """ service = DelServiceForm(request.POST or None, instances=instances) if service.is_valid(): service_dels = service.cleaned_data['service'] @@ -776,34 +1056,56 @@ def del_service(request, instances): service_del.delete() messages.success(request, "Le service a été supprimée") except ProtectedError: - messages.error(request, "Erreur le service suivant %s ne peut être supprimé" % service_del) + messages.error( + request, + ("Erreur le service suivant %s ne peut être supprimé" + % service_del) + ) return redirect(reverse('machines:index-service')) - return form({'serviceform': service, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) + return form( + {'serviceform': service, 'action_name': 'Supprimer'}, + 'machines/machine.html', + request + ) + @login_required @can_create(Vlan) def add_vlan(request): + """ View used to add a VLAN object """ vlan = VlanForm(request.POST or None) if vlan.is_valid(): vlan.save() messages.success(request, "Cet enregistrement vlan a été ajouté") return redirect(reverse('machines:index-vlan')) - return form({'vlanform': vlan, 'action_name' : 'Créer'}, 'machines/machine.html', request) + return form( + {'vlanform': vlan, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(Vlan) -def edit_vlan(request, vlan_instance, vlanid): +def edit_vlan(request, vlan_instance, **_kwargs): + """ View used to edit a VLAN object """ vlan = VlanForm(request.POST or None, instance=vlan_instance) if vlan.is_valid(): if vlan.changed_data: vlan.save() messages.success(request, "Vlan modifié") return redirect(reverse('machines:index-vlan')) - return form({'vlanform': vlan, 'action_name' : 'Editer'}, 'machines/machine.html', request) + return form( + {'vlanform': vlan, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_delete_set(Vlan) def del_vlan(request, instances): + """ View used to delete a VLAN object """ vlan = DelVlanForm(request.POST or None, instances=instances) if vlan.is_valid(): vlan_dels = vlan.cleaned_data['vlan'] @@ -812,34 +1114,56 @@ def del_vlan(request, instances): vlan_del.delete() messages.success(request, "Le vlan a été supprimée") except ProtectedError: - messages.error(request, "Erreur le Vlan suivant %s ne peut être supprimé" % vlan_del) + messages.error( + request, + ("Erreur le Vlan suivant %s ne peut être supprimé" + % vlan_del) + ) return redirect(reverse('machines:index-vlan')) - return form({'vlanform': vlan, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) + return form( + {'vlanform': vlan, 'action_name': 'Supprimer'}, + 'machines/machine.html', + request + ) + @login_required @can_create(Nas) def add_nas(request): + """ View used to add a NAS object """ nas = NasForm(request.POST or None) if nas.is_valid(): nas.save() messages.success(request, "Cet enregistrement nas a été ajouté") return redirect(reverse('machines:index-nas')) - return form({'nasform': nas, 'action_name' : 'Créer'}, 'machines/machine.html', request) + return form( + {'nasform': nas, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + @login_required @can_edit(Nas) -def edit_nas(request, nas_instance, nasid): +def edit_nas(request, nas_instance, **_kwargs): + """ View used to edit a NAS object """ nas = NasForm(request.POST or None, instance=nas_instance) if nas.is_valid(): if nas.changed_data: nas.save() messages.success(request, "Nas modifié") return redirect(reverse('machines:index-nas')) - return form({'nasform': nas, 'action_name' : 'Editer'}, 'machines/machine.html', request) + return form( + {'nasform': nas, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + @login_required @can_delete_set(Nas) def del_nas(request, instances): + """ View used to delete a NAS object """ nas = DelNasForm(request.POST or None, instances=instances) if nas.is_valid(): nas_dels = nas.cleaned_data['nas'] @@ -848,47 +1172,104 @@ def del_nas(request, instances): nas_del.delete() messages.success(request, "Le nas a été supprimé") except ProtectedError: - messages.error(request, "Erreur le Nas suivant %s ne peut être supprimé" % nas_del) + messages.error( + request, + ("Erreur le Nas suivant %s ne peut être supprimé" + % nas_del) + ) return redirect(reverse('machines:index-nas')) - return form({'nasform': nas, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) + return form( + {'nasform': nas, 'action_name': 'Supprimer'}, + 'machines/machine.html', + request + ) + @login_required @can_view_all(Machine) def index(request): - pagination_large_number = GeneralOption.get_cached_value('pagination_large_number') - machines_list = Machine.objects.select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type').prefetch_related('interface_set__type__ip_type__extension').prefetch_related('interface_set__domain__related_domain__extension').prefetch_related('interface_set__ipv6list') + """ The home view for this app. Displays the list of registered + machines in Re2o """ + pagination_large_number = (GeneralOption + .get_cached_value('pagination_large_number')) + machines_list = (Machine.objects + .select_related('user') + .prefetch_related('interface_set__domain__extension') + .prefetch_related('interface_set__ipv4__ip_type') + .prefetch_related( + 'interface_set__type__ip_type__extension' + ).prefetch_related( + 'interface_set__domain__related_domain__extension' + ).prefetch_related('interface_set__ipv6list')) machines_list = SortTable.sort( machines_list, request.GET.get('col'), request.GET.get('order'), SortTable.MACHINES_INDEX ) - machines_list = re2o_paginator(request, machines_list, pagination_large_number) - return render(request, 'machines/index.html', {'machines_list': machines_list}) + machines_list = re2o_paginator( + request, + machines_list, + pagination_large_number + ) + return render( + request, + 'machines/index.html', + {'machines_list': machines_list} + ) + @login_required @can_view_all(IpType) def index_iptype(request): - iptype_list = IpType.objects.select_related('extension').select_related('vlan').order_by('type') - return render(request, 'machines/index_iptype.html', {'iptype_list':iptype_list}) + """ View displaying the list of existing types of IP """ + iptype_list = (IpType.objects + .select_related('extension') + .select_related('vlan') + .order_by('type')) + return render( + request, + 'machines/index_iptype.html', + {'iptype_list': iptype_list} + ) + @login_required @can_view_all(Vlan) def index_vlan(request): + """ View displaying the list of existing VLANs """ vlan_list = Vlan.objects.prefetch_related('iptype_set').order_by('vlan_id') - return render(request, 'machines/index_vlan.html', {'vlan_list':vlan_list}) + return render( + request, + 'machines/index_vlan.html', + {'vlan_list': vlan_list} + ) + @login_required @can_view_all(MachineType) def index_machinetype(request): - machinetype_list = MachineType.objects.select_related('ip_type').order_by('type') - return render(request, 'machines/index_machinetype.html', {'machinetype_list':machinetype_list}) + """ View displaying the list of existing types of machines """ + machinetype_list = (MachineType.objects + .select_related('ip_type') + .order_by('type')) + return render( + request, + 'machines/index_machinetype.html', + {'machinetype_list': machinetype_list} + ) + @login_required @can_view_all(Nas) def index_nas(request): - nas_list = Nas.objects.select_related('machine_type').select_related('nas_type').order_by('name') - return render(request, 'machines/index_nas.html', {'nas_list':nas_list}) + """ View displaying the list of existing NAS """ + nas_list = (Nas.objects + .select_related('machine_type') + .select_related('nas_type') + .order_by('name')) + return render(request, 'machines/index_nas.html', {'nas_list': nas_list}) + @login_required @can_view_all(SOA) @@ -898,54 +1279,122 @@ def index_nas(request): @can_view_all(Srv) @can_view_all(Extension) def index_extension(request): - extension_list = Extension.objects.select_related('origin').select_related('soa').order_by('name') + """ View displaying the list of existing extensions, the list of + existing SOA records, the list of existing MX records , the list of + existing NS records, the list of existing TXT records and the list of + existing SRV records """ + extension_list = (Extension.objects + .select_related('origin') + .select_related('soa') + .order_by('name')) soa_list = SOA.objects.order_by('name') - mx_list = Mx.objects.order_by('zone').select_related('zone').select_related('name__extension') - ns_list = Ns.objects.order_by('zone').select_related('zone').select_related('ns__extension') + mx_list = (Mx.objects + .order_by('zone') + .select_related('zone') + .select_related('name__extension')) + ns_list = (Ns.objects + .order_by('zone') + .select_related('zone') + .select_related('ns__extension')) txt_list = Txt.objects.all().select_related('zone') - srv_list = Srv.objects.all().select_related('extension').select_related('target__extension') - return render(request, 'machines/index_extension.html', {'extension_list':extension_list, 'soa_list': soa_list, 'mx_list': mx_list, 'ns_list': ns_list, 'txt_list' : txt_list, 'srv_list': srv_list}) + srv_list = (Srv.objects + .all() + .select_related('extension') + .select_related('target__extension')) + return render( + request, + 'machines/index_extension.html', + { + 'extension_list': extension_list, + 'soa_list': soa_list, + 'mx_list': mx_list, + 'ns_list': ns_list, + 'txt_list': txt_list, + 'srv_list': srv_list + } + ) + @login_required @can_edit(Interface) def index_alias(request, interface, interfaceid): - alias_list = Domain.objects.filter(cname=Domain.objects.filter(interface_parent=interface)).order_by('name') - return render(request, 'machines/index_alias.html', {'alias_list':alias_list, 'interface_id': interfaceid}) + """ View used to display the list of existing alias of an interface """ + alias_list = Domain.objects.filter( + cname=Domain.objects.filter(interface_parent=interface) + ).order_by('name') + return render( + request, + 'machines/index_alias.html', + {'alias_list': alias_list, 'interface_id': interfaceid} + ) + @login_required @can_edit(Interface) def index_ipv6(request, interface, interfaceid): + """ View used to display the list of existing IPv6 of an interface """ ipv6_list = Ipv6List.objects.filter(interface=interface) - return render(request, 'machines/index_ipv6.html', {'ipv6_list':ipv6_list, 'interface_id': interfaceid}) + return render( + request, + 'machines/index_ipv6.html', + {'ipv6_list': ipv6_list, 'interface_id': interfaceid} + ) + @login_required @can_view_all(Service) def index_service(request): - service_list = Service.objects.prefetch_related('service_link_set__server__domain__extension').all() - servers_list = Service_link.objects.select_related('server__domain__extension').select_related('service').all() - return render(request, 'machines/index_service.html', {'service_list':service_list, 'servers_list':servers_list}) + """ View used to display the list of existing services """ + service_list = (Service.objects + .prefetch_related( + 'service_link_set__server__domain__extension' + ).all()) + servers_list = (Service_link.objects + .select_related('server__domain__extension') + .select_related('service') + .all()) + return render( + request, + 'machines/index_service.html', + {'service_list': service_list, 'servers_list': servers_list} + ) @login_required @can_view_all(OuverturePortList) def index_portlist(request): - port_list = OuverturePortList.objects.prefetch_related('ouvertureport_set')\ - .prefetch_related('interface_set__domain__extension')\ - .prefetch_related('interface_set__machine__user').order_by('name') - return render(request, "machines/index_portlist.html", {'port_list':port_list}) + """ View used to display the list of existing port policies """ + port_list = (OuverturePortList.objects + .prefetch_related('ouvertureport_set') + .prefetch_related('interface_set__domain__extension') + .prefetch_related('interface_set__machine__user') + .order_by('name')) + return render( + request, + "machines/index_portlist.html", + {'port_list': port_list} + ) + @login_required @can_edit(OuverturePortList) -def edit_portlist(request, ouvertureportlist_instance, ouvertureportlistid): - port_list = EditOuverturePortListForm(request.POST or None, instance=ouvertureportlist_instance) +def edit_portlist(request, ouvertureportlist_instance, **_kwargs): + """ View used to edit a port policy """ + port_list = EditOuverturePortListForm( + request.POST or None, + instance=ouvertureportlist_instance + ) port_formset = modelformset_factory( - OuverturePort, - fields=('begin','end','protocole','io'), - extra=0, - can_delete=True, - min_num=1, - validate_min=True, - )(request.POST or None, queryset=ouvertureportlist_instance.ouvertureport_set.all()) + OuverturePort, + fields=('begin', 'end', 'protocole', 'io'), + extra=0, + can_delete=True, + min_num=1, + validate_min=True, + )( + request.POST or None, + queryset=ouvertureportlist_instance.ouvertureport_set.all() + ) if port_list.is_valid() and port_formset.is_valid(): if port_list.changed_data: pl = port_list.save() @@ -959,26 +1408,34 @@ def edit_portlist(request, ouvertureportlist_instance, ouvertureportlistid): port.save() messages.success(request, "Liste de ports modifiée") return redirect(reverse('machines:index-portlist')) - return form({'port_list' : port_list, 'ports' : port_formset}, 'machines/edit_portlist.html', request) + return form( + {'port_list': port_list, 'ports': port_formset}, + 'machines/edit_portlist.html', + request + ) + @login_required @can_delete(OuverturePortList) -def del_portlist(request, port_list_instance, ouvertureportlistid): +def del_portlist(request, port_list_instance, **_kwargs): + """ View used to delete a port policy """ port_list_instance.delete() messages.success(request, "La liste de ports a été supprimée") return redirect(reverse('machines:index-portlist')) + @login_required @can_create(OuverturePortList) def add_portlist(request): + """ View used to add a port policy """ port_list = EditOuverturePortListForm(request.POST or None) port_formset = modelformset_factory( OuverturePort, - fields=('begin','end','protocole','io'), + fields=('begin', 'end', 'protocole', 'io'), extra=0, can_delete=True, - min_num=1, - validate_min=True, + min_num=1, + validate_min=True, )(request.POST or None, queryset=OuverturePort.objects.none()) if port_list.is_valid() and port_formset.is_valid(): pl = port_list.save() @@ -990,166 +1447,277 @@ def add_portlist(request): port.save() messages.success(request, "Liste de ports créée") return redirect(reverse('machines:index-portlist')) - return form({'port_list' : port_list, 'ports' : port_formset}, 'machines/edit_portlist.html', request) - port_list = EditOuverturePortListForm(request.POST or None) - if port_list.is_valid(): - port_list.save() - messages.success(request, "Liste de ports créée") - return redirect(reverse('machines:index-portlist')) - return form({'machineform' : port_list, 'action_name' : 'Créer'}, 'machines/machine.html', request) + return form( + {'port_list': port_list, 'ports': port_formset}, + 'machines/edit_portlist.html', + request + ) + @login_required @can_create(OuverturePort) @can_edit(Interface) -def configure_ports(request, interface_instance, interfaceid): +def configure_ports(request, interface_instance, **_kwargs): + """ View to display the list of configured port policy for an + interface """ if not interface_instance.may_have_port_open(): - messages.error(request, "Attention, l'ipv4 n'est pas publique, l'ouverture n'aura pas d'effet en v4") - interface = EditOuverturePortConfigForm(request.POST or None, instance=interface_instance) + messages.error( + request, + ("Attention, l'ipv4 n'est pas publique, l'ouverture n'aura pas " + "d'effet en v4") + ) + interface = EditOuverturePortConfigForm( + request.POST or None, + instance=interface_instance + ) if interface.is_valid(): if interface.changed_data: interface.save() messages.success(request, "Configuration des ports mise à jour.") return redirect(reverse('machines:index')) - return form({'interfaceform' : interface, 'action_name' : 'Editer la configuration'}, 'machines/machine.html', request) + return form( + {'interfaceform': interface, 'action_name': 'Editer la configuration'}, + 'machines/machine.html', + request + ) + + +## Framework Rest -""" Framework Rest """ class JSONResponse(HttpResponse): + """ Class to build a JSON response. Used for API """ def __init__(self, data, **kwargs): content = JSONRenderer().render(data) kwargs['content_type'] = 'application/json' super(JSONResponse, self).__init__(content, **kwargs) + @csrf_exempt @login_required @permission_required('machines.serveur') -def mac_ip_list(request): +def mac_ip_list(_request): + """ API view to list the active and assigned interfaces """ interfaces = all_active_assigned_interfaces() seria = InterfaceSerializer(interfaces, many=True) return seria.data + @csrf_exempt @login_required @permission_required('machines.serveur') -def full_mac_ip_list(request): +def full_mac_ip_list(_request): + """ API view to list the active and assigned interfaces. More + detailed than mac_ip_list(request) """ interfaces = all_active_assigned_interfaces(full=True) seria = FullInterfaceSerializer(interfaces, many=True) return seria.data -@csrf_exempt -@login_required -@permission_required('machines.serveur') -def alias(request): - alias = Domain.objects.filter(interface_parent=None).filter(cname__in=Domain.objects.filter(interface_parent__in=Interface.objects.exclude(ipv4=None))).select_related('extension').select_related('cname__extension') - seria = DomainSerializer(alias, many=True) - return JSONResponse(seria.data) @csrf_exempt @login_required @permission_required('machines.serveur') -def corresp(request): +def alias(_request): + """ API view to list the alias (CNAME) for all assigned interfaces """ + alias = (Domain.objects + .filter(interface_parent=None) + .filter( + cname__in=Domain.objects.filter( + interface_parent__in=Interface.objects.exclude(ipv4=None) + ) + ).select_related('extension') + .select_related('cname__extension')) + seria = DomainSerializer(alias, many=True) + return JSONResponse(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +def corresp(_request): + """ API view to list the types of IP and infos about it """ type = IpType.objects.all().select_related('extension') seria = TypeSerializer(type, many=True) return JSONResponse(seria.data) -@csrf_exempt -@login_required -@permission_required('machines.serveur') -def mx(request): - mx = Mx.objects.all().select_related('zone').select_related('name__extension') - seria = MxSerializer(mx, many=True) - return JSONResponse(seria.data) @csrf_exempt @login_required @permission_required('machines.serveur') -def txt(request): +def mx(_request): + """ API view to list the MX records """ + mx = (Mx.objects.all() + .select_related('zone') + .select_related('name__extension')) + seria = MxSerializer(mx, many=True) + return JSONResponse(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +def txt(_request): + """ API view to list the TXT records """ txt = Txt.objects.all().select_related('zone') seria = TxtSerializer(txt, many=True) return JSONResponse(seria.data) + @csrf_exempt @login_required @permission_required('machines.serveur') -def srv(request): - srv = Srv.objects.all().select_related('extension').select_related('target__extension') +def srv(_request): + """ API view to list the SRV records """ + srv = (Srv.objects + .all() + .select_related('extension') + .select_related('target__extension')) seria = SrvSerializer(srv, many=True) return JSONResponse(seria.data) -@csrf_exempt -@login_required -@permission_required('machines.serveur') -def ns(request): - ns = Ns.objects.exclude(ns__in=Domain.objects.filter(interface_parent__in=Interface.objects.filter(ipv4=None))).select_related('zone').select_related('ns__extension') - seria = NsSerializer(ns, many=True) - return JSONResponse(seria.data) @csrf_exempt @login_required @permission_required('machines.serveur') -def zones(request): +def ns(_request): + """ API view to list the NS records """ + ns = (Ns.objects + .exclude( + ns__in=Domain.objects.filter( + interface_parent__in=Interface.objects.filter(ipv4=None) + ) + ).select_related('zone') + .select_related('ns__extension')) + seria = NsSerializer(ns, many=True) + return JSONResponse(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +def zones(_request): + """ API view to list the DNS zones """ zones = Extension.objects.all().select_related('origin') seria = ExtensionSerializer(zones, many=True) return JSONResponse(seria.data) + @csrf_exempt @login_required @permission_required('machines.serveur') def mac_ip(request): + """ API view to list the active and assigned interfaces """ seria = mac_ip_list(request) return JSONResponse(seria) + @csrf_exempt @login_required @permission_required('machines.serveur') def mac_ip_dns(request): + """ API view to list the active and assigned interfaces. More + detailed than mac_ip_list(request) """ seria = full_mac_ip_list(request) return JSONResponse(seria) -@csrf_exempt -@login_required -@permission_required('machines.serveur') -def service_servers(request): - service_link = Service_link.objects.all().select_related('server__domain').select_related('service') - seria = ServiceServersSerializer(service_link, many=True) - return JSONResponse(seria.data) @csrf_exempt @login_required @permission_required('machines.serveur') -def ouverture_ports(request): - r = {'ipv4':{}, 'ipv6':{}} - for o in OuverturePortList.objects.all().prefetch_related('ouvertureport_set').prefetch_related('interface_set', 'interface_set__ipv4'): +def service_servers(_request): + """ API view to list the service links """ + service_link = (Service_link.objects + .all() + .select_related('server__domain') + .select_related('service')) + seria = ServiceServersSerializer(service_link, many=True) + return JSONResponse(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +def ouverture_ports(_request): + """ API view to list the port policies for each IP """ + r = {'ipv4': {}, 'ipv6': {}} + for o in (OuverturePortList.objects + .all() + .prefetch_related('ouvertureport_set') + .prefetch_related('interface_set', 'interface_set__ipv4')): pl = { - "tcp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.IN))), - "tcp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.OUT))), - "udp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.IN))), - "udp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.OUT))), + "tcp_in": set(map( + str, + o.ouvertureport_set.filter( + protocole=OuverturePort.TCP, + io=OuverturePort.IN + ) + )), + "tcp_out": set(map( + str, + o.ouvertureport_set.filter( + protocole=OuverturePort.TCP, + io=OuverturePort.OUT + ) + )), + "udp_in": set(map( + str, + o.ouvertureport_set.filter( + protocole=OuverturePort.UDP, + io=OuverturePort.IN + ) + )), + "udp_out": set(map( + str, + o.ouvertureport_set.filter( + protocole=OuverturePort.UDP, + io=OuverturePort.OUT + ) + )), } for i in filter_active_interfaces(o.interface_set): if i.may_have_port_open(): d = r['ipv4'].get(i.ipv4.ipv4, {}) - d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) - d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"]) - d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) - d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) + d["tcp_in"] = (d.get("tcp_in", set()) + .union(pl["tcp_in"])) + d["tcp_out"] = (d.get("tcp_out", set()) + .union(pl["tcp_out"])) + d["udp_in"] = (d.get("udp_in", set()) + .union(pl["udp_in"])) + d["udp_out"] = (d.get("udp_out", set()) + .union(pl["udp_out"])) r['ipv4'][i.ipv4.ipv4] = d if i.ipv6(): for ipv6 in i.ipv6(): d = r['ipv6'].get(ipv6.ipv6, {}) - d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) - d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"]) - d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) - d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) + d["tcp_in"] = (d.get("tcp_in", set()) + .union(pl["tcp_in"])) + d["tcp_out"] = (d.get("tcp_out", set()) + .union(pl["tcp_out"])) + d["udp_in"] = (d.get("udp_in", set()) + .union(pl["udp_in"])) + d["udp_out"] = (d.get("udp_out", set()) + .union(pl["udp_out"])) r['ipv6'][ipv6.ipv6] = d return JSONResponse(r) + @csrf_exempt @login_required @permission_required('machines.serveur') def regen_achieved(request): - obj = Service_link.objects.filter(service__in=Service.objects.filter(service_type=request.POST['service']), server__in=Interface.objects.filter(domain__in=Domain.objects.filter(name=request.POST['server']))) + """ API view to list the regen status for each (Service link, Server) + couple """ + obj = (Service_link.objects + .filter( + service__in=Service.objects.filter( + service_type=request.POST['service'] + ), + server__in=Interface.objects.filter( + domain__in=Domain.objects.filter( + name=request.POST['server'] + ) + ) + )) if obj: obj.first().done_regen() return HttpResponse("Ok") - diff --git a/preferences/__init__.py b/preferences/__init__.py index e895e295..c287fce8 100644 --- a/preferences/__init__.py +++ b/preferences/__init__.py @@ -1,2 +1,28 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2017 Gabriel Détraz +# Copyright © 2017 Goulven Kermarec +# Copyright © 2017 Augustin Lemesle +# Copyright © 2018 Maël Kervella +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""preferences +The app in charge of storing all the preferences for the local installation +""" from .acl import * diff --git a/preferences/acl.py b/preferences/acl.py index 8ffb4c9b..1f3f666e 100644 --- a/preferences/acl.py +++ b/preferences/acl.py @@ -26,6 +26,7 @@ Here are defined some functions to check acl on the application. """ + def can_view(user): """Check if an user can view the application. diff --git a/preferences/aes_field.py b/preferences/aes_field.py index ce90e1f4..1d3ffa54 100644 --- a/preferences/aes_field.py +++ b/preferences/aes_field.py @@ -1,3 +1,34 @@ +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2017 Gabriel Détraz +# Copyright © 2017 Goulven Kermarec +# Copyright © 2017 Augustin Lemesle +# Copyright © 2018 Maël Kervella +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +# App de gestion des machines pour re2o +# Gabriel Détraz, Augustin Lemesle +# Gplv2 +"""preferences.aes_field +Module defining a AESEncryptedField object that can be used in forms +to handle the use of properly encrypting and decrypting AES keys +""" + import string import binascii from random import choice @@ -10,10 +41,13 @@ EOD = '`%EofD%`' # This should be something that will not occur in strings def genstring(length=16, chars=string.printable): + """ Generate a random string of length `length` and composed of + the characters in `chars` """ return ''.join([choice(chars) for i in range(length)]) def encrypt(key, s): + """ AES Encrypt a secret `s` with the key `key` """ obj = AES.new(key) datalength = len(s) + len(EOD) if datalength < 16: @@ -25,12 +59,15 @@ def encrypt(key, s): def decrypt(key, s): + """ AES Decrypt a secret `s` with the key `key` """ obj = AES.new(key) ss = obj.decrypt(s) return ss.split(bytes(EOD, 'utf-8'))[0] class AESEncryptedField(models.CharField): + """ A Field that can be used in forms for adding the support + of AES ecnrypted fields """ def save_form_data(self, instance, data): setattr(instance, self.name, binascii.b2a_base64(encrypt(settings.AES_KEY, data))) @@ -41,16 +78,10 @@ class AESEncryptedField(models.CharField): return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8') - def from_db_value(self, value, expression, connection, *args): - if value is None: - return value - return decrypt(settings.AES_KEY, - binascii.a2b_base64(value)).decode('utf-8') - def get_prep_value(self, value): if value is None: return value return binascii.b2a_base64(encrypt( - settings.AES_KEY, - value + settings.AES_KEY, + value )) diff --git a/preferences/apps.py b/preferences/apps.py deleted file mode 100644 index 85238099..00000000 --- a/preferences/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class PreferencesConfig(AppConfig): - name = 'preferences' diff --git a/preferences/forms.py b/preferences/forms.py index a6b8426a..b4a79dd0 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -44,12 +44,16 @@ class EditOptionalUserForm(ModelForm): prefix=prefix, **kwargs ) - self.fields['is_tel_mandatory'].label = 'Exiger un numéro de\ - téléphone' - self.fields['user_solde'].label = 'Activation du solde pour\ - les utilisateurs' + self.fields['is_tel_mandatory'].label = ( + 'Exiger un numéro de téléphone' + ) + self.fields['user_solde'].label = ( + 'Activation du solde pour les utilisateurs' + ) self.fields['max_solde'].label = 'Solde maximum' - self.fields['min_online_payment'].label = 'Montant de rechargement minimum en ligne' + self.fields['min_online_payment'].label = ( + 'Montant de rechargement minimum en ligne' + ) self.fields['self_adhesion'].label = 'Auto inscription' @@ -162,7 +166,6 @@ class EditAssoOptionForm(ModelForm): return cleaned_data - class EditMailMessageOptionForm(ModelForm): """Formulaire d'edition des messages de bienvenue personnalisés""" class Meta: diff --git a/preferences/models.py b/preferences/models.py index c0d761e6..e64d49ab 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -27,26 +27,33 @@ from __future__ import unicode_literals from django.utils.functional import cached_property from django.db import models -import cotisations.models -import machines.models -from django.db.models.signals import post_save, post_delete +from django.db.models.signals import post_save from django.dispatch import receiver from django.core.cache import cache -from .aes_field import AESEncryptedField +import cotisations.models +import machines.models from re2o.mixins import AclMixin +from .aes_field import AESEncryptedField + + class PreferencesModel(models.Model): + """ Base object for the Preferences objects + Defines methods to handle the cache of the settings (they should + not change a lot) """ @classmethod def set_in_cache(cls): + """ Save the preferences in a server-side cache """ instance, _created = cls.objects.get_or_create() cache.set(cls().__class__.__name__.lower(), instance, None) return instance @classmethod def get_cached_value(cls, key): + """ Get the preferences from the server-side cache """ instance = cache.get(cls().__class__.__name__.lower()) - if instance == None: + if instance is None: instance = cls.set_in_cache() return getattr(instance, key) @@ -111,7 +118,7 @@ class OptionalUser(AclMixin, PreferencesModel): @receiver(post_save, sender=OptionalUser) -def optionaluser_post_save(sender, **kwargs): +def optionaluser_post_save(**kwargs): """Ecriture dans le cache""" user_pref = kwargs['instance'] user_pref.set_in_cache() @@ -146,7 +153,8 @@ class OptionalMachine(AclMixin, PreferencesModel): @cached_property def ipv6(self): - return not self.get_cached_value('ipv6_mode') == 'DISABLED' + """ Check if the IPv6 option is activated """ + return not self.get_cached_value('ipv6_mode') == 'DISABLED' class Meta: permissions = ( @@ -155,7 +163,7 @@ class OptionalMachine(AclMixin, PreferencesModel): @receiver(post_save, sender=OptionalMachine) -def optionalmachine_post_save(sender, **kwargs): +def optionalmachine_post_save(**kwargs): """Synchronisation ipv6 et ecriture dans le cache""" machine_pref = kwargs['instance'] machine_pref.set_in_cache() @@ -203,7 +211,7 @@ class OptionalTopologie(AclMixin, PreferencesModel): @receiver(post_save, sender=OptionalTopologie) -def optionaltopologie_post_save(sender, **kwargs): +def optionaltopologie_post_save(**kwargs): """Ecriture dans le cache""" topologie_pref = kwargs['instance'] topologie_pref.set_in_cache() @@ -230,7 +238,7 @@ class GeneralOption(AclMixin, PreferencesModel): blank=True, ) GTU = models.FileField( - upload_to = '', + upload_to='', default="", null=True, blank=True, @@ -243,7 +251,7 @@ class GeneralOption(AclMixin, PreferencesModel): @receiver(post_save, sender=GeneralOption) -def generaloption_post_save(sender, **kwargs): +def generaloption_post_save(**kwargs): """Ecriture dans le cache""" general_pref = kwargs['instance'] general_pref.set_in_cache() @@ -317,7 +325,7 @@ class AssoOption(AclMixin, PreferencesModel): @receiver(post_save, sender=AssoOption) -def assooption_post_save(sender, **kwargs): +def assooption_post_save(**kwargs): """Ecriture dans le cache""" asso_pref = kwargs['instance'] asso_pref.set_in_cache() diff --git a/preferences/templates/preferences/preferences.html b/preferences/templates/preferences/preferences.html index 16d7a74d..e8972d8d 100644 --- a/preferences/templates/preferences/preferences.html +++ b/preferences/templates/preferences/preferences.html @@ -33,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} -
+ {% csrf_token %} {% if preferenceform %} {% bootstrap_form preferenceform %} diff --git a/preferences/tests.py b/preferences/tests.py index 7ce503c2..8b980c73 100644 --- a/preferences/tests.py +++ b/preferences/tests.py @@ -1,3 +1,29 @@ -from django.test import TestCase +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2017 Gabriel Détraz +# Copyright © 2017 Goulven Kermarec +# Copyright © 2017 Augustin Lemesle +# Copyright © 2018 Maël Kervella +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""preferences.tests +The tests for the Preferences module. +""" + +# from django.test import TestCase # Create your tests here. diff --git a/preferences/urls.py b/preferences/urls.py index 3bc15275..9b51a432 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -27,8 +27,8 @@ from __future__ import unicode_literals from django.conf.urls import url -from . import views import re2o +from . import views urlpatterns = [ @@ -73,7 +73,7 @@ urlpatterns = [ r'^history/(?P\w+)/(?P[0-9]+)$', re2o.views.history, name='history', - kwargs={'application':'preferences'}, + kwargs={'application': 'preferences'}, ), url(r'^$', views.display_options, name='display-options'), ] diff --git a/preferences/views.py b/preferences/views.py index 22341c28..3393aa75 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -31,17 +31,17 @@ topologie, users, service...) from __future__ import unicode_literals from django.urls import reverse -from django.shortcuts import render, redirect +from django.shortcuts import redirect from django.contrib import messages -from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.decorators import login_required from django.db.models import ProtectedError from django.db import transaction -from reversion.models import Version from reversion import revisions as reversion from re2o.views import form from re2o.acl import can_create, can_edit, can_delete_set, can_view_all + from .forms import ServiceForm, DelServiceForm from .models import Service, OptionalUser, OptionalMachine, AssoOption from .models import MailMessageOption, GeneralOption, OptionalTopologie @@ -119,7 +119,7 @@ def edit_options(request, section): @can_create(Service) def add_service(request): """Ajout d'un service de la page d'accueil""" - service = ServiceForm(request.POST or None) + service = ServiceForm(request.POST or None, request.FILES or None) if service.is_valid(): with transaction.atomic(), reversion.create_revision(): service.save() @@ -128,7 +128,7 @@ def add_service(request): messages.success(request, "Ce service a été ajouté") return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': service, 'action_name' : 'Ajouter'}, + {'preferenceform': service, 'action_name': 'Ajouter'}, 'preferences/preferences.html', request ) @@ -136,9 +136,9 @@ def add_service(request): @login_required @can_edit(Service) -def edit_service(request, service_instance, serviceid): +def edit_service(request, service_instance, **_kwargs): """Edition des services affichés sur la page d'accueil""" - service = ServiceForm(request.POST or None, instance=service_instance) + service = ServiceForm(request.POST or None, request.FILES or None,instance=service_instance) if service.is_valid(): with transaction.atomic(), reversion.create_revision(): service.save() @@ -151,7 +151,7 @@ def edit_service(request, service_instance, serviceid): messages.success(request, "Service modifié") return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': service, 'action_name' : 'Editer'}, + {'preferenceform': service, 'action_name': 'Editer'}, 'preferences/preferences.html', request ) @@ -175,7 +175,7 @@ def del_services(request, instances): suivant %s ne peut être supprimé" % services_del) return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': services, 'action_name' : 'Supprimer'}, + {'preferenceform': services, 'action_name': 'Supprimer'}, 'preferences/preferences.html', request ) diff --git a/re2o/__init__.py b/re2o/__init__.py index fc1be5d7..eda09716 100644 --- a/re2o/__init__.py +++ b/re2o/__init__.py @@ -20,4 +20,15 @@ # 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. - +"""re2o +The main app of Re2o. In charge of all the basics elements which are not +specific to anyother apps. It includes : + * Templates used in multiple places + * Templatetags used in multiple places + * ACL base + * Mixins base + * Settings for the Django project + * The login part + * Some utility scripts + * ... +""" diff --git a/re2o/acl.py b/re2o/acl.py index a4c0027c..6ab34350 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -33,14 +33,6 @@ from django.contrib import messages from django.shortcuts import redirect from django.urls import reverse -import cotisations -import logs -import machines -import preferences -import search -import topologie -import users - def can_create(model): """Decorator to check if an user can create a model. @@ -49,7 +41,11 @@ def can_create(model): of models. """ def decorator(view): + """The decorator to use on a specific view + """ def wrapper(request, *args, **kwargs): + """The wrapper used for a specific request + """ can, msg = model.can_create(request.user, *args, **kwargs) if not can: messages.error( @@ -68,31 +64,37 @@ def can_edit(model, *field_list): kind of models. """ def decorator(view): + """The decorator to use on a specific view + """ def wrapper(request, *args, **kwargs): + """The wrapper used for a specific request + """ try: instance = model.get_instance(*args, **kwargs) except model.DoesNotExist: messages.error(request, u"Entrée inexistante") - return redirect(reverse('users:profil', - kwargs={'userid': str(request.user.id)} - )) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) can, msg = instance.can_edit(request.user) if not can: messages.error( request, msg or "Vous ne pouvez pas accéder à ce menu") - return redirect(reverse('users:profil', - kwargs={'userid': str(request.user.id)} - )) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) for field in field_list: - can_change = getattr(instance, 'can_change_' + field) - can, msg = can_change(request.user, *args, **kwargs) + can_change_fct = getattr(instance, 'can_change_' + field) + can, msg = can_change_fct(request.user, *args, **kwargs) if not can: messages.error( request, msg or "Vous ne pouvez pas accéder à ce menu") - return redirect(reverse('users:profil', - kwargs={'userid': str( - request.user.id)} - )) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) return view(request, instance, *args, **kwargs) return wrapper return decorator @@ -103,17 +105,21 @@ def can_change(model, *field_list): Difference with can_edit : take a class and not an instance """ def decorator(view): + """The decorator to use on a specific view + """ def wrapper(request, *args, **kwargs): + """The wrapper used for a specific request + """ for field in field_list: - can_change = getattr(model, 'can_change_' + field) - can, msg = can_change(request.user, *args, **kwargs) + can_change_fct = getattr(model, 'can_change_' + field) + can, msg = can_change_fct(request.user, *args, **kwargs) if not can: messages.error( request, msg or "Vous ne pouvez pas accéder à ce menu") - return redirect(reverse('users:profil', - kwargs={'userid': str( - request.user.id)} - )) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) return view(request, *args, **kwargs) return wrapper return decorator @@ -127,21 +133,27 @@ def can_delete(model): kind of models. """ def decorator(view): + """The decorator to use on a specific view + """ def wrapper(request, *args, **kwargs): + """The wrapper used for a specific request + """ try: instance = model.get_instance(*args, **kwargs) except model.DoesNotExist: messages.error(request, u"Entrée inexistante") - return redirect(reverse('users:profil', - kwargs={'userid': str(request.user.id)} - )) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) can, msg = instance.can_delete(request.user) if not can: messages.error( request, msg or "Vous ne pouvez pas accéder à ce menu") - return redirect(reverse('users:profil', - kwargs={'userid': str(request.user.id)} - )) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) return view(request, instance, *args, **kwargs) return wrapper return decorator @@ -151,19 +163,25 @@ def can_delete_set(model): """Decorator which returns a list of detable models by request user. If none of them, return an error""" def decorator(view): + """The decorator to use on a specific view + """ def wrapper(request, *args, **kwargs): + """The wrapper used for a specific request + """ all_objects = model.objects.all() instances_id = [] for instance in all_objects: - can, msg = instance.can_delete(request.user) + can, _msg = instance.can_delete(request.user) if can: instances_id.append(instance.id) instances = model.objects.filter(id__in=instances_id) if not instances: - messages.error(request, "Vous ne pouvez pas accéder à ce menu") - return redirect(reverse('users:profil', - kwargs={'userid': str(request.user.id)} - )) + messages.error( + request, "Vous ne pouvez pas accéder à ce menu") + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) return view(request, instances, *args, **kwargs) return wrapper return decorator @@ -177,21 +195,27 @@ def can_view(model): kind of models. """ def decorator(view): + """The decorator to use on a specific view + """ def wrapper(request, *args, **kwargs): + """The wrapper used for a specific request + """ try: instance = model.get_instance(*args, **kwargs) except model.DoesNotExist: messages.error(request, u"Entrée inexistante") - return redirect(reverse('users:profil', - kwargs={'userid': str(request.user.id)} - )) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) can, msg = instance.can_view(request.user) if not can: messages.error( request, msg or "Vous ne pouvez pas accéder à ce menu") - return redirect(reverse('users:profil', - kwargs={'userid': str(request.user.id)} - )) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) return view(request, instance, *args, **kwargs) return wrapper return decorator @@ -201,14 +225,19 @@ def can_view_all(model): """Decorator to check if an user can view a class of model. """ def decorator(view): + """The decorator to use on a specific view + """ def wrapper(request, *args, **kwargs): + """The wrapper used for a specific request + """ can, msg = model.can_view_all(request.user) if not can: messages.error( request, msg or "Vous ne pouvez pas accéder à ce menu") - return redirect(reverse('users:profil', - kwargs={'userid': str(request.user.id)} - )) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) return view(request, *args, **kwargs) return wrapper return decorator @@ -220,15 +249,20 @@ def can_view_app(app_name): assert app_name in sys.modules.keys() def decorator(view): + """The decorator to use on a specific view + """ def wrapper(request, *args, **kwargs): + """The wrapper used for a specific request + """ app = sys.modules[app_name] can, msg = app.can_view(request.user) if can: return view(request, *args, **kwargs) messages.error(request, msg) - return redirect(reverse('users:profil', - kwargs={'userid': str(request.user.id)} - )) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) return wrapper return decorator @@ -236,13 +270,16 @@ def can_view_app(app_name): def can_edit_history(view): """Decorator to check if an user can edit history.""" def wrapper(request, *args, **kwargs): + """The wrapper used for a specific request + """ if request.user.has_perm('admin.change_logentry'): return view(request, *args, **kwargs) messages.error( request, "Vous ne pouvez pas éditer l'historique." ) - return redirect(reverse('users:profil', - kwargs={'userid': str(request.user.id)} - )) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) return wrapper diff --git a/re2o/contributors.py b/re2o/contributors.py index 00e97e41..951acc47 100644 --- a/re2o/contributors.py +++ b/re2o/contributors.py @@ -1,3 +1,30 @@ -#!/usr/bin/env python3 +"""re2o.contributors +A list of the proud contributors to Re2o +""" -CONTRIBUTORS = ['Gabriel "Chirac" Détraz', 'Maël "MoaMoaK" Kervella', 'Hugo "Klafyvel" Levy--Falk', 'Augustin "Dahlaro" Lemesle', 'Goulven "Lhark" Kermarec', 'Guillaume "Guimoz" Goessel', 'Yoann "Nanoy" Pietri', 'Matthieu "Lebanni" Michelet', 'Arthur "Grizzly" Grisel-Davy', 'Simon "Rezatoune" Brélivet', 'Sellem Lev-Arcady', 'David "5-1" Sinquin', 'Pierre "Redstorm" Cadart', 'Éloi "Goslig" Alain', 'Laouen "Volgarr" Fernet', 'Joanne Steiner', '"Krokmou"', 'Thibault "Tipunchetrhum" de Boutray', 'Baptiste "B" Fournier', 'Daniel "Dstan" Stan', 'Hugo "Shaka" Hervieux', '"Mikachu"', 'Thomas "Nymous" Gaudin', '"Esum"'] +CONTRIBUTORS = [ + 'Gabriel "Chirac" Détraz', + 'Maël "MoaMoaK" Kervella', + 'Hugo "Klafyvel" Levy--Falk', + 'Augustin "Dahlaro" Lemesle', + 'Goulven "Lhark" Kermarec', + 'Guillaume "Guimoz" Goessel', + 'Yoann "Nanoy" Pietri', + 'Matthieu "Lebanni" Michelet', + 'Arthur "Grizzly" Grisel-Davy', + 'Simon "Rezatoune" Brélivet', + 'Sellem Lev-Arcady', + 'David "5-1" Sinquin', + 'Pierre "Redstorm" Cadart', + 'Éloi "Goslig" Alain', + 'Laouen "Volgarr" Fernet', + 'Joanne Steiner', + '"Krokmou"', + 'Thibault "Tipunchetrhum" de Boutray', + 'Baptiste "B" Fournier', + 'Daniel "Dstan" Stan', + 'Hugo "Shaka" Hervieux', + '"Mikachu"', + 'Thomas "Nymous" Gaudin', + '"Esum"' +] diff --git a/re2o/field_permissions.py b/re2o/field_permissions.py index dc5466c4..5febeb63 100644 --- a/re2o/field_permissions.py +++ b/re2o/field_permissions.py @@ -1,15 +1,45 @@ -from django.db import models -from django import forms -from functools import partial - +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2017 Gabriel Détraz +# Copyright © 2017 Goulven Kermarec +# Copyright © 2017 Augustin Lemesle +# Copyright © 2018 Maël Kervella +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""re2o.field_permissions +A model mixin and a field mixin used to remove some unauthorized fields +from the form automatically generated from the model. The model must +subclass `FieldPermissionModelMixin` and the form must subclass +`FieldPermissionFieldMixin` so when a Django form is generated from the +fields of the models, some fields will be removed if the user don't have +the rights to change them (can_change_{name}) +""" class FieldPermissionModelMixin: + """ The model mixin. Defines the `has_field_perm` function """ field_permissions = {} # {'field_name': callable} FIELD_PERM_CODENAME = 'can_change_{model}_{name}' FIELD_PERMISSION_GETTER = 'can_change_{name}' FIELD_PERMISSION_MISSING_DEFAULT = True def has_field_perm(self, user, field): + """ Checks if a `user` has the right to edit the `field` + of this model """ if field in self.field_permissions: checks = self.field_permissions[field] if not isinstance(checks, (list, tuple)): @@ -39,21 +69,18 @@ class FieldPermissionModelMixin: # Try to find a user setting that qualifies them for permission. for perm in checks: if callable(perm): - result, reason = perm(user_request=user) + result, _reason = perm(user_request=user) if result is not None: return result else: - result = user.has_perm(perm) # Don't supply 'obj', or else infinite recursion. + # Don't supply 'obj', or else infinite recursion. + result = user.has_perm(perm) if result: return True # If no requirement can be met, then permission is denied. return False -class FieldPermissionModel(FieldPermissionModelMixin, models.Model): - class Meta: - abstract = True - class FieldPermissionFormMixin: """ @@ -71,9 +98,5 @@ class FieldPermissionFormMixin: self.remove_unauthorized_field(name) def remove_unauthorized_field(self, name): + """ Remove one field from the fields of the form """ del self.fields[name] - - -class FieldPermissionForm(FieldPermissionFormMixin, forms.ModelForm): - pass - diff --git a/re2o/login.py b/re2o/login.py index 83ca4d14..b867e836 100644 --- a/re2o/login.py +++ b/re2o/login.py @@ -24,7 +24,9 @@ # -*- coding: utf-8 -*- # Module d'authentification # David Sinquin, Gabriel Détraz, Goulven Kermarec - +"""re2o.login +Module in charge of handling the login process and verifications +""" import hashlib import binascii @@ -42,6 +44,7 @@ DIGEST_LEN = 20 def makeSecret(password): + """ Build a hashed and salted version of the password """ salt = os.urandom(4) h = hashlib.sha1(password.encode()) h.update(salt) @@ -49,11 +52,13 @@ def makeSecret(password): def hashNT(password): - hash = hashlib.new('md4', password.encode('utf-16le')).digest() - return binascii.hexlify(hash).upper() + """ Build a md4 hash of the password to use as the NT-password """ + hash_str = hashlib.new('md4', password.encode('utf-16le')).digest() + return binascii.hexlify(hash_str).upper() def checkPassword(challenge_password, password): + """ Check if a given password match the hash of a stored password """ challenge_bytes = decodestring(challenge_password[ALGO_LEN:].encode()) digest = challenge_bytes[:DIGEST_LEN] salt = challenge_bytes[DIGEST_LEN:] @@ -74,7 +79,7 @@ class SSHAPasswordHasher(hashers.BasePasswordHasher): algorithm = ALGO_NAME - def encode(self, password, salt, iterations=None): + def encode(self, password, salt): """ Hash and salt the given password using SSHA algorithm @@ -92,16 +97,16 @@ class SSHAPasswordHasher(hashers.BasePasswordHasher): def safe_summary(self, encoded): """ - Provides a safe summary ofthe password + Provides a safe summary of the password """ assert encoded.startswith(self.algorithm) - hash = encoded[ALGO_LEN:] - hash = binascii.hexlify(decodestring(hash.encode())).decode() + hash_str = encoded[ALGO_LEN:] + hash_str = binascii.hexlify(decodestring(hash_str.encode())).decode() return OrderedDict([ ('algorithm', self.algorithm), ('iterations', 0), - ('salt', hashers.mask_hash(hash[2*DIGEST_LEN:], show=2)), - ('hash', hashers.mask_hash(hash[:2*DIGEST_LEN])), + ('salt', hashers.mask_hash(hash_str[2*DIGEST_LEN:], show=2)), + ('hash', hashers.mask_hash(hash_str[:2*DIGEST_LEN])), ]) def harden_runtime(self, password, encoded): diff --git a/re2o/management/commands/gen_contrib.py b/re2o/management/commands/gen_contrib.py index 08e57304..6003b30f 100644 --- a/re2o/management/commands/gen_contrib.py +++ b/re2o/management/commands/gen_contrib.py @@ -20,20 +20,28 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Write in a python file the list of all contributors sorted by number of commits. -This list is extracted from the FedeRez gitlab repository. +Write in a python file the list of all contributors sorted by number of +commits. This list is extracted from the current gitlab repository. """ -from django.core.management.base import BaseCommand, CommandError import os +from django.core.management.base import BaseCommand + class Command(BaseCommand): - help = 'Update contributors list' + """ The command object for `gen_contrib` """ + help = 'Update contributors list' def handle(self, *args, **options): - contributeurs = [item.split('\t')[1] for item in os.popen("git shortlog -s -n").read().split("\n") if '\t' in item] + contributeurs = [ + item.split('\t')[1] + for item in os.popen("git shortlog -s -n").read().split("\n") + if '\t' in item + ] self.stdout.write(self.style.SUCCESS("Exportation Sucessfull")) with open("re2o/contributors.py", "w") as contrib_file: - contrib_file.write("#!/usr/bin/env python3\n") + contrib_file.write("\"\"\"re2o.contributors\n") + contrib_file.write("A list of the proud contributors to Re2o\n") + contrib_file.write("\"\"\"\n") contrib_file.write("\n") contrib_file.write("CONTRIBUTORS = " + str(contributeurs)) diff --git a/re2o/mixins.py b/re2o/mixins.py index 307074ff..c13e841c 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -19,27 +19,44 @@ # 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. +"""re2o.mixins +A set of mixins used all over the project to avoid duplicating code +""" from reversion import revisions as reversion class RevMixin(object): + """ A mixin to subclass the save and delete function of a model + to enforce the versioning of the object before those actions + really happen """ def save(self, *args, **kwargs): + """ Creates a version of this object and save it to database """ if self.pk is None: reversion.set_comment("Création") return super(RevMixin, self).save(*args, **kwargs) def delete(self, *args, **kwargs): + """ Creates a version of this object and delete it from database """ reversion.set_comment("Suppresion") return super(RevMixin, self).delete(*args, **kwargs) class FormRevMixin(object): + """ A mixin to subclass the save function of a form + to enforce the versionning of the object before it is really edited """ def save(self, *args, **kwargs): + """ Create a version of this object and save it to database """ if reversion.get_comment() != "" and self.changed_data != []: - reversion.set_comment(reversion.get_comment() + ",%s" % ', '.join(field for field in self.changed_data)) + reversion.set_comment( + reversion.get_comment() + ",%s" + % ', '.join(field for field in self.changed_data) + ) elif self.changed_data: - reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in self.changed_data)) + reversion.set_comment( + "Champs modifié(s) : %s" + % ', '.join(field for field in self.changed_data) + ) return super(FormRevMixin, self).save(*args, **kwargs) @@ -47,23 +64,29 @@ class AclMixin(object): """This mixin is used in nearly every class/models defined in re2o apps. It is used by acl, in models (decorators can_...) and in templates tags :get_instance: Applied on a class, take an id argument, return an instance - :can_create: Applied on a class, take the requested user, return if the user - can do the creation - :can_edit: Applied on an instance, return if the user can edit the instance - :can_delete: Applied on an instance, return if the user can delete the instance - :can_view: Applied on an instance, return if the user can view the instance - :can_view_all: Applied on a class, return if the user can view all instances""" + :can_create: Applied on a class, take the requested user, return if the + user can do the creation + :can_edit: Applied on an instance, return if the user can edit the + instance + :can_delete: Applied on an instance, return if the user can delete the + instance + :can_view: Applied on an instance, return if the user can view the + instance + :can_view_all: Applied on a class, return if the user can view all + instances""" @classmethod def get_classname(cls): + """ Returns the name of the class where this mixin is used """ return str(cls.__name__).lower() @classmethod def get_modulename(cls): + """ Returns the name of the module where this mixin is used """ return str(cls.__module__).split('.')[0].lower() @classmethod - def get_instance(cls, *args, **kwargs): + def get_instance(cls, *_args, **kwargs): """Récupère une instance :param objectid: Instance id à trouver :return: Une instance de la classe évidemment""" @@ -71,42 +94,66 @@ class AclMixin(object): return cls.objects.get(pk=object_id) @classmethod - def can_create(cls, user_request, *args, **kwargs): + def can_create(cls, user_request, *_args, **_kwargs): """Verifie que l'user a les bons droits pour créer un object :param user_request: instance utilisateur qui fait la requête :return: soit True, soit False avec la raison de l'échec""" - return user_request.has_perm(cls.get_modulename() + '.add_' + cls.get_classname()), u"Vous n'avez pas le droit\ - de créer un " + cls.get_classname() + return ( + user_request.has_perm( + cls.get_modulename() + '.add_' + cls.get_classname() + ), + u"Vous n'avez pas le droit de créer un " + cls.get_classname() + ) - def can_edit(self, user_request, *args, **kwargs): + def can_edit(self, user_request, *_args, **_kwargs): """Verifie que l'user a les bons droits pour editer cette instance :param self: Instance à editer :param user_request: Utilisateur qui fait la requête :return: soit True, soit False avec la raison de l'échec""" - return user_request.has_perm(self.get_modulename() + '.change_' + self.get_classname()), u"Vous n'avez pas le droit d'éditer des " + self.get_classname() + return ( + user_request.has_perm( + self.get_modulename() + '.change_' + self.get_classname() + ), + u"Vous n'avez pas le droit d'éditer des " + self.get_classname() + ) - def can_delete(self, user_request, *args, **kwargs): + def can_delete(self, user_request, *_args, **_kwargs): """Verifie que l'user a les bons droits pour delete cette instance :param self: Instance à delete :param user_request: Utilisateur qui fait la requête :return: soit True, soit False avec la raison de l'échec""" - return user_request.has_perm(self.get_modulename() + '.delete_' + self.get_classname()), u"Vous n'avez pas le droit d'éditer des " + self.get_classname() + return ( + user_request.has_perm( + self.get_modulename() + '.delete_' + self.get_classname() + ), + u"Vous n'avez pas le droit d'éditer des " + self.get_classname() + ) @classmethod - def can_view_all(cls, user_request, *args, **kwargs): + def can_view_all(cls, user_request, *_args, **_kwargs): """Vérifie qu'on peut bien afficher l'ensemble des objets, droit particulier view objet correspondant :param user_request: instance user qui fait l'edition :return: True ou False avec la raison de l'échec le cas échéant""" - return user_request.has_perm(cls.get_modulename() + '.view_' + cls.get_classname()), u"Vous n'avez pas le droit de voir des " + cls.get_classname() + return ( + user_request.has_perm( + cls.get_modulename() + '.view_' + cls.get_classname() + ), + u"Vous n'avez pas le droit de voir des " + cls.get_classname() + ) - def can_view(self, user_request, *args, **kwargs): + def can_view(self, user_request, *_args, **_kwargs): """Vérifie qu'on peut bien voir cette instance particulière avec droit view objet :param self: instance à voir :param user_request: instance user qui fait l'edition :return: True ou False avec la raison de l'échec le cas échéant""" - return user_request.has_perm(self.get_modulename() + '.view_' + self.get_classname()), u"Vous n'avez pas le droit de voir des " + self.get_classname() + return ( + user_request.has_perm( + self.get_modulename() + '.view_' + self.get_classname() + ), + u"Vous n'avez pas le droit de voir des " + self.get_classname() + ) diff --git a/re2o/script_utils.py b/re2o/script_utils.py index e72ea626..e1420e6d 100644 --- a/re2o/script_utils.py +++ b/re2o/script_utils.py @@ -18,33 +18,42 @@ # 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. +"""re2o.script_utils +A set of utility scripts that can be used as standalone to interact easily +with Re2o throught the CLI +""" -import os, sys, pwd +import os +from os.path import dirname +import sys +import pwd + +from getpass import getpass +from reversion import revisions as reversion -proj_path="/var/www/re2o" -os.environ.setdefault("DJANGO_SETTINGS_MODULE","re2o.settings") -sys.path.append(proj_path) -os.chdir(proj_path) from django.core.wsgi import get_wsgi_application -application = get_wsgi_application() - - from django.core.management.base import CommandError +from django.db import transaction +from django.utils.html import strip_tags + from users.models import User -from django.utils.html import strip_tags -from reversion import revisions as reversion -from django.db import transaction -from getpass import getpass +proj_path = dirname(dirname(__file__)) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings") +sys.path.append(proj_path) +os.chdir(proj_path) + +application = get_wsgi_application() def get_user(pseudo): """Cherche un utilisateur re2o à partir de son pseudo""" user = User.objects.filter(pseudo=pseudo) - if len(user)==0: + if len(user) == 0: raise CommandError("Utilisateur invalide") - if len(user)>1: - raise CommandError("Plusieurs utilisateurs correspondant à ce pseudo. Ceci NE DEVRAIT PAS arriver") + if len(user) > 1: + raise CommandError("Plusieurs utilisateurs correspondant à ce " + "pseudo. Ceci NE DEVRAIT PAS arriver") return user[0] @@ -53,7 +62,7 @@ def get_system_user(): return pwd.getpwuid(int(os.getenv("SUDO_UID") or os.getuid())).pw_name -def form_cli(Form,user,action,*args,**kwargs): +def form_cli(Form, user, action, *args, **kwargs): """ Remplit un formulaire à partir de la ligne de commande Form : le formulaire (sous forme de classe) à remplir @@ -61,26 +70,30 @@ def form_cli(Form,user,action,*args,**kwargs): action : l'action réalisée par le formulaire (pour les logs) Les arguments suivants sont transmis tels quels au formulaire. """ - data={} - dumb_form = Form(user=user,*args,**kwargs) + data = {} + dumb_form = Form(user=user, *args, **kwargs) for key in dumb_form.fields: - if not dumb_form.fields[key].widget.input_type=='hidden': - if dumb_form.fields[key].widget.input_type=='password': - data[key]=getpass("%s : " % dumb_form.fields[key].label) + if not dumb_form.fields[key].widget.input_type == 'hidden': + if dumb_form.fields[key].widget.input_type == 'password': + data[key] = getpass("%s : " % dumb_form.fields[key].label) else: - data[key]=input("%s : " % dumb_form.fields[key].label) + data[key] = input("%s : " % dumb_form.fields[key].label) - form = Form(data,user=user,*args,**kwargs) + form = Form(data, user=user, *args, **kwargs) if not form.is_valid(): sys.stderr.write("Erreurs : \n") for err in form.errors: - #Oui, oui, on gère du HTML là où d'autres ont eu la lumineuse idée de le mettre - sys.stderr.write("\t%s : %s\n" % (err,strip_tags(form.errors[err]))) + # Oui, oui, on gère du HTML là où d'autres ont eu la + # lumineuse idée de le mettre + sys.stderr.write( + "\t%s : %s\n" % (err, strip_tags(form.errors[err])) + ) raise CommandError("Formulaire invalide") with transaction.atomic(), reversion.create_revision(): - form.save() - reversion.set_user(user) - reversion.set_comment(action) + form.save() + reversion.set_user(user) + reversion.set_comment(action) - sys.stdout.write("%s : effectué. La modification peut prendre quelques minutes pour s'appliquer.\n" % action) + sys.stdout.write("%s : effectué. La modification peut prendre " + "quelques minutes pour s'appliquer.\n" % action) diff --git a/re2o/settings.py b/re2o/settings.py index dce3ef8f..bac8982b 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -35,38 +35,37 @@ https://docs.djangoproject.com/en/1.8/ref/settings/ from __future__ import unicode_literals -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os from .settings_local import * +# The root directory for the project +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ - # Auth definition - PASSWORD_HASHERS = ( 're2o.login.SSHAPasswordHasher', 'django.contrib.auth.hashers.PBKDF2PasswordHasher', ) - -AUTH_USER_MODEL = 'users.User' -LOGIN_URL = '/login/' -LOGIN_REDIRECT_URL = '/' - +AUTH_USER_MODEL = 'users.User' # The class to use for authentication +LOGIN_URL = '/login/' # The URL for login page +LOGIN_REDIRECT_URL = '/' # The URL for redirecting after login # Application definition - -INSTALLED_APPS = ( +DJANGO_CONTRIB_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', +) +EXTERNAL_CONTRIB_APPS = ( 'bootstrap3', + 'rest_framework', + 'reversion', +) +LOCAL_APPS = ( 'users', 'machines', 'cotisations', @@ -75,11 +74,14 @@ INSTALLED_APPS = ( 're2o', 'preferences', 'logs', - 'rest_framework', - 'reversion', - 'api' -) + OPTIONNAL_APPS - + 'api', +) +INSTALLED_APPS = ( + DJANGO_CONTRIB_APPS + + EXTERNAL_CONTRIB_APPS + + LOCAL_APPS + + OPTIONNAL_APPS +) MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', @@ -93,14 +95,17 @@ MIDDLEWARE_CLASSES = ( 'reversion.middleware.RevisionMiddleware', ) +# The root url module to define the project URLs ROOT_URLCONF = 're2o.urls' +# The templates configuration (see Django documentation) TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ - os.path.join(BASE_DIR, 'templates').replace('\\', '/'), - ], + # Use only absolute paths with '/' delimiters even on Windows + os.path.join(BASE_DIR, 'templates').replace('\\', '/'), + ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -115,62 +120,52 @@ TEMPLATES = [ }, ] +# The WSGI module to use in a server environment WSGI_APPLICATION = 're2o.wsgi.application' - # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ - LANGUAGE_CODE = 'en' - +USE_I18N = True +USE_L10N = True # Proritary location search for translations # then searches in {app}/locale/ for app in INSTALLED_APPS +# Use only absolute paths with '/' delimiters even on Windows LOCALE_PATHS = [ - BASE_DIR + '/templates/locale/' # to define translations outside of apps + # For translations outside of apps + os.path.join(BASE_DIR, 'templates', 'locale').replace('\\', '/') ] -TIME_ZONE = 'Europe/Paris' - -USE_I18N = True - -USE_L10N = True - +# Should use time zone ? USE_TZ = True +# Router config for database DATABASE_ROUTERS = ['ldapdb.router.Router'] - -# django-bootstrap3 config dictionnary +# django-bootstrap3 config BOOTSTRAP3 = { - 'jquery_url': '/static/js/jquery-2.2.4.min.js', - 'base_url': '/static/bootstrap/', - 'include_jquery': True, - } - + 'jquery_url': '/static/js/jquery-2.2.4.min.js', + 'base_url': '/static/bootstrap/', + 'include_jquery': True, +} BOOTSTRAP_BASE_URL = '/static/bootstrap/' +# Directories where collectstatic should look for static files +# Use only absolute paths with '/' delimiters even on Windows STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. - os.path.join( - BASE_DIR, - 'static', - ), + os.path.join(BASE_DIR, 'static').replace('\\', '/'), ) - -MEDIA_ROOT = '/var/www/re2o/media' - -STATIC_URL = '/static/' - +# Directory where the static files served by the server are stored STATIC_ROOT = os.path.join(BASE_DIR, 'static_files') +# The URL to access the static files +STATIC_URL = '/static/' +# Directory where the media files served by the server are stored +MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace('\\', '/') +# The URL to access the static files +MEDIA_URL = '/media/' -RIGHTS_LINK = { - 'cableur' : ['bureau','infra','bofh','tresorier'], - 'bofh' : ['bureau','tresorier'], - } - +# Models to use for graphs GRAPH_MODELS = { - 'all_applications': True, - 'group_models': True, + 'all_applications': True, + 'group_models': True, } diff --git a/re2o/settings_local.example.py b/re2o/settings_local.example.py index c541840f..ef017c5d 100644 --- a/re2o/settings_local.example.py +++ b/re2o/settings_local.example.py @@ -19,45 +19,56 @@ # 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. +"""re2o.settings_locale.example +The example settings_locale.py file with all the available +options for a locale configuration of re2o +""" from __future__ import unicode_literals +# A secret key used by the server. SECRET_KEY = 'SUPER_SECRET_KEY' +# The password to access the project database DB_PASSWORD = 'SUPER_SECRET_DB' -# AES key for secret key encryption length must be a multiple of 16 -AES_KEY = 'THE_AES_KEY' - +# AES key for secret key encryption. +# The length must be a multiple of 16 +AES_KEY = 'A_SECRET_AES_KEY' +# Should the server run in debug mode ? # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False +# A list of admins of the services. Receive mails when an error occurs ADMINS = [('Example', 'rezo-admin@example.org')] -SERVER_EMAIL = 'no-reply@example.org' - -# Obligatoire, liste des host autorisés +# The list of hostname the server will respond to. ALLOWED_HOSTS = ['URL_SERVER'] +# The time zone the server is runned in +TIME_ZONE = 'Europe/Paris' + +# The storage systems parameters to use DATABASES = { - 'default': { + 'default': { # The DB 'ENGINE': 'db_engine', 'NAME': 'db_name_value', 'USER': 'db_user_value', 'PASSWORD': DB_PASSWORD, 'HOST': 'db_host_value', }, - 'ldap': { + 'ldap': { # The LDAP 'ENGINE': 'ldapdb.backends.ldap', 'NAME': 'ldap://ldap_host_ip/', 'USER': 'ldap_dn', - # 'TLS': True, + 'TLS': True, 'PASSWORD': 'SUPER_SECRET_LDAP', - } + } } -# Security settings, à activer une fois https en place +# Security settings for secure https +# Activate once https is correctly configured SECURE_CONTENT_TYPE_NOSNIFF = False SECURE_BROWSER_XSS_FILTER = False SESSION_COOKIE_SECURE = False @@ -66,30 +77,33 @@ CSRF_COOKIE_HTTPONLY = False X_FRAME_OPTIONS = 'DENY' SESSION_COOKIE_AGE = 60 * 60 * 3 +# The path where your organization logo is stored LOGO_PATH = "static_files/logo.png" -EMAIL_HOST = 'MY_EMAIL_HOST' -EMAIL_PORT = MY_EMAIL_PORT +# The mail configuration for Re2o to send mails +SERVER_EMAIL = 'no-reply@example.org' # The mail address to use +EMAIL_HOST = 'MY_EMAIL_HOST' # The host to use +EMAIL_PORT = MY_EMAIL_PORT # The port to use -# Reglages pour la bdd ldap +# Settings of the LDAP structure LDAP = { - 'base_user_dn' : 'cn=Utilisateurs,dc=example,dc=org', - 'base_userservice_dn' : 'ou=service-users,dc=example,dc=org', - 'base_usergroup_dn' : 'ou=posix,ou=groups,dc=example,dc=org', - 'base_userservicegroup_dn' : 'ou=services,ou=groups,dc=example,dc=org', - 'user_gid' : 500, + 'base_user_dn': 'cn=Utilisateurs,dc=example,dc=org', + 'base_userservice_dn': 'ou=service-users,dc=example,dc=org', + 'base_usergroup_dn': 'ou=posix,ou=groups,dc=example,dc=org', + 'base_userservicegroup_dn': 'ou=services,ou=groups,dc=example,dc=org', + 'user_gid': 500, } - +# A range of UID to use. Used in linux environement UID_RANGES = { - 'users' : [21001,30000], - 'service-users' : [20000,21000], + 'users': [21001, 30000], + 'service-users': [20000, 21000], } -# Chaque groupe a un gid assigné, voici la place libre pour assignation +# A range of GID to use. Used in linux environement GID_RANGES = { - 'posix' : [501, 600], + 'posix': [501, 600], } +# Some Django apps you want to add in you local project OPTIONNAL_APPS = () - diff --git a/re2o/templates/re2o/index.html b/re2o/templates/re2o/index.html index 25eec921..3de7945c 100644 --- a/re2o/templates/re2o/index.html +++ b/re2o/templates/re2o/index.html @@ -37,7 +37,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% for service in service_list %}
- {{ service.name }} + {% if service.image %} + {{ service.name }} + {% endif %}

{{ service.name }}

{{ service.description }}

diff --git a/re2o/templatetags/__init__.py b/re2o/templatetags/__init__.py index 5be3ef33..cff50933 100644 --- a/re2o/templatetags/__init__.py +++ b/re2o/templatetags/__init__.py @@ -18,4 +18,3 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - diff --git a/re2o/templatetags/acl.py b/re2o/templatetags/acl.py index 0f67169f..fccbea1d 100644 --- a/re2o/templatetags/acl.py +++ b/re2o/templatetags/acl.py @@ -85,32 +85,32 @@ register = template.Library() MODEL_NAME = { # cotisations - 'Facture' : cotisations.models.Facture, - 'Vente' : cotisations.models.Vente, - 'Article' : cotisations.models.Article, - 'Banque' : cotisations.models.Banque, - 'Paiement' : cotisations.models.Paiement, - 'Cotisation' : cotisations.models.Cotisation, + 'Facture': cotisations.models.Facture, + 'Vente': cotisations.models.Vente, + 'Article': cotisations.models.Article, + 'Banque': cotisations.models.Banque, + 'Paiement': cotisations.models.Paiement, + 'Cotisation': cotisations.models.Cotisation, # machines - 'Machine' : machines.models.Machine, - 'MachineType' : machines.models.MachineType, - 'IpType' : machines.models.IpType, - 'Vlan' : machines.models.Vlan, - 'Nas' : machines.models.Nas, - 'SOA' : machines.models.SOA, - 'Extension' : machines.models.Extension, - 'Mx' : machines.models.Mx, - 'Ns' : machines.models.Ns, - 'Txt' : machines.models.Txt, - 'Srv' : machines.models.Srv, - 'Interface' : machines.models.Interface, - 'Domain' : machines.models.Domain, - 'IpList' : machines.models.IpList, - 'Ipv6List' : machines.models.Ipv6List, - 'machines.Service' : machines.models.Service, - 'Service_link' : machines.models.Service_link, - 'OuverturePortList' : machines.models.OuverturePortList, - 'OuverturePort' : machines.models.OuverturePort, + 'Machine': machines.models.Machine, + 'MachineType': machines.models.MachineType, + 'IpType': machines.models.IpType, + 'Vlan': machines.models.Vlan, + 'Nas': machines.models.Nas, + 'SOA': machines.models.SOA, + 'Extension': machines.models.Extension, + 'Mx': machines.models.Mx, + 'Ns': machines.models.Ns, + 'Txt': machines.models.Txt, + 'Srv': machines.models.Srv, + 'Interface': machines.models.Interface, + 'Domain': machines.models.Domain, + 'IpList': machines.models.IpList, + 'Ipv6List': machines.models.Ipv6List, + 'machines.Service': machines.models.Service, + 'Service_link': machines.models.Service_link, + 'OuverturePortList': machines.models.OuverturePortList, + 'OuverturePort': machines.models.OuverturePort, # preferences 'OptionalUser': preferences.models.OptionalUser, 'OptionalMachine': preferences.models.OptionalMachine, @@ -120,25 +120,25 @@ MODEL_NAME = { 'AssoOption': preferences.models.AssoOption, 'MailMessageOption': preferences.models.MailMessageOption, # topologie - 'Stack' : topologie.models.Stack, - 'Switch' : topologie.models.Switch, - 'AccessPoint' : topologie.models.AccessPoint, - 'ModelSwitch' : topologie.models.ModelSwitch, - 'ConstructorSwitch' : topologie.models.ConstructorSwitch, - 'Port' : topologie.models.Port, - 'Room' : topologie.models.Room, - 'Building' : topologie.models.Building, - 'SwitchBay' : topologie.models.SwitchBay, + 'Stack': topologie.models.Stack, + 'Switch': topologie.models.Switch, + 'AccessPoint': topologie.models.AccessPoint, + 'ModelSwitch': topologie.models.ModelSwitch, + 'ConstructorSwitch': topologie.models.ConstructorSwitch, + 'Port': topologie.models.Port, + 'Room': topologie.models.Room, + 'Building': topologie.models.Building, + 'SwitchBay': topologie.models.SwitchBay, # users - 'User' : users.models.User, - 'Adherent' : users.models.Adherent, - 'Club' : users.models.Club, - 'ServiceUser' : users.models.ServiceUser, - 'School' : users.models.School, - 'ListRight' : users.models.ListRight, - 'ListShell' : users.models.ListShell, - 'Ban' : users.models.Ban, - 'Whitelist' : users.models.Whitelist, + 'User': users.models.User, + 'Adherent': users.models.Adherent, + 'Club': users.models.Club, + 'ServiceUser': users.models.ServiceUser, + 'School': users.models.School, + 'ListRight': users.models.ListRight, + 'ListShell': users.models.ListShell, + 'Ban': users.models.Ban, + 'Whitelist': users.models.Whitelist, } @@ -184,17 +184,41 @@ def get_callback(tag_name, obj=None): if tag_name == 'cannot_view_all': return acl_fct(obj.can_view_all, True) if tag_name == 'can_view_app': - return acl_fct(lambda x : (not any(not sys.modules[o].can_view(x) for o in obj), None), False) + return acl_fct( + lambda x: ( + not any(not sys.modules[o].can_view(x) for o in obj), + None + ), + False + ) if tag_name == 'cannot_view_app': - return acl_fct(lambda x : (not any(not sys.modules[o].can_view(x) for o in obj), None), True) + return acl_fct( + lambda x: ( + not any(not sys.modules[o].can_view(x) for o in obj), + None + ), + True + ) if tag_name == 'can_edit_history': - return acl_fct(lambda user:(user.has_perm('admin.change_logentry'),None),False) + return acl_fct( + lambda user: (user.has_perm('admin.change_logentry'), None), + False + ) if tag_name == 'cannot_edit_history': - return acl_fct(lambda user:(user.has_perm('admin.change_logentry'),None),True) + return acl_fct( + lambda user: (user.has_perm('admin.change_logentry'), None), + True + ) if tag_name == 'can_view_any_app': - return acl_fct(lambda x : (any(sys.modules[o].can_view(x) for o in obj), None), False) + return acl_fct( + lambda x: (any(sys.modules[o].can_view(x) for o in obj), None), + False + ) if tag_name == 'cannot_view_any_app': - return acl_fct(lambda x : (any(sys.modules[o].can_view(x) for o in obj), None), True) + return acl_fct( + lambda x: (any(sys.modules[o].can_view(x) for o in obj), None), + True + ) raise template.TemplateSyntaxError( "%r tag is not a valid can_xxx tag" % tag_name @@ -246,11 +270,11 @@ def acl_app_filter(parser, token): tag_name, *app_name = token.split_contents() except ValueError: raise template.TemplateSyntaxError( - "%r tag require 1 argument : an application" + "%r tag require 1 argument: an application" % token.contents.split()[0] ) for name in app_name: - if not name in sys.modules.keys(): + if name not in sys.modules.keys(): raise template.TemplateSyntaxError( "%r is not a registered application for acl." % name @@ -270,6 +294,7 @@ def acl_app_filter(parser, token): return AclNode(callback, oknodes, konodes) + @register.tag('can_change') @register.tag('cannot_change') def acl_change_filter(parser, token): @@ -277,13 +302,13 @@ def acl_change_filter(parser, token): try: tag_content = token.split_contents() - tag_name = tag_content[0] + # tag_name = tag_content[0] model_name = tag_content[1] field_name = tag_content[2] args = tag_content[3:] except ValueError: raise template.TemplateSyntaxError( - "%r tag require at least 2 argument : the model and the field" + "%r tag require at least 2 argument: the model and the field" % token.contents.split()[0] ) @@ -306,6 +331,7 @@ def acl_change_filter(parser, token): return AclNode(callback, oknodes, konodes, *args) + @register.tag('can_create') @register.tag('cannot_create') @register.tag('can_edit_all') @@ -324,7 +350,7 @@ def acl_model_filter(parser, token): args = tag_content[2:] except ValueError: raise template.TemplateSyntaxError( - "%r tag require at least 1 argument : the model" + "%r tag require at least 1 argument: the model" % token.contents.split()[0] ) @@ -364,7 +390,7 @@ def acl_instance_filter(parser, token): args = tag_content[2:] except ValueError: raise template.TemplateSyntaxError( - "%r tag require at least 1 argument : the instance" + "%r tag require at least 1 argument: the instance" % token.contents.split()[0] ) diff --git a/re2o/templatetags/massive_bootstrap_form.py b/re2o/templatetags/massive_bootstrap_form.py index 26a9bcc8..8bfdbee8 100644 --- a/re2o/templatetags/massive_bootstrap_form.py +++ b/re2o/templatetags/massive_bootstrap_form.py @@ -36,13 +36,14 @@ from bootstrap3.forms import render_field register = template.Library() + @register.simple_tag def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): """ Render a form where some specific fields are rendered using Twitter - Typeahead and/or splitree's Bootstrap Tokenfield to improve the performance, the - speed and UX when dealing with very large datasets (select with 50k+ elts - for instance). + Typeahead and/or splitree's Bootstrap Tokenfield to improve the + performance, the speed and UX when dealing with very large datasets + (select with 50k+ elts for instance). When the fields specified should normally be rendered as a select with single selectable option, Twitter Typeahead is used for a better display and the matching query engine. When dealing with multiple selectable @@ -189,8 +190,6 @@ def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): return mbf_form.render() - - class MBFForm(): """ An object to hold all the information and useful methods needed to create and render a massive django form into an actual HTML and JS @@ -198,7 +197,6 @@ class MBFForm(): Every field that is not listed is rendered as a normal bootstrap_field. """ - def __init__(self, form, mbf_fields, *args, **kwargs): # The django form object self.form = form @@ -224,14 +222,13 @@ class MBFForm(): # HTML code to insert inside a template self.html = "" - def render(self): """ HTML code for the fully rendered form with all the necessary form """ for name, field in self.form.fields.items(): - if not name in self.exclude: + if name not in self.exclude: - if name in self.fields and not name in self.hidden_fields: + if name in self.fields and name not in self.hidden_fields: mbf_field = MBFField( name, field, @@ -256,9 +253,6 @@ class MBFForm(): return mark_safe(self.html) - - - class MBFField(): """ An object to hold all the information and useful methods needed to create and render a massive django form field into an actual HTML and JS @@ -270,7 +264,6 @@ class MBFField(): the displayed input. It's used to store the actual data that will be sent to the server """ - def __init__(self, name_, field_, bound_, choices_, engine_, match_func_, update_on_, gen_select_, *args_, **kwargs_): @@ -278,8 +271,8 @@ class MBFField(): if not isinstance(field_.widget, Select): raise ValueError( ('Field named {f_name} is not a Select and' - 'can\'t be rendered with massive_bootstrap_form.' - ).format( + 'can\'t be rendered with massive_bootstrap_form.') + .format( f_name=name_ ) ) @@ -324,7 +317,6 @@ class MBFField(): self.args = args_ self.kwargs = kwargs_ - def default_choices(self): """ JS code of the variable choices_ """ @@ -351,7 +343,6 @@ class MBFField(): ) ) - def default_engine(self): """ Default JS code of the variable engine_ """ return ( @@ -365,7 +356,6 @@ class MBFField(): name=self.name ) - def default_datasets(self): """ Default JS script of the datasets to use with typeahead """ return ( @@ -384,7 +374,6 @@ class MBFField(): match_func=self.match_func ) - def default_match_func(self): """ Default JS code of the matching function to use with typeahed """ return ( @@ -402,14 +391,12 @@ class MBFField(): name=self.name ) - def render(self): """ HTML code for the fully rendered field """ self.gen_displayed_div() self.gen_hidden_div() return mark_safe(self.html) - def gen_displayed_div(self): """ Generate HTML code for the div that contains displayed tags """ if self.gen_select: @@ -434,7 +421,6 @@ class MBFField(): if not self.gen_select: self.html += self.replace_input - def gen_hidden_div(self): """ Generate HTML code for the div that contains hidden tags """ self.gen_full_js() @@ -449,7 +435,6 @@ class MBFField(): attrs={'id': self.div2_id} ) - def hidden_input(self): """ HTML for the hidden input element """ return render_tag( @@ -462,14 +447,12 @@ class MBFField(): } ) - def gen_full_js(self): """ Generate the full script tag containing the JS code """ self.create_js() self.fill_js() self.get_script() - def create_js(self): """ Generate a template for the whole script to use depending on gen_select and multiple """ @@ -549,7 +532,6 @@ class MBFField(): '}} );' ) - def fill_js(self): """ Fill the template with the correct values """ self.js_script = self.js_script.format( @@ -571,11 +553,12 @@ class MBFField(): typ_init_input=self.typeahead_init_input() ) - def get_script(self): """ Insert the JS code inside a script tag """ - self.js_script = render_tag('script', content=mark_safe(self.js_script)) - + self.js_script = render_tag( + 'script', + content=mark_safe(self.js_script) + ) def del_select(self): """ JS code to delete the select if it has been generated and replace @@ -589,7 +572,6 @@ class MBFField(): replace_input=self.replace_input ) - def gen_hidden(self): """ JS code to add a hidden tag to store the value. """ return ( @@ -606,7 +588,6 @@ class MBFField(): html_name=self.bound.html_name ) - def typeahead_init_input(self): """ JS code to init the fields values """ init_key = self.bound.value() or '""' @@ -624,7 +605,6 @@ class MBFField(): hidden_id=self.hidden_id ) - def typeahead_reset_input(self): """ JS code to reset the fields values """ return ( @@ -635,7 +615,6 @@ class MBFField(): hidden_id=self.hidden_id ) - def typeahead_select(self): """ JS code to create the function triggered when an item is selected through typeahead """ @@ -649,7 +628,6 @@ class MBFField(): hidden_id=self.hidden_id ) - def typeahead_change(self): """ JS code of the function triggered when an item is changed (i.e. looses focus and value has changed since the moment it gained focus ) @@ -666,7 +644,6 @@ class MBFField(): hidden_id=self.hidden_id ) - def typeahead_updates(self): """ JS code for binding external fields changes with a reset """ reset_input = self.typeahead_reset_input() @@ -683,7 +660,6 @@ class MBFField(): ) for u_id in self.update_on] return ''.join(updates) - def tokenfield_init_input(self): """ JS code to init the fields values """ init_key = self.bound.value() or '""' @@ -700,7 +676,6 @@ class MBFField(): ) ) - def tokenfield_reset_input(self): """ JS code to reset the fields values """ return ( @@ -709,7 +684,6 @@ class MBFField(): input_id=self.input_id ) - def tokenfield_create(self): """ JS code triggered when a new token is created in tokenfield. """ return ( @@ -739,7 +713,6 @@ class MBFField(): div2_id=self.div2_id ) - def tokenfield_edit(self): """ JS code triggered when a token is edited in tokenfield. """ return ( @@ -765,7 +738,6 @@ class MBFField(): hidden_id=self.hidden_id ) - def tokenfield_remove(self): """ JS code trigggered when a token is removed from tokenfield. """ return ( @@ -791,7 +763,6 @@ class MBFField(): hidden_id=self.hidden_id ) - def tokenfield_updates(self): """ JS code for binding external fields changes with a reset """ reset_input = self.tokenfield_reset_input() diff --git a/re2o/templatetags/self_adhesion.py b/re2o/templatetags/self_adhesion.py index e1577a13..3b463e68 100644 --- a/re2o/templatetags/self_adhesion.py +++ b/re2o/templatetags/self_adhesion.py @@ -19,12 +19,20 @@ # 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. +"""re2o.templatetags.self_adhesion +A simple templatagetag which returns the value of the option `self_adhesion` +which indicated if a user can creates an account by himself +""" + from django import template -from preferences.models import OptionalUser, GeneralOption +from preferences.models import OptionalUser + register = template.Library() + @register.simple_tag def self_adhesion(): + """ Returns True if the user are allowed to create accounts """ options, _created = OptionalUser.objects.get_or_create() return options.self_adhesion diff --git a/re2o/urls.py b/re2o/urls.py index 52eafaab..b1cccfd9 100644 --- a/re2o/urls.py +++ b/re2o/urls.py @@ -48,6 +48,8 @@ from django.contrib.auth import views as auth_views from .views import index, about_page +handler500 = 're2o.views.handler500' + urlpatterns = [ url(r'^$', index, name='index'), url(r'^about/$', about_page, name='about'), diff --git a/re2o/utils.py b/re2o/utils.py index 3bbfffd4..5a4f9400 100644 --- a/re2o/utils.py +++ b/re2o/utils.py @@ -36,18 +36,13 @@ Fonction : from __future__ import unicode_literals - from django.utils import timezone from django.db.models import Q -from django.contrib import messages -from django.shortcuts import redirect -from django.urls import reverse from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from cotisations.models import Cotisation, Facture, Paiement, Vente -from machines.models import Domain, Interface, Machine +from cotisations.models import Cotisation, Facture, Vente +from machines.models import Interface, Machine from users.models import Adherent, User, Ban, Whitelist -from preferences.models import Service def all_adherent(search_time=None): @@ -118,14 +113,18 @@ def all_has_access(search_time=None): def filter_active_interfaces(interface_set): """Filtre les machines autorisées à sortir sur internet dans une requête""" - return interface_set.filter( - machine__in=Machine.objects.filter( - user__in=all_has_access() - ).filter(active=True) - ).select_related('domain').select_related('machine')\ - .select_related('type').select_related('ipv4')\ - .select_related('domain__extension').select_related('ipv4__ip_type')\ - .distinct() + return (interface_set + .filter( + machine__in=Machine.objects.filter( + user__in=all_has_access() + ).filter(active=True) + ).select_related('domain') + .select_related('machine') + .select_related('type') + .select_related('ipv4') + .select_related('domain__extension') + .select_related('ipv4__ip_type') + .distinct()) def filter_complete_interfaces(interface_set): @@ -160,6 +159,7 @@ def all_active_assigned_interfaces_count(): """ Version light seulement pour compter""" return all_active_interfaces_count().filter(ipv4__isnull=False) + class SortTable: """ Class gathering uselful stuff to sort the colums of a table, according to the column and order requested. It's used with a dict of possible @@ -171,7 +171,8 @@ class SortTable: # the url value and the values are a list of model field name to use to # order the request. They are applied in the order they are given. # A 'default' might be provided to specify what to do if the requested col - # doesn't match any keys. + # doesn't match any keys. + USERS_INDEX = { 'user_name': ['name'], 'user_surname': ['surname'], @@ -255,7 +256,7 @@ class SortTable: } TOPOLOGIE_INDEX_MODEL_SWITCH = { 'model-switch_name': ['reference'], - 'model-switch_contructor' : ['constructor__name'], + 'model-switch_contructor': ['constructor__name'], 'default': ['reference'], } TOPOLOGIE_INDEX_SWITCH_BAY = { @@ -290,6 +291,7 @@ class SortTable: else: return request + def re2o_paginator(request, query_set, pagination_number): """Paginator script for list display in re2o. :request: @@ -307,6 +309,7 @@ def re2o_paginator(request, query_set, pagination_number): results = paginator.page(paginator.num_pages) return results + def remove_user_room(room): """ Déménage de force l'ancien locataire de la chambre """ try: diff --git a/re2o/views.py b/re2o/views.py index 4d7e47d0..19b52832 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -26,30 +26,31 @@ les views from __future__ import unicode_literals +from itertools import chain +import git +from reversion.models import Version + from django.http import Http404 from django.urls import reverse from django.shortcuts import render, redirect from django.template.context_processors import csrf -from django.contrib.auth.decorators import login_required, permission_required -from reversion.models import Version +from django.contrib.auth.decorators import login_required from django.contrib import messages from django.conf import settings from django.utils.translation import ugettext as _ from django.views.decorators.cache import cache_page -import git -import os -import time -from itertools import chain - -from preferences.models import Service -from preferences.models import OptionalUser, GeneralOption, AssoOption -import users, preferences, cotisations, topologie, machines +import preferences +from preferences.models import Service, GeneralOption, AssoOption +import users +import cotisations +import topologie +import machines from .utils import re2o_paginator -from .settings import BASE_DIR, INSTALLED_APPS, MIDDLEWARE_CLASSES from .contributors import CONTRIBUTORS + def form(ctx, template, request): """Form générique, raccourci importé par les fonctions views du site""" context = ctx @@ -64,56 +65,58 @@ def index(request): services[indice % 3].append(serv) return form({'services_urls': services}, 're2o/index.html', request) + #: Binding the corresponding char sequence of history url to re2o models. HISTORY_BIND = { - 'users' : { - 'user' : users.models.User, - 'ban' : users.models.Ban, - 'whitelist' : users.models.Whitelist, - 'school' : users.models.School, - 'listright' : users.models.ListRight, - 'serviceuser' : users.models.ServiceUser, - 'listshell' : users.models.ListShell, + 'users': { + 'user': users.models.User, + 'ban': users.models.Ban, + 'whitelist': users.models.Whitelist, + 'school': users.models.School, + 'listright': users.models.ListRight, + 'serviceuser': users.models.ServiceUser, + 'listshell': users.models.ListShell, }, - 'preferences' : { - 'service' : preferences.models.Service, + 'preferences': { + 'service': preferences.models.Service, }, - 'cotisations' : { - 'facture' : cotisations.models.Facture, - 'article' : cotisations.models.Article, - 'paiement' : cotisations.models.Paiement, - 'banque' : cotisations.models.Banque, + 'cotisations': { + 'facture': cotisations.models.Facture, + 'article': cotisations.models.Article, + 'paiement': cotisations.models.Paiement, + 'banque': cotisations.models.Banque, }, - 'topologie' : { - 'switch' : topologie.models.Switch, - 'port' : topologie.models.Port, - 'room' : topologie.models.Room, - 'stack' : topologie.models.Stack, - 'modelswitch' : topologie.models.ModelSwitch, - 'constructorswitch' : topologie.models.ConstructorSwitch, - 'accesspoint' : topologie.models.AccessPoint, - 'switchbay' : topologie.models.SwitchBay, - 'building' : topologie.models.Building, + 'topologie': { + 'switch': topologie.models.Switch, + 'port': topologie.models.Port, + 'room': topologie.models.Room, + 'stack': topologie.models.Stack, + 'modelswitch': topologie.models.ModelSwitch, + 'constructorswitch': topologie.models.ConstructorSwitch, + 'accesspoint': topologie.models.AccessPoint, + 'switchbay': topologie.models.SwitchBay, + 'building': topologie.models.Building, }, - 'machines' : { - 'machine' : machines.models.Machine, - 'interface' : machines.models.Interface, - 'domain' : machines.models.Domain, - 'machinetype' : machines.models.MachineType, - 'iptype' : machines.models.IpType, - 'extension' : machines.models.Extension, - 'soa' : machines.models.SOA, - 'mx' : machines.models.Mx, - 'txt' : machines.models.Txt, - 'srv' : machines.models.Srv, - 'ns' : machines.models.Ns, - 'service' : machines.models.Service, - 'vlan' : machines.models.Vlan, - 'nas' : machines.models.Nas, - 'ipv6list' : machines.models.Ipv6List, + 'machines': { + 'machine': machines.models.Machine, + 'interface': machines.models.Interface, + 'domain': machines.models.Domain, + 'machinetype': machines.models.MachineType, + 'iptype': machines.models.IpType, + 'extension': machines.models.Extension, + 'soa': machines.models.SOA, + 'mx': machines.models.Mx, + 'txt': machines.models.Txt, + 'srv': machines.models.Srv, + 'ns': machines.models.Ns, + 'service': machines.models.Service, + 'vlan': machines.models.Vlan, + 'nas': machines.models.Nas, + 'ipv6list': machines.models.Ipv6List, }, } + @login_required def history(request, application, object_name, object_id): """Render history for a model. @@ -136,7 +139,7 @@ def history(request, application, object_name, object_id): """ try: model = HISTORY_BIND[application][object_name] - except KeyError as e: + except KeyError: raise Http404(u"Il n'existe pas d'historique pour ce modèle.") object_name_id = object_name + 'id' kwargs = {object_name_id: object_id} @@ -144,21 +147,23 @@ def history(request, application, object_name, object_id): instance = model.get_instance(**kwargs) except model.DoesNotExist: messages.error(request, u"Entrée inexistante") - return redirect(reverse('users:profil', - kwargs={'userid':str(request.user.id)} + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} )) can, msg = instance.can_view(request.user) if not can: messages.error(request, msg or "Vous ne pouvez pas accéder à ce menu") return redirect(reverse( 'users:profil', - kwargs={'userid':str(request.user.id)} + kwargs={'userid': str(request.user.id)} )) pagination_number = GeneralOption.get_cached_value('pagination_number') reversions = Version.objects.get_for_object(instance) if hasattr(instance, 'linked_objects'): for related_object in chain(instance.linked_objects()): - reversions = reversions | Version.objects.get_for_object(related_object) + reversions = (reversions | + Version.objects.get_for_object(related_object)) reversions = re2o_paginator(request, reversions, pagination_number) return render( request, @@ -169,10 +174,13 @@ def history(request, application, object_name, object_id): @cache_page(7 * 24 * 60 * 60) def about_page(request): + """ The view for the about page. + Fetch some info about the configuration of the project. If it can't + get the info from the Git repository, fallback to default string """ option = AssoOption.objects.get() git_info_contributors = CONTRIBUTORS try: - git_repo = git.Repo(BASE_DIR) + git_repo = git.Repo(settings.BASE_DIR) git_info_remote = ", ".join(git_repo.remote().urls) git_info_branch = git_repo.active_branch.name last_commit = git_repo.commit() @@ -185,14 +193,14 @@ def about_page(request): git_info_commit = NO_GIT_MSG git_info_commit_date = NO_GIT_MSG - dependencies = INSTALLED_APPS + MIDDLEWARE_CLASSES + dependencies = settings.INSTALLED_APPS + settings.MIDDLEWARE_CLASSES return render( request, "re2o/about.html", { - 'description': option.description , - 'AssoName' : option.name , + 'description': option.description, + 'AssoName': option.name, 'git_info_contributors': git_info_contributors, 'git_info_remote': git_info_remote, 'git_info_branch': git_info_branch, @@ -202,3 +210,7 @@ def about_page(request): } ) + +def handler500(request): + """The handler view for a 500 error""" + return render(request, 'errors/500.html') diff --git a/re2o/wsgi.py b/re2o/wsgi.py index deb6b330..2a60249b 100644 --- a/re2o/wsgi.py +++ b/re2o/wsgi.py @@ -32,8 +32,9 @@ https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ from __future__ import unicode_literals import os -import sys from os.path import dirname +import sys + from django.core.wsgi import get_wsgi_application diff --git a/search/__init__.py b/search/__init__.py index df6e4256..5460ff54 100644 --- a/search/__init__.py +++ b/search/__init__.py @@ -20,5 +20,8 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""search +The app in charge of evrything related to the search function +""" from .acl import * diff --git a/search/acl.py b/search/acl.py index 5c80e473..7ae541f8 100644 --- a/search/acl.py +++ b/search/acl.py @@ -26,7 +26,8 @@ Here are defined some functions to check acl on the application. """ -def can_view(user): + +def can_view(_user): """Check if an user can view the application. Args: diff --git a/search/tests.py b/search/tests.py index 21fa6d24..d15f1f07 100644 --- a/search/tests.py +++ b/search/tests.py @@ -19,7 +19,10 @@ # 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. +"""search.tests +The tests for the Search module. +""" -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/search/views.py b/search/views.py index 732ec510..871515fa 100644 --- a/search/views.py +++ b/search/views.py @@ -144,7 +144,7 @@ def search_single_word(word, filters, user, if not User.can_view_all(user)[0]: filter_users &= Q(id=user.id) filter_clubs = filter_users - filter_users |= Q(name__icontains=word) + filter_users |= Q(name__icontains=word) filters['users'] |= filter_users filters['clubs'] |= filter_clubs diff --git a/templates/errors/500.html b/templates/errors/500.html new file mode 100644 index 00000000..70468623 --- /dev/null +++ b/templates/errors/500.html @@ -0,0 +1,56 @@ + + + + Re2o : Internal Server Error + + + + + +

Error 500 : Internal Server Error

+
+

+ Congratulations !! You have discovered a bug on Re2o and you've reached a page we try + to hide, you can be proud of youself. We try to track those bugs down but apparently + we have missed one. We sincerely thank you for the help : it is not that easy to catch + them all. +

+

+ An email has been automatically sent to the site administrators. Please avoid + spamming them by trigerring the same issue multiple times. The mail should + contains all the details necessary to understand what went wrong but if your help was + needed, you will probably be contacted by them. +

+

+ This issue will be fixed as soon as possible but please take into consideration the + administrators may not be always available. If your request is really urgent, inform + your local organization, they will help you temporarily fix the issue. +

+

+ Error 500 Funny Image +

+

+ If you have no idea what you've done : + Go back to a safe page +

+ + diff --git a/topologie/__init__.py b/topologie/__init__.py index df6e4256..9e8202ea 100644 --- a/topologie/__init__.py +++ b/topologie/__init__.py @@ -20,5 +20,9 @@ # 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. +"""topologie +The app in charge of handling all the informations about the network +topology like the switches, the rooms, how are the connections, ... +""" from .acl import * diff --git a/topologie/acl.py b/topologie/acl.py index aa6adbde..ebef17c9 100644 --- a/topologie/acl.py +++ b/topologie/acl.py @@ -26,6 +26,7 @@ Here are defined some functions to check acl on the application. """ + def can_view(user): """Check if an user can view the application. diff --git a/topologie/forms.py b/topologie/forms.py index bb0c2888..18831217 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -32,16 +32,18 @@ NewSwitchForm) from __future__ import unicode_literals +from django import forms +from django.forms import ModelForm +from django.db.models import Prefetch + from machines.models import Interface from machines.forms import ( - EditInterfaceForm, EditMachineForm, NewMachineForm ) -from django import forms -from django.forms import ModelForm, Form -from django.db.models import Prefetch -from .models import ( +from re2o.mixins import FormRevMixin + +from .models import ( Port, Switch, Room, @@ -52,7 +54,7 @@ from .models import ( SwitchBay, Building, ) -from re2o.mixins import FormRevMixin + class PortForm(FormRevMixin, ModelForm): """Formulaire pour la création d'un port d'un switch @@ -82,32 +84,48 @@ class EditPortForm(FormRevMixin, ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditPortForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['machine_interface'].queryset = Interface.objects.all()\ - .select_related('domain__extension') - self.fields['related'].queryset = Port.objects.all()\ + self.fields['machine_interface'].queryset = ( + Interface.objects.all().select_related('domain__extension') + ) + self.fields['related'].queryset = ( + Port.objects.all() .prefetch_related(Prefetch( - 'switch__interface_set', - queryset=Interface.objects.select_related('ipv4__ip_type__extension').select_related('domain__extension') + 'switch__interface_set', + queryset=(Interface.objects + .select_related('ipv4__ip_type__extension') + .select_related('domain__extension')) )) + ) class AddPortForm(FormRevMixin, ModelForm): """Permet d'ajouter un port de switch. Voir EditPortForm pour plus d'informations""" class Meta(PortForm.Meta): - fields = ['port', 'room', 'machine_interface', 'related', - 'radius', 'vlan_force', 'details'] + fields = [ + 'port', + 'room', + 'machine_interface', + 'related', + 'radius', + 'vlan_force', + 'details' + ] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(AddPortForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['machine_interface'].queryset = Interface.objects.all()\ - .select_related('domain__extension') - self.fields['related'].queryset = Port.objects.all()\ - .prefetch_related(Prefetch( - 'switch__interface_set', - queryset=Interface.objects.select_related('ipv4__ip_type__extension').select_related('domain__extension') + self.fields['machine_interface'].queryset = ( + Interface.objects.all().select_related('domain__extension') + ) + self.fields['related'].queryset = ( + Port.objects.all().prefetch_related(Prefetch( + 'switch__interface_set', + queryset=(Interface.objects + .select_related('ipv4__ip_type__extension') + .select_related('domain__extension')) )) + ) class StackForm(FormRevMixin, ModelForm): @@ -170,15 +188,22 @@ class CreatePortsForm(forms.Form): class EditModelSwitchForm(FormRevMixin, ModelForm): """Permet d'éediter un modèle de switch : nom et constructeur""" - members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False) - + members = forms.ModelMultipleChoiceField( + Switch.objects.all(), + required=False + ) + class Meta: model = ModelSwitch fields = '__all__' def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(EditModelSwitchForm, self).__init__(*args, prefix=prefix, **kwargs) + super(EditModelSwitchForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) instance = kwargs.get('instance', None) if instance: self.initial['members'] = Switch.objects.filter(model=instance) @@ -197,13 +222,20 @@ class EditConstructorSwitchForm(FormRevMixin, ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(EditConstructorSwitchForm, self).__init__(*args, prefix=prefix, **kwargs) + super(EditConstructorSwitchForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) class EditSwitchBayForm(FormRevMixin, ModelForm): """Permet d'éditer une baie de brassage""" - members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False) - + members = forms.ModelMultipleChoiceField( + Switch.objects.all(), + required=False + ) + class Meta: model = SwitchBay fields = '__all__' diff --git a/topologie/models.py b/topologie/models.py index 2a42957b..d8ff2d71 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -47,9 +47,10 @@ from django.db import IntegrityError from django.db import transaction from reversion import revisions as reversion -from machines.models import Machine, Interface, regen +from machines.models import Machine, regen from re2o.mixins import AclMixin, RevMixin + class Stack(AclMixin, RevMixin, models.Model): """Un objet stack. Regrouppe des switchs en foreign key ,contient une id de stack, un switch id min et max dans @@ -85,12 +86,12 @@ class Stack(AclMixin, RevMixin, models.Model): class AccessPoint(AclMixin, Machine): """Define a wireless AP. Inherit from machines.interfaces - + Definition pour une borne wifi , hérite de machines.interfaces """ PRETTY_NAME = "Borne WiFi" - location = models.CharField( + location = models.CharField( max_length=255, help_text="Détails sur la localisation de l'AP", blank=True, @@ -120,7 +121,6 @@ class Switch(AclMixin, Machine): id_max de la stack parente""" PRETTY_NAME = "Switch / Commutateur" - number = models.PositiveIntegerField() stack = models.ForeignKey( 'topologie.Stack', @@ -165,7 +165,8 @@ class Switch(AclMixin, Machine): ne peut être nul"}) def create_ports(self, begin, end): - """ Crée les ports de begin à end si les valeurs données sont cohérentes. """ + """ Crée les ports de begin à end si les valeurs données + sont cohérentes. """ s_begin = s_end = 0 nb_ports = self.ports.count() @@ -192,6 +193,7 @@ class Switch(AclMixin, Machine): ValidationError("Création d'un port existant.") def main_interface(self): + """ Returns the 'main' interface of the switch """ return self.interface_set.first() def __str__(self): @@ -331,14 +333,15 @@ class Port(AclMixin, RevMixin, models.Model): ("view_port", "Peut voir un objet port"), ) - def get_instance(portid, *args, **kwargs): - return Port.objects\ - .select_related('machine_interface__domain__extension')\ - .select_related('machine_interface__machine__switch')\ - .select_related('room')\ - .select_related('related')\ - .prefetch_related('switch__interface_set__domain__extension')\ - .get(pk=portid) + @classmethod + def get_instance(cls, portid, *_args, **kwargs): + return (cls.objects + .select_related('machine_interface__domain__extension') + .select_related('machine_interface__machine__switch') + .select_related('room') + .select_related('related') + .prefetch_related('switch__interface_set__domain__extension') + .get(pk=portid)) def make_port_related(self): """ Synchronise le port distant sur self""" @@ -363,18 +366,24 @@ class Port(AclMixin, RevMixin, models.Model): cohérence""" if hasattr(self, 'switch'): if self.port > self.switch.number: - raise ValidationError("Ce port ne peut exister,\ - numero trop élevé") - if self.room and self.machine_interface or self.room and\ - self.related or self.machine_interface and self.related: - raise ValidationError("Chambre, interface et related_port sont\ - mutuellement exclusifs") + raise ValidationError( + "Ce port ne peut exister, numero trop élevé" + ) + if (self.room and self.machine_interface or + self.room and self.related or + self.machine_interface and self.related): + raise ValidationError( + "Chambre, interface et related_port sont mutuellement " + "exclusifs" + ) if self.related == self: raise ValidationError("On ne peut relier un port à lui même") if self.related and not self.related.related: if self.related.machine_interface or self.related.room: - raise ValidationError("Le port relié est déjà occupé, veuillez\ - le libérer avant de créer une relation") + raise ValidationError( + "Le port relié est déjà occupé, veuillez le libérer " + "avant de créer une relation" + ) else: self.make_port_related() elif hasattr(self, 'related_port'): @@ -402,18 +411,18 @@ class Room(AclMixin, RevMixin, models.Model): @receiver(post_save, sender=AccessPoint) -def ap_post_save(sender, **kwargs): +def ap_post_save(**_kwargs): """Regeneration des noms des bornes vers le controleur""" regen('unifi-ap-names') @receiver(post_delete, sender=AccessPoint) -def ap_post_delete(sender, **kwargs): +def ap_post_delete(**_kwargs): """Regeneration des noms des bornes vers le controleur""" regen('unifi-ap-names') @receiver(post_delete, sender=Stack) -def stack_post_delete(sender, **kwargs): +def stack_post_delete(**_kwargs): """Vide les id des switches membres d'une stack supprimée""" Switch.objects.filter(stack=None).update(stack_member_id=None) diff --git a/topologie/tests.py b/topologie/tests.py index 21fa6d24..dfe72a14 100644 --- a/topologie/tests.py +++ b/topologie/tests.py @@ -19,7 +19,10 @@ # 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. +"""topologie.tests +The tests for the Topologie module. +""" -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/topologie/urls.py b/topologie/urls.py index 9b0a7065..af3327b7 100644 --- a/topologie/urls.py +++ b/topologie/urls.py @@ -51,12 +51,10 @@ urlpatterns = [ url(r'^switch/(?P[0-9]+)$', views.index_port, name='index-port'), - url( - r'^history/(?P\w+)/(?P[0-9]+)$', + url(r'^history/(?P\w+)/(?P[0-9]+)$', re2o.views.history, name='history', - kwargs={'application':'topologie'}, - ), + kwargs={'application': 'topologie'}), url(r'^edit_port/(?P[0-9]+)$', views.edit_port, name='edit-port'), url(r'^new_port/(?P[0-9]+)$', views.new_port, name='new-port'), url(r'^del_port/(?P[0-9]+)$', views.del_port, name='del-port'), @@ -64,7 +62,9 @@ urlpatterns = [ views.edit_switch, name='edit-switch'), url(r'^new_stack/$', views.new_stack, name='new-stack'), - url(r'^index_physical_grouping/$', views.index_physical_grouping, name='index-physical-grouping'), + url(r'^index_physical_grouping/$', + views.index_physical_grouping, + name='index-physical-grouping'), url(r'^edit_stack/(?P[0-9]+)$', views.edit_stack, name='edit-stack'), @@ -73,16 +73,13 @@ urlpatterns = [ name='del-stack'), url(r'^index_model_switch/$', views.index_model_switch, - name='index-model-switch' - ), + name='index-model-switch'), url(r'^index_model_switch/$', views.index_model_switch, - name='index-model-switch' - ), + name='index-model-switch'), url(r'^new_model_switch/$', views.new_model_switch, - name='new-model-switch' - ), + name='new-model-switch'), url(r'^edit_model_switch/(?P[0-9]+)$', views.edit_model_switch, name='edit-model-switch'), @@ -91,8 +88,7 @@ urlpatterns = [ name='del-model-switch'), url(r'^new_constructor_switch/$', views.new_constructor_switch, - name='new-constructor-switch' - ), + name='new-constructor-switch'), url(r'^edit_constructor_switch/(?P[0-9]+)$', views.edit_constructor_switch, name='edit-constructor-switch'), @@ -101,8 +97,7 @@ urlpatterns = [ name='del-constructor-switch'), url(r'^new_switch_bay/$', views.new_switch_bay, - name='new-switch-bay' - ), + name='new-switch-bay'), url(r'^edit_switch_bay/(?P[0-9]+)$', views.edit_switch_bay, name='edit-switch-bay'), @@ -111,8 +106,7 @@ urlpatterns = [ name='del-switch-bay'), url(r'^new_building/$', views.new_building, - name='new-building' - ), + name='new-building'), url(r'^edit_building/(?P[0-9]+)$', views.edit_building, name='edit-building'), diff --git a/topologie/views.py b/topologie/views.py index 82d945e5..5e7144c3 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -38,37 +38,12 @@ from __future__ import unicode_literals from django.urls import reverse from django.shortcuts import render, redirect from django.contrib import messages -from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.decorators import login_required from django.db import IntegrityError -from django.db import transaction from django.db.models import ProtectedError, Prefetch from django.core.exceptions import ValidationError from django.contrib.staticfiles.storage import staticfiles_storage -from topologie.models import ( - Switch, - Port, - Room, - Stack, - ModelSwitch, - ConstructorSwitch, - AccessPoint, - SwitchBay, - Building -) -from topologie.forms import EditPortForm, NewSwitchForm, EditSwitchForm -from topologie.forms import ( - AddPortForm, - EditRoomForm, - StackForm, - EditModelSwitchForm, - EditConstructorSwitchForm, - CreatePortsForm, - AddAccessPointForm, - EditAccessPointForm, - EditSwitchBayForm, - EditBuildingForm -) from users.views import form from re2o.utils import re2o_paginator, SortTable from re2o.acl import ( @@ -80,8 +55,6 @@ from re2o.acl import ( ) from machines.forms import ( DomainForm, - NewMachineForm, - EditMachineForm, EditInterfaceForm, AddInterfaceForm ) @@ -89,6 +62,33 @@ from machines.views import generate_ipv4_mbf_param from machines.models import Interface from preferences.models import AssoOption, GeneralOption +from .models import ( + Switch, + Port, + Room, + Stack, + ModelSwitch, + ConstructorSwitch, + AccessPoint, + SwitchBay, + Building +) +from .forms import ( + EditPortForm, + NewSwitchForm, + EditSwitchForm, + AddPortForm, + EditRoomForm, + StackForm, + EditModelSwitchForm, + EditConstructorSwitchForm, + CreatePortsForm, + AddAccessPointForm, + EditAccessPointForm, + EditSwitchBayForm, + EditBuildingForm +) + from subprocess import Popen,PIPE @@ -96,12 +96,14 @@ from subprocess import Popen,PIPE @can_view_all(Switch) def index(request): """ Vue d'affichage de tous les swicthes""" - switch_list = Switch.objects\ - .prefetch_related(Prefetch( - 'interface_set', - queryset=Interface.objects.select_related('ipv4__ip_type__extension').select_related('domain__extension') - ))\ - .select_related('stack') + switch_list = (Switch.objects + .prefetch_related(Prefetch( + 'interface_set', + queryset=(Interface.objects + .select_related('ipv4__ip_type__extension') + .select_related('domain__extension')) + )) + .select_related('stack')) switch_list = SortTable.sort( switch_list, request.GET.get('col'), @@ -110,9 +112,11 @@ def index(request): ) pagination_number = GeneralOption.get_cached_value('pagination_number') switch_list = re2o_paginator(request, switch_list, pagination_number) - return render(request, 'topologie/index.html', { - 'switch_list': switch_list - }) + return render( + request, + 'topologie/index.html', + {'switch_list': switch_list} + ) @login_required @@ -120,27 +124,33 @@ def index(request): @can_view(Switch) def index_port(request, switch, switchid): """ Affichage de l'ensemble des ports reliés à un switch particulier""" - port_list = Port.objects.filter(switch=switch)\ - .select_related('room')\ - .select_related('machine_interface__domain__extension')\ - .select_related('machine_interface__machine__user')\ - .select_related('related__switch')\ - .prefetch_related(Prefetch( - 'related__switch__interface_set', - queryset=Interface.objects.select_related('domain__extension') - ))\ - .select_related('switch') + port_list = (Port.objects + .filter(switch=switch) + .select_related('room') + .select_related('machine_interface__domain__extension') + .select_related('machine_interface__machine__user') + .select_related('related__switch') + .prefetch_related(Prefetch( + 'related__switch__interface_set', + queryset=(Interface.objects + .select_related('domain__extension')) + )) + .select_related('switch')) port_list = SortTable.sort( port_list, request.GET.get('col'), request.GET.get('order'), SortTable.TOPOLOGIE_INDEX_PORT ) - return render(request, 'topologie/index_p.html', { - 'port_list': port_list, - 'id_switch': switchid, - 'nom_switch': switch - }) + return render( + request, + 'topologie/index_p.html', + { + 'port_list': port_list, + 'id_switch': switchid, + 'nom_switch': switch + } + ) @login_required @@ -156,20 +166,24 @@ def index_room(request): ) pagination_number = GeneralOption.get_cached_value('pagination_number') room_list = re2o_paginator(request, room_list, pagination_number) - return render(request, 'topologie/index_room.html', { - 'room_list': room_list - }) + return render( + request, + 'topologie/index_room.html', + {'room_list': room_list} + ) @login_required @can_view_all(AccessPoint) def index_ap(request): """ Affichage de l'ensemble des bornes""" - ap_list = AccessPoint.objects\ - .prefetch_related(Prefetch( - 'interface_set', - queryset=Interface.objects.select_related('ipv4__ip_type__extension').select_related('domain__extension') - )) + ap_list = (AccessPoint.objects + .prefetch_related(Prefetch( + 'interface_set', + queryset=(Interface.objects + .select_related('ipv4__ip_type__extension') + .select_related('domain__extension')) + ))) ap_list = SortTable.sort( ap_list, request.GET.get('col'), @@ -178,9 +192,11 @@ def index_ap(request): ) pagination_number = GeneralOption.get_cached_value('pagination_number') ap_list = re2o_paginator(request, ap_list, pagination_number) - return render(request, 'topologie/index_ap.html', { - 'ap_list': ap_list - }) + return render( + request, + 'topologie/index_ap.html', + {'ap_list': ap_list} + ) @login_required @@ -189,8 +205,10 @@ def index_ap(request): @can_view_all(SwitchBay) def index_physical_grouping(request): """Affichage de la liste des stacks (affiche l'ensemble des switches)""" - stack_list = Stack.objects\ - .prefetch_related('switch_set__interface_set__domain__extension') + stack_list = (Stack.objects + .prefetch_related( + 'switch_set__interface_set__domain__extension' + )) building_list = Building.objects.all() switch_bay_list = SwitchBay.objects.select_related('building') stack_list = SortTable.sort( @@ -211,11 +229,15 @@ def index_physical_grouping(request): request.GET.get('order'), SortTable.TOPOLOGIE_INDEX_SWITCH_BAY ) - return render(request, 'topologie/index_physical_grouping.html', { - 'stack_list': stack_list, - 'switch_bay_list': switch_bay_list, - 'building_list' : building_list, - }) + return render( + request, + 'topologie/index_physical_grouping.html', + { + 'stack_list': stack_list, + 'switch_bay_list': switch_bay_list, + 'building_list': building_list, + } + ) @login_required @@ -237,10 +259,14 @@ def index_model_switch(request): request.GET.get('order'), SortTable.TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH ) - return render(request, 'topologie/index_model_switch.html', { - 'model_switch_list': model_switch_list, - 'constructor_switch_list': constructor_switch_list, - }) + return render( + request, + 'topologie/index_model_switch.html', + { + 'model_switch_list': model_switch_list, + 'constructor_switch_list': constructor_switch_list, + } + ) @login_required @@ -263,14 +289,17 @@ def new_port(request, switchid): messages.error(request, "Ce port existe déjà") return redirect(reverse( 'topologie:index-port', - kwargs={'switchid':switchid} - )) - return form({'id_switch': switchid,'topoform': port, 'action_name' : 'Ajouter'}, 'topologie/topo.html', request) + kwargs={'switchid': switchid} + )) + return form( + {'id_switch': switchid, 'topoform': port, 'action_name': 'Ajouter'}, + 'topologie/topo.html', + request) @login_required @can_edit(Port) -def edit_port(request, port_object, portid): +def edit_port(request, port_object, **_kwargs): """ Edition d'un port. Permet de changer le switch parent et l'affectation du port""" @@ -282,25 +311,36 @@ def edit_port(request, port_object, portid): return redirect(reverse( 'topologie:index-port', kwargs={'switchid': str(port_object.switch.id)} - )) - return form({'id_switch': str(port_object.switch.id), 'topoform': port, 'action_name' : 'Editer'}, 'topologie/topo.html', request) + )) + return form( + { + 'id_switch': str(port_object.switch.id), + 'topoform': port, + 'action_name': 'Editer' + }, + 'topologie/topo.html', + request + ) @login_required @can_delete(Port) -def del_port(request, port, portid): +def del_port(request, port, **_kwargs): """ Supprime le port""" if request.method == "POST": try: port.delete() messages.success(request, "Le port a été détruit") except ProtectedError: - messages.error(request, "Le port %s est affecté à un autre objet,\ - impossible de le supprimer" % port) + messages.error( + request, + ("Le port %s est affecté à un autre objet, impossible " + "de le supprimer" % port) + ) return redirect(reverse( 'topologie:index-port', - kwargs={'switchid':str(port.switch.id)} - )) + kwargs={'switchid': str(port.switch.id)} + )) return form({'objet': port}, 'topologie/delete.html', request) @@ -312,39 +352,50 @@ def new_stack(request): if stack.is_valid(): stack.save() messages.success(request, "Stack crée") - return form({'topoform': stack, 'action_name' : 'Créer'}, 'topologie/topo.html', request) + return form( + {'topoform': stack, 'action_name': 'Créer'}, + 'topologie/topo.html', + request + ) @login_required @can_edit(Stack) -def edit_stack(request, stack, stackid): +def edit_stack(request, stack, **_kwargs): """Edition d'un stack (nombre de switches, nom...)""" stack = StackForm(request.POST or None, instance=stack) if stack.is_valid(): if stack.changed_data: stack.save() return redirect(reverse('topologie:index-physical-grouping')) - return form({'topoform': stack, 'action_name' : 'Editer'}, 'topologie/topo.html', request) + return form( + {'topoform': stack, 'action_name': 'Editer'}, + 'topologie/topo.html', + request + ) @login_required @can_delete(Stack) -def del_stack(request, stack, stackid): +def del_stack(request, stack, **_kwargs): """Supprime un stack""" if request.method == "POST": try: stack.delete() messages.success(request, "La stack a eté détruite") except ProtectedError: - messages.error(request, "La stack %s est affectée à un autre\ - objet, impossible de la supprimer" % stack) + messages.error( + request, + ("La stack %s est affectée à un autre objet, impossible " + "de la supprimer" % stack) + ) return redirect(reverse('topologie:index-physical-grouping')) return form({'objet': stack}, 'topologie/delete.html', request) @login_required @can_edit(Stack) -def edit_switchs_stack(request, stack, stackid): +def edit_switchs_stack(request, stack, **_kwargs): """Permet d'éditer la liste des switches dans une stack et l'ajouter""" if request.method == "POST": @@ -375,30 +426,37 @@ def new_switch(request): if switch.is_valid() and interface.is_valid(): user = AssoOption.get_cached_value('utilisateur_asso') if not user: - messages.error(request, "L'user association n'existe pas encore,\ - veuillez le créer ou le linker dans preferences") + messages.error( + request, + ("L'user association n'existe pas encore, veuillez le " + "créer ou le linker dans preferences") + ) return redirect(reverse('topologie:index')) - new_switch = switch.save(commit=False) - new_switch.user = user - new_interface_instance = interface.save(commit=False) - domain.instance.interface_parent = new_interface_instance + new_switch_obj = switch.save(commit=False) + new_switch_obj.user = user + new_interface_obj = interface.save(commit=False) + domain.instance.interface_parent = new_interface_obj if domain.is_valid(): - new_domain_instance = domain.save(commit=False) - new_switch.save() - new_interface_instance.machine = new_switch - new_interface_instance.save() - new_domain_instance.interface_parent = new_interface_instance - new_domain_instance.save() + new_domain_obj = domain.save(commit=False) + new_switch_obj.save() + new_interface_obj.machine = new_switch_obj + new_interface_obj.save() + new_domain_obj.interface_parent = new_interface_obj + new_domain_obj.save() messages.success(request, "Le switch a été créé") return redirect(reverse('topologie:index')) i_mbf_param = generate_ipv4_mbf_param(interface, False) - return form({ - 'topoform': interface, - 'machineform': switch, - 'domainform': domain, - 'i_mbf_param': i_mbf_param, - 'device' : 'switch', - }, 'topologie/topo_more.html', request) + return form( + { + 'topoform': interface, + 'machineform': switch, + 'domainform': domain, + 'i_mbf_param': i_mbf_param, + 'device': 'switch', + }, + 'topologie/topo_more.html', + request + ) @login_required @@ -433,9 +491,13 @@ def create_ports(request, switchid): messages.error(request, ''.join(e)) return redirect(reverse( 'topologie:index-port', - kwargs={'switchid':switchid} + kwargs={'switchid': switchid} )) - return form({'id_switch': switchid, 'topoform': port_form}, 'topologie/switch.html', request) + return form( + {'id_switch': switchid, 'topoform': port_form}, + 'topologie/switch.html', + request + ) @login_required @@ -459,26 +521,30 @@ def edit_switch(request, switch, switchid): instance=switch.interface_set.first().domain ) if switch_form.is_valid() and interface_form.is_valid(): - new_switch = switch_form.save(commit=False) - new_interface_instance = interface_form.save(commit=False) - new_domain = domain_form.save(commit=False) + new_switch_obj = switch_form.save(commit=False) + new_interface_obj = interface_form.save(commit=False) + new_domain_obj = domain_form.save(commit=False) if switch_form.changed_data: - new_switch.save() + new_switch_obj.save() if interface_form.changed_data: - new_interface_instance.save() + new_interface_obj.save() if domain_form.changed_data: - new_domain.save() + new_domain_obj.save() messages.success(request, "Le switch a bien été modifié") return redirect(reverse('topologie:index')) - i_mbf_param = generate_ipv4_mbf_param(interface_form, False ) - return form({ - 'id_switch': switchid, - 'topoform': interface_form, - 'machineform': switch_form, - 'domainform': domain_form, - 'i_mbf_param': i_mbf_param, - 'device' : 'switch', - }, 'topologie/topo_more.html', request) + i_mbf_param = generate_ipv4_mbf_param(interface_form, False) + return form( + { + 'id_switch': switchid, + 'topoform': interface_form, + 'machineform': switch_form, + 'domainform': domain_form, + 'i_mbf_param': i_mbf_param, + 'device': 'switch', + }, + 'topologie/topo_more.html', + request + ) @login_required @@ -501,35 +567,42 @@ def new_ap(request): if ap.is_valid() and interface.is_valid(): user = AssoOption.get_cached_value('utilisateur_asso') if not user: - messages.error(request, "L'user association n'existe pas encore,\ - veuillez le créer ou le linker dans preferences") + messages.error( + request, + ("L'user association n'existe pas encore, veuillez le " + "créer ou le linker dans preferences") + ) return redirect(reverse('topologie:index')) - new_ap = ap.save(commit=False) - new_ap.user = user - new_interface = interface.save(commit=False) - domain.instance.interface_parent = new_interface + new_ap_obj = ap.save(commit=False) + new_ap_obj.user = user + new_interface_obj = interface.save(commit=False) + domain.instance.interface_parent = new_interface_obj if domain.is_valid(): - new_domain_instance = domain.save(commit=False) - new_ap.save() - new_interface.machine = new_ap - new_interface.save() - new_domain_instance.interface_parent = new_interface - new_domain_instance.save() + new_domain_obj = domain.save(commit=False) + new_ap_obj.save() + new_interface_obj.machine = new_ap_obj + new_interface_obj.save() + new_domain_obj.interface_parent = new_interface_obj + new_domain_obj.save() messages.success(request, "La borne a été créé") return redirect(reverse('topologie:index-ap')) i_mbf_param = generate_ipv4_mbf_param(interface, False) - return form({ - 'topoform': interface, - 'machineform': ap, - 'domainform': domain, - 'i_mbf_param': i_mbf_param, - 'device' : 'wifi ap', - }, 'topologie/topo_more.html', request) + return form( + { + 'topoform': interface, + 'machineform': ap, + 'domainform': domain, + 'i_mbf_param': i_mbf_param, + 'device': 'wifi ap', + }, + 'topologie/topo_more.html', + request + ) @login_required @can_edit(AccessPoint) -def edit_ap(request, ap, accesspointid): +def edit_ap(request, ap, **_kwargs): """ Edition d'un switch. Permet de chambre nombre de ports, place dans le stack, interface et machine associée""" interface_form = EditInterfaceForm( @@ -549,29 +622,36 @@ def edit_ap(request, ap, accesspointid): if ap_form.is_valid() and interface_form.is_valid(): user = AssoOption.get_cached_value('utilisateur_asso') if not user: - messages.error(request, "L'user association n'existe pas encore,\ - veuillez le créer ou le linker dans preferences") + messages.error( + request, + ("L'user association n'existe pas encore, veuillez le " + "créer ou le linker dans preferences") + ) return redirect(reverse('topologie:index-ap')) - new_ap = ap_form.save(commit=False) - new_interface = interface_form.save(commit=False) - new_domain = domain_form.save(commit=False) + new_ap_obj = ap_form.save(commit=False) + new_interface_obj = interface_form.save(commit=False) + new_domain_obj = domain_form.save(commit=False) if ap_form.changed_data: - new_ap.save() + new_ap_obj.save() if interface_form.changed_data: - new_interface.save() + new_interface_obj.save() if domain_form.changed_data: - new_domain.save() + new_domain_obj.save() messages.success(request, "La borne a été modifiée") return redirect(reverse('topologie:index-ap')) - i_mbf_param = generate_ipv4_mbf_param(interface_form, False ) - return form({ - 'topoform': interface_form, - 'machineform': ap_form, - 'domainform': domain_form, - 'i_mbf_param': i_mbf_param, - 'device' : 'wifi ap', - }, 'topologie/topo_more.html', request) - + i_mbf_param = generate_ipv4_mbf_param(interface_form, False) + return form( + { + 'topoform': interface_form, + 'machineform': ap_form, + 'domainform': domain_form, + 'i_mbf_param': i_mbf_param, + 'device': 'wifi ap', + }, + 'topologie/topo_more.html', + request + ) + @login_required @can_create(Room) @@ -582,12 +662,16 @@ def new_room(request): room.save() messages.success(request, "La chambre a été créé") return redirect(reverse('topologie:index-room')) - return form({'topoform': room, 'action_name' : 'Ajouter'}, 'topologie/topo.html', request) + return form( + {'topoform': room, 'action_name': 'Ajouter'}, + 'topologie/topo.html', + request + ) @login_required @can_edit(Room) -def edit_room(request, room, roomid): +def edit_room(request, room, **_kwargs): """ Edition numero et details de la chambre""" room = EditRoomForm(request.POST or None, instance=room) if room.is_valid(): @@ -595,25 +679,33 @@ def edit_room(request, room, roomid): room.save() messages.success(request, "La chambre a bien été modifiée") return redirect(reverse('topologie:index-room')) - return form({'topoform': room, 'action_name' : 'Editer'}, 'topologie/topo.html', request) + return form( + {'topoform': room, 'action_name': 'Editer'}, + 'topologie/topo.html', + request + ) @login_required @can_delete(Room) -def del_room(request, room, roomid): +def del_room(request, room, **_kwargs): """ Suppression d'un chambre""" if request.method == "POST": try: room.delete() messages.success(request, "La chambre/prise a été détruite") except ProtectedError: - messages.error(request, "La chambre %s est affectée à un autre objet,\ - impossible de la supprimer (switch ou user)" % room) + messages.error( + request, + ("La chambre %s est affectée à un autre objet, impossible " + "de la supprimer (switch ou user)" % room) + ) return redirect(reverse('topologie:index-room')) - return form({ - 'objet': room, - 'objet_name': 'Chambre' - }, 'topologie/delete.html', request) + return form( + {'objet': room, 'objet_name': 'Chambre'}, + 'topologie/delete.html', + request + ) @login_required @@ -625,39 +717,54 @@ def new_model_switch(request): model_switch.save() messages.success(request, "Le modèle a été créé") return redirect(reverse('topologie:index-model-switch')) - return form({'topoform': model_switch, 'action_name' : 'Ajouter'}, 'topologie/topo.html', request) + return form( + {'topoform': model_switch, 'action_name': 'Ajouter'}, + 'topologie/topo.html', + request + ) @login_required @can_edit(ModelSwitch) -def edit_model_switch(request, model_switch, modelswitchid): +def edit_model_switch(request, model_switch, **_kwargs): """ Edition d'un modèle de switch""" - model_switch = EditModelSwitchForm(request.POST or None, instance=model_switch) + model_switch = EditModelSwitchForm( + request.POST or None, + instance=model_switch + ) if model_switch.is_valid(): if model_switch.changed_data: model_switch.save() messages.success(request, "Le modèle a bien été modifié") return redirect(reverse('topologie:index-model-switch')) - return form({'topoform': model_switch, 'action_name' : 'Editer'}, 'topologie/topo.html', request) + return form( + {'topoform': model_switch, 'action_name': 'Editer'}, + 'topologie/topo.html', + request + ) @login_required @can_delete(ModelSwitch) -def del_model_switch(request, model_switch, modelswitchid): +def del_model_switch(request, model_switch, **_kwargs): """ Suppression d'un modèle de switch""" if request.method == "POST": try: model_switch.delete() messages.success(request, "Le modèle a été détruit") except ProtectedError: - messages.error(request, "Le modèle %s est affectée à un autre objet,\ - impossible de la supprimer (switch ou user)" % model_switch) + messages.error( + request, + ("Le modèle %s est affectée à un autre objet, impossible " + "de la supprimer (switch ou user)" % model_switch) + ) return redirect(reverse('topologie:index-model-switch')) - return form({ - 'objet': model_switch, - 'objet_name': 'Modèle de switch' - }, 'topologie/delete.html', request) + return form( + {'objet': model_switch, 'objet_name': 'Modèle de switch'}, + 'topologie/delete.html', + request + ) @login_required @@ -669,12 +776,16 @@ def new_switch_bay(request): switch_bay.save() messages.success(request, "La baie a été créé") return redirect(reverse('topologie:index-physical-grouping')) - return form({'topoform': switch_bay, 'action_name' : 'Ajouter'}, 'topologie/topo.html', request) + return form( + {'topoform': switch_bay, 'action_name': 'Ajouter'}, + 'topologie/topo.html', + request + ) @login_required @can_edit(SwitchBay) -def edit_switch_bay(request, switch_bay, switchbayid): +def edit_switch_bay(request, switch_bay, **_kwargs): """ Edition d'une baie de switch""" switch_bay = EditSwitchBayForm(request.POST or None, instance=switch_bay) if switch_bay.is_valid(): @@ -682,25 +793,33 @@ def edit_switch_bay(request, switch_bay, switchbayid): switch_bay.save() messages.success(request, "Le switch a bien été modifié") return redirect(reverse('topologie:index-physical-grouping')) - return form({'topoform': switch_bay, 'action_name' : 'Editer'}, 'topologie/topo.html', request) + return form( + {'topoform': switch_bay, 'action_name': 'Editer'}, + 'topologie/topo.html', + request + ) @login_required @can_delete(SwitchBay) -def del_switch_bay(request, switch_bay, switchbayid): +def del_switch_bay(request, switch_bay, **_kwargs): """ Suppression d'une baie de switch""" if request.method == "POST": try: switch_bay.delete() messages.success(request, "La baie a été détruite") except ProtectedError: - messages.error(request, "La baie %s est affecté à un autre objet,\ - impossible de la supprimer (switch ou user)" % switch_bay) + messages.error( + request, + ("La baie %s est affecté à un autre objet, impossible " + "de la supprimer (switch ou user)" % switch_bay) + ) return redirect(reverse('topologie:index-physical-grouping')) - return form({ - 'objet': switch_bay, - 'objet_name': 'Baie de switch' - }, 'topologie/delete.html', request) + return form( + {'objet': switch_bay, 'objet_name': 'Baie de switch'}, + 'topologie/delete.html', + request + ) @login_required @@ -712,12 +831,16 @@ def new_building(request): building.save() messages.success(request, "Le batiment a été créé") return redirect(reverse('topologie:index-physical-grouping')) - return form({'topoform': building, 'action_name' : 'Ajouter'}, 'topologie/topo.html', request) + return form( + {'topoform': building, 'action_name': 'Ajouter'}, + 'topologie/topo.html', + request + ) @login_required @can_edit(Building) -def edit_building(request, building, buildingid): +def edit_building(request, building, **_kwargs): """ Edition d'un batiment""" building = EditBuildingForm(request.POST or None, instance=building) if building.is_valid(): @@ -725,25 +848,33 @@ def edit_building(request, building, buildingid): building.save() messages.success(request, "Le batiment a bien été modifié") return redirect(reverse('topologie:index-physical-grouping')) - return form({'topoform': building, 'action_name' : 'Editer'}, 'topologie/topo.html', request) + return form( + {'topoform': building, 'action_name': 'Editer'}, + 'topologie/topo.html', + request + ) @login_required @can_delete(Building) -def del_building(request, building, buildingid): +def del_building(request, building, **_kwargs): """ Suppression d'un batiment""" if request.method == "POST": try: building.delete() messages.success(request, "La batiment a été détruit") except ProtectedError: - messages.error(request, "Le batiment %s est affecté à un autre objet,\ - impossible de la supprimer (switch ou user)" % building) + messages.error( + request, + ("Le batiment %s est affecté à un autre objet, impossible " + "de la supprimer (switch ou user)" % building) + ) return redirect(reverse('topologie:index-physical-grouping')) - return form({ - 'objet': building, - 'objet_name': 'Bâtiment' - }, 'topologie/delete.html', request) + return form( + {'objet': building, 'objet_name': 'Bâtiment'}, + 'topologie/delete.html', + request + ) @login_required @@ -755,34 +886,48 @@ def new_constructor_switch(request): constructor_switch.save() messages.success(request, "Le constructeur a été créé") return redirect(reverse('topologie:index-model-switch')) - return form({'topoform': constructor_switch, 'action_name' : 'Ajouter'}, 'topologie/topo.html', request) + return form( + {'topoform': constructor_switch, 'action_name': 'Ajouter'}, + 'topologie/topo.html', + request + ) @login_required @can_edit(ConstructorSwitch) -def edit_constructor_switch(request, constructor_switch, constructorswitchid): +def edit_constructor_switch(request, constructor_switch, **_kwargs): """ Edition d'un constructeur de switch""" - constructor_switch = EditConstructorSwitchForm(request.POST or None, instance=constructor_switch) + constructor_switch = EditConstructorSwitchForm( + request.POST or None, + instance=constructor_switch + ) if constructor_switch.is_valid(): if constructor_switch.changed_data: constructor_switch.save() messages.success(request, "Le modèle a bien été modifié") return redirect(reverse('topologie:index-model-switch')) - return form({'topoform': constructor_switch, 'action_name' : 'Editer'}, 'topologie/topo.html', request) + return form( + {'topoform': constructor_switch, 'action_name': 'Editer'}, + 'topologie/topo.html', + request + ) @login_required @can_delete(ConstructorSwitch) -def del_constructor_switch(request, constructor_switch, constructorswitchid): +def del_constructor_switch(request, constructor_switch, **_kwargs): """ Suppression d'un constructeur de switch""" if request.method == "POST": try: constructor_switch.delete() messages.success(request, "Le constructeur a été détruit") except ProtectedError: - messages.error(request, "Le constructeur %s est affecté à un autre objet,\ - impossible de la supprimer (switch ou user)" % constructor_switch) + messages.error( + request, + ("Le constructeur %s est affecté à un autre objet, impossible " + "de la supprimer (switch ou user)" % constructor_switch) + ) return redirect(reverse('topologie:index-model-switch')) return form({ 'objet': constructor_switch, @@ -883,4 +1028,4 @@ def recursive_switchs(port_start, switch_before, lignes,detected): links.append("\"{}\"".format(sw.id)) lignes.append(links[0]+" -> "+links[1]) lignes, detected = recursive_switchs(port.related, port_start.switch, lignes, detected) - return (lignes, detected) \ No newline at end of file + return (lignes, detected) diff --git a/users/__init__.py b/users/__init__.py index df6e4256..b661a850 100644 --- a/users/__init__.py +++ b/users/__init__.py @@ -20,5 +20,12 @@ # 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. +"""users +The app managing everything related to the users such as personal +informations or the right groups. +This is probably the most central app. It is strongly linked with +all the other because a user has devices (machines), a cotisation +(cotisations), a room (topologie) +""" from .acl import * diff --git a/users/acl.py b/users/acl.py index 2e82b0f3..8eca1e63 100644 --- a/users/acl.py +++ b/users/acl.py @@ -26,6 +26,7 @@ Here are defined some functions to check acl on the application. """ + def can_view(user): """Check if an user can view the application. diff --git a/users/admin.py b/users/admin.py index a6b70009..a902d2e2 100644 --- a/users/admin.py +++ b/users/admin.py @@ -56,19 +56,6 @@ from .forms import ( ) -class UserAdmin(admin.ModelAdmin): - """Administration d'un user""" - list_display = ( - 'surname', - 'pseudo', - 'email', - 'school', - 'shell', - 'state' - ) - search_fields = ('surname', 'pseudo') - - class LdapUserAdmin(admin.ModelAdmin): """Administration du ldapuser""" list_display = ('name', 'uidNumber', 'login_shell') @@ -143,7 +130,8 @@ class UserAdmin(VersionAdmin, BaseUserAdmin): 'is_admin', 'shell' ) - list_display = ('pseudo',) + # Need to reset the settings from BaseUserAdmin + # They are using fields we don't use like 'is_staff' list_filter = () fieldsets = ( (None, {'fields': ('pseudo', 'password')}), @@ -175,7 +163,7 @@ class UserAdmin(VersionAdmin, BaseUserAdmin): } ), ) - search_fields = ('pseudo',) + search_fields = ('pseudo', 'surname') ordering = ('pseudo',) filter_horizontal = () diff --git a/users/forms.py b/users/forms.py index 3617b26f..0a17df8b 100644 --- a/users/forms.py +++ b/users/forms.py @@ -41,6 +41,10 @@ from django.utils import timezone from django.contrib.auth.models import Group, Permission from preferences.models import OptionalUser +from re2o.utils import remove_user_room +from re2o.mixins import FormRevMixin +from re2o.field_permissions import FieldPermissionFormMixin + from .models import ( User, ServiceUser, @@ -52,9 +56,6 @@ from .models import ( Adherent, Club ) -from re2o.utils import remove_user_room -from re2o.mixins import FormRevMixin -from re2o.field_permissions import FieldPermissionFormMixin class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm): @@ -89,12 +90,16 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm): password1 = self.cleaned_data.get("passwd1") password2 = self.cleaned_data.get("passwd2") if password1 and password2 and password1 != password2: - raise forms.ValidationError("Les 2 nouveaux mots de passe sont différents") + raise forms.ValidationError( + "Les 2 nouveaux mots de passe sont différents" + ) return password2 def clean_selfpasswd(self): """Verifie si il y a lieu que le mdp self est correct""" - if not self.instance.check_password(self.cleaned_data.get("selfpasswd")): + if not self.instance.check_password( + self.cleaned_data.get("selfpasswd") + ): raise forms.ValidationError("Le mot de passe actuel est incorrect") return @@ -386,7 +391,11 @@ class ClubAdminandMembersForm(FormRevMixin, ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(ClubAdminandMembersForm, self).__init__(*args, prefix=prefix, **kwargs) + super(ClubAdminandMembersForm, self).__init__( + *args, + prefix=prefix, + **kwargs + ) class PasswordForm(FormRevMixin, ModelForm): diff --git a/users/management/commands/chgpass.py b/users/management/commands/chgpass.py index c3fabf8a..9763ae4c 100644 --- a/users/management/commands/chgpass.py +++ b/users/management/commands/chgpass.py @@ -19,12 +19,14 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import os, pwd +import os +import pwd from django.core.management.base import BaseCommand, CommandError from users.forms import PassForm from re2o.script_utils import get_user, get_system_user, form_cli + class Command(BaseCommand): help = "Changer le mot de passe d'un utilisateur" @@ -42,6 +44,13 @@ class Command(BaseCommand): if not ok: raise CommandError(msg) - self.stdout.write("Changement du mot de passe de %s" % target_user.pseudo) + self.stdout.write( + "Changement du mot de passe de %s" % target_user.pseudo + ) - form_cli(PassForm,current_user,"Changement du mot de passe",instance=target_user) + form_cli( + PassForm, + current_user, + "Changement du mot de passe", + instance=target_user + ) diff --git a/users/management/commands/chsh.py b/users/management/commands/chsh.py index 6c5b06f7..6921ad79 100644 --- a/users/management/commands/chsh.py +++ b/users/management/commands/chsh.py @@ -19,7 +19,9 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import os, sys, pwd +import os +import sys +import pwd from django.core.management.base import BaseCommand, CommandError from django.db import transaction @@ -28,6 +30,7 @@ from reversion import revisions as reversion from users.models import User, ListShell from re2o.script_utils import get_user, get_system_user + class Command(BaseCommand): help = 'Change the default shell of a user' @@ -36,13 +39,13 @@ class Command(BaseCommand): def handle(self, *args, **options): - current_username = get_system_user() + current_username = get_system_user() current_user = get_user(current_username) target_username = options["target_username"] or current_username target_user = get_user(target_username) - #L'utilisateur n'a pas le droit de changer le shell + # L'utilisateur n'a pas le droit de changer le shell ok, msg = target_user.can_change_shell(current_user) if not ok: raise CommandError(msg) @@ -52,9 +55,16 @@ class Command(BaseCommand): current_shell = "inconnu" if target_user.shell: current_shell = target_user.shell.get_pretty_name() - self.stdout.write("Choisissez un shell pour l'utilisateur %s (le shell actuel est %s) :" % (target_user.pseudo, current_shell)) + self.stdout.write( + "Choisissez un shell pour l'utilisateur %s (le shell actuel est " + "%s) :" % (target_user.pseudo, current_shell) + ) for shell in shells: - self.stdout.write("%d - %s (%s)" % (shell.id, shell.get_pretty_name(), shell.shell)) + self.stdout.write("%d - %s (%s)" % ( + shell.id, + shell.get_pretty_name(), + shell.shell + )) shell_id = input("Entrez un nombre : ") try: @@ -72,4 +82,7 @@ class Command(BaseCommand): reversion.set_user(current_user) reversion.set_comment("Shell modifié") - self.stdout.write(self.style.SUCCESS("Shell modifié. La modification peut prendre quelques minutes pour s'appliquer.")) + self.stdout.write(self.style.SUCCESS( + "Shell modifié. La modification peut prendre quelques minutes " + "pour s'appliquer." + )) diff --git a/users/management/commands/derniere_connexion.py b/users/management/commands/derniere_connexion.py index 94dbf939..d936fda8 100644 --- a/users/management/commands/derniere_connexion.py +++ b/users/management/commands/derniere_connexion.py @@ -4,7 +4,6 @@ # quelques clics. # # Copyright © 2018 Benjamin Graillot -# # Copyright © 2013-2015 Raphaël-David Lasseri # # This program is free software; you can redistribute it and/or modify @@ -32,7 +31,8 @@ from users.models import User # Une liste d'expressions régulières à chercher dans les logs. # Elles doivent contenir un groupe 'date' et un groupe 'user'. -# Pour le CAS on prend comme entrée cat ~/cas.log | grep -B 2 -A 2 "ACTION: AUTHENTICATION_SUCCESS"| grep 'WHEN\|WHO'|sed 'N;s/\n/ /' +# Pour le CAS on prend comme entrée +# cat ~/cas.log | grep -B 2 -A 2 "ACTION: AUTHENTICATION_SUCCESS"| grep 'WHEN\|WHO'|sed 'N;s/\n/ /' COMPILED_REGEX = map(re.compile, [ r'^(?P\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}).*(?:'r'dovecot.*Login: user=<|'r'sshd.*Accepted.*for 'r')(?P[^ >]+).*$', r'^(?P.*) LOGIN INFO User logged in : (?P.*)', @@ -48,8 +48,10 @@ DATE_FORMATS = [ "%a %b %d CEST %H:%M:%S%Y" ] + class Command(BaseCommand): - help = 'Update the time of the latest connection for users by matching stdin against a set of regular expressions' + help = ('Update the time of the latest connection for users by matching ' + 'stdin against a set of regular expressions') def handle(self, *args, **options): @@ -65,7 +67,9 @@ class Command(BaseCommand): for i, regex in enumerate(COMPILED_REGEX): m = regex.match(line) if m: - parsed_log[m.group('user')] = make_aware(datetime.strptime(m.group('date'), DATE_FORMATS[i])) + parsed_log[m.group('user')] = make_aware( + datetime.strptime(m.group('date'), DATE_FORMATS[i]) + ) return parsed_log parsed_log = parse_logs(sys.stdin) diff --git a/users/management/commands/email.py b/users/management/commands/email.py index a7518b9f..5e1d02c4 100644 --- a/users/management/commands/email.py +++ b/users/management/commands/email.py @@ -7,8 +7,11 @@ from users.models import User UTC = pytz.timezone('UTC') + +# TODO : remove of finsihed this because currently it should +# be failing! Who commited that ?! class Command(BaseCommand): - commands = ['email_remainder',] + commands = ['email_remainder'] args = '[command]' help = 'Send email remainders' @@ -27,6 +30,6 @@ class Command(BaseCommand): elif remaining.days == 1: last_day_reminder() + def month_reminder(): pass - diff --git a/users/management/commands/ldap_sync.py b/users/management/commands/ldap_sync.py index 7588ffc9..9301c788 100644 --- a/users/management/commands/ldap_sync.py +++ b/users/management/commands/ldap_sync.py @@ -20,6 +20,7 @@ from django.core.management.base import BaseCommand, CommandError from users.models import User + class Command(BaseCommand): help = 'Synchronise le ldap à partir du sql. A utiliser dans un cron' @@ -37,4 +38,3 @@ class Command(BaseCommand): def handle(self, *args, **options): for usr in User.objects.all(): usr.ldap_sync(mac_refresh=options['full']) - diff --git a/users/migrations/0035_auto_20161018_0046.py b/users/migrations/0035_auto_20161018_0046.py index 90387af6..aea0d13d 100644 --- a/users/migrations/0035_auto_20161018_0046.py +++ b/users/migrations/0035_auto_20161018_0046.py @@ -37,6 +37,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='user', name='uid_number', - field=models.IntegerField(unique=True, default=users.models.User.auto_uid), + field=models.IntegerField(unique=True, default=users.models.get_fresh_user_uid), ), ] diff --git a/users/migrations/0056_auto_20171015_2033.py b/users/migrations/0056_auto_20171015_2033.py index a47aca6a..90423340 100644 --- a/users/migrations/0056_auto_20171015_2033.py +++ b/users/migrations/0056_auto_20171015_2033.py @@ -32,6 +32,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='user', name='uid_number', - field=models.PositiveIntegerField(default=users.models.User.auto_uid, unique=True), + field=models.PositiveIntegerField(default=users.models.get_fresh_user_uid, unique=True), ), ] diff --git a/users/migrations/0071_auto_20180415_1252.py b/users/migrations/0071_auto_20180415_1252.py new file mode 100644 index 00000000..faf56a5b --- /dev/null +++ b/users/migrations/0071_auto_20180415_1252.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-04-15 10:52 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0070_auto_20180324_1906'), + ] + + operations = [ + migrations.AlterField( + model_name='listright', + name='unix_name', + field=models.CharField(max_length=255, unique=True, validators=[django.core.validators.RegexValidator('^[a-z]+$', message='Les groupes unix ne peuvent contenir que des lettres minuscules')]), + ), + ] diff --git a/users/models.py b/users/models.py index efd46a75..f7bfc128 100644 --- a/users/models.py +++ b/users/models.py @@ -73,7 +73,7 @@ from reversion import revisions as reversion import ldapdb.models import ldapdb.models.fields -from re2o.settings import RIGHTS_LINK, LDAP, GID_RANGES, UID_RANGES +from re2o.settings import LDAP, GID_RANGES, UID_RANGES from re2o.login import hashNT from re2o.field_permissions import FieldPermissionModelMixin from re2o.mixins import AclMixin, RevMixin @@ -153,7 +153,7 @@ class UserManager(BaseUserManager): user.set_password(password) if su: - user.is_superuser=True + user.is_superuser = True user.save(using=self._db) return user @@ -171,7 +171,9 @@ class UserManager(BaseUserManager): """ return self._create_user(pseudo, surname, email, password, True) -class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMixin, AclMixin): + +class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, + PermissionsMixin, AclMixin): """ Definition de l'utilisateur de base. Champs principaux : name, surnname, pseudo, email, room, password Herite du django BaseUser et du système d'auth django""" @@ -185,10 +187,6 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix (2, 'STATE_ARCHIVE'), ) - def auto_uid(): - """Renvoie un uid libre""" - return get_fresh_user_uid() - surname = models.CharField(max_length=255) pseudo = models.CharField( max_length=32, @@ -218,8 +216,15 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix state = models.IntegerField(choices=STATES, default=STATE_ACTIVE) registered = models.DateTimeField(auto_now_add=True) telephone = models.CharField(max_length=15, blank=True, null=True) - uid_number = models.PositiveIntegerField(default=auto_uid, unique=True) - rezo_rez_uid = models.PositiveIntegerField(unique=True, blank=True, null=True) + uid_number = models.PositiveIntegerField( + default=get_fresh_user_uid, + unique=True + ) + rezo_rez_uid = models.PositiveIntegerField( + unique=True, + blank=True, + null=True + ) USERNAME_FIELD = 'pseudo' REQUIRED_FIELDS = ['surname', 'email'] @@ -228,13 +233,18 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix class Meta: permissions = ( - ("change_user_password", "Peut changer le mot de passe d'un user"), + ("change_user_password", + "Peut changer le mot de passe d'un user"), ("change_user_state", "Peut éditer l'etat d'un user"), ("change_user_force", "Peut forcer un déménagement"), ("change_user_shell", "Peut éditer le shell d'un user"), - ("change_user_groups", "Peut éditer les groupes d'un user ! Permission critique"), - ("change_all_users", "Peut éditer tous les users, y compris ceux dotés de droits. Superdroit"), - ("view_user", "Peut voir un objet user quelquonque"), + ("change_user_groups", + "Peut éditer les groupes d'un user ! Permission critique"), + ("change_all_users", + "Peut éditer tous les users, y compris ceux dotés de droits. " + "Superdroit"), + ("view_user", + "Peut voir un objet user quelquonque"), ) @cached_property @@ -267,10 +277,14 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix @cached_property def is_class_club(self): + """ Returns True if the object is a Club (subclassing User) """ + # TODO : change to isinstance (cleaner) return hasattr(self, 'club') @cached_property def is_class_adherent(self): + """ Returns True if the object is a Adherent (subclassing User) """ + # TODO : change to isinstance (cleaner) return hasattr(self, 'adherent') @property @@ -286,7 +300,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix @property def is_admin(self): """ Renvoie si l'user est admin""" - admin,_ = Group.objects.get_or_create(name="admin") + admin, _ = Group.objects.get_or_create(name="admin") return self.is_superuser or admin in self.groups.all() def get_full_name(self): @@ -393,8 +407,9 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix def has_access(self): """ Renvoie si un utilisateur a accès à internet """ - return self.state == User.STATE_ACTIVE\ - and not self.is_ban() and (self.is_connected() or self.is_whitelisted()) + return (self.state == User.STATE_ACTIVE and + not self.is_ban() and + (self.is_connected() or self.is_whitelisted())) def end_access(self): """ Renvoie la date de fin normale d'accès (adhésion ou whiteliste)""" @@ -480,7 +495,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix self.assign_ips() self.state = User.STATE_ACTIVE - def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True, group_refresh=False): + def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True, + group_refresh=False): """ Synchronisation du ldap. Synchronise dans le ldap les attributs de self Options : base : synchronise tous les attributs de base - nom, prenom, @@ -573,12 +589,15 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix 'asso_mail': AssoOption.get_cached_value('contact'), 'site_name': GeneralOption.get_cached_value('site_name'), 'url': request.build_absolute_uri( - reverse('users:process', kwargs={'token': req.token})), - 'expire_in': str(GeneralOption.get_cached_value('req_expire_hrs')) + ' heures', - } + reverse('users:process', kwargs={'token': req.token}) + ), + 'expire_in': str( + GeneralOption.get_cached_value('req_expire_hrs') + ) + ' heures', + } send_mail( - 'Changement de mot de passe du %(name)s / Password\ - renewal for %(name)s' % {'name': AssoOption.get_cached_value('name')}, + 'Changement de mot de passe du %(name)s / Password renewal for ' + '%(name)s' % {'name': AssoOption.get_cached_value('name')}, template.render(context), GeneralOption.get_cached_value('email_from'), [req.user.email], @@ -590,7 +609,9 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix """ Fonction appellée par freeradius. Enregistre la mac pour une machine inconnue sur le compte de l'user""" all_interfaces = self.user_interfaces(active=False) - if all_interfaces.count() > OptionalMachine.get_cached_value('max_lambdauser_interfaces'): + if all_interfaces.count() > OptionalMachine.get_cached_value( + 'max_lambdauser_interfaces' + ): return False, "Maximum de machines enregistrees atteinte" if not nas_type: return False, "Re2o ne sait pas à quel machinetype affecter cette\ @@ -625,9 +646,9 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix template = loader.get_template('users/email_auto_newmachine') context = Context({ 'nom': self.get_full_name(), - 'mac_address' : interface.mac_address, + 'mac_address': interface.mac_address, 'asso_name': AssoOption.get_cached_value('name'), - 'interface_name' : interface.domain, + 'interface_name': interface.domain, 'asso_email': AssoOption.get_cached_value('contact'), 'pseudo': self.pseudo, }) @@ -668,19 +689,19 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix num += 1 return composed_pseudo(num) - def can_edit(self, user_request, *args, **kwargs): - """Check if an user can edit an user object. + def can_edit(self, user_request, *_args, **_kwargs): + """Check if a user can edit a user object. :param self: The user which is to be edited. :param user_request: The user who requests to edit self. :return: a message and a boolean which is True if self is a club and - user_request one of its member, or if user_request is self, or if - user_request has the 'cableur' right. + user_request one of its member, or if user_request is self, or if + user_request has the 'cableur' right. """ if self.is_class_club and user_request.is_class_adherent: - if self == user_request or \ - user_request.has_perm('users.change_user') or \ - user_request.adherent in self.club.administrators.all(): + if (self == user_request or + user_request.has_perm('users.change_user') or + user_request.adherent in self.club.administrators.all()): return True, None else: return False, u"Vous n'avez pas le droit d'éditer ce club" @@ -691,98 +712,162 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix return True, None elif user_request.has_perm('users.change_user'): if self.groups.filter(listright__critical=True): - return False, u"Utilisateurs avec droits critiques, ne peut etre édité" + return False, (u"Utilisateurs avec droits critiques, ne " + "peut etre édité") elif self == AssoOption.get_cached_value('utilisateur_asso'): - return False, u"Impossible d'éditer l'utilisateur asso sans droit change_all_users" + return False, (u"Impossible d'éditer l'utilisateur asso " + "sans droit change_all_users") else: return True, None elif user_request.has_perm('users.change_all_users'): return True, None else: - return False, u"Vous ne pouvez éditer un autre utilisateur que vous même" + return False, (u"Vous ne pouvez éditer un autre utilisateur " + "que vous même") - def can_change_password(self, user_request, *args, **kwargs): + def can_change_password(self, user_request, *_args, **_kwargs): + """Check if a user can change a user's password + + :param self: The user which is to be edited + :param user_request: The user who request to edit self + :returns: a message and a boolean which is True if self is a club + and user_request one of it's admins, or if user_request is self, + or if user_request has the right to change other's password + """ if self.is_class_club and user_request.is_class_adherent: - if self == user_request or \ - user_request.has_perm('users.change_user_password') or \ - user_request.adherent in self.club.administrators.all(): + if (self == user_request or + user_request.has_perm('users.change_user_password') or + user_request.adherent in self.club.administrators.all()): return True, None else: return False, u"Vous n'avez pas le droit d'éditer ce club" else: - if self == user_request or \ - user_request.has_perm('users.change_user_groups'): - # Peut éditer les groupes d'un user, c'est un privilège élevé, True + if (self == user_request or + user_request.has_perm('users.change_user_groups')): + # Peut éditer les groupes d'un user, + # c'est un privilège élevé, True return True, None - elif user_request.has_perm('users.change_user') and not self.groups.all(): + elif (user_request.has_perm('users.change_user') and + not self.groups.all()): return True, None else: - return False, u"Vous ne pouvez éditer un autre utilisateur que vous même" + return False, (u"Vous ne pouvez éditer un autre utilisateur " + "que vous même") - def check_selfpasswd(self, user_request, *args, **kwargs): + def check_selfpasswd(self, user_request, *_args, **_kwargs): + """ Returns (True, None) if user_request is self, else returns + (False, None) + """ return user_request == self, None @staticmethod - def can_change_state(user_request, *args, **kwargs): - return user_request.has_perm('users.change_user_state'), "Droit requis pour changer l'état" + def can_change_state(user_request, *_args, **_kwargs): + """ Check if a user can change a state + + :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 state + """ + return ( + user_request.has_perm('users.change_user_state'), + "Droit requis pour changer l'état" + ) @staticmethod - def can_change_shell(user_request, *args, **kwargs): - return user_request.has_perm('users.change_user_shell'), "Droit requis pour changer le shell" + def can_change_shell(user_request, *_args, **_kwargs): + """ Check if a user can change a shell + + :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 shell + """ + return ( + user_request.has_perm('users.change_user_shell'), + "Droit requis pour changer le shell" + ) @staticmethod - def can_change_force(user_request, *args, **kwargs): - return user_request.has_perm('users.change_user_force'), "Droit requis pour forcer le déménagement" + def can_change_force(user_request, *_args, **_kwargs): + """ Check if a user can change a force + + :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 force + """ + return ( + user_request.has_perm('users.change_user_force'), + "Droit requis pour forcer le déménagement" + ) @staticmethod - def can_change_groups(user_request, *args, **kwargs): - return user_request.has_perm('users.change_user_groups'), "Droit requis pour éditer les groupes de l'user" + def can_change_groups(user_request, *_args, **_kwargs): + """ Check if a user can change a group - def can_view(self, user_request, *args, **kwargs): + :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 group + """ + return ( + user_request.has_perm('users.change_user_groups'), + "Droit requis pour éditer les groupes de l'user" + ) + + def can_view(self, user_request, *_args, **_kwargs): """Check if an user can view an user object. :param self: The targeted user. :param user_request: The user who ask for viewing the target. :return: A boolean telling if the acces is granted and an explanation - text + text """ if self.is_class_club and user_request.is_class_adherent: - if self == user_request or \ - user_request.has_perm('users.view_user') or \ - user_request.adherent in self.club.administrators.all() or \ - user_request.adherent in self.club.members.all(): + if (self == user_request or + user_request.has_perm('users.view_user') or + user_request.adherent in self.club.administrators.all() or + user_request.adherent in self.club.members.all()): return True, None else: return False, u"Vous n'avez pas le droit de voir ce club" else: - if self == user_request or user_request.has_perm('users.view_user'): + if (self == user_request or + user_request.has_perm('users.view_user')): return True, None else: - return False, u"Vous ne pouvez voir un autre utilisateur que vous même" + return False, (u"Vous ne pouvez voir un autre utilisateur " + "que vous même") - def can_view_all(user_request, *args, **kwargs): + @staticmethod + def can_view_all(user_request, *_args, **_kwargs): """Check if an user can access to the list of every user objects :param user_request: The user who wants to view the list. - :return: True if the user can view the list and an explanation message. + :return: True if the user can view the list and an explanation + message. """ - return user_request.has_perm('users.view_user'), u"Vous n'avez pas accès à la liste des utilisateurs." + return ( + user_request.has_perm('users.view_user'), + u"Vous n'avez pas accès à la liste des utilisateurs." + ) - def can_delete(self, user_request, *args, **kwargs): + def can_delete(self, user_request, *_args, **_kwargs): """Check if an user can delete an user object. :param self: The user who is to be deleted. :param user_request: The user who requests deletion. - :return: True if user_request has the right 'bureau', and a message. + :return: True if user_request has the right 'bureau', and a + message. """ - return user_request.has_perm('users.delete_user'), u"Vous ne pouvez pas supprimer cet utilisateur." + return ( + user_request.has_perm('users.delete_user'), + u"Vous ne pouvez pas supprimer cet utilisateur." + ) def __init__(self, *args, **kwargs): super(User, self).__init__(*args, **kwargs) self.field_permissions = { - 'shell' : self.can_change_shell, - 'force' : self.can_change_force, - 'selfpasswd' : self.check_selfpasswd, + 'shell': self.can_change_shell, + 'force': self.can_change_force, + 'selfpasswd': self.check_selfpasswd, } def __str__(self): @@ -790,6 +875,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMix class Adherent(User): + """ A class representing a member (it's a user with special + informations) """ PRETTY_NAME = "Adhérents" name = models.CharField(max_length=255) room = models.OneToOneField( @@ -799,32 +886,40 @@ class Adherent(User): null=True ) - def get_instance(adherentid, *args, **kwargs): + @classmethod + def get_instance(cls, adherentid, *_args, **_kwargs): """Try to find an instance of `Adherent` with the given id. :param adherentid: The id of the adherent we are looking for. :return: An adherent. """ - return Adherent.objects.get(pk=adherentid) + return cls.objects.get(pk=adherentid) - def can_create(user_request, *args, **kwargs): + @staticmethod + def can_create(user_request, *_args, **_kwargs): """Check if an user can create an user object. :param user_request: The user who wants to create a user 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. + a user or if the `options.all_can_create` is set. """ - if(not user_request.is_authenticated and not OptionalUser.get_cached_value('self_adhesion')): + if (not user_request.is_authenticated and + not OptionalUser.get_cached_value('self_adhesion')): return False, None else: - if(OptionalUser.get_cached_value('all_can_create_adherent') or OptionalUser.get_cached_value('self_adhesion')): + if (OptionalUser.get_cached_value('all_can_create_adherent') or + OptionalUser.get_cached_value('self_adhesion')): return True, None else: - return user_request.has_perm('users.add_user'), u"Vous n'avez pas le\ - droit de créer un utilisateur" + return ( + user_request.has_perm('users.add_user'), + u"Vous n'avez pas le droit de créer un utilisateur" + ) class Club(User): + """ A class representing a club (it is considered as a user + with special informations) """ PRETTY_NAME = "Clubs" room = models.ForeignKey( 'topologie.Room', @@ -843,15 +938,16 @@ class Club(User): related_name='club_members' ) mailing = models.BooleanField( - default = False + default=False ) - def can_create(user_request, *args, **kwargs): + @staticmethod + def can_create(user_request, *_args, **_kwargs): """Check if an user can create an user object. :param user_request: The user who wants to create a user 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. + an user or if the `options.all_can_create` is set. """ if not user_request.is_authenticated: return False, None @@ -859,54 +955,68 @@ class Club(User): if OptionalUser.get_cached_value('all_can_create_club'): return True, None else: - return user_request.has_perm('users.add_user'), u"Vous n'avez pas le\ - droit de créer un club" + return ( + user_request.has_perm('users.add_user'), + u"Vous n'avez pas le droit de créer un club" + ) - def can_view_all(user_request, *args, **kwargs): + @staticmethod + def can_view_all(user_request, *_args, **_kwargs): """Check if an user can access to the list of every user objects :param user_request: The user who wants to view the list. - :return: True if the user can view the list and an explanation message. + :return: True if the user can view the list and an explanation + message. """ if user_request.has_perm('users.view_user'): return True, None - if hasattr(user_request,'is_class_adherent') and user_request.is_class_adherent: - if user_request.adherent.club_administrator.all() or user_request.adherent.club_members.all(): + if (hasattr(user_request, 'is_class_adherent') and + user_request.is_class_adherent): + if (user_request.adherent.club_administrator.all() or + user_request.adherent.club_members.all()): return True, None return False, u"Vous n'avez pas accès à la liste des utilisateurs." - def get_instance(clubid, *args, **kwargs): + @classmethod + def get_instance(cls, clubid, *_args, **_kwargs): """Try to find an instance of `Club` with the given id. :param clubid: The id of the adherent we are looking for. :return: A club. """ - return Club.objects.get(pk=clubid) + return cls.objects.get(pk=clubid) @receiver(post_save, sender=Adherent) @receiver(post_save, sender=Club) @receiver(post_save, sender=User) -def user_post_save(sender, **kwargs): +def user_post_save(**kwargs): """ Synchronisation post_save : envoie le mail de bienvenue si creation Synchronise le ldap""" - is_created = kwargs['created'] + # is_created = kwargs['created'] user = kwargs['instance'] - #if is_created: - #user.notif_inscription() - user.ldap_sync(base=True, access_refresh=True, mac_refresh=False, group_refresh=True) + # TODO : remove if unnecessary + # if is_created: + # user.notif_inscription() + user.ldap_sync( + base=True, + access_refresh=True, + mac_refresh=False, + group_refresh=True + ) regen('mailing') @receiver(post_delete, sender=Adherent) @receiver(post_delete, sender=Club) @receiver(post_delete, sender=User) -def user_post_delete(sender, **kwargs): +def user_post_delete(**kwargs): """Post delete d'un user, on supprime son instance ldap""" user = kwargs['instance'] user.ldap_del() regen('mailing') + class ServiceUser(RevMixin, AclMixin, AbstractBaseUser): """ Classe des users daemons, règle leurs accès au ldap""" readonly = 'readonly' @@ -943,6 +1053,14 @@ class ServiceUser(RevMixin, AclMixin, AbstractBaseUser): ("view_serviceuser", "Peut voir un objet serviceuser"), ) + def get_full_name(self): + """ Renvoie le nom complet du serviceUser formaté nom/prénom""" + return "ServiceUser <{name}>".format(name=self.pseudo) + + def get_short_name(self): + """ Renvoie seulement le nom""" + return self.pseudo + def ldap_sync(self): """ Synchronisation du ServiceUser dans sa version ldap""" try: @@ -977,15 +1095,16 @@ class ServiceUser(RevMixin, AclMixin, AbstractBaseUser): def __str__(self): return self.pseudo + @receiver(post_save, sender=ServiceUser) -def service_user_post_save(sender, **kwargs): +def service_user_post_save(**kwargs): """ Synchronise un service user ldap après modification django""" service_user = kwargs['instance'] service_user.ldap_sync() @receiver(post_delete, sender=ServiceUser) -def service_user_post_delete(sender, **kwargs): +def service_user_post_delete(**kwargs): """ Supprime un service user ldap après suppression django""" service_user = kwargs['instance'] service_user.ldap_del() @@ -1019,8 +1138,8 @@ class ListRight(RevMixin, AclMixin, Group): unique=True, validators=[RegexValidator( '^[a-z]+$', - message="Les groupes unix ne peuvent contenir\ - que des lettres minuscules" + message=("Les groupes unix ne peuvent contenir que des lettres " + "minuscules") )] ) gid = models.PositiveIntegerField(unique=True, null=True) @@ -1060,14 +1179,14 @@ class ListRight(RevMixin, AclMixin, Group): @receiver(post_save, sender=ListRight) -def listright_post_save(sender, **kwargs): +def listright_post_save(**kwargs): """ Synchronise le droit ldap quand il est modifié""" right = kwargs['instance'] right.ldap_sync() @receiver(post_delete, sender=ListRight) -def listright_post_delete(sender, **kwargs): +def listright_post_delete(**kwargs): """Suppression d'un groupe ldap après suppression coté django""" right = kwargs['instance'] right.ldap_del() @@ -1140,7 +1259,7 @@ class Ban(RevMixin, AclMixin, models.Model): """Ce ban est-il actif?""" return self.date_end > timezone.now() - def can_view(self, user_request, *args, **kwargs): + def can_view(self, user_request, *_args, **_kwargs): """Check if an user can view a Ban object. :param self: The targeted object. @@ -1148,10 +1267,10 @@ class Ban(RevMixin, AclMixin, models.Model): :return: A boolean telling if the acces is granted and an explanation text """ - if not user_request.has_perm('users.view_ban') and\ - self.user != user_request: - return False, u"Vous n'avez pas le droit de voir les bannissements\ - autre que les vôtres" + if (not user_request.has_perm('users.view_ban') and + self.user != user_request): + return False, (u"Vous n'avez pas le droit de voir les " + "bannissements autre que les vôtres") else: return True, None @@ -1160,7 +1279,7 @@ class Ban(RevMixin, AclMixin, models.Model): @receiver(post_save, sender=Ban) -def ban_post_save(sender, **kwargs): +def ban_post_save(**kwargs): """ Regeneration de tous les services après modification d'un ban""" ban = kwargs['instance'] is_created = kwargs['created'] @@ -1177,7 +1296,7 @@ def ban_post_save(sender, **kwargs): @receiver(post_delete, sender=Ban) -def ban_post_delete(sender, **kwargs): +def ban_post_delete(**kwargs): """ Regen de tous les services après suppression d'un ban""" user = kwargs['instance'].user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) @@ -1203,9 +1322,10 @@ class Whitelist(RevMixin, AclMixin, models.Model): ) def is_active(self): + """ Is this whitelisting active ? """ return self.date_end > timezone.now() - def can_view(self, user_request, *args, **kwargs): + def can_view(self, user_request, *_args, **_kwargs): """Check if an user can view a Whitelist object. :param self: The targeted object. @@ -1213,10 +1333,10 @@ class Whitelist(RevMixin, AclMixin, models.Model): :return: A boolean telling if the acces is granted and an explanation text """ - if not user_request.has_perm('users.view_whitelist') and\ - self.user != user_request: - return False, u"Vous n'avez pas le droit de voir les accès\ - gracieux autre que les vôtres" + if (not user_request.has_perm('users.view_whitelist') and + self.user != user_request): + return False, (u"Vous n'avez pas le droit de voir les accès " + "gracieux autre que les vôtres") else: return True, None @@ -1225,7 +1345,7 @@ class Whitelist(RevMixin, AclMixin, models.Model): @receiver(post_save, sender=Whitelist) -def whitelist_post_save(sender, **kwargs): +def whitelist_post_save(**kwargs): """Après modification d'une whitelist, on synchronise les services et on lui permet d'avoir internet""" whitelist = kwargs['instance'] @@ -1242,7 +1362,7 @@ def whitelist_post_save(sender, **kwargs): @receiver(post_delete, sender=Whitelist) -def whitelist_post_delete(sender, **kwargs): +def whitelist_post_delete(**kwargs): """Après suppression d'une whitelist, on supprime l'accès internet en forçant la régénration""" user = kwargs['instance'].user @@ -1270,8 +1390,12 @@ class Request(models.Model): def save(self): if not self.expires_at: - self.expires_at = timezone.now() \ - + datetime.timedelta(hours=GeneralOption.get_cached_value('req_expire_hrs')) + self.expires_at = (timezone.now() + + datetime.timedelta( + hours=GeneralOption.get_cached_value( + 'req_expire_hrs' + ) + )) if not self.token: self.token = str(uuid.uuid4()).replace('-', '') # remove hyphens super(Request, self).save() @@ -1375,7 +1499,10 @@ class LdapUserGroup(ldapdb.models.Model): # attributes gid = ldapdb.models.fields.IntegerField(db_column='gidNumber') - members = ldapdb.models.fields.ListField(db_column='memberUid', blank=True) + members = ldapdb.models.fields.ListField( + db_column='memberUid', + blank=True + ) name = ldapdb.models.fields.CharField( db_column='cn', max_length=200, diff --git a/users/serializers.py b/users/serializers.py index 95388d41..be925881 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -20,19 +20,30 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -#Maël Kervella +# Maël Kervella + +"""users.serializers +Serializers for the User app +""" from rest_framework import serializers from users.models import Club, Adherent + class MailingSerializer(serializers.ModelSerializer): + """ Serializer to build Mailing objects """ + name = serializers.CharField(source='pseudo') class Meta: model = Club fields = ('name',) + class MailingMemberSerializer(serializers.ModelSerializer): + """ Serializer fot the Adherent objects (who belong to a + Mailing) """ + class Meta: model = Adherent fields = ('email',) diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index ba45c0bd..169524f5 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -82,8 +82,17 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %}
- + {% if users.is_class_club %} + + {% if users.club.mailing %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} diff --git a/users/tests.py b/users/tests.py index 21fa6d24..85a8e9f1 100644 --- a/users/tests.py +++ b/users/tests.py @@ -19,7 +19,10 @@ # 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. +"""users.tests +The tests for the Users module. +""" -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/users/urls.py b/users/urls.py index b8f428b0..05f72be0 100644 --- a/users/urls.py +++ b/users/urls.py @@ -34,109 +34,79 @@ urlpatterns = [ url(r'^new_user/$', views.new_user, name='new-user'), url(r'^new_club/$', views.new_club, name='new-club'), url(r'^edit_info/(?P[0-9]+)$', views.edit_info, name='edit-info'), - url( - r'^edit_club_admin_members/(?P[0-9]+)$', + url(r'^edit_club_admin_members/(?P[0-9]+)$', views.edit_club_admin_members, - name='edit-club-admin-members' - ), + name='edit-club-admin-members'), url(r'^state/(?P[0-9]+)$', views.state, name='state'), url(r'^groups/(?P[0-9]+)$', views.groups, name='groups'), url(r'^password/(?P[0-9]+)$', views.password, name='password'), - url(r'^del_group/(?P[0-9]+)/(?P[0-9]+)$', views.del_group, name='del-group'), + url(r'^del_group/(?P[0-9]+)/(?P[0-9]+)$', + views.del_group, + name='del-group'), url(r'^new_serviceuser/$', views.new_serviceuser, name='new-serviceuser'), - url( - r'^edit_serviceuser/(?P[0-9]+)$', + url(r'^edit_serviceuser/(?P[0-9]+)$', views.edit_serviceuser, - name='edit-serviceuser' - ), - url( - r'^del_serviceuser/(?P[0-9]+)$', + name='edit-serviceuser'), + url(r'^del_serviceuser/(?P[0-9]+)$', views.del_serviceuser, - name='del-serviceuser' - ), + name='del-serviceuser'), url(r'^add_ban/(?P[0-9]+)$', views.add_ban, name='add-ban'), url(r'^edit_ban/(?P[0-9]+)$', views.edit_ban, name='edit-ban'), - url( - r'^add_whitelist/(?P[0-9]+)$', + url(r'^add_whitelist/(?P[0-9]+)$', views.add_whitelist, - name='add-whitelist' - ), - url( - r'^edit_whitelist/(?P[0-9]+)$', + name='add-whitelist'), + url(r'^edit_whitelist/(?P[0-9]+)$', views.edit_whitelist, - name='edit-whitelist' - ), + name='edit-whitelist'), url(r'^add_school/$', views.add_school, name='add-school'), - url( - r'^edit_school/(?P[0-9]+)$', + url(r'^edit_school/(?P[0-9]+)$', views.edit_school, - name='edit-school' - ), + name='edit-school'), url(r'^del_school/$', views.del_school, name='del-school'), url(r'^add_listright/$', views.add_listright, name='add-listright'), - url( - r'^edit_listright/(?P[0-9]+)$', + url(r'^edit_listright/(?P[0-9]+)$', views.edit_listright, - name='edit-listright' - ), + name='edit-listright'), url(r'^del_listright/$', views.del_listright, name='del-listright'), url(r'^add_shell/$', views.add_shell, name='add-shell'), - url( - r'^edit_shell/(?P[0-9]+)$', + url(r'^edit_shell/(?P[0-9]+)$', views.edit_shell, - name='edit-shell' - ), - url( - r'^del_shell/(?P[0-9]+)$', + name='edit-shell'), + url(r'^del_shell/(?P[0-9]+)$', views.del_shell, - name='del-shell' - ), + name='del-shell'), url(r'^profil/(?P[0-9]+)$', views.profil, name='profil'), url(r'^index_ban/$', views.index_ban, name='index-ban'), url(r'^index_white/$', views.index_white, name='index-white'), url(r'^index_school/$', views.index_school, name='index-school'), url(r'^index_shell/$', views.index_shell, name='index-shell'), url(r'^index_listright/$', views.index_listright, name='index-listright'), - url( - r'^index_serviceusers/$', + url(r'^index_serviceusers/$', views.index_serviceusers, - name='index-serviceusers' - ), + name='index-serviceusers'), url(r'^mon_profil/$', views.mon_profil, name='mon-profil'), url(r'^process/(?P[a-z0-9]{32})/$', views.process, name='process'), url(r'^reset_password/$', views.reset_password, name='reset-password'), url(r'^mass_archive/$', views.mass_archive, name='mass-archive'), - url( - r'^history/(?P\w+)/(?P[0-9]+)$', + url(r'^history/(?P\w+)/(?P[0-9]+)$', re2o.views.history, name='history', - kwargs={'application':'users'}, - ), + kwargs={'application': 'users'}), url(r'^$', views.index, name='index'), url(r'^index_clubs/$', views.index_clubs, name='index-clubs'), - url( - r'^rest/ml/std/$', + url(r'^rest/ml/std/$', views.ml_std_list, - name='ml-std-list' - ), - url( - r'^rest/ml/std/member/(?P\w+)/$', + name='ml-std-list'), + url(r'^rest/ml/std/member/(?P\w+)/$', views.ml_std_members, - name='ml-std-members' - ), - url( - r'^rest/ml/club/$', + name='ml-std-members'), + url(r'^rest/ml/club/$', views.ml_club_list, - name='ml-club-list' - ), - url( - r'^rest/ml/club/admin/(?P\w+)/$', + name='ml-club-list'), + url(r'^rest/ml/club/admin/(?P\w+)/$', views.ml_club_admins, - name='ml-club-admins' - ), - url( - r'^rest/ml/club/member/(?P\w+)/$', + name='ml-club-admins'), + url(r'^rest/ml/club/member/(?P\w+)/$', views.ml_club_members, - name='ml-club-members' - ), + name='ml-club-members'), ] diff --git a/users/views.py b/users/views.py index b0cfb000..a86a6e47 100644 --- a/users/views.py +++ b/users/views.py @@ -39,22 +39,37 @@ from django.urls import reverse from django.shortcuts import get_object_or_404, render, redirect from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.db.models import ProtectedError, Q -from django.db import IntegrityError +from django.db.models import ProtectedError from django.utils import timezone from django.db import transaction from django.http import HttpResponse from django.http import HttpResponseRedirect from django.views.decorators.csrf import csrf_exempt - from rest_framework.renderers import JSONRenderer - - -from reversion.models import Version from reversion import revisions as reversion -from users.serializers import MailingSerializer, MailingMemberSerializer -from users.models import ( + +from cotisations.models import Facture +from machines.models import Machine +from preferences.models import OptionalUser, GeneralOption, AssoOption +from re2o.views import form +from re2o.utils import ( + all_has_access, + SortTable, + re2o_paginator +) +from re2o.acl import ( + can_create, + can_edit, + can_delete_set, + can_delete, + can_view, + can_view_all, + can_change +) + +from .serializers import MailingSerializer, MailingMemberSerializer +from .models import ( User, Ban, Whitelist, @@ -66,7 +81,7 @@ from users.models import ( Club, ListShell, ) -from users.forms import ( +from .forms import ( BanForm, WhitelistForm, DelSchoolForm, @@ -86,25 +101,7 @@ from users.forms import ( ClubAdminandMembersForm, GroupForm ) -from cotisations.models import Facture -from machines.models import Machine -from preferences.models import OptionalUser, GeneralOption, AssoOption -from re2o.views import form -from re2o.utils import ( - all_has_access, - SortTable, - re2o_paginator -) -from re2o.acl import ( - can_create, - can_edit, - can_delete_set, - can_delete, - can_view, - can_view_all, - can_change -) @can_create(Adherent) def new_user(request): @@ -121,9 +118,19 @@ def new_user(request): pour l'initialisation du mot de passe a été envoyé" % user.pseudo) return redirect(reverse( 'users:profil', - kwargs={'userid':str(user.id)} - )) - return form({'userform': user,'GTU_sum_up':GTU_sum_up,'GTU':GTU,'showCGU':True, 'action_name':'Créer un utilisateur'}, 'users/user.html', request) + kwargs={'userid': str(user.id)} + )) + return form( + { + 'userform': user, + 'GTU_sum_up': GTU_sum_up, + 'GTU': GTU, + 'showCGU': True, + 'action_name': 'Créer un utilisateur' + }, + 'users/user.html', + request + ) @login_required @@ -140,26 +147,41 @@ def new_club(request): pour l'initialisation du mot de passe a été envoyé" % club.pseudo) return redirect(reverse( 'users:profil', - kwargs={'userid':str(club.id)} - )) - return form({'userform': club, 'showCGU':False, 'action_name':'Créer un club'}, 'users/user.html', request) + kwargs={'userid': str(club.id)} + )) + return form( + {'userform': club, 'showCGU': False, 'action_name': 'Créer un club'}, + 'users/user.html', + request + ) @login_required @can_edit(Club) -def edit_club_admin_members(request, club_instance, clubid): +def edit_club_admin_members(request, club_instance, **_kwargs): """Vue d'edition de la liste des users administrateurs et membres d'un club""" - club = ClubAdminandMembersForm(request.POST or None, instance=club_instance) + club = ClubAdminandMembersForm( + request.POST or None, + instance=club_instance + ) if club.is_valid(): if club.changed_data: club.save() messages.success(request, "Le club a bien été modifié") return redirect(reverse( 'users:profil', - kwargs={'userid':str(club_instance.id)} - )) - return form({'userform': club, 'showCGU':False, 'action_name':'Editer les admin et membres'}, 'users/user.html', request) + kwargs={'userid': str(club_instance.id)} + )) + return form( + { + 'userform': club, + 'showCGU': False, + 'action_name': 'Editer les admin et membres' + }, + 'users/user.html', + request + ) @login_required @@ -169,26 +191,30 @@ def edit_info(request, user, userid): si l'id est différent de request.user, vérifie la possession du droit cableur """ if user.is_class_adherent: - user = AdherentForm( + user_form = AdherentForm( request.POST or None, instance=user.adherent, user=request.user ) - elif user.is_class_club: - user = ClubForm( + else: + user_form = ClubForm( request.POST or None, instance=user.club, user=request.user ) - if user.is_valid(): - if user.changed_data: - user.save() + if user_form.is_valid(): + if user_form.changed_data: + user_form.save() messages.success(request, "L'user a bien été modifié") return redirect(reverse( 'users:profil', - kwargs={'userid':str(userid)} - )) - return form({'userform': user, 'action_name': "Editer l'utilisateur"}, 'users/user.html', request) + kwargs={'userid': str(userid)} + )) + return form( + {'userform': user_form, 'action_name': "Editer l'utilisateur"}, + 'users/user.html', + request + ) @login_required @@ -196,35 +222,44 @@ def edit_info(request, user, userid): def state(request, user, userid): """ Changer l'etat actif/desactivé/archivé d'un user, need droit bureau """ - state = StateForm(request.POST or None, instance=user) - if state.is_valid(): - if state.changed_data: - if state.cleaned_data['state'] == User.STATE_ARCHIVE: + state_form = StateForm(request.POST or None, instance=user) + if state_form.is_valid(): + if state_form.changed_data: + if state_form.cleaned_data['state'] == User.STATE_ARCHIVE: user.archive() - elif state.cleaned_data['state'] == User.STATE_ACTIVE: + elif state_form.cleaned_data['state'] == User.STATE_ACTIVE: user.unarchive() - state.save() + state_form.save() messages.success(request, "Etat changé avec succès") return redirect(reverse( 'users:profil', - kwargs={'userid':str(userid)} - )) - return form({'userform': state, 'action_name': "Editer l'état"}, 'users/user.html', request) + kwargs={'userid': str(userid)} + )) + return form( + {'userform': state_form, 'action_name': "Editer l'état"}, + 'users/user.html', + request + ) @login_required @can_edit(User, 'groups') def groups(request, user, userid): - group = GroupForm(request.POST or None, instance=user) - if group.is_valid(): - if group.changed_data: - group.save() + """ View to edit the groups of a user """ + group_form = GroupForm(request.POST or None, instance=user) + if group_form.is_valid(): + if group_form.changed_data: + group_form.save() messages.success(request, "Groupes changés avec succès") return redirect(reverse( 'users:profil', - kwargs={'userid':str(userid)} + kwargs={'userid': str(userid)} )) - return form({'userform': group, 'action_name':'Editer les groupes'}, 'users/user.html', request) + return form( + {'userform': group_form, 'action_name': 'Editer les groupes'}, + 'users/user.html', + request + ) @login_required @@ -239,15 +274,20 @@ def password(request, user, userid): u_form.save() messages.success(request, "Le mot de passe a changé") return redirect(reverse( - 'users:profil', - kwargs={'userid':str(user.id)} + 'users:profil', + kwargs={'userid': str(userid)} )) - return form({'userform': u_form, 'action_name':'Changer le mot de passe'}, 'users/user.html', request) + return form( + {'userform': u_form, 'action_name': 'Changer le mot de passe'}, + 'users/user.html', + request + ) @login_required @can_edit(User, 'groups') -def del_group(request, user, userid, listrightid): +def del_group(request, user, listrightid, **_kwargs): + """ View used to delete a group """ user.groups.remove(ListRight.objects.get(id=listrightid)) user.save() messages.success(request, "Droit supprimé à %s" % user) @@ -268,14 +308,21 @@ def new_serviceuser(request): "L'utilisateur %s a été crée" % user_object.pseudo ) return redirect(reverse('users:index-serviceusers')) - return form({'userform': user, 'action_name':'Créer un serviceuser'}, 'users/user.html', request) + return form( + {'userform': user, 'action_name': 'Créer un serviceuser'}, + 'users/user.html', + request + ) @login_required @can_edit(ServiceUser) -def edit_serviceuser(request, serviceuser, serviceuserid): +def edit_serviceuser(request, serviceuser, **_kwargs): """ Edit a ServiceUser """ - serviceuser = EditServiceUserForm(request.POST or None, instance=serviceuser) + serviceuser = EditServiceUserForm( + request.POST or None, + instance=serviceuser + ) if serviceuser.is_valid(): user_object = serviceuser.save(commit=False) if serviceuser.cleaned_data['password']: @@ -284,12 +331,16 @@ def edit_serviceuser(request, serviceuser, serviceuserid): user_object.save() messages.success(request, "L'user a bien été modifié") return redirect(reverse('users:index-serviceusers')) - return form({'userform': serviceuser, 'action_name':'Editer un serviceuser'}, 'users/user.html', request) + return form( + {'userform': serviceuser, 'action_name': 'Editer un serviceuser'}, + 'users/user.html', + request + ) @login_required @can_delete(ServiceUser) -def del_serviceuser(request, serviceuser, serviceuserid): +def del_serviceuser(request, serviceuser, **_kwargs): """Suppression d'un ou plusieurs serviceusers""" if request.method == "POST": serviceuser.delete() @@ -312,22 +363,27 @@ def add_ban(request, user, userid): ban_instance = Ban(user=user) ban = BanForm(request.POST or None, instance=ban_instance) if ban.is_valid(): - _ban_object = ban.save() + ban.save() messages.success(request, "Bannissement ajouté") return redirect(reverse( 'users:profil', - kwargs={'userid':str(userid)} + kwargs={'userid': str(userid)} )) if user.is_ban(): messages.error( request, "Attention, cet utilisateur a deja un bannissement actif" ) - return form({'userform': ban, 'action_name': 'Ajouter un ban'}, 'users/user.html', request) + return form( + {'userform': ban, 'action_name': 'Ajouter un ban'}, + 'users/user.html', + request + ) + @login_required @can_edit(Ban) -def edit_ban(request, ban_instance, banid): +def edit_ban(request, ban_instance, **_kwargs): """ Editer un bannissement, nécessite au moins le droit bofh (a fortiori bureau) Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement""" @@ -337,7 +393,11 @@ def edit_ban(request, ban_instance, banid): ban.save() messages.success(request, "Bannissement modifié") return redirect(reverse('users:index')) - return form({'userform': ban, 'action_name': 'Editer un ban'}, 'users/user.html', request) + return form( + {'userform': ban, 'action_name': 'Editer un ban'}, + 'users/user.html', + request + ) @login_required @@ -358,19 +418,23 @@ def add_whitelist(request, user, userid): messages.success(request, "Accès à titre gracieux accordé") return redirect(reverse( 'users:profil', - kwargs={'userid':str(userid)} - )) + kwargs={'userid': str(userid)} + )) if user.is_whitelisted(): messages.error( request, "Attention, cet utilisateur a deja un accès gracieux actif" ) - return form({'userform': whitelist, 'action_name': 'Ajouter une whitelist'}, 'users/user.html', request) + return form( + {'userform': whitelist, 'action_name': 'Ajouter une whitelist'}, + 'users/user.html', + request + ) @login_required @can_edit(Whitelist) -def edit_whitelist(request, whitelist_instance, whitelistid): +def edit_whitelist(request, whitelist_instance, **_kwargs): """ Editer un accès gracieux, temporaire ou permanent. Need droit cableur Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, @@ -384,7 +448,11 @@ def edit_whitelist(request, whitelist_instance, whitelistid): whitelist.save() messages.success(request, "Whitelist modifiée") return redirect(reverse('users:index')) - return form({'userform': whitelist, 'action_name': 'Editer une whitelist'}, 'users/user.html', request) + return form( + {'userform': whitelist, 'action_name': 'Editer une whitelist'}, + 'users/user.html', + request + ) @login_required @@ -397,12 +465,16 @@ def add_school(request): school.save() messages.success(request, "L'établissement a été ajouté") return redirect(reverse('users:index-school')) - return form({'userform': school, 'action_name':'Ajouter'}, 'users/user.html', request) + return form( + {'userform': school, 'action_name': 'Ajouter'}, + 'users/user.html', + request + ) @login_required @can_edit(School) -def edit_school(request, school_instance, schoolid): +def edit_school(request, school_instance, **_kwargs): """ Editer un établissement d'enseignement à partir du schoolid dans la base de donnée, need cableur""" school = SchoolForm(request.POST or None, instance=school_instance) @@ -411,7 +483,11 @@ def edit_school(request, school_instance, schoolid): school.save() messages.success(request, "Établissement modifié") return redirect(reverse('users:index-school')) - return form({'userform': school, 'action_name':'Editer'}, 'users/user.html', request) + return form( + {'userform': school, 'action_name': 'Editer'}, + 'users/user.html', + request + ) @login_required @@ -434,7 +510,11 @@ def del_school(request, instances): "L'établissement %s est affecté à au moins un user, \ vous ne pouvez pas le supprimer" % school_del) return redirect(reverse('users:index-school')) - return form({'userform': school, 'action_name': 'Supprimer'}, 'users/user.html', request) + return form( + {'userform': school, 'action_name': 'Supprimer'}, + 'users/user.html', + request + ) @login_required @@ -446,12 +526,16 @@ def add_shell(request): shell.save() messages.success(request, "Le shell a été ajouté") return redirect(reverse('users:index-shell')) - return form({'userform': shell, 'action_name':'Ajouter'}, 'users/user.html', request) + return form( + {'userform': shell, 'action_name': 'Ajouter'}, + 'users/user.html', + request + ) @login_required @can_edit(ListShell) -def edit_shell(request, shell_instance, listshellid): +def edit_shell(request, shell_instance, **_kwargs): """ Editer un shell à partir du listshellid""" shell = ShellForm(request.POST or None, instance=shell_instance) if shell.is_valid(): @@ -459,12 +543,16 @@ def edit_shell(request, shell_instance, listshellid): shell.save() messages.success(request, "Le shell a été modifié") return redirect(reverse('users:index-shell')) - return form({'userform': shell, 'action_name':'Editer'}, 'users/user.html', request) + return form( + {'userform': shell, 'action_name': 'Editer'}, + 'users/user.html', + request + ) @login_required @can_delete(ListShell) -def del_shell(request, shell, listshellid): +def del_shell(request, shell, **_kwargs): """Destruction d'un shell""" if request.method == "POST": shell.delete() @@ -487,12 +575,16 @@ def add_listright(request): listright.save() messages.success(request, "Le droit/groupe a été ajouté") return redirect(reverse('users:index-listright')) - return form({'userform': listright, 'action_name': 'Ajouter'}, 'users/user.html', request) + return form( + {'userform': listright, 'action_name': 'Ajouter'}, + 'users/user.html', + request + ) @login_required @can_edit(ListRight) -def edit_listright(request, listright_instance, listrightid): +def edit_listright(request, listright_instance, **_kwargs): """ Editer un groupe/droit, necessite droit bureau, à partir du listright id """ listright = ListRightForm( @@ -504,7 +596,11 @@ def edit_listright(request, listright_instance, listrightid): listright.save() messages.success(request, "Droit modifié") return redirect(reverse('users:index-listright')) - return form({'userform': listright, 'action_name': 'Editer'}, 'users/user.html', request) + return form( + {'userform': listright, 'action_name': 'Editer'}, + 'users/user.html', + request + ) @login_required @@ -525,7 +621,11 @@ def del_listright(request, instances): "Le groupe %s est affecté à au moins un user, \ vous ne pouvez pas le supprimer" % listright_del) return redirect(reverse('users:index-listright')) - return form({'userform': listright, 'action_name': 'Supprimer'}, 'users/user.html', request) + return form( + {'userform': listright, 'action_name': 'Supprimer'}, + 'users/user.html', + request + ) @login_required @@ -587,7 +687,11 @@ def index_clubs(request): SortTable.USERS_INDEX ) clubs_list = re2o_paginator(request, clubs_list, pagination_number) - return render(request, 'users/index_clubs.html', {'clubs_list': clubs_list}) + return render( + request, + 'users/index_clubs.html', + {'clubs_list': clubs_list} + ) @login_required @@ -688,13 +792,13 @@ def mon_profil(request): """ Lien vers profil, renvoie request.id à la fonction """ return redirect(reverse( 'users:profil', - kwargs={'userid':str(request.user.id)} + kwargs={'userid': str(request.user.id)} )) @login_required @can_view(User) -def profil(request, users, userid): +def profil(request, users, **_kwargs): """ Affiche un profil, self or cableur, prend un userid en argument """ machines = Machine.objects.filter(user=users).select_related('user')\ .prefetch_related('interface_set__domain__extension')\ @@ -707,7 +811,9 @@ def profil(request, users, userid): request.GET.get('order'), SortTable.MACHINES_INDEX ) - pagination_large_number = GeneralOption.get_cached_value('pagination_large_number') + pagination_large_number = GeneralOption.get_cached_value( + 'pagination_large_number' + ) machines = re2o_paginator(request, machines, pagination_large_number) factures = Facture.objects.filter(user=users) factures = SortTable.sort( @@ -742,7 +848,7 @@ def profil(request, users, userid): 'ban_list': bans, 'white_list': whitelists, 'user_solde': user_solde, - 'allow_online_payment' : allow_online_payment, + 'allow_online_payment': allow_online_payment, } ) @@ -758,12 +864,20 @@ def reset_password(request): ) except User.DoesNotExist: messages.error(request, "Cet utilisateur n'existe pas") - return form({'userform': userform, 'action_name': 'Réinitialiser'}, 'users/user.html', request) + return form( + {'userform': userform, 'action_name': 'Réinitialiser'}, + 'users/user.html', + request + ) user.reset_passwd_mail(request) messages.success(request, "Un mail pour l'initialisation du mot\ de passe a été envoyé") redirect(reverse('index')) - return form({'userform': userform, 'action_name': 'Réinitialiser'}, 'users/user.html', request) + return form( + {'userform': userform, 'action_name': 'Réinitialiser'}, + 'users/user.html', + request + ) def process(request, token): @@ -790,7 +904,11 @@ def process_passwd(request, req): req.delete() messages.success(request, "Le mot de passe a changé") return redirect(reverse('index')) - return form({'userform': u_form, 'action_name': 'Changer le mot de passe'}, 'users/user.html', request) + return form( + {'userform': u_form, 'action_name': 'Changer le mot de passe'}, + 'users/user.html', + request + ) class JSONResponse(HttpResponse): @@ -804,7 +922,7 @@ class JSONResponse(HttpResponse): @csrf_exempt @login_required @permission_required('machines.serveur') -def ml_std_list(request): +def ml_std_list(_request): """ API view sending all the available standard mailings""" return JSONResponse([ {'name': 'adherents'} @@ -830,7 +948,7 @@ def ml_std_members(request, ml_name): @csrf_exempt @login_required @permission_required('machines.serveur') -def ml_club_list(request): +def ml_club_list(_request): """ API view sending all the available club mailings""" clubs = Club.objects.filter(mailing=True).values('pseudo') seria = MailingSerializer(clubs, many=True) @@ -862,6 +980,9 @@ def ml_club_members(request, ml_name): except Club.DoesNotExist: messages.error(request, "Cette mailing n'existe pas") return redirect(reverse('index')) - members = club.administrators.all().values('email').distinct() | club.members.all().values('email').distinct() + members = ( + club.administrators.all().values('email').distinct() | + club.members.all().values('email').distinct() + ) seria = MailingMemberSerializer(members, many=True) return JSONResponse(seria.data)
PrénomMailing{{ users.pseudo }}(-admin)Mailing désactivéePrénom {{ users.name }}Nom {{ users.surname }}