diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index aafa0a50..fcf55cfa 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -43,11 +43,6 @@ 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. os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings") @@ -59,6 +54,12 @@ os.chdir(proj_path) # This is so models get loaded. application = get_wsgi_application() +from machines.models import Interface, IpList, Nas, Domain +from topologie.models import 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 diff --git a/logs/templates/logs/aff_stats_droits.html b/logs/templates/logs/aff_stats_droits.html index bf672b67..6e424223 100644 --- a/logs/templates/logs/aff_stats_droits.html +++ b/logs/templates/logs/aff_stats_droits.html @@ -66,7 +66,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{{utilisateur.last}}

{% endif %} + {% if droit != 'Superuser' %} + {% else %} + + {% endif %} @@ -79,4 +83,4 @@ with this program; if not, write to the Free Software Foundation, Inc., -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/logs/views.py b/logs/views.py index afb0a118..1fdda9fb 100644 --- a/logs/views.py +++ b/logs/views.py @@ -41,7 +41,7 @@ from django.urls import reverse from django.shortcuts import render, redirect from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.db.models import Count, Max +from django.db.models import Count, Max, F from reversion.models import Revision from reversion.models import Version, ContentType @@ -195,9 +195,7 @@ def revert_action(request, revision_id): @login_required -@can_view_all(IpList) -@can_view_all(Interface) -@can_view_all(User) +@can_view_all(IpList, Interface, User) def stats_general(request): """Statistiques générales affinées sur les ip, activées, utilisées par range, et les statistiques générales sur les users : users actifs, @@ -313,10 +311,7 @@ def stats_general(request): @login_required -@can_view_app('users') -@can_view_app('cotisations') -@can_view_app('machines') -@can_view_app('topologie') +@can_view_app('users', 'cotisations', 'machines', 'topologie') def stats_models(request): """Statistiques générales, affiche les comptages par models: nombre d'users, d'écoles, de droits, de bannissements, @@ -469,9 +464,14 @@ def stats_droits(request): 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') + last=Max('revision__date_created'), ) + stats_list['Superuser'] = User.objects.filter(is_superuser=True).annotate( + num=Count('revision'), + last=Max('revision__date_created'), + ) + return render( request, 'logs/stats_droits.html', diff --git a/machines/migrations/0080_auto_20180502_2334.py b/machines/migrations/0080_auto_20180502_2334.py new file mode 100644 index 00000000..7097dde2 --- /dev/null +++ b/machines/migrations/0080_auto_20180502_2334.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-05-03 04:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0079_auto_20180416_0107'), + ] + + operations = [ + migrations.AlterField( + model_name='ns', + name='ns', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='machines.Domain'), + ), + ] diff --git a/machines/models.py b/machines/models.py index 9ed973f2..9eb6323a 100644 --- a/machines/models.py +++ b/machines/models.py @@ -613,7 +613,7 @@ class Ns(RevMixin, AclMixin, models.Model): PRETTY_NAME = "Enregistrements NS" zone = models.ForeignKey('Extension', on_delete=models.PROTECT) - ns = models.OneToOneField('Domain', on_delete=models.PROTECT) + ns = models.ForeignKey('Domain', on_delete=models.PROTECT) class Meta: permissions = ( diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html index f6da4165..56eec21d 100644 --- a/machines/templates/machines/aff_machines.html +++ b/machines/templates/machines/aff_machines.html @@ -28,7 +28,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% include "pagination.html" with list=machines_list %} {% endif %} - diff --git a/machines/views.py b/machines/views.py index c3033049..75c2f483 100644 --- a/machines/views.py +++ b/machines/views.py @@ -1272,12 +1272,7 @@ def index_nas(request): @login_required -@can_view_all(SOA) -@can_view_all(Mx) -@can_view_all(Ns) -@can_view_all(Txt) -@can_view_all(Srv) -@can_view_all(Extension) +@can_view_all(SOA, Mx, Ns, Txt, Srv, Extension) def index_extension(request): """ View displaying the list of existing extensions, the list of existing SOA records, the list of existing MX records , the list of diff --git a/preferences/aes_field.py b/preferences/aes_field.py index 1d3ffa54..302aa82b 100644 --- a/preferences/aes_field.py +++ b/preferences/aes_field.py @@ -1,3 +1,4 @@ +# 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. diff --git a/preferences/views.py b/preferences/views.py index b2a6ba4c..b8ca39d2 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -58,13 +58,8 @@ from . import forms @login_required -@can_view_all(OptionalUser) -@can_view_all(OptionalMachine) -@can_view_all(OptionalTopologie) -@can_view_all(GeneralOption) -@can_view_all(AssoOption) -@can_view_all(MailMessageOption) -@can_view_all(HomeOption) +@can_view_all(OptionalUser, OptionalMachine, OptionalTopologie, GeneralOption, + AssoOption, MailMessageOption, HomeOption) def display_options(request): """Vue pour affichage des options (en vrac) classé selon les models correspondants dans un tableau""" @@ -149,7 +144,11 @@ def add_service(request): @can_edit(Service) def edit_service(request, service_instance, **_kwargs): """Edition des services affichés sur la page d'accueil""" - service = ServiceForm(request.POST or None, request.FILES 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() diff --git a/re2o/acl.py b/re2o/acl.py index 6ab34350..d96a281a 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -28,135 +28,177 @@ Here are defined some decorators that can be used in views to handle ACL. from __future__ import unicode_literals import sys +from itertools import chain +from django.db.models import Model from django.contrib import messages from django.shortcuts import redirect from django.urls import reverse -def can_create(model): - """Decorator to check if an user can create a model. - It assumes that a valid user exists in the request and that the model has a - method can_create(user) which returns true if the user can create this kind - of models. +def acl_base_decorator(method_name, *targets, on_instance=True): + """Base decorator for acl. It checks if the `request.user` has the + permission by calling model.method_name. If the flag on_instance is True, + tries to get an instance of the model by calling + `model.get_instance(*args, **kwargs)` and runs `instance.mehod_name` + rather than model.method_name. + + It is not intended to be used as is. It is a base for others ACL + decorators. + + Args: + method_name: The name of the method which is to to be used for ACL. + (ex: 'can_edit') WARNING: if no method called 'method_name' exists, + then no error will be triggered, the decorator will act as if + permission was granted. This is to allow you to run ACL tests on + fields only. If the method exists, it has to return a 2-tuple + `(can, reason)` with `can` being a boolean stating whether the + access is granted and `reason` a message to be displayed if `can` + equals `False` (can be `None`) + *targets: The targets. Targets are specified like a sequence of models + and fields names. As an example + ``` + acl_base_decorator('can_edit', ModelA, 'field1', 'field2', \ +ModelB, ModelC, 'field3', on_instance=False) + ``` + will make the following calls (where `user` is the current user, + `*args` and `**kwargs` are the arguments initially passed to the + view): + - `ModelA.can_edit(user, *args, **kwargs)` + - `ModelA.can_change_field1(user, *args, **kwargs)` + - `ModelA.can_change_field2(user, *args, **kwargs)` + - `ModelB.can_edit(user, *args, **kwargs)` + - `ModelC.can_edit(user, *args, **kwargs)` + - `ModelC.can_change_field3(user, *args, **kwargs)` + + Note that + ``` + acl_base_decorator('can_edit', 'field1', ModelA, 'field2', \ +on_instance=False) + ``` + would have the same effect that + ``` + acl_base_decorator('can_edit', ModelA, 'field1', 'field2', \ +on_instance=False) + ``` + But don't do that, it's silly. + on_instance: When `on_instance` equals `False`, the decorator runs the + ACL method on the model class rather than on an instance. If an + instance need to fetched, it is done calling the assumed existing + method `get_instance` of the model, with the arguments originally + passed to the view. + + Returns: + The user is either redirected to their own page with an explanation + message if at least one access is not granted, or to the view. In order + to avoid duplicate DB calls, when the `on_instance` flag equals `True`, + the instances are passed to the view. Example, with this decorator: + ``` + acl_base_decorator('can_edit', ModelA, 'field1', 'field2', ModelB,\ +ModelC) + ``` + The view will be called like this: + ``` + view(request, instance_of_A, instance_of_b, *args, **kwargs) + ``` + where `*args` and `**kwargs` are the original view arguments. """ + + def group_targets(): + """This generator parses the targets of the decorator, yielding + 2-tuples of (model, [fields]). + """ + current_target = None + current_fields = [] + for target in targets: + if not isinstance(target, str): + if current_target: + yield (current_target, current_fields) + current_target = target + current_fields = [] + else: + current_fields.append(target) + yield (current_target, current_fields) + 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( - request, msg or "Vous ne pouvez pas accéder à ce menu") - return redirect(reverse('index')) - return view(request, *args, **kwargs) - return wrapper - return decorator + """The wrapper used for a specific request""" + instances = [] + def process_target(target, fields): + """This function calls the methods on the target and checks for + the can_change_`field` method with the given fields. It also + stores the instances of models in order to avoid duplicate DB + calls for the view. + """ + if on_instance: + try: + target = target.get_instance(*args, **kwargs) + instances.append(target) + except target.DoesNotExist: + yield False, u"Entrée inexistante" + return + if hasattr(target, method_name): + can_fct = getattr(target, method_name) + yield can_fct(request.user, *args, **kwargs) + for field in fields: + can_change_fct = getattr(target, 'can_change_' + field) + yield can_change_fct(request.user, *args, **kwargs) -def can_edit(model, *field_list): - """Decorator to check if an user can edit a model. - It tries to get an instance of the model, using - `model.get_instance(*args, **kwargs)` and assumes that the model has a - method `can_edit(user)` which returns `true` if the user can edit this - 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)} - )) - 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)} - )) - for field in field_list: - can_change_fct = getattr(instance, 'can_change_' + field) - can, msg = can_change_fct(request.user, *args, **kwargs) - if not can: + error_messages = [ + x[1] for x in chain.from_iterable( + process_target(x[0], x[1]) for x in group_targets() + ) if not x[0] + ] + if error_messages: + for msg in error_messages: 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 view(request, instance, *args, **kwargs) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) + return view(request, *chain(instances, args), **kwargs) return wrapper return decorator -def can_change(model, *field_list): +def can_create(*models): + """Decorator to check if an user can create the given models. It runs + `acl_base_decorator` with the flag `on_instance=False` and the method + 'can_create'. See `acl_base_decorator` documentation for further details. + """ + return acl_base_decorator('can_create', *models, on_instance=False) + + +def can_edit(*targets): + """Decorator to check if an user can edit the models. + It runs `acl_base_decorator` with the flag `on_instance=True` and the + method 'can_edit'. See `acl_base_decorator` documentation for further + details. + """ + return acl_base_decorator('can_edit', *targets) + + +def can_change(*targets): """Decorator to check if an user can edit a field of a model class. - Difference with can_edit : take a class and not an instance + Difference with can_edit : takes a class and not an instance + It runs `acl_base_decorator` with the flag `on_instance=False` and the + method 'can_change'. See `acl_base_decorator` documentation for further + details. """ - 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_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 view(request, *args, **kwargs) - return wrapper - return decorator + return acl_base_decorator('can_change', *targets) -def can_delete(model): +def can_delete(*targets): """Decorator to check if an user can delete a model. - It tries to get an instance of the model, using - `model.get_instance(*args, **kwargs)` and assumes that the model has a - method `can_delete(user)` which returns `true` if the user can delete this - kind of models. + It runs `acl_base_decorator` with the flag `on_instance=True` and the + method 'can_edit'. See `acl_base_decorator` documentation for further + details. """ - 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)} - )) - 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 view(request, instance, *args, **kwargs) - return wrapper - return decorator + return acl_base_decorator('can_delete', *targets) def can_delete_set(model): @@ -187,84 +229,34 @@ def can_delete_set(model): return decorator -def can_view(model): +def can_view(*targets): """Decorator to check if an user can view a model. - It tries to get an instance of the model, using - `model.get_instance(*args, **kwargs)` and assumes that the model has a - method `can_view(user)` which returns `true` if the user can view this - kind of models. + It runs `acl_base_decorator` with the flag `on_instance=True` and the + method 'can_view'. See `acl_base_decorator` documentation for further + details. """ - 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)} - )) - 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 view(request, instance, *args, **kwargs) - return wrapper - return decorator + return acl_base_decorator('can_view', *targets) -def can_view_all(model): +def can_view_all(*targets): """Decorator to check if an user can view a class of model. + It runs `acl_base_decorator` with the flag `on_instance=False` and the + method 'can_view_all'. See `acl_base_decorator` documentation for further + details. """ - 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 view(request, *args, **kwargs) - return wrapper - return decorator + return acl_base_decorator('can_view_all', *targets, on_instance=False) -def can_view_app(app_name): - """Decorator to check if an user can view an application. +def can_view_app(*apps_name): + """Decorator to check if an user can view the applications. """ - 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 wrapper - return decorator + for app_name in apps_name: + assert app_name in sys.modules.keys() + return acl_base_decorator( + 'can_view', + *chain(sys.modules[app_name] for app_name in apps_name), + on_instance=False + ) def can_edit_history(view): diff --git a/re2o/locale/fr/LC_MESSAGES/django.mo b/re2o/locale/fr/LC_MESSAGES/django.mo index eb533f3e..b05137c3 100644 Binary files a/re2o/locale/fr/LC_MESSAGES/django.mo and b/re2o/locale/fr/LC_MESSAGES/django.mo differ diff --git a/re2o/locale/fr/LC_MESSAGES/django.po b/re2o/locale/fr/LC_MESSAGES/django.po index 2cabc6ec..1ece2026 100644 --- a/re2o/locale/fr/LC_MESSAGES/django.po +++ b/re2o/locale/fr/LC_MESSAGES/django.po @@ -30,6 +30,14 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +#: settings.py:140 +msgid "English" +msgstr "Anglais" + +#: settings.py:141 +msgid "French" +msgstr "Français" + #: templates/re2o/about.html:29 templates/re2o/about.html:35 msgid "About Re2o" msgstr "A propos de Re2o" @@ -147,6 +155,10 @@ msgstr "" msgid "Dependencies" msgstr "Dépendances" +#: templates/re2o/buttons/setlang.html:34 +msgid "Translation in development" +msgstr "Traduction en développement" + #: views.py:172 msgid "No Git repository configured" msgstr "Aucun repository git configuré" diff --git a/re2o/settings.py b/re2o/settings.py index bac8982b..52606b6d 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -37,6 +37,7 @@ from __future__ import unicode_literals import os from .settings_local import * +from django.utils.translation import ugettext_lazy as _ # The root directory for the project # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -135,6 +136,10 @@ LOCALE_PATHS = [ # For translations outside of apps os.path.join(BASE_DIR, 'templates', 'locale').replace('\\', '/') ] +LANGUAGES = [ + ('en', _('English')), + ('fr', _('French')) +] # Should use time zone ? USE_TZ = True diff --git a/re2o/urls.py b/re2o/urls.py index b1cccfd9..47172521 100644 --- a/re2o/urls.py +++ b/re2o/urls.py @@ -55,6 +55,7 @@ urlpatterns = [ url(r'^about/$', about_page, name='about'), url('^logout/', auth_views.logout, {'next_page': '/'}), url('^', include('django.contrib.auth.urls')), + url(r'^i18n/', include('django.conf.urls.i18n')), url(r'^admin/', include(admin.site.urls)), url(r'^users/', include('users.urls', namespace='users')), url(r'^search/', include('search.urls', namespace='search')), diff --git a/re2o/utils.py b/re2o/utils.py index 5a4f9400..75304369 100644 --- a/re2o/utils.py +++ b/re2o/utils.py @@ -44,6 +44,49 @@ from cotisations.models import Cotisation, Facture, Vente from machines.models import Interface, Machine from users.models import Adherent, User, Ban, Whitelist +# Mapping of srtftime format for better understanding +# https://docs.python.org/3.6/library/datetime.html#strftime-strptime-behavior +datetime_mapping={ + '%a': '%a', + '%A': '%A', + '%w': '%w', + '%d': 'dd', + '%b': '%b', + '%B': '%B', + '%m': 'mm', + '%y': 'yy', + '%Y': 'yyyy', + '%H': 'HH', + '%I': 'HH(12h)', + '%p': 'AMPM', + '%M': 'MM', + '%S': 'SS', + '%f': 'µµ', + '%z': 'UTC(+/-HHMM)', + '%Z': 'UTC(TZ)', + '%j': '%j', + '%U': 'ww', + '%W': 'ww', + '%c': '%c', + '%x': '%x', + '%X': '%X', + '%%': '%%', +} + + +def convert_datetime_format(format): + i=0 + new_format = "" + while i < len(format): + if format[i] == '%': + char = format[i:i+2] + new_format += datetime_mapping.get(char, char) + i += 2 + else: + new_format += format[i] + i += 1 + return new_format + def all_adherent(search_time=None): """ Fonction renvoyant tous les users adherents. Optimisee pour n'est @@ -318,3 +361,19 @@ def remove_user_room(room): return user.room = None user.save() + + +def get_input_formats_help_text(input_formats): + """Returns a help text about the possible input formats""" + if len(input_formats) > 1: + help_text_template="Format: {main} {more}" + else: + help_text_template="Format: {main}" + more_text_template="" + help_text = help_text_template.format( + main=convert_datetime_format(input_formats[0]), + more=more_text_template.format( + '\n'.join(map(convert_datetime_format, input_formats)) + ) + ) + return help_text diff --git a/search/forms.py b/search/forms.py index b0668ef9..8cdb1cc1 100644 --- a/search/forms.py +++ b/search/forms.py @@ -26,6 +26,7 @@ from __future__ import unicode_literals from django import forms from django.forms import Form +from re2o.utils import get_input_formats_help_text CHOICES_USER = ( ('0', 'Actifs'), @@ -91,12 +92,17 @@ class SearchFormPlus(Form): s = forms.DateField( required=False, label="Date de début", - help_text='DD/MM/YYYY', - input_formats=['%d/%m/%Y'] ) e = forms.DateField( required=False, - help_text='DD/MM/YYYY', - input_formats=['%d/%m/%Y'], label="Date de fin" ) + + def __init__(self, *args, **kwargs): + super(SearchFormPlus, self).__init__(*args, **kwargs) + self.fields['s'].help_text = get_input_formats_help_text( + self.fields['s'].input_formats + ) + self.fields['e'].help_text = get_input_formats_help_text( + self.fields['e'].input_formats + ) diff --git a/static/css/base.css b/static/css/base.css index 7338fab6..b6a7ae26 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -35,6 +35,26 @@ footer a { border-radius: 0; } +/* Add right colors for buttons in dropdown in navbar-inverse (else it is light + * gray on white bg and white when hovered */ +.navbar-inverse .dropdown-menu .btn-link { + text-decoration: none; + color: #262626; + padding: 0; +} +.navbar-inverse .dropdown-menu .btn-link:hover { + background-color: #f5f5f5; +} +@media screen and (max-width: 767px) { + .navbar-inverse .dropdown-menu .btn-link { + color: #9d9d9d; + } + .navbar-inverse .dropdown-menu .btn-link:hover { + color: #fff; + background-color: transparent; + } +} + /* Set height of the grid so .sidenav can be 100% (adjust as needed) */ .row.content { height: 100%; diff --git a/templates/base.html b/templates/base.html index 297cac89..f198feec 100644 --- a/templates/base.html +++ b/templates/base.html @@ -118,7 +118,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_end %}
  • - {% trans "About" %} + {% trans "About" %}
  • {% if not request.user.is_authenticated %} {% if var_sa %} @@ -145,48 +145,11 @@ with this program; if not, write to the Free Software Foundation, Inc., - {% endif %} - - - {% comment %} - - - {% endcomment %} diff --git a/templates/buttons/setlang.html b/templates/buttons/setlang.html new file mode 100644 index 00000000..3d435ef3 --- /dev/null +++ b/templates/buttons/setlang.html @@ -0,0 +1,50 @@ +{% comment %} +Re2o est un logiciel d'administration développé initiallement au rezometz. Il +se veut agnostique au réseau considéré, de manière à être installable en +quelques clics. + +Copyright © 2018 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. +{% endcomment %} + +{% load i18n %} + +{% get_current_language as LANGUAGE_CODE %} +{% get_available_languages as LANGUAGES %} +{% get_language_info_list for LANGUAGES as languages %} + + + + + diff --git a/templates/pagination.html b/templates/pagination.html index a3a51602..d0bb3801 100644 --- a/templates/pagination.html +++ b/templates/pagination.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load url_insert_param %} +{% if list.paginator.num_pages > 1 %} - + +{% endif %} diff --git a/topologie/templates/topologie/aff_port.html b/topologie/templates/topologie/aff_port.html index 837c2b53..deeb0655 100644 --- a/topologie/templates/topologie/aff_port.html +++ b/topologie/templates/topologie/aff_port.html @@ -24,69 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} - -
    -
    - - - - {% for port in port_list|slice:"::2" %} - - {% endfor %} - - - - - {% for port in port_list|slice:"::2" %} - {% if port.room %} - - {% endfor %} - - - - - - {% for port in port_list|slice:"1::2" %} - - {% endfor %} - - - - - {% for port in port_list|slice:"1::2" %} - {% if port.room %} - - {% endfor %} - - -
    {{ port.port }}
    - {{ port.room }} - {% elif port.machine_interface %} - - {{ port.machine_interface }} - {% elif port.related%} - - {{ port.related }} - {% else %} - - Vide - {% endif %} -
    {{ port.port }}
    - {{ port.room }} - {% elif port.machine_interface %} - - {{ port.machine_interface }} - {% elif port.related%} - - {{ port.related }} - {% else %} - - Vide - {% endif %} -
    - - -
    @@ -105,37 +42,47 @@ with this program; if not, write to the Free Software Foundation, Inc., - + {% endfor %} diff --git a/topologie/templates/topologie/aff_repr_switch.html b/topologie/templates/topologie/aff_repr_switch.html new file mode 100644 index 00000000..236dee56 --- /dev/null +++ b/topologie/templates/topologie/aff_repr_switch.html @@ -0,0 +1,116 @@ +{% comment %} +Re2o est un logiciel d'administration développé initiallement au rezometz. Il +se veut agnostique au réseau considéré, de manière à être installable en +quelques clics. + +Copyright © 2017 Gabriel Détraz +Copyright © 2017 Goulven Kermarec +Copyright © 2017 Augustin Lemesle + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +{% endcomment %} + +{% load acl %} + +
    +
    {{ port.port }} - {% if port.room %} - {{ port.room }} - {% endif %} + {% if port.room %}{{ port.room }}{% endif %} - {% if port.machine_interface %} - {{ port.machine_interface }} - {% endif %} + {% if port.machine_interface %} + {% can_view port.machine_interface.machine.user %} + + {{ port.machine_interface }} + + {% acl_else %} + {{ port.machine_interface }} + {% acl_end %} + {% endif %} - {% if port.related %} - {{ port.related }} - {% endif %} + {% if port.related %} + {% can_view port.related.switch %} + + {{ port.related }} + + {% acl_else %} + {{ port.related }} + {% acl_end %} + {% endif %} {{ port.radius }}{% if not port.vlan_force %} Aucun{%else %}{{ port.vlan_force }}{% endif %}{% if not port.vlan_force %}Aucun{% else %}{{ port.vlan_force }}{% endif %} {{ port.details }} - {% can_edit port %} + {% can_edit port %} - {% acl_end %} - {% can_delete port %} + {% acl_end %} + {% can_delete port %} - {% acl_end %} + {% acl_end %}
    + + + + {% for port in port_list|slice:"::2" %} + + {% endfor %} + + + + + {% for port in port_list|slice:"::2" %} + {% if port.room %} + + {% elif port.machine_interface %} + + {% elif port.related%} + + {% else %} + + {% endif %} + {% endfor %} + + + + + + {% for port in port_list|slice:"1::2" %} + + {% endfor %} + + + + + {% for port in port_list|slice:"1::2" %} + {% if port.room %} + + {% elif port.machine_interface %} + + {% elif port.related%} + + {% else %} + + {% endif %} + {% endfor %} + + +
    {{ port.port }}
    + {{ port.room }} + + {% can_view port.machine_interface.machine.user %} + + {{ port.machine_interface }} + + {% acl_else %} + {{ port.machine_interface }} + {% acl_end %} + + {% can_view port.related.switch %} + + {{ port.related }} + + {% acl_else %} + {{ port.related }} + {% acl_end %} + + Vide +
    {{ port.port }}
    + {{ port.room }} + + {% can_view port.machine_interface.machine.user %} + + {{ port.machine_interface }} + + {% acl_else %} + {{ port.machine_interface }} + {% acl_end %} + + {% can_view port.related.switch %} + + {{ port.related }} + + {% acl_else %} + {{ port.related }} + {% acl_end %} + + Vide +
    +
    diff --git a/topologie/templates/topologie/index_p.html b/topologie/templates/topologie/index_p.html index 0bd62039..138cc62c 100644 --- a/topologie/templates/topologie/index_p.html +++ b/topologie/templates/topologie/index_p.html @@ -36,6 +36,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Ajouter des ports {% acl_end %}
    +{% include "topologie/aff_repr_switch.html" with port_list=port_list %} {% include "topologie/aff_port.html" with port_list=port_list %}

    diff --git a/topologie/views.py b/topologie/views.py index 5ca5eb7a..992e1df1 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -208,9 +208,7 @@ def index_ap(request): @login_required -@can_view_all(Stack) -@can_view_all(Building) -@can_view_all(SwitchBay) +@can_view_all(Stack, Building, SwitchBay) def index_physical_grouping(request): """Affichage de la liste des stacks (affiche l'ensemble des switches)""" stack_list = (Stack.objects @@ -249,8 +247,7 @@ def index_physical_grouping(request): @login_required -@can_view_all(ModelSwitch) -@can_view_all(ConstructorSwitch) +@can_view_all(ModelSwitch, ConstructorSwitch) def index_model_switch(request): """ Affichage de l'ensemble des modèles de switches""" model_switch_list = ModelSwitch.objects.select_related('constructor') diff --git a/users/forms.py b/users/forms.py index 0a17df8b..b9d2b826 100644 --- a/users/forms.py +++ b/users/forms.py @@ -41,7 +41,7 @@ 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.utils import remove_user_room, get_input_formats_help_text from re2o.mixins import FormRevMixin from re2o.field_permissions import FieldPermissionFormMixin @@ -422,12 +422,19 @@ class ServiceUserForm(FormRevMixin, ModelForm): class Meta: model = ServiceUser - fields = ('pseudo', 'access_group') + fields = ('pseudo', 'access_group','comment') def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs) + def save(self, commit=True): + """Changement du mot de passe""" + user = super(ServiceUserForm, self).save(commit=False) + if self.cleaned_data['password']: + user.set_password(self.cleaned_data.get("password")) + user.save() + class EditServiceUserForm(ServiceUserForm): """Formulaire d'edition de base d'un service user. Ne permet @@ -447,7 +454,7 @@ class StateForm(FormRevMixin, ModelForm): super(StateForm, self).__init__(*args, prefix=prefix, **kwargs) -class GroupForm(FormRevMixin, ModelForm): +class GroupForm(FieldPermissionFormMixin, FormRevMixin, ModelForm): """ Gestion des groupes d'un user""" groups = forms.ModelMultipleChoiceField( Group.objects.all(), @@ -457,11 +464,13 @@ class GroupForm(FormRevMixin, ModelForm): class Meta: model = User - fields = ['groups'] + fields = ['is_superuser', 'groups'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(GroupForm, self).__init__(*args, prefix=prefix, **kwargs) + if 'is_superuser' in self.fields: + self.fields['is_superuser'].label = "Superuser" class SchoolForm(FormRevMixin, ModelForm): @@ -558,6 +567,9 @@ class BanForm(FormRevMixin, ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(BanForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['date_end'].label = 'Date de fin' + self.fields['date_end'].help_text = get_input_formats_help_text( + self.fields['date_end'].input_formats + ) class Meta: model = Ban @@ -570,6 +582,9 @@ class WhitelistForm(FormRevMixin, ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(WhitelistForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['date_end'].label = 'Date de fin' + self.fields['date_end'].help_text = get_input_formats_help_text( + self.fields['date_end'].input_formats + ) class Meta: model = Whitelist diff --git a/users/migrations/0072_auto_20180426_2021.py b/users/migrations/0072_auto_20180426_2021.py new file mode 100644 index 00000000..f60103e0 --- /dev/null +++ b/users/migrations/0072_auto_20180426_2021.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-04-26 18:21 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0071_auto_20180415_1252'), + ] + + operations = [ + migrations.AlterField( + model_name='ban', + name='date_end', + field=models.DateTimeField(), + ), + migrations.AlterField( + model_name='whitelist', + name='date_end', + field=models.DateTimeField(), + ), + ] diff --git a/users/models.py b/users/models.py index f7bfc128..1e27e0f2 100644 --- a/users/models.py +++ b/users/models.py @@ -812,6 +812,18 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, "Droit requis pour éditer les groupes de l'user" ) + @staticmethod + def can_change_is_superuser(user_request, *_args, **_kwargs): + """ Check if an user can change a is_superuser flag + + :param user_request: The user who request + :returns: a message and a boolean which is True if permission is granted. + """ + return ( + user_request.is_superuser, + "Droit superuser requis pour éditer le flag superuser" + ) + def can_view(self, user_request, *_args, **_kwargs): """Check if an user can view an user object. @@ -1229,7 +1241,7 @@ class Ban(RevMixin, AclMixin, models.Model): user = models.ForeignKey('User', on_delete=models.PROTECT) raison = models.CharField(max_length=255) date_start = models.DateTimeField(auto_now_add=True) - date_end = models.DateTimeField(help_text='%d/%m/%y %H:%M:%S') + date_end = models.DateTimeField() state = models.IntegerField(choices=STATES, default=STATE_HARD) class Meta: @@ -1314,7 +1326,7 @@ class Whitelist(RevMixin, AclMixin, models.Model): user = models.ForeignKey('User', on_delete=models.PROTECT) raison = models.CharField(max_length=255) date_start = models.DateTimeField(auto_now_add=True) - date_end = models.DateTimeField(help_text='%d/%m/%y %H:%M:%S') + date_end = models.DateTimeField() class Meta: permissions = ( diff --git a/users/templates/users/aff_listright.html b/users/templates/users/aff_listright.html index be94b146..8906b38e 100644 --- a/users/templates/users/aff_listright.html +++ b/users/templates/users/aff_listright.html @@ -33,6 +33,44 @@ with this program; if not, write to the Free Software Foundation, Inc., + {% if superuser_right %} + + Superuser + + True + + + + + Donne tous les droits sur Re2o. + + + + + + +
    +
    +
    +
      + {% for user in superuser_right %} +
    • + {{user}} + + + +
    • + {% endfor %} +
    +
    +
    +
    + + + + {% endif %} {% for listright in listright_list %} @@ -48,9 +86,9 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - {{ listright.details }} + + + {{ listright.details }} {% include 'buttons/edit.html' with href='users:edit-listright' id=listright.id %} {% include 'buttons/history.html' with href='users:history' name='listright' id=listright.id %} diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index 169524f5..abba61a2 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -245,7 +245,7 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %}

    Machines - {{machines_list.count}} + {{nb_machines}}

    diff --git a/users/urls.py b/users/urls.py index 05f72be0..5d868196 100644 --- a/users/urls.py +++ b/users/urls.py @@ -43,6 +43,9 @@ urlpatterns = [ url(r'^del_group/(?P[0-9]+)/(?P[0-9]+)$', views.del_group, name='del-group'), + url(r'^del_superuser/(?P[0-9]+)$', + views.del_superuser, + name='del-superuser'), url(r'^new_serviceuser/$', views.new_serviceuser, name='new-serviceuser'), url(r'^edit_serviceuser/(?P[0-9]+)$', views.edit_serviceuser, diff --git a/users/views.py b/users/views.py index a86a6e47..34a08313 100644 --- a/users/views.py +++ b/users/views.py @@ -246,7 +246,7 @@ def state(request, user, userid): @can_edit(User, 'groups') def groups(request, user, userid): """ View to edit the groups of a user """ - group_form = GroupForm(request.POST or None, instance=user) + group_form = GroupForm(request.POST or None, instance=user, user=request.user) if group_form.is_valid(): if group_form.changed_data: group_form.save() @@ -294,18 +294,26 @@ def del_group(request, user, listrightid, **_kwargs): return HttpResponseRedirect(request.META.get('HTTP_REFERER')) +@login_required +@can_edit(User, 'is_superuser') +def del_superuser(request, user, **_kwargs): + """Remove the superuser right of an user.""" + user.is_superuser = False + user.save() + messages.success(request, "%s n'est plus superuser" % user) + return HttpResponseRedirect(request.META.get('HTTP_REFERER')) + + @login_required @can_create(ServiceUser) def new_serviceuser(request): """ Vue de création d'un nouvel utilisateur service""" user = ServiceUserForm(request.POST or None) if user.is_valid(): - user_object = user.save(commit=False) - user_object.set_password(user.cleaned_data['password']) - user_object.save() + user.save() messages.success( request, - "L'utilisateur %s a été crée" % user_object.pseudo + "L'utilisateur a été crée" ) return redirect(reverse('users:index-serviceusers')) return form( @@ -324,11 +332,8 @@ def edit_serviceuser(request, serviceuser, **_kwargs): instance=serviceuser ) if serviceuser.is_valid(): - user_object = serviceuser.save(commit=False) - if serviceuser.cleaned_data['password']: - user_object.set_password(serviceuser.cleaned_data['password']) if serviceuser.changed_data: - user_object.save() + serviceuser.save() messages.success(request, "L'user a bien été modifié") return redirect(reverse('users:index-serviceusers')) return form( @@ -344,7 +349,7 @@ def del_serviceuser(request, serviceuser, **_kwargs): """Suppression d'un ou plusieurs serviceusers""" if request.method == "POST": serviceuser.delete() - messages.success(request, "L'user a été détruite") + messages.success(request, "L'user a été détruit") return redirect(reverse('users:index-serviceusers')) return form( {'objet': serviceuser, 'objet_name': 'serviceuser'}, @@ -768,10 +773,14 @@ def index_listright(request): """ Affiche l'ensemble des droits""" listright_list = ListRight.objects.order_by('unix_name')\ .prefetch_related('permissions').prefetch_related('user_set') + superuser_right = User.objects.filter(is_superuser=True) return render( request, 'users/index_listright.html', - {'listright_list': listright_list} + { + 'listright_list': listright_list, + 'superuser_right' : superuser_right, + } ) @@ -814,6 +823,7 @@ def profil(request, users, **_kwargs): pagination_large_number = GeneralOption.get_cached_value( 'pagination_large_number' ) + nb_machines = machines.count() machines = re2o_paginator(request, machines, pagination_large_number) factures = Facture.objects.filter(user=users) factures = SortTable.sort( @@ -844,6 +854,7 @@ def profil(request, users, **_kwargs): { 'users': users, 'machines_list': machines, + 'nb_machines' : nb_machines, 'facture_list': factures, 'ban_list': bans, 'white_list': whitelists,