8
0
Fork 0
mirror of https://gitlab.federez.net/re2o/re2o synced 2024-05-02 15:43:28 +00:00

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

This commit is contained in:
Grizzly 2019-01-09 17:40:50 +00:00
commit 8035f14e1b
81 changed files with 3109 additions and 1025 deletions

2
.gitignore vendored
View file

@ -5,7 +5,7 @@ __pycache__/
*.swp
# Translations
#*.mo TODO
*.mo
*.pot
# Django stuff

View file

@ -391,13 +391,25 @@ class OptionalTopologieSerializer(NamespacedHMSerializer):
class Meta:
model = preferences.OptionalTopologie
fields = ('radius_general_policy', 'vlan_decision_ok',
'vlan_decision_nok', 'switchs_ip_type', 'switchs_web_management',
fields = ('switchs_ip_type', 'switchs_web_management',
'switchs_web_management_ssl', 'switchs_rest_management',
'switchs_management_utils', 'switchs_management_interface_ip',
'provision_switchs_enabled', 'switchs_provision', 'switchs_management_sftp_creds')
class RadiusOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.RadiusOption` objects
"""
class Meta:
model = preferences.RadiusOption
fields = ('radius_general_policy', 'unknown_machine',
'unknown_machine_vlan', 'unknown_port',
'unknown_port_vlan', 'unknown_room', 'unknown_room_vlan',
'non_member', 'non_member_vlan', 'banned', 'banned_vlan',
'vlan_decision_ok')
class GeneralOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.GeneralOption` objects.
"""
@ -407,9 +419,8 @@ class GeneralOptionSerializer(NamespacedHMSerializer):
fields = ('general_message_fr', 'general_message_en',
'search_display_page', 'pagination_number',
'pagination_large_number', 'req_expire_hrs',
'site_name', 'email_from', 'GTU_sum_up',
'GTU')
'site_name', 'main_site_url', 'email_from',
'GTU_sum_up', 'GTU')
class HomeServiceSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.Service` objects.
@ -633,9 +644,8 @@ class AdherentSerializer(NamespacedHMSerializer):
}
class HomeCreationSerializer(NamespacedHMSerializer):
"""Serialize 'users.models.User' minimal infos to create home
"""
class BasicUserSerializer(NamespacedHMSerializer):
"""Serialize 'users.models.User' minimal infos"""
uid = serializers.IntegerField(source='uid_number')
gid = serializers.IntegerField(source='gid_number')
@ -813,7 +823,8 @@ class SwitchPortSerializer(serializers.ModelSerializer):
model = topologie.Switch
fields = ('short_name', 'model', 'switchbay', 'ports', 'ipv4', 'ipv6',
'interfaces_subnet', 'interfaces6_subnet', 'automatic_provision', 'rest_enabled',
'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value')
'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value',
'list_modules')
# LOCAL EMAILS
@ -1001,6 +1012,17 @@ class CNAMERecordSerializer(serializers.ModelSerializer):
model = machines.Domain
fields = ('alias', 'hostname')
class DNAMERecordSerializer(serializers.ModelSerializer):
"""Serialize `machines.models.Domain` objects with the data needed to
generate a DNAME DNS record.
"""
alias = serializers.CharField(read_only=True)
zone = serializers.CharField(read_only=True)
class Meta:
model = machines.DName
fields = ('alias', 'zone')
class DNSZonesSerializer(serializers.ModelSerializer):
"""Serialize the data about DNS Zones.
@ -1015,14 +1037,14 @@ class DNSZonesSerializer(serializers.ModelSerializer):
a_records = ARecordSerializer(many=True, source='get_associated_a_records')
aaaa_records = AAAARecordSerializer(many=True, source='get_associated_aaaa_records')
cname_records = CNAMERecordSerializer(many=True, source='get_associated_cname_records')
dname_records = DNAMERecordSerializer(many=True, source='get_associated_dname_records')
sshfp_records = SSHFPInterfaceSerializer(many=True, source='get_associated_sshfp_records')
class Meta:
model = machines.Extension
fields = ('name', 'soa', 'ns_records', 'originv4', 'originv6',
'mx_records', 'txt_records', 'srv_records', 'a_records',
'aaaa_records', 'cname_records', 'sshfp_records')
'aaaa_records', 'cname_records', 'dname_records', 'sshfp_records')
#REMINDER

View file

@ -67,6 +67,7 @@ router.register_viewset(r'machines/role', views.RoleViewSet)
router.register_view(r'preferences/optionaluser', views.OptionalUserView),
router.register_view(r'preferences/optionalmachine', views.OptionalMachineView),
router.register_view(r'preferences/optionaltopologie', views.OptionalTopologieView),
router.register_view(r'preferences/radiusoption', views.RadiusOptionView),
router.register_view(r'preferences/generaloption', views.GeneralOptionView),
router.register_viewset(r'preferences/service', views.HomeServiceViewSet, base_name='homeservice'),
router.register_view(r'preferences/assooption', views.AssoOptionView),
@ -88,6 +89,8 @@ router.register(r'topologie/portprofile', views.PortProfileViewSet)
# USERS
router.register_viewset(r'users/user', views.UserViewSet, base_name='user')
router.register_viewset(r'users/homecreation', views.HomeCreationViewSet, base_name='homecreation')
router.register_viewset(r'users/normaluser', views.NormalUserViewSet, base_name='normaluser')
router.register_viewset(r'users/criticaluser', views.CriticalUserViewSet, base_name='criticaluser')
router.register_viewset(r'users/club', views.ClubViewSet)
router.register_viewset(r'users/adherent', views.AdherentViewSet)
router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet)

View file

@ -292,6 +292,17 @@ class OptionalTopologieView(generics.RetrieveAPIView):
return preferences.OptionalTopologie.objects.first()
class RadiusOptionView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.OptionalTopologie` settings.
"""
permission_classes = (ACLPermission,)
perms_map = {'GET': [preferences.RadiusOption.can_view_all]}
serializer_class = serializers.RadiusOptionSerializer
def get_object(self):
return preferences.RadiusOption.objects.first()
class GeneralOptionView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.GeneralOption` settings.
"""
@ -445,7 +456,19 @@ class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users` objects to create homes.
"""
queryset = users.User.objects.exclude(Q(state=users.User.STATE_DISABLED) | Q(state=users.User.STATE_NOT_YET_ACTIVE))
serializer_class = serializers.HomeCreationSerializer
serializer_class = serializers.BasicUserSerializer
class NormalUserViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users`without specific rights objects."""
queryset = users.User.objects.exclude(groups__listright__critical=True).distinct()
serializer_class = serializers.BasicUserSerializer
class CriticalUserViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users`without specific rights objects."""
queryset = users.User.objects.filter(groups__listright__critical=True).distinct()
serializer_class = serializers.BasicUserSerializer
class ClubViewSet(viewsets.ReadOnlyModelViewSet):
@ -541,8 +564,8 @@ class ServiceRegenViewSet(viewsets.ModelViewSet):
# Config des switches
class SwitchPortView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files.
"""Output each port of a switch, to be serialized with
additionnal informations (profiles etc)
"""
queryset = topologie.Switch.objects.all().select_related("switchbay").select_related("model__constructor").prefetch_related("ports__custom_profile__vlan_tagged").prefetch_related("ports__custom_profile__vlan_untagged").prefetch_related("ports__machine_interface__domain__extension").prefetch_related("ports__room")
@ -551,16 +574,14 @@ class SwitchPortView(generics.ListAPIView):
# Rappel fin adhésion
class ReminderView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files.
"""Output for users to remind an end of their subscription.
"""
queryset = preferences.Reminder.objects.all()
serializer_class = serializers.ReminderSerializer
class RoleView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files.
"""Output of roles for each server
"""
queryset = machines.Role.objects.all().prefetch_related('servers')
serializer_class = serializers.RoleSerializer

View file

@ -17,3 +17,4 @@ libjs-bootstrap
fonts-font-awesome
graphviz
git
gettext

View file

@ -30,7 +30,7 @@ from django.contrib import admin
from reversion.admin import VersionAdmin
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
from .models import CustomInvoice
from .models import CustomInvoice, CostEstimate
class FactureAdmin(VersionAdmin):
@ -38,6 +38,11 @@ class FactureAdmin(VersionAdmin):
pass
class CostEstimateAdmin(VersionAdmin):
"""Admin class for cost estimates."""
pass
class CustomInvoiceAdmin(VersionAdmin):
"""Admin class for custom invoices."""
pass
@ -76,3 +81,4 @@ admin.site.register(Paiement, PaiementAdmin)
admin.site.register(Vente, VenteAdmin)
admin.site.register(Cotisation, CotisationAdmin)
admin.site.register(CustomInvoice, CustomInvoiceAdmin)
admin.site.register(CostEstimate, CostEstimateAdmin)

View file

@ -46,7 +46,10 @@ from django.shortcuts import get_object_or_404
from re2o.field_permissions import FieldPermissionFormMixin
from re2o.mixins import FormRevMixin
from .models import Article, Paiement, Facture, Banque, CustomInvoice
from .models import (
Article, Paiement, Facture, Banque,
CustomInvoice, Vente, CostEstimate
)
from .payment_methods import balance
@ -104,7 +107,44 @@ class SelectArticleForm(FormRevMixin, Form):
user = kwargs.pop('user')
target_user = kwargs.pop('target_user', None)
super(SelectArticleForm, self).__init__(*args, **kwargs)
self.fields['article'].queryset = Article.find_allowed_articles(user, target_user)
self.fields['article'].queryset = Article.find_allowed_articles(
user, target_user)
class DiscountForm(Form):
"""
Form used in oder to create a discount on an invoice.
"""
is_relative = forms.BooleanField(
label=_("Discount is on percentage"),
required=False,
)
discount = forms.DecimalField(
label=_("Discount"),
max_value=100,
min_value=0,
max_digits=5,
decimal_places=2,
required=False,
)
def apply_to_invoice(self, invoice):
invoice_price = invoice.prix_total()
discount = self.cleaned_data['discount']
is_relative = self.cleaned_data['is_relative']
if is_relative:
amount = discount/100 * invoice_price
else:
amount = discount
if amount:
name = _("{}% discount") if is_relative else _("{}€ discount")
name = name.format(discount)
Vente.objects.create(
facture=invoice,
name=name,
prix=-amount,
number=1
)
class CustomInvoiceForm(FormRevMixin, ModelForm):
@ -116,6 +156,15 @@ class CustomInvoiceForm(FormRevMixin, ModelForm):
fields = '__all__'
class CostEstimateForm(FormRevMixin, ModelForm):
"""
Form used to create a cost estimate.
"""
class Meta:
model = CostEstimate
exclude = ['paid', 'final_invoice']
class ArticleForm(FormRevMixin, ModelForm):
"""
Form used to create an article.
@ -248,7 +297,8 @@ class RechargeForm(FormRevMixin, Form):
super(RechargeForm, self).__init__(*args, **kwargs)
self.fields['payment'].empty_label = \
_("Select a payment method")
self.fields['payment'].queryset = Paiement.find_allowed_payments(user_source).exclude(is_balance=True)
self.fields['payment'].queryset = Paiement.find_allowed_payments(
user_source).exclude(is_balance=True)
def clean(self):
"""
@ -266,4 +316,3 @@ class RechargeForm(FormRevMixin, Form):
}
)
return self.cleaned_data

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-29 14:22
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0035_notepayment'),
]
operations = [
migrations.AddField(
model_name='custominvoice',
name='remark',
field=models.TextField(blank=True, null=True, verbose_name='Remark'),
),
]

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-29 21:03
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0036_custominvoice_remark'),
]
operations = [
migrations.CreateModel(
name='CostEstimate',
fields=[
('custominvoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.CustomInvoice')),
('validity', models.DurationField(verbose_name='Period of validity')),
('final_invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='origin_cost_estimate', to='cotisations.CustomInvoice')),
],
options={
'permissions': (('view_costestimate', 'Can view a cost estimate object'),),
},
bases=('cotisations.custominvoice',),
),
]

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-31 22:57
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0037_costestimate'),
]
operations = [
migrations.AlterField(
model_name='costestimate',
name='final_invoice',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='origin_cost_estimate', to='cotisations.CustomInvoice'),
),
migrations.AlterField(
model_name='costestimate',
name='validity',
field=models.DurationField(help_text='DD HH:MM:SS', verbose_name='Period of validity'),
),
migrations.AlterField(
model_name='custominvoice',
name='paid',
field=models.BooleanField(default=False, verbose_name='Paid'),
),
]

View file

@ -284,8 +284,65 @@ class CustomInvoice(BaseInvoice):
verbose_name=_("Address")
)
paid = models.BooleanField(
verbose_name=_("Paid")
verbose_name=_("Paid"),
default=False
)
remark = models.TextField(
verbose_name=_("Remark"),
blank=True,
null=True
)
class CostEstimate(CustomInvoice):
class Meta:
permissions = (
('view_costestimate', _("Can view a cost estimate object")),
)
validity = models.DurationField(
verbose_name=_("Period of validity"),
help_text="DD HH:MM:SS"
)
final_invoice = models.ForeignKey(
CustomInvoice,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="origin_cost_estimate",
primary_key=False
)
def create_invoice(self):
"""Create a CustomInvoice from the CostEstimate."""
if self.final_invoice is not None:
return self.final_invoice
invoice = CustomInvoice()
invoice.recipient = self.recipient
invoice.payment = self.payment
invoice.address = self.address
invoice.paid = False
invoice.remark = self.remark
invoice.date = timezone.now()
invoice.save()
self.final_invoice = invoice
self.save()
for sale in self.vente_set.all():
Vente.objects.create(
facture=invoice,
name=sale.name,
prix=sale.prix,
number=sale.number,
)
return invoice
def can_delete(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.delete_costestimate'):
return False, _("You don't have the right "
"to delete a cost estimate.")
if self.final_invoice is not None:
return False, _("The cost estimate has an "
"invoice and cannot be deleted.")
return True, None
# TODO : change Vente to Purchase
@ -624,7 +681,7 @@ class Article(RevMixin, AclMixin, models.Model):
objects_pool = cls.objects.filter(
Q(type_user='All') | Q(type_user='Adherent')
)
if not target_user.is_adherent():
if target_user is not None and not target_user.is_adherent():
objects_pool = objects_pool.filter(
Q(type_cotisation='All') | Q(type_cotisation='Adhesion')
)

View file

@ -0,0 +1,101 @@
{% 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 Hugo Levy-Falk
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 %}
{% load acl %}
{% load logs_extra %}
{% load design %}
<div class="table-responsive">
{% if cost_estimate_list.paginator %}
{% include 'pagination.html' with list=cost_estimate_list%}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>
{% trans "Recipient" as tr_recip %}
{% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_user %}
</th>
<th>{% trans "Designation" %}</th>
<th>{% trans "Total price" %}</th>
<th>
{% trans "Payment method" as tr_payment_method %}
{% include 'buttons/sort.html' with prefix='invoice' col='payement' text=tr_payment_method %}
</th>
<th>
{% trans "Date" as tr_date %}
{% include 'buttons/sort.html' with prefix='invoice' col='date' text=tr_date %}
</th>
<th>
{% trans "Validity" as tr_validity %}
{% include 'buttons/sort.html' with prefix='invoice' col='validity' text=tr_validity %}
</th>
<th>
{% trans "Cost estimate ID" as tr_estimate_id %}
{% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_estimate_id %}
</th>
<th>
{% trans "Invoice created" as tr_invoice_created%}
{% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_created %}
</th>
<th></th>
<th></th>
</tr>
</thead>
{% for estimate in cost_estimate_list %}
<tr>
<td>{{ estimate.recipient }}</td>
<td>{{ estimate.name }}</td>
<td>{{ estimate.prix_total }}</td>
<td>{{ estimate.payment }}</td>
<td>{{ estimate.date }}</td>
<td>{{ estimate.validity }}</td>
<td>{{ estimate.id }}</td>
<td>
{% if estimate.final_invoice %}
<a href="{% url 'cotisations:edit-custom-invoice' estimate.final_invoice.pk %}"><i style="color: #1ECA18;" class="fa fa-check"></i></a>
{% else %}
<i style="color: #D10115;" class="fa fa-times"></i>'
{% endif %}
</td>
<td>
{% can_edit estimate %}
{% include 'buttons/edit.html' with href='cotisations:edit-cost-estimate' id=estimate.id %}
{% acl_end %}
{% history_button estimate %}
{% include 'buttons/suppr.html' with href='cotisations:del-cost-estimate' id=estimate.id %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:cost-estimate-to-invoice' estimate.id %}">
<i class="fa fa-file"></i>
</a>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:cost-estimate-pdf' estimate.id %}">
<i class="fa fa-file-pdf-o"></i> {% trans "PDF" %}
</a>
</td>
</tr>
{% endfor %}
</table>
{% if custom_invoice_list.paginator %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% endif %}
</div>

View file

@ -35,7 +35,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post">
{% csrf_token %}
{% if title %}
<h3>{{title}}</h3>
{% else %}
<h3>{% trans "Edit the invoice" %}</h3>
{% endif %}
{% massive_bootstrap_form factureform 'user' %}
{{ venteform.management_form }}
<h3>{% trans "Articles" %}</h3>

View file

@ -44,6 +44,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}
</p>
{% endif %}
{% if factureform %}
{% bootstrap_form_errors factureform %}
{% endif %}
{% if discount_form %}
{% bootstrap_form_errors discount_form %}
{% endif %}
<form class="form" method="post">
{% csrf_token %}
@ -68,8 +74,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %}
</div>
<input class="btn btn-primary btn-block" role="button" value="{% trans "Add an extra article"%}" id="add_one">
<h3>{% trans "Discount" %}</h3>
{% if discount_form %}
{% bootstrap_form discount_form %}
{% endif %}
<p>
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
</p>
{% endif %}
{% bootstrap_button action_name button_type='submit' icon='ok' button_class='btn-success' %}
@ -78,105 +88,117 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if articlesformset or payment_method%}
<script type="text/javascript">
{% if articlesformset %}
var prices = {};
{% for article in articlelist %}
prices[{{ article.id|escapejs }}] = {{ article.prix }};
{% endfor %}
var prices = {};
{% for article in articlelist %}
prices[{{ article.id|escapejs }}] = {{ article.prix }};
{% endfor %}
var template = `Article : &nbsp;
{% bootstrap_form articlesformset.empty_form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm"
id="id_form-__prefix__-article-remove" type="button">
<span class="fa fa-times"></span>
</button>`
var template = `Article : &nbsp;
{% bootstrap_form articlesformset.empty_form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm"
id="id_form-__prefix__-article-remove" type="button">
<span class="fa fa-times"></span>
</button>`
function add_article(){
// Index start at 0 => new_index = number of items
var new_index =
document.getElementsByClassName('product_to_sell').length;
document.getElementById('id_form-TOTAL_FORMS').value ++;
var new_article = document.createElement('div');
new_article.className = 'product_to_sell form-inline';
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('form_set').appendChild(new_article);
add_listenner_for_id(new_index);
}
function add_article(){
// Index start at 0 => new_index = number of items
var new_index =
document.getElementsByClassName('product_to_sell').length;
document.getElementById('id_form-TOTAL_FORMS').value ++;
var new_article = document.createElement('div');
new_article.className = 'product_to_sell form-inline';
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('form_set').appendChild(new_article);
add_listenner_for_id(new_index);
}
function update_price(){
var price = 0;
var product_count =
document.getElementsByClassName('product_to_sell').length;
var article, article_price, quantity;
for (i = 0; i < product_count; ++i){
article = document.getElementById(
function update_price(){
var price = 0;
var product_count =
document.getElementsByClassName('product_to_sell').length;
var article, article_price, quantity;
for (i = 0; i < product_count; ++i){
article = document.getElementById(
'id_form-' + i.toString() + '-article').value;
if (article == '') {
continue;
}
article_price = prices[article];
quantity = document.getElementById(
if (article == '') {
continue;
}
article_price = prices[article];
quantity = document.getElementById(
'id_form-' + i.toString() + '-quantity').value;
price += article_price * quantity;
}
document.getElementById('total_price').innerHTML =
price.toFixed(2).toString().replace('.', ',');
price += article_price * quantity;
}
function add_listenner_for_id(i){
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("onkeypress", update_price, true);
document.getElementById('id_form-' + i.toString() + '-quantity')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article-remove')
.addEventListener("click", function(event) {
var article = event.target.parentNode;
article.parentNode.removeChild(article);
document.getElementById('id_form-TOTAL_FORMS').value --;
update_price();
})
{% if discount_form %}
var relative_discount = document.getElementById('id_is_relative').checked;
var discount = document.getElementById('id_discount').value;
if(relative_discount) {
discount = discount/100 * price;
}
price -= discount;
{% endif %}
document.getElementById('total_price').innerHTML =
price.toFixed(2).toString().replace('.', ',');
}
// Add events manager when DOM is fully loaded
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("add_one")
.addEventListener("click", add_article, true);
var product_count =
document.getElementsByClassName('product_to_sell').length;
for (i = 0; i < product_count; ++i){
add_listenner_for_id(i);
}
update_price();
});
function add_listenner_for_id(i){
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("onkeypress", update_price, true);
document.getElementById('id_form-' + i.toString() + '-quantity')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article-remove')
.addEventListener("click", function(event) {
var article = event.target.parentNode;
article.parentNode.removeChild(article);
document.getElementById('id_form-TOTAL_FORMS').value --;
update_price();
})
}
// Add events manager when DOM is fully loaded
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("add_one")
.addEventListener("click", add_article, true);
var product_count =
document.getElementsByClassName('product_to_sell').length;
for (i = 0; i < product_count; ++i){
add_listenner_for_id(i);
}
document.getElementById('id_discount')
.addEventListener('change', update_price, true);
document.getElementById('id_is_relative')
.addEventListener('click', update_price, true);
update_price();
});
{% endif %}
{% if payment_method.templates %}
var TEMPLATES = [
"",
{% for t in payment_method.templates %}
{% if t %}
`{% bootstrap_form t %}`,
{% else %}
"",
{% endif %}
{% endfor %}
];
function update_payment_method_form(){
var method = document.getElementById('paymentMethodSelect').value;
if(method==""){
method=0;
}
else{
method = Number(method);
method += 1;
}
console.log(method);
var html = TEMPLATES[method];
document.getElementById('paymentMethod').innerHTML = html;
var TEMPLATES = [
"",
{% for t in payment_method.templates %}
{% if t %}
`{% bootstrap_form t %}`,
{% else %}
"",
{% endif %}
{% endfor %}
];
function update_payment_method_form(){
var method = document.getElementById('paymentMethodSelect').value;
if(method==""){
method=0;
}
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
else{
method = Number(method);
method += 1;
}
console.log(method);
var html = TEMPLATES[method];
document.getElementById('paymentMethod').innerHTML = html;
}
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
{% endif %}
</script>
{% endif %}

View file

@ -43,7 +43,7 @@
\begin{document}
%----------------------------------------------------------------------------------------
% HEADING SECTION
%----------------------------------------------------------------------------------------
@ -70,13 +70,17 @@
{\bf Siret :} {{siret|safe}}
\vspace{2cm}
\begin{tabular*}{\textwidth}{@{\extracolsep{\fill}} l r}
{\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\
{\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\
{% if fid is not None %}
{% if is_estimate %}
{\bf Devis n\textsuperscript{o} :} {{ fid }} & \\
{% else %}
{\bf Facture n\textsuperscript{o} :} {{ fid }} & \\
{% endif %}
{% endif %}
\end{tabular*}
\\
@ -84,39 +88,57 @@
%----------------------------------------------------------------------------------------
% TABLE OF EXPENSES
%----------------------------------------------------------------------------------------
\begin{tabularx}{\textwidth}{|X|r|r|r|}
\hline
\textbf{Désignation} & \textbf{Prix Unit.} \euro & \textbf{Quantité} & \textbf{Prix total} \euro\\
\doublehline
{% for a in article %}
{{a.name}} & {{a.price}} \euro & {{a.quantity}} & {{a.total_price}} \euro\\
\hline
{% endfor %}
\end{tabularx}
\vspace{1cm}
\hfill
\begin{tabular}{|l|r|}
\hline
\textbf{Total} & {{total|floatformat:2}} \euro \\
{% if not is_estimate %}
\textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\
\doublehline
\textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\
{% endif %}
\hline
\end{tabular}
\vspace{1cm}
\begin{tabularx}{\textwidth}{r X}
\hline
\textbf{Moyen de paiement} & {{payment_method|default:"Non spécifié"}} \\
\hline
{% if remark %}
\textbf{Remarque} & {{remark|safe}} \\
\hline
{% endif %}
{% if end_validity %}
\textbf{Validité} & Jusqu'au {{end_validity}} \\
\hline
{% endif %}
\end{tabularx}
\vfill
%----------------------------------------------------------------------------------------
% FOOTNOTE
%----------------------------------------------------------------------------------------
\hrule
\smallskip
\footnotesize{TVA non applicable, art. 293 B du CGI}

View file

@ -0,0 +1,36 @@
{% extends "cotisations/sidebar.html" %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load acl %}
{% load i18n %}
{% block title %}{% trans "Cost estimates" %}{% endblock %}
{% block content %}
<h2>{% trans "Cost estimates list" %}</h2>
{% can_create CostEstimate %}
{% include "buttons/add.html" with href='cotisations:new-cost-estimate'%}
{% acl_end %}
{% include 'cotisations/aff_cost_estimate.html' %}
{% endblock %}

View file

@ -45,6 +45,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-list-ul"></i> {% trans "Custom invoices" %}
</a>
{% acl_end %}
{% can_view_all CostEstimate %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-cost-estimate" %}">
<i class="fa fa-list-ul"></i> {% trans "Cost estimate" %}
</a>
{% acl_end %}
{% can_view_all Article %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
<i class="fa fa-list-ul"></i> {% trans "Available articles" %}

View file

@ -36,6 +36,7 @@ from django.template import Context
from django.http import HttpResponse
from django.conf import settings
from django.utils.text import slugify
import logging
TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-')
@ -48,8 +49,9 @@ def render_invoice(_request, ctx={}):
Render an invoice using some available information such as the current
date, the user, the articles, the prices, ...
"""
is_estimate = ctx.get('is_estimate', False)
filename = '_'.join([
'invoice',
'cost_estimate' if is_estimate else 'invoice',
slugify(ctx.get('asso_name', "")),
slugify(ctx.get('recipient_name', "")),
str(ctx.get('DATE', datetime.now()).year),
@ -93,6 +95,20 @@ def create_pdf(template, ctx={}):
return pdf
def escape_chars(string):
"""Escape the '%' and the '' signs to avoid messing with LaTeX"""
if not isinstance(string, str):
return string
mapping = (
('', r'\euro'),
('%', r'\%'),
)
r = str(string)
for k, v in mapping:
r = r.replace(k, v)
return r
def render_tex(_request, template, ctx={}):
"""Creates a PDF from a LaTex templates using pdflatex.

View file

@ -51,11 +51,41 @@ urlpatterns = [
views.facture_pdf,
name='facture-pdf'
),
url(
r'^new_cost_estimate/$',
views.new_cost_estimate,
name='new-cost-estimate'
),
url(
r'^index_cost_estimate/$',
views.index_cost_estimate,
name='index-cost-estimate'
),
url(
r'^cost_estimate_pdf/(?P<costestimateid>[0-9]+)$',
views.cost_estimate_pdf,
name='cost-estimate-pdf',
),
url(
r'^index_custom_invoice/$',
views.index_custom_invoice,
name='index-custom-invoice'
),
url(
r'^edit_cost_estimate/(?P<costestimateid>[0-9]+)$',
views.edit_cost_estimate,
name='edit-cost-estimate'
),
url(
r'^cost_estimate_to_invoice/(?P<costestimateid>[0-9]+)$',
views.cost_estimate_to_invoice,
name='cost-estimate-to-invoice'
),
url(
r'^del_cost_estimate/(?P<costestimateid>[0-9]+)$',
views.del_cost_estimate,
name='del-cost-estimate'
),
url(
r'^new_custom_invoice/$',
views.new_custom_invoice,

View file

@ -47,7 +47,10 @@ from users.models import User
from re2o.settings import LOGO_PATH
from re2o import settings
from re2o.views import form
from re2o.utils import SortTable, re2o_paginator
from re2o.base import (
SortTable,
re2o_paginator,
)
from re2o.acl import (
can_create,
can_edit,
@ -65,7 +68,8 @@ from .models import (
Paiement,
Banque,
CustomInvoice,
BaseInvoice
BaseInvoice,
CostEstimate
)
from .forms import (
FactureForm,
@ -77,9 +81,11 @@ from .forms import (
DelBanqueForm,
SelectArticleForm,
RechargeForm,
CustomInvoiceForm
CustomInvoiceForm,
DiscountForm,
CostEstimateForm,
)
from .tex import render_invoice
from .tex import render_invoice, escape_chars
from .payment_methods.forms import payment_method_factory
from .utils import find_payment_method
@ -175,7 +181,58 @@ def new_facture(request, user, userid):
)
# TODO : change facture to invoice
@login_required
@can_create(CostEstimate)
def new_cost_estimate(request):
"""
View used to generate a custom invoice. It's mainly used to
get invoices that are not taken into account, for the administrative
point of view.
"""
# The template needs the list of articles (for the JS part)
articles = Article.objects.filter(
Q(type_user='All') | Q(type_user=request.user.class_name)
)
# Building the invocie form and the article formset
cost_estimate_form = CostEstimateForm(request.POST or None)
articles_formset = formset_factory(SelectArticleForm)(
request.POST or None,
form_kwargs={'user': request.user}
)
discount_form = DiscountForm(request.POST or None)
if cost_estimate_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
cost_estimate_instance = cost_estimate_form.save()
for art_item in articles_formset:
if art_item.cleaned_data:
article = art_item.cleaned_data['article']
quantity = art_item.cleaned_data['quantity']
Vente.objects.create(
facture=cost_estimate_instance,
name=article.name,
prix=article.prix,
type_cotisation=article.type_cotisation,
duration=article.duration,
number=quantity
)
discount_form.apply_to_invoice(cost_estimate_instance)
messages.success(
request,
_("The cost estimate was created.")
)
return redirect(reverse('cotisations:index-cost-estimate'))
return form({
'factureform': cost_estimate_form,
'action_name': _("Confirm"),
'articlesformset': articles_formset,
'articlelist': articles,
'discount_form': discount_form,
'title': _("Cost estimate"),
}, 'cotisations/facture.html', request)
@login_required
@can_create(CustomInvoice)
def new_custom_invoice(request):
@ -195,8 +252,9 @@ def new_custom_invoice(request):
request.POST or None,
form_kwargs={'user': request.user}
)
discount_form = DiscountForm(request.POST or None)
if invoice_form.is_valid() and articles_formset.is_valid():
if invoice_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
new_invoice_instance = invoice_form.save()
for art_item in articles_formset:
if art_item.cleaned_data:
@ -210,6 +268,7 @@ def new_custom_invoice(request):
duration=article.duration,
number=quantity
)
discount_form.apply_to_invoice(new_invoice_instance)
messages.success(
request,
_("The custom invoice was created.")
@ -220,7 +279,8 @@ def new_custom_invoice(request):
'factureform': invoice_form,
'action_name': _("Confirm"),
'articlesformset': articles_formset,
'articlelist': articles
'articlelist': articles,
'discount_form': discount_form
}, 'cotisations/facture.html', request)
@ -263,7 +323,8 @@ def facture_pdf(request, facture, **_kwargs):
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
'payment_method': facture.paiement.moyen,
})
@ -328,6 +389,55 @@ def del_facture(request, facture, **_kwargs):
}, 'cotisations/delete.html', request)
@login_required
@can_edit(CostEstimate)
def edit_cost_estimate(request, invoice, **kwargs):
# Building the invocie form and the article formset
invoice_form = CostEstimateForm(
request.POST or None,
instance=invoice
)
purchases_objects = Vente.objects.filter(facture=invoice)
purchase_form_set = modelformset_factory(
Vente,
fields=('name', 'number'),
extra=0,
max_num=len(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()
purchase_form.save()
messages.success(
request,
_("The cost estimate was edited.")
)
return redirect(reverse('cotisations:index-cost-estimate'))
return form({
'factureform': invoice_form,
'venteform': purchase_form,
'title': "Edit the cost estimate"
}, 'cotisations/edit_facture.html', request)
@login_required
@can_edit(CostEstimate)
@can_create(CustomInvoice)
def cost_estimate_to_invoice(request, cost_estimate, **_kwargs):
"""Create a custom invoice from a cos estimate"""
cost_estimate.create_invoice()
messages.success(
request,
_("An invoice was successfully created from your cost estimate.")
)
return redirect(reverse('cotisations:index-custom-invoice'))
@login_required
@can_edit(CustomInvoice)
def edit_custom_invoice(request, invoice, **kwargs):
@ -364,10 +474,10 @@ def edit_custom_invoice(request, invoice, **kwargs):
@login_required
@can_view(CustomInvoice)
def custom_invoice_pdf(request, invoice, **_kwargs):
@can_view(CostEstimate)
def cost_estimate_pdf(request, invoice, **_kwargs):
"""
View used to generate a PDF file from an existing invoice in database
View used to generate a PDF file from an existing cost estimate in database
Creates a line for each Purchase (thus article sold) and generate the
invoice with the total price, the payment method, the address and the
legal information for the user.
@ -379,7 +489,7 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
purchases_info = []
for purchase in purchases_objects:
purchases_info.append({
'name': purchase.name,
'name': escape_chars(purchase.name),
'price': purchase.prix,
'quantity': purchase.number,
'total_price': purchase.prix_total
@ -398,11 +508,74 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
'payment_method': invoice.payment,
'remark': invoice.remark,
'end_validity': invoice.date + invoice.validity,
'is_estimate': True,
})
@login_required
@can_delete(CostEstimate)
def del_cost_estimate(request, estimate, **_kwargs):
"""
View used to delete an existing invocie.
"""
if request.method == "POST":
estimate.delete()
messages.success(
request,
_("The cost estimate was deleted.")
)
return redirect(reverse('cotisations:index-cost-estimate'))
return form({
'objet': estimate,
'objet_name': _("Cost Estimate")
}, 'cotisations/delete.html', request)
@login_required
@can_view(CustomInvoice)
def custom_invoice_pdf(request, invoice, **_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
invoice with the total price, the payment method, the address and the
legal information for the user.
"""
# TODO : change vente to purchase
purchases_objects = Vente.objects.all().filter(facture=invoice)
# Get the article list and build an list out of it
# contiaining (article_name, article_price, quantity, total_price)
purchases_info = []
for purchase in purchases_objects:
purchases_info.append({
'name': escape_chars(purchase.name),
'price': purchase.prix,
'quantity': purchase.number,
'total_price': purchase.prix_total
})
return render_invoice(request, {
'paid': invoice.paid,
'fid': invoice.id,
'DATE': invoice.date,
'recipient_name': invoice.recipient,
'address': invoice.address,
'article': purchases_info,
'total': invoice.prix_total(),
'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'),
'line2': AssoOption.get_cached_value('adresse2'),
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
'payment_method': invoice.payment,
'remark': invoice.remark,
})
# TODO : change facture to invoice
@login_required
@can_delete(CustomInvoice)
def del_custom_invoice(request, invoice, **_kwargs):
@ -753,12 +926,35 @@ def index_banque(request):
})
@login_required
@can_view_all(CustomInvoice)
def index_cost_estimate(request):
"""View used to display every custom invoice."""
pagination_number = GeneralOption.get_cached_value('pagination_number')
cost_estimate_list = CostEstimate.objects.prefetch_related('vente_set')
cost_estimate_list = SortTable.sort(
cost_estimate_list,
request.GET.get('col'),
request.GET.get('order'),
SortTable.COTISATIONS_CUSTOM
)
cost_estimate_list = re2o_paginator(
request,
cost_estimate_list,
pagination_number,
)
return render(request, 'cotisations/index_cost_estimate.html', {
'cost_estimate_list': cost_estimate_list
})
@login_required
@can_view_all(CustomInvoice)
def index_custom_invoice(request):
"""View used to display every custom invoice."""
pagination_number = GeneralOption.get_cached_value('pagination_number')
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set')
cost_estimate_ids = [i for i, in CostEstimate.objects.values_list('id')]
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set').exclude(id__in=cost_estimate_ids)
custom_invoice_list = SortTable.sort(
custom_invoice_list,
request.GET.get('col'),

View file

@ -57,14 +57,9 @@ 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
from preferences.models import RadiusOption
options, created = OptionalTopologie.objects.get_or_create()
VLAN_NOK = options.vlan_decision_nok.vlan_id
VLAN_OK = options.vlan_decision_ok.vlan_id
RADIUS_POLICY = options.radius_general_policy
#: Serveur radius de test (pas la prod)
TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False))
@ -287,6 +282,7 @@ def find_nas_from_request(nas_id):
.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.
@ -327,32 +323,50 @@ def check_user_machine_and_register(nas_type, username, mac_address):
def decide_vlan_switch(nas_machine, nas_type, port_number,
mac_address):
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
- bloq : VLAN_NOK
- force : placement sur le vlan indiqué dans la bdd
- mode strict :
- pas de chambre associée : VLAN_NOK
- pas d'utilisateur dans la chambre : VLAN_NOK
- cotisation non à jour : VLAN_NOK
- sinon passe à common (ci-dessous)
- mode common :
- interface connue (macaddress):
- utilisateur proprio non cotisant ou banni : VLAN_NOK
- user à jour : VLAN_OK
- interface inconnue :
- register mac désactivé : VLAN_NOK
- register mac activé -> redirection vers webauth
- tous les modes:
- nas inconnu: VLAN_OK
- port inconnu: Politique définie dans RadiusOption
- pas de radius sur le port: VLAN_OK
- force: placement sur le vlan indiqué dans la bdd
- mode strict:
- pas de chambre associée: Politique définie
dans RadiusOption
- pas d'utilisateur dans la chambre : Rejet
(redirection web si disponible)
- utilisateur de la chambre banni ou désactivé : Rejet
(redirection web si disponible)
- utilisateur de la chambre non cotisant et non whiteslist:
Politique définie dans RadiusOption
- sinon passe à common (ci-dessous)
- mode common :
- interface connue (macaddress):
- utilisateur proprio non cotisant / machine désactivée:
Politique définie dans RadiusOption
- utilisateur proprio banni :
Politique définie dans RadiusOption
- user à jour : VLAN_OK (réassignation de l'ipv4 au besoin)
- interface inconnue :
- register mac désactivé : Politique définie
dans RadiusOption
- register mac activé: redirection vers webauth
Returns:
tuple avec :
- Nom du switch (str)
- chambre (str)
- raison de la décision (str)
- vlan_id (int)
- decision (bool)
"""
# Get port from switch and port number
extra_log = ""
# Si le NAS est inconnu, on place sur le vlan defaut
if not nas_machine:
return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK, True)
return ('?', u'Chambre inconnue', u'Nas inconnu', RadiusOption.get_cached_value('vlan_decision_ok').vlan_id, True)
sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine)))
@ -367,7 +381,13 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
# Aucune information particulière ne permet de déterminer quelle
# politique à appliquer sur ce port
if not port:
return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK, True)
return (
sw_name,
"Chambre inconnue",
u'Port inconnu',
getattr(RadiusOption.get_cached_value('unknown_port_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('unknown_port')!= RadiusOption.REJECT
)
# On récupère le profil du port
port_profile = port.get_port_profile
@ -378,11 +398,11 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id)
extra_log = u"Force sur vlan " + str(DECISION_VLAN)
else:
DECISION_VLAN = VLAN_OK
DECISION_VLAN = RadiusOption.get_cached_value('vlan_decision_ok')
# Si le port est désactivé, on rejette sur le vlan de déconnexion
# Si le port est désactivé, on rejette la connexion
if not port.state:
return (sw_name, port.room, u'Port desactivé', VLAN_NOK, True)
return (sw_name, port.room, u'Port desactivé', None, False)
# Si radius est désactivé, on laisse passer
if port_profile.radius_type == 'NO':
@ -392,33 +412,68 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
DECISION_VLAN,
True)
# Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment
# Si le 802.1X est activé sur ce port, cela veut dire que la personne a
# été accept précédemment
# Par conséquent, on laisse passer sur le bon vlan
if nas_type.port_access_mode == '802.1X' and port_profile.radius_type == '802.1X':
if (nas_type.port_access_mode, port_profile.radius_type) == ('802.1X', '802.1X'):
room = port.room or "Chambre/local inconnu"
return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN, True)
return (
sw_name,
room,
u'Acceptation authentification 802.1X',
DECISION_VLAN,
True
)
# Sinon, cela veut dire qu'on fait de l'auth radius par mac
# Si le port est en mode strict, on vérifie que tous les users
# rattachés à ce port sont bien à jour de cotisation. Sinon on rejette (anti squattage)
# Il n'est pas possible de se connecter sur une prise strict sans adhérent à jour de cotis
# dedans
# rattachés à ce port sont bien à jour de cotisation. Sinon on rejette
# (anti squattage)
# Il n'est pas possible de se connecter sur une prise strict sans adhérent
# à jour de cotis dedans
if port_profile.radius_mode == 'STRICT':
room = port.room
if not room:
return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK, True)
return (
sw_name,
"Inconnue",
u'Chambre inconnue',
getattr(RadiusOption.get_cached_value('unknown_room_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('unknown_room')!= RadiusOption.REJECT
)
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 -> Web redirect', None, False)
return (
sw_name,
room,
u'Chambre non cotisante -> Web redirect',
None,
False
)
for user in room_user:
if not user.has_access():
return (sw_name, room, u'Chambre resident desactive -> Web redirect', None, False)
if user.is_ban() or user.state != User.STATE_ACTIVE:
return (
sw_name,
room,
u'Utilisateur banni ou désactivé -> Web redirect',
None,
False
)
elif not (user.is_connected() or user.is_whitelisted()):
return (
sw_name,
room,
u'Utilisateur non cotisant',
getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT
)
# else: user OK, on passe à la verif MAC
# Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd
# Si on fait de l'auth par mac, on cherche l'interface
# via sa mac dans la bdd
if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT':
# Authentification par mac
interface = (Interface.objects
@ -428,38 +483,67 @@ def decide_vlan_switch(nas_machine, nas_type, port_number,
.first())
if not interface:
room = port.room
# On essaye de register la mac, si l'autocapture a été activée
# Sinon on rejette sur vlan_nok
if not nas_type.autocapture_mac:
return (sw_name, "", u'Machine inconnue', VLAN_NOK, True)
# On rejette pour basculer sur du webauth
# On essaye de register la mac, si l'autocapture a été activée,
# on rejette pour faire une redirection web si possible.
if nas_type.autocapture_mac:
return (
sw_name,
room,
u'Machine Inconnue -> Web redirect',
None,
False
)
# Sinon on bascule sur la politique définie dans les options
# radius.
else:
return (sw_name, room, u'Machine Inconnue -> Web redirect', None, False)
return (
sw_name,
"",
u'Machine inconnue',
getattr(RadiusOption.get_cached_value('unknown_machine_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('unknown_machine')!= RadiusOption.REJECT
)
# L'interface a été trouvée, on vérifie qu'elle est active, sinon on reject
# L'interface a été trouvée, on vérifie qu'elle est active,
# sinon on reject
# Si elle n'a pas d'ipv4, on lui en met une
# Enfin on laisse passer sur le vlan pertinent
else:
room = port.room
if interface.machine.user.is_ban():
return (
sw_name,
room,
u'Adherent banni',
getattr(RadiusOption.get_cached_value('banned_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('banned')!= RadiusOption.REJECT
)
if not interface.is_active:
return (sw_name,
room,
u'Machine non active / adherent non cotisant',
VLAN_NOK,
True)
## Si on choisi de placer les machines sur le vlan correspondant à leur type :
if RADIUS_POLICY == 'MACHINE':
return (
sw_name,
room,
u'Machine non active / adherent non cotisant',
getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT
)
# Si on choisi de placer les machines sur le vlan
# correspondant à leur type :
if RadiusOption.get_cached_value('radius_general_policy') == 'MACHINE':
DECISION_VLAN = interface.type.ip_type.vlan.vlan_id
if not interface.ipv4:
interface.assign_ipv4()
return (sw_name,
room,
u"Ok, Reassignation de l'ipv4" + extra_log,
DECISION_VLAN,
True)
return (
sw_name,
room,
u"Ok, Reassignation de l'ipv4" + extra_log,
DECISION_VLAN,
True
)
else:
return (sw_name,
room,
u'Machine OK' + extra_log,
DECISION_VLAN,
True)
return (
sw_name,
room,
u'Machine OK' + extra_log,
DECISION_VLAN,
True
)

View file

@ -316,6 +316,10 @@ update_django() {
echo "Collecting web frontend statics ..."
python3 manage.py collectstatic --noinput
echo "Collecting web frontend statics: Done"
echo "Generating locales ..."
python3 manage.py compilemessages
echo "Generating locales: Done"
}

Binary file not shown.

View file

@ -102,15 +102,18 @@ from re2o.utils import (
all_baned,
all_has_access,
all_adherent,
all_active_assigned_interfaces_count,
all_active_interfaces_count,
)
from re2o.base import (
re2o_paginator,
SortTable
)
from re2o.acl import (
can_view_all,
can_view_app,
can_edit_history,
)
from re2o.utils import all_active_assigned_interfaces_count
from re2o.utils import all_active_interfaces_count, SortTable
@login_required

View file

@ -273,6 +273,7 @@ class ExtensionForm(FormRevMixin, ModelForm):
self.fields['origin'].label = _("A record origin")
self.fields['origin_v6'].label = _("AAAA record origin")
self.fields['soa'].label = _("SOA record to use")
self.fields['dnssec'].label = _("Sign with DNSSEC")
class DelExtensionForm(FormRevMixin, Form):

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-24 14:00
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0096_auto_20181013_1417'),
]
operations = [
migrations.AddField(
model_name='extension',
name='dnssec',
field=models.BooleanField(default=False, help_text='Should the zone be signed with DNSSEC'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2019-01-02 23:45
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0097_extension_dnssec'),
]
operations = [
migrations.AlterField(
model_name='role',
name='specific_role',
field=models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursif-server', 'Recursive DNS server'), ('dns-recursive-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'RADIUS server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gateway')], max_length=32, null=True),
),
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2019-01-02 23:45
from __future__ import unicode_literals
from django.db import migrations, models
def migrate(apps, schema_editor):
Role = apps.get_model('machines', 'Role')
for role in Role.objects.filter(specific_role='dns-recursif-server'):
role.specific_role = 'dns-recursive-server'
role.save()
class Migration(migrations.Migration):
dependencies = [
('machines', '0098_auto_20190102_1745'),
]
operations = [
migrations.RunPython(migrate),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2019-01-02 23:53
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0099_role_recursive_dns'),
]
operations = [
migrations.AlterField(
model_name='role',
name='specific_role',
field=models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursive-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'RADIUS server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gateway')], max_length=32, null=True),
),
]

View file

@ -696,6 +696,10 @@ class Extension(RevMixin, AclMixin, models.Model):
'SOA',
on_delete=models.CASCADE
)
dnssec = models.BooleanField(
default=False,
help_text=_("Should the zone be signed with DNSSEC")
)
class Meta:
permissions = (
@ -741,6 +745,9 @@ class Extension(RevMixin, AclMixin, models.Model):
.filter(cname__interface_parent__in=all_active_assigned_interfaces())
.prefetch_related('cname'))
def get_associated_dname_records(self):
return (DName.objects.filter(alias=self))
@staticmethod
def can_use_all(user_request, *_args, **_kwargs):
"""Superdroit qui permet d'utiliser toutes les extensions sans
@ -1089,7 +1096,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
.get_cached_value('ipv6_mode') == 'DHCPV6'):
return self.ipv6list.filter(slaac_ip=False)
else:
return None
return []
def mac_bare(self):
""" Formatage de la mac type mac_bare"""
@ -1373,7 +1380,10 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
.filter(interface=self.interface, slaac_ip=True)
.exclude(id=self.id)):
raise ValidationError(_("A SLAAC IP address is already registered."))
prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8')
try:
prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8')
except AttributeError: # Prevents from crashing when there is no defined prefix_v6
prefix_v6 = None
if prefix_v6:
if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] !=
IPv6Address(prefix_v6).exploded[:20]):
@ -1602,7 +1612,7 @@ class Role(RevMixin, AclMixin, models.Model):
ROLE = (
('dhcp-server', _("DHCP server")),
('switch-conf-server', _("Switches configuration server")),
('dns-recursif-server', _("Recursive DNS server")),
('dns-recursive-server', _("Recursive DNS server")),
('ntp-server', _("NTP server")),
('radius-server', _("RADIUS server")),
('log-server', _("Log server")),
@ -1631,18 +1641,6 @@ class Role(RevMixin, AclMixin, models.Model):
verbose_name = _("server role")
verbose_name_plural = _("server roles")
@classmethod
def get_instance(cls, roleid, *_args, **_kwargs):
"""Get the Role instance with roleid.
Args:
roleid: The id
Returns:
The role.
"""
return cls.objects.get(pk=roleid)
@classmethod
def interface_for_roletype(cls, roletype):
"""Return interfaces for a roletype"""
@ -1657,14 +1655,6 @@ class Role(RevMixin, AclMixin, models.Model):
machine__interface__role=cls.objects.filter(specific_role=roletype)
)
@classmethod
def get_instance(cls, roleid, *_args, **_kwargs):
"""Get the Machine instance with machineid.
:param userid: The id
:return: The user
"""
return cls.objects.get(pk=roleid)
@classmethod
def interface_for_roletype(cls, roletype):
"""Return interfaces for a roletype"""

View file

@ -38,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if ipv6_enabled %}
<th>{% trans "AAAA record origin" %}</th>
{% endif %}
<th>{% trans "DNSSEC" %}</th>
<th></th>
</tr>
</thead>
@ -50,6 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if ipv6_enabled %}
<td>{{ extension.origin_v6 }}</td>
{% endif %}
<td>{{ extension.dnssec|tick }}</td>
<td class="text-right">
{% can_edit extension %}
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}

View file

@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div class="table-responsive">
{% if machines_list.paginator %}
{% include "pagination.html" with list=machines_list %}
{% include "pagination.html" with list=machines_list go_to_id="machines" %}
{% endif %}
<table class="table" id="machines_table">
@ -215,6 +215,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</script>
{% if machines_list.paginator %}
{% include "pagination.html" with list=machines_list %}
{% include "pagination.html" with list=machines_list go_to_id="machines" %}
{% endif %}
</div>

View file

@ -95,9 +95,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if interfaceform %}
<h3>{% trans "Interface" %}</h3>
{% if i_mbf_param %}
{% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %}
{% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' mbf_param=i_mbf_param %}
{% else %}
{% massive_bootstrap_form interfaceform 'ipv4,machine' %}
{% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' %}
{% endif %}
{% endif %}
{% if domainform %}
@ -146,7 +146,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %}
{% if aliasform %}
<h3>{% trans "Alias" %}</h3>
{% bootstrap_form aliasform %}
{% massive_bootstrap_form aliasform 'extension' %}
{% endif %}
{% if serviceform %}
<h3>{% trans "Service" %}</h3>

View file

@ -55,6 +55,8 @@ from re2o.acl import (
from re2o.utils import (
all_active_assigned_interfaces,
filter_active_interfaces,
)
from re2o.base import (
SortTable,
re2o_paginator,
)

View file

@ -42,6 +42,7 @@ from .models import (
Reminder,
RadiusKey,
SwitchManagementCred,
RadiusOption,
)
from topologie.models import Switch
@ -114,11 +115,6 @@ class EditOptionalTopologieForm(ModelForm):
prefix=prefix,
**kwargs
)
self.fields['radius_general_policy'].label = _("RADIUS general policy")
self.fields['vlan_decision_ok'].label = _("VLAN for machines accepted"
" by RADIUS")
self.fields['vlan_decision_nok'].label = _("VLAN for machines rejected"
" by RADIUS")
self.initial['automatic_provision_switchs'] = Switch.objects.filter(automatic_provision=True).order_by('interface__domain__name')
@ -229,6 +225,13 @@ class EditHomeOptionForm(ModelForm):
self.fields['twitter_account_name'].label = _("Twitter account name")
class EditRadiusOptionForm(ModelForm):
"""Edition forms for Radius options"""
class Meta:
model = RadiusOption
fields = '__all__'
class ServiceForm(ModelForm):
"""Edition, ajout de services sur la page d'accueil"""
class Meta:

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-11-14 16:46
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0053_optionaluser_self_change_room'),
]
operations = [
migrations.AddField(
model_name='generaloption',
name='main_site_url',
field=models.URLField(default='http://re2o.example.org', max_length=255),
),
]

View file

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-10-13 14:29
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
('machines', '0095_auto_20180919_2225'),
('preferences', '0055_generaloption_main_site_url'),
]
operations = [
migrations.CreateModel(
name='RadiusOption',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('radius_general_policy', models.CharField(choices=[('MACHINE', "On the IP range's VLAN of the machine"), ('DEFINED', "Preset in 'VLAN for machines accepted by RADIUS'")], default='DEFINED', max_length=32)),
],
options={
'verbose_name': 'radius policies',
},
bases=(re2o.mixins.AclMixin, models.Model),
),
migrations.AddField(
model_name='radiusoption',
name='banned_vlan',
field=models.ForeignKey(blank=True, help_text='Vlan for banned if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='banned_vlan', to='machines.Vlan', verbose_name='Banned Vlan'),
),
migrations.AddField(
model_name='radiusoption',
name='non_member_vlan',
field=models.ForeignKey(blank=True, help_text='Vlan for non members if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='non_member_vlan', to='machines.Vlan', verbose_name='Non member Vlan'),
),
migrations.AddField(
model_name='radiusoption',
name='unknown_machine_vlan',
field=models.ForeignKey(blank=True, help_text='Vlan for unknown machines if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_machine_vlan', to='machines.Vlan', verbose_name='Unknown machine Vlan'),
),
migrations.AddField(
model_name='radiusoption',
name='unknown_port_vlan',
field=models.ForeignKey(blank=True, help_text='Vlan for unknown ports if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_port_vlan', to='machines.Vlan', verbose_name='Unknown port Vlan'),
),
migrations.AddField(
model_name='radiusoption',
name='unknown_room_vlan',
field=models.ForeignKey(blank=True, help_text='Vlan for unknown room if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_room_vlan', to='machines.Vlan', verbose_name='Unknown room Vlan'),
),
migrations.AddField(
model_name='radiusoption',
name='banned',
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for banned users.'),
),
migrations.AddField(
model_name='radiusoption',
name='non_member',
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy non member users.'),
),
migrations.AddField(
model_name='radiusoption',
name='unknown_machine',
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown machines'),
),
migrations.AddField(
model_name='radiusoption',
name='unknown_port',
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown machines'),
),
migrations.AddField(
model_name='radiusoption',
name='unknown_room',
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for machine connecting from unregistered room (relevant on ports with STRICT radius mode)'),
),
migrations.AddField(
model_name='radiusoption',
name='vlan_decision_ok',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_ok_option', to='machines.Vlan'),
),
]

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-10-13 14:29
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import re2o.mixins
def create_radius_policy(apps, schema_editor):
OptionalTopologie = apps.get_model('preferences', 'OptionalTopologie')
RadiusOption = apps.get_model('preferences', 'RadiusOption')
option,_ = OptionalTopologie.objects.get_or_create()
radius_option = RadiusOption()
radius_option.radius_general_policy = option.radius_general_policy
radius_option.vlan_decision_ok = option.vlan_decision_ok
radius_option.save()
def revert_radius(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('machines', '0095_auto_20180919_2225'),
('preferences', '0055_generaloption_main_site_url'),
('preferences', '0056_1_radiusoption'),
]
operations = [
migrations.RunPython(create_radius_policy, revert_radius),
]

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-10-13 14:29
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
('machines', '0095_auto_20180919_2225'),
('preferences', '0055_generaloption_main_site_url'),
('preferences', '0056_2_radiusoption'),
]
operations = [
migrations.RemoveField(
model_name='optionaltopologie',
name='radius_general_policy',
),
migrations.RemoveField(
model_name='optionaltopologie',
name='vlan_decision_nok',
),
migrations.RemoveField(
model_name='optionaltopologie',
name='vlan_decision_ok',
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-04 13:57
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0056_3_radiusoption'),
]
operations = [
migrations.AlterField(
model_name='radiusoption',
name='unknown_port',
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown port'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2019-01-05 17:15
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0056_4_radiusoption'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='all_users_active',
field=models.BooleanField(default=False, help_text='If True, all new created and connected users are active. If False, only when a valid registration has been paid'),
),
]

View file

@ -116,6 +116,11 @@ class OptionalUser(AclMixin, PreferencesModel):
default=False,
help_text=_("A new user can create their account on Re2o")
)
all_users_active = models.BooleanField(
default=False,
help_text=_("If True, all new created and connected users are active.\
If False, only when a valid registration has been paid")
)
class Meta:
permissions = (
@ -199,25 +204,6 @@ class OptionalTopologie(AclMixin, PreferencesModel):
('tftp', 'tftp'),
)
radius_general_policy = models.CharField(
max_length=32,
choices=CHOICE_RADIUS,
default='DEFINED'
)
vlan_decision_ok = models.OneToOneField(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='decision_ok',
blank=True,
null=True
)
vlan_decision_nok = models.OneToOneField(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='decision_nok',
blank=True,
null=True
)
switchs_web_management = models.BooleanField(
default=False,
help_text="Web management, activé si provision automatique"
@ -297,19 +283,19 @@ class OptionalTopologie(AclMixin, PreferencesModel):
log_servers = Role.all_interfaces_for_roletype("log-server").filter(type__ip_type=self.switchs_ip_type)
radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(type__ip_type=self.switchs_ip_type)
dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server")
dns_recursive_servers = Role.all_interfaces_for_roletype("dns-recursive-server").filter(type__ip_type=self.switchs_ip_type)
subnet = None
subnet6 = None
if self.switchs_ip_type:
subnet = self.switchs_ip_type.ip_set_full_info
subnet6 = self.switchs_ip_type.ip6_set_full_info
return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'subnet': subnet, 'subnet6': subnet6}
return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'dns_recursive_servers': return_ips_dict(dns_recursive_servers), 'subnet': subnet, 'subnet6': subnet6}
@cached_property
def provision_switchs_enabled(self):
"""Return true if all settings are ok : switchs on automatic provision,
ip_type"""
return bool(self.provisioned_switchs and self.switchs_ip_type and SwitchManagementCred.objects.filter(default_switch=True).exists() and self.switchs_management_interface_ip and bool(self.switchs_provision != 'sftp' or self.switchs_management_sftp_creds))
class Meta:
permissions = (
("view_optionaltopologie", _("Can view the topology options")),
@ -431,6 +417,7 @@ class GeneralOption(AclMixin, PreferencesModel):
req_expire_hrs = models.IntegerField(default=48)
site_name = models.CharField(max_length=32, default="Re2o")
email_from = models.EmailField(default="www-data@example.com")
main_site_url = models.URLField(max_length=255, default="http://re2o.example.org")
GTU_sum_up = models.TextField(
default="",
blank=True,
@ -587,3 +574,122 @@ class MailMessageOption(AclMixin, models.Model):
)
verbose_name = _("email message options")
class RadiusOption(AclMixin, PreferencesModel):
class Meta:
verbose_name = _("radius policies")
MACHINE = 'MACHINE'
DEFINED = 'DEFINED'
CHOICE_RADIUS = (
(MACHINE, _("On the IP range's VLAN of the machine")),
(DEFINED, _("Preset in 'VLAN for machines accepted by RADIUS'")),
)
REJECT = 'REJECT'
SET_VLAN = 'SET_VLAN'
CHOICE_POLICY = (
(REJECT, _('Reject the machine')),
(SET_VLAN, _('Place the machine on the VLAN'))
)
radius_general_policy = models.CharField(
max_length=32,
choices=CHOICE_RADIUS,
default='DEFINED'
)
unknown_machine = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
verbose_name=_("Policy for unknown machines"),
)
unknown_machine_vlan = models.ForeignKey(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='unknown_machine_vlan',
blank=True,
null=True,
verbose_name=_('Unknown machine Vlan'),
help_text=_(
'Vlan for unknown machines if not rejected.'
)
)
unknown_port = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
verbose_name=_("Policy for unknown port"),
)
unknown_port_vlan = models.ForeignKey(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='unknown_port_vlan',
blank=True,
null=True,
verbose_name=_('Unknown port Vlan'),
help_text=_(
'Vlan for unknown ports if not rejected.'
)
)
unknown_room = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
verbose_name=_(
"Policy for machine connecting from "
"unregistered room (relevant on ports with STRICT "
"radius mode)"
),
)
unknown_room_vlan = models.ForeignKey(
'machines.Vlan',
related_name='unknown_room_vlan',
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name=_('Unknown room Vlan'),
help_text=_(
'Vlan for unknown room if not rejected.'
)
)
non_member = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
verbose_name=_("Policy non member users."),
)
non_member_vlan = models.ForeignKey(
'machines.Vlan',
related_name='non_member_vlan',
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name=_('Non member Vlan'),
help_text=_(
'Vlan for non members if not rejected.'
)
)
banned = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
verbose_name=_("Policy for banned users."),
)
banned_vlan = models.ForeignKey(
'machines.Vlan',
related_name='banned_vlan',
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name=_('Banned Vlan'),
help_text=_(
'Vlan for banned if not rejected.'
)
)
vlan_decision_ok = models.OneToOneField(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='vlan_ok_option',
blank=True,
null=True
)

View file

@ -0,0 +1,96 @@
{% 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 Hugo Levy-Falk
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 %}
{% load acl %}
{% load logs_extra %}
<table>
<tr>
<th>{% trans "General policy for VLAN setting" %}</th>
<td>{{ radiusoptions.radius_general_policy }}</td>
<td>{% trans "This setting defines the VLAN policy after acceptance by RADIUS: either on the IP range's VLAN of the machine, or a VLAN preset in 'VLAN for machines accepted by RADIUS'" %}</td>
</tr>
<tr>
<th>{% trans "VLAN for machines accepted by RADIUS" %}</th>
<td><span class="label label-success">Vlan {{ radiusoptions.vlan_decision_ok }}</span></td>
</tr>
</table>
<hr/>
<table class="table table-striped">
<thead>
<tr>
<th>{% trans "Situation" %}</th>
<th>{% trans "Behavior" %}</th>
</tr>
</thead>
<tr>
<th>{% trans "Unknown machine" %}</th>
<td>
{% if radiusoptions.unknown_machine == 'REJECT' %}
<span class="label label-danger">{% trans "Reject" %}</span>
{% else %}
<span class="label label-success">Vlan {{ radiusoptions.unknown_machine_vlan }}</span>
{% endif %}
</td>
</tr>
<tr>
<th>{% trans "Unknown port" %}</th>
<td>
{% if radiusoptions.unknown_port == 'REJECT' %}
<span class="label label-danger">{% trans "Reject" %}</span>
{% else %}
<span class="label label-success">Vlan {{ radiusoptions.unknown_port_vlan }}</span>
{% endif %}
</td>
</tr>
<tr>
<th>{% trans "Unknown room" %}</th>
<td>
{% if radiusoptions.unknown_room == 'REJECT' %}
<span class="label label-danger">{% trans "Reject" %}</span>
{% else %}
<span class="label label-success">Vlan {{ radiusoptions.unknown_room_vlan }}</span>
{% endif %}
</td>
</tr>
<tr>
<th>{% trans "Non member" %}</th>
<td>
{% if radiusoptions.non_member == 'REJECT' %}
<span class="label label-danger">{% trans "Reject" %}</span>
{% else %}
<span class="label label-success">Vlan {{ radiusoptions.non_member_vlan }}</span>
{% endif %}
</td>
</tr>
<tr>
<th>{% trans "Banned user" %}</th>
<td>
{% if radiusoptions.unknown_port == 'REJECT' %}
<span class="label label-danger">{% trans "Reject" %}</span>
{% else %}
<span class="label label-success">Vlan {{ radiusoptions.banned_vlan }}</span>
{% endif %}
</td>
</tr>
</table>

View file

@ -31,293 +31,427 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "Preferences" %}{% endblock %}
{% block content %}
<h4>{% trans "User preferences" %}</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalUser' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<p>
</p>
<h5>{% trans "General preferences" %}</h5>
<table class="table table-striped">
<tr>
<th>{% trans "Creation of members by everyone" %}</th>
<td>{{ useroptions.all_can_create_adherent|tick }}</td>
<th>{% trans "Creation of clubs by everyone" %}</th>
<td>{{ useroptions.all_can_create_club|tick }}</td>
</tr>
<tr>
<th>{% trans "Self registration" %}</th>
<td>{{ useroptions.self_adhesion|tick }}</td>
<th>{% trans "Delete not yet active users after" %}</th>
<td>{{ useroptions.delete_notyetactive }} days</td>
</tr>
</table>
<div id="accordion">
<h5>{% trans "Users general permissions" %}</h5>
<table class="table table-striped">
<tr>
<th>{% trans "Default shell for users" %}</th>
<td>{{ useroptions.shell_default }}</td>
<th>{% trans "Users can edit their shell" %}</th>
<td>{{ useroptions.self_change_shell|tick }}</td>
</tr>
<tr>
<th>{% trans "Users can edit their room" %}</th>
<td>{{ useroptions.self_change_room|tick }}</td>
<th>{% trans "Telephone number required" %}</th>
<td>{{ useroptions.is_tel_mandatory|tick }}</td>
</tr>
<tr>
<th>{% trans "GPG fingerprint field" %}</th>
<td>{{ useroptions.gpg_fingerprint|tick }}</td>
</tr>
</table>
<div class="panel panel-default" id="general">
<div class="panel-heading" data-toggle="collapse" href="#collapse_general">
<h4 class="panel-title" id="general">
<a><i class="fa fa-cog"></i> {% trans "General preferences" %}</a>
</h4>
</div>
<div id="collapse_general" class="panel-collapse panel-body collapse">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'GeneralOption' %}">
<i class="fa fa-edit"></i>{% trans "Edit" %}
</a>
<p></p>
<table class="table table-striped">
<tr>
<th>{% trans "Website name" %}</th>
<td>{{ generaloptions.site_name }}</td>
<th>{% trans "Email address for automatic emailing" %}</th>
<td>{{ generaloptions.email_from }}</td>
</tr>
<tr>
<th>{% trans "Number of results displayed when searching" %}</th>
<td>{{ generaloptions.search_display_page }}</td>
<th>{% trans "Number of items per page (standard size)" %}</th>
<td>{{ generaloptions.pagination_number }}</td>
</tr>
<tr>
<th>{% trans "Number of items per page (large size)" %}</th>
<td>{{ generaloptions.pagination_large_number }}</td>
<th>{% trans "Time before expiration of the reset password link (in hours)" %}</th>
<td>{{ generaloptions.req_expire_hrs }}</td>
</tr>
<tr>
<th>{% trans "General message displayed on the website" %}</th>
<td>{{ generaloptions.general_message }}</td>
<th>{% trans "Main site url" %}</th>
<td>{{ generaloptions.main_site_url }}</td>
</tr>
<tr>
<th>{% trans "Summary of the General Terms of Use" %}</th>
<td>{{ generaloptions.GTU_sum_up }}</td>
<th>{% trans "General Terms of Use" %}</th>
<td>{{ generaloptions.GTU }}</th>
</tr>
</table>
<table class="table table-striped">
<tr>
<th>{% trans "Local email accounts enabled" %}</th>
<td>{{ useroptions.local_email_accounts_enabled|tick }}</td>
<th>{% trans "Local email domain" %}</th>
<td>{{ useroptions.local_email_domain }}</td>
</tr>
<tr>
<th>{% trans "Maximum number of email aliases allowed" %}</th>
<td>{{ useroptions.max_email_address }}</td>
</tr>
</table>
</div>
</div>
<div class="panel panel-default" id="users">
<div class="panel-heading" data-toggle="collapse" href="#collapse_users">
<h4 class="panel-title">
<a><i class="fa fa-users fa-fw"></i> {% trans "User preferences" %}</a>
</h4>
</div>
<div id="collapse_users" class="panel-collapse panel-body collapse">
<p></p>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalUser' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<p></p>
<table class="table table-striped">
<tr>
<th>{% trans "Creation of members by everyone" %}</th>
<td>{{ useroptions.all_can_create_adherent|tick }}</td>
<th>{% trans "Creation of clubs by everyone" %}</th>
<td>{{ useroptions.all_can_create_club|tick }}</td>
</tr>
<tr>
<th>{% trans "Self registration" %}</th>
<td>{{ useroptions.self_adhesion|tick }}</td>
<th>{% trans "Delete not yet active users after" %}</th>
<td>{{ useroptions.delete_notyetactive }} days</td>
</tr>
<tr>
<th>{% trans "All users are active by default" %}</th>
<td>{{ useroptions.all_users_active|tick }}</td>
</tr>
</table>
<h4 id="users">{% trans "Users general permissions" %}</h4>
<table class="table table-striped">
<tr>
<th>{% trans "Default shell for users" %}</th>
<td>{{ useroptions.shell_default }}</td>
<th>{% trans "Users can edit their shell" %}</th>
<td>{{ useroptions.self_change_shell|tick }}</td>
</tr>
<tr>
<th>{% trans "Users can edit their room" %}</th>
<td>{{ useroptions.self_change_room|tick }}</td>
<th>{% trans "Telephone number required" %}</th>
<td>{{ useroptions.is_tel_mandatory|tick }}</td>
</tr>
<tr>
<th>{% trans "GPG fingerprint field" %}</th>
<td>{{ useroptions.gpg_fingerprint|tick }}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="panel panel-default" id="machines">
<div class="panel-heading" data-toggle="collapse" href="#collapse_machines">
<h4 class ="panel-title">
<a><i class="fa fa-desktop"></i> {% trans "Machines preferences" %}</a>
</h4>
</div>
<div id="collapse_machines" class="panel-collapse panel-body collapse">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<p></p>
<table class="table table-striped">
<tr>
<th>{% trans "Password per machine" %}</th>
<td>{{ machineoptions.password_machine|tick }}</td>
<th>{% trans "Maximum number of interfaces allowed for a standard user" %}</th>
<td>{{ machineoptions.max_lambdauser_interfaces }}</td>
</tr>
<tr>
<th>{% trans "Maximum number of DNS aliases allowed for a standard user" %}</th>
<td>{{ machineoptions.max_lambdauser_aliases }}</td>
<th>{% trans "IPv6 support" %}</th>
<td>{{ machineoptions.ipv6_mode }}</td>
</tr>
<tr>
<th>{% trans "Creation of machines" %}</th>
<td>{{ machineoptions.create_machine|tick }}</td>
</tr>
</table>
</div>
</div>
<div class="panel panel-default" id="topo">
<div class="panel-heading" data-toggle="collapse" href="#collapse_topo">
<h4 class="panel-title">
<a><i class="fa fa-sitemap"></i> {% trans "Topology preferences" %}</a>
</h4>
</div>
<div id="collapse_topo" class="panel-collapse panel-body collapse">
<h5>{% trans "Email accounts preferences" %}</h5>
<table class="table table-striped">
<tr>
<th>{% trans "Local email accounts enabled" %}</th>
<td>{{ useroptions.local_email_accounts_enabled|tick }}</td>
<th>{% trans "Local email domain" %}</th>
<td>{{ useroptions.local_email_domain }}</td>
</tr>
<tr>
<th>{% trans "Maximum number of email aliases allowed" %}</th>
<td>{{ useroptions.max_email_address }}</td>
</tr>
</table>
<h4>{% trans "Machines preferences" %}</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<p>
</p>
<table class="table table-striped">
<tr>
<th>{% trans "Password per machine" %}</th>
<td>{{ machineoptions.password_machine|tick }}</td>
<th>{% trans "Maximum number of interfaces allowed for a standard user" %}</th>
<td>{{ machineoptions.max_lambdauser_interfaces }}</td>
</tr>
<tr>
<th>{% trans "Maximum number of DNS aliases allowed for a standard user" %}</th>
<td>{{ machineoptions.max_lambdauser_aliases }}</td>
<th>{% trans "IPv6 support" %}</th>
<td>{{ machineoptions.ipv6_mode }}</td>
</tr>
<tr>
<th>{% trans "Creation of machines" %}</th>
<td>{{ machineoptions.create_machine|tick }}</td>
</tr>
</table>
<h4>{% trans "Topology preferences" %}</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalTopologie' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<p>
</p>
<table class="table table-striped">
<tr>
<th>{% trans "General policy for VLAN setting" %}</th>
<td>{{ topologieoptions.radius_general_policy }}</td>
<th>{% trans "This setting defines the VLAN policy after acceptance by RADIUS: either on the IP range's VLAN of the machine, or a VLAN preset in 'VLAN for machines accepted by RADIUS'" %}</th>
<td></td>
</tr>
<tr>
<th>{% trans "VLAN for machines accepted by RADIUS" %}</th>
<td>{{ topologieoptions.vlan_decision_ok }}</td>
<th>{% trans "VLAN for machines rejected by RADIUS" %}</th>
<td>{{ topologieoptions.vlan_decision_nok }}</td>
</tr>
<tr>
<th>Placement sur ce vlan par default en cas de rejet</th>
<td>{{ topologieoptions.vlan_decision_nok }}</td>
</tr>
</table>
<p></p>
<table class="table table-striped">
<tr>
<th>{% trans "General policy for VLAN setting" %}</th>
<td>{{ topologieoptions.radius_general_policy }}</td>
<th>{% trans "This setting defines the VLAN policy after acceptance by RADIUS: either on the IP range's VLAN of the machine, or a VLAN preset in 'VLAN for machines accepted by RADIUS'" %}</th>
<td></td>
</tr>
<tr>
<th>{% trans "VLAN for machines accepted by RADIUS" %}</th>
<td>{{ topologieoptions.vlan_decision_ok }}</td>
<th>{% trans "VLAN for machines rejected by RADIUS" %}</th>
<td>{{ topologieoptions.vlan_decision_nok }}</td>
</tr>
<tr>
<th>{% trans "VLAN for non members machines" %}</th>
<td>{{ topologieoptions.vlan_non_member }}</td>
</tr>
</table>
<h6>Clef radius</h6>
{% can_create RadiusKey%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-radiuskey' %}"><i class="fa fa-plus"></i> Ajouter une clef radius</a>
{% acl_end %}
{% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %}
<h4>Clef radius</h4>
{% can_create RadiusKey%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-radiuskey' %}"><i class="fa fa-plus"></i> Ajouter une clef radius</a>
{% acl_end %}
{% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %}
<h4>Configuration des switches</h4>
<table class="table table-striped">
<tr>
<th>Web management, activé si provision automatique</th>
<td>{{ topologieoptions.switchs_web_management }}</td>
<th>Rest management, activé si provision auto</th>
<td>{{ topologieoptions.switchs_rest_management }}</td>
</tr>
</table>
</div>
</div>
<div class="panel panel-default" id="switches">
<div class="panel-heading" data-toggle="collapse" href="#collapse_switches">
<h4 class="panel-title">
<a><i class="fa fa-server"></i> Configuration des Switches</a>
</h4>
</div>
<div id="collapse_switches" class="panel-collapse panel-body collapse">
<h5>{% if topologieoptions.provision_switchs_enabled %}<span class="label label-success">Provision de la config des switchs{% else %}<span class="label label-danger">Provision de la config des switchs{% endif%}</span></h5>
<table class="table table-striped">
<tr>
<th>Switchs configurés automatiquement</th>
<td>{{ topologieoptions.provisioned_switchs|join:", " }} {% if topologieoptions.provisioned_switchs %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span></td>
</tr>
<tr>
<th>Plage d'ip de management des switchs</th>
<td>{{ topologieoptions.switchs_ip_type }} {% if topologieoptions.switchs_ip_type %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span></td>
</tr>
<tr>
<th>Serveur des config des switchs</th>
<td>{{ topologieoptions.switchs_management_interface }} {% if topologieoptions.switchs_management_interface %} - {{ topologieoptions.switchs_management_interface_ip }} <span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span></td>
</tr>
<tr>
<th>Mode de provision des switchs</th>
<td>{{ topologieoptions.switchs_provision }}</td>
</tr>
<tr>
<th>Mode TFTP</th>
<td><span class="label label-success"> OK</span></td>
</tr>
<tr>
<th>Mode SFTP</th>
<td>{% if topologieoptions.switchs_management_sftp_creds %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Creds manquants{% endif %}</span></td>
</tr>
</table>
<h6>Creds de management des switchs</h6>
{% can_create SwitchManagementCred%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-switchmanagementcred' %}"><i class="fa fa-plus"></i> Ajouter un id/mdp de management switch</a>
{% acl_end %}
<p>
</p>
{% if switchmanagementcred_list %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span>
{% include "preferences/aff_switchmanagementcred.html" with switchmanagementcred_list=switchmanagementcred_list %}
<h4>{% trans "General preferences" %}</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'GeneralOption' %}">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalTopologie' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<p>
</p>
<table class="table table-striped">
<tr>
<th>{% trans "Website name" %}</th>
<td>{{ generaloptions.site_name }}</td>
<th>{% trans "Email address for automatic emailing" %}</th>
<td>{{ generaloptions.email_from }}</td>
</tr>
<tr>
<th>{% trans "Number of results displayed when searching" %}</th>
<td>{{ generaloptions.search_display_page }}</td>
<th>{% trans "Number of items per page (standard size)" %}</th>
<td>{{ generaloptions.pagination_number }}</td>
</tr>
<tr>
<th>{% trans "Number of items per page (large size)" %}</th>
<td>{{ generaloptions.pagination_large_number }}</td>
<th>{% trans "Time before expiration of the reset password link (in hours)" %}</th>
<td>{{ generaloptions.req_expire_hrs }}</td>
</tr>
<tr>
<th>{% trans "General message displayed on the website" %}</th>
<td>{{ generaloptions.general_message }}</td>
<th>{% trans "Summary of the General Terms of Use" %}</th>
<td>{{ generaloptions.GTU_sum_up }}</td>
</tr>
<tr>
<th>{% trans "General Terms of Use" %}</th>
<td>{{ generaloptions.GTU }}</th>
</tr>
</table>
<h4>{% trans "Information about the organisation" %}</h4>
<p></p>
<table class="table table-striped">
<tr>
<th>Web management, activé si provision automatique</th>
<td>{{ topologieoptions.switchs_web_management }}</td>
<th>Rest management, activé si provision auto</th>
<td>{{ topologieoptions.switchs_rest_management }}</td>
</tr>
</table>
<h5>{% if topologieoptions.provision_switchs_enabled %}<span class="label label-success">Provision de la config des switchs{% else %}<span class="label label-danger">Provision de la config des switchs{% endif%}</span></h5>
<table class="table table-striped">
<tr>
<th>Switchs configurés automatiquement</th>
<td>{{ topologieoptions.provisioned_switchs|join:", " }} {% if topologieoptions.provisioned_switchs %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span></td>
</tr>
<tr>
<th>Plage d'ip de management des switchs</th>
<td>{{ topologieoptions.switchs_ip_type }} {% if topologieoptions.switchs_ip_type %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span></td>
</tr>
<tr>
<th>Serveur des config des switchs</th>
<td>{{ topologieoptions.switchs_management_interface }} {% if topologieoptions.switchs_management_interface %} - {{ topologieoptions.switchs_management_interface_ip }} <span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span></td>
</tr>
<tr>
<th>Mode de provision des switchs</th>
<td>{{ topologieoptions.switchs_provision }}</td>
</tr>
<tr>
<th>Mode TFTP</th>
<td><span class="label label-success"> OK</span></td>
</tr>
<tr>
<th>Mode SFTP</th>
<td>{% if topologieoptions.switchs_management_sftp_creds %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Creds manquants{% endif %}</span></td>
</tr>
</table>
<h6>Creds de management des switchs</h6>
{% can_create SwitchManagementCred%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-switchmanagementcred' %}"><i class="fa fa-plus"></i> Ajouter un id/mdp de management switch</a>
{% acl_end %}
<p>
</p>
{% if switchmanagementcred_list %}<span class="label label-success"> OK{% else %}<span class="label label-danger">Manquant{% endif %}</span>
{% include "preferences/aff_switchmanagementcred.html" with switchmanagementcred_list=switchmanagementcred_list %}
</div>
</div>
<div class="panel panel-default" id="radius">
<div class="panel-heading" data-toggle="collapse" href="#collapse_radius">
<h4 class="panel-title"><a><i class="fa fa-circle"></i> {% trans "Radius preferences" %}</h4></a>
</div>
<div id="collapse_radius" class="panel-collapse panel-body collapse">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'RadiusOption' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
{% include "preferences/aff_radiusoptions.html" %}
</div>
</div>
<div class="panel panel-default" id="asso">
<div class="panel-heading" data-toggle="collapse" href="#collapse_asso">
<h4 class="panel-title">
<a><i class="fa fa-at"></i> {% trans "Information about the organisation" %}</a>
</h4>
</div>
<div id="collapse_asso" class="panel-collapse panel-body collapse">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'AssoOption' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<p>
</p>
<table class="table table-striped">
<tr>
<th>{% trans "Name" %}</th>
<td>{{ assooptions.name }}</td>
<th>{% trans "SIRET number" %}</th>
<td>{{ assooptions.siret }}</td>
</tr>
<tr>
<th>{% trans "Address" %}</th>
<td>{{ assooptions.adresse1 }}<br>
{{ assooptions.adresse2 }}
</td>
<th>{% trans "Contact email address" %}</th>
<td>{{ assooptions.contact }}</td>
</tr>
<tr>
<th>{% trans "Telephone number" %}</th>
<td>{{ assooptions.telephone }}</td>
<th>{% trans "Usual name" %}</th>
<td>{{ assooptions.pseudo }}</td>
</tr>
<tr>
<th>{% trans "User object of the organisation" %}</th>
<td>{{ assooptions.utilisateur_asso }}</td>
<th>{% trans "Description of the organisation" %}</th>
<td>{{ assooptions.description|safe }}</td>
</tr>
</table>
<h4>{% trans "Custom email message" %}</h4>
<p></p>
<table class="table table-striped">
<tr>
<th>{% trans "Name" %}</th>
<td>{{ assooptions.name }}</td>
<th>{% trans "SIRET number" %}</th>
<td>{{ assooptions.siret }}</td>
</tr>
<tr>
<th>{% trans "Address" %}</th>
<td>{{ assooptions.adresse1 }}<br>
{{ assooptions.adresse2 }}
</td>
<th>{% trans "Contact email address" %}</th>
<td>{{ assooptions.contact }}</td>
</tr>
<tr>
<th>{% trans "Telephone number" %}</th>
<td>{{ assooptions.telephone }}</td>
<th>{% trans "Usual name" %}</th>
<td>{{ assooptions.pseudo }}</td>
</tr>
<tr>
<th>{% trans "User object of the organisation" %}</th>
<td>{{ assooptions.utilisateur_asso }}</td>
<th>{% trans "Description of the organisation" %}</th>
<td>{{ assooptions.description|safe }}</td>
</tr>
</table>
</div>
</div>
<div class="panel panel-default" id="mail">
<div class="panel-heading" data-toggle="collapse" href="#collapse_mail">
<h4 class="panel-title">
<a><i class="fa fa-comment"></i> Message pour les mails</a>
</h4>
</div>
<div id="collapse_mail" class="panel-collapse panel-body collapse">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'MailMessageOption' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<p>
</p>
<table class="table table-striped">
<tr>
<th>{% trans "Welcome email (in French)" %}</th>
<td>{{ mailmessageoptions.welcome_mail_fr|safe }}</td>
</tr>
<tr>
<th>{% trans "Welcome email (in English)" %}</th>
<td>{{ mailmessageoptions.welcome_mail_en|safe }}</td>
</tr>
</table>
<h4>Options pour le mail de fin d'adhésion</h2>
<p></p>
<table class="table table-striped">
<tr>
<th>{% trans "Welcome email (in French)" %}</th>
<td>{{ mailmessageoptions.welcome_mail_fr|safe }}</td>
</tr>
<tr>
<th>{% trans "Welcome email (in English)" %}</th>
<td>{{ mailmessageoptions.welcome_mail_en|safe }}</td>
</tr>
</table>
</div>
</div>
<div class="panel panel-default" id="rappels">
<div class="panel-heading" data-toggle="collapse" href="#collapse_rappels">
<h4 class="panel-title">
<a><i class="fa fa-bell"></i> Options pour le mail de fin d'adhésion</a>
</h4>
</div>
<div id="collapse_rappels" class="panel-collapse panel-body collapse">
{% can_create preferences.Reminder%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-reminder' %}"><i class="fa fa-plus"></i> Ajouter un rappel</a>
<p></p>
{% acl_end %}
{% include "preferences/aff_reminder.html" with reminder_list=reminder_list %}
</div>
</div>
<h4>{% trans "List of services and homepage preferences" %}</h4>
<div class="panel panel-default" id="services">
<div class="panel-heading" data-toggle="collapse" href="#collapse_services">
<h4 class="panel-title">
<a><i class="fa fa-home"></i> {% trans "List of services and homepage preferences" %}</a>
</h4>
</div>
<div id="collapse_services" class="panel-collapse panel-body collapse">
{% can_create preferences.Service%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-service' %}"><i class="fa fa-plus"></i>{% trans " Add a service" %}</a>
<p></p>
{% acl_end %}
{% include "preferences/aff_service.html" with service_list=service_list %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'HomeOption' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<h2>{% trans "List of contact email addresses" %}</h2>
</div>
</div>
<div class="panel panel-default" id="contact">
<div class="panel-heading" data-toggle="collapse" href="#collapse_contact">
<h4 class="panel-title">
<a><i class="fa fa-list-ul"></i> {% trans "List of contact email addresses" %}</a>
</h4>
</div>
<div id="collapse_contact" class="panel-collapse panel-body collapse">
{% can_create preferences.MailContact %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-mailcontact' %}"><i class="fa fa-plus"></i>{% trans "Add an address" %}</a>
{% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-mailcontact' %}"><i class="fa fa-trash"></i>{% trans "Delete one or several addresses" %}</a>
<p></p>
{% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %}
<p>
</p>
<table class="table table-striped">
<tr>
<th>{% trans "Twitter account URL" %}</th>
<td>{{ homeoptions.twitter_url }}</td>
<th>{% trans "Twitter account name" %}</th>
<td>{{ homeoptions.twitter_account_name }}</td>
</tr>
<tr>
<th>{% trans "Facebook account URL" %}</th>
<td>{{ homeoptions.facebook_url }}</td>
</tr>
</table>
</div>
</div>
<div class="panel panel-default" id="social">
<div class="panel-heading" data-toggle="collapse" href="#collapse_social">
<h4 class="panel-title">
<a><i class="fa fa-facebook"></i><i class="fa fa-twitter"></i> Réseaux sociaux</a>
</h4>
</div>
<div id="collapse_social" class="panel-collapse panel-body collapse">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'HomeOption' %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<p></p>
<table class="table table-striped">
<tr>
<th>{% trans "Twitter account URL" %}</th>
<td>{{ homeoptions.twitter_url }}</td>
<th>{% trans "Twitter account name" %}</th>
<td>{{ homeoptions.twitter_account_name }}</td>
</tr>
<tr>
<th>{% trans "Facebook account URL" %}</th>
<td>{{ homeoptions.facebook_url }}</td>
</tr>
</table>
</div>
</div>
{% endblock %}

View file

@ -37,6 +37,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %}
{% if formset %}
{{ formset.management_form }}
{% for f in formset %}
{% bootstrap_form f %}
{% endfor %}
{% endif %}
{% trans "Edit" as tr_edit %}
{% bootstrap_button tr_edit button_type="submit" icon='ok' button_class='btn-success' %}
</form>

View file

@ -22,6 +22,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.
{% endcomment %}
{% load acl %}
{% load i18n %}
{% block sidebar %}

View file

@ -66,6 +66,11 @@ urlpatterns = [
views.edit_options,
name='edit-options'
),
url(
r'^edit_options/(?P<section>RadiusOption)$',
views.edit_options,
name='edit-options'
),
url(r'^add_service/$', views.add_service, name='add-service'),
url(
r'^edit_service/(?P<serviceid>[0-9]+)$',

View file

@ -62,7 +62,8 @@ from .models import (
HomeOption,
Reminder,
RadiusKey,
SwitchManagementCred
SwitchManagementCred,
RadiusOption,
)
from . import models
from . import forms
@ -86,6 +87,7 @@ def display_options(request):
reminder_list = Reminder.objects.all()
radiuskey_list = RadiusKey.objects.all()
switchmanagementcred_list = SwitchManagementCred.objects.all()
radiusoptions, _ = RadiusOption.objects.get_or_create()
return form({
'useroptions': useroptions,
'machineoptions': machineoptions,
@ -98,7 +100,8 @@ def display_options(request):
'mailcontact_list': mailcontact_list,
'reminder_list': reminder_list,
'radiuskey_list' : radiuskey_list,
'switchmanagementcred_list': switchmanagementcred_list,
'switchmanagementcred_list': switchmanagementcred_list,
'radiusoptions' : radiusoptions,
}, 'preferences/display_preferences.html', request)
@ -134,7 +137,9 @@ def edit_options(request, section):
messages.success(request, _("The preferences were edited."))
return redirect(reverse('preferences:display-options'))
return form(
{'options': options},
{
'options': options,
},
'preferences/edit_preferences.html',
request
)

View file

@ -37,8 +37,8 @@ from django.db import models
from django import forms
from django.conf import settings
EOD = '`%EofD%`' # This should be something that will not occur in strings
EOD_asbyte = b'`%EofD%`' # This should be something that will not occur in strings
EOD = EOD_asbyte.decode('utf-8')
def genstring(length=16, chars=string.printable):
""" Generate a random string of length `length` and composed of
@ -46,23 +46,23 @@ def genstring(length=16, chars=string.printable):
return ''.join([choice(chars) for i in range(length)])
def encrypt(key, s):
""" AES Encrypt a secret `s` with the key `key` """
def encrypt(key, secret):
""" AES Encrypt a secret with the key `key` """
obj = AES.new(key)
datalength = len(s) + len(EOD)
datalength = len(secret) + len(EOD)
if datalength < 16:
saltlength = 16 - datalength
else:
saltlength = 16 - datalength % 16
ss = ''.join([s, EOD, genstring(saltlength)])
return obj.encrypt(ss)
encrypted_secret = ''.join([secret, EOD, genstring(saltlength)])
return obj.encrypt(encrypted_secret)
def decrypt(key, s):
""" AES Decrypt a secret `s` with the key `key` """
def decrypt(key, secret):
""" AES Decrypt a secret with the key `key` """
obj = AES.new(key)
ss = obj.decrypt(s)
return ss.split(bytes(EOD, 'utf-8'))[0]
uncrypted_secret = obj.decrypt(secret)
return uncrypted_secret.split(EOD_asbyte)[0]
class AESEncryptedFormField(forms.CharField):
@ -81,8 +81,7 @@ class AESEncryptedField(models.CharField):
if value is None:
return None
try:
return decrypt(settings.AES_KEY,
binascii.a2b_base64(value)).decode('utf-8')
return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8')
except Exception as e:
raise ValueError(value)
@ -90,18 +89,14 @@ class AESEncryptedField(models.CharField):
if value is None:
return value
try:
return decrypt(settings.AES_KEY,
binascii.a2b_base64(value)).decode('utf-8')
return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8')
except Exception as e:
raise ValueError(value)
def get_prep_value(self, value):
if value is None:
return value
return binascii.b2a_base64(encrypt(
settings.AES_KEY,
value
)).decode('utf-8')
return binascii.b2a_base64(encrypt(settings.AES_KEY, value)).decode('utf-8')
def formfield(self, **kwargs):
defaults = {'form_class': AESEncryptedFormField}

267
re2o/base.py Normal file
View file

@ -0,0 +1,267 @@
# -*- 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 © 2018 Gabriel Détraz
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# -*- coding: utf-8 -*-
"""
Regroupe les fonctions transversales utiles
Et non corrélées/dépendantes des autres applications
"""
import smtplib
from django.utils.translation import ugettext_lazy as _
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from re2o.settings import EMAIL_HOST
# 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 smtp_check(local_part):
"""Return True if the local_part is already taken
False if available"""
try:
srv = smtplib.SMTP(EMAIL_HOST)
srv.putcmd("vrfy", local_part)
reply_code = srv.getreply()[0]
srv.close()
if reply_code in [250, 252]:
return True, _("This domain is already taken")
except:
return True, _("Smtp unreachable")
return False, None
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 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
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
values and associated model_fields """
# All the possible possible values
# The naming convention is based on the URL or the views function
# The syntax to describe the sort to apply is a dict where the keys are
# 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.
USERS_INDEX = {
'user_name': ['name'],
'user_surname': ['surname'],
'user_pseudo': ['pseudo'],
'user_room': ['room'],
'default': ['state', 'pseudo']
}
USERS_INDEX_BAN = {
'ban_user': ['user__pseudo'],
'ban_start': ['date_start'],
'ban_end': ['date_end'],
'default': ['-date_end']
}
USERS_INDEX_WHITE = {
'white_user': ['user__pseudo'],
'white_start': ['date_start'],
'white_end': ['date_end'],
'default': ['-date_end']
}
USERS_INDEX_SCHOOL = {
'school_name': ['name'],
'default': ['name']
}
MACHINES_INDEX = {
'machine_name': ['name'],
'default': ['pk']
}
COTISATIONS_INDEX = {
'cotis_user': ['user__pseudo'],
'cotis_paiement': ['paiement__moyen'],
'cotis_date': ['date'],
'cotis_id': ['id'],
'default': ['-date']
}
COTISATIONS_CUSTOM = {
'invoice_date': ['date'],
'invoice_id': ['id'],
'invoice_recipient': ['recipient'],
'invoice_address': ['address'],
'invoice_payment': ['payment'],
'default': ['-date']
}
COTISATIONS_CONTROL = {
'control_name': ['user__adherent__name'],
'control_surname': ['user__surname'],
'control_paiement': ['paiement'],
'control_date': ['date'],
'control_valid': ['valid'],
'control_control': ['control'],
'control_id': ['id'],
'control_user-id': ['user__id'],
'default': ['-date']
}
TOPOLOGIE_INDEX = {
'switch_dns': ['interface__domain__name'],
'switch_ip': ['interface__ipv4__ipv4'],
'switch_loc': ['switchbay__name'],
'switch_ports': ['number'],
'switch_stack': ['stack__name'],
'default': ['switchbay', 'stack', 'stack_member_id']
}
TOPOLOGIE_INDEX_PORT = {
'port_port': ['port'],
'port_room': ['room__name'],
'port_interface': ['machine_interface__domain__name'],
'port_related': ['related__switch__name'],
'port_radius': ['radius'],
'port_vlan': ['vlan_force__name'],
'default': ['port']
}
TOPOLOGIE_INDEX_ROOM = {
'room_name': ['name'],
'default': ['name']
}
TOPOLOGIE_INDEX_BUILDING = {
'building_name': ['name'],
'default': ['name']
}
TOPOLOGIE_INDEX_BORNE = {
'ap_name': ['interface__domain__name'],
'ap_ip': ['interface__ipv4__ipv4'],
'ap_mac': ['interface__mac_address'],
'default': ['interface__domain__name']
}
TOPOLOGIE_INDEX_STACK = {
'stack_name': ['name'],
'stack_id': ['stack_id'],
'default': ['stack_id'],
}
TOPOLOGIE_INDEX_MODEL_SWITCH = {
'model-switch_name': ['reference'],
'model-switch_contructor': ['constructor__name'],
'default': ['reference'],
}
TOPOLOGIE_INDEX_SWITCH_BAY = {
'switch-bay_name': ['name'],
'switch-bay_building': ['building__name'],
'default': ['name'],
}
TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = {
'constructor-switch_name': ['name'],
'default': ['name'],
}
LOGS_INDEX = {
'sum_date': ['revision__date_created'],
'default': ['-revision__date_created'],
}
LOGS_STATS_LOGS = {
'logs_author': ['user__name'],
'logs_date': ['date_created'],
'default': ['-date_created']
}
@staticmethod
def sort(request, col, order, values):
""" Check if the given values are possible and add .order_by() and
a .reverse() as specified according to those values """
fields = values.get(col, None)
if not fields:
fields = values.get('default', [])
request = request.order_by(*fields)
if values.get(col, None) and order == 'desc':
return request.reverse()
else:
return request
def re2o_paginator(request, query_set, pagination_number):
"""Paginator script for list display in re2o.
:request:
:query_set: Query_set to paginate
:pagination_number: Number of entries to display"""
paginator = Paginator(query_set, pagination_number)
page = request.GET.get('page')
try:
results = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
results = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
results = paginator.page(paginator.num_pages)
return results

Binary file not shown.

View file

@ -114,9 +114,9 @@ class CryptPasswordHasher(hashers.BasePasswordHasher):
Check password against encoded using CRYPT algorithm
"""
assert encoded.startswith(self.algorithm)
salt = hash_password_salt(challenge_password)
return constant_time_compare(crypt.crypt(password.encode(), salt),
challenge.encode())
salt = hash_password_salt(encoded)
return constant_time_compare(crypt.crypt(password, salt),
encoded)
def safe_summary(self, encoded):
"""

View file

@ -38,55 +38,11 @@ from __future__ import unicode_literals
from django.utils import timezone
from django.db.models import Q
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
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
from preferences.models import AssoOption
def all_adherent(search_time=None):
""" Fonction renvoyant tous les users adherents. Optimisee pour n'est
@ -103,7 +59,7 @@ def all_adherent(search_time=None):
vente__in=Vente.objects.filter(
facture__in=Facture.objects.all().exclude(valid=False)
)
).filter(date_end__gt=search_time)
).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))
)
)
).distinct()
@ -115,7 +71,7 @@ def all_baned(search_time=None):
search_time = timezone.now()
return User.objects.filter(
ban__in=Ban.objects.filter(
date_end__gt=search_time
Q(date_start__lt=search_time) & Q(date_end__gt=search_time)
)
).distinct()
@ -126,20 +82,23 @@ def all_whitelisted(search_time=None):
search_time = timezone.now()
return User.objects.filter(
whitelist__in=Whitelist.objects.filter(
date_end__gt=search_time
Q(date_start__lt=search_time) & Q(date_end__gt=search_time)
)
).distinct()
def all_has_access(search_time=None):
""" Renvoie tous les users beneficiant d'une connexion
: user adherent ou whiteliste et non banni """
""" Return all connected users : active users and whitelisted +
asso_user defined in AssoOption pannel
----
Renvoie tous les users beneficiant d'une connexion
: user adherent et whiteliste non banni plus l'utilisateur asso"""
if search_time is None:
search_time = timezone.now()
return User.objects.filter(
filter_user = (
Q(state=User.STATE_ACTIVE) &
~Q(ban__in=Ban.objects.filter(date_end__gt=search_time)) &
(Q(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)) |
~Q(ban__in=Ban.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) &
(Q(whitelist__in=Whitelist.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) |
Q(facture__in=Facture.objects.filter(
vente__in=Vente.objects.filter(
cotisation__in=Cotisation.objects.filter(
@ -148,10 +107,14 @@ def all_has_access(search_time=None):
facture__in=Facture.objects.all()
.exclude(valid=False)
)
).filter(date_end__gt=search_time)
).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))
)
)))
).distinct()
)
asso_user = AssoOption.get_cached_value('utilisateur_asso')
if asso_user:
filter_user |= Q(id=asso_user.id)
return User.objects.filter(filter_user).distinct()
def filter_active_interfaces(interface_set):
@ -203,164 +166,6 @@ def all_active_assigned_interfaces_count():
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
values and associated model_fields """
# All the possible possible values
# The naming convention is based on the URL or the views function
# The syntax to describe the sort to apply is a dict where the keys are
# 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.
USERS_INDEX = {
'user_name': ['name'],
'user_surname': ['surname'],
'user_pseudo': ['pseudo'],
'user_room': ['room'],
'default': ['state', 'pseudo']
}
USERS_INDEX_BAN = {
'ban_user': ['user__pseudo'],
'ban_start': ['date_start'],
'ban_end': ['date_end'],
'default': ['-date_end']
}
USERS_INDEX_WHITE = {
'white_user': ['user__pseudo'],
'white_start': ['date_start'],
'white_end': ['date_end'],
'default': ['-date_end']
}
USERS_INDEX_SCHOOL = {
'school_name': ['name'],
'default': ['name']
}
MACHINES_INDEX = {
'machine_name': ['name'],
'default': ['pk']
}
COTISATIONS_INDEX = {
'cotis_user': ['user__pseudo'],
'cotis_paiement': ['paiement__moyen'],
'cotis_date': ['date'],
'cotis_id': ['id'],
'default': ['-date']
}
COTISATIONS_CUSTOM = {
'invoice_date': ['date'],
'invoice_id': ['id'],
'invoice_recipient': ['recipient'],
'invoice_address': ['address'],
'invoice_payment': ['payment'],
'default': ['-date']
}
COTISATIONS_CONTROL = {
'control_name': ['user__adherent__name'],
'control_surname': ['user__surname'],
'control_paiement': ['paiement'],
'control_date': ['date'],
'control_valid': ['valid'],
'control_control': ['control'],
'control_id': ['id'],
'control_user-id': ['user__id'],
'default': ['-date']
}
TOPOLOGIE_INDEX = {
'switch_dns': ['interface__domain__name'],
'switch_ip': ['interface__ipv4__ipv4'],
'switch_loc': ['switchbay__name'],
'switch_ports': ['number'],
'switch_stack': ['stack__name'],
'default': ['switchbay', 'stack', 'stack_member_id']
}
TOPOLOGIE_INDEX_PORT = {
'port_port': ['port'],
'port_room': ['room__name'],
'port_interface': ['machine_interface__domain__name'],
'port_related': ['related__switch__name'],
'port_radius': ['radius'],
'port_vlan': ['vlan_force__name'],
'default': ['port']
}
TOPOLOGIE_INDEX_ROOM = {
'room_name': ['name'],
'default': ['name']
}
TOPOLOGIE_INDEX_BUILDING = {
'building_name': ['name'],
'default': ['name']
}
TOPOLOGIE_INDEX_BORNE = {
'ap_name': ['interface__domain__name'],
'ap_ip': ['interface__ipv4__ipv4'],
'ap_mac': ['interface__mac_address'],
'default': ['interface__domain__name']
}
TOPOLOGIE_INDEX_STACK = {
'stack_name': ['name'],
'stack_id': ['stack_id'],
'default': ['stack_id'],
}
TOPOLOGIE_INDEX_MODEL_SWITCH = {
'model-switch_name': ['reference'],
'model-switch_contructor': ['constructor__name'],
'default': ['reference'],
}
TOPOLOGIE_INDEX_SWITCH_BAY = {
'switch-bay_name': ['name'],
'switch-bay_building': ['building__name'],
'default': ['name'],
}
TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = {
'constructor-switch_name': ['name'],
'default': ['name'],
}
LOGS_INDEX = {
'sum_date': ['revision__date_created'],
'default': ['-revision__date_created'],
}
LOGS_STATS_LOGS = {
'logs_author': ['user__name'],
'logs_date': ['date_created'],
'default': ['-date_created']
}
@staticmethod
def sort(request, col, order, values):
""" Check if the given values are possible and add .order_by() and
a .reverse() as specified according to those values """
fields = values.get(col, None)
if not fields:
fields = values.get('default', [])
request = request.order_by(*fields)
if values.get(col, None) and order == 'desc':
return request.reverse()
else:
return request
def re2o_paginator(request, query_set, pagination_number):
"""Paginator script for list display in re2o.
:request:
:query_set: Query_set to paginate
:pagination_number: Number of entries to display"""
paginator = Paginator(query_set, pagination_number)
page = request.GET.get('page')
try:
results = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
results = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
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:
@ -370,18 +175,3 @@ def remove_user_room(room):
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

@ -27,7 +27,7 @@ from __future__ import unicode_literals
from django import forms
from django.forms import Form
from django.utils.translation import ugettext_lazy as _
from re2o.utils import get_input_formats_help_text
from re2o.base import get_input_formats_help_text
CHOICES_USER = (
('0', _("Active")),

View file

@ -46,7 +46,7 @@ from search.forms import (
CHOICES_AFF,
initial_choices
)
from re2o.utils import SortTable
from re2o.base import SortTable
from re2o.acl import can_view_all

View file

@ -79,19 +79,6 @@ a > i.fa {
vertical-align: middle;
}
/* Pull sidebars to the bottom */
@media (min-width: 767px) {
.row {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.row > [class*='col-'] {
flex-direction: column;
}
}
/* On small screens, set height to 'auto' for sidenav and grid */
@media screen and (max-width: 767px) {
.sidenav {
@ -102,7 +89,7 @@ a > i.fa {
}
.table-responsive {
overflow-y: visible;
overflow: visible;
}
/* Make modal wider on wide screens */
@ -145,3 +132,14 @@ th.long_text{
.dashboard{
text-align: center;
}
/* Detailed information on profile page */
dl.profile-info {
margin-top: -16px;
margin-bottom: 0;
}
dl.profile-info > div {
padding: 8px;
border-top: 1px solid #ddd;
}

View file

@ -0,0 +1,33 @@
// 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 Alexandre Iooss
//
// 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.
// This script makes URL hash controls Bootstrap collapse
// e.g. if there is #information in the URL
// then the collapse with id "information" will be open.
$(document).ready(function () {
if(location.hash != null && location.hash !== ""){
// Open the collapse corresponding to URL hash
$(location.hash + '.collapse').collapse('show');
} else {
// Open default collapse
$('.collapse-default.collapse').collapse('show');
}
});

View file

@ -45,6 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% bootstrap_javascript %}
<script src="/static/js/typeahead/typeahead.js"></script>
<script src="/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js"></script>
<script src="{% static 'js/collapse-from-url.js' %}"></script>
{# Load CSS #}
{% bootstrap_css %}

View file

@ -23,23 +23,52 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load url_insert_param %}
{% load i18n %}
{% 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>
<li><a href="{% url_insert_param request.get_full_path page=list.previous_page_number %}"> < </a></li>
{% endif %}
{% for page in list.paginator.page_range %}
{% if list.number <= page|add:"3" and list.number >= page|add:"-3" %}
<li class="{% if list.number == page %}active{% endif %}"><a href="{% url_insert_param request.get_full_path page=page %}">{{ page }}</a></li>
<ul class="pagination text-center">
{% if list.has_previous %}
<li>
<a href="{% url_insert_param request.get_full_path page=1 %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">{% trans "First" %}</span>
</a>
</li>
<li>
<a href="{% url_insert_param request.get_full_path page=list.previous_page_number %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">
<span aria-hidden="true">&lsaquo;</span>
<span class="sr-only">{% trans "Previous" %}</span>
</a>
</li>
{% else %}
<li class="disabled"><span aria-hidden="true">&laquo;</span></li>
<li class="disabled"><span aria-hidden="true">&lsaquo;</span></li>
{% endif %}
{% endfor %}
{% if list.has_next %}
<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>
{% for page in list.paginator.page_range %}
{% if list.number <= page|add:"3" and list.number >= page|add:"-3" %}
<li class="{% if list.number == page %}active{% endif %}">
<a href="{% url_insert_param request.get_full_path page=page %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">{{ page }}</a>
</li>
{% endif %}
{% endfor %}
{% if list.has_next %}
<li>
<a href="{% url_insert_param request.get_full_path page=list.next_page_number %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">
<span aria-hidden="true">&rsaquo;</span>
<span class="sr-only">{% trans "Next" %}</span>
</a>
</li>
<li>
<a href="{% url_insert_param request.get_full_path page=list.paginator.page_range|length %}{% if go_to_id %}#{{ go_to_id }}{% endif %}">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">{% trans "Last" %}</span>
</a>
</li>
{% else %}
<li class="disabled"><span aria-hidden="true">&rsaquo;</span></li>
<li class="disabled"><span aria-hidden="true">&raquo;</span></li>
{% endif %}
</ul>
{% endif %}

View file

@ -55,6 +55,8 @@ from .models import (
SwitchBay,
Building,
PortProfile,
ModuleSwitch,
ModuleOnSwitch,
)
@ -269,3 +271,23 @@ class EditPortProfileForm(FormRevMixin, ModelForm):
prefix=prefix,
**kwargs)
class EditModuleForm(FormRevMixin, ModelForm):
"""Add and edit module instance"""
class Meta:
model = ModuleSwitch
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditModuleForm, self).__init__(*args, prefix=prefix, **kwargs)
class EditSwitchModuleForm(FormRevMixin, ModelForm):
"""Add/edit a switch to a module"""
class Meta:
model = ModuleOnSwitch
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditSwitchModuleForm, self).__init__(*args, prefix=prefix, **kwargs)

View file

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-30 17:19
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
('topologie', '0066_modelswitch_commercial_name'),
]
operations = [
migrations.CreateModel(
name='ModuleOnSwitch',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slot', models.CharField(help_text='Slot on switch', max_length=15, verbose_name='Slot')),
],
options={
'verbose_name': 'link between switchs and modules',
'permissions': (('view_moduleonswitch', 'Can view a moduleonswitch object'),),
},
bases=(re2o.mixins.AclMixin, re2o.mixins.RevMixin, models.Model),
),
migrations.CreateModel(
name='ModuleSwitch',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reference', models.CharField(help_text='Reference of a module', max_length=255, verbose_name='Module reference')),
('comment', models.CharField(blank=True, help_text='Comment', max_length=255, null=True, verbose_name='Comment')),
],
options={
'verbose_name': 'Module of a switch',
'permissions': (('view_moduleswitch', 'Can view a module object'),),
},
bases=(re2o.mixins.AclMixin, re2o.mixins.RevMixin, models.Model),
),
migrations.AddField(
model_name='modelswitch',
name='is_itself_module',
field=models.BooleanField(default=False, help_text='Does the switch, itself, considered as a module'),
),
migrations.AddField(
model_name='modelswitch',
name='is_modular',
field=models.BooleanField(default=False, help_text='Is this switch model modular'),
),
migrations.AddField(
model_name='moduleonswitch',
name='module',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='topologie.ModuleSwitch'),
),
migrations.AddField(
model_name='moduleonswitch',
name='switch',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='topologie.Switch'),
),
migrations.AlterUniqueTogether(
name='moduleonswitch',
unique_together=set([('slot', 'switch')]),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2019-01-02 23:58
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('topologie', '0067_auto_20181230_1819'),
]
operations = [
migrations.AlterField(
model_name='modelswitch',
name='is_itself_module',
field=models.BooleanField(default=False, help_text='Is the switch, itself, considered as a module'),
),
]

View file

@ -252,6 +252,7 @@ class Switch(AclMixin, Machine):
help_text='Provision automatique de ce switch',
)
class Meta:
unique_together = ('stack', 'stack_member_id')
permissions = (
@ -281,31 +282,18 @@ class Switch(AclMixin, Machine):
def create_ports(self, begin, end):
""" 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()
if nb_ports > 0:
ports = self.ports.order_by('port').values('port')
s_begin = ports.first().get('port')
s_end = ports.last().get('port')
if end < begin:
raise ValidationError(_("The end port is less than the start"
" port."))
if end - begin > self.number:
ports_to_create = range(begin, end + 1)
existing_ports = Port.objects.filter(switch=self.switch).values_list('port', flat=True)
non_existing_ports = list(set(ports_to_create) - set(existing_ports))
if len(non_existing_ports) + existing_ports.count() > self.number:
raise ValidationError(_("This switch can't have that many ports."))
begin_range = range(begin, s_begin)
end_range = range(s_end+1, end+1)
for i in itertools.chain(begin_range, end_range):
port = Port()
port.switch = self
port.port = i
try:
with transaction.atomic(), reversion.create_revision():
port.save()
reversion.set_comment(_("Creation"))
except IntegrityError:
ValidationError(_("Creation of an existing port."))
with transaction.atomic(), reversion.create_revision():
reversion.set_comment(_("Creation"))
Port.objects.bulk_create([Port(switch=self.switch, port=port_id) for port_id in non_existing_ports])
def main_interface(self):
""" Returns the 'main' interface of the switch
@ -317,7 +305,7 @@ class Switch(AclMixin, Machine):
@cached_property
def get_name(self):
return self.name or self.main_interface().domain.name
return self.name or getattr(self.main_interface(), 'domain', 'Unknown')
@cached_property
def get_radius_key(self):
@ -380,6 +368,17 @@ class Switch(AclMixin, Machine):
"""Return dict ip6:subnet for all ipv6 of the switch"""
return dict((str(interface.ipv6().first()), interface.type.ip_type.ip6_set_full_info) for interface in self.interface_set.all())
@cached_property
def list_modules(self):
"""Return modules of that switch, list of dict (rank, reference)"""
modules = []
if getattr(self.model, 'is_modular', None):
if self.model.is_itself_module:
modules.append((1, self.model.reference))
for module_of_self in self.moduleonswitch_set.all():
modules.append((module_of_self.slot, module_of_self.module.reference))
return modules
def __str__(self):
return str(self.get_name)
@ -402,6 +401,14 @@ class ModelSwitch(AclMixin, RevMixin, models.Model):
null=True,
blank=True
)
is_modular = models.BooleanField(
default=False,
help_text=_("Is this switch model modular"),
)
is_itself_module = models.BooleanField(
default=False,
help_text=_("Is the switch, itself, considered as a module"),
)
class Meta:
permissions = (
@ -417,6 +424,53 @@ class ModelSwitch(AclMixin, RevMixin, models.Model):
return str(self.constructor) + ' ' + self.reference
class ModuleSwitch(AclMixin, RevMixin, models.Model):
"""A module of a switch"""
reference = models.CharField(
max_length=255,
help_text=_("Reference of a module"),
verbose_name=_("Module reference")
)
comment = models.CharField(
max_length=255,
null=True,
blank=True,
help_text=_("Comment"),
verbose_name=_("Comment")
)
class Meta:
permissions = (
("view_moduleswitch", _("Can view a module object")),
)
verbose_name = _("Module of a switch")
def __str__(self):
return str(self.reference)
class ModuleOnSwitch(AclMixin, RevMixin, models.Model):
"""Link beetween module and switch"""
module = models.ForeignKey('ModuleSwitch', on_delete=models.CASCADE)
switch = models.ForeignKey('Switch', on_delete=models.CASCADE)
slot = models.CharField(
max_length=15,
help_text=_("Slot on switch"),
verbose_name=_("Slot")
)
class Meta:
permissions = (
("view_moduleonswitch", _("Can view a moduleonswitch object")),
)
verbose_name = _("link between switchs and modules")
unique_together = ['slot', 'switch']
def __str__(self):
return 'On slot ' + str(self.slot) + ' of ' + str(self.switch)
class ConstructorSwitch(AclMixin, RevMixin, models.Model):
"""Un constructeur de switch"""

View file

@ -0,0 +1,110 @@
{% 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 %}
{% load logs_extra %}
{% load i18n %}
{% if module_list.paginator %}
{% include "pagination.html" with list=module_list %}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>{% trans "Reference" %}</th>
<th>{% trans "Comment" %}</th>
<th>{% trans "Switchs" %}</th>
<th></th>
</tr>
</thead>
{% for module in module_list %}
<tr>
<td>{{ module.reference }}</td>
<td>{{ module.comment }}</td>
<td>
{% for module_switch in module.moduleonswitch_set.all %}
<b>Slot</b> {{ module_switch.slot }} <b>of</b> {{ module_switch.switch }}
{% can_edit module_switch %}
<a class="btn btn-primary btn-xs" role="button" title={% trans "Edit" %} href="{% url 'topologie:edit-module-on' module_switch.id %}">
<i class="fa fa-edit"></i>
</a>
{% acl_end %}
{% can_delete module_switch %}
<a class="btn btn-danger btn-xs" role="button" title={% trans "Delete" %} href="{% url 'topologie:del-module-on' module_switch.id %}">
<i class="fa fa-trash"></i>
</a>
{% acl_end %}
<br>
{% endfor %}
</td>
<td class="text-right">
{% can_edit module %}
<a class="btn btn-primary btn-sm" role="button" title={% trans "Add" %} href="{% url 'topologie:add-module-on' %}">
<i class="fa fa-plus"></i>
</a>
<a class="btn btn-primary btn-sm" role="button" title={% trans "Edit" %} href="{% url 'topologie:edit-module' module.id %}">
<i class="fa fa-edit"></i>
</a>
{% acl_end %}
{% history_button module %}
{% can_delete module %}
<a class="btn btn-danger btn-sm" role="button" title={% trans "Delete" %} href="{% url 'topologie:del-module' module.id %}">
<i class="fa fa-trash"></i>
</a>
{% acl_end %}
</td>
</tr>
{% endfor %}
</table>
{% if module_list.paginator %}
{% include "pagination.html" with list=module_list %}
{% endif %}
<h4>{% trans "All modular switchs" %}</h4>
<table class="table table-striped">
<thead>
<th>{% trans "Switch" %}</th>
<th>{% trans "Reference" %}</th>
<th>{% trans "Slot" %}</th>
<tbody>
{% for switch in modular_switchs %}
{% if switch.list_modules %}
<tr class="info">
<td colspan="4">
{{ switch }}
</td>
</tr>
{% for module in switch.list_modules %}
<tr>
<td></td>
<td>{{ module.1 }}</td>
<td>{{ module.0 }}</td>
</tr>
{% endfor %}
{% endif %}
{% endfor %}
</table>

View file

@ -0,0 +1,43 @@
{% extends "topologie/sidebar.html" %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load bootstrap3 %}
{% load acl %}
{% load i18n %}
{% block title %}{% trans "Topology" %}{% endblock %}
{% block content %}
<h2>{% trans "Modules of switchs" %}</h2>
{% can_create ModuleSwitch %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'topologie:add-module' %}"><i class="fa fa-plus"></i>{% trans " Add a module" %}</a>
<hr>
{% acl_end %}
{% include "topologie/aff_modules.html" with module_list=module_list modular_switchs=modular_switchs %}
<br />
<br />
<br />
{% endblock %}

View file

@ -33,6 +33,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="list-group-item list-group-item-info" href="{% url "topologie:index" %}">
<i class="fa fa-microchip"></i>
{% trans "Switches" %}
</a>
<a class="list-group-item list-group-item-info" href="{% url "topologie:index-module" %}">
<i class="fa fa-microchip"></i>
{% trans "Switches modules" %}
</a>
<a class="list-group-item list-group-item-info" href="{% url "topologie:index-port-profile" %}">
<i class="fa fa-cogs"></i>

View file

@ -37,7 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %}
<form class="form" method="post">
{% csrf_token %}
{% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged' %}
{% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged,switch' %}
{% bootstrap_button action_name icon='ok' button_class='btn-success' %}
</form>
<br />

View file

@ -123,4 +123,15 @@ urlpatterns = [
url(r'^edit_vlanoptions/(?P<vlanid>[0-9]+)$',
views.edit_vlanoptions,
name='edit-vlanoptions'),
]
url(r'^add_module/$', views.add_module, name='add-module'),
url(r'^edit_module/(?P<moduleswitchid>[0-9]+)$',
views.edit_module,
name='edit-module'),
url(r'^del_module/(?P<moduleswitchid>[0-9]+)$', views.del_module, name='del-module'),
url(r'^index_module/$', views.index_module, name='index-module'),
url(r'^add_module_on/$', views.add_module_on, name='add-module-on'),
url(r'^edit_module_on/(?P<moduleonswitchid>[0-9]+)$',
views.edit_module_on,
name='edit-module-on'),
url(r'^del_module_on/(?P<moduleonswitchid>[0-9]+)$', views.del_module_on, name='del-module-on'),
]

View file

@ -48,7 +48,10 @@ from django.utils.translation import ugettext as _
import tempfile
from users.views import form
from re2o.utils import re2o_paginator, SortTable
from re2o.base import (
re2o_paginator,
SortTable,
)
from re2o.acl import (
can_create,
can_edit,
@ -83,6 +86,8 @@ from .models import (
Building,
Server,
PortProfile,
ModuleSwitch,
ModuleOnSwitch,
)
from .forms import (
EditPortForm,
@ -99,6 +104,8 @@ from .forms import (
EditSwitchBayForm,
EditBuildingForm,
EditPortProfileForm,
EditModuleForm,
EditSwitchModuleForm,
)
from subprocess import (
@ -313,6 +320,22 @@ def index_model_switch(request):
)
@login_required
@can_view_all(ModuleSwitch)
def index_module(request):
"""Display all modules of switchs"""
module_list = ModuleSwitch.objects.all()
modular_switchs = Switch.objects.filter(model__is_modular=True)
pagination_number = GeneralOption.get_cached_value('pagination_number')
module_list = re2o_paginator(request, module_list, pagination_number)
return render(
request,
'topologie/index_module.html',
{'module_list': module_list,
'modular_switchs': modular_switchs}
)
@login_required
@can_edit(Vlan)
def edit_vlanoptions(request, vlan_instance, **_kwargs):
@ -530,19 +553,13 @@ def create_ports(request, switchid):
except Switch.DoesNotExist:
messages.error(request, _("Nonexistent switch"))
return redirect(reverse('topologie:index'))
s_begin = s_end = 0
nb_ports = switch.ports.count()
if nb_ports > 0:
ports = switch.ports.order_by('port').values('port')
s_begin = ports.first().get('port')
s_end = ports.last().get('port')
first_port = getattr(switch.ports.order_by('port').first(), 'port', 1)
last_port = switch.number + first_port - 1
port_form = CreatePortsForm(
request.POST or None,
initial={'begin': s_begin, 'end': s_end}
initial={'begin': first_port, 'end': last_port}
)
if port_form.is_valid():
begin = port_form.cleaned_data['begin']
end = port_form.cleaned_data['end']
@ -1051,6 +1068,115 @@ def del_port_profile(request, port_profile, **_kwargs):
)
@login_required
@can_create(ModuleSwitch)
def add_module(request):
""" View used to add a Module object """
module = EditModuleForm(request.POST or None)
if module.is_valid():
module.save()
messages.success(request, _("The module was created."))
return redirect(reverse('topologie:index-module'))
return form(
{'topoform': module, 'action_name': _("Create a module")},
'topologie/topo.html',
request
)
@login_required
@can_edit(ModuleSwitch)
def edit_module(request, module_instance, **_kwargs):
""" View used to edit a Module object """
module = EditModuleForm(request.POST or None, instance=module_instance)
if module.is_valid():
if module.changed_data:
module.save()
messages.success(request, _("The module was edited."))
return redirect(reverse('topologie:index-module'))
return form(
{'topoform': module, 'action_name': _("Edit")},
'topologie/topo.html',
request
)
@login_required
@can_delete(ModuleSwitch)
def del_module(request, module, **_kwargs):
"""Compleete delete a module"""
if request.method == "POST":
try:
module.delete()
messages.success(request, _("The module was deleted."))
except ProtectedError:
messages.error(
request,
(_("The module %s is used by another object, impossible to"
" deleted it.") % module)
)
return redirect(reverse('topologie:index-module'))
return form(
{'objet': module, 'objet_name': _("Module")},
'topologie/delete.html',
request
)
@login_required
@can_create(ModuleOnSwitch)
def add_module_on(request):
"""Add a module to a switch"""
module_switch = EditSwitchModuleForm(request.POST or None)
if module_switch.is_valid():
module_switch.save()
messages.success(request, _("The module added to that switch"))
return redirect(reverse('topologie:index-module'))
return form(
{'topoform': module_switch, 'action_name': _("Create")},
'topologie/topo.html',
request
)
@login_required
@can_edit(ModuleOnSwitch)
def edit_module_on(request, module_instance, **_kwargs):
""" View used to edit a Module object """
module = EditSwitchModuleForm(request.POST or None, instance=module_instance)
if module.is_valid():
if module.changed_data:
module.save()
messages.success(request, _("The module was edited."))
return redirect(reverse('topologie:index-module'))
return form(
{'topoform': module, 'action_name': _("Edit")},
'topologie/topo.html',
request
)
@login_required
@can_delete(ModuleOnSwitch)
def del_module_on(request, module, **_kwargs):
"""Compleete delete a module"""
if request.method == "POST":
try:
module.delete()
messages.success(request, _("The module was deleted."))
except ProtectedError:
messages.error(
request,
(_("The module %s is used by another object, impossible to"
" deleted it.") % module)
)
return redirect(reverse('topologie:index-module'))
return form(
{'objet': module, 'objet_name': _("Module")},
'topologie/delete.html',
request
)
def make_machine_graph():
"""
Create the graph of switchs, machines and access points.

View file

@ -45,7 +45,8 @@ from django.utils.safestring import mark_safe
from machines.models import Interface, Machine, Nas
from topologie.models import Port
from preferences.models import OptionalUser
from re2o.utils import remove_user_room, get_input_formats_help_text
from re2o.utils import remove_user_room
from re2o.base import get_input_formats_help_text
from re2o.mixins import FormRevMixin
from re2o.field_permissions import FieldPermissionFormMixin
@ -116,6 +117,7 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
"""Changement du mot de passe"""
user = super(PassForm, self).save(commit=False)
user.set_password(self.cleaned_data.get("passwd1"))
user.set_active()
user.save()
@ -323,14 +325,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
self.fields['room'].empty_label = _("No room")
self.fields['school'].empty_label = _("Select a school")
def clean_email(self):
if not OptionalUser.objects.first().local_email_domain in self.cleaned_data.get('email'):
return self.cleaned_data.get('email').lower()
else:
raise forms.ValidationError(
_("You can't use a {} address.").format(
OptionalUser.objects.first().local_email_domain))
class Meta:
model = Adherent
fields = [
@ -344,6 +338,19 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
'room',
]
force = forms.BooleanField(
label=_("Force the move?"),
initial=False,
required=False
)
def clean_email(self):
if not OptionalUser.objects.first().local_email_domain in self.cleaned_data.get('email'):
return self.cleaned_data.get('email').lower()
else:
raise forms.ValidationError(
_("You can't use a {} address.").format(
OptionalUser.objects.first().local_email_domain))
def clean_telephone(self):
"""Verifie que le tel est présent si 'option est validée
@ -355,12 +362,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
)
return telephone
force = forms.BooleanField(
label=_("Force the move?"),
initial=False,
required=False
)
def clean_force(self):
"""On supprime l'ancien user de la chambre si et seulement si la
case est cochée"""
@ -368,6 +369,7 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
remove_user_room(self.cleaned_data.get('room'))
return
class AdherentCreationForm(AdherentForm):
"""Formulaire de création d'un user.
AdherentForm auquel on ajoute une checkbox afin d'éviter les
@ -383,8 +385,22 @@ class AdherentCreationForm(AdherentForm):
# Checkbox for GTU
gtu_check = forms.BooleanField(required=True)
gtu_check.label = mark_safe("{} <a href='/media/{}' download='CGU'>{}</a>{}".format(
_("I commit to accept the"), GeneralOption.get_cached_value('GTU'), _("General Terms of Use"), _(".")))
#gtu_check.label = mark_safe("{} <a href='/media/{}' download='CGU'>{}</a>{}".format(
# _("I commit to accept the"), GeneralOption.get_cached_value('GTU'), _("General Terms of Use"), _(".")))
class Meta:
model = Adherent
fields = [
'name',
'surname',
'pseudo',
'email',
'school',
'comment',
'telephone',
'room',
'state',
]
def __init__(self, *args, **kwargs):
super(AdherentCreationForm, self).__init__(*args, **kwargs)
@ -398,12 +414,6 @@ class AdherentEditForm(AdherentForm):
if 'shell' in self.fields:
self.fields['shell'].empty_label = _("Default shell")
def clean_gpg_fingerprint(self):
"""Format the GPG fingerprint"""
gpg_fingerprint = self.cleaned_data.get('gpg_fingerprint', None)
if gpg_fingerprint:
return gpg_fingerprint.replace(' ', '').upper()
class Meta:
model = Adherent
fields = [
@ -429,6 +439,7 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
self.fields['surname'].label = _("Name")
self.fields['school'].label = _("School")
self.fields['comment'].label = _("Comment")
self.fields['email'].label = _("Email Address")
if 'room' in self.fields:
self.fields['room'].label = _("Room")
self.fields['room'].empty_label = _("No room")
@ -443,7 +454,9 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
'school',
'comment',
'room',
'email',
'telephone',
'email',
'shell',
'mailing'
]
@ -488,13 +501,14 @@ class PasswordForm(FormRevMixin, ModelForm):
class ServiceUserForm(FormRevMixin, ModelForm):
""" Modification d'un service user"""
"""Service user creation
force initial password set"""
password = forms.CharField(
label=_("New password"),
max_length=255,
validators=[MinLengthValidator(8)],
widget=forms.PasswordInput,
required=False
required=True
)
class Meta:
@ -506,7 +520,7 @@ class ServiceUserForm(FormRevMixin, ModelForm):
super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs)
def save(self, commit=True):
"""Changement du mot de passe"""
"""Password change"""
user = super(ServiceUserForm, self).save(commit=False)
if self.cleaned_data['password']:
user.set_password(self.cleaned_data.get("password"))
@ -516,6 +530,14 @@ class ServiceUserForm(FormRevMixin, ModelForm):
class EditServiceUserForm(ServiceUserForm):
"""Formulaire d'edition de base d'un service user. Ne permet
d'editer que son group d'acl et son commentaire"""
password = forms.CharField(
label=_("New password"),
max_length=255,
validators=[MinLengthValidator(8)],
widget=forms.PasswordInput,
required=False
)
class Meta(ServiceUserForm.Meta):
fields = ['access_group', 'comment']

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-28 19:39
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0078_auto_20181011_1405'),
]
operations = [
migrations.AlterField(
model_name='adherent',
name='gpg_fingerprint',
field=models.CharField(blank=True, max_length=49, null=True),
),
]

View file

@ -81,6 +81,7 @@ 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
from re2o.base import smtp_check
from cotisations.models import Cotisation, Facture, Paiement, Vente
from machines.models import Domain, Interface, Machine, regen
@ -93,7 +94,7 @@ from preferences.models import OptionalMachine, MailMessageOption
def linux_user_check(login):
""" Validation du pseudo pour respecter les contraintes unix"""
UNIX_LOGIN_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9-]*[$]?$")
UNIX_LOGIN_PATTERN = re.compile("^[a-z][a-z0-9-]*[$]?$")
return UNIX_LOGIN_PATTERN.match(login)
@ -336,7 +337,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
def set_active(self):
"""Enable this user if he subscribed successfully one time before"""
if self.state == self.STATE_NOT_YET_ACTIVE:
if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists():
if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists() or OptionalUser.get_cached_value('all_users_active'):
self.state = self.STATE_ACTIVE
self.save()
@ -474,7 +475,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
""" 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()))
(self.is_connected() or self.is_whitelisted())) \
or self == AssoOption.get_cached_value('utilisateur_asso')
def end_access(self):
""" Renvoie la date de fin normale d'accès (adhésion ou whiteliste)"""
@ -576,7 +578,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
mac_refresh : synchronise les machines de l'user
group_refresh : synchronise les group de l'user
Si l'instance n'existe pas, on crée le ldapuser correspondant"""
if sys.version_info[0] >= 3:
if sys.version_info[0] >= 3 and self.state != self.STATE_ARCHIVE and\
self.state != self.STATE_DISABLED:
self.refresh_from_db()
try:
user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
@ -693,10 +696,8 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
def autoregister_machine(self, mac_address, nas_type):
""" 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'
):
allowed, _message = Machine.can_create(self, self.id)
if not allowed:
return False, _("Maximum number of registered machines reached.")
if not nas_type:
return False, _("Re2o doesn't know wich machine type to assign.")
@ -1025,17 +1026,12 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
):
raise ValidationError("This pseudo is already in use.")
if not self.local_email_enabled and not self.email and not (self.state == self.STATE_ARCHIVE):
raise ValidationError(
{'email': (
_("There is neither a local email address nor an external"
raise ValidationError(_("There is neither a local email address nor an external"
" email address for this user.")
), }
)
if self.local_email_redirect and not self.email:
raise ValidationError(
{'local_email_redirect': (
_("You can't redirect your local emails if no external email"
" address has been set.")), }
raise ValidationError(_("You can't redirect your local emails if no external email"
" address has been set.")
)
def __str__(self):
@ -1054,20 +1050,27 @@ class Adherent(User):
null=True
)
gpg_fingerprint = models.CharField(
max_length=40,
max_length=49,
blank=True,
null=True,
validators=[RegexValidator(
'^[0-9A-F]{40}$',
message=_("A GPG fingerprint must contain 40 hexadecimal"
" characters.")
)]
)
class Meta(User.Meta):
verbose_name = _("member")
verbose_name_plural = _("members")
def format_gpgfp(self):
"""Format gpg finger print as AAAA BBBB... from a string AAAABBBB...."""
self.gpg_fingerprint = ' '.join([self.gpg_fingerprint[i:i + 4] for i in range(0, len(self.gpg_fingerprint), 4)])
def validate_gpgfp(self):
"""Validate from raw entry if is it a valid gpg fp"""
if self.gpg_fingerprint:
gpg_fingerprint = self.gpg_fingerprint.replace(' ', '').upper()
if not re.match("^[0-9A-F]{40}$", gpg_fingerprint):
raise ValidationError(_("A gpg fingerprint must contain 40 hexadecimal carracters"))
self.gpg_fingerprint = gpg_fingerprint
@classmethod
def get_instance(cls, adherentid, *_args, **_kwargs):
"""Try to find an instance of `Adherent` with the given id.
@ -1098,6 +1101,13 @@ class Adherent(User):
_("You don't have the right to create a user.")
)
def clean(self, *args, **kwargs):
"""Format the GPG fingerprint"""
super(Adherent, self).clean(*args, **kwargs)
if self.gpg_fingerprint:
self.validate_gpgfp()
self.format_gpgfp()
class Club(User):
""" A class representing a club (it is considered as a user
@ -1889,6 +1899,9 @@ class EMailAddress(RevMixin, AclMixin, models.Model):
def clean(self, *args, **kwargs):
self.local_part = self.local_part.lower()
if "@" in self.local_part:
raise ValidationError(_("The local part must not contain @."))
if "@" in self.local_part or "+" in self.local_part:
raise ValidationError(_("The local part must not contain @ or +."))
result, reason = smtp_check(self.local_part)
if result:
raise ValidationError(reason)
super(EMailAddress, self).clean(*args, **kwargs)

View file

@ -23,7 +23,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load bootstrap3 %}
{% load acl %}
{% load logs_extra %}
{% load design %}
@ -78,8 +77,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if solde_activated %}
<div class="col-sm-6 col-md-4">
<div class="panel panel-info">
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse" data-target="#collapse4">
{{ users.solde }} <i class="fa fa-eur"></i>
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse"
data-target="#subscriptions">
{{ users.solde }} <i class="fa fa-eur"></i>
</div>
<div class="panel-body dashboard">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:credit-solde' users.id %}">
@ -92,8 +92,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div class="col-sm-6 {% if solde_activated %}col-md-4{% else %}col-md-6{% endif %}">
{% if nb_machines %}
<div class="panel panel-info">
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse" data-target="#collapse3">
<i class="fa fa-desktop"></i>{% trans " Machines" %} <span class="badge"> {{ nb_machines }}</span>
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse"
data-target="#machines">
<i class="fa fa-desktop"></i>{% trans " Machines" %} <span class="badge"> {{ nb_machines }}</span>
</div>
<div class="panel-body dashboard">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
@ -103,7 +104,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div>
{% else %}
<div class="panel panel-warning">
<div class="panel-heading dashboard">{% trans "No machine" %}</div>
<div class="panel-heading dashboard" data-parent="#accordion" data-toggle="collapse"
data-target="#machines">
{% trans "No machine" %}
</div>
<div class="panel-body dashboard">
<a class="btn btn-warning btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
<i class="fa fa-desktop"></i>{% trans " Add a machine" %}
@ -117,158 +121,200 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div class="panel-group" id="accordion">
<div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse1">
<h3 class="panel-title pull-left" >
<i class="fa fa-user"></i>{% trans " Detailed information" %}
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse"
data-target="#information">
<h3 class="panel-title pull-left">
<i class="fa fa-user"></i>{% trans " Detailed information" %}
</h3>
</div>
<div class="panel-collapse collapse in" id="collapse1">
<div class="panel-body">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:password' users.id %}">
<i class="fa fa-lock"></i>
{% trans "Change the password" %}
</a>
{% can_change User state %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:state' users.id %}">
<i class="fa fa-id-badge"></i>
{% trans "Change the state" %}
</a>
{% acl_end %}
{% can_change User groups %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:groups' users.id %}">
<i class="fa fa-check"></i>
{% trans "Edit the groups" %}
</a>
{% acl_end %}
{% history_button users text=True %}
<div class="panel-collapse collapse collapse-default" id="information">
<div class="panel-body">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}">
<i class="fa fa-edit"></i>
{% trans "Edit" %}
</a>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:password' users.id %}">
<i class="fa fa-lock"></i>
{% trans "Change the password" %}
</a>
{% can_change User state %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:state' users.id %}">
<i class="fa fa-id-badge"></i>
{% trans "Change the state" %}
</a>
{% acl_end %}
{% can_change User groups %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:groups' users.id %}">
<i class="fa fa-check"></i>
{% trans "Edit the groups" %}
</a>
{% acl_end %}
{% history_button users text=True %}
</ul>
</div>
<div class="panel-body">
<div class="table-responsive">
<table class="table table-striped">
<tr>
<dl class="dl-horizontal row profile-info">
<div class="col-md-6">
{% if users.is_class_club %}
<th>{% trans "Mailing" %}</th>
{% if users.club.mailing %}
<td>{{ users.pseudo }}(-admin)</td>
{% else %}
<td>{% trans "Mailing disabled" %}</td>
{% endif %}
{% else %}
<th>{% trans "Firt name" %}</th>
<td>{{ users.name }}</td>
{% endif %}
<th>{% trans "Surname" %}</th>
<td>{{ users.surname }}</td>
</tr>
<tr>
<th>{% trans "Username" %}</th>
<td>{{ users.pseudo }}</td>
<th>{% trans "Email address" %}</th>
<td><a href="mailto:{{ users.email }}">{{users.email}}</a></td>
</tr>
<tr>
<th>{% trans "Room" %}</th>
<td>{{ users.room }} {% can_view_all Port %}{% if users.room.port_set.all %} / {{ users.room.port_set.all|join:", " }} {% endif %}{% acl_end %}</td>
<th>{% trans "Telephone number" %}</th>
<td>{{ users.telephone }}</td>
</tr>
<tr>
<th>{% trans "School" %}</th>
<td>{{ users.school }}</td>
<th>{% trans "Comment" %}</th>
<td>{{ users.comment }}</td>
</tr>
<tr>
<th>{% trans "Registration date" %}</th>
<td>{{ users.registered }}</td>
<th>{% trans "Last login" %}</th>
<td>{{ users.last_login }}</td>
</tr>
<tr>
<th>{% trans "End of membership" %}</th>
{% if users.end_adhesion != None %}
<td><i class="text-success">{{ users.end_adhesion }}</i></td>
<dt>{% trans "Mailing" %}</dt>
{% if users.club.mailing %}
<dd>{{ users.pseudo }}(-admin)</dd>
{% else %}
<dd>{% trans "Mailing disabled" %}</dd>
{% endif %}
{% else %}
<td><i class="text-danger">{% trans "Not a member" %}</i></td>
<dt>{% trans "Firt name" %}</dt>
<dd>{{ users.name }}</dd>
{% endif %}
<th>{% trans "Whitelist" %}</th>
{% if users.end_whitelist != None %}
<td><i class="text-success">{{ users.end_whitelist }}</i></td>
{% else %}
<td><i class="text-warning">{% trans "None" %}</i></td>
{% endif %}
<tr>
<th>{% trans "Ban" %}</th>
{% if users.end_ban != None %}
<td><i class="text-danger">{{ users.end_ban }}</i></td>
{% else %}
<td><i class="text-success">{% trans "Not banned" %}</i></td>
{% endif %}
<th>{% trans "State" %}</th>
{% if users.state == 0 %}
<td><i class="text-success">{% trans "Active" %}</i></td>
{% elif users.state == 1 %}
<td><i class="text-warning">{% trans "Disabled" %}</i></td>
{% elif users.state == 2 %}
<td><i class="text-danger">{% trans "Archived" %}</i></td>
{% elif users.state == 3 %}
<td><i class="text-danger">{% trans "Not yet Member" %}</i></td>
</div>
<div class="col-md-6">
<dt>{% trans "Surname" %}</dt>
<dd>{{ users.surname }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "Username" %}</dt>
<dd>{{ users.pseudo }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "Email address" %}</dt>
<dd><a href="mailto:{{ users.email }}">{{ users.email }}</a></dd>
</div>
<div class="col-md-6">
<dt>{% trans "Room" %}</dt>
<dd>
{{ users.room }} {% can_view_all Port %}{% if users.room.port_set.all %} /
{{ users.room.port_set.all|join:", " }} {% endif %}{% acl_end %}
</dd>
</div>
<div class="col-md-6">
<dt>{% trans "Telephone number" %}</dt>
<dd>{{ users.telephone }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "School" %}</dt>
<dd>{{ users.school }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "Comment" %}</dt>
<dd>{{ users.comment }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "Registration date" %}</dt>
<dd>{{ users.registered }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "Last login" %}</dt>
<dd>{{ users.last_login }}</dd>
</div>
<div class="col-md-6">
<dt>{% trans "End of membership" %}</dt>
{% if users.end_adhesion != None %}
<dd><i class="text-success">{{ users.end_adhesion }}</i></dd>
{% else %}
<dd><i class="text-danger">{% trans "Not a member" %}</i></dd>
{% endif %}
</tr>
<tr>
<th>{% trans "Internet access" %}</th>
</div>
<div class="col-md-6">
<dt>{% trans "Whitelist" %}</dt>
{% if users.end_whitelist != None %}
<dd><i class="text-success">{{ users.end_whitelist }}</i></dd>
{% else %}
<dd><i class="text-warning">{% trans "None" %}</i></dd>
{% endif %}
</div>
<div class="col-md-6">
<dt>{% trans "Ban" %}</dt>
{% if users.end_ban != None %}
<dd><i class="text-danger">{{ users.end_ban }}</i></dd>
{% else %}
<dd><i class="text-success">{% trans "Not banned" %}</i></dd>
{% endif %}
</div>
<div class="col-md-6">
<dt>{% trans "State" %}</dt>
{% if users.state == 0 %}
<dd><i class="text-success">{% trans "Active" %}</i></dd>
{% elif users.state == 1 %}
<dd><i class="text-warning">{% trans "Disabled" %}</i></dd>
{% elif users.state == 2 %}
<dd><i class="text-danger">{% trans "Archived" %}</i></dd>
{% elif users.state == 3 %}
<dd><i class="text-danger">{% trans "Not yet Member" %}</i></dd>
{% endif %}
</div>
<div class="col-md-6">
<dt>{% trans "Internet access" %}</dt>
{% if users.has_access == True %}
<td><i class="text-success">{% blocktrans with end_access=users.end_access %}Active (until {{ end_access }}){% endblocktrans %}</i></td>
<dd><i class="text-success">
{% blocktrans with end_access=users.end_access %}Active
(until {{ end_access }}){% endblocktrans %}</i></dd>
{% else %}
<td><i class="text-danger">{% trans "Disabled" %}</i></td>
<dd><i class="text-danger">{% trans "Disabled" %}</i></dd>
{% endif %}
<th>{% trans "Groups of rights" %}</th>
</div>
<div class="col-md-6">
<dt>{% trans "Groups of rights" %}</dt>
{% if users.groups.all %}
<td>{{ users.groups.all|join:", "}}</td>
<dd>{{ users.groups.all|join:", " }}</dd>
{% else %}
<td>{% trans "None" %}</td>
<dd>{% trans "None" %}</dd>
{% endif %}
</tr>
<tr>
<th>{% trans "Balance" %}</th>
<td>{{ users.solde }} €
{% if user_solde %}
<a class="btn btn-primary btn-sm" style='float:right' role="button" href="{% url 'cotisations:credit-solde' users.pk%}">
<i class="fa fa-eur"></i>
{% trans "Refill" %}
</a>
{% endif %}
</td>
{% if users.adherent.gpg_fingerprint %}
<th>{% trans "GPG fingerprint" %}</th>
<td>{{ users.adherent.gpg_fingerprint }}</td>
{% endif %}
</tr>
<tr>
{% if users.shell %}
<th>{% trans "Shell" %}</th>
<td>{{ users.shell }}</td>
{% endif %}
</tr>
</table>
</div>
</div>
<div class="col-md-6">
<dt>{% trans "Balance" %}</dt>
<dd>
{{ users.solde }} €
{% if user_solde %}
<a class="btn btn-primary btn-sm" style='float:right' role="button"
href="{% url 'cotisations:credit-solde' users.pk %}">
<i class="fa fa-eur"></i>
{% trans "Refill" %}
</a>
{% endif %}
</dd>
</div>
{% if users.adherent.gpg_fingerprint %}
<div class="col-md-6 col-xs-12">
<dt>{% trans "GPG fingerprint" %}</dt>
<dd>{{ users.adherent.gpg_fingerprint }}</dd>
</div>
{% endif %}
{% if users.shell %}
<div class="col-md-6 col-xs-12">
<dt>{% trans "Shell" %}</dt>
<dd>{{ users.shell }}</dd>
</div>
{% endif %}
</dl>
</div>
</div>
</div>
{% if users.is_class_club %}
<div class="panel panel-default">
<div class="panel-heading clearfix" data-parent="#accordion" data-toggle="collapse" data-target="#collapse2">
<div class="panel-heading clearfix" data-parent="#accordion" data-toggle="collapse" data-target="#club">
<h3 class="panel-title pull-left">
<i class="fa fa-users"></i>{% trans " Manage the club" %}
</h3>
</div>
<div class="panel-collapse collapse" id="collapse2">
<div class="panel-collapse collapse" id="club">
<div class="panel-body">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-club-admin-members' users.club.id %}">
<i class="fa fa-lock"></i>
@ -319,14 +365,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse3">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse"
data-target="#machines">
<h3 class="panel-title pull-left">
<i class="fa fa-desktop"></i>
{% trans "Machines" %}
<span class="badge">{{nb_machines}}</span>
</h3>
</div>
<div id="collapse3" class="panel-collapse collapse">
<div id="machines" class="panel-collapse collapse">
<div class="panel-body">
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:new-machine' users.id %}">
<i class="fa fa-desktop"></i>
@ -343,13 +390,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse4">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse"
data-target="#subscriptions">
<h3 class="panel-title pull-left">
<i class="fa fa-eur"></i>
{% trans "Subscriptions" %}
</h3>
</div>
<div id="collapse4" class="panel-collapse collapse">
<div id="subscriptions" class="panel-collapse collapse">
<div class="panel-body">
{% can_create Facture %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:new-facture' users.id %}">
@ -374,13 +422,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse5">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#bans">
<h3 class="panel-title pull-left">
<i class="fa fa-ban"></i>
{% trans "Bans" %}
</h3>
</div>
<div id="collapse5" class="panel-collapse collapse">
<div id="bans" class="panel-collapse collapse">
<div class="panel-body">
{% can_create Ban %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-ban' users.id %}">
@ -399,13 +447,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse6">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#whitelists">
<h3 class="panel-title pull-left">
<i class="fa fa-check-circle"></i>
{% trans "Whitelists" %}
</h3>
</div>
<div id="collapse6" class="panel-collapse collapse">
<div id="whitelists" class="panel-collapse collapse">
<div class="panel-body">
{% can_create Whitelist %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-whitelist' users.id %}">
@ -424,12 +472,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#collapse7">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#email">
<h3 class="panel-title pull-left">
<i class="fa fa-envelope"></i>{% trans " Email settings" %}
</h3>
</div>
<div id="collapse7" class="panel-collapse collapse">
<div id="email" class="panel-collapse collapse">
<div class="panel-body">
{% can_edit users %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-email-settings' users.id %}">

View file

@ -57,8 +57,10 @@ from preferences.models import OptionalUser, GeneralOption, AssoOption
from re2o.views import form
from re2o.utils import (
all_has_access,
SortTable,
re2o_paginator
)
from re2o.base import (
re2o_paginator,
SortTable
)
from re2o.acl import (
can_create,