3
0
Fork 0
mirror of https://github.com/nanoy42/coope synced 2024-05-03 08:02:24 +00:00

Merge pull request #20 from nanoy42/release-3.7

Release 3.7
This commit is contained in:
Yoann Pietri 2019-09-23 17:55:13 +02:00 committed by GitHub
commit f2c6ae8ab0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1745 additions and 238 deletions

View file

@ -1,3 +1,12 @@
## v3.7
* Corrections de bugs mineurs et d'erreur d'affichage
* Mise en place des rechargements directs sur les transactions
* Réinitialisation de mot de passe
* Mise en place de l'initialisation de mot de passe et de l'envoi des statuts et du RI par mail à l'inscritpion
* Mise en place d'une barre de recherche globale
* Mise à jour de stellar (pour la navigation en particulier)
* Passage sous django_tex par pip
* Ajout des propositions d'améliorations
## v3.6.4
* Ajout d'un champ use_stocks
* Séparation des formulaires de fût

View file

@ -34,11 +34,12 @@ INSTALLED_APPS = [
'users',
'preferences',
'coopeV3',
'search',
'dal',
'dal_select2',
'simple_history',
'django_tex',
'debug_toolbar'
'debug_toolbar',
]
MIDDLEWARE = [
@ -129,4 +130,6 @@ LOGIN_URL = '/users/login'
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')
MEDIA_URL = '/media/'
INTERNAL_IPS = ["127.0.0.1"]
INTERNAL_IPS = ["127.0.0.1"]
EMAIL_SUBJECT_PREFIX = "[Coopé Technopôle Metz] "

View file

@ -31,7 +31,8 @@ urlpatterns = [
path('users/', include('users.urls')),
path('gestion/', include('gestion.urls')),
path('preferences/', include('preferences.urls')),
path('search/', include('search.urls')),
path('users/', include('django.contrib.auth.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2017 Martin Bierbaum
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,50 +0,0 @@
import os
from subprocess import PIPE, run
import tempfile
from django.template.loader import get_template
from django_tex.exceptions import TexError
from django.conf import settings
DEFAULT_INTERPRETER = 'pdflatex'
def run_tex(source):
"""
Copy the source to temp dict and run latex.
"""
with tempfile.TemporaryDirectory() as tempdir:
filename = os.path.join(tempdir, 'texput.tex')
with open(filename, 'x', encoding='utf-8') as f:
f.write(source)
print(source)
latex_interpreter = getattr(settings, 'LATEX_INTERPRETER', DEFAULT_INTERPRETER)
latex_command = 'cd "{tempdir}" && {latex_interpreter} -interaction=batchmode {path}'.format(tempdir=tempdir, latex_interpreter=latex_interpreter, path=os.path.basename(filename))
process = run(latex_command, shell=True, stdout=PIPE, stderr=PIPE)
try:
if process.returncode == 1:
with open(os.path.join(tempdir, 'texput.log'), encoding='utf8') as f:
log = f.read()
raise TexError(log=log, source=source)
with open(os.path.join(tempdir, 'texput.pdf'), 'rb') as pdf_file:
pdf = pdf_file.read()
except FileNotFoundError:
if process.stderr:
raise Exception(process.stderr.decode('utf-8'))
raise
return pdf
def compile_template_to_pdf(template_name, context):
"""
Compile the source with :func:`~django_tex.core.render_template_with_context` and :func:`~django_tex.core.run_tex`.
"""
source = render_template_with_context(template_name, context)
return run_tex(source)
def render_template_with_context(template_name, context):
"""
Render the template
"""
template = get_template(template_name, using='tex')
return template.render(context)

View file

@ -1,11 +0,0 @@
from django.template.backends.jinja2 import Jinja2
class TeXEngine(Jinja2):
app_dirname = 'templates'
def __init__(self, params):
default_environment = 'django_tex.environment.environment'
if 'environment' not in params['OPTIONS'] or not params['OPTIONS']['environment']:
params['OPTIONS']['environment'] = default_environment
super().__init__(params)

View file

@ -1,16 +0,0 @@
from jinja2 import Environment
from django.template.defaultfilters import register
from django_tex.filters import FILTERS as tex_specific_filters
# Django's built-in filters ...
filters = register.filters
# ... updated with tex specific filters
filters.update(tex_specific_filters)
def environment(**options):
env = Environment(**options)
env.filters = filters
return env

View file

@ -1,40 +0,0 @@
import re
def prettify_message(message):
'''
Helper methods that removes consecutive whitespaces and newline characters
'''
# Replace consecutive whitespaces with a single whitespace
message = re.sub(r'[ ]{2,}', ' ', message)
# Replace consecutive newline characters, optionally separated by whitespace, with a single newline
message = re.sub(r'([\r\n][ \t]*)+', '\n', message)
return message
def tokenizer(code):
token_specification = [
('ERROR', r'\! (?:.+[\r\n])+[\r\n]+'),
('WARNING', r'latex warning.*'),
('NOFILE', r'no file.*')
]
token_regex = '|'.join('(?P<{}>{})'.format(label, regex) for label, regex in token_specification)
for m in re.finditer(token_regex, code, re.IGNORECASE):
token_dict = dict(type=m.lastgroup, message=prettify_message(m.group()))
yield token_dict
class TexError(Exception):
def __init__(self, log, source):
self.log = log
self.source = source
self.tokens = list(tokenizer(self.log))
self.message = self.get_message()
def get_message(self):
for token in self.tokens:
if token['type'] == 'ERROR':
return token['message']
return 'No error message found in log'
def __str__(self):
return self.message

View file

@ -1,9 +0,0 @@
from django.utils.formats import localize_input
def do_linebreaks(value):
return value.replace('\n', '\\\\\n')
FILTERS = {
'localize': localize_input,
'linebreaks': do_linebreaks
}

View file

@ -1,19 +0,0 @@
from django.db import models
from django.core.exceptions import ValidationError
from django.template import TemplateDoesNotExist
from django.utils.translation import ugettext_lazy as _
from django.template.loader import get_template
def validate_template_path(name):
try:
get_template(name, using='tex')
except TemplateDoesNotExist:
raise ValidationError(_('Template not found.'))
class TeXTemplateFile(models.Model):
title = models.CharField(max_length=255)
name = models.CharField(max_length=255, validators=[validate_template_path,])
class Meta:
abstract = True

View file

@ -1,17 +0,0 @@
from django.http import HttpResponse
from django_tex.core import compile_template_to_pdf
class PDFResponse(HttpResponse):
def __init__(self, content, filename=None):
super(PDFResponse, self).__init__(content_type='application/pdf')
self['Content-Disposition'] = 'filename="{}"'.format(filename)
self.write(content)
def render_to_pdf(request, template_name, context=None, filename=None):
# Request is not needed and only included to make the signature conform to django's render function
pdf = compile_template_to_pdf(template_name, context)
return PDFResponse(pdf, filename=filename)

View file

@ -49,11 +49,10 @@ class CreateKegForm(forms.ModelForm):
class Meta:
model = Keg
fields = ["name", "stockHold", "amount", "capacity"]
fields = ["name", "stockHold", "amount", "capacity", "deg"]
widgets = {'amount': forms.TextInput}
category = forms.ModelChoiceField(queryset=Category.objects.all(), label="Catégorie", help_text="Catégorie dans laquelle placer les produits pinte, demi (et galopin si besoin).")
deg = forms.DecimalField(max_digits=5, decimal_places=2, label="Degré", validators=[MinValueValidator(0)])
create_galopin = forms.BooleanField(required=False, label="Créer le produit galopin ?")
def clean(self):
@ -68,7 +67,7 @@ class EditKegForm(forms.ModelForm):
class Meta:
model = Keg
fields = ["name", "stockHold", "amount", "capacity", "pinte", "demi", "galopin"]
fields = ["name", "stockHold", "amount", "capacity", "pinte", "demi", "galopin", "deg"]
widgets = {'amount': forms.TextInput}
def clean(self):

View file

@ -0,0 +1,33 @@
# Generated by Django 2.1 on 2019-09-12 07:51
import django.core.validators
from django.db import migrations, models
def update(apps, schema_editor):
Keg = apps.get_model('gestion', 'Keg')
for keg in Keg.objects.all():
keg.deg = keg.pinte.deg
keg.save()
def reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('gestion', '0013_auto_20190829_1219'),
]
operations = [
migrations.AddField(
model_name='historicalkeg',
name='deg',
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Degré'),
),
migrations.AddField(
model_name='keg',
name='deg',
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Degré'),
),
migrations.RunPython(update, reverse)
]

View file

@ -194,6 +194,7 @@ class Keg(models.Model):
"""
If True, will be displayed on :func:`~gestion.views.manage` view
"""
deg = models.DecimalField(default=0,max_digits=5, decimal_places=2, verbose_name="Degré", validators=[MinValueValidator(0)])
history = HistoricalRecords()
def __str__(self):

View file

@ -88,9 +88,9 @@ Facture FE\FactureNum
À régler par chèque, espèces ou par virement bancaire :
\begin{center}
\begin{tabular}{|c c c c|}
\hline \textbf{Code banque} & \textbf{Code guichet}& \textbf{Nº de Compte} & \textbf{Clé RIB} \\
\hline \textbf{Code banque} & \textbf{Code guichet}& \textbf{N$^\circ{}$ de Compte} & \textbf{Clé RIB} \\
20041 & 01010 & 1074350Z031 & 48 \\
\hline \textbf{IBAN Nº} & \multicolumn{3}{|l|}{ FR82 2004 1010 1010 7435 0Z03 148 } \\
\hline \textbf{IBAN N$^\circ{}$} & \multicolumn{3}{|l|}{ FR82 2004 1010 1010 7435 0Z03 148 } \\
\hline \textbf{BIC} & \multicolumn{3}{|l|}{ PSSTFRPPNCY }\\
\hline \textbf{Domiciliation} & \multicolumn{3}{|l|}{La Banque Postale - Centre Financier - 54900 Nancy CEDEX 9}\\
\hline \textbf{Titulaire} & \multicolumn{3}{|l|}{ASSO COOPE TECHNOPOLE METZ}\\

View file

@ -53,7 +53,7 @@
}
</style>
{% if perms.gestion.add_consumptionhistory %}
<section id="intro" class="main">
<section id="first" class="main">
<div class="spotlight">
<div class="content">
<header class="major">
@ -61,7 +61,6 @@
</header>
<div class="row uniform">
<div class="12u$">
<a class="button small" href=""><i class="fa fa-times"></i> Annuler</a><br><br>
{{gestion_form}}
</div>
</div>
@ -84,7 +83,7 @@
<td id="balance">0€</td>
<td id="totalAmount">0€</td>
<td id="totalAfter">0€</td>
<td>{% for pm in pay_buttons %}<button class="btn small pay_button" data-payment="{{pm.pk}}"><i class="fa fa-{{pm.icon}}"></i> {{pm.name}}</button> {% endfor %}</td>
<td>{% for pm in pay_buttons %}<button class="btn small pay_button" data-payment="{{pm.pk}}"><i class="fa fa-{{pm.icon}}"></i> {{pm.name}}</button> {% endfor %} <a class="button small" href="" tooltip="lol"><i class="fa fa-times"></i> Annuler</a></td>
</tr>
</tbody>
</table>
@ -127,6 +126,25 @@
{% if not cotisations|divisibleby:3 %}
</tr>
{% endif %}
<tr style="text-align:center; font-weight:bold;">
<td colspan="1">Rechargements</td>
<td>
<div class="dropdown">
<button onclick="dropdown('myDropdown1')" class="dropbtn small">Rechargement 1€</button>
<div id="myDropdown1" class="dropdown-content">
{% for pm in pay_buttons %}{% if not pm.affect_balance%}<a class="reload" data-payment="{{pm.pk}}" data-payment-name="{{pm.name}}" target="1"><i class="fa fa-{{pm.icon}}"></i> {{pm.name}}</a> {% endif %}{% endfor %}
</div>
</div>
</td>
<td>
<div class="dropdown">
<button onclick="dropdown('myDropdown2')" class="dropbtn small" target="myDropdown2">Rechargement 10€</button>
<div id="myDropdown2" class="dropdown-content">
{% for pm in pay_buttons %}{% if not pm.affect_balance%}<a class="reload" data-payment="{{pm.pk}}" data-payment-name="{{pm.name}}" target="10"><i class="fa fa-{{pm.icon}}"></i> {{pm.name}}</a> {% endif %}{% endfor %}
</div>
</div>
</td>
</tr>
<tr style="text-align:center; font-weight:bold;"><td colspan="4">Bières pression</td></tr>
{% for product in bieresPression %}
{% if forloop.counter0|divisibleby:3 %}

View file

@ -28,7 +28,7 @@
<td>{{ menu.name }}</td>
<td>{{ menu.amount}} €</td>
<td>{% for art in menu.articles.all %}<a href="{% url 'gestion:productProfile' art.pk %}">{{art}}</a>,{% endfor %}</td>
<td>{{ menu.is_active | yesno:"Oui, Non"}}</td>
<td><i class="fa fa-{{ menu.is_active | yesno:'check,times'}}"></i></td>
<td>{% if perms.gestion.change_menu %}<a href="{% url 'gestion:switchActivateMenu' menu.pk %}" class="button small">{% if menu.is_active %}<i class="fa fa-times-cirlce"></i> Désa{% else %}<i class="fa fa-check-circle"></i> A{% endif %}ctiver</a> <a href="{% url 'gestion:editMenu' menu.pk %}" class="button small"><i class="fa fa-pencil-alt"></i> Modifier</a>{% endif %}</td>
</tr>
{% endfor %}

View file

@ -34,7 +34,7 @@
<td>{{ product.amount}}</td>
<td>{{ product.stock }}</td>
<td>{{ product.category }}</td>
<td>{{ product.is_active | yesno:"Oui, Non"}}</td>
<td><i class="fa fa-{{ product.is_active | yesno:'check,times'}}"></i></td>
<td>{{ product.deg }}</td>
<td>{{ product.volume }} cl</td>
<td><a href="{% url 'gestion:productProfile' product.pk %}" class="button small"><i class="fa fa-eye"></i> Profil</a> {% if perms.gestion.change_product %}<a href="{% url 'gestion:switchActivate' product.pk %}" class="button small">{% if product.is_active %}<i class="fa fa-times-circle"></i> Désa{% else %}<i class="fa fa-check-circle"></i> A{% endif %}ctiver</a> <a href="{% url 'gestion:editProduct' product.pk %}" class="button small"><i class="fa fa-pencil-alt"></i> Modifier</a>{% endif %}</td>

View file

@ -9,7 +9,7 @@
</ul>
{% endblock %}
{% block content %}
<section id="intro" class="main">
<section id="first" class="main">
<div class="spotlight">
<div class="content">
<header class="major">

View file

@ -87,14 +87,29 @@ def order(request):
menus = json.loads(request.POST["menus"])
listPintes = json.loads(request.POST["listPintes"])
cotisations = json.loads(request.POST['cotisations'])
reloads = json.loads(request.POST['reloads'])
gp,_ = GeneralPreferences.objects.get_or_create(pk=1)
if (not order) and (not menus) and (not cotisations):
raise Exception("Pas de commande.")
if(reloads):
for reload in reloads:
reload_amount = Decimal(reload["value"])*Decimal(reload["quantity"])
if(reload_amount <= 0):
raise Exception("Impossible d'effectuer un rechargement négatif")
reload_payment_method = get_object_or_404(PaymentMethod, pk=reload["payment_method"])
if not reload_payment_method.is_usable_in_reload:
raise Exception("Le moyen de paiement ne peut pas être utilisé pour les rechargements.")
reload_entry = Reload(customer=user, amount=reload_amount, PaymentMethod=reload_payment_method, coopeman=request.user)
reload_entry.save()
user.profile.credit += reload_amount
user.save()
if(cotisations):
for co in cotisations:
cotisation = Cotisation.objects.get(pk=co['pk'])
for i in range(co['quantity']):
cotisation_history = CotisationHistory(cotisation=cotisation)
if not paymentMethod.is_usable_in_cotisation:
raise Exception("Le moyen de paiement ne peut pas être utilisé pour les cotisations.")
if(paymentMethod.affect_balance):
if(user.profile.balance >= cotisation_history.cotisation.amount):
user.profile.debit += cotisation_history.cotisation.amount
@ -592,7 +607,29 @@ def editKeg(request, pk):
keg = get_object_or_404(Keg, pk=pk)
form = EditKegForm(request.POST or None, instance=keg)
if(form.is_valid()):
form.save()
try:
price_profile = PriceProfile.objects.get(use_for_draft=True)
except:
messages.error(request, "Il n'y a pas de profil de prix pour les pressions")
return redirect(reverse('preferences:priceProfilesIndex'))
keg = form.save()
# Update produtcs
name = form.cleaned_data["name"][4:]
pinte_price = compute_price(keg.amount/(2*keg.capacity), price_profile.a, price_profile.b, price_profile.c, price_profile.alpha)
pinte_price = ceil(10*pinte_price)/10
keg.pinte.deg = keg.deg
keg.pinte.amount = pinte_price
keg.pinte.name = "Pinte " + name
keg.pinte.save()
keg.demi.deg = keg.deg
keg.demi.amount = ceil(5*pinte_price)/10
keg.demi.name = "Demi " + name
keg.demi.save()
if(keg.galopin):
keg.galopin.deg = deg
keg.galopin.amount = ceil(2.5 * pinte_price)/10
keg.galopin.name = "Galopin " + name
keg.galopin.save()
messages.success(request, "Le fût a bien été modifié")
return redirect(reverse('gestion:kegsList'))
return render(request, "form.html", {"form": form, "form_title": "Modification d'un fût", "form_button": "Modifier", "form_button_icon": "pencil-alt"})
@ -617,6 +654,15 @@ def openKeg(request):
keg.stockHold -= 1
keg.is_active = True
keg.save()
if keg.pinte:
keg.pinte.is_active = True
keg.pinte.save()
if keg.demi:
keg.demi.is_active = True
keg.demi.save()
if keg.galopin:
keg.galopin.is_active = True
keg.galopin.save()
messages.success(request, "Le fut a bien été percuté")
return redirect(reverse('gestion:kegsList'))
return render(request, "form.html", {"form": form, "form_title":"Percutage d'un fût", "form_button":"Percuter", "form_button_icon": "fill-drip"})
@ -643,6 +689,15 @@ def openDirectKeg(request, pk):
keg.stockHold -= 1
keg.is_active = True
keg.save()
if keg.pinte:
keg.pinte.is_active = True
keg.pinte.save()
if keg.demi:
keg.demi.is_active = True
keg.demi.save()
if keg.galopin:
keg.galopin.is_active = True
keg.galopin.save()
messages.success(request, "Le fût a bien été percuté")
else:
messages.error(request, "Il n'y a pas de fût en stock")
@ -664,6 +719,15 @@ def closeKeg(request):
kegHistory.save()
keg.is_active = False
keg.save()
if keg.pinte:
keg.pinte.is_active = False
keg.pinte.save()
if keg.demi:
keg.demi.is_active = False
keg.demi.save()
if keg.galopin:
keg.galopin.is_active = False
keg.galopin.save()
messages.success(request, "Le fût a bien été fermé")
return redirect(reverse('gestion:kegsList'))
return render(request, "form.html", {"form": form, "form_title":"Fermeture d'un fût", "form_button":"Fermer le fût", "form_button_icon": "fill"})
@ -686,6 +750,15 @@ def closeDirectKeg(request, pk):
kegHistory.save()
keg.is_active = False
keg.save()
if keg.pinte:
keg.pinte.is_active = False
keg.pinte.save()
if keg.demi:
keg.demi.is_active = False
keg.demi.save()
if keg.galopin:
keg.galopin.is_active = False
keg.galopin.save()
messages.success(request, "Le fût a bien été fermé")
else:
messages.error(request, "Le fût n'est pas ouvert")

View file

@ -1,6 +1,6 @@
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from .models import PaymentMethod, GeneralPreferences, Cotisation, DivideHistory, PriceProfile
from .models import PaymentMethod, GeneralPreferences, Cotisation, DivideHistory, PriceProfile, Improvement
class CotisationAdmin(SimpleHistoryAdmin):
"""
@ -40,8 +40,18 @@ class DivideHistoryAdmin(SimpleHistoryAdmin):
list_display = ('date', 'total_cotisations', 'total_cotisations_amount', 'total_ptm_amount', 'coopeman')
ordering = ('-date',)
class ImprovementAdmin(SimpleHistoryAdmin):
"""
The admin class for Improvement.
"""
list_display = ('title', 'mode', 'seen', 'done', 'date')
ordering = ('-date',)
search_fields = ('title', 'description')
list_filter = ('mode', 'seen', 'done')
admin.site.register(PaymentMethod, PaymentMethodAdmin)
admin.site.register(GeneralPreferences, GeneralPreferencesAdmin)
admin.site.register(Cotisation, CotisationAdmin)
admin.site.register(PriceProfile, PriceProfileAdmin)
admin.site.register(DivideHistory, DivideHistoryAdmin)
admin.site.register(DivideHistory, DivideHistoryAdmin)
admin.site.register(Improvement, ImprovementAdmin)

View file

@ -1,7 +1,7 @@
from django import forms
from django.core.exceptions import ValidationError
from .models import Cotisation, PaymentMethod, GeneralPreferences, PriceProfile
from .models import Cotisation, PaymentMethod, GeneralPreferences, PriceProfile, Improvement
class CotisationForm(forms.ModelForm):
"""
@ -50,3 +50,11 @@ class GeneralPreferencesForm(forms.ModelForm):
'home_text': forms.Textarea(attrs={'placeholder': 'Ce message sera affiché sur la page d\'accueil'})
}
class ImprovementForm(forms.ModelForm):
"""
Form to create an improvement
"""
class Meta:
model = Improvement
fields = ["title", "mode", "description"]

View file

@ -0,0 +1,31 @@
# Generated by Django 2.1 on 2019-09-08 09:59
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('preferences', '0018_auto_20190627_2302'),
]
operations = [
migrations.CreateModel(
name='Improvement',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('mode', models.IntegerField(choices=[(0, 'Bug'), (1, 'Amélioration'), (2, 'Nouvelle fonctionnalité')])),
('description', models.TextField()),
('seen', models.BooleanField(default=False)),
('done', models.BooleanField(default=False)),
('coopeman', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='improvement_submitted', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Amélioration',
},
),
]

View file

@ -0,0 +1,39 @@
# Generated by Django 2.1 on 2019-09-08 10:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0019_improvement'),
]
operations = [
migrations.AddField(
model_name='improvement',
name='date',
field=models.DateTimeField(auto_now_add=True, default='2019-09-08 00:00'),
preserve_default=False,
),
migrations.AlterField(
model_name='improvement',
name='done',
field=models.BooleanField(default=False, verbose_name='Fait ?'),
),
migrations.AlterField(
model_name='improvement',
name='mode',
field=models.IntegerField(choices=[(0, 'Bug'), (1, 'Amélioration'), (2, 'Nouvelle fonctionnalité')], verbose_name='Type'),
),
migrations.AlterField(
model_name='improvement',
name='seen',
field=models.BooleanField(default=False, verbose_name='Vu ?'),
),
migrations.AlterField(
model_name='improvement',
name='title',
field=models.CharField(max_length=255, verbose_name='Titre'),
),
]

View file

@ -202,3 +202,31 @@ class PriceProfile(models.Model):
def __str__(self):
return self.name
class Improvement(models.Model):
"""
Stores bugs and amelioration proposals.
"""
BUG = 0
AMELIORATION = 1
NEWFEATURE = 2
MODES = (
(BUG, "Bug"),
(AMELIORATION, "Amélioration"),
(NEWFEATURE, "Nouvelle fonctionnalité")
)
class Meta:
verbose_name = "Amélioration"
title = models.CharField(max_length=255, verbose_name="Titre")
mode = models.IntegerField(choices=MODES, verbose_name="Type")
description = models.TextField()
seen = models.BooleanField(default=False, verbose_name="Vu ?")
done = models.BooleanField(default=False, verbose_name="Fait ?")
coopeman = models.ForeignKey(User, on_delete=models.PROTECT, related_name="improvement_submitted")
date = models.DateTimeField(auto_now_add=True)

View file

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block entete %}Amélioration {{improvement.title}}{% endblock %}
{% block navbar %}
<ul>
<li><a href="#first">{{improvement.title}}</a></li>
</ul>
{% endblock %}
{% block content %}
<section id="first" class="main">
<header class="major">
<h2>{{improvement.title}}</h2>
</header>
<a href="{% url 'preferences:improvementsIndex' %}" class="button">Retour à la liste des améliorations</a><br><br>
<strong>Titre : </strong> {{improvement.title}}<br>
<strong>Type : </strong> {{improvement.get_mode_display}}<br>
<strong>Date : </strong> {{improvement.date}}<br>
<strong>Fait : </strong> {{improvement.done|yesno:"Oui,Non"}}<br>
<strong>Coopeman : </strong> {{improvement.coopeman}}<br>
<strong>Description : </strong> {{improvement.description}}<br>
</section>
{% endblock %}

View file

@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block entete %}Améliorations{% endblock %}
{% block navbar %}
<ul>
<li><a href="#first">Liste des améliorations à faire</a></li>
<li><a href="#seconde">Liste des améliorations faîtes</a></li>
</ul>
{% endblock %}
{% block content %}
<section id="first" class="main">
<header class="major">
<h2>Liste des améliorations à faire</h2>
</header>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Titre</th>
<th>Type</th>
<th>Vu ?</th>
<th>Date</th>
<th>Administration</th>
</tr>
</thead>
<tbody>
{% for improvement in todo_improvements %}
<tr>
<td>{{improvement.title}}</td>
<td>{{improvement.get_mode_display}}</td>
<td><i class="fa fa-{{improvement.seen|yesno:'check,times'}}"></i></td>
<td>{{improvement.date}}</td>
<td><a href="{% url 'preferences:improvementProfile' improvement.pk %}" class="button small"><i class="fa fa-eye"></i> Voir</a> <a href="{% url 'preferences:changeImprovementState' improvement.pk %}" class="button small"><i class="fa fa-check"></i> Passer en fait</a> <a href="{% url 'preferences:deleteImprovement' improvement.pk %}" class="button small"><i class="fa fa-trash"></i> Supprimer</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<section id="second" class="main">
<header class="major">
<h2>Liste des améliorations faîtes</h2>
</header>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Titre</th>
<th>Type</th>
<th>Vu ?</th>
<th>Date</th>
<th>Administration</th>
</tr>
</thead>
<tbody>
{% for improvement in done_improvements %}
<tr>
<td>{{improvement.title}}</td>
<td>{{improvement.get_mode_display}}</td>
<td><i class="fa fa-{{improvement.seen|yesno:'check,times'}}"></i></td>
<td>{{improvement.date}}</td>
<td><a href="{% url 'preferences:improvementProfile' improvement.pk %}" class="button small"><i class="fa fa-eye"></i> Voir</a> <a href="{% url 'preferences:changeImprovementState' improvement.pk %}" class="button small"><i class="fa fa-check"></i> Passer en non fait</a> <a href="{% url 'preferences:deleteImprovement' improvement.pk %}" class="button small"><i class="fa fa-trash"></i> Supprimer</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% endblock %}

View file

@ -30,10 +30,10 @@
{% for pm in paymentMethods %}
<tr>
<td>{{ pm.name }} </td>
<td>{{ pm.is_active | yesno:"Oui, Non"}}</td>
<td>{{ pm.is_usable_in_cotisation | yesno:"Oui, Non" }}</td>
<td>{{ pm.is_usable_in_reload | yesno:"Oui, Non" }}</td>
<td>{{ pm.affect_balance | yesno:"Oui, Non" }}</td>
<td><i class="fa fa-{{ pm.is_active | yesno:'check,times'}}"></i></td>
<td><i class="fa fa-{{ pm.is_usable_in_cotisation | yesno:'check,times' }}"></i></td>
<td><i class="fa fa-{{ pm.is_usable_in_reload | yesno:'check,times' }}"></i></td>
<td><i class="fa fa-{{ pm.affect_balance | yesno:'check,times' }}"></i></td>
<td><i class="fa fa-{{ pm.icon }}"></i></td>
<td>{% if perms.preferences.change_paymentmethod %}<a class="button small" href="{% url 'preferences:editPaymentMethod' pm.pk %}"><i class="fa fa-pencil-alt"></i> Modifier</a> {% endif %}{% if perms.preferences.delete_paymentmethod %}<a class="button small" href="{% url 'preferences:deletePaymentMethod' pm.pk %}"><i class="fa fa-trash"></i> Supprimer</a>{% endif %}</td>
</tr>

View file

@ -34,7 +34,7 @@
<td>{{ pp.b }}</td>
<td>{{ pp.c }}</td>
<td>{{ pp.alpha }}</td>
<td>{{ pp.use_for_draft | yesno:"Oui,Non"}}</td>
<td><i class="fa fa-{{ pp.use_for_draft | yesno:'check,times'}}"></i></td>
<td>{% if perms.preferences.change_priceprofile %}<a class="button small" href="{% url 'preferences:editPriceProfile' pp.pk %}"><i class="fa fa-pencil-alt"></i> Modifier</a> {% endif %}{% if perms.preferences.delete_priceprofile %}<a class="button small" href="{% url 'preferences:deletePriceProfile' pp.pk %}"><i class="fa fa-trash"></i> Supprimer</a>{% endif %}</td>
</tr>
{% endfor %}

View file

@ -19,5 +19,10 @@ urlpatterns = [
path('deletePriceProfile/<int:pk>', views.delete_price_profile, name="deletePriceProfile"),
path('inactive', views.inactive, name="inactive"),
path('getConfig', views.get_config, name="getConfig"),
path('getCotisation/<int:pk>', views.get_cotisation, name="getCotisation")
,]
path('getCotisation/<int:pk>', views.get_cotisation, name="getCotisation"),
path('addImprovement', views.add_improvement, name="addImprovement"),
path('improvementsIndex', views.improvements_index, name="improvementsIndex"),
path('improvementProfile/<int:pk>', views.improvement_profile, name="improvementProfile"),
path('deleteImprovement/<int:pk>', views.delete_improvement, name="deleteImprovement"),
path('changeImprovementState/<int:pk>', views.change_improvement_state, name="changeImprovementState"),
]

View file

@ -7,12 +7,13 @@ from django.contrib.auth.decorators import login_required, permission_required
from django.http import HttpResponse
from django.forms.models import model_to_dict
from django.http import Http404
from django.core.mail import mail_admins
from coopeV3.acl import active_required
from .models import GeneralPreferences, Cotisation, PaymentMethod, PriceProfile
from .models import GeneralPreferences, Cotisation, PaymentMethod, PriceProfile, Improvement
from .forms import CotisationForm, PaymentMethodForm, GeneralPreferencesForm, PriceProfileForm
from .forms import CotisationForm, PaymentMethodForm, GeneralPreferencesForm, PriceProfileForm, ImprovementForm
@active_required
@login_required
@ -245,3 +246,73 @@ def delete_price_profile(request,pk):
price_profile.delete()
messages.success(request, message)
return redirect(reverse('preferences:priceProfilesIndex'))
########## Improvements ##########
@active_required
@login_required
def add_improvement(request):
"""
Display a form to create an improvement. Any logged user can access it
"""
form = ImprovementForm(request.POST or None)
if form.is_valid():
improvement = form.save(commit=False)
improvement.coopeman = request.user
improvement.save()
mail_admins("Nouvelle proposition d'amélioration", "Une nouvelle proposition d'amélioration a été postée (" + improvement.title + ", " + improvement.get_mode_display() + "). Le corps est le suivant : " + improvement.description)
messages.success(request, "Votre proposition a bien été envoyée")
return redirect(reverse('home'))
return render(request, "form.html", {"form": form, "form_title": "Proposition d'amélioration", "form_button": "Envoyer", "form_button_icon": "bug"})
@active_required
@login_required
@permission_required('preferences.view_improvement')
def improvements_index(request):
"""
Display all improvements
"""
todo_improvements = Improvement.objects.filter(done=False).order_by('-date')
done_improvements = Improvement.objects.filter(done=True).order_by('-date')
return render(request, "preferences/improvements_index.html", {"todo_improvements": todo_improvements, "done_improvements": done_improvements})
@active_required
@login_required
@permission_required('preferences.view_improvement')
@permission_required('preferences.change_improvement')
def improvement_profile(request, pk):
"""
Display an improvement
"""
improvement = get_object_or_404(Improvement, pk=pk)
improvement.seen = 1
improvement.save()
return render(request, "preferences/improvement_profile.html", {"improvement": improvement})
@active_required
@login_required
@permission_required('preferences.change_improvement')
def change_improvement_state(request, pk):
"""
Change done state of an improvement
"""
improvement = get_object_or_404(Improvement, pk=pk)
improvement.done = 1 - improvement.done
improvement.save()
messages.success(request, "L'état a bien été changé")
return redirect(reverse('preferences:improvementsIndex'))
@active_required
@login_required
@permission_required('preferences.delete_improvement')
def delete_improvement(request, pk):
"""
Delete an improvement
"""
improvement = get_object_or_404(Improvement, pk=pk)
improvement.delete()
messages.success(request, "L'amélioration a bien été supprimée.")
return redirect(reverse('preferences:improvementsIndex'))

View file

@ -6,3 +6,4 @@ docutils==0.14
django-simple-history==2.5.1
jinja2==2.10
Sphinx==1.8.4
django-tex==1.1.7

3
search/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
search/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class SearchConfig(AppConfig):
name = 'search'

View file

3
search/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View file

@ -0,0 +1,224 @@
{% extends 'base.html' %}
{% block entete %}Recherche{% endblock %}
{% block navbar%}
<ul>
{% if perms.auth.view_user %}
<li><a href="#first">Utilisateurs ({{users.count}})</a></li>
{% endif %}
{% if perms.gestion.view_product %}
<li><a href="#second">Produits ({{products.count}})</a></li>
{% endif %}
{% if perms.gestion.view_keg %}
<li><a href="#third">Fûts ({{kegs.count}})</a></li>
{% endif %}
{% if perms.gestion.view_menu %}
<li><a href="#fourth">Menus ({{menus.count}})</a></li>
{% endif %}
{% if perms.auth.view_group %}
<li><a href="#fifth">Groupes ({{groups.count}})</a></li>
{% endif %}
</ul>
{% endblock %}
{% block content %}
{% if perms.auth.view_user %}
<section id="first" class="main">
<header class="major">
<h2>Résultats dans les utilisateurs ({{users.count}} résultat{% if users.count != 1 %}s{% endif %})</h2>
</header>
<section>
{% if users.count %}
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Nom d'utilisateur</th>
<th>Prénom Nom</th>
<th>Solde</th>
<th>Fin d'adhésion</th>
<th>Staff</th>
<th>Profil</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{user.username}}</td>
<td>{{user.first_name}} {{user.last_name}}</td>
<td>{{user.profile.balance}} €</td>
<td>{% if user.profile.is_adherent %}{{user.profile.cotisationEnd}}{% else %}Non adhérent{% endif%}</td>
<td><i class="fa fa-{{user.is_staff|yesno:'check,times'}}"></i></td>
<td><a class="button small" href="{% url 'users:profile' user.pk %}"><i class="fa fa-user"></i> Profil</a></td>
</tr>
{%endfor%}
</tbody>
</table>
</div>
{% else %}
Aucun résultat n'a pu être trouvé.
{% endif %}
</section>
</section>
{% endif %}
{% if perms.gestion.view_product %}
<section id="second" class="main">
<header class="major">
<h2>Résultats dans les produits ({{products.count}} résultat{% if products.count != 1 %}s{% endif %})</h2>
</header>
<section>
{% if products.count %}
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Nom</th>
<th>Prix</th>
<th>Actif</th>
<th>Catégorie</th>
<th>Adhérent</th>
<th>Stock</th>
<th>Volume</th>
<th>Degré</th>
<th>Administration</th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr>
<td>{{product.name}}</td>
<td>{{product.amount}} €</td>
<td><i class="fa fa-{{product.is_active|yesno:'check,times'}}"></i></td>
<td>{{product.category}}</td>
<td><i class="fa fa-{{product.adherentRequired|yesno:'check,times'}}"></i></td>
<td>{{product.stock}}</td>
<td>{{product.volume}} cl</td>
<td>{{product.deg}}</td>
<td>{% if perms.gestion.change_product %}<a class="button small" href="{% url 'gestion:switchActivate' product.pk %}"><i class="fa fa-check-circle"></i> {{product.is_active|yesno:"Désa,A"}}ctiver</a> <a class="button small" href="{% url 'gestion:editProduct' product.pk %}"><i class="fa fa-pencil-alt"></i> Modifier</a>{% endif %}</td>
</tr>
{%endfor%}
</tbody>
</table>
</div>
{% else %}
Aucun résultat n'a pu être trouvé.
{% endif %}
</section>
</section>
{% endif %}
{% if perms.gestion.view_keg %}
<section id="third" class="main">
<header class="major">
<h2>Résultats dans les fûts ({{kegs.count}} résultat{% if kegs.count != 1 %}s{% endif %})</h2>
</header>
<section>
{% if kegs.count %}
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Nom</th>
<th>Stock</th>
<th>Capacité</th>
<th>Actif</th>
<th>Prix du fût</th>
<th>Degré</th>
<th>Historique</th>
<th>Administration</th>
</tr>
</thead>
<tbody>
{% for keg in kegs %}
<tr>
<td>{{keg.name}}</td>
<td>{{keg.stockHold}}</td>
<td>{{keg.capacity}} L</td>
<td><i class="fa fa-{{keg.is_active|yesno:'check,times'}}"></i></td>
<td>{{keg.amount}} €</td>
<td>{{keg.deg}}°</td>
<td><a href="{% url 'gestion:kegH' keg.pk %}" class="button small"><i class="fa fa-history"></i> Voir</a></td>
<td>{% if perms.gestion.change_keg %}<a class="button small" href="{% url 'gestion:editKeg' keg.pk %}"><i class="fa fa-pencil-alt"></i> Modifier</a>{% endif %}</td>
</tr>
{%endfor%}
</tbody>
</table>
</div>
{% else %}
Aucun résultat n'a pu être trouvé.
{% endif %}
</section>
</section>
{% endif %}
{% if perms.gestion.view_menu %}
<section id="fourth" class="main">
<header class="major">
<h2>Résultats dans les menus ({{menus.count}} résultat{% if menus.count != 1 %}s{% endif %})</h2>
</header>
<section>
{% if menus.count %}
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Nom</th>
<th>Prix</th>
<th>Actif</th>
<th>Adhérent</th>
<th>Nombre de produit</th>
<th>Administration</th>
</tr>
</thead>
<tbody>
{% for menu in menus %}
<tr>
<td>{{menu.name}}</td>
<td>{{menu.amount}} €</td>
<td><i class="fa fa-{{menu.is_active|yesno:'check,times'}}"></i></td>
<td><i class="fa fa-{{menu.adherentRequired|yesno:'check,times'}}"></i></td>
<td>{{menu.articles.count}}</td>
<td>{% if perms.gestion.change_menu %}<a class="button small" href="{% url 'gestion:switchActivateMenu' menu.pk %}"><i class="fa fa-check-circle"></i> {{menu_is_active|yesno:"Désa,A"}}ctiver</a> <a class="button small" href="{% url 'gestion:editMenu' menu.pk %}"><i class="fa fa-pencil-alt"></i> Modifier</a>{% endif %}</td>
</tr>
{%endfor%}
</tbody>
</table>
</div>
{% else %}
Aucun résultat n'a pu être trouvé.
{% endif %}
</section>
</section>
{% endif %}
{% if perms.auth.view_group %}
<section id="fifth" class="main">
<header class="major">
<h2>Résultats dans les groupes ({{groups.count}} résultat{% if groups.count != 1 %}s{% endif %})</h2>
</header>
<section>
{% if groups.count %}
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Nom</th>
<th>Nombre de droits</th>
<th>Nombre d'utilisateurs</th>
<th>Administrer</th>
</tr>
</thead>
<tbody>
{% for group in groups %}
<tr>
<td>{{group.name}}</td>
<td>{{group.permissions.count}}</td>
<td>{{group.user_set.count}}</td>
<td><a href="{% url 'users:groupProfile' group.pk %}" class="button small"><i class="fa fa-eye"></i> Voir</a>{% if perms.auth.change_group %}<a href="{% url 'users:editGroup' group.pk %}" class="button small"><i class="fa fa-pencil-alt"></i> Modifier</a>{% endif %}</td>
</tr>
{%endfor%}
</tbody>
</table>
</div>
{% else %}
Aucun résultat n'a pu être trouvé.
{% endif %}
</section>
</section>
{% endif %}
{% endblock %}

3
search/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

8
search/urls.py Normal file
View file

@ -0,0 +1,8 @@
from django.urls import path
from . import views
app_name="search"
urlpatterns = [
path('search', views.search, name="search"),
]

25
search/views.py Normal file
View file

@ -0,0 +1,25 @@
from django.shortcuts import render
from django.db.models import Q
from django.contrib.auth.models import User, Group
from django.contrib.auth.decorators import login_required
from coopeV3.acl import active_required
from gestion.models import Product, Menu, Keg
@active_required
@login_required
def search(request):
q = request.GET.get("q")
if q:
users = User.objects.filter(Q(username__icontains=q) | Q(first_name__icontains=q) | Q(last_name__icontains=q))
products = Product.objects.filter(name__icontains=q)
kegs = Keg.objects.filter(name__icontains=q)
menus = Menu.objects.filter(name__icontains=q)
groups = Group.objects.filter(name__icontains=q)
else:
users = User.objects.none()
products = Product.objects.none()
kegs = Keg.objects.none()
menus = Menu.objects.none()
groups = Group.objects.none()
return render(request, "search/search.html", {"q": q, "users": users, "products": products, "kegs": kegs, "menus": menus, "groups": groups})

28
staticfiles/dropdown.css Normal file
View file

@ -0,0 +1,28 @@
/* The container <div> - needed to position the dropdown content */
.dropdown {
position: relative;
display: inline-block;
}
/* Dropdown Content (Hidden by Default) */
.dropdown-content {
display: none;
position: absolute;
background-color: #f1f1f1;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
}
/* Links inside the dropdown */
.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
cursor: pointer;
}
/* Show the dropdown menu (use JS to add this class to the .dropdown-content container when the user clicks on the dropdown button) */
.show {
display:block;
}

27
staticfiles/dropdown.js Normal file
View file

@ -0,0 +1,27 @@
/* When the user clicks on the button,
toggle between hiding and showing the dropdown content */
function dropdown(target) {
var dropdowns = document.getElementsByClassName("dropdown-content");
var i;
for (i = 0; i < dropdowns.length; i++) {
var openDropdown = dropdowns[i];
if (openDropdown.classList.contains('show')) {
openDropdown.classList.remove('show');
}
}
document.getElementById(target).classList.toggle("show");
}
// Close the dropdown menu if the user clicks outside of it
window.onclick = function(event) {
if (!event.target.matches('.dropbtn')) {
var dropdowns = document.getElementsByClassName("dropdown-content");
var i;
for (i = 0; i < dropdowns.length; i++) {
var openDropdown = dropdowns[i];
if (openDropdown.classList.contains('show')) {
openDropdown.classList.remove('show');
}
}
}
}

2
staticfiles/js/breakpoints.min.js vendored Normal file
View file

@ -0,0 +1,2 @@
/* breakpoints.js v1.0 | @ajlkn | MIT licensed */
var breakpoints=function(){"use strict";function e(e){t.init(e)}var t={list:null,media:{},events:[],init:function(e){t.list=e,window.addEventListener("resize",t.poll),window.addEventListener("orientationchange",t.poll),window.addEventListener("load",t.poll),window.addEventListener("fullscreenchange",t.poll)},active:function(e){var n,a,s,i,r,d,c;if(!(e in t.media)){if(">="==e.substr(0,2)?(a="gte",n=e.substr(2)):"<="==e.substr(0,2)?(a="lte",n=e.substr(2)):">"==e.substr(0,1)?(a="gt",n=e.substr(1)):"<"==e.substr(0,1)?(a="lt",n=e.substr(1)):"!"==e.substr(0,1)?(a="not",n=e.substr(1)):(a="eq",n=e),n&&n in t.list)if(i=t.list[n],Array.isArray(i)){if(r=parseInt(i[0]),d=parseInt(i[1]),isNaN(r)){if(isNaN(d))return;c=i[1].substr(String(d).length)}else c=i[0].substr(String(r).length);if(isNaN(r))switch(a){case"gte":s="screen";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: -1px)";break;case"not":s="screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (max-width: "+d+c+")"}else if(isNaN(d))switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen";break;case"gt":s="screen and (max-width: -1px)";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+")";break;default:s="screen and (min-width: "+r+c+")"}else switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+"), screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (min-width: "+r+c+") and (max-width: "+d+c+")"}}else s="("==i.charAt(0)?"screen and "+i:i;t.media[e]=!!s&&s}return t.media[e]!==!1&&window.matchMedia(t.media[e]).matches},on:function(e,n){t.events.push({query:e,handler:n,state:!1}),t.active(e)&&n()},poll:function(){var e,n;for(e=0;e<t.events.length;e++)n=t.events[e],t.active(n.query)?n.state||(n.state=!0,n.handler()):n.state&&(n.state=!1)}};return e._=t,e.on=function(e,n){t.on(e,n)},e.active=function(e){return t.active(e)},e}();!function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?module.exports=t():e.breakpoints=t()}(this,function(){return breakpoints});

2
staticfiles/js/browser.min.js vendored Normal file
View file

@ -0,0 +1,2 @@
/* browser.js v1.0 | @ajlkn | MIT licensed */
var browser=function(){"use strict";var e={name:null,version:null,os:null,osVersion:null,touch:null,mobile:null,_canUse:null,canUse:function(n){e._canUse||(e._canUse=document.createElement("div"));var o=e._canUse.style,r=n.charAt(0).toUpperCase()+n.slice(1);return n in o||"Moz"+r in o||"Webkit"+r in o||"O"+r in o||"ms"+r in o},init:function(){var n,o,r,i,t=navigator.userAgent;for(n="other",o=0,r=[["firefox",/Firefox\/([0-9\.]+)/],["bb",/BlackBerry.+Version\/([0-9\.]+)/],["bb",/BB[0-9]+.+Version\/([0-9\.]+)/],["opera",/OPR\/([0-9\.]+)/],["opera",/Opera\/([0-9\.]+)/],["edge",/Edge\/([0-9\.]+)/],["safari",/Version\/([0-9\.]+).+Safari/],["chrome",/Chrome\/([0-9\.]+)/],["ie",/MSIE ([0-9]+)/],["ie",/Trident\/.+rv:([0-9]+)/]],i=0;i<r.length;i++)if(t.match(r[i][1])){n=r[i][0],o=parseFloat(RegExp.$1);break}for(e.name=n,e.version=o,n="other",o=0,r=[["ios",/([0-9_]+) like Mac OS X/,function(e){return e.replace("_",".").replace("_","")}],["ios",/CPU like Mac OS X/,function(e){return 0}],["wp",/Windows Phone ([0-9\.]+)/,null],["android",/Android ([0-9\.]+)/,null],["mac",/Macintosh.+Mac OS X ([0-9_]+)/,function(e){return e.replace("_",".").replace("_","")}],["windows",/Windows NT ([0-9\.]+)/,null],["bb",/BlackBerry.+Version\/([0-9\.]+)/,null],["bb",/BB[0-9]+.+Version\/([0-9\.]+)/,null],["linux",/Linux/,null],["bsd",/BSD/,null],["unix",/X11/,null]],i=0;i<r.length;i++)if(t.match(r[i][1])){n=r[i][0],o=parseFloat(r[i][2]?r[i][2](RegExp.$1):RegExp.$1);break}e.os=n,e.osVersion=o,e.touch="wp"==e.os?navigator.msMaxTouchPoints>0:!!("ontouchstart"in window),e.mobile="wp"==e.os||"android"==e.os||"ios"==e.os||"bb"==e.os}};return e.init(),e}();!function(e,n){"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?module.exports=n():e.browser=n()}(this,function(){return browser});

2
staticfiles/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
staticfiles/js/jquery.scrollex.min.js vendored Normal file
View file

@ -0,0 +1,2 @@
/* jquery.scrollex v0.2.1 | (c) @ajlkn | github.com/ajlkn/jquery.scrollex | MIT licensed */
!function(t){function e(t,e,n){return"string"==typeof t&&("%"==t.slice(-1)?t=parseInt(t.substring(0,t.length-1))/100*e:"vh"==t.slice(-2)?t=parseInt(t.substring(0,t.length-2))/100*n:"px"==t.slice(-2)&&(t=parseInt(t.substring(0,t.length-2)))),t}var n=t(window),i=1,o={};n.on("scroll",function(){var e=n.scrollTop();t.map(o,function(t){window.clearTimeout(t.timeoutId),t.timeoutId=window.setTimeout(function(){t.handler(e)},t.options.delay)})}).on("load",function(){n.trigger("scroll")}),jQuery.fn.scrollex=function(l){var s=t(this);if(0==this.length)return s;if(this.length>1){for(var r=0;r<this.length;r++)t(this[r]).scrollex(l);return s}if(s.data("_scrollexId"))return s;var a,u,h,c,p;switch(a=i++,u=jQuery.extend({top:0,bottom:0,delay:0,mode:"default",enter:null,leave:null,initialize:null,terminate:null,scroll:null},l),u.mode){case"top":h=function(t,e,n,i,o){return t>=i&&o>=t};break;case"bottom":h=function(t,e,n,i,o){return n>=i&&o>=n};break;case"middle":h=function(t,e,n,i,o){return e>=i&&o>=e};break;case"top-only":h=function(t,e,n,i,o){return i>=t&&n>=i};break;case"bottom-only":h=function(t,e,n,i,o){return n>=o&&o>=t};break;default:case"default":h=function(t,e,n,i,o){return n>=i&&o>=t}}return c=function(t){var i,o,l,s,r,a,u=this.state,h=!1,c=this.$element.offset();i=n.height(),o=t+i/2,l=t+i,s=this.$element.outerHeight(),r=c.top+e(this.options.top,s,i),a=c.top+s-e(this.options.bottom,s,i),h=this.test(t,o,l,r,a),h!=u&&(this.state=h,h?this.options.enter&&this.options.enter.apply(this.element):this.options.leave&&this.options.leave.apply(this.element)),this.options.scroll&&this.options.scroll.apply(this.element,[(o-r)/(a-r)])},p={id:a,options:u,test:h,handler:c,state:null,element:this,$element:s,timeoutId:null},o[a]=p,s.data("_scrollexId",p.id),p.options.initialize&&p.options.initialize.apply(this),s},jQuery.fn.unscrollex=function(){var e=t(this);if(0==this.length)return e;if(this.length>1){for(var n=0;n<this.length;n++)t(this[n]).unscrollex();return e}var i,l;return(i=e.data("_scrollexId"))?(l=o[i],window.clearTimeout(l.timeoutId),delete o[i],e.removeData("_scrollexId"),l.options.terminate&&l.options.terminate.apply(this),e):e}}(jQuery);

2
staticfiles/js/jquery.scrolly.min.js vendored Normal file
View file

@ -0,0 +1,2 @@
/* jquery.scrolly v1.0.0-dev | (c) @ajlkn | MIT licensed */
(function(e){function u(s,o){var u,a,f;if((u=e(s))[t]==0)return n;a=u[i]()[r];switch(o.anchor){case"middle":f=a-(e(window).height()-u.outerHeight())/2;break;default:case r:f=Math.max(a,0)}return typeof o[i]=="function"?f-=o[i]():f-=o[i],f}var t="length",n=null,r="top",i="offset",s="click.scrolly",o=e(window);e.fn.scrolly=function(i){var o,a,f,l,c=e(this);if(this[t]==0)return c;if(this[t]>1){for(o=0;o<this[t];o++)e(this[o]).scrolly(i);return c}l=n,f=c.attr("href");if(f.charAt(0)!="#"||f[t]<2)return c;a=jQuery.extend({anchor:r,easing:"swing",offset:0,parent:e("body,html"),pollOnce:!1,speed:1e3},i),a.pollOnce&&(l=u(f,a)),c.off(s).on(s,function(e){var t=l!==n?l:u(f,a);t!==n&&(e.preventDefault(),a.parent.stop().animate({scrollTop:t},a.speed,a.easing))})}})(jQuery);

123
staticfiles/js/main.js Normal file
View file

@ -0,0 +1,123 @@
/*
Stellar by HTML5 UP
html5up.net | @ajlkn
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
*/
(function($) {
var $window = $(window),
$body = $('body'),
$main = $('#main');
// Breakpoints.
breakpoints({
xlarge: [ '1281px', '1680px' ],
large: [ '981px', '1280px' ],
medium: [ '737px', '980px' ],
small: [ '481px', '736px' ],
xsmall: [ '361px', '480px' ],
xxsmall: [ null, '360px' ]
});
// Play initial animations on page load.
$window.on('load', function() {
window.setTimeout(function() {
$body.removeClass('is-preload');
}, 100);
});
// Nav.
var $nav = $('#nav');
if ($nav.length > 0) {
// Shrink effect.
$main
.scrollex({
mode: 'top',
enter: function() {
$nav.addClass('alt');
},
leave: function() {
$nav.removeClass('alt');
},
});
// Links.
var $nav_a = $nav.find('a');
$nav_a
.scrolly({
speed: 1000,
offset: function() { return $nav.height(); }
})
.on('click', function() {
var $this = $(this);
// External link? Bail.
if ($this.attr('href').charAt(0) != '#')
return;
// Deactivate all links.
$nav_a
.removeClass('active')
.removeClass('active-locked');
// Activate link *and* lock it (so Scrollex doesn't try to activate other links as we're scrolling to this one's section).
$this
.addClass('active')
.addClass('active-locked');
})
.each(function() {
var $this = $(this),
id = $this.attr('href'),
$section = $(id);
// No section for this link? Bail.
if ($section.length < 1)
return;
// Scrollex.
$section.scrollex({
mode: 'middle',
initialize: function() {
// Deactivate section.
if (browser.canUse('transition'))
$section.addClass('inactive');
},
enter: function() {
// Activate section.
$section.removeClass('inactive');
// No locked links? Deactivate all links and activate this section's one.
if ($nav_a.filter('.active-locked').length == 0) {
$nav_a.removeClass('active');
$this.addClass('active');
}
// Otherwise, if this section's link is the one that's locked, unlock it.
else if ($this.hasClass('active-locked'))
$this.removeClass('active-locked');
}
});
});
}
// Scrolly.
$('.scrolly').scrolly({
speed: 1000
});
})(jQuery);

587
staticfiles/js/util.js Normal file
View file

@ -0,0 +1,587 @@
(function($) {
/**
* Generate an indented list of links from a nav. Meant for use with panel().
* @return {jQuery} jQuery object.
*/
$.fn.navList = function() {
var $this = $(this);
$a = $this.find('a'),
b = [];
$a.each(function() {
var $this = $(this),
indent = Math.max(0, $this.parents('li').length - 1),
href = $this.attr('href'),
target = $this.attr('target');
b.push(
'<a ' +
'class="link depth-' + indent + '"' +
( (typeof target !== 'undefined' && target != '') ? ' target="' + target + '"' : '') +
( (typeof href !== 'undefined' && href != '') ? ' href="' + href + '"' : '') +
'>' +
'<span class="indent-' + indent + '"></span>' +
$this.text() +
'</a>'
);
});
return b.join('');
};
/**
* Panel-ify an element.
* @param {object} userConfig User config.
* @return {jQuery} jQuery object.
*/
$.fn.panel = function(userConfig) {
// No elements?
if (this.length == 0)
return $this;
// Multiple elements?
if (this.length > 1) {
for (var i=0; i < this.length; i++)
$(this[i]).panel(userConfig);
return $this;
}
// Vars.
var $this = $(this),
$body = $('body'),
$window = $(window),
id = $this.attr('id'),
config;
// Config.
config = $.extend({
// Delay.
delay: 0,
// Hide panel on link click.
hideOnClick: false,
// Hide panel on escape keypress.
hideOnEscape: false,
// Hide panel on swipe.
hideOnSwipe: false,
// Reset scroll position on hide.
resetScroll: false,
// Reset forms on hide.
resetForms: false,
// Side of viewport the panel will appear.
side: null,
// Target element for "class".
target: $this,
// Class to toggle.
visibleClass: 'visible'
}, userConfig);
// Expand "target" if it's not a jQuery object already.
if (typeof config.target != 'jQuery')
config.target = $(config.target);
// Panel.
// Methods.
$this._hide = function(event) {
// Already hidden? Bail.
if (!config.target.hasClass(config.visibleClass))
return;
// If an event was provided, cancel it.
if (event) {
event.preventDefault();
event.stopPropagation();
}
// Hide.
config.target.removeClass(config.visibleClass);
// Post-hide stuff.
window.setTimeout(function() {
// Reset scroll position.
if (config.resetScroll)
$this.scrollTop(0);
// Reset forms.
if (config.resetForms)
$this.find('form').each(function() {
this.reset();
});
}, config.delay);
};
// Vendor fixes.
$this
.css('-ms-overflow-style', '-ms-autohiding-scrollbar')
.css('-webkit-overflow-scrolling', 'touch');
// Hide on click.
if (config.hideOnClick) {
$this.find('a')
.css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');
$this
.on('click', 'a', function(event) {
var $a = $(this),
href = $a.attr('href'),
target = $a.attr('target');
if (!href || href == '#' || href == '' || href == '#' + id)
return;
// Cancel original event.
event.preventDefault();
event.stopPropagation();
// Hide panel.
$this._hide();
// Redirect to href.
window.setTimeout(function() {
if (target == '_blank')
window.open(href);
else
window.location.href = href;
}, config.delay + 10);
});
}
// Event: Touch stuff.
$this.on('touchstart', function(event) {
$this.touchPosX = event.originalEvent.touches[0].pageX;
$this.touchPosY = event.originalEvent.touches[0].pageY;
})
$this.on('touchmove', function(event) {
if ($this.touchPosX === null
|| $this.touchPosY === null)
return;
var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX,
diffY = $this.touchPosY - event.originalEvent.touches[0].pageY,
th = $this.outerHeight(),
ts = ($this.get(0).scrollHeight - $this.scrollTop());
// Hide on swipe?
if (config.hideOnSwipe) {
var result = false,
boundary = 20,
delta = 50;
switch (config.side) {
case 'left':
result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta);
break;
case 'right':
result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta));
break;
case 'top':
result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta);
break;
case 'bottom':
result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta));
break;
default:
break;
}
if (result) {
$this.touchPosX = null;
$this.touchPosY = null;
$this._hide();
return false;
}
}
// Prevent vertical scrolling past the top or bottom.
if (($this.scrollTop() < 0 && diffY < 0)
|| (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {
event.preventDefault();
event.stopPropagation();
}
});
// Event: Prevent certain events inside the panel from bubbling.
$this.on('click touchend touchstart touchmove', function(event) {
event.stopPropagation();
});
// Event: Hide panel if a child anchor tag pointing to its ID is clicked.
$this.on('click', 'a[href="#' + id + '"]', function(event) {
event.preventDefault();
event.stopPropagation();
config.target.removeClass(config.visibleClass);
});
// Body.
// Event: Hide panel on body click/tap.
$body.on('click touchend', function(event) {
$this._hide(event);
});
// Event: Toggle.
$body.on('click', 'a[href="#' + id + '"]', function(event) {
event.preventDefault();
event.stopPropagation();
config.target.toggleClass(config.visibleClass);
});
// Window.
// Event: Hide on ESC.
if (config.hideOnEscape)
$window.on('keydown', function(event) {
if (event.keyCode == 27)
$this._hide(event);
});
return $this;
};
/**
* Apply "placeholder" attribute polyfill to one or more forms.
* @return {jQuery} jQuery object.
*/
$.fn.placeholder = function() {
// Browser natively supports placeholders? Bail.
if (typeof (document.createElement('input')).placeholder != 'undefined')
return $(this);
// No elements?
if (this.length == 0)
return $this;
// Multiple elements?
if (this.length > 1) {
for (var i=0; i < this.length; i++)
$(this[i]).placeholder();
return $this;
}
// Vars.
var $this = $(this);
// Text, TextArea.
$this.find('input[type=text],textarea')
.each(function() {
var i = $(this);
if (i.val() == ''
|| i.val() == i.attr('placeholder'))
i
.addClass('polyfill-placeholder')
.val(i.attr('placeholder'));
})
.on('blur', function() {
var i = $(this);
if (i.attr('name').match(/-polyfill-field$/))
return;
if (i.val() == '')
i
.addClass('polyfill-placeholder')
.val(i.attr('placeholder'));
})
.on('focus', function() {
var i = $(this);
if (i.attr('name').match(/-polyfill-field$/))
return;
if (i.val() == i.attr('placeholder'))
i
.removeClass('polyfill-placeholder')
.val('');
});
// Password.
$this.find('input[type=password]')
.each(function() {
var i = $(this);
var x = $(
$('<div>')
.append(i.clone())
.remove()
.html()
.replace(/type="password"/i, 'type="text"')
.replace(/type=password/i, 'type=text')
);
if (i.attr('id') != '')
x.attr('id', i.attr('id') + '-polyfill-field');
if (i.attr('name') != '')
x.attr('name', i.attr('name') + '-polyfill-field');
x.addClass('polyfill-placeholder')
.val(x.attr('placeholder')).insertAfter(i);
if (i.val() == '')
i.hide();
else
x.hide();
i
.on('blur', function(event) {
event.preventDefault();
var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
if (i.val() == '') {
i.hide();
x.show();
}
});
x
.on('focus', function(event) {
event.preventDefault();
var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']');
x.hide();
i
.show()
.focus();
})
.on('keypress', function(event) {
event.preventDefault();
x.val('');
});
});
// Events.
$this
.on('submit', function() {
$this.find('input[type=text],input[type=password],textarea')
.each(function(event) {
var i = $(this);
if (i.attr('name').match(/-polyfill-field$/))
i.attr('name', '');
if (i.val() == i.attr('placeholder')) {
i.removeClass('polyfill-placeholder');
i.val('');
}
});
})
.on('reset', function(event) {
event.preventDefault();
$this.find('select')
.val($('option:first').val());
$this.find('input,textarea')
.each(function() {
var i = $(this),
x;
i.removeClass('polyfill-placeholder');
switch (this.type) {
case 'submit':
case 'reset':
break;
case 'password':
i.val(i.attr('defaultValue'));
x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
if (i.val() == '') {
i.hide();
x.show();
}
else {
i.show();
x.hide();
}
break;
case 'checkbox':
case 'radio':
i.attr('checked', i.attr('defaultValue'));
break;
case 'text':
case 'textarea':
i.val(i.attr('defaultValue'));
if (i.val() == '') {
i.addClass('polyfill-placeholder');
i.val(i.attr('placeholder'));
}
break;
default:
i.val(i.attr('defaultValue'));
break;
}
});
});
return $this;
};
/**
* Moves elements to/from the first positions of their respective parents.
* @param {jQuery} $elements Elements (or selector) to move.
* @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations.
*/
$.prioritize = function($elements, condition) {
var key = '__prioritize';
// Expand $elements if it's not already a jQuery object.
if (typeof $elements != 'jQuery')
$elements = $($elements);
// Step through elements.
$elements.each(function() {
var $e = $(this), $p,
$parent = $e.parent();
// No parent? Bail.
if ($parent.length == 0)
return;
// Not moved? Move it.
if (!$e.data(key)) {
// Condition is false? Bail.
if (!condition)
return;
// Get placeholder (which will serve as our point of reference for when this element needs to move back).
$p = $e.prev();
// Couldn't find anything? Means this element's already at the top, so bail.
if ($p.length == 0)
return;
// Move element to top of parent.
$e.prependTo($parent);
// Mark element as moved.
$e.data(key, $p);
}
// Moved already?
else {
// Condition is true? Bail.
if (condition)
return;
$p = $e.data(key);
// Move element back to its original location (using our placeholder).
$e.insertAfter($p);
// Unmark element as moved.
$e.removeData(key);
}
});
};
})(jQuery);

View file

@ -2,6 +2,7 @@ total = 0
products = []
menus = []
cotisations = []
reloads = []
paymentMethod = null
balance = 0
username = ""
@ -95,12 +96,33 @@ function add_cotisation(pk, duration, amount){
generate_html();
}
function add_reload(value, payment_method, payment_method_name){
exist = false;
index = -1;
for(k=0; k < reloads.length; k++){
if(reloads[k].value == value && reloads[k].payment_method == payment_method){
exist = true;
index = k;
}
}
if(exist){
reloads[index].quantity += 1;
}else{
reloads.push({"value": value, "quantity": 1, "payment_method": payment_method, "payment_method_name": payment_method_name});
}
generate_html();
}
function generate_html(){
html = "";
for(k=0;k<cotisations.length;k++){
cotisation = cotisations[k];
html += '<tr><td></td><td>Cotisation ' + String(cotisation.duration) + ' jours</td><td>' + String(cotisation.amount) + ' €</td><td><input type="number" data-target="' + String(k) + '" onChange="updateCotisationInput(this)" value="' + String(cotisation.quantity) + '"/></td><td>' + String(Number((cotisation.quantity * cotisation.amount).toFixed(2))) + ' €</td></tr>';
}
for(k=0;k<reloads.length;k++){
reload = reloads[k];
html += '<tr><td>Rechargement ' + String(reload.payment_method_name) + " " + String(reload.value) + ' €</td><td>-' + String(reload.value) + ' €</td><td><input type="number" data-target="' + String(k) + '" onChange="updateReloadInput(this)" value="' + String(reload.quantity) + '"/></td><td>-' + String(Number((reload.quantity * reload.value).toFixed(2))) + ' €</td></tr>';
}
for(k=0;k<products.length;k++){
product = products[k]
html += '<tr><td>' + product.name + '</td><td>' + String(product.amount) + ' €</td><td><input type="number" data-target="' + String(k) + '" onChange="updateInput(this)" value="' + String(product.quantity) + '"/></td><td>' + String(Number((product.quantity * product.amount).toFixed(2))) + ' €</td></tr>';
@ -109,7 +131,7 @@ function generate_html(){
menu = menus[k]
html += '<tr><td>' + menu.name + '</td><td>' + String(menu.amount) + ' €</td><td><input type="number" data-target="' + String(k) + '" onChange="updateMenuInput(this)" value="' + String(menu.quantity) + '"/></td><td>' + String(Number((menu.quantity * menu.amount).toFixed(2))) + ' €</td></tr>';
}
$("#items").html(html)
$("#items").html(html);
updateTotal();
}
@ -124,6 +146,9 @@ function updateTotal(){
for(k=0; k<cotisations.length;k++){
total += cotisations[k].quantity * cotisations[k].amount;
}
for(k=0; k<reloads.length;k++){
total -= reloads[k].quantity * reloads[k].value;
}
$("#totalAmount").text(String(Number(total.toFixed(2))) + "€")
totalAfter = balance - total
$("#totalAfter").text(String(Number(totalAfter.toFixed(2))) + "€")
@ -150,6 +175,13 @@ function updateCotisationInput(a){
generate_html();
}
function updateReloadInput(a){
quantity = parseInt(a.value);
k = parseInt(a.getAttribute("data-target"));
reloads[k].quantity = quantity;
generate_html();
}
$(document).ready(function(){
$(".cotisation-hidden").hide();
get_config();
@ -166,6 +198,10 @@ $(document).ready(function(){
cotisation = get_cotisation($(this).attr('target'));
});
$(".reload").click(function(){
add_reload(parseInt($(this).attr('target')), parseInt($(this).attr('data-payment')), $(this).attr('data-payment-name'));
})
$("#id_client").on('change', function(){
id_user = $("#id_client").val();
$.get("/users/getUser/" + id_user, function(data){
@ -206,7 +242,7 @@ $(document).ready(function(){
}
}
}
$.post("order", {"user":id_user, "paymentMethod": $(this).attr('data-payment'), "order_length": products.length + menus.length + cotisations.length, "order": JSON.stringify(products), "amount": total, "menus": JSON.stringify(menus), "listPintes": JSON.stringify(listPintes), "cotisations": JSON.stringify(cotisations)}, function(data){
$.post("order", {"user":id_user, "paymentMethod": $(this).attr('data-payment'), "order_length": products.length + menus.length + cotisations.length + reloads.length, "order": JSON.stringify(products), "amount": total, "menus": JSON.stringify(menus), "listPintes": JSON.stringify(listPintes), "cotisations": JSON.stringify(cotisations), "reloads": JSON.stringify(reloads)}, function(data){
alert(data);
location.reload();
}).fail(function(data){

View file

@ -9,6 +9,7 @@
<link rel="icon" sizes="32x32" href="{% static 'favicon32.ico' %}" type="image/x-icon">
<link rel="icon" sizes="96x96" href="{% static 'favicon96.ico' %}" type="image/x-icon">
<link rel="stylesheet" href="{% static 'css/main.css' %}" />
<link rel="stylesheet" href="{% static 'dropdown.css' %}" />
{% block extra_css %}{% endblock %}
{% block extra_script %}{% endblock %}
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
@ -17,6 +18,10 @@
</head>
<body>
<div id="wrapper">
<form method="get" action="/search/search">
<input id="search_input" placeholder="Rechercher" name="q" value="{{q}}" style="float:left; color:black;"> <button class="button small" action="submit" style="float:left;background-color:white;color:black;margin-left:10px;min-width:0;"><i class="fa fa-search" style="color:black"></i></button>
</form>
<header id="header" class="alt">
<span class="logo"><img src="{%static 'Images/coope.png' %}" alt="" /></span>
<h1>{% block entete %}{% endblock %}</h1>
@ -55,5 +60,13 @@
}
</script>
{% endif %}
<script src="{% static 'dropdown.js' %}"></script>
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/jquery.scrollex.min.js' %}"></script>
<script src="{% static 'js/jquery.scrolly.min.js' %}"></script>
<script src="{% static 'js/browser.min.js' %}"></script>
<script src="{% static 'js/breakpoints.min.js' %}"></script>
<script src="{% static 'js/util.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script>
</body>
</html>

View file

@ -42,6 +42,6 @@
<li><a href="https://www.facebook.com/coopesmetz/" class="icon fa-facebook alt"><span class="label">Facebook</span></a></li>
</ul>
</section>
<p class="copyright">coope.rez v3.6.4 (release stable) &copy; 2018-2019 Yoann Pietri. <a href="{% url 'about'%}">À propos du projet</a>.</p>
<p class="copyright">coope.rez v3.7.0 (release stable) &copy; 2018-2019 Yoann Pietri. <a href="{% url 'about'%}">À propos du projet</a>.</p>

View file

@ -70,6 +70,14 @@
<i class="fa fa-search-dollar"></i> <a href="{% url 'gestion:compute-price' %}">Calcul de prix</a>
</span>
{% endif %}
<span class="tabulation2">
<i class="fa fa-bug"></i> <a href="{% url 'preferences:addImprovement' %}">Proposition d'amélioration</a>
</span>
{% if perms.preferences.view_improvement %}
<span class="tabulation2">
<i class="fa fa-bug"></i> <a href="{% url 'preferences:improvementsIndex' %}">Améliorations</a>
</span>
{% endif %}
<span class="tabulation2">
<i class="fa fa-bed"></i> <a href="{% url 'users:logout' %}">Deconnexion</a>
</span>
@ -77,4 +85,4 @@
<span class="tabulation2">
<i class="fa fa-sign-in-alt"></i> <a href="{% url 'users:login' %}">Connexion</a>
</span>
{% endif %}
{% endif %}

View file

@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block entete %}Réinitilisation du mot de passe{% endblock %}
{% block navbar %}
<ul>
<li><a href="#first">Réinitialisation du mot de passe</a></li>
</ul>
{% endblock %}
{% block content %}
<section id="first" class="main">
<header class="major">
<h2>Réinitialisation du mot de passe</h2>
<p>Mot de passe réinitialisé.</p>
</header>
Vous pouvez vous connecter en vous rendant sur la <a href="{% url 'users:login' %}">page de connexion</a>.
</section>
{{form.media}}
{% endblock %}

View file

@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% block entete %}Réinitilisation du mot de passe{% endblock %}
{% block navbar %}
<ul>
<li><a href="#first">Réinitialisation du mot de passe</a></li>
</ul>
{% endblock %}
{% block content %}
<section id="first" class="main">
<header class="major">
<h2>Réinitialisation du mot de passe</h2>
</header>
<section>
<form method="post">
{% csrf_token %}
{{ form }}
<br>
<button type="submit"><i class="fa fa-lock"></i> Changer le mot de passe</button>
</form>
</section>
</section>
{{form.media}}
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends 'base.html' %}
{% block entete %}Réinitilisation du mot de passe{% endblock %}
{% block navbar %}
<ul>
<li><a href="#first">Réinitialisation du mot de passe</a></li>
</ul>
{% endblock %}
{% block content %}
<section id="first" class="main">
<header class="major">
<h2>Réinitialisation du mot de passe</h2>
<p>Un mail vous a été envoyé avec un lien pour réinitialiser le mot de passe.</p>
</header>
</section>
{{form.media}}
{% endblock %}

View file

@ -0,0 +1,11 @@
{% autoescape off %}
Bonjour {{user.username}},
Vous avez demandé une réinitalisation de votre mot de passe sur le site de gestion de la Coopé Technopôle Metz, vous pouvez le faire en cliquant sur le lien ci dessous:
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
Si le lien ne fonctionne pas en cliquant, vous pouvez le copier-coller dans votre navigateur,
Le staff Coopé Technopôle Metz
{% endautoescape %}

View file

@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block entete %}Réinitilisation du mot de passe{% endblock %}
{% block navbar %}
<ul>
<li><a href="#first">Réinitialisation du mot de passe</a></li>
</ul>
{% endblock %}
{% block content %}
<section id="first" class="main">
<header class="major">
<h2>Réinitialisation du mot de passe</h2>
<p>Vous recevrez un lien pour réinitilisaser votre mot de passe sur votre adresse e-mail.</p>
</header>
<section>
<form method="post">
{% csrf_token %}
{{ form }}
<br>
<button type="submit"><i class="fa fa-lock"></i> Réinitialiser</button>
</form>
</section>
</section>
{{form.media}}
{% endblock %}

View file

@ -0,0 +1 @@
Réinitialisation du mot de passe Coopé TM

View file

@ -21,6 +21,12 @@ class CreateUserForm(forms.ModelForm):
school = forms.ModelChoiceField(queryset=School.objects.all(), label="École")
def clean(self):
cleaned_data = super().clean()
email = cleaned_data.get("email")
if User.objects.filter(email=email).count() > 0:
raise forms.ValidationError("L'email est déjà utilisé")
class CreateGroupForm(forms.ModelForm):
"""
Form to create a new group (:class:`django.contrib.auth.models.Group`).

View file

@ -7,6 +7,7 @@ from simple_history.models import HistoricalRecords
from preferences.models import PaymentMethod, Cotisation
from gestion.models import ConsumptionHistory
class School(models.Model):
"""
Stores school.

View file

@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% block entete %}{{form_title}}{% endblock %}
{% block navbar %}
<ul>
<li><a href="#first">{{form_title}}</a></li>
</ul>
{% endblock %}
{% block content %}
<section id="first" class="main">
<header class="major">
<h2>{{form_title}}</h2>
<p>{{form_p}}</p>
</header>
<section>
<form action="" method="post">
{% csrf_token %}
{{ form }}
<br>
{{ extra_html | safe }}<br><br>
<button type="submit"><i class="fa fa-{{form_button_icon}}"></i> {{form_button}}</button>
</form>
</section>
Si vous avez perdu votre mot de passe : <a href="{% url 'password_reset' %}">mot de passe oublié</a>.
</section>
{% if extra_css %}
<style>
{{extra_css}}
</style>
{% endif %}
{{form.media}}
{% endblock %}

View file

@ -58,9 +58,6 @@
{% if self %}
<span class="tabulation"><a href="{% url 'users:editPassword' user.pk %}"><i class="fa fa-user-lock"></i> Changer mon mot de passe</a></span>
{% endif %}
{% if perms.users.can_reset_password %}
<span class="tabulation"><a href="{% url 'users:resetPassword' user.pk %}"><i class="fa fa-lock-open"></i> Réinitialiser le mot de passe</a></span>
{% endif %}
{% if perms.users.can_change_user_perm %}
<span class="tabulation"><a href="{% url 'users:editGroups' user.pk %}"><i class="fa fa-layer-group"></i> Changer les groupes</a></span>
{% endif %}

View file

@ -0,0 +1,15 @@
{% autoescape off %}
Bonjour {{user.username}},<br>
Vous venez de créer votre compte sur le logiciel de gestion de l'association Coopé Technopôle Metz. Pour finir vous adhésion à l'association, vous devez
<ul>
<li>lire et accepter les statuts et le règlement intérieur (disponibles en pièces jointes),</li>
<li>vous acquittez d'une cotisation auprès de l'un de nos membres actifs.</li>
</ul>
Vous pouvez acceder à votre compte sur {{protocol}}://{{domain}} après avoir activé votre mot de passe avec le lien suivant : <br>
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}<br><br>
Le Staff Coopé Technopôle Metz
{% endautoescape %}

View file

@ -0,0 +1,11 @@
Bonjour {{user.username}},
Vous venez de créer votre compte sur le logiciel de gestion de l'association Coopé Technopôle Metz. Pour finir vous adhésion à l'association, vous devez
- lire et accepter les statuts et le règlement intérieur (disponibles en pièces jointes),
- vous acquittez d'une cotisation auprès de l'un de nos membres actifs.
Vous pouvez acceder à votre compte sur {{procotol}}://{{domain}} après avoir activé votre mot de passe avec le lien suivant :
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
Le Staff Coopé Technopôle Metz

View file

@ -1,4 +1,5 @@
from django.urls import path
from django.urls import path, include
from . import views
app_name="users"
@ -13,7 +14,6 @@ urlpatterns = [
path('editGroups/<int:pk>', views.editGroups, name="editGroups"),
path('editPassword/<int:pk>', views.editPassword, name="editPassword"),
path('editUser/<int:pk>', views.editUser, name="editUser"),
path('resetPassword/<int:pk>', views.resetPassword, name="resetPassword"),
path('groupsIndex', views.groupsIndex, name="groupsIndex"),
path('groupProfile/<int:pk>', views.groupProfile, name="groupProfile"),
path('createGroup', views.createGroup, name="createGroup"),

View file

@ -2,14 +2,21 @@ from django.shortcuts import render, get_object_or_404, redirect
from django.urls import reverse
from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_encode
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.core.mail import EmailMultiAlternatives
from django.template.loader import get_template
from django.template import Context
from django.contrib.auth.decorators import login_required, permission_required
from django.forms.models import model_to_dict
from django.utils import timezone
from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from django.utils.encoding import force_bytes
import simplejson as json
from datetime import datetime, timedelta
@ -23,6 +30,7 @@ from coopeV3.acl import admin_required, superuser_required, self_or_has_perm, ac
from .models import CotisationHistory, WhiteListHistory, School
from .forms import CreateUserForm, LoginForm, CreateGroupForm, EditGroupForm, SelectUserForm, GroupsEditForm, EditPasswordForm, addCotisationHistoryForm, addCotisationHistoryForm, addWhiteListHistoryForm, SelectNonAdminUserForm, SelectNonSuperUserForm, SchoolForm, ExportForm
from gestion.models import Reload, Consumption, ConsumptionHistory, MenuHistory
from preferences.models import GeneralPreferences
@active_required
def loginView(request):
@ -38,7 +46,7 @@ def loginView(request):
return redirect(reverse('home'))
else:
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", "form_button_icon": "sign-in-alt"})
return render(request, "users/login.html", {"form_entete": "Connexion", "form": form, "form_title": "Connexion", "form_button": "Se connecter", "form_button_icon": "sign-in-alt"})
@active_required
@login_required
@ -169,6 +177,30 @@ def createUser(request):
user.save()
user.profile.school = form.cleaned_data['school']
user.save()
uid = urlsafe_base64_encode(force_bytes(user.pk)).decode('UTF-8')
print(uid)
token = default_token_generator.make_token(user)
plaintext = get_template('users/welcome_email.txt')
htmly = get_template('users/welcome_email.html')
context = {'user': user, 'uid': uid, 'token': token, 'protocol': "http", 'domain': get_current_site(request).name}
text_content = plaintext.render(context)
html_content = htmly.render(context)
email = EmailMultiAlternatives(
"Bienvenue à l'association Coopé Technopôle Metz",
text_content,
"Coopé Technopôle Metz <no-reply@coope.rezometz.org>",
[user.email],
reply_to=["coopemetz@gmail.com"]
)
email.attach_alternative(html_content, "text/html")
gp,_ = GeneralPreferences.objects.get_or_create(pk=1)
if gp.statutes:
#email.attach("statuts.pdf", gp.statutes.read(), "application/pdf")
pass
if gp.rules:
#email.attach("ri.pdf", gp.rules.read(), "application/pdf")
pass
email.send()
messages.success(request, "L'utilisateur a bien été créé")
return redirect(reverse('users:profile', kwargs={'pk':user.pk}))
return render(request, "form.html", {"form_entete": "Gestion des utilisateurs", "form":form, "form_title":"Création d'un nouvel utilisateur", "form_button":"Créer mon compte", "form_button_icon": "user-plus", 'extra_html': '<strong>En cliquant sur le bouton "Créer mon compte", vous :<ul><li>attestez sur l\'honneur que les informations fournies à l\'association Coopé Technopôle Metz sont correctes et que vous n\'avez jamais été enregistré dans l\'association sous un autre nom / pseudonyme</li><li>joignez l\'association de votre plein gré</li><li>vous engagez à respecter les statuts et le réglement intérieur de l\'association (envoyés par mail)</li><li>reconnaissez le but de l\'assocation Coopé Technopôle Metz et vous attestez avoir pris conaissances des droits et des devoirs des membres de l\'association</li><li>consentez à ce que les données fournies à l\'association, ainsi que vos autres données de compte (débit, crédit, solde et historique des transactions) soient stockées dans le logiciel de gestion et accessibles par tous les membres actifs de l\'association, en particulier par le comité de direction</li></ul></strong>'})
@ -250,23 +282,6 @@ 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", "form_button_icon": "pencil-alt"})
@active_required
@login_required
@permission_required('auth.change_user')
def resetPassword(request, pk):
"""
Reset the password of a user (:class:`django.contrib.auth.models.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.")
return redirect(reverse('users:profile', kwargs={'pk': pk}))
else:
user.set_password(user.username)
user.save()
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')
@ -343,7 +358,7 @@ def gen_user_infos(request, pk):
"""
Generates a latex document include adhesion certificate and list of `cotisations <users.models.CotisationHistory>`.
"""
user= get_object_or_404(User, pk=pk)
user = get_object_or_404(User, pk=pk)
cotisations = CotisationHistory.objects.filter(user=user).order_by('-paymentDate')
now = datetime.now()
path = os.path.join(settings.BASE_DIR, "templates/coope.png")