Browse Source

Merge pull request #20 from nanoy42/release-3.7

Release 3.7
pull/21/head v3.7.0
Yoann Pietri 6 years ago
committed by GitHub
parent
commit
f2c6ae8ab0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      CHANGELOG.md
  2. 5
      coopeV3/settings.py
  3. 3
      coopeV3/urls.py
  4. 21
      django_tex/LICENSE
  5. 50
      django_tex/core.py
  6. 11
      django_tex/engine.py
  7. 16
      django_tex/environment.py
  8. 40
      django_tex/exceptions.py
  9. 9
      django_tex/filters.py
  10. 19
      django_tex/models.py
  11. 17
      django_tex/views.py
  12. 5
      gestion/forms.py
  13. 33
      gestion/migrations/0014_auto_20190912_0951.py
  14. 1
      gestion/models.py
  15. 4
      gestion/templates/gestion/invoice.tex
  16. 24
      gestion/templates/gestion/manage.html
  17. 2
      gestion/templates/gestion/menus_list.html
  18. 2
      gestion/templates/gestion/products_list.html
  19. 2
      gestion/templates/gestion/ranking.html
  20. 75
      gestion/views.py
  21. 12
      preferences/admin.py
  22. 10
      preferences/forms.py
  23. 31
      preferences/migrations/0019_improvement.py
  24. 39
      preferences/migrations/0020_auto_20190908_1217.py
  25. 28
      preferences/models.py
  26. 21
      preferences/templates/preferences/improvement_profile.html
  27. 68
      preferences/templates/preferences/improvements_index.html
  28. 8
      preferences/templates/preferences/payment_methods_index.html
  29. 2
      preferences/templates/preferences/price_profiles_index.html
  30. 9
      preferences/urls.py
  31. 75
      preferences/views.py
  32. 1
      requirements.txt
  33. 0
      search/__init__.py
  34. 3
      search/admin.py
  35. 5
      search/apps.py
  36. 0
      search/migrations/__init__.py
  37. 3
      search/models.py
  38. 224
      search/templates/search/search.html
  39. 3
      search/tests.py
  40. 8
      search/urls.py
  41. 25
      search/views.py
  42. 28
      staticfiles/dropdown.css
  43. 27
      staticfiles/dropdown.js
  44. 2
      staticfiles/js/breakpoints.min.js
  45. 2
      staticfiles/js/browser.min.js
  46. 2
      staticfiles/js/jquery.min.js
  47. 2
      staticfiles/js/jquery.scrollex.min.js
  48. 2
      staticfiles/js/jquery.scrolly.min.js
  49. 123
      staticfiles/js/main.js
  50. 587
      staticfiles/js/util.js
  51. 40
      staticfiles/manage.js
  52. 13
      templates/base.html
  53. 2
      templates/footer.html
  54. 8
      templates/nav.html
  55. 17
      templates/registration/password_reset_complete.html
  56. 23
      templates/registration/password_reset_confirm.html
  57. 16
      templates/registration/password_reset_done.html
  58. 11
      templates/registration/password_reset_email.html
  59. 24
      templates/registration/password_reset_form.html
  60. 1
      templates/registration/password_reset_subject.txt
  61. 6
      users/forms.py
  62. 1
      users/models.py
  63. 31
      users/templates/users/login.html
  64. 3
      users/templates/users/profile.html
  65. 15
      users/templates/users/welcome_email.html
  66. 11
      users/templates/users/welcome_email.txt
  67. 4
      users/urls.py
  68. 53
      users/views.py

9
CHANGELOG.md

@ -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

5
coopeV3/settings.py

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

3
coopeV3/urls.py

@ -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)

21
django_tex/LICENSE

@ -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.

50
django_tex/core.py

@ -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)

11
django_tex/engine.py

@ -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)

16
django_tex/environment.py

@ -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

40
django_tex/exceptions.py

@ -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

9
django_tex/filters.py

@ -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
}

19
django_tex/models.py

@ -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

17
django_tex/views.py

@ -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)

5
gestion/forms.py

@ -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):

33
gestion/migrations/0014_auto_20190912_0951.py

@ -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)
]

1
gestion/models.py

@ -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):

4
gestion/templates/gestion/invoice.tex

@ -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}\\

24
gestion/templates/gestion/manage.html

@ -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 %}

2
gestion/templates/gestion/menus_list.html

@ -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 %}

2
gestion/templates/gestion/products_list.html

@ -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>

2
gestion/templates/gestion/ranking.html

@ -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">

75
gestion/views.py

@ -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")

12
preferences/admin.py

@ -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(Improvement, ImprovementAdmin)

10
preferences/forms.py

@ -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"]

31
preferences/migrations/0019_improvement.py

@ -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',
},
),
]

39
preferences/migrations/0020_auto_20190908_1217.py

@ -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'),
),
]

28
preferences/models.py

@ -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)

21
preferences/templates/preferences/improvement_profile.html

@ -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 %}

68
preferences/templates/preferences/improvements_index.html

@ -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 %}

8
preferences/templates/preferences/payment_methods_index.html

@ -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>

2
preferences/templates/preferences/price_profiles_index.html

@ -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 %}

9
preferences/urls.py

@ -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"),
]

75
preferences/views.py

@ -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'))

1
requirements.txt

@ -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

0
django_tex/__init__.py → search/__init__.py

3
search/admin.py

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

5
search/apps.py

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

0
search/migrations/__init__.py

3
search/models.py

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

224
search/templates/search/search.html

@ -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

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

8
search/urls.py

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

File diff suppressed because one or more lines are too long

2
staticfiles/js/jquery.scrollex.min.js

@ -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

@ -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

@ -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

@ -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);

40
staticfiles/manage.js

@ -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){

13
templates/base.html

@ -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>

2
templates/footer.html

@ -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>

8
templates/nav.html

@ -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>

17
templates/registration/password_reset_complete.html

@ -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 %}

23
templates/registration/password_reset_confirm.html

@ -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 %}

16
templates/registration/password_reset_done.html

@ -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 %}

11
templates/registration/password_reset_email.html

@ -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 %}

24
templates/registration/password_reset_form.html

@ -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 %}

1
templates/registration/password_reset_subject.txt

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

6
users/forms.py

@ -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`).

1
users/models.py

@ -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.

31
users/templates/users/login.html

@ -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 %}

3
users/templates/users/profile.html

@ -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 %}

15
users/templates/users/welcome_email.html

@ -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 %}

11
users/templates/users/welcome_email.txt

@ -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

4
users/urls.py

@ -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"),

53
users/views.py

@ -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")

Loading…
Cancel
Save