...
 
Commits (36)
# Re2o
Gnu public license v2.0
GNU public license v2.0
## Avant propos
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
Re2o est un logiciel d'administration développé initialement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
......@@ -31,15 +31,15 @@ Pour cela :
## Fonctionnement général
Re2o est séparé entre les models, qui sont visible sur le schéma des
dépendances. Il s'agit en réalité des tables sql, et les fields etant les
Re2o est séparé entre les models, qui sont visibles sur le schéma des
dépendances. Il s'agit en réalité des tables sql, et les fields étant les
colonnes.
Ceci dit il n'est jamais nécessaire de toucher directement au sql, django
procédant automatiquement à tout cela.
On crée donc différents models (user, right pour les droits des users,
interfaces, IpList pour l'ensemble des adresses ip, etc)
Du coté des forms, il s'agit des formulaire d'édition des models. Il
Du coté des forms, il s'agit des formulaires d'édition des models. Il
s'agit de ModelForms django, qui héritent des models très simplement, voir la
documentation django models forms.
......@@ -56,12 +56,20 @@ d'accéder à ces vues, utilisé par re2o-tools.
# Requète en base de donnée
Pour avoir un shell, il suffit de lancer '''python3 manage.py shell'''
Pour charger des objets, example avec User, faire :
''' from users.models import User'''
Pour charger les objets django, il suffit de faire User.objects.all()
pour tous les users par exemple.
Il est ensuite aisé de faire des requètes, par exemple
User.objects.filter(pseudo='test')
Des exemples et la documentation complète sur les requètes django sont
Pour avoir un shell, lancer :
```.bash
python3 manage.py shell
```
Pour charger des objets (exemple avec User), faire :
```.python
from users.models import User
```
Pour charger les objets django, il suffit de faire `User.objects.all()`
pour tous les users par exemple.
Il est ensuite aisé de faire des requêtes, par exemple
`User.objects.filter(pseudo='test')`
Des exemples et la documentation complète sur les requêtes django sont
disponible sur le site officiel.
......@@ -29,6 +29,7 @@ the response (JSON or other), the CSRF exempting, ...
import datetime
from django.conf import settings
from django.db.models import Q
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
......@@ -421,7 +422,7 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users` objects to create homes.
"""
queryset = users.User.objects.all()
queryset = users.User.objects.exclude(Q(state=users.User.STATE_DISABLED) | Q(state=users.User.STATE_NOT_YET_ACTIVE))
serializer_class = serializers.HomeCreationSerializer
class ClubViewSet(viewsets.ReadOnlyModelViewSet):
......
......@@ -102,7 +102,7 @@ class SelectArticleForm(FormRevMixin, Form):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
target_user = kwargs.pop('target_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)
......
......@@ -681,8 +681,9 @@ msgstr ""
#: templates/cotisations/delete.html:38
#: templates/cotisations/edit_facture.html:60
#: views.py:181 views.py:235
msgid "Confirm"
msgstr "Confirmer"
msgstr "Valider"
#: templates/cotisations/edit_facture.html:31
#: templates/cotisations/facture.html:30
......@@ -695,12 +696,13 @@ msgstr "Modifier la facture"
#: templates/cotisations/edit_facture.html:41
#: templates/cotisations/facture.html:56
msgid "Invoice's articles"
msgstr "Articles de la facture"
#: templates/cotisations/index_article.html:30
msgid "Articles"
msgstr "Articles"
#: templates/cotisations/facture.html:37
msgid "New invoice"
msgstr "Nouvelle facture"
msgid "Buy"
msgstr "Acheter une cotisation"
#: templates/cotisations/facture.html:40
#, python-format
......@@ -713,8 +715,8 @@ msgid "Current balance: %(balance)s €"
msgstr "Solde actuel : %(balance)s €"
#: templates/cotisations/facture.html:70
msgid "Add an article"
msgstr "Ajouter un article"
msgid "Add an extra article"
msgstr "Ajouter un article supplémentaire"
#: templates/cotisations/facture.html:72
msgid "Total price: <span id=\"total_price\">0,00</span> €"
......@@ -728,9 +730,6 @@ msgstr "Factures"
msgid "Subscriptions"
msgstr "Cotisations"
#: templates/cotisations/index_article.html:30
msgid "Articles"
msgstr "Articles"
#: templates/cotisations/index_article.html:33
msgid "Article types list"
......@@ -812,9 +811,6 @@ msgstr "Contrôler les factures"
msgid "You need to choose at least one article."
msgstr "Vous devez choisir au moins un article."
#: views.py:181 views.py:235
msgid "Create"
msgstr "Créer"
#: views.py:228
msgid "The custom invoice was created."
......
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-08-31 13:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0033_auto_20180818_1319'),
]
operations = [
migrations.AlterField(
model_name='facture',
name='valid',
field=models.BooleanField(default=False, verbose_name='validated'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-09-01 11:27
from __future__ import unicode_literals
import cotisations.payment_methods.mixins
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0034_auto_20180831_1532'),
]
operations = [
migrations.CreateModel(
name='NotePayment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('server', models.CharField(max_length=255, verbose_name='server')),
('port', models.PositiveIntegerField(blank=True, null=True)),
('id_note', models.PositiveIntegerField(blank=True, null=True)),
('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')),
],
options={
'verbose_name': 'NoteKfet',
},
bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model),
),
]
......@@ -50,7 +50,7 @@ from machines.models import regen
from re2o.field_permissions import FieldPermissionModelMixin
from re2o.mixins import AclMixin, RevMixin
from cotisations.utils import find_payment_method
from cotisations.utils import find_payment_method, send_mail_invoice
from cotisations.validators import check_no_balance
......@@ -137,7 +137,7 @@ class Facture(BaseInvoice):
)
# TODO : change name to validity for clarity
valid = models.BooleanField(
default=True,
default=False,
verbose_name=_("validated")
)
# TODO : changed name to controlled for clarity
......@@ -231,19 +231,26 @@ class Facture(BaseInvoice):
self.field_permissions = {
'control': self.can_change_control,
}
self.__original_valid = self.valid
def save(self, *args, **kwargs):
super(Facture, self).save(*args, **kwargs)
if not self.__original_valid and self.valid:
send_mail_invoice(self)
def __str__(self):
return str(self.user) + ' ' + str(self.date)
@receiver(post_save, sender=Facture)
def facture_post_save(**kwargs):
"""
Synchronise the LDAP user after an invoice has been saved.
"""
facture = kwargs['instance']
user = facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
if facture.valid:
user = facture.user
user.set_active()
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
@receiver(post_delete, sender=Facture)
......@@ -472,7 +479,8 @@ def vente_post_save(**kwargs):
purchase.create_cotis()
purchase.cotisation.save()
user = purchase.facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
user.set_active()
user.ldap_sync(base=True, access_refresh=True, mac_refresh=False)
# TODO : change vente to purchase
......@@ -602,7 +610,9 @@ class Article(RevMixin, AclMixin, models.Model):
user: The user requesting articles.
target_user: The user to sell articles
"""
if target_user.is_class_club:
if target_user is None:
objects_pool = cls.objects.filter(Q(type_user='All'))
elif target_user.is_class_club:
objects_pool = cls.objects.filter(
Q(type_user='All') | Q(type_user='Club')
)
......@@ -700,6 +710,10 @@ class Paiement(RevMixin, AclMixin, models.Model):
if payment_method is not None and use_payment_method:
return payment_method.end_payment(invoice, request)
## So make this invoice valid, trigger send mail
invoice.valid = True
invoice.save()
# In case a cotisation was bought, inform the user, the
# cotisation time has been extended too
if any(sell.type_cotisation for sell in invoice.vente_set.all()):
......
......@@ -127,10 +127,11 @@ method to your model, where `form` is an instance of
"""
from . import comnpay, cheque, balance, urls
from . import comnpay, cheque, balance, note_kfet, urls
PAYMENT_METHODS = [
comnpay,
cheque,
balance,
note_kfet
]
......@@ -74,8 +74,6 @@ class BalancePayment(PaymentMethodMixin, models.Model):
user = invoice.user
total_price = invoice.prix_total()
if float(user.solde) - float(total_price) < self.minimum_balance:
invoice.valid = False
invoice.save()
messages.error(
request,
_("Your balance is too low for this operation.")
......
......@@ -46,8 +46,6 @@ class ChequePayment(PaymentMethodMixin, models.Model):
"""Invalidates the invoice then redirect the user towards a view asking
for informations to add to the invoice before validating it.
"""
invoice.valid = False
invoice.save()
return redirect(reverse(
'cotisations:cheque:validate',
kwargs={'invoice_pk': invoice.pk}
......
......@@ -81,8 +81,6 @@ class ComnpayPayment(PaymentMethodMixin, models.Model):
a facture id, the price and the secret transaction data stored in
the preferences.
"""
invoice.valid = False
invoice.save()
host = request.get_host()
p = Transaction(
str(self.payment_credential),
......
......@@ -68,7 +68,7 @@ def accept_payment(request, factureid):
)
return redirect(reverse(
'users:profil',
kwargs={'userid': request.user.id}
kwargs={'userid': invoice.user.id}
))
......
# -*- 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 Detraz, Pierre-Antoine Comby
#
# 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 module contains a method to pay online using comnpay.
"""
from . import models, urls
NAME = "NOTE"
PaymentMethod = models.NotePayment
# -*- 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 Pierre-Antoine Comby
# Copyright © 2018 Gabriel Detraz
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from django import forms
from django.utils.translation import ugettext_lazy as _
from cotisations.utils import find_payment_method
class NoteCredentialForm(forms.Form):
"""A special form to get credential to connect to a NoteKfet2015 server throught his API
object.
"""
login = forms.CharField(
label=_("pseudo note")
)
password = forms.CharField(
label=_("Password"),
widget=forms.PasswordInput
)
# -*- 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 Pierre-Antoine Comby
# Copyright © 2018 Gabriel Detraz
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from django.db import models
from django.shortcuts import render
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django.contrib import messages
from cotisations.models import Paiement
from cotisations.payment_methods.mixins import PaymentMethodMixin
from django.shortcuts import render, redirect
class NotePayment(PaymentMethodMixin, models.Model):
"""
The model allowing you to pay with NoteKfet2015.
"""
class Meta:
verbose_name = _("NoteKfet")
payment = models.OneToOneField(
Paiement,
on_delete = models.CASCADE,
related_name = 'payment_method',
editable = False
)
server = models.CharField(
max_length=255,
verbose_name=_("server")
)
port = models.PositiveIntegerField(
blank = True,
null = True
)
id_note = models.PositiveIntegerField(
blank = True,
null = True
)
def end_payment(self, invoice, request):
return redirect(reverse(
'cotisations:note_kfet:note_payment',
kwargs={'factureid': invoice.id}
))
#!/usr/bin/python3
# -*- coding:utf-8 -*-
# Codé par PAC , forké de 20-100
""" Module pour dialoguer avec la NoteKfet2015 """
import socket
import json
import ssl
import traceback
def get_response(socket):
length_str = b''
char = socket.recv(1)
while char != b'\n':
length_str += char
char = socket.recv(1)
total = int(length_str)
return json.loads(socket.recv(total).decode('utf-8'))
def connect(server, port):
sock = socket.socket()
try:
# On établit la connexion sur port 4242
sock.connect((server, port))
# On passe en SSL
sock = ssl.wrap_socket(sock)
# On fait un hello
sock.send(b'["hello", "manual"]')
retcode = get_response(sock)
except:
# Si on a foiré quelque part, c'est que le serveur est down
return (False, sock, "Serveur indisponible")
return (True, sock, "")
def login(server, port, username, password, masque = [[], [], True]):
result, sock, err = connect(server, port)
if not result:
return (False, None, err)
try:
commande = ["login", [username, password, "bdd", masque]]
sock.send(json.dumps(commande).encode("utf-8"))
response = get_response(sock)
retcode = response['retcode']
if retcode == 0:
return (True, sock, "")
elif retcode == 5:
return (False, sock, "Login incorrect")
else:
return (False, sock, "Erreur inconnue " + str(retcode))
except:
# Si on a foiré quelque part, c'est que le serveur est down
return (False, sock, "Erreur de communication avec le serveur")
def don(sock, montant, id_note, facture):
"""
Faire faire un don à l'id_note
"""
try:
sock.send(json.dumps(["dons", [[id_note], round(montant*100), "Facture : id=%s, designation=%s" % (facture.id, facture.name())]]).encode("utf-8"))
response = get_response(sock)
retcode = response['retcode']
transaction_retcode = response["msg"][0][0]
if 0 < retcode < 100 or 200 <= retcode or 0 < transaction_retcode < 100 or 200 <= transaction_retcode:
return (False, "Transaction échouée. (Solde trop négatif ?)")
elif retcode == 0:
return (True, "")
else:
return (False, "Erreur inconnue " + str(retcode))
except:
return (False, "Erreur de communication avec le serveur")
# -*- 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 Detraz
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from django.conf.urls import url
from . import views
urlpatterns = [
url(
r'^note_payment/(?P<factureid>[0-9]+)$',
views.note_payment,
name='note_payment'
),
]
# -*- 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 Detraz
# Copyright © 2018 Pierre-Antoine Comby
#
# 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.
"""Payment
Here are the views needed by comnpay
"""
from collections import OrderedDict
from django.urls import reverse
from django.shortcuts import redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.views.decorators.csrf import csrf_exempt
from django.utils.datastructures import MultiValueDictKeyError
from django.utils.translation import ugettext as _
from django.http import HttpResponse, HttpResponseBadRequest
from cotisations.models import Facture
from cotisations.utils import find_payment_method
from .models import NotePayment
from re2o.views import form
from re2o.acl import (
can_create,
can_edit
)
from .note import login, don
from .forms import NoteCredentialForm
@login_required
@can_edit(Facture)
def note_payment(request, facture, factureid):
"""
Build a request to start the negociation with NoteKfet by using
a facture id, the price and the login/password data stored in
the preferences.
"""
user = facture.user
payment_method = find_payment_method(facture.paiement)
if not payment_method or not isinstance(payment_method, NotePayment):
messages.error(request, "Erreur inconnue")
return redirect(reverse(
'users:profil',
kwargs={'userid': user.id}
))
noteform = NoteCredentialForm(request.POST or None)
if noteform.is_valid():
pseudo = noteform.cleaned_data['login']
password = noteform.cleaned_data['password']
result, sock, err = login(payment_method.server, payment_method.port, pseudo, password)
if not result:
messages.error(request, err)
return form(
{'form': noteform, 'amount': facture.prix_total()},
"cotisations/payment.html",
request
)
else:
result, err = don(sock, facture.prix_total(), payment_method.id_note, facture)
if not result:
messages.error(request, err)
return form(
{'form': noteform, 'amount': facture.prix_total()},
"cotisations/payment.html",
request
)
facture.valid = True
facture.save()
messages.success(request, "Le paiement par note a bien été effectué")
return redirect(reverse(
'users:profil',
kwargs={'userid': user.id}
))
return form(
{'form': noteform, 'amount': facture.prix_total()},
"cotisations/payment.html",
request
)
......@@ -19,9 +19,10 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from django.conf.urls import include, url
from . import comnpay, cheque
from . import comnpay, cheque, note_kfet
urlpatterns = [
url(r'^comnpay/', include(comnpay.urls, namespace='comnpay')),
url(r'^cheque/', include(cheque.urls, namespace='cheque')),
url(r'^note_kfet/', include(note_kfet.urls, namespace='note_kfet')),
]
......@@ -73,7 +73,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_delete facture %}
{% include 'buttons/suppr.html' with href='cotisations:del-facture' id=facture.id %}
{% acl_end %}
{% history_button facture text=True html_class=False %}
{% history_button facture %}
</td>
<td>
{% if facture.valid %}
......
......@@ -105,7 +105,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %}
</table>
{% trans "Edit" as tr_edit %}
{% bootstrap_button tr_edit button_type='submit' icon='star' %}
{% bootstrap_button tr_edit button_type='submit' icon='ok' button_class='btn-success' %}
</form>
{% endblock %}
......
......@@ -36,7 +36,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% blocktrans %}Warning: are you sure you really want to delete this {{ object_name }} object ( {{ objet }} )?{% endblocktrans %}
</h4>
{% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type='submit' icon='trash' %}
{% bootstrap_button tr_confirm button_type='submit' icon='trash' button_class='btn-danger' %}
</form>
{% endblock %}
......@@ -38,7 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<h3>{% trans "Edit the invoice" %}</h3>
{% massive_bootstrap_form factureform 'user' %}
{{ venteform.management_form }}
<h3>{% trans "Invoice's articles" %}</h3>
<h3>{% trans "Articles" %}</h3>
<table class="table table-striped">
<thead>
<tr>
......@@ -58,7 +58,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %}
</table>
{% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type='submit' icon='star' %}
{% bootstrap_button tr_confirm button_type='submit' icon='ok' button_class='btn-success' %}
</form>
{% endblock %}
......
......@@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if title %}
<h3>{{ title }}</h3>
{% else %}
<h3>{% trans "New invoice" %}</h3>
<h3>{% trans "Buy" %}</h3>
{% endif %}
{% if max_balance %}
<h4>{% blocktrans %}Maximum allowed balance: {{ max_balance }} €{% endblocktrans %}</h4>
......@@ -53,7 +53,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div id="paymentMethod"></div>
{% endif %}
{% if articlesformset %}
<h3>{% trans "Invoice's articles" %}</h3>
<h3>{% trans "Articles" %}</h3>
<div id="form_set" class="form-group">
{{ articlesformset.management_form }}
{% for articlesform in articlesformset.forms %}
......@@ -67,12 +67,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div>
{% endfor %}
</div>
<input class="btn btn-primary btn-sm" role="button" value="{% trans "Add an article"%}" id="add_one">
<input class="btn btn-primary btn-block" role="button" value="{% trans "Add an extra article"%}" id="add_one">
<p>
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
</p>
{% endif %}
{% bootstrap_button action_name button_type='submit' icon='star' %}
{% bootstrap_button action_name button_type='submit' icon='ok' button_class='btn-success' %}
</form>
{% if articlesformset or payment_method%}
......
......@@ -40,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% bootstrap_form form %}
{% endif %}
{% trans "Pay" as tr_pay %}
{% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %}
{% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' button_class='btn-success' %}
</form>
{% endblock %}
......@@ -89,7 +89,7 @@ def send_mail_invoice(invoice):
'Votre facture / Your invoice',
template.render(ctx),
GeneralOption.get_cached_value('email_from'),
[invoice.user.email],
[invoice.user.get_mail],
attachments=[('invoice.pdf', pdf, 'application/pdf')]
)
mail.send()
......@@ -81,7 +81,7 @@ from .forms import (
)
from .tex import render_invoice
from .payment_methods.forms import payment_method_factory
from .utils import find_payment_method, send_mail_invoice
from .utils import find_payment_method
@login_required
......@@ -148,8 +148,6 @@ def new_facture(request, user, userid):
p.facture = new_invoice_instance
p.save()
send_mail_invoice(new_invoice_instance)
return new_invoice_instance.paiement.end_payment(
new_invoice_instance,
request
......@@ -171,7 +169,7 @@ def new_facture(request, user, userid):
'articlesformset': article_formset,
'articlelist': article_list,
'balance': balance,
'action_name': _('Create'),
'action_name': _('Confirm'),
},
'cotisations/facture.html', request
)
......@@ -193,9 +191,9 @@ def new_custom_invoice(request):
# Building the invocie form and the article formset
invoice_form = CustomInvoiceForm(request.POST or None)
article_formset = formset_factory(SelectArticleForm)(
articles_formset = formset_factory(SelectArticleForm)(
request.POST or None,
form_kwargs={'user': request.user, 'target_user': user}
form_kwargs={'user': request.user}
)
if invoice_form.is_valid() and articles_formset.is_valid():
......@@ -218,10 +216,9 @@ def new_custom_invoice(request):
)
return redirect(reverse('cotisations:index-custom-invoice'))
return form({
'factureform': invoice_form,
'action_name': _("Create"),
'action_name': _("Confirm"),
'articlesformset': articles_formset,
'articlelist': articles
}, 'cotisations/facture.html', request)
......@@ -839,7 +836,6 @@ def credit_solde(request, user, **_kwargs):
else:
price_ok = True
if price_ok:
invoice.valid = True
invoice.save()
Vente.objects.create(
facture=invoice,
......@@ -848,8 +844,6 @@ def credit_solde(request, user, **_kwargs):
number=1
)
send_mail_invoice(invoice)
return invoice.paiement.end_payment(invoice, request)
p = get_object_or_404(Paiement, is_balance=True)
return form({
......@@ -857,6 +851,6 @@ def credit_solde(request, user, **_kwargs):
'balance': user.solde,
'title': _("Refill your balance"),
'action_name': _("Pay"),
'max_balance': p.payment_method.maximum_balance,
'max_balance': find_payment_method(p).maximum_balance,
}, 'cotisations/facture.html', request)
......@@ -254,6 +254,14 @@ def stats_general(request):
.count()),
Club.objects.filter(state=Club.STATE_ARCHIVE).count()
],
'not_active_users': [
_("Not yet active users"),
User.objects.filter(state=User.STATE_NOT_YET_ACTIVE).count(),
(Adherent.objects
.filter(state=Adherent.STATE_NOT_YET_ACTIVE)
.count()),
Club.objects.filter(state=Club.STATE_NOT_YET_ACTIVE).count()
],
'adherent_users': [
_("Contributing members"),
_all_adherent.count(),
......
......@@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}
<h4>{% blocktrans %}Warning: are you sure you want to delete this object {{ objet_name }} ( {{ objet }} )?{% endblocktrans %}</h4>
{% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type="submit" icon="trash" %}
{% bootstrap_button tr_confirm button_type="submit" icon='trash' button_class='btn-danger' %}
</form>
<br />
<br />
......
......@@ -51,7 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<input class="btn btn-primary btn-sm" role="button" value=value id="add_one">
</p>
{% trans "Create or edit" as tr_create_or_edit %}
{% bootstrap_button tr_create_or_edit button_type="submit" icon="star" %}
{% bootstrap_button tr_create_or_edit icon='ok' button_class='btn-success' %}
</form>
<script type="text/javascript">
var template = `{{ports.empty_form}}`;
......
......@@ -168,7 +168,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<h3>{% trans "IPv6 address" %}</h3>
{% bootstrap_form ipv6form %}
{% endif %}
{% bootstrap_button action_name button_type="submit" icon="star" %}
{% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}
</form>
<br />
<br />
......
......@@ -38,7 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}
{% massive_bootstrap_form options 'utilisateur_asso' %}
{% trans "Edit" as tr_edit %}
{% bootstrap_button tr_edit button_type="submit" icon="star" %}
{% bootstrap_button tr_edit button_type="submit" icon='ok' button_class='btn-success' %}
</form>
<br />
<br />
......
......@@ -39,7 +39,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if preferenceform %}
{% bootstrap_form preferenceform %}
{% endif %}
{% bootstrap_button action_name button_type="submit" icon="star" %}
{% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}
</form>
<br />
<br />
......
......@@ -232,7 +232,7 @@ msgstr "Accéder à mon profil"
#: templates/re2o/index.html:79
msgid "Services of the organisation"
msgstr "Serices de l'association"
msgstr "Services de l'association"
#: templates/re2o/index.html:93
msgid "Go there"
......
......@@ -33,6 +33,7 @@ CHOICES_USER = (
('0', _("Active")),
('1', _("Disabled")),
('2', _("Archived")),
('3', _("Not Yet Active")),
)
CHOICES_AFF = (
......
......@@ -140,7 +140,9 @@ def search_single_word(word, filters, user,
) | Q(
room__name__icontains=word
) | Q(
room__name__icontains=word
email__icontains=word
) | Q(
telephone__icontains=word
)
) & Q(state__in=user_state)
if not User.can_view_all(user)[0]:
......
......@@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}
<h4>{% blocktrans %}Warning: are you sure you want to delete this {{ objet_name }} object ( {{ objet }} )?{% endblocktrans %}</h4>
{% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type="submit" icon="trash" %}
{% bootstrap_button tr_confirm button_type="submit" icon="trash" button_class='btn-danger' %}
</form>
<br />
<br />
......
......@@ -44,7 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% massive_bootstrap_form topoform 'switch_interface' %}
{% endif %}
{% trans "Create" as tr_create %}
{% bootstrap_button tr_create button_type="submit" icon="ok" %}
{% bootstrap_button tr_create button_type="submit" icon='ok' button_class='btn-success' %}
</form>
<br />
<br />
......
......@@ -38,7 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post">
{% csrf_token %}
{% massive_bootstrap_form topoform 'room,related,machine_interface,members' %}
{% bootstrap_button action_name button_type="submit" icon="ok" %}
{% bootstrap_button action_name icon='ok' button_class='btn-success' %}
</form>
<br />
<br />
......
......@@ -57,7 +57,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% bootstrap_form domainform %}
{% endif %}
{% trans "Create or edit" as tr_create_or_edit %}
{% bootstrap_button tr_create_or_edit button_type="submit" icon="ok" %}
{% bootstrap_button tr_create_or_edit button_type="submit" icon='ok' button_class='btn-success' %}
</form>
<br />
<br />
......
......@@ -41,6 +41,8 @@ from django.utils import timezone
from django.contrib.auth.models import Group, Permission
from django.utils.translation import ugettext_lazy as _
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.mixins import FormRevMixin
......@@ -660,3 +662,53 @@ class EmailSettingsForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
model = User
fields = ['email','local_email_enabled', 'local_email_redirect']
class InitialRegisterForm(forms.Form):
register_room = forms.BooleanField(required=False)
register_machine = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
switch_ip = kwargs.pop('switch_ip')
switch_port = kwargs.pop('switch_port')
client_mac = kwargs.pop('client_mac')
self.user = kwargs.pop('user')
if switch_ip and switch_port:
# Looking for a port
port = Port.objects.filter(switch__interface__ipv4__ipv4=switch_ip, port=switch_port).first()
# If a port exists, checking there is a room AND radius
if port:
if port.get_port_profile.radius_type != 'NO' and port.get_port_profile.radius_mode == 'STRICT' and hasattr(port, 'room'):
# Requesting user is not in this room ?
if self.user.room != port.room:
self.new_room = port.room
if client_mac and switch_ip:
# If this interface doesn't already exists
if not Interface.objects.filter(mac_address=client_mac):
self.mac_address = client_mac
self.nas_type = Nas.objects.filter(nas_type__interface__ipv4__ipv4=switch_ip).first()
super(InitialRegisterForm, self).__init__(*args, **kwargs)
if hasattr(self, 'new_room'):
self.fields['register_room'].label = _("New connection from room %s. Is it yours? If that is the case, type OK." % self.new_room)
else:
self.fields.pop('register_room')
if hasattr(self, 'mac_address'):
self.fields['register_machine'].label = _("New connection from new device. Register it? Say Yes to get Internet access from it (MAC Address : %s)." % self.mac_address)
else:
self.fields.pop('register_machine')
def clean_register_room(self):
if self.cleaned_data['register_room']:
if self.user.is_class_adherent:
remove_user_room(self.new_room)
user = self.user.adherent
user.room = self.new_room
user.save()
if self.user.is_class_club:
user = self.user.club
user.room = self.new_room
user.save()
def clean_register_machine(self):
if self.cleaned_data['register_machine']:
if self.mac_address and self.nas_type:
self.user.autoregister_machine(self.mac_address, self.nas_type)
......@@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-23 15:36+0200\n"
"POT-Creation-Date: 2018-08-28 23:07+0200\n"
"PO-Revision-Date: 2018-06-27 23:35+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
......@@ -35,181 +35,197 @@ msgstr ""
msgid "You don't have the right to view this application."
msgstr "Vous n'avez pas le droit de voir cette application."
#: forms.py:70
#: forms.py:72
msgid "Current password"
msgstr "Mot de passe actuel"
#: forms.py:75 forms.py:447
#: forms.py:77 forms.py:449
msgid "New password"
msgstr "Nouveau mot de passe"
#: forms.py:81
#: forms.py:83
msgid "New password confirmation"
msgstr "Confirmation du nouveau mot de passe"
#: forms.py:98
#: forms.py:100
msgid "The new passwords don't match."
msgstr "Les nouveaux mots de passe ne correspondent pas."
#: forms.py:107
#: forms.py:109
msgid "The current password is incorrect."
msgstr "Le mot de passe actuel est incorrect."
#: forms.py:125 forms.py:178 models.py:1550
#: forms.py:127 forms.py:180 models.py:1550
msgid "Password"
msgstr "Mot de passe"
#: forms.py:131 forms.py:184
#: forms.py:133 forms.py:186
msgid "Password confirmation"
msgstr "Confirmation du mot de passe"
#: forms.py:136 forms.py:227
#: forms.py:138 forms.py:229
msgid "Is admin"
msgstr "Est admin"
#: forms.py:146
#: forms.py:148
msgid "You can't use an internal address as your external address."
msgstr ""
"Vous ne pouvez pas utiliser une adresse interne pour votre adresse externe."
#: forms.py:159 forms.py:208
#: forms.py:161 forms.py:210
msgid "The passwords don't match."
msgstr "Les mots de passe ne correspondent pas."
#: forms.py:236
#: forms.py:238
#, python-format
msgid "User is admin: %s"
msgstr "L'utilisateur est admin : %s"
#: forms.py:284 templates/users/aff_clubs.html:38
#: forms.py:286 templates/users/aff_clubs.html:38
#: templates/users/aff_listright.html:63 templates/users/aff_listright.html:168
#: templates/users/aff_users.html:39 templates/users/profil.html:169
#: templates/users/profil.html:283 templates/users/profil.html:302
msgid "Username"
msgstr "Pseudo"
#: forms.py:299
#: forms.py:301
msgid "Impossible to archive users whose end access date is in the future."
msgstr ""
"Impossible d'archiver des utilisateurs dont la date de fin de connexion est "
"dans le futur."
#: forms.py:311 templates/users/profil.html:282 templates/users/profil.html:301
#: forms.py:313 templates/users/profil.html:282 templates/users/profil.html:301
msgid "First name"
msgstr "Prénom"
#: forms.py:312 templates/users/aff_users.html:37
#: forms.py:314 templates/users/aff_users.html:37
#: templates/users/profil.html:165 templates/users/profil.html:281
#: templates/users/profil.html:300
msgid "Surname"
msgstr "Nom"
#: forms.py:313 models.py:1551 templates/users/aff_emailaddress.html:36
#: forms.py:315 models.py:1551 templates/users/aff_emailaddress.html:36
#: templates/users/profil.html:171
msgid "Email address"
msgstr "Adresse mail"
#: forms.py:314 forms.py:385 forms.py:516 templates/users/aff_schools.html:37
#: forms.py:316 forms.py:387 forms.py:518 templates/users/aff_schools.html:37
#: templates/users/profil.html:181
msgid "School"
msgstr "Établissement"
#: forms.py:315 forms.py:386 models.py:1204
#: forms.py:317 forms.py:388 models.py:1204
#: templates/users/aff_serviceusers.html:34 templates/users/profil.html:183
msgid "Comment"
msgstr "Commentaire"
#: forms.py:316 forms.py:387 templates/users/aff_clubs.html:40
#: forms.py:318 forms.py:389 templates/users/aff_clubs.html:40
#: templates/users/aff_users.html:41 templates/users/profil.html:175
msgid "Room"
msgstr "Chambre"
#: forms.py:317 forms.py:388
#: forms.py:319 forms.py:390
msgid "No room"
msgstr "Pas de chambre"
#: forms.py:318 forms.py:389
#: forms.py:320 forms.py:391
msgid "Select a school"
msgstr "Sélectionnez un établissement"
#: forms.py:319
#: forms.py:321
msgid "Leave empty if you don't have any GPG key."
msgstr "Laissez vide si vous n'avez pas de clé GPG."
#: forms.py:321
#: forms.py:323
msgid "Default shell"
msgstr "Interface système par défaut"
#: forms.py:328 forms.py:656
#: forms.py:330 forms.py:658
msgid "You can't use a {} address."
msgstr "Vous ne pouvez pas utiliser une adresse {}."
#: forms.py:353 forms.py:411
#: forms.py:355 forms.py:413
msgid "A valid telephone number is required."
msgstr "Un numéro de téléphone valide est requis."
#: forms.py:364
#: forms.py:366
msgid "Force the move?"
msgstr "Forcer le déménagement ?"
#: forms.py:384 templates/users/aff_clubs.html:36
#: forms.py:386 templates/users/aff_clubs.html:36
#: templates/users/aff_serviceusers.html:32
msgid "Name"
msgstr "Nom"
#: forms.py:390
#: forms.py:392
msgid "Use a mailing list"
msgstr "Utiliser une liste de diffusion"
#: forms.py:504 templates/users/aff_listright.html:38
#: forms.py:506 templates/users/aff_listright.html:38
msgid "Superuser"
msgstr "Superutilisateur"
#: forms.py:528
#: forms.py:530
msgid "Shell name"
msgstr "Nom de l'interface système"
#: forms.py:547
#: forms.py:549
msgid "Name of the group of rights"
msgstr "Nom du groupe de droits"
#: forms.py:558
#: forms.py:560
msgid "GID. Warning: this field must not be edited after creation."
msgstr "GID. Attention : ce champ ne doit pas être modifié après création."
#: forms.py:566
#: forms.py:568
msgid "Current groups of rights"
msgstr "Groupes de droits actuels"
#: forms.py:583
#: forms.py:585
msgid "Current schools"
msgstr "Établissements actuels"
#: forms.py:601 forms.py:615 templates/users/aff_bans.html:41
#: forms.py:603 forms.py:617 templates/users/aff_bans.html:41
#: templates/users/aff_whitelists.html:41
msgid "End date"
msgstr "Date de fin"
#: forms.py:629 models.py:1748
#: forms.py:631 models.py:1748
msgid "Local part of the email address"
msgstr "Partie locale de l'adresse mail"
#: forms.py:630
#: forms.py:632
msgid "Can't contain @"
msgstr "Ne peut pas contenir @"
#: forms.py:645
#: forms.py:647
msgid "Main email address"
msgstr "Adresse mail principale"
#: forms.py:647
#: forms.py:649
msgid "Redirect local emails"
msgstr "Rediriger les mails locaux"
#: forms.py:649
#: forms.py:651
msgid "Use local emails"
msgstr "Utiliser les mails locaux"
#: forms.py:691
#, python-format
msgid "New connection from room %s. Is it yours? If that is the case, type OK."
msgstr ""
"Nouvelle connexion depuis la chambre %s. Est-ce la vôtre ? Si c'est le cas, "
"tapez OK."
#: forms.py:695
#, python-format
msgid ""
"New connection from new device. Register it? Say Yes to get Internet access "
"from it (MAC Address : %s)."
msgstr ""
"Nouvelle connexion depuis un nouvel appareil. L'enregistrer ? Dites Oui pour "
"avoir accès à Internet depuis cet appareil (adresse MAC : %s)."
#: models.py:105
#, python-format
msgid "The username '%(label)s' contains forbidden characters."
......@@ -722,7 +738,8 @@ msgstr "Confirmer"
#: templates/users/index_schools.html:30
#: templates/users/index_serviceusers.html:30
#: templates/users/index_shell.html:30 templates/users/index_whitelist.html:29
#: templates/users/sidebar.html:52 templates/users/user.html:30
#: templates/users/plugin_out.html:31 templates/users/sidebar.html:52
#: templates/users/user.html:30
msgid "Users"
msgstr "Utilisateurs"
......@@ -816,6 +833,15 @@ msgid "The following users will be archived (%(to_archive_list|length)s):"
msgstr ""
"Les utilisateus suivants vont être archivés (%(to_archive_list|length)s :"
#: templates/users/plugin_out.html:35
msgid ""
"Your machine and your room were successfully registered. Please disconnect "
"and reconnect your Ethernet cable to benefit from a wired connection."
msgstr ""
"Votre machine et votre chambre ont bien été enregistrées. Veuillez"
" débrancher et rebrancher votre câble Ethernet pour bénéficier d'une"
" connexion filaire."
#: templates/users/profil.html:37
#, python-format
msgid "Welcome %(name)s %(surname)s"
......@@ -881,7 +907,7 @@ msgstr " Informations détaillées"
msgid "Edit"
msgstr "Modifier"
#: templates/users/profil.html:133 views.py:282 views.py:1079
#: templates/users/profil.html:133 views.py:283 views.py:1080
msgid "Change the password"
msgstr "Changer le mot de passe"
......@@ -889,7 +915,7 @@ msgstr "Changer le mot de passe"
msgid "Change the state"
msgstr "Changer l'état"
#: templates/users/profil.html:144 views.py:260
#: templates/users/profil.html:144 views.py:261
msgid "Edit the groups"
msgstr "Modifier les groupes"
......@@ -956,7 +982,7 @@ msgstr "Solde"
#: templates/users/profil.html:241
msgid "Refill"
msgstr "Rechager"
msgstr "Recharger"
#: templates/users/profil.html:246
msgid "GPG fingerprint"
......@@ -1002,7 +1028,7 @@ msgstr "Modifier le solde"
msgid "No invoice"
msgstr "Pas de facture"
#: templates/users/profil.html:385 views.py:384
#: templates/users/profil.html:385 views.py:385
msgid "Add a ban"
msgstr "Ajouter un bannissement"
......@@ -1058,7 +1084,7 @@ msgstr " Ajouter une adresse mail"
msgid "Create a club or organisation"
msgstr "Créer un club ou une association"
#: templates/users/sidebar.html:39 views.py:134
#: templates/users/sidebar.html:39 views.py:135
msgid "Create a user"
msgstr "Créer un utilisateur"
......@@ -1095,178 +1121,178 @@ msgstr "Conditions Générales d'Utilisation"
msgid "Summary of the General Terms of Use"
msgstr "Résumé des Conditions Générales d'Utilisation"
#: views.py:122
#: views.py:123
#, python-format
msgid "The user %s was created, an email to set the password was sent."
msgstr ""
"L'utilisateur %s a été créé, un mail pour initialiser le mot de passe a été "
"envoyé."
#: views.py:151
#: views.py:152
#, python-format
msgid "The club %s was created, an email to set the password was sent."
msgstr ""
"Le club %s a été créé, un mail pour initialiser le mot de passe a été envoyé."
#: views.py:158
#: views.py:159
msgid "Create a club"
msgstr "Créer un club"
#: views.py:176
#: views.py:177
msgid "The club was edited."
msgstr "Le club a été modifié."
#: views.py:185
#: views.py:186
msgid "Edit the admins and members"
msgstr "Modifier les admins et les membres"
#: views.py:213
#: views.py:214
msgid "The user was edited."
msgstr "L'utilisateur a été modifié."
#: views.py:219
#: views.py:220
msgid "Edit the user"
msgstr "Modifier l'utilisateur"
#: views.py:233
#: views.py:234
msgid "The state was edited."
msgstr "L'état a été modifié."
#: views.py:239
#: views.py:240
msgid "Edit the state"
msgstr "Modifier l'état"
#: views.py:254
#: views.py:255
msgid "The groups were edited."
msgstr "Les groupes ont été modifiés."
#: views.py:276 views.py:1076
#: views.py:277 views.py:1077
msgid "The password was changed."
msgstr "Le mot de passe a été changé."
#: views.py:294
#: views.py:295
#, python-format
msgid "%s was removed from the group."
msgstr "%s a été retiré du groupe."
#: views.py:304
#: views.py:305
#, python-format
msgid "%s is no longer superuser."
msgstr "%s n'est plus superutilisateur."
#: views.py:317
#: views.py:318
msgid "The service user was created."
msgstr "L'utilisateur service a été créé."