8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-12-23 15:33:45 +00:00

Merge branch 'master' into graph_topo

This commit is contained in:
Gabriel Detraz 2018-05-11 18:54:18 +02:00 committed by chirac
commit e690292e77
31 changed files with 647 additions and 353 deletions

View file

@ -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

View file

@ -66,7 +66,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td><p class="text-success">{{utilisateur.last}}</p></td>
{% endif %}
<td>
{% if droit != 'Superuser' %}
<a href="{% url 'users:del-group' utilisateur.id droit.id %}">
{% else %}
<a href="{% url 'users:del-superuser' utilisateur.id %}">
{% endif %}
<button type="button" class="btn btn-danger" aria-label="Left Align">
<span class="fa fa-user-times" aria-hidden="true"></span>
</button>
@ -79,4 +83,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div>
</div>
</div>
{% endfor %}
{% endfor %}

View file

@ -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',

View file

@ -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'),
),
]

View file

@ -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 = (

View file

@ -28,7 +28,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% include "pagination.html" with list=machines_list %}
{% endif %}
<table class="table" id="machines_table">
<colgroup>
<col>

View file

@ -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

View file

@ -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.

View file

@ -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()

View file

@ -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):

Binary file not shown.

View file

@ -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é"

View file

@ -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

View file

@ -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')),

View file

@ -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="<i class=\"fa fa-question-circle\" title=\"{}\"></i>"
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

View file

@ -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
)

View file

@ -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%;

View file

@ -118,7 +118,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</li>
{% acl_end %}
<li>
<a href="{% url 'about' %}"><i class="fa fa-info-circle"></i> {% trans "About" %}</a>
<a href="{% url 'about' %}"><i class="fa fa-info-circle"></i> {% trans "About" %}</a>
</li>
{% 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.,
</div>
</form>
</li>
{% endif %}
</ul>
</ul>
{% comment %}
<div class="navbar-right">
<form action="{% url "search:search"%}" class="navbar-form" role="search">
<div class="input-group">
<input type="text" class="form-control" placeholder="Search" name="q" id="search-term" {% if search_term %}value="{{ search_term }}"{% endif %}>
<div class="input-group-btn">
<button class="btn btn-default" type="submit"><i class="fa fa-search"></i></button>
<a href="{% url "search:searchp" %}" class="btn btn-default" role="button"><i class="fa fa-plus"></i></a>
</div>
</div>
</form>
</div>
<ul class="nav navbar-nav navbar-right">
{% if not request.user.is_authenticated %}
{% if var_sa %}
<li>
<a href="{% url 'users:new-user' %}">
<i class="fa fa-user-plus"></i> Créer un compte
</a>
</li>
{% endif %}
<li>
<a href="{% url 'login' %}">
<i class="fa fa-sign-in-alt"></i> Login
</a>
</li>
{% endif %}
{% can_view_app preferences %}
{% endif %}
<li>
<a href="{% url 'preferences:display-options' %}">
<i class="fa fa-cogs"></i> Preferences
</a>
</li>
{% acl_end %}
<li>
<a href="{% url 'about' %}"><i class="fa fa-info-circle"></i> A propos</a>
{% include 'buttons/setlang.html' %}
</li>
</ul>
{% endcomment %}
</div>
</div>
</nav>

View file

@ -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 %}
<a class="dropdown" type="button" role="button" id="setlang"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-globe"></i> <span class="caret"></span>
</a>
<div class="dropdown-menu" aria-labelledby="setlang">
<p style="text-align: center"><small><i class="fa fa-exclamation-triangle"></i> {% trans "Translation in development" %}</small></p>
<hr>
<form method="post" action="{% url 'set_language' %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
{% for language in languages %}
<button type="submit" name="language" value="{{ language.code }}"
class="btn btn-link btn-block
{% if language.code == LANGUAGE_CODE %}disabled{% endif %}">
{% if language.code == LANGUAGE_CODE %}
<i class="fa fa-check"></i>
{% endif %}
{{ language.name_local | title }} ({{ language.code }})
</button>
{% endfor %}
</form>
</div>

View file

@ -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 %}
<ul class="pagination nav navbar-nav">
{% if list.has_previous %}
<li><a href="{% url_insert_param request.get_full_path page=1 %}"> << </a></li>
@ -39,5 +40,5 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<li><a href="{% url_insert_param request.get_full_path page=list.next_page_number %}"> > </a></li>
<li><a href="{% url_insert_param request.get_full_path page=list.paginator.page_range|length %}"> >> </a></li>
{% endif %}
</ul>
</ul>
{% endif %}

View file

@ -24,69 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %}
<div class="table-responsive" style="font-size: 12px">
<table class="table table-bordered text-center text-nowrap">
<thead>
<tr>
{% for port in port_list|slice:"::2" %}
<td class="bg-primary text-white">{{ port.port }}</td>
{% endfor %}
</tr>
<tr>
{% for port in port_list|slice:"::2" %}
{% if port.room %}
<td class="p-3 mb-2 bg-success text-dark">
{{ port.room }}
{% elif port.machine_interface %}
<td class="p-3 mb-2 bg-warning text-dark">
<a href="{% url 'users:profil' userid=port.machine_interface.machine.user.id %}">{{ port.machine_interface }}</a>
{% elif port.related%}
<td class="p-3 mb-2 bg-danger text-dark">
<a href="{% url 'topologie:index-port' switchid=port.related.switch.id %}">{{ port.related }}</a>
{% else %}
<td class="p-3 mb-2 bg-info text-dark">
Vide
{% endif %}
</td>
{% endfor %}
</tr>
<tr>
{% for port in port_list|slice:"1::2" %}
<td class="bg-primary text-white">{{ port.port }}</td>
{% endfor %}
</tr>
<tr>
{% for port in port_list|slice:"1::2" %}
{% if port.room %}
<td class="p-3 mb-2 bg-success text-dark">
{{ port.room }}
{% elif port.machine_interface %}
<td class="p-3 mb-2 bg-warning text-dark">
<a href="{% url 'users:profil' userid=port.machine_interface.machine.user.id %}">{{ port.machine_interface }}</a>
{% elif port.related%}
<td class="p-3 mb-2 bg-danger text-dark">
<a href="{% url 'topologie:index-port' switchid=port.related.switch.id %}">{{ port.related }}</a>
{% else %}
<td class="p-3 mb-2 bg-info text-dark">
Vide
{% endif %}
</td>
{% endfor %}
</tr>
</table>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
@ -105,37 +42,47 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr>
<td>{{ port.port }}</td>
<td>
{% if port.room %}
{{ port.room }}
{% endif %}
{% if port.room %}{{ port.room }}{% endif %}
</td>
<td>
{% if port.machine_interface %}
<a href="{% url 'users:profil' userid=port.machine_interface.machine.user.id %}">{{ port.machine_interface }}</a>
{% endif %}
{% if port.machine_interface %}
{% can_view port.machine_interface.machine.user %}
<a href="{% url 'users:profil' userid=port.machine_interface.machine.user.id %}">
{{ port.machine_interface }}
</a>
{% acl_else %}
{{ port.machine_interface }}
{% acl_end %}
{% endif %}
</td>
<td>
{% if port.related %}
<a href="{% url 'topologie:index-port' switchid=port.related.switch.id %}">{{ port.related }}</a>
{% endif %}
{% if port.related %}
{% can_view port.related.switch %}
<a href="{% url 'topologie:index-port' switchid=port.related.switch.id %}">
{{ port.related }}
</a>
{% acl_else %}
{{ port.related }}
{% acl_end %}
{% endif %}
</td>
<td>{{ port.radius }}</td>
<td>{% if not port.vlan_force %} Aucun{%else %}{{ port.vlan_force }}{% endif %}</td>
<td>{% if not port.vlan_force %}Aucun{% else %}{{ port.vlan_force }}{% endif %}</td>
<td>{{ port.details }}</td>
<td class="text-right">
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'port' port.pk %}">
<i class="fa fa-history"></i>
</a>
{% can_edit port %}
{% can_edit port %}
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'topologie:edit-port' port.id %}">
<i class="fa fa-edit"></i>
</a>
{% acl_end %}
{% can_delete port %}
{% acl_end %}
{% can_delete port %}
<a class="btn btn-danger btn-sm" role="button" title="Supprimer" href="{% url 'topologie:del-port' port.pk %}">
<i class="fa fa-trash"></i>
</a>
{% acl_end %}
{% acl_end %}
</td>
</tr>
{% endfor %}

View file

@ -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 %}
<div class="table-responsive" style="font-size: 12px">
<table class="table table-bordered text-center text-nowrap">
<thead>
<tr>
{% for port in port_list|slice:"::2" %}
<td class="bg-primary text-white">{{ port.port }}</td>
{% endfor %}
</tr>
<tr>
{% for port in port_list|slice:"::2" %}
{% if port.room %}
<td class="p-3 mb-2 bg-success text-dark">
{{ port.room }}
</td>
{% elif port.machine_interface %}
<td class="p-3 mb-2 bg-warning text-dark">
{% can_view port.machine_interface.machine.user %}
<a href="{% url 'users:profil' userid=port.machine_interface.machine.user.id %}">
{{ port.machine_interface }}
</a>
{% acl_else %}
{{ port.machine_interface }}
{% acl_end %}
</td>
{% elif port.related%}
<td class="p-3 mb-2 bg-danger text-dark">
{% can_view port.related.switch %}
<a href="{% url 'topologie:index-port' switchid=port.related.switch.id %}">
{{ port.related }}
</a>
{% acl_else %}
{{ port.related }}
{% acl_end %}
</td>
{% else %}
<td class="p-3 mb-2 bg-info text-dark">
Vide
</td>
{% endif %}
{% endfor %}
</tr>
<tr>
{% for port in port_list|slice:"1::2" %}
<td class="bg-primary text-white">{{ port.port }}</td>
{% endfor %}
</tr>
<tr>
{% for port in port_list|slice:"1::2" %}
{% if port.room %}
<td class="p-3 mb-2 bg-success text-dark">
{{ port.room }}
</td>
{% elif port.machine_interface %}
<td class="p-3 mb-2 bg-warning text-dark">
{% can_view port.machine_interface.machine.user %}
<a href="{% url 'users:profil' userid=port.machine_interface.machine.user.id %}">
{{ port.machine_interface }}
</a>
{% acl_else %}
{{ port.machine_interface }}
{% acl_end %}
</td>
{% elif port.related%}
<td class="p-3 mb-2 bg-danger text-dark">
{% can_view port.related.switch %}
<a href="{% url 'topologie:index-port' switchid=port.related.switch.id %}">
{{ port.related }}
</a>
{% acl_else %}
{{ port.related }}
{% acl_end %}
</td>
{% else %}
<td class="p-3 mb-2 bg-info text-dark">
Vide
</td>
{% endif %}
{% endfor %}
</tr>
</table>
</div>

View file

@ -36,6 +36,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-primary btn-sm" role="button" href="{% url 'topologie:create-ports' id_switch %}"><i class="fa fa-plus"></i> Ajouter des ports</a>
{% acl_end %}
<hr>
{% include "topologie/aff_repr_switch.html" with port_list=port_list %}
{% include "topologie/aff_port.html" with port_list=port_list %}
<br />
<br />

View file

@ -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')

View file

@ -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

View file

@ -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(),
),
]

View file

@ -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 = (

View file

@ -33,6 +33,44 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th></th>
</tr>
</thead>
{% if superuser_right %}
<tr class="active">
<td>Superuser</td>
<td></td>
<td>True</td>
<td>
<button class="btn btn-default" data-parent="#accordion_superuser" type="button" data-toggle="collapse" data-target="#collapseListRight_user_superuser" aria-expanded="true" aria-controls="collapseListRight_user_superuser">
Utilisateurs ({{ superuser_right.count }})
</button>
</td>
<td>
Donne tous les droits sur Re2o.
</td>
<td class="text-right">
</td>
</tr>
<tr>
<td colspan=5>
<div class="panel-group" id="accordion_superuser" role="tablist" aria-multiselectable="true" style="margin-bottom: 0px;">
<div class="panel" style="border: none;">
<div class="panel-collapse collapse in" id="collapseListRight_user_superuser" role="tabpanel">
<ul class="list-group" style="margin-bottom: 0px">
{% for user in superuser_right %}
<li class="list-group-item col-xs-12 col-sm-6 col-md-4" style="border:none;">
{{user}}
<a role="button" href="{% url 'users:del-superuser' user.pk %}" title="{{ desc|default:"Supprimer" }}">
<i class="fa fa-times" style="color:red"></i>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</td>
</tr>
{% endif %}
{% for listright in listright_list %}
<tr class="active">
<td>
@ -48,9 +86,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<button class="btn btn-default" data-parent="#accordion_{{listright.gid}}" type="button" data-toggle="collapse" data-target="#collapseListRight_perm_{{listright.gid}}" aria-expanded="true" aria-controls="collapseListRight_perm_{{listright.gid}}">
Ensemble des permissions ({{ listright.permissions.all|length }})
</button>
</div>
</td>
<td>{{ listright.details }}</td>
</div>
</td>
<td>{{ listright.details }}</td>
<td class="text-right">
{% 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 %}

View file

@ -245,7 +245,7 @@ non adhérent</span>{% endif %} et votre connexion est {% if users.has_access %}
<h3 class="panel-title pull-left">
<i class="fa fa-desktop"></i>
Machines
<span class="badge">{{machines_list.count}}</span>
<span class="badge">{{nb_machines}}</span>
</h3>
</div>
<div id="collapse3" class="panel-collapse collapse">

View file

@ -43,6 +43,9 @@ urlpatterns = [
url(r'^del_group/(?P<userid>[0-9]+)/(?P<listrightid>[0-9]+)$',
views.del_group,
name='del-group'),
url(r'^del_superuser/(?P<userid>[0-9]+)$',
views.del_superuser,
name='del-superuser'),
url(r'^new_serviceuser/$', views.new_serviceuser, name='new-serviceuser'),
url(r'^edit_serviceuser/(?P<serviceuserid>[0-9]+)$',
views.edit_serviceuser,

View file

@ -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,