diff --git a/CHANGELOG.md b/CHANGELOG.md index 031d8a6..b84aba8 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/coopeV3/settings.py b/coopeV3/settings.py index 7b3448b..5555b2e 100644 --- a/coopeV3/settings.py +++ b/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 = [ @@ -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"] \ No newline at end of file +INTERNAL_IPS = ["127.0.0.1"] + +EMAIL_SUBJECT_PREFIX = "[Coopé Technopôle Metz] " \ No newline at end of file diff --git a/coopeV3/urls.py b/coopeV3/urls.py index 8247789..294fecc 100644 --- a/coopeV3/urls.py +++ b/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) diff --git a/django_tex/LICENSE b/django_tex/LICENSE deleted file mode 100644 index f3d9af1..0000000 --- a/django_tex/LICENSE +++ /dev/null @@ -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. diff --git a/django_tex/core.py b/django_tex/core.py deleted file mode 100644 index 22fdacb..0000000 --- a/django_tex/core.py +++ /dev/null @@ -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) diff --git a/django_tex/engine.py b/django_tex/engine.py deleted file mode 100644 index 33c1d7f..0000000 --- a/django_tex/engine.py +++ /dev/null @@ -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) diff --git a/django_tex/environment.py b/django_tex/environment.py deleted file mode 100644 index 472a443..0000000 --- a/django_tex/environment.py +++ /dev/null @@ -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 diff --git a/django_tex/exceptions.py b/django_tex/exceptions.py deleted file mode 100644 index 5d4ee38..0000000 --- a/django_tex/exceptions.py +++ /dev/null @@ -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 diff --git a/django_tex/filters.py b/django_tex/filters.py deleted file mode 100644 index 732374f..0000000 --- a/django_tex/filters.py +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/django_tex/models.py b/django_tex/models.py deleted file mode 100644 index 214d31e..0000000 --- a/django_tex/models.py +++ /dev/null @@ -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 diff --git a/django_tex/views.py b/django_tex/views.py deleted file mode 100644 index df65223..0000000 --- a/django_tex/views.py +++ /dev/null @@ -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) diff --git a/gestion/forms.py b/gestion/forms.py index 20822bc..cb75c03 100644 --- a/gestion/forms.py +++ b/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): diff --git a/gestion/migrations/0014_auto_20190912_0951.py b/gestion/migrations/0014_auto_20190912_0951.py new file mode 100644 index 0000000..e1f5dba --- /dev/null +++ b/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) + ] diff --git a/gestion/models.py b/gestion/models.py index 627cebf..f736d66 100644 --- a/gestion/models.py +++ b/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): diff --git a/gestion/templates/gestion/invoice.tex b/gestion/templates/gestion/invoice.tex index 8fd7136..aceff2c 100644 --- a/gestion/templates/gestion/invoice.tex +++ b/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}\\ diff --git a/gestion/templates/gestion/manage.html b/gestion/templates/gestion/manage.html index d4f49f6..4d2da2a 100644 --- a/gestion/templates/gestion/manage.html +++ b/gestion/templates/gestion/manage.html @@ -53,7 +53,7 @@ } {% if perms.gestion.add_consumptionhistory %} -
+
@@ -61,7 +61,6 @@
- Annuler

{{gestion_form}}
@@ -84,7 +83,7 @@ 0€ 0€ 0€ - {% for pm in pay_buttons %} {% endfor %} + {% for pm in pay_buttons %} {% endfor %} Annuler @@ -127,6 +126,25 @@ {% if not cotisations|divisibleby:3 %} {% endif %} + + Rechargements + + + + + + + Bières pression {% for product in bieresPression %} {% if forloop.counter0|divisibleby:3 %} diff --git a/gestion/templates/gestion/menus_list.html b/gestion/templates/gestion/menus_list.html index 453240a..d80ef1d 100644 --- a/gestion/templates/gestion/menus_list.html +++ b/gestion/templates/gestion/menus_list.html @@ -28,7 +28,7 @@ {{ menu.name }} {{ menu.amount}} € {% for art in menu.articles.all %}{{art}},{% endfor %} - {{ menu.is_active | yesno:"Oui, Non"}} + {% if perms.gestion.change_menu %}{% if menu.is_active %} Désa{% else %} A{% endif %}ctiver Modifier{% endif %} {% endfor %} diff --git a/gestion/templates/gestion/products_list.html b/gestion/templates/gestion/products_list.html index 2a0aa44..5b1122b 100644 --- a/gestion/templates/gestion/products_list.html +++ b/gestion/templates/gestion/products_list.html @@ -34,7 +34,7 @@ {{ product.amount}} {{ product.stock }} {{ product.category }} - {{ product.is_active | yesno:"Oui, Non"}} + {{ product.deg }} {{ product.volume }} cl Profil {% if perms.gestion.change_product %}{% if product.is_active %} Désa{% else %} A{% endif %}ctiver Modifier{% endif %} diff --git a/gestion/templates/gestion/ranking.html b/gestion/templates/gestion/ranking.html index 9b6114b..12d4028 100644 --- a/gestion/templates/gestion/ranking.html +++ b/gestion/templates/gestion/ranking.html @@ -9,7 +9,7 @@ {% endblock %} {% block content %} -
+
diff --git a/gestion/views.py b/gestion/views.py index fc45a4b..810d5bf 100644 --- a/gestion/views.py +++ b/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") diff --git a/preferences/admin.py b/preferences/admin.py index 626fa75..239dc61 100644 --- a/preferences/admin.py +++ b/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) \ No newline at end of file +admin.site.register(DivideHistory, DivideHistoryAdmin) +admin.site.register(Improvement, ImprovementAdmin) \ No newline at end of file diff --git a/preferences/forms.py b/preferences/forms.py index 1ccd939..089269f 100644 --- a/preferences/forms.py +++ b/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"] diff --git a/preferences/migrations/0019_improvement.py b/preferences/migrations/0019_improvement.py new file mode 100644 index 0000000..6e24176 --- /dev/null +++ b/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', + }, + ), + ] diff --git a/preferences/migrations/0020_auto_20190908_1217.py b/preferences/migrations/0020_auto_20190908_1217.py new file mode 100644 index 0000000..f246c11 --- /dev/null +++ b/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'), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 161b079..4ffc74e 100644 --- a/preferences/models.py +++ b/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) + + \ No newline at end of file diff --git a/preferences/templates/preferences/improvement_profile.html b/preferences/templates/preferences/improvement_profile.html new file mode 100644 index 0000000..ff2af09 --- /dev/null +++ b/preferences/templates/preferences/improvement_profile.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% block entete %}Amélioration {{improvement.title}}{% endblock %} +{% block navbar %} + +{% endblock %} +{% block content %} +
+
+

{{improvement.title}}

+
+ Retour à la liste des améliorations

+ Titre : {{improvement.title}}
+ Type : {{improvement.get_mode_display}}
+ Date : {{improvement.date}}
+ Fait : {{improvement.done|yesno:"Oui,Non"}}
+ Coopeman : {{improvement.coopeman}}
+ Description : {{improvement.description}}
+
+{% endblock %} diff --git a/preferences/templates/preferences/improvements_index.html b/preferences/templates/preferences/improvements_index.html new file mode 100644 index 0000000..bf899cd --- /dev/null +++ b/preferences/templates/preferences/improvements_index.html @@ -0,0 +1,68 @@ +{% extends "base.html" %} +{% block entete %}Améliorations{% endblock %} +{% block navbar %} + +{% endblock %} +{% block content %} +
+
+

Liste des améliorations à faire

+
+
+ + + + + + + + + + + + {% for improvement in todo_improvements %} + + + + + + + + {% endfor %} + +
TitreTypeVu ?DateAdministration
{{improvement.title}}{{improvement.get_mode_display}}{{improvement.date}} Voir Passer en fait Supprimer
+
+
+
+
+

Liste des améliorations faîtes

+
+
+ + + + + + + + + + + + {% for improvement in done_improvements %} + + + + + + + + {% endfor %} + +
TitreTypeVu ?DateAdministration
{{improvement.title}}{{improvement.get_mode_display}}{{improvement.date}} Voir Passer en non fait Supprimer
+
+
+{% endblock %} diff --git a/preferences/templates/preferences/payment_methods_index.html b/preferences/templates/preferences/payment_methods_index.html index 31e6454..6fdd8ee 100644 --- a/preferences/templates/preferences/payment_methods_index.html +++ b/preferences/templates/preferences/payment_methods_index.html @@ -30,10 +30,10 @@ {% for pm in paymentMethods %} {{ pm.name }} - {{ pm.is_active | yesno:"Oui, Non"}} - {{ pm.is_usable_in_cotisation | yesno:"Oui, Non" }} - {{ pm.is_usable_in_reload | yesno:"Oui, Non" }} - {{ pm.affect_balance | yesno:"Oui, Non" }} + + + + {% if perms.preferences.change_paymentmethod %} Modifier {% endif %}{% if perms.preferences.delete_paymentmethod %} Supprimer{% endif %} diff --git a/preferences/templates/preferences/price_profiles_index.html b/preferences/templates/preferences/price_profiles_index.html index 8cbddae..a01b71b 100644 --- a/preferences/templates/preferences/price_profiles_index.html +++ b/preferences/templates/preferences/price_profiles_index.html @@ -34,7 +34,7 @@ {{ pp.b }} {{ pp.c }} {{ pp.alpha }} - {{ pp.use_for_draft | yesno:"Oui,Non"}} + {% if perms.preferences.change_priceprofile %} Modifier {% endif %}{% if perms.preferences.delete_priceprofile %} Supprimer{% endif %} {% endfor %} diff --git a/preferences/urls.py b/preferences/urls.py index fa31467..ddf0878 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -19,5 +19,10 @@ urlpatterns = [ path('deletePriceProfile/', views.delete_price_profile, name="deletePriceProfile"), path('inactive', views.inactive, name="inactive"), path('getConfig', views.get_config, name="getConfig"), - path('getCotisation/', views.get_cotisation, name="getCotisation") -,] + path('getCotisation/', views.get_cotisation, name="getCotisation"), + path('addImprovement', views.add_improvement, name="addImprovement"), + path('improvementsIndex', views.improvements_index, name="improvementsIndex"), + path('improvementProfile/', views.improvement_profile, name="improvementProfile"), + path('deleteImprovement/', views.delete_improvement, name="deleteImprovement"), + path('changeImprovementState/', views.change_improvement_state, name="changeImprovementState"), +] diff --git a/preferences/views.py b/preferences/views.py index ffeda09..d8bc7b9 100644 --- a/preferences/views.py +++ b/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')) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 935875a..2b26c1c 100644 --- a/requirements.txt +++ b/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 diff --git a/django_tex/__init__.py b/search/__init__.py similarity index 100% rename from django_tex/__init__.py rename to search/__init__.py diff --git a/search/admin.py b/search/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/search/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/search/apps.py b/search/apps.py new file mode 100644 index 0000000..5726231 --- /dev/null +++ b/search/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SearchConfig(AppConfig): + name = 'search' diff --git a/search/migrations/__init__.py b/search/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/search/models.py b/search/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/search/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/search/templates/search/search.html b/search/templates/search/search.html new file mode 100644 index 0000000..088ce56 --- /dev/null +++ b/search/templates/search/search.html @@ -0,0 +1,224 @@ +{% extends 'base.html' %} +{% block entete %}Recherche{% endblock %} +{% block navbar%} + +{% endblock %} +{% block content %} +{% if perms.auth.view_user %} +
+
+

Résultats dans les utilisateurs ({{users.count}} résultat{% if users.count != 1 %}s{% endif %})

+
+
+ {% if users.count %} +
+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {%endfor%} + +
Nom d'utilisateurPrénom NomSoldeFin d'adhésionStaffProfil
{{user.username}}{{user.first_name}} {{user.last_name}}{{user.profile.balance}} €{% if user.profile.is_adherent %}{{user.profile.cotisationEnd}}{% else %}Non adhérent{% endif%} Profil
+
+ {% else %} + Aucun résultat n'a pu être trouvé. + {% endif %} +
+
+{% endif %} +{% if perms.gestion.view_product %} +
+
+

Résultats dans les produits ({{products.count}} résultat{% if products.count != 1 %}s{% endif %})

+
+
+ {% if products.count %} +
+ + + + + + + + + + + + + + + + {% for product in products %} + + + + + + + + + + + + {%endfor%} + +
NomPrixActifCatégorieAdhérentStockVolumeDegréAdministration
{{product.name}}{{product.amount}} €{{product.category}}{{product.stock}}{{product.volume}} cl{{product.deg}}{% if perms.gestion.change_product %} {{product.is_active|yesno:"Désa,A"}}ctiver Modifier{% endif %}
+
+ {% else %} + Aucun résultat n'a pu être trouvé. + {% endif %} +
+
+{% endif %} +{% if perms.gestion.view_keg %} +
+
+

Résultats dans les fûts ({{kegs.count}} résultat{% if kegs.count != 1 %}s{% endif %})

+
+
+ {% if kegs.count %} +
+ + + + + + + + + + + + + + + {% for keg in kegs %} + + + + + + + + + + + {%endfor%} + +
NomStockCapacitéActifPrix du fûtDegréHistoriqueAdministration
{{keg.name}}{{keg.stockHold}}{{keg.capacity}} L{{keg.amount}} €{{keg.deg}}° Voir{% if perms.gestion.change_keg %} Modifier{% endif %}
+
+ {% else %} + Aucun résultat n'a pu être trouvé. + {% endif %} +
+
+{% endif %} +{% if perms.gestion.view_menu %} +
+
+

Résultats dans les menus ({{menus.count}} résultat{% if menus.count != 1 %}s{% endif %})

+
+
+ {% if menus.count %} +
+ + + + + + + + + + + + + {% for menu in menus %} + + + + + + + + + {%endfor%} + +
NomPrixActifAdhérentNombre de produitAdministration
{{menu.name}}{{menu.amount}} €{{menu.articles.count}}{% if perms.gestion.change_menu %} {{menu_is_active|yesno:"Désa,A"}}ctiver Modifier{% endif %}
+
+ {% else %} + Aucun résultat n'a pu être trouvé. + {% endif %} +
+
+{% endif %} +{% if perms.auth.view_group %} +
+
+

Résultats dans les groupes ({{groups.count}} résultat{% if groups.count != 1 %}s{% endif %})

+
+
+ {% if groups.count %} +
+ + + + + + + + + + + {% for group in groups %} + + + + + + + {%endfor%} + +
NomNombre de droitsNombre d'utilisateursAdministrer
{{group.name}}{{group.permissions.count}}{{group.user_set.count}} Voir{% if perms.auth.change_group %} Modifier{% endif %}
+
+ {% else %} + Aucun résultat n'a pu être trouvé. + {% endif %} +
+
+{% endif %} +{% endblock %} diff --git a/search/tests.py b/search/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/search/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/search/urls.py b/search/urls.py new file mode 100644 index 0000000..cc7974b --- /dev/null +++ b/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"), +] \ No newline at end of file diff --git a/search/views.py b/search/views.py new file mode 100644 index 0000000..49306ee --- /dev/null +++ b/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}) \ No newline at end of file diff --git a/staticfiles/dropdown.css b/staticfiles/dropdown.css new file mode 100644 index 0000000..97b8a3d --- /dev/null +++ b/staticfiles/dropdown.css @@ -0,0 +1,28 @@ +/* The container
- 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; +} \ No newline at end of file diff --git a/staticfiles/dropdown.js b/staticfiles/dropdown.js new file mode 100644 index 0000000..93f4671 --- /dev/null +++ b/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'); + } + } + } +} \ No newline at end of file diff --git a/staticfiles/js/breakpoints.min.js b/staticfiles/js/breakpoints.min.js new file mode 100644 index 0000000..32419cc --- /dev/null +++ b/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;e0:!!("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}); diff --git a/staticfiles/js/jquery.min.js b/staticfiles/js/jquery.min.js new file mode 100644 index 0000000..a1c07fd --- /dev/null +++ b/staticfiles/js/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 01){for(var r=0;r=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;n1){for(o=0;o 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); \ No newline at end of file diff --git a/staticfiles/js/util.js b/staticfiles/js/util.js new file mode 100644 index 0000000..bdb8e9f --- /dev/null +++ b/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( + '' + + '' + + $this.text() + + '' + ); + + }); + + 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 = $( + $('
') + .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); \ No newline at end of file diff --git a/staticfiles/manage.js b/staticfiles/manage.js index 0efcd3c..aa0a629 100644 --- a/staticfiles/manage.js +++ b/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' + String(cotisation.amount) + ' €' + String(Number((cotisation.quantity * cotisation.amount).toFixed(2))) + ' €'; } + for(k=0;k-' + String(reload.value) + ' €-' + String(Number((reload.quantity * reload.value).toFixed(2))) + ' €'; + } for(k=0;k' + String(product.amount) + ' €' + String(Number((product.quantity * product.amount).toFixed(2))) + ' €'; @@ -109,7 +131,7 @@ function generate_html(){ menu = menus[k] html += '' + menu.name + '' + String(menu.amount) + ' €' + String(Number((menu.quantity * menu.amount).toFixed(2))) + ' €'; } - $("#items").html(html) + $("#items").html(html); updateTotal(); } @@ -124,6 +146,9 @@ function updateTotal(){ for(k=0; k
- + diff --git a/templates/nav.html b/templates/nav.html index c8583bd..4a82cd4 100644 --- a/templates/nav.html +++ b/templates/nav.html @@ -70,6 +70,14 @@ Calcul de prix {% endif %} + + Proposition d'amélioration + +{% if perms.preferences.view_improvement %} + + Améliorations + +{% endif %} Deconnexion @@ -77,4 +85,4 @@ Connexion -{% endif %} +{% endif %} \ No newline at end of file diff --git a/templates/registration/password_reset_complete.html b/templates/registration/password_reset_complete.html new file mode 100644 index 0000000..b364960 --- /dev/null +++ b/templates/registration/password_reset_complete.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% block entete %}Réinitilisation du mot de passe{% endblock %} +{% block navbar %} + +{% endblock %} +{% block content %} +
+
+

Réinitialisation du mot de passe

+

Mot de passe réinitialisé.

+
+ Vous pouvez vous connecter en vous rendant sur la page de connexion. +
+{{form.media}} +{% endblock %} diff --git a/templates/registration/password_reset_confirm.html b/templates/registration/password_reset_confirm.html new file mode 100644 index 0000000..ed0cc09 --- /dev/null +++ b/templates/registration/password_reset_confirm.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} +{% block entete %}Réinitilisation du mot de passe{% endblock %} +{% block navbar %} + +{% endblock %} +{% block content %} +
+
+

Réinitialisation du mot de passe

+
+
+
+ {% csrf_token %} + {{ form }} +
+ +
+
+
+{{form.media}} +{% endblock %} diff --git a/templates/registration/password_reset_done.html b/templates/registration/password_reset_done.html new file mode 100644 index 0000000..4a5d686 --- /dev/null +++ b/templates/registration/password_reset_done.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% block entete %}Réinitilisation du mot de passe{% endblock %} +{% block navbar %} + +{% endblock %} +{% block content %} +
+
+

Réinitialisation du mot de passe

+

Un mail vous a été envoyé avec un lien pour réinitialiser le mot de passe.

+
+
+{{form.media}} +{% endblock %} diff --git a/templates/registration/password_reset_email.html b/templates/registration/password_reset_email.html new file mode 100644 index 0000000..28e59a8 --- /dev/null +++ b/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 %} \ No newline at end of file diff --git a/templates/registration/password_reset_form.html b/templates/registration/password_reset_form.html new file mode 100644 index 0000000..934fdcd --- /dev/null +++ b/templates/registration/password_reset_form.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% block entete %}Réinitilisation du mot de passe{% endblock %} +{% block navbar %} + +{% endblock %} +{% block content %} +
+
+

Réinitialisation du mot de passe

+

Vous recevrez un lien pour réinitilisaser votre mot de passe sur votre adresse e-mail.

+
+
+
+ {% csrf_token %} + {{ form }} +
+ +
+
+
+{{form.media}} +{% endblock %} diff --git a/templates/registration/password_reset_subject.txt b/templates/registration/password_reset_subject.txt new file mode 100644 index 0000000..2ad6191 --- /dev/null +++ b/templates/registration/password_reset_subject.txt @@ -0,0 +1 @@ +Réinitialisation du mot de passe Coopé TM \ No newline at end of file diff --git a/users/forms.py b/users/forms.py index ae4481a..ddf26ef 100644 --- a/users/forms.py +++ b/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`). diff --git a/users/models.py b/users/models.py index 080b812..52231e2 100644 --- a/users/models.py +++ b/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. diff --git a/users/templates/users/login.html b/users/templates/users/login.html new file mode 100644 index 0000000..1c3f803 --- /dev/null +++ b/users/templates/users/login.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} +{% block entete %}{{form_title}}{% endblock %} +{% block navbar %} + +{% endblock %} +{% block content %} +
+
+

{{form_title}}

+

{{form_p}}

+
+
+
+ {% csrf_token %} + {{ form }} +
+ {{ extra_html | safe }}

+ +
+
+ Si vous avez perdu votre mot de passe : mot de passe oublié. +
+{% if extra_css %} + +{% endif %} +{{form.media}} +{% endblock %} diff --git a/users/templates/users/profile.html b/users/templates/users/profile.html index 0b90ecf..c7b3de7 100644 --- a/users/templates/users/profile.html +++ b/users/templates/users/profile.html @@ -58,9 +58,6 @@ {% if self %} Changer mon mot de passe {% endif %} - {% if perms.users.can_reset_password %} - Réinitialiser le mot de passe - {% endif %} {% if perms.users.can_change_user_perm %} Changer les groupes {% endif %} diff --git a/users/templates/users/welcome_email.html b/users/templates/users/welcome_email.html new file mode 100644 index 0000000..8c7c420 --- /dev/null +++ b/users/templates/users/welcome_email.html @@ -0,0 +1,15 @@ +{% autoescape off %} +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 {{protocol}}://{{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 +{% endautoescape %} \ No newline at end of file diff --git a/users/templates/users/welcome_email.txt b/users/templates/users/welcome_email.txt new file mode 100644 index 0000000..c2ffdb1 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/users/urls.py b/users/urls.py index 51532e8..413178d 100644 --- a/users/urls.py +++ b/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/', views.editGroups, name="editGroups"), path('editPassword/', views.editPassword, name="editPassword"), path('editUser/', views.editUser, name="editUser"), - path('resetPassword/', views.resetPassword, name="resetPassword"), path('groupsIndex', views.groupsIndex, name="groupsIndex"), path('groupProfile/', views.groupProfile, name="groupProfile"), path('createGroup', views.createGroup, name="createGroup"), diff --git a/users/views.py b/users/views.py index c129b7b..8d437a5 100644 --- a/users/views.py +++ b/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 ", + [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': 'En cliquant sur le bouton "Créer mon compte", vous :
  • 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
  • joignez l\'association de votre plein gré
  • vous engagez à respecter les statuts et le réglement intérieur de l\'association (envoyés par mail)
  • 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
  • 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
'}) @@ -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 `. """ - 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")