diff --git a/.gitignore b/.gitignore index 438dfbcd..c65c2cc3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ re2o.png __pycache__/* static_files/* static/logo/* +media/* diff --git a/cotisations/forms.py b/cotisations/forms.py index 5845611a..c4e02397 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -26,9 +26,8 @@ importé par les views. Permet de créer une nouvelle facture pour un user (NewFactureForm), et de l'editer (soit l'user avec EditFactureForm, soit le trésorier avec TrezEdit qui a plus de possibilités que self -notamment sur le controle trésorier) - -SelectArticleForm est utilisée lors de la creation d'une facture en +notamment sur le controle trésorier SelectArticleForm est utilisée +lors de la creation d'une facture en parrallèle de NewFacture pour le choix des articles désirés. (la vue correspondante est unique) @@ -40,8 +39,10 @@ from __future__ import unicode_literals from django import forms from django.db.models import Q from django.forms import ModelForm, Form -from django.core.validators import MinValueValidator +from django.core.validators import MinValueValidator,MaxValueValidator from .models import Article, Paiement, Facture, Banque +from preferences.models import OptionalUser +from users.models import User from re2o.field_permissions import FieldPermissionFormMixin @@ -246,3 +247,58 @@ class DelBanqueForm(Form): self.fields['banques'].queryset = instances else: self.fields['banques'].queryset = Banque.objects.all() + + +class NewFactureSoldeForm(NewFactureForm): + """Creation d'une facture, moyen de paiement, banque et numero + de cheque""" + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + self.fields['cheque'].required = False + self.fields['banque'].required = False + self.fields['cheque'].label = 'Numero de chèque' + self.fields['banque'].empty_label = "Non renseigné" + self.fields['paiement'].empty_label = "Séléctionner\ + une bite de paiement" + paiement_list = Paiement.objects.filter(type_paiement=1) + if paiement_list: + self.fields['paiement'].widget\ + .attrs['data-cheque'] = paiement_list.first().id + + class Meta: + model = Facture + fields = ['paiement', 'banque'] + + + def clean(self): + cleaned_data = super(NewFactureSoldeForm, self).clean() + paiement = cleaned_data.get("paiement") + cheque = cleaned_data.get("cheque") + banque = cleaned_data.get("banque") + if not paiement: + raise forms.ValidationError("Le moyen de paiement est obligatoire") + elif paiement.type_paiement == "check" and not (cheque and banque): + raise forms.ValidationError("Le numéro de chèque et\ + la banque sont obligatoires.") + return cleaned_data + + +class RechargeForm(Form): + value = forms.FloatField( + label='Valeur', + min_value=0.01, + validators = [] + ) + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user') + super(RechargeForm, self).__init__(*args, **kwargs) + + def clean_value(self): + value = self.cleaned_data['value'] + options, _created = OptionalUser.objects.get_or_create() + if value < options.min_online_payment: + raise forms.ValidationError("Montant inférieur au montant minimal de paiement en ligne (%s) €" % options.min_online_payment) + if value + self.user.solde > options.max_solde: + raise forms.ValidationError("Le solde ne peux excéder %s " % options.max_solde) + return value diff --git a/cotisations/models.py b/cotisations/models.py index a775237c..07c3b8fa 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -289,7 +289,7 @@ class Vente(models.Model): if not user_request.has_perm('cotisations.change_vente'): return False, u"Vous n'avez pas le droit d'éditer les ventes" elif not user_request.has_perm('cotisations.change_all_facture') and not self.facture.user.can_edit(user_request, *args, **kwargs)[0]: - return False, u"Vous ne pouvez pas éditer les factures de cet user protégé" + return False, u"Vous ne pouvez pas éditer les factures de cet user protégé" elif not user_request.has_perm('cotisations.change_all_vente') and\ (self.facture.control or not self.facture.valid): return False, u"Vous n'avez pas le droit d'éditer une vente\ diff --git a/cotisations/payment.py b/cotisations/payment.py new file mode 100644 index 00000000..07cbe5dc --- /dev/null +++ b/cotisations/payment.py @@ -0,0 +1,113 @@ +"""Payment + +Here are defined some views dedicated to online payement. +""" +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.http import HttpResponse, HttpResponseBadRequest + +from collections import OrderedDict + +from preferences.models import AssoOption +from .models import Facture +from .payment_utils.comnpay import Payment as ComnpayPayment + +@csrf_exempt +@login_required +def accept_payment(request, factureid): + facture = get_object_or_404(Facture, id=factureid) + messages.success( + request, + "Le paiement de {} € a été accepté.".format(facture.prix()) + ) + return redirect(reverse('users:profil', kwargs={'userid':request.user.id})) + + +@csrf_exempt +@login_required +def refuse_payment(request): + messages.error( + request, + "Le paiement a été refusé." + ) + return redirect(reverse('users:profil', kwargs={'userid':request.user.id})) + +@csrf_exempt +def ipn(request): + option, _created = AssoOption.objects.get_or_create() + p = ComnpayPayment() + order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', ) + try: + data = OrderedDict([(f, request.POST[f]) for f in order]) + except MultiValueDictKeyError: + return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") + + if not p.validSec(data, option.payment_pass): + return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") + + result = True if (request.POST['result'] == 'OK') else False + idTpe = request.POST['idTpe'] + idTransaction = request.POST['idTransaction'] + + # On vérifie que le paiement nous est destiné + if not idTpe == option.payment_id: + return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") + + try: + factureid = int(idTransaction) + except ValueError: + return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") + + facture = get_object_or_404(Facture, id=factureid) + + # On vérifie que le paiement est valide + if not result: + # Le paiement a échoué : on effectue les actions nécessaires (On indique qu'elle a échoué) + facture.delete() + + # On notifie au serveur ComNPay qu'on a reçu les données pour traitement + return HttpResponse("HTTP/1.1 200 OK") + + facture.valid = True + facture.save() + + # A nouveau, on notifie au serveur qu'on a bien traité les données + return HttpResponse("HTTP/1.0 200 OK") + + +def comnpay(facture, request): + host = request.get_host() + option, _created = AssoOption.objects.get_or_create() + p = ComnpayPayment( + str(option.payment_id), + str(option.payment_pass), + 'https://' + host + reverse( + 'cotisations:accept_payment', + kwargs={'factureid':facture.id} + ), + 'https://' + host + reverse('cotisations:refuse_payment'), + 'https://' + host + reverse('cotisations:ipn'), + "", + "D" + ) + r = { + 'action' : 'https://secure.homologation.comnpay.com', + 'method' : 'POST', + 'content' : p.buildSecretHTML( + "Rechargement du solde", + facture.prix(), + idTransaction=str(facture.id) + ), + 'amount' : facture.prix, + } + return r + + +PAYMENT_SYSTEM = { + 'COMNPAY' : comnpay, + 'NONE' : None +} diff --git a/cotisations/payment_utils/__init__.py b/cotisations/payment_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cotisations/payment_utils/comnpay.py b/cotisations/payment_utils/comnpay.py new file mode 100644 index 00000000..6c1701d3 --- /dev/null +++ b/cotisations/payment_utils/comnpay.py @@ -0,0 +1,68 @@ +import time +from random import randrange +import base64 +import hashlib +from collections import OrderedDict +from itertools import chain + +class Payment(): + + vad_number = "" + secret_key = "" + urlRetourOK = "" + urlRetourNOK = "" + urlIPN = "" + source = "" + typeTr = "D" + + def __init__(self, vad_number = "", secret_key = "", urlRetourOK = "", urlRetourNOK = "", urlIPN = "", source="", typeTr="D"): + self.vad_number = vad_number + self.secret_key = secret_key + self.urlRetourOK = urlRetourOK + self.urlRetourNOK = urlRetourNOK + self.urlIPN = urlIPN + self.source = source + self.typeTr = typeTr + + def buildSecretHTML(self, produit="Produit", montant="0.00", idTransaction=""): + if idTransaction == "": + self.idTransaction = str(time.time())+self.vad_number+str(randrange(999)) + else: + self.idTransaction = idTransaction + + array_tpe = OrderedDict( + montant= str(montant), + idTPE= self.vad_number, + idTransaction= self.idTransaction, + devise= "EUR", + lang= 'fr', + nom_produit= produit, + source= self.source, + urlRetourOK= self.urlRetourOK, + urlRetourNOK= self.urlRetourNOK, + typeTr= str(self.typeTr) + ) + + if self.urlIPN!="": + array_tpe['urlIPN'] = self.urlIPN + + array_tpe['key'] = self.secret_key; + strWithKey = base64.b64encode(bytes('|'.join(array_tpe.values()), 'utf-8')) + del array_tpe["key"] + array_tpe['sec'] = hashlib.sha512(strWithKey).hexdigest() + + ret = "" + for key in array_tpe: + ret += '' + + return ret + + def validSec(self, values, secret_key): + if "sec" in values: + sec = values['sec'] + del values["sec"] + strWithKey = hashlib.sha512(base64.b64encode(bytes('|'.join(values.values()) +"|"+secret_key, 'utf-8'))).hexdigest() + return strWithKey.upper() == sec.upper() + else: + return False + diff --git a/cotisations/templates/cotisations/new_facture.html b/cotisations/templates/cotisations/new_facture.html index f2586e8b..9d466cee 100644 --- a/cotisations/templates/cotisations/new_facture.html +++ b/cotisations/templates/cotisations/new_facture.html @@ -34,6 +34,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}

Nouvelle facture

+

+ Solde de l'utilisateur : {{ user.solde }} € +

{% bootstrap_form factureform %} {{ venteform.management_form }} diff --git a/cotisations/templates/cotisations/new_facture_solde.html b/cotisations/templates/cotisations/new_facture_solde.html new file mode 100644 index 00000000..2efd8e81 --- /dev/null +++ b/cotisations/templates/cotisations/new_facture_solde.html @@ -0,0 +1,157 @@ + +{% 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 bootstrap3 %} +{% load staticfiles%} + +{% block title %}Création et modification de factures{% endblock %} + +{% block content %} +{% bootstrap_form_errors venteform.management_form %} + + + {% csrf_token %} +

Nouvelle facture

+ {{ venteform.management_form }} + +

Articles de la facture

+
+ {% for form in venteform.forms %} +
+ Article :   + {% bootstrap_form form label_class='sr-only' %} +   + +
+ {% endfor %} +
+ +

+ Prix total : 0,00 € +

+ {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} +
+ + + +{% endblock %} + diff --git a/cotisations/templates/cotisations/payment.html b/cotisations/templates/cotisations/payment.html new file mode 100644 index 00000000..46f26784 --- /dev/null +++ b/cotisations/templates/cotisations/payment.html @@ -0,0 +1,37 @@ +{% 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 bootstrap3 %} +{% load staticfiles%} + +{% block title %}Rechargement du solde{% endblock %} + +{% block content %} +

Recharger de {{ amount }} €

+
+ {{ content | safe }} + {% bootstrap_button "Payer" button_type="submit" icon="piggy-bank" %} +
+{% endblock %} diff --git a/cotisations/templates/cotisations/recharge.html b/cotisations/templates/cotisations/recharge.html new file mode 100644 index 00000000..d38b7614 --- /dev/null +++ b/cotisations/templates/cotisations/recharge.html @@ -0,0 +1,39 @@ +{% 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 bootstrap3 %} +{% load staticfiles%} + +{% block title %}Rechargement du solde{% endblock %} + +{% block content %} +

Rechargement du solde

+

Solde : {{ request.user.solde }} €

+
+ {% csrf_token %} + {% bootstrap_form rechargeform %} + {% bootstrap_button "Valider" button_type="submit" icon="piggy-bank" %} +
+{% endblock %} diff --git a/cotisations/urls.py b/cotisations/urls.py index 2a0c0163..0040e48c 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -26,6 +26,7 @@ from django.conf.urls import url import re2o from . import views +from . import payment urlpatterns = [ url(r'^new_facture/(?P[0-9]+)$', @@ -110,5 +111,25 @@ urlpatterns = [ views.control, name='control' ), + url(r'^new_facture_solde/(?P[0-9]+)$', + views.new_facture_solde, + name='new_facture_solde' + ), + url(r'^recharge/$', + views.recharge, + name='recharge' + ), + url(r'^payment/accept/(?P[0-9]+)$', + payment.accept_payment, + name='accept_payment' + ), + url(r'^payment/refuse/$', + payment.refuse_payment, + name='refuse_payment' + ), + url(r'^payment/ipn/$', + payment.ipn, + name='ipn' + ), url(r'^$', views.index, name='index'), ] diff --git a/cotisations/views.py b/cotisations/views.py index d7d953c9..16d25295 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -29,6 +29,7 @@ import os from django.urls import reverse from django.shortcuts import render, redirect from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.core.validators import MaxValueValidator from django.contrib.auth.decorators import login_required, permission_required from django.contrib import messages from django.db.models import ProtectedError @@ -36,6 +37,8 @@ from django.db import transaction from django.db.models import Q from django.forms import modelformset_factory, formset_factory from django.utils import timezone +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.debug import sensitive_variables from reversion import revisions as reversion from reversion.models import Version # Import des models, forms et fonctions re2o @@ -67,8 +70,11 @@ from .forms import ( NewFactureFormPdf, SelectUserArticleForm, SelectClubArticleForm, - CreditSoldeForm + CreditSoldeForm, + NewFactureSoldeForm, + RechargeForm ) +from . import payment from .tex import render_invoice @@ -584,3 +590,127 @@ def index(request): return render(request, 'cotisations/index.html', { 'facture_list': facture_list }) + + +@login_required +def new_facture_solde(request, userid): + """Creation d'une facture pour un user. Renvoie la liste des articles + et crée des factures dans un formset. Utilise un peu de js coté template + pour ajouter des articles. + Parse les article et boucle dans le formset puis save les ventes, + enfin sauve la facture parente. + TODO : simplifier cette fonction, déplacer l'intelligence coté models + Facture et Vente.""" + user = request.user + facture = Facture(user=user) + paiement, _created = Paiement.objects.get_or_create(moyen='Solde') + facture.paiement = paiement + # Le template a besoin de connaitre les articles pour le js + article_list = Article.objects.filter( + Q(type_user='All') | Q(type_user=request.user.class_name) + ) + if request.user.is_class_club: + article_formset = formset_factory(SelectClubArticleForm)(request.POST or None) + else: + article_formset = formset_factory(SelectUserArticleForm)(request.POST or None) + if article_formset.is_valid(): + articles = article_formset + # Si au moins un article est rempli + if any(art.cleaned_data for art in articles): + options, _created = OptionalUser.objects.get_or_create() + user_solde = options.user_solde + solde_negatif = options.solde_negatif + # Si on paye par solde, que l'option est activée, + # on vérifie que le négatif n'est pas atteint + if user_solde: + prix_total = 0 + for art_item in articles: + if art_item.cleaned_data: + prix_total += art_item.cleaned_data['article']\ + .prix*art_item.cleaned_data['quantity'] + if float(user.solde) - float(prix_total) < solde_negatif: + messages.error(request, "Le solde est insuffisant pour\ + effectuer l'opération") + return redirect(reverse( + 'users:profil', + kwargs={'userid': userid} + )) + with transaction.atomic(), reversion.create_revision(): + facture.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + for art_item in articles: + if art_item.cleaned_data: + article = art_item.cleaned_data['article'] + quantity = art_item.cleaned_data['quantity'] + new_vente = Vente.objects.create( + facture=facture, + name=article.name, + prix=article.prix, + type_cotisation=article.type_cotisation, + duration=article.duration, + number=quantity + ) + with transaction.atomic(), reversion.create_revision(): + new_vente.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + if any(art_item.cleaned_data['article'].type_cotisation + for art_item in articles if art_item.cleaned_data): + messages.success( + request, + "La cotisation a été prolongée\ + pour l'adhérent %s jusqu'au %s" % ( + user.pseudo, user.end_adhesion() + ) + ) + else: + messages.success(request, "La facture a été crée") + return redirect(reverse( + 'users:profil', + kwargs={'userid': userid} + )) + messages.error( + request, + u"Il faut au moins un article valide pour créer une facture" + ) + return redirect(reverse( + 'users:profil', + kwargs={'userid': userid} + )) + + return form({ + 'venteform': article_formset, + 'articlelist': article_list + }, 'cotisations/new_facture_solde.html', request) + + +@login_required +def recharge(request): + options, _created = AssoOption.objects.get_or_create() + if options.payment == 'NONE': + messages.error( + request, + "Le paiement en ligne est désactivé." + ) + return redirect(reverse( + 'users:profil', + kwargs={'userid': request.user.id} + )) + f = RechargeForm(request.POST or None, user=request.user) + if f.is_valid(): + facture = Facture(user=request.user) + paiement, _created = Paiement.objects.get_or_create(moyen='Rechargement en ligne') + facture.paiement = paiement + facture.valid = False + facture.save() + v = Vente.objects.create( + facture=facture, + name='solde', + prix=f.cleaned_data['value'], + number=1, + ) + v.save() + content = payment.PAYMENT_SYSTEM[options.payment](facture, request) + return render(request, 'cotisations/payment.html', content) + return form({'rechargeform':f}, 'cotisations/recharge.html', request) diff --git a/install_re2o.sh b/install_re2o.sh index 5c39345b..e2fc60ac 100755 --- a/install_re2o.sh +++ b/install_re2o.sh @@ -28,8 +28,9 @@ setup_ldap() { install_re2o_server() { - - +echo "Installation de Re2o ! +Cet utilitaire va procéder à l'installation initiale de re2o. Le serveur présent doit être vierge. +Preconfiguration..." export DEBIAN_FRONTEND=noninteractive @@ -107,7 +108,7 @@ clear if [ $sql_is_local == 2 ] -then +then TITLE="Login sql" sql_login=$(dialog --title "$TITLE" \ --backtitle "$BACKTITLE" \ @@ -169,7 +170,7 @@ ldap_password=$(dialog --title "$TITLE" \ 2>&1 >/dev/tty) clear if [ $ldap_is_local == 2 ] -then +then TITLE="Cn ldap admin" ldap_cn=$(dialog --title "$TITLE" \ --backtitle "$BACKTITLE" \ @@ -209,7 +210,7 @@ email_port=$(dialog --clear \ 2>&1 >/dev/tty) clear if [ $ldap_is_local == 2 ] -then +then TITLE="Cn ldap admin" ldap_cn=$(dialog --title "$TITLE" \ --backtitle "$BACKTITLE" \ @@ -236,7 +237,7 @@ install_base=$(dialog --clear \ $HEIGHT $WIDTH \ 2>&1 >/dev/tty) -apt-get -y install python3-django python3-dateutil texlive-latex-base texlive-fonts-recommended python3-djangorestframework python3-django-reversion python3-pip libsasl2-dev libldap2-dev libssl-dev +apt-get -y install python3-django python3-dateutil texlive-latex-base texlive-fonts-recommended python3-djangorestframework python3-django-reversion python3-pip libsasl2-dev libldap2-dev libssl-dev python3-crypto pip3 install django-bootstrap3 pip3 install django-ldapdb pip3 install django-macaddress @@ -253,7 +254,7 @@ then echo $mysql_command while true; do read -p "Continue (y/n)?" choice - case "$choice" in + case "$choice" in y|Y ) break;; n|N ) exit;; * ) echo "invalid";; @@ -276,14 +277,14 @@ else echo sudo -u postgres psql $pgsql_command3 while true; do read -p "Continue (y/n)?" choice - case "$choice" in + case "$choice" in y|Y ) break;; n|N ) exit;; * ) echo "invalid";; esac done fi -fi +fi if [ $ldap_is_local == 1 ] then @@ -430,7 +431,7 @@ if [ ! -z "$1" ] then if [ $1 == ldap ] then -if [ ! -z "$2" ] +if [ ! -z "$2" ] then echo Installation du ldap setup_ldap $2 $3 diff --git a/machines/models.py b/machines/models.py index 59965983..15864707 100644 --- a/machines/models.py +++ b/machines/models.py @@ -2153,3 +2153,4 @@ def srv_post_save(sender, **kwargs): def text_post_delete(sender, **kwargs): """Regeneration dns après modification d'un SRV""" regen('dns') + diff --git a/preferences/aes_field.py b/preferences/aes_field.py new file mode 100644 index 00000000..27b026f7 --- /dev/null +++ b/preferences/aes_field.py @@ -0,0 +1,55 @@ +import string +import binascii +from random import choice +from Crypto.Cipher import AES + +from django.db import models +from django.conf import settings + +EOD = '`%EofD%`' # This should be something that will not occur in strings + + +def genstring(length=16, chars=string.printable): + return ''.join([choice(chars) for i in range(length)]) + + +def encrypt(key, s): + obj = AES.new(key) + datalength = len(s) + len(EOD) + if datalength < 16: + saltlength = 16 - datalength + else: + saltlength = 16 - datalength % 16 + ss = ''.join([s, EOD, genstring(saltlength)]) + return obj.encrypt(ss) + + +def decrypt(key, s): + obj = AES.new(key) + ss = obj.decrypt(s) + print(ss) + return ss.split(bytes(EOD, 'utf-8'))[0] + + +class AESEncryptedField(models.CharField): + def save_form_data(self, instance, data): + setattr(instance, self.name, + binascii.b2a_base64(encrypt(settings.AES_KEY, data))) + + def to_python(self, value): + if value is None: + return None + return decrypt(settings.AES_KEY, + binascii.a2b_base64(value)).decode('utf-8') + + def from_db_value(self, value, expression, connection, *args): + if value is None: + return value + return decrypt(settings.AES_KEY, + binascii.a2b_base64(value)).decode('utf-8') + + def get_prep_value(self, value): + return binascii.b2a_base64(encrypt( + settings.AES_KEY, + value + )) diff --git a/preferences/forms.py b/preferences/forms.py index 7dda8620..fc214f52 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -48,6 +48,9 @@ class EditOptionalUserForm(ModelForm): téléphone' self.fields['user_solde'].label = 'Activation du solde pour\ les utilisateurs' + self.fields['max_solde'].label = 'Solde maximum' + self.fields['min_online_payment'].label = 'Montant de rechargement minimum en ligne' + self.fields['self_adhesion'].label = 'Auto inscription' class EditOptionalMachineForm(ModelForm): @@ -114,6 +117,7 @@ class EditGeneralOptionForm(ModelForm): self.fields['site_name'].label = 'Nom du site web' self.fields['email_from'].label = "Adresse mail d\ 'expedition automatique" + self.fields['GTU_sum_up'].label = "Résumé des CGU" class EditAssoOptionForm(ModelForm): diff --git a/preferences/migrations/0028_auto_20180111_1129.py b/preferences/migrations/0028_auto_20180111_1129.py new file mode 100644 index 00000000..c6c03719 --- /dev/null +++ b/preferences/migrations/0028_auto_20180111_1129.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-11 10:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0027_merge_20180106_2019'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='max_recharge', + field=models.DecimalField(decimal_places=2, default=100, max_digits=5), + ), + ] diff --git a/preferences/migrations/0029_auto_20180111_1134.py b/preferences/migrations/0029_auto_20180111_1134.py new file mode 100644 index 00000000..92220312 --- /dev/null +++ b/preferences/migrations/0029_auto_20180111_1134.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-11 10:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0028_auto_20180111_1129'), + ] + + operations = [ + migrations.AddField( + model_name='assooption', + name='payment', + field=models.CharField(choices=[('NONE', 'NONE'), ('COMNPAY', 'COMNPAY')], default='NONE', max_length=255), + ), + ] diff --git a/preferences/migrations/0030_auto_20180111_2346.py b/preferences/migrations/0030_auto_20180111_2346.py new file mode 100644 index 00000000..7f912bbd --- /dev/null +++ b/preferences/migrations/0030_auto_20180111_2346.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-11 22:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0029_auto_20180111_1134'), + ] + + operations = [ + migrations.RemoveField( + model_name='optionaluser', + name='max_recharge', + ), + migrations.AddField( + model_name='optionaluser', + name='max_solde', + field=models.DecimalField(decimal_places=2, default=50, max_digits=5), + ), + ] diff --git a/preferences/migrations/0031_optionaluser_self_adhesion.py b/preferences/migrations/0031_optionaluser_self_adhesion.py new file mode 100644 index 00000000..48a95044 --- /dev/null +++ b/preferences/migrations/0031_optionaluser_self_adhesion.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-12 11:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0030_auto_20180111_2346'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='self_adhesion', + field=models.BooleanField(default=False, help_text='Un nouvel utilisateur peut se créer son compte sur re2o'), + ), + ] diff --git a/preferences/migrations/0032_optionaluser_min_online_payment.py b/preferences/migrations/0032_optionaluser_min_online_payment.py new file mode 100644 index 00000000..ef78d012 --- /dev/null +++ b/preferences/migrations/0032_optionaluser_min_online_payment.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-13 16:43 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0031_optionaluser_self_adhesion'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='min_online_payment', + field=models.DecimalField(decimal_places=2, default=10, max_digits=5), + ), + ] diff --git a/preferences/migrations/0033_generaloption_gtu_sum_up.py b/preferences/migrations/0033_generaloption_gtu_sum_up.py new file mode 100644 index 00000000..63c2df5e --- /dev/null +++ b/preferences/migrations/0033_generaloption_gtu_sum_up.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 19:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0032_optionaluser_min_online_payment'), + ] + + operations = [ + migrations.AddField( + model_name='generaloption', + name='GTU_sum_up', + field=models.TextField(blank=True, default='', help_text='Résumé des CGU'), + ), + ] diff --git a/preferences/migrations/0034_auto_20180114_2025.py b/preferences/migrations/0034_auto_20180114_2025.py new file mode 100644 index 00000000..b6969021 --- /dev/null +++ b/preferences/migrations/0034_auto_20180114_2025.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 19:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0033_generaloption_gtu_sum_up'), + ] + + operations = [ + migrations.AddField( + model_name='generaloption', + name='GTU', + field=models.FileField(default='', upload_to='GTU'), + ), + migrations.AlterField( + model_name='generaloption', + name='GTU_sum_up', + field=models.TextField(blank=True, default=''), + ), + ] diff --git a/preferences/migrations/0035_auto_20180114_2132.py b/preferences/migrations/0035_auto_20180114_2132.py new file mode 100644 index 00000000..e3767828 --- /dev/null +++ b/preferences/migrations/0035_auto_20180114_2132.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 20:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0034_auto_20180114_2025'), + ] + + operations = [ + migrations.AlterField( + model_name='generaloption', + name='GTU', + field=models.FileField(default='', upload_to='/var/www/static/'), + ), + ] diff --git a/preferences/migrations/0036_auto_20180114_2141.py b/preferences/migrations/0036_auto_20180114_2141.py new file mode 100644 index 00000000..1b844ac8 --- /dev/null +++ b/preferences/migrations/0036_auto_20180114_2141.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 20:41 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0035_auto_20180114_2132'), + ] + + operations = [ + migrations.AlterField( + model_name='generaloption', + name='GTU', + field=models.FileField(default='', upload_to=''), + ), + ] diff --git a/preferences/migrations/0037_auto_20180114_2156.py b/preferences/migrations/0037_auto_20180114_2156.py new file mode 100644 index 00000000..efafa806 --- /dev/null +++ b/preferences/migrations/0037_auto_20180114_2156.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 20:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0036_auto_20180114_2141'), + ] + + operations = [ + migrations.AlterField( + model_name='generaloption', + name='GTU', + field=models.FileField(default='', null=True, upload_to=''), + ), + ] diff --git a/preferences/migrations/0038_auto_20180114_2209.py b/preferences/migrations/0038_auto_20180114_2209.py new file mode 100644 index 00000000..3077ebff --- /dev/null +++ b/preferences/migrations/0038_auto_20180114_2209.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 21:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0037_auto_20180114_2156'), + ] + + operations = [ + migrations.AlterField( + model_name='generaloption', + name='GTU', + field=models.FileField(blank=True, default='', null=True, upload_to=''), + ), + ] diff --git a/preferences/migrations/0039_auto_20180115_0003.py b/preferences/migrations/0039_auto_20180115_0003.py new file mode 100644 index 00000000..da1b6631 --- /dev/null +++ b/preferences/migrations/0039_auto_20180115_0003.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 23:03 +from __future__ import unicode_literals + +from django.db import migrations, models +import preferences.aes_field + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0038_auto_20180114_2209'), + ] + + operations = [ + migrations.AddField( + model_name='assooption', + name='payment_id', + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name='assooption', + name='payment_pass', + field=preferences.aes_field.AESEncryptedField(max_length=255, null=True), + ), + ] diff --git a/preferences/migrations/0040_auto_20180115_0010.py b/preferences/migrations/0040_auto_20180115_0010.py new file mode 100644 index 00000000..407cb831 --- /dev/null +++ b/preferences/migrations/0040_auto_20180115_0010.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 23:10 +from __future__ import unicode_literals + +from django.db import migrations, models +import preferences.aes_field + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0039_auto_20180115_0003'), + ] + + operations = [ + migrations.AlterField( + model_name='assooption', + name='payment_id', + field=models.CharField(default='', max_length=255), + ), + migrations.AlterField( + model_name='assooption', + name='payment_pass', + field=preferences.aes_field.AESEncryptedField(default='', max_length=255), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 8dfc4260..d4ef9645 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -28,6 +28,8 @@ from __future__ import unicode_literals from django.db import models import cotisations.models +from .aes_field import AESEncryptedField + class OptionalUser(models.Model): """Options pour l'user : obligation ou nom du telephone, @@ -41,11 +43,25 @@ class OptionalUser(models.Model): decimal_places=2, default=0 ) + max_solde = models.DecimalField( + max_digits=5, + decimal_places=2, + default=50 + ) + min_online_payment = models.DecimalField( + max_digits=5, + decimal_places=2, + default=10 + ) gpg_fingerprint = models.BooleanField(default=True) all_can_create = models.BooleanField( default=False, help_text="Tous les users peuvent en créer d'autres", ) + self_adhesion = models.BooleanField( + default=False, + help_text="Un nouvel utilisateur peut se créer son compte sur re2o" + ) class Meta: permissions = ( @@ -107,7 +123,10 @@ class OptionalUser(models.Model): def clean(self): """Creation du mode de paiement par solde""" if self.user_solde: - cotisations.models.Paiement.objects.get_or_create(moyen="Solde") + p = cotisations.models.Paiement.objects.filter(moyen="Solde") + if not len(p): + c = cotisations.models.Paiement(moyen="Solde") + c.save() class OptionalMachine(models.Model): @@ -285,6 +304,16 @@ class GeneralOption(models.Model): req_expire_hrs = models.IntegerField(default=48) site_name = models.CharField(max_length=32, default="Re2o") email_from = models.EmailField(default="www-data@serveur.net") + GTU_sum_up = models.TextField( + default="", + blank=True, + ) + GTU = models.FileField( + upload_to = '', + default="", + null=True, + blank=True, + ) class Meta: permissions = ( @@ -436,6 +465,23 @@ class AssoOption(models.Model): blank=True, null=True ) + PAYMENT = ( + ('NONE', 'NONE'), + ('COMNPAY', 'COMNPAY'), + ) + payment = models.CharField(max_length=255, + choices=PAYMENT, + default='NONE', + ) + payment_id = models.CharField( + max_length=255, + default='', + ) + payment_pass = AESEncryptedField( + max_length=255, + default='', + ) + class Meta: permissions = ( diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 7802929d..e46564a3 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -54,7 +54,17 @@ with this program; if not, write to the Free Software Foundation, Inc., Creations d'users par tous {{ useroptions.all_can_create }} + Auto inscription + {{ useroptions.self_adhesion }} + {% if useroptions.user_solde %} + + Solde maximum + {{ useroptions.max_solde }} + Montant minimal de rechargement en ligne + {{ useroptions.min_online_payment }} + + {% endif %}

Préférences machines

@@ -127,7 +137,13 @@ with this program; if not, write to the Free Software Foundation, Inc., Message global affiché sur le site {{ generaloptions.general_message }} + Résumé des CGU + {{ generaloptions.GTU_sum_up }} + + CGU + {{generaloptions.GTU}} +

Données de l'association

@@ -159,7 +175,10 @@ with this program; if not, write to the Free Software Foundation, Inc., Objet utilisateur de l'association {{ assooptions.utilisateur_asso }} + Moyen de paiement automatique + {{ assooptions.payment }} +

Messages personalisé dans les mails

diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html index 02f006c1..8d75f289 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -33,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Edition des préférences

-
+ {% csrf_token %} {% massive_bootstrap_form options 'utilisateur_asso' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} diff --git a/preferences/views.py b/preferences/views.py index c7ee5202..7a609a76 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -95,6 +95,7 @@ def edit_options(request, section): return redirect(reverse('index')) options = form_instance( request.POST or None, + request.FILES or None, instance=options_instance ) if options.is_valid(): diff --git a/re2o/acl.py b/re2o/acl.py index ffbbea42..23636ee4 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -45,11 +45,10 @@ def can_create(model): def decorator(view): def wrapper(request, *args, **kwargs): can, msg = model.can_create(request.user, *args, **kwargs) + #options, _created = OptionalUser.objects.get_or_create() if not can: messages.error(request, msg or "Vous ne pouvez pas accéder à ce menu") - return redirect(reverse('users:profil', - kwargs={'userid':str(request.user.id)} - )) + return redirect(reverse('index')) return view(request, *args, **kwargs) return wrapper return decorator diff --git a/re2o/settings.py b/re2o/settings.py index c342ef93..f88bd266 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -150,7 +150,7 @@ STATICFILES_DIRS = ( ), ) -MEDIA_ROOT = '/var/www/re2o/static' +MEDIA_ROOT = '/var/www/re2o/media' STATIC_URL = '/static/' diff --git a/re2o/settings_local.example.py b/re2o/settings_local.example.py index 107fd18f..26c1317d 100644 --- a/re2o/settings_local.example.py +++ b/re2o/settings_local.example.py @@ -26,6 +26,10 @@ SECRET_KEY = 'SUPER_SECRET_KEY' DB_PASSWORD = 'SUPER_SECRET_DB' +# AES key for secret key encryption +AES_KEY = 'WHAT_A_WONDERFULL_KEY' + + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False diff --git a/re2o/templatetags/self_adhesion.py b/re2o/templatetags/self_adhesion.py new file mode 100644 index 00000000..e1577a13 --- /dev/null +++ b/re2o/templatetags/self_adhesion.py @@ -0,0 +1,30 @@ +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2017 Gabriel Détraz +# Copyright © 2017 Goulven Kermarec +# Copyright © 2017 Augustin Lemesle +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from django import template +from preferences.models import OptionalUser, GeneralOption + +register = template.Library() + +@register.simple_tag +def self_adhesion(): + options, _created = OptionalUser.objects.get_or_create() + return options.self_adhesion diff --git a/requirements.txt b/requirements.txt index 7b5aa474..32957784 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ django-bootstrap3 django-macaddress python-dateutil +pycrypto diff --git a/templates/base.html b/templates/base.html index b3a9bb5d..0a0c3db0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -26,6 +26,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {# Load the tag library #} {% load bootstrap3 %} {% load acl %} +{% load self_adhesion %} +{% self_adhesion as var_sa %} @@ -102,17 +104,26 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_view_app preferences %}