diff --git a/.gitignore b/.gitignore index 2e8c3f4..8e15950 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ tags # End of https://www.gitignore.io/api/vim,git,django +.vscode \ No newline at end of file diff --git a/coopeV3/settings.py b/coopeV3/settings.py index 8b51238..0cb6d72 100644 --- a/coopeV3/settings.py +++ b/coopeV3/settings.py @@ -37,12 +37,13 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.admindocs', 'gestion', 'users', 'preferences', 'coopeV3', 'dal', - 'dal_select2', + 'dal_select2', ] MIDDLEWARE = [ @@ -129,4 +130,4 @@ STATICFILES_DIRS = [ ] STATIC_ROOT = os.path.join(BASE_DIR, 'static') - +LOGIN_URL = '/users/login' \ No newline at end of file diff --git a/coopeV3/urls.py b/coopeV3/urls.py index 62c9708..83de9b1 100644 --- a/coopeV3/urls.py +++ b/coopeV3/urls.py @@ -20,8 +20,9 @@ from . import views urlpatterns = [ path('', views.home, name="home"), + path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/', admin.site.urls), path('users/', include('users.urls')), path('gestion/', include('gestion.urls')), - path('preferences/', include('preferences.urls')), + path('preferences/', include('preferences.urls')), ] diff --git a/gestion/admin.py b/gestion/admin.py index ff8c544..65d67ea 100644 --- a/gestion/admin.py +++ b/gestion/admin.py @@ -1,8 +1,9 @@ from django.contrib import admin -from .models import Reload, Refund, Product, Keg +from .models import Reload, Refund, Product, Keg, ConsumptionHistory admin.site.register(Reload) admin.site.register(Refund) admin.site.register(Product) admin.site.register(Keg) +admin.site.register(ConsumptionHistory) \ No newline at end of file diff --git a/gestion/forms.py b/gestion/forms.py index ebdd877..8d3250c 100644 --- a/gestion/forms.py +++ b/gestion/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.core.exceptions import ValidationError from django.contrib.auth.models import User from dal import autocomplete @@ -8,14 +9,38 @@ from preferences.models import PaymentMethod from coopeV3.widgets import SearchField class ReloadForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super(ReloadForm, self).__init__(*args, **kwargs) + self.fields['PaymentMethod'].queryset = PaymentMethod.objects.filter(is_usable_in_reload=True) + class Meta: model = Reload fields = ("customer", "amount", "PaymentMethod") + widgets = {'customer': autocomplete.ModelSelect2(url='users:active-users-autocomplete', attrs={'data-minimum-input-length':2})} + + def clean_amount(self): + if self.cleaned_data['amount'] <= 0: + raise ValidationError( + "Le montant doit être strictement positif" + ) + else: + return self.cleaned_data['amount'] + class RefundForm(forms.ModelForm): class Meta: model = Refund fields = ("customer", "amount") + widgets = {'customer': autocomplete.ModelSelect2(url='users:active-users-autocomplete', attrs={'data-minimum-input-length':2})} + + def clean_amount(self): + if self.cleaned_data['amount'] <= 0: + raise ValidationError( + "Le montant doit être strictement positif" + ) + else: + return self.cleaned_data['amount'] + class ProductForm(forms.ModelForm): class Meta: @@ -32,6 +57,11 @@ class MenuForm(forms.ModelForm): model = Menu fields = "__all__" +class SearchProductForm(forms.Form): + product = forms.ModelChoiceField(queryset=Product.objects.all(), required=True, label="Produit", widget=autocomplete.ModelSelect2(url='gestion:products-autocomplete', attrs={'data-minimum-input-length':2})) + +class SearchMenuForm(forms.Form): + menu = forms.ModelChoiceField(queryset=Menu.objects.all(), required=True, label="Menu", widget=autocomplete.ModelSelect2(url='gestion:menus-autocomplete', attrs={'data-minimum-input-length':2})) + class GestionForm(forms.Form): - client = forms.ModelChoiceField(queryset=User.objects.filter(is_active=True), required=True, label="Client", widget=autocomplete.ModelSelect2(url='users:active-users-autocomplete', attrs={'data-minimum-input-length':2})) - paymentMethod = forms.ModelChoiceField(queryset=PaymentMethod.objects.all(), required=True, label="Moyen de paiement") + client = forms.ModelChoiceField(queryset=User.objects.filter(is_active=True), required=True, label="Client", widget=autocomplete.ModelSelect2(url='users:active-users-autocomplete', attrs={'data-minimum-input-length':2})) \ No newline at end of file diff --git a/gestion/models.py b/gestion/models.py index b4fffc6..94da081 100644 --- a/gestion/models.py +++ b/gestion/models.py @@ -138,7 +138,7 @@ class MenuHistory(models.Model): coopeman = models.ForeignKey(User, on_delete=models.PROTECT, related_name="menu_selled") def __str__(self): - return "{2} a consommé {0} {1}".format(self.quantite, self.menu, self.client) + return "{2} a consommé {0} {1}".format(self.quantity, self.menu, self.customer) class ConsumptionHistory(models.Model): customer = models.ForeignKey(User, on_delete=models.PROTECT, related_name="consumption_taken") diff --git a/gestion/templates/gestion/manage.html b/gestion/templates/gestion/manage.html index ca522d7..8f1eec4 100644 --- a/gestion/templates/gestion/manage.html +++ b/gestion/templates/gestion/manage.html @@ -61,7 +61,7 @@ 0€ 0€ 0€ - + {% for pm in pay_buttons %} {% endfor %} @@ -93,11 +93,11 @@ - {% for produit in bieresPression %} + {% for product in bieresPression %} {% if forloop.counter0|divisibleby:4 %} {% endif %} - + {% if forloop.counter|divisibleby:4 %} {% endif %} @@ -106,11 +106,11 @@ {% endif %} - {% for produit in bieresBouteille %} + {% for product in bieresBouteille %} {% if forloop.counter0|divisibleby:4 %} {% endif %} - + {% if forloop.counter|divisibleby:4 %} {% endif %} @@ -119,11 +119,11 @@ {% endif %} - {% for produit in panini %} + {% for product in panini %} {% if forloop.counter0|divisibleby:4 %} {% endif %} - + {% if forloop.counter|divisibleby:4 %} {% endif %} @@ -132,11 +132,11 @@ {% endif %} - {% for produit in soft %} + {% for product in soft %} {% if forloop.counter0|divisibleby:4 %} {% endif %} - + {% if forloop.counter|divisibleby:4 %} {% endif %} @@ -146,11 +146,11 @@ {% endif %} - {% for produit in autreBouffe %} + {% for product in autreBouffe %} {% if forloop.counter0|divisibleby:4 %} {% endif %} - + {% if forloop.counter|divisibleby:4 %} {% endif %} @@ -160,11 +160,11 @@ {% endif %} {% if menus %} - {% for produit in menus %} + {% for product in menus %} {% if forloop.counter0|divisibleby:4 %} {% endif %} - + {% if forloop.counter|divisibleby:4 %} {% endif %} @@ -208,5 +208,7 @@ {% endif %} {{gestion_form.media}} +{{reload_form.media}} +{{refund_form.media}} {%endblock%} diff --git a/gestion/templates/gestion/products_index.html b/gestion/templates/gestion/products_index.html index 629dbe8..049d3cf 100644 --- a/gestion/templates/gestion/products_index.html +++ b/gestion/templates/gestion/products_index.html @@ -16,8 +16,8 @@ Actions possibles :
@@ -26,9 +26,10 @@ Actions possibles :
diff --git a/gestion/templates/gestion/products_list.html b/gestion/templates/gestion/products_list.html index 0c26c7f..eb96d41 100644 --- a/gestion/templates/gestion/products_list.html +++ b/gestion/templates/gestion/products_list.html @@ -13,43 +13,40 @@

Liste des produits

- Actions possibles : - -
-
-
-

Futs

-
- Actions possibles : - -
-
-
-

Menus

-
- Actions possibles : - -
-
-
-

Stocks

-
- Actions possibles : - + Créer un produit

+
+
Bières pression
Bières bouteilles
Paninis
Softs
Bouffe
Menus
+ + + + + + + + + + + + + + + + {% for product in products %} + + + + + + + + + + + + + {% endfor %} + +
NomPrixStock (soute)Stock (bar)Code barreCatégorieActifDegréVolumeAdministrer
{{ product.name }}{{ product.amount}}{{ product.stockHold }}{{ product.stockBar }}{{ product.barcode }}{{ product.category }}{{ product.is_active }}{{ product.degree }}{{ product.volume }}
+ {% endblock %} diff --git a/gestion/urls.py b/gestion/urls.py index 3e0e17b..a400287 100644 --- a/gestion/urls.py +++ b/gestion/urls.py @@ -8,8 +8,15 @@ urlpatterns = [ path('reload', views.reload, name="reload"), path('refund', views.refund, name="refund"), path('productsIndex', views.productsIndex, name="productsIndex"), + path('productsList', views.productsList, name="productsList"), path('addProduct', views.addProduct, name="addProduct"), path('addKeg', views.addKeg, name="addKeg"), path('addMenu', views.addMenu, name="addMenu"), path('getProduct/', views.getProduct, name="getProduct"), -] + path('order', views.order, name="order"), + path('ranking', views.ranking, name="ranking"), + path('annualRanking', views.annualRanking, name="annualRanking"), + path('searchProduct', views.searchProduct, name="searchProduct"), + path('productProfile/', views.productProfile, name="productProfile"), + path('products-autocomplete', views.ProductsAutocomplete.as_view(), name="products-autocomplete"), +] \ No newline at end of file diff --git a/gestion/views.py b/gestion/views.py index 59e8b6d..bd4d23b 100644 --- a/gestion/views.py +++ b/gestion/views.py @@ -1,16 +1,26 @@ -from django.shortcuts import render, redirect +from django.shortcuts import render, redirect, get_object_or_404 from django.contrib import messages from django.urls import reverse -from django.http import HttpResponse +from django.http import HttpResponse, Http404 from django.contrib.auth.models import User +from django.views.decorators.csrf import csrf_exempt +from django.contrib.auth.decorators import login_required, permission_required -import json +from coopeV3.acl import active_required, acl_or + +import simplejson as json from dal import autocomplete +from decimal import * -from .forms import ReloadForm, RefundForm, ProductForm, KegForm, MenuForm, GestionForm -from .models import Product, Menu, Keg +from .forms import ReloadForm, RefundForm, ProductForm, KegForm, MenuForm, GestionForm, SearchMenuForm, SearchProductForm +from .models import Product, Menu, Keg, ConsumptionHistory +from preferences.models import PaymentMethod +@active_required +@login_required +@acl_or('gestion.add_consumptionhistory', 'gestion.add_reload', 'gestion.add_refund') def manage(request): + pay_buttons = PaymentMethod.objects.filter(is_active=True) gestion_form = GestionForm(request.POST or None) reload_form = ReloadForm(request.POST or None) refund_form = RefundForm(request.POST or None) @@ -28,8 +38,37 @@ def manage(request): bieresPression.append(keg.demi) if(keg.galopin): bieresPression.append(keg.galopin) - return render(request, "gestion/manage.html", {"gestion_form": gestion_form, "reload_form": reload_form, "refund_form": refund_form, "bieresPression": bieresPression, "bieresBouteille": bieresBouteille, "panini": panini, "food": food, "soft": soft, "menus": menus}) + return render(request, "gestion/manage.html", {"gestion_form": gestion_form, "reload_form": reload_form, "refund_form": refund_form, "bieresPression": bieresPression, "bieresBouteille": bieresBouteille, "panini": panini, "food": food, "soft": soft, "menus": menus, "pay_buttons": pay_buttons}) +@login_required +@permission_required('gestion.add_consumptionhistory') +@csrf_exempt +def order(request): + print(request.POST) + if("user" not in request.POST or "paymentMethod" not in request.POST or "amount" not in request.POST or "order" not in request.POST): + raise Http404("Erreur du POST") + else: + user = get_object_or_404(User, pk=request.POST['user']) + paymentMethod = get_object_or_404(PaymentMethod, pk=request.POST['paymentMethod']) + amount = Decimal(request.POST['amount']) + order = json.loads(request.POST["order"]) + if(len(order) == 0 or amount == 0): + raise Http404("Pas de commande") + if(paymentMethod.affect_balance): + if(user.profile.balance < amount): + raise Http404("Solde inférieur au prix de la commande") + else: + user.profile.debit += amount + user.save() + for o in order: + print(o) + product = get_object_or_404(Product, pk=o["pk"]) + ch = ConsumptionHistory(customer = user, quantity = int(o["quantity"]), paymentMethod=paymentMethod, product=product, amount=int(o["quantity"])*product.amount, coopeman=request.user) + ch.save() + return HttpResponse("La commande a bien été effectuée") + +@login_required +@permission_required('gestion.add_reload') def reload(request): reload_form = ReloadForm(request.POST or None) if(reload_form.is_valid()): @@ -45,6 +84,8 @@ def reload(request): messages.error(request, "Le rechargement a échoué") return redirect(reverse('gestion:manage')) +@login_required +@permission_required('gestion.add_refund') def refund(request): refund_form = RefundForm(request.POST or None) if(refund_form.is_valid()): @@ -63,9 +104,15 @@ def refund(request): messages.error(request, "Le remboursement a échoué") return redirect(reverse('gestion:manage')) +########## Products ########## + +@login_required +@acl_or('gestion.add_product', 'gestion.view_product', 'gestion.add_keg', 'gestion.view_keg', 'gestion.change_keg', 'gestion.view_menu', 'gestion.add_menu') def productsIndex(request): return render(request, "gestion/products_index.html") +@login_required +@permission_required('gestion.add_product') def addProduct(request): form = ProductForm(request.POST or None) if(form.is_valid()): @@ -74,18 +121,43 @@ def addProduct(request): return redirect(reverse('gestion:productsIndex')) return render(request, "form.html", {"form": form, "form_title": "Ajout d'un produit", "form_button": "Ajouter"}) +@login_required +@permission_required('gestion.view_product') def productsList(request): products = Product.objects.all() return render(request, "gestion/products_list.html", {"products": products}) +@login_required +@permission_required('gestion.view_product') +def searchProduct(request): + form = SearchProductForm(request.POST or None) + if(form.is_valid()): + return redirect(reverse('gestion:productProfile', kwargs={'pk': form.cleaned_data['product'].pk })) + return render(request, "form.html", {"form": form, "form_title":"Rechercher un produit", "form_button": "Rechercher"}) + +@login_required +@permission_required('gestion.view_product') +def productProfile(request, pk): + product = get_object_or_404(Product, pk=pk) + return render(request, "gestion/product_profile.html", {"product": product}) + +@login_required def getProduct(request, barcode): product = Product.objects.get(barcode=barcode) - data = json.dumps({"pk": product.pk, "barcode" : product.barcode, "name": product.name, "amount" : float(product.amount)}) + data = json.dumps({"pk": product.pk, "barcode" : product.barcode, "name": product.name, "amount" : product.amount}) return HttpResponse(data, content_type='application/json') +class ProductsAutocomplete(autocomplete.Select2QuerySetView): + def get_queryset(self): + qs = Product.objects.all() + if self.q: + qs = qs.filter(name__istartswith=self.q) + return qs ########## Kegs ########## +@login_required +@permission_required('gestion.add_keg') def addKeg(request): form = KegForm(request.POST or None) if(form.is_valid()): @@ -97,6 +169,8 @@ def addKeg(request): ########## Menus ########## +@login_required +@permission_required('gestion.add_menu') def addMenu(request): form = MenuForm(request.POST or None) extra_css = "#id_articles{height:200px;}" @@ -106,3 +180,53 @@ def addMenu(request): return redirect(reverse('gestion:productsIndex')) return render(request, "form.html", {"form":form, "form_title": "Ajout d'un menu", "form_button": "Ajouter", "extra_css": extra_css}) + +@login_required +@permission_required('gestion.view_menu') +def searchMenu(request): + """ + Search a menu via SearchMenuForm instance + + **Context** + + ``form_entete`` + The form title. + + ``form`` + The SearchMenuForm instance. + + ``form_button`` + The content of the form button. + + **Template** + + :template:`form.html` + """ + form = SearchMenuForm(request.POST or None) + if(form.is_valid()): + menu = form.menu + return redirect(reverse('gestion:changeMenu', kwargs={'pk':menu.pk})) + return render(request, "form.html", {"form": form, "form_title": "Recherche d'un menu", "form_button": "Modifier"}) + +class MenusAutocomplete(autocomplete.Select2QuerySetView): + def get_queryset(self): + qs = Menu.objects.all() + if self.q: + qs = qs.filter(name__istartswith=self.q) + return qs +########## Ranking ########## + +@login_required +def ranking(request): + bestBuyers = User.objects.order_by('-profile__debit')[:25] + customers = User.objects.all() + list = [] + for customer in customers: + alcohol = customer.profile.alcohol + list.append([customer, alcohol]) + bestDrinkers = sorted(list, key=lambda x: x[1], reverse=True)[:25] + return render(request, "gestion/ranking.html", {"bestBuyers": bestBuyers, "bestDrinkers": bestDrinkers}) + +@login_required +def annualRanking(request): + return render(request, "gestion/annual_ranking.html") \ No newline at end of file diff --git a/preferences/forms.py b/preferences/forms.py index 6271cab..52a197e 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.core.exceptions import ValidationError from .models import Cotisation, PaymentMethod, GeneralPreferences @@ -6,6 +7,14 @@ class CotisationForm(forms.ModelForm): class Meta: model = Cotisation fields = "__all__" + + def clean_amount(self): + if self.cleaned_data['amount'] <= 0: + raise ValidationError( + "Le montant doit être strictement positif" + ) + else: + return self.cleaned_data['amount'] class PaymentMethodForm(forms.ModelForm): class Meta: diff --git a/preferences/models.py b/preferences/models.py index 0c5dbb7..4502f93 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -3,7 +3,8 @@ from django.db import models class PaymentMethod(models.Model): name = models.CharField(max_length=255, verbose_name="Nom") is_active = models.BooleanField(default=True, verbose_name="Actif") - is_usable_in_cotisation = models.BooleanField(default=True, verbose_name="Utilisable pour les cotisations") + is_usable_in_cotisation = models.BooleanField(default=True, verbose_name="Cotisations ?") + is_usable_in_reload = models.BooleanField(default=True, verbose_name="Rechargements ?") affect_balance = models.BooleanField(default=False, verbose_name="Affecte le solde") def __str__(self): diff --git a/preferences/templates/preferences/payment_methods_index.html b/preferences/templates/preferences/payment_methods_index.html index f5ef170..8bd9bcc 100644 --- a/preferences/templates/preferences/payment_methods_index.html +++ b/preferences/templates/preferences/payment_methods_index.html @@ -17,7 +17,8 @@ Nom Actif ? - Utilisable dans les cotisations + Cotisations ? + Rechargements ? Affecte le solde Administration @@ -28,6 +29,7 @@ {{ pm.name }} {{ pm.is_active | yesno:"Oui, Non"}} {{ pm.is_usable_in_cotisation | yesno:"Oui, Non" }} + {{ pm.is_usable_in_reload | yesno:"Oui, Non" }} {{ pm.affect_balance | yesno:"Oui, Non" }} Modifier Supprimer diff --git a/preferences/views.py b/preferences/views.py index c16aa75..58ff90b 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -1,11 +1,17 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.contrib import messages from django.urls import reverse +from django.contrib.auth.decorators import login_required, permission_required + +from coopeV3.acl import active_required from .models import GeneralPreferences, Cotisation, PaymentMethod from .forms import CotisationForm, PaymentMethodForm, GeneralPreferencesForm +@active_required +@login_required +@permission_required('preferences.add_generalpreferences') def generalPreferences(request): gp,_ = GeneralPreferences.objects.get_or_create(pk=1) form = GeneralPreferencesForm(request.POST or None, instance=gp) @@ -15,10 +21,16 @@ def generalPreferences(request): ########## Cotisations ########## +@active_required +@login_required +@permission_required('preferences.view_cotisation') def cotisationsIndex(request): cotisations = Cotisation.objects.all() return render(request, "preferences/cotisations_index.html", {"cotisations": cotisations}) +@active_required +@login_required +@permission_required('preferences.add_cotisation') def addCotisation(request): form = CotisationForm(request.POST or None) if(form.is_valid()): @@ -27,6 +39,9 @@ def addCotisation(request): return redirect(reverse('preferences:cotisationsIndex')) return render(request, "form.html", {"form": form, "form_title": "Création d'une cotisation", "form_button": "Créer"}) +@active_required +@login_required +@permission_required('preferences.change_cotisation') def editCotisation(request, pk): cotisation = get_object_or_404(Cotisation, pk=pk) form = CotisationForm(request.POST or None, instance=cotisation) @@ -36,6 +51,9 @@ def editCotisation(request, pk): return redirect(reverse('preferences:cotisationsIndex')) return render(request, "form.html", {"form": form, "form_title": "Modification d'une cotisation", "form_button": "Modifier"}) +@active_required +@login_required +@permission_required('preferences.delete_cotisation') def deleteCotisation(request,pk): cotisation = get_object_or_404(Cotisation, pk=pk) message = "La cotisation (" + str(cotisation.duration) + " jours, " + str(cotisation.amount) + "€) a bien été supprimée" @@ -46,10 +64,16 @@ def deleteCotisation(request,pk): ########## Payment Methods ########## +@active_required +@login_required +@permission_required('preferences.view_paymentmethod') def paymentMethodsIndex(request): paymentMethods = PaymentMethod.objects.all() return render(request, "preferences/payment_methods_index.html", {"paymentMethods": paymentMethods}) +@active_required +@login_required +@permission_required('preferences.add_paymentmethod') def addPaymentMethod(request): form = PaymentMethodForm(request.POST or None) if(form.is_valid()): @@ -58,6 +82,9 @@ def addPaymentMethod(request): return redirect(reverse('preferences:paymentMethodsIndex')) return render(request, "form.html", {"form": form, "form_title": "Création d'un moyen de paiement", "form_button": "Créer"}) +@active_required +@login_required +@permission_required('preferences.change_paymentmethod') def editPaymentMethod(request, pk): paymentMethod = get_object_or_404(PaymentMethod, pk=pk) form = PaymentMethodForm(request.POST or None, instance=paymentMethod) @@ -67,9 +94,12 @@ def editPaymentMethod(request, pk): return redirect(reverse('preferences:paymentMethodsIndex')) return render(request, "form.html", {"form": form, "form_title": "Modification d'un moyen de paiement", "form_button": "Modifier"}) +@active_required +@login_required +@permission_required('preferences.delete_paymentmethod') def deletePaymentMethod(request,pk): paymentMethod = get_object_or_404(PaymentMethod, pk=pk) message = "Le moyen de paiement " + paymentMethod.name + " a bien été supprimé" paymentMethod.delete() messages.success(request, message) - return redirect(reverse('preferences:paymentMethodsIndex')) + return redirect(reverse('preferences:paymentMethodsIndex')) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 226d374..46ba27e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ Django==2.1 django-autocomplete-light==3.3.2 pytz==2018.5 +simplejson==3.16.0 +docutils==0.14 \ No newline at end of file diff --git a/staticfiles/manage.js b/staticfiles/manage.js index dbd3ce3..5d1c896 100644 --- a/staticfiles/manage.js +++ b/staticfiles/manage.js @@ -1,4 +1,4 @@ -totalAmount = 0 +total = 0 products = [] paymentMethod = null balance = 0 @@ -64,4 +64,16 @@ $(document).ready(function(){ window.location.reload() }); }); + $(".pay_button").click(function(){ + alert('Tentative de paiment avec le moyen ' + $(this).attr('data-payment')); + console.log(products) + $.post("order", {"user":id, "paymentMethod": $(this).attr('data-payment'), "order_length": products.length, "order": JSON.stringify(products), "amount": total}, function(data){ + alert(data); + location.reload(); + }).fail(function(data){ + alert("Impossible d'effectuer la transaction"); + location.reload(); + }); + + }); }); diff --git a/templates/footer.html b/templates/footer.html index dbaf833..64d1593 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -1,12 +1,7 @@ {% load vip %}

A propos

-

{% lorem %}

- +

L'association Coopé Technopole Metz est une association de droit local dont le siège social est établi à la résidence Edouard Branly. Son but est d'entretenir un lieu convivial sous la forme d'un bar associatif. Les membres actifs sont les coopemen (ou coopewomen).

Contacts

diff --git a/templates/nav.html b/templates/nav.html index 136250a..f982a0b 100644 --- a/templates/nav.html +++ b/templates/nav.html @@ -12,13 +12,13 @@ Gestion des produits - Comptabilité + Comptabilité - Classement + Classement - Classement sur l'année + Classement sur l'année Admin diff --git a/users/admin.py b/users/admin.py index 67e5fc6..8547939 100644 --- a/users/admin.py +++ b/users/admin.py @@ -1,8 +1,9 @@ from django.contrib import admin +from django.contrib.auth.models import Permission from .models import School, Profile, CotisationHistory +admin.site.register(Permission) admin.site.register(School) admin.site.register(Profile) -admin.site.register(CotisationHistory) -# Register your models here. +admin.site.register(CotisationHistory) \ No newline at end of file diff --git a/users/forms.py b/users/forms.py index 2fb2a44..a8f0fd7 100644 --- a/users/forms.py +++ b/users/forms.py @@ -25,14 +25,13 @@ class EditGroupForm(forms.ModelForm): fields = "__all__" class SelectUserForm(forms.Form): - user = forms.ModelChoiceField(queryset=User.objects.all(), label="Utilisateur") + user = forms.ModelChoiceField(queryset=User.objects.all(), required=True, label="Utilisateur", widget=autocomplete.ModelSelect2(url='users:all-users-autocomplete', attrs={'data-minimum-input-length':2})) class SelectNonSuperUserForm(forms.Form): user = forms.ModelChoiceField(queryset=User.objects.filter(is_active=True), required=True, label="Utilisateur", widget=autocomplete.ModelSelect2(url='users:non-super-users-autocomplete', attrs={'data-minimum-input-length':2})) class SelectNonAdminUserForm(forms.Form): - user = forms.ModelChoiceField(queryset=User.objects.filter(is_active=True), required=True, label="Utilisateur", widget=autocomplete.ModelSelect2(url='users:active-users-autocomplete', attrs={'data-minimum-input-length':2})) - + user = forms.ModelChoiceField(queryset=User.objects.filter(is_active=True), required=True, label="Utilisateur", widget=autocomplete.ModelSelect2(url='users:non-admin-users-autocomplete', attrs={'data-minimum-input-length':2})) class GroupsEditForm(forms.ModelForm): class Meta: diff --git a/users/models.py b/users/models.py index 1508f7d..58e87d4 100644 --- a/users/models.py +++ b/users/models.py @@ -4,6 +4,7 @@ from django.db.models.signals import post_save from django.dispatch import receiver from preferences.models import PaymentMethod, Cotisation +from gestion.models import ConsumptionHistory class School(models.Model): name = models.CharField(max_length=255, verbose_name="Nom") @@ -12,6 +13,11 @@ class School(models.Model): return self.name class CotisationHistory(models.Model): + class Meta: + permissions = ( + ("validate_consumptionhistory", "Peut (in)valider les cotisations"), + ) + WAITING = 0 VALID = 1 INVALID = 2 @@ -57,12 +63,12 @@ class Profile(models.Model): @property def alcohol(self): - #consos = Consommation.objects.filter(client=self).select_related('produit') - #alcool = 0 - #for conso in consos: - #produit = conso.produit - #alcool += conso.nombre * float(produit.deg) * produit.volume * 0.79 /10 /1000 - return 0 + consumptions = ConsumptionHistory.objects.filter(customer=self.user).select_related('product') + alcohol = 0 + for consumption in consumptions: + product = consumption.product + alcohol += consumption.quantity * float(product.deg) * product.volume * 0.79 /10 /1000 + return alcohol def __str__(self): return str(self.user) @@ -74,4 +80,9 @@ def create_user_profile(sender, instance, created, **kwargs): @receiver(post_save, sender=User) def save_user_profile(sender, instance, **kwargs): - instance.profile.save() \ No newline at end of file + instance.profile.save() + +def str_user(self): + return self.username + " (" + self.first_name + " " + self.last_name + ", " + str(self.profile.balance) + "€)" + +User.add_to_class("__str__", str_user) \ No newline at end of file diff --git a/users/templates/users/groups_index.html b/users/templates/users/groups_index.html index b5a4f23..c9feac0 100644 --- a/users/templates/users/groups_index.html +++ b/users/templates/users/groups_index.html @@ -10,6 +10,7 @@

Liste des groupes de droit

+ Ajouter un groupe de droit

diff --git a/users/templates/users/profile.html b/users/templates/users/profile.html index 0e68c56..2ea0534 100644 --- a/users/templates/users/profile.html +++ b/users/templates/users/profile.html @@ -130,13 +130,13 @@ - {%for reload in lastReloads%} + {% for reload in reloads %} - - + + - {%endfor%} + {% endfor %}
{{reload.amount}}{{reload.paymentMethod}}{{reload.amount}}€{{reload.PaymentMethod}} {{reload.date}}
@@ -208,11 +208,4 @@
- - - -{%endblock%} -{%block addScript %} - - -{%endblock%} +{%endblock%} \ No newline at end of file diff --git a/users/urls.py b/users/urls.py index 12a2dbc..8637409 100644 --- a/users/urls.py +++ b/users/urls.py @@ -30,6 +30,7 @@ urlpatterns = [ path('all-users-autocomplete', views.AllUsersAutocomplete.as_view(), name="all-users-autocomplete"), path('active-users-autcocomplete', views.ActiveUsersAutocomplete.as_view(), name="active-users-autocomplete"), path('non-super-users-autocomplete', views.NonSuperUserAutocomplete.as_view(), name="non-super-users-autocomplete"), + path('non-admin-users-autocomplete', views.NonAdminUserAutocomplete.as_view(), name="non-admin-users-autocomplete"), path('getUser/', views.getUser, name="getUser"), path('addCotisationHistory/', views.addCotisationHistory, name="addCotisationHistory"), path('validateCotisationHistory/', views.validateCotisationHistory, name="validateCotisationHistory"), @@ -39,4 +40,5 @@ urlpatterns = [ path('createSchool', views.createSchool, name="createSchool"), path('editSchool/', views.editSchool, name="editSchool"), path('deleteSchool/', views.deleteSchool, name="deleteSchool"), + path('allReloads//', views.allReloads, name="allReloads"), ] diff --git a/users/views.py b/users/views.py index d01cce8..2d36ee5 100644 --- a/users/views.py +++ b/users/views.py @@ -5,16 +5,38 @@ from django.contrib.auth import authenticate, login, logout from django.contrib import messages from django.db.models import Q from django.http import HttpResponse, HttpResponseRedirect +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.contrib.auth.decorators import login_required, permission_required -import json +import simplejson as json from datetime import datetime, timedelta - from dal import autocomplete +from coopeV3.acl import admin_required, superuser_required, self_or_has_perm, active_required from .models import CotisationHistory, WhiteListHistory, School from .forms import CreateUserForm, LoginForm, CreateGroupForm, EditGroupForm, SelectUserForm, GroupsEditForm, EditPasswordForm, addCotisationHistoryForm, addCotisationHistoryForm, addWhiteListHistoryForm, SelectNonAdminUserForm, SelectNonSuperUserForm, SchoolForm +from gestion.models import Reload +@active_required def loginView(request): + """ + Display the login form for :model:`User`. + + **Context** + + ``form_entete`` + Title of the form. + + ``form`` + The login form. + + ``form_button`` + Content of the form button. + + **Template** + + :template:`form.html` + """ form = LoginForm(request.POST or None) if(form.is_valid()): user = authenticate(username=form.cleaned_data['username'], password=form.cleaned_data['password']) @@ -29,26 +51,91 @@ def loginView(request): messages.error(request, "Nom d'utilisateur et/ou mot de passe invalide") return render(request, "form.html", {"form_entete": "Connexion", "form": form, "form_title": "Connexion", "form_button": "Se connecter"}) +@active_required +@login_required def logoutView(request): + """ + Logout the logged user + """ logout(request) messages.success(request, "Vous êtes à présent déconnecté") return redirect(reverse('home')) +@active_required +@login_required +@permission_required('auth.view_user') def index(request): - return render(request, "users/index.html") + """ + Display the index for user related actions -########## schools ########## + **Template** + + :template:`users/index.html` + """ + return render(request, "users/index.html") ########## users ########## +@active_required +@login_required +@self_or_has_perm('pk', 'auth.view_user') def profile(request, pk): + """ + Display the profile for the requested user + + ``pk`` + The primary key for user + + **Context** + + ``user`` + The instance of User + + ``self`` + Boolean value wich indicates if the current logged user and the request user are the same + + ``cotisations`` + List of the user's cotisations + + ``whitelists`` + List of the user's whitelists + + ``reloads`` + List of the last 5 reloads of the user + + **Template** + + :template:`users/profile.html` + """ user = get_object_or_404(User, pk=pk) self = request.user == user cotisations = CotisationHistory.objects.filter(user=user) whitelists = WhiteListHistory.objects.filter(user=user) - return render(request, "users/profile.html", {"user":user, "self":self, "cotisations":cotisations, "whitelists": whitelists}) + reloads = Reload.objects.filter(customer=user).order_by('-date') + return render(request, "users/profile.html", {"user":user, "self":self, "cotisations":cotisations, "whitelists": whitelists, "reloads": reloads}) +@active_required +@login_required +@permission_required('auth.add_user') def createUser(request): + """ + Display a CreateUserForm instance. + + **Context** + + ``form_entete`` + The form title. + + ``form`` + The CreateUserForm instance. + + ``form_button`` + The content of the form button. + + **Template** + + :template:`form.html` + """ form = CreateUserForm(request.POST or None) if(form.is_valid()): user = form.save(commit=False) @@ -58,17 +145,77 @@ def createUser(request): user.save() return render(request, "form.html", {"form_entete": "Gestion des utilisateurs", "form":form, "form_title":"Création d'un nouvel utilisateur", "form_button":"Créer l'utilisateur"}) +@active_required +@login_required +@permission_required('auth.view_user') def searchUser(request): + """ + Display a simple searchForm for User. + + **Context** + + ``form_entete`` + The form title. + + ``form`` + The searchForm instance. + + ``form_button`` + The content of the form button. + + **Template** + + :template:`form.html` + """ form = SelectUserForm(request.POST or None) if(form.is_valid()): return redirect(reverse('users:profile', kwargs={"pk":form.cleaned_data['user'].pk})) return render(request, "form.html", {"form_entete": "Gestion des utilisateurs", "form": form, "form_title": "Rechercher un utilisateur", "form_button": "Afficher le profil"}) +@active_required +@login_required +@permission_required('auth.view_user') def usersIndex(request): + """ + Display the list of all users. + + **Context** + + ``users`` + The list of all users + + **Template** + + :template:`users/users_index.html` + """ users = User.objects.all() return render(request, "users/users_index.html", {"users":users}) +@active_required +@login_required +@permission_required('auth.change_user') def editGroups(request, pk): + """ + Edit the groups of a user. + + ``pk`` + The pk of the user. + + **Context** + + ``form_entete`` + The form title. + + ``form`` + The GroupsEditForm instance. + + ``form_button`` + The content of the form button. + + **Template** + + :template:`form.html` + """ user = get_object_or_404(User, pk=pk) form = GroupsEditForm(request.POST or None, instance=user) if(form.is_valid()): @@ -78,7 +225,30 @@ def editGroups(request, pk): extra_css = "#id_groups{height:200px;}" return render(request, "form.html", {"form_entete": "Gestion de l'utilisateur " + user.username, "form": form, "form_title": "Modification des groupes", "form_button": "Enregistrer", "extra_css": extra_css}) +@active_required +@login_required +@permission_required('auth.change_user') def editPassword(request, pk): + """ + Change the password of a user. + + ``pk`` + The pk of the user. + + **Context** + ``form_entete`` + The form title. + + ``form`` + The EditPasswordForm instance. + + ``form_button`` + The content of the form button. + + **Template** + + :template:`form.html` + """ user = get_object_or_404(User, pk=pk) if user != request.user: messages.error(request, "Vous ne pouvez modifier le mot de passe d'un autre utilisateur") @@ -95,7 +265,31 @@ def editPassword(request, pk): messages.error(request, "Le mot de passe actuel est incorrect") return render(request, "form.html", {"form_entete": "Modification de mon compte", "form": form, "form_title": "Modification de mon mot de passe", "form_button": "Modifier mon mot de passe"}) +@active_required +@login_required +@permission_required('auth.change_user') def editUser(request, pk): + """ + Edit a user and user profile + + ``pk`` + The pk of the user. + + **Context** + + ``form_entete`` + The form title. + + ``form`` + The CreateUserForm instance. + + ``form_button`` + The content of the form button. + + **Template** + + :template:`form.html` + """ user = get_object_or_404(User, pk=pk) form = CreateUserForm(request.POST or None, instance=user, initial = {'school': user.profile.school}) if(form.is_valid()): @@ -105,7 +299,17 @@ def editUser(request, pk): return redirect(reverse('users:profile', kwargs={'pk': pk})) return render(request, "form.html", {"form_entete":"Modification du compte " + user.username, "form": form, "form_title": "Modification des informations", "form_button": "Modifier"}) +@active_required +@login_required +@permission_required('auth.change_user') def resetPassword(request, pk): + """ + Reset the password of a user. + + ``pk`` + The pk of the user + + """ user = get_object_or_404(User, pk=pk) if user.is_superuser: messages.error(request, "Impossible de réinitialiser le mot de passe de " + user.username + " : il est superuser.") @@ -116,22 +320,114 @@ def resetPassword(request, pk): messages.success(request, "Le mot de passe de " + user.username + " a bien été réinitialisé.") return redirect(reverse('users:profile', kwargs={'pk': pk})) +@active_required +@login_required +@permission_required('auth.view_user') def getUser(request, pk): + """ + Return username and balance of the requested user (pk) + + ``pk`` + The pk of the user + """ user = get_object_or_404(User, pk=pk) - data = json.dumps({"username": user.username, "balance": float(user.profile.balance)}) + data = json.dumps({"username": user.username, "balance": user.profile.balance}) return HttpResponse(data, content_type='application/json') +@active_required +@login_required +@self_or_has_perm('pk', 'auth.view_user') +def allReloads(request, pk, page): + """ + Display all the reloads of the requested user. + + ``pk`` + The pk of the user. + ``page`` + The page number. + + **Context** + + ``reloads`` + The reloads of the page. + ``user`` + The requested user + + **Template** + + :template:`users/allReloads.html` + """ + user = get_object_or_404(User, pk=pk) + allReloads = Reload.objects.filter(customer=user).order_by('-date') + paginator = Paginator(allReloads, 2) + reloads = paginator.get_page(page) + return render(request, "users/allReloads.html", {"reloads": reloads, "user":user}) + ########## Groups ########## +@active_required +@login_required +@permission_required('auth.view_group') def groupsIndex(request): + """ + Display all the groups. + + **Context** + + ``groups`` + List of all groups. + + **Template** + + :template:`users/groups_index.html` + """ groups = Group.objects.all() return render(request, "users/groups_index.html", {"groups": groups}) +@active_required +@login_required +@permission_required('auth.view_group') def groupProfile(request, pk): + """ + Display the profile of a group. + + ``pk`` + The pk of the group. + + **Context** + + ``group`` + The requested group. + + **Template** + + :template:`users/group_profile.html` + """ group = get_object_or_404(Group, pk=pk) return render(request, "users/group_profile.html", {"group": group}) +@active_required +@login_required +@permission_required('auth.add_group') def createGroup(request): + """ + Create a group with a CreateGroupForm instance. + + **Context** + + ``form_entete`` + The form title. + + ``form`` + The CreateGroupForm instance. + + ``form_button`` + The content of the form button. + + **Template** + + :template:`form.html` + """ form = CreateGroupForm(request.POST or None) if(form.is_valid()): group = form.save() @@ -139,7 +435,31 @@ def createGroup(request): return redirect(reverse('users:groupProfile', kwargs={'pk': group.pk})) return render(request, "form.html", {"form_entete": "Gestion des utilisateurs", "form":form, "form_title": "Création d'un groupe de droit", "form_button": "Créer le groupe de droit"}) +@active_required +@login_required +@permission_required('auth.change_group') def editGroup(request, pk): + """ + Edit a group with a EditGroupForm instance. + + ``pk`` + The pk of the group. + + **Context** + + ``form_entete`` + The form title. + + ``form`` + The EditGroupForm instance. + + ``form_button`` + The content of the form button. + + **Template** + + :template:`form.html` + """ group = get_object_or_404(Group, pk=pk) form = EditGroupForm(request.POST or None, instance=group) extra_css = "#id_permissions{height:200px;}" @@ -149,7 +469,17 @@ def editGroup(request, pk): return redirect(reverse('users:groupProfile', kwargs={'pk': group.pk})) return render(request, "form.html", {"form_entete": "Gestion des utilisateurs", "form": form, "form_title": "Modification du groupe de droit " + group.name, "form_button": "Modifier le groupe de droit", "extra_css":extra_css}) +@active_required +@login_required +@permission_required('auth.delete_group') def deleteGroup(request, pk): + """ + Delete the requested group. + + ``pk`` + The pk of the group + + """ group = get_object_or_404(Group, pk=pk) if group.user_set.count() == 0: name = group.name @@ -160,7 +490,20 @@ def deleteGroup(request, pk): messages.error(request, "Impossible de supprimer le groupe " + group.name + " : il y a encore des utilisateurs") return redirect(reverse('users:groupProfile', kwargs={'pk': group.pk})) +@active_required +@login_required +@permission_required('auth.change_group') def removeRight(request, groupPk, permissionPk): + """ + Remove a right from a given group. + + ``groupPk`` + The pk of the group. + + ``permissionPk`` + The pk of the right. + + """ group = get_object_or_404(Group, pk=groupPk) perm = get_object_or_404(Permission, pk=permissionPk) if perm in group.permissions.all(): @@ -170,7 +513,20 @@ def removeRight(request, groupPk, permissionPk): messages.error(request, "Impossible de retirer la permission " + perm.codename + " du groupe " + group.name) return redirect(reverse('users:groupProfile', kwargs={'pk': groupPk}) + "#second") +@active_required +@login_required +@permission_required('auth.change_user') def removeUser(request, groupPk, userPk): + """ + Remove a user from a given group. + + ``groupPk`` + The pk of the group. + + ``userPk`` + The pk of the user. + + """ group = get_object_or_404(Group, pk=groupPk) user = get_object_or_404(User, pk=userPk) if(group in user.groups.all()): @@ -182,10 +538,16 @@ def removeUser(request, groupPk, userPk): ########## admins ########## +@active_required +@login_required +@admin_required def adminsIndex(request): admins = User.objects.filter(is_staff=True) return render(request, "users/admins_index.html", {"admins": admins}) +@active_required +@login_required +@admin_required def addAdmin(request): form = SelectNonAdminUserForm(request.POST or None) if(form.is_valid()): @@ -196,6 +558,9 @@ def addAdmin(request): return redirect(reverse('users:adminsIndex')) return render(request, "form.html", {"form_entete": "Gestion des admins", "form": form, "form_title": "Ajout d'un admin", "form_button":"Ajouter l'utilisateur aux admins"}) +@active_required +@login_required +@admin_required def removeAdmin(request, pk): user = get_object_or_404(User, pk=pk) if user.is_staff: @@ -214,10 +579,16 @@ def removeAdmin(request, pk): ########## superusers ########## +@active_required +@login_required +@superuser_required def superusersIndex(request): superusers = User.objects.filter(is_superuser=True) return render(request, "users/superusers_index.html", {"superusers": superusers}) +@active_required +@login_required +@superuser_required def addSuperuser(request): form = SelectNonSuperUserForm(request.POST or None) if(form.is_valid()): @@ -229,6 +600,9 @@ def addSuperuser(request): return redirect(reverse('users:superusersIndex')) return render(request, "form.html", {"form_entete": "Gestion des superusers", "form": form, "form_title": "Ajout d'un superuser", "form_button":"Ajouter l'utilisateur aux superusers"}) +@active_required +@login_required +@superuser_required def removeSuperuser(request, pk): user = get_object_or_404(User, pk=pk) if user.is_superuser: @@ -244,11 +618,20 @@ def removeSuperuser(request, pk): ########## Cotisations ########## +@active_required +@login_required +@permission_required('users.add_cotisationhistory') def addCotisationHistory(request, pk): user = get_object_or_404(User, pk=pk) form = addCotisationHistoryForm(request.POST or None) if(form.is_valid()): cotisation = form.save(commit=False) + if(cotisation.paymentMethod.affect_balance): + if(user.profile.balance >= cotisation.amount): + user.profile.balance -= cotisation.amount + else: + cotisation.delete() + messages.error(request, "Solde insuffisant") cotisation.user = user cotisation.coopeman = request.user cotisation.amount = cotisation.cotisation.amount @@ -264,6 +647,9 @@ def addCotisationHistory(request, pk): return redirect(reverse('users:profile',kwargs={'pk':user.pk})) return render(request, "form.html",{"form": form, "form_title": "Ajout d'une cotisation pour l'utilisateur " + str(user), "form_button": "Ajouter"}) +@active_required +@login_required +@permission_required('users.validate_consumptionhistory') def validateCotisationHistory(request, pk): cotisationHistory = get_object_or_404(CotisationHistory, pk=pk) cotisationHistory.valid = CotisationHistory.VALID @@ -271,18 +657,26 @@ def validateCotisationHistory(request, pk): messages.success(request, "La cotisation a bien été validée") return HttpResponseRedirect(request.META.get('HTTP_REFERER')) +@active_required +@login_required +@permission_required('users.validate_consumptionhistory') def invalidateCotisationHistory(request, pk): cotisationHistory = get_object_or_404(CotisationHistory, pk=pk) cotisationHistory.valid = CotisationHistory.INVALID cotisationHistory.save() user = cotisationHistory.user user.profile.cotisationEnd = user.profile.cotisationEnd - timedelta(days=cotisationHistory.duration) + if(cotisationHistory.paymentMethod.affect_balance): + user.profile.balance += cotisation.amount user.save() messages.success(request, "La cotisation a bien été invalidée") return HttpResponseRedirect(request.META.get('HTTP_REFERER')) ########## Whitelist ########## +@active_required +@login_required +@permission_required('users.add_whitelisthistory') def addWhiteListHistory(request, pk): user = get_object_or_404(User, pk=pk) form = addWhiteListHistoryForm(request.POST or None) @@ -303,10 +697,16 @@ def addWhiteListHistory(request, pk): ########## Schools ########## +@active_required +@login_required +@permission_required('users.view_school') def schoolsIndex(request): schools = School.objects.all() return render(request, "users/schools_index.html", {"schools": schools}) +@active_required +@login_required +@permission_required('users.add_school') def createSchool(request): form = SchoolForm(request.POST or None) if(form.is_valid()): @@ -315,6 +715,9 @@ def createSchool(request): return redirect(reverse('users:schoolsIndex')) return render(request, "form.html", {"form": form, "form_title": "Création d'une école", "form_button": "Créer"}) +@active_required +@login_required +@permission_required('users.change_school') def editSchool(request, pk): school = get_object_or_404(School, pk=pk) form = SchoolForm(request.POST or None, instance=school) @@ -324,6 +727,9 @@ def editSchool(request, pk): return redirect(reverse('users:schoolsIndex')) return render(request, "form.html", {"form": form, "form_title": "Modification de l'école " + str(school), "form_button": "Modifier"}) +@active_required +@login_required +@permission_required('users.delete_school') def deleteSchool(request, pk): school = get_object_or_404(School, pk=pk) message = "L'école " + str(school) + " a bien été supprimée" @@ -355,6 +761,13 @@ class AdherentAutocomplete(autocomplete.Select2QuerySetView): class NonSuperUserAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): qs = User.objects.filter(is_superuser=False) + if self.q: + qs = qs.filter(Q(username__istartswith=self.q) | Q(first_name__istartswith=self.q) | Q(last_name__istartswith=self.q)) + return qs + +class NonAdminUserAutocomplete(autocomplete.Select2QuerySetView): + def get_queryset(self): + qs = User.objects.filter(is_staff=False) if self.q: qs = qs.filter(Q(username__istartswith=self.q) | Q(first_name__istartswith=self.q) | Q(last_name__istartswith=self.q)) return qs \ No newline at end of file