From e37c3d1c688da7f733ff2677f2b4fa7e926a2048 Mon Sep 17 00:00:00 2001 From: nanoy Date: Sun, 6 Jan 2019 16:01:22 +0100 Subject: [PATCH 01/15] =?UTF-8?q?Pr=C3=A9paration=20de=20la=20version=203.?= =?UTF-8?q?3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/footer.html b/templates/footer.html index 11893b6..608943c 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -39,6 +39,6 @@
  • Facebook
  • - + From bc4532f1a7fdf9450a0ca0217a6e40cc031f4753 Mon Sep 17 00:00:00 2001 From: Nanoy Date: Sun, 13 Jan 2019 19:42:44 +0100 Subject: [PATCH 02/15] Prise en charge de pipenv --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0964b9a..b6332fc 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ tags .vscode venv static/ +Pipfile From 26f638251d65530348e4aea33ca4fc262dc7c828 Mon Sep 17 00:00:00 2001 From: Nanoy Date: Mon, 14 Jan 2019 02:43:47 +0100 Subject: [PATCH 03/15] Ajout de liens pour les profils users et produits --- gestion/templates/gestion/menus_list.html | 2 +- gestion/templates/gestion/pintes_user_list.html | 2 +- gestion/templates/gestion/products_list.html | 2 +- gestion/templates/gestion/ranking.html | 4 ++-- users/templates/users/admins_index.html | 2 +- users/templates/users/allReloads.html | 2 +- users/templates/users/all_consumptions.html | 4 ++-- users/templates/users/all_menus.html | 2 +- users/templates/users/group_profile.html | 2 +- users/templates/users/profile.html | 2 +- users/templates/users/superusers_index.html | 2 +- users/templates/users/users_index.html | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/gestion/templates/gestion/menus_list.html b/gestion/templates/gestion/menus_list.html index 230e719..3cecb99 100644 --- a/gestion/templates/gestion/menus_list.html +++ b/gestion/templates/gestion/menus_list.html @@ -29,7 +29,7 @@ {{ menu.name }} {{ menu.amount}} € {{ menu.barcode }} - {% for art in menu.articles.all %}{{art}},{% endfor %} + {% 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 %} diff --git a/gestion/templates/gestion/pintes_user_list.html b/gestion/templates/gestion/pintes_user_list.html index d7de296..a827972 100644 --- a/gestion/templates/gestion/pintes_user_list.html +++ b/gestion/templates/gestion/pintes_user_list.html @@ -22,7 +22,7 @@ {% for user in users %} - {{ user }} + {{user}} Profil {{ user.pintes_owned_currently.count }} diff --git a/gestion/templates/gestion/products_list.html b/gestion/templates/gestion/products_list.html index eda88c4..ce840fd 100644 --- a/gestion/templates/gestion/products_list.html +++ b/gestion/templates/gestion/products_list.html @@ -32,7 +32,7 @@ {% for product in products %} - {{ product.name }} + {{ product.name }} {{ product.amount}} {{ product.stockHold }} {{ product.stockBar }} diff --git a/gestion/templates/gestion/ranking.html b/gestion/templates/gestion/ranking.html index 95c5a0d..204fc50 100644 --- a/gestion/templates/gestion/ranking.html +++ b/gestion/templates/gestion/ranking.html @@ -28,7 +28,7 @@ {%for customer in bestBuyers%} {{ forloop.counter }} - {{ customer.username }} + {{ customer.username }} {{ customer.profile.debit }}€ {%endfor%} @@ -59,7 +59,7 @@ {% for customer in bestDrinkers %} {{ forloop.counter }} - {{ customer.0.username }} + {{ customer.0.username }} {{ customer.1 }} {%endfor%} diff --git a/users/templates/users/admins_index.html b/users/templates/users/admins_index.html index 8dacf18..8c1e04c 100644 --- a/users/templates/users/admins_index.html +++ b/users/templates/users/admins_index.html @@ -23,7 +23,7 @@ {% for user in admins %} - {{ user }} {% if user.is_superuser %}(superuser){% endif %} + {{ user }}{ {% if user.is_superuser %}(superuser){% endif %} Profil {% if not user.is_superuser %}Retirer des admins{% endif %} diff --git a/users/templates/users/allReloads.html b/users/templates/users/allReloads.html index e06fe90..6731132 100644 --- a/users/templates/users/allReloads.html +++ b/users/templates/users/allReloads.html @@ -4,7 +4,7 @@ {% block navbar %} {% endblock %} {% block content %} diff --git a/users/templates/users/all_consumptions.html b/users/templates/users/all_consumptions.html index 02729bd..985963c 100644 --- a/users/templates/users/all_consumptions.html +++ b/users/templates/users/all_consumptions.html @@ -10,7 +10,7 @@ {% block content %}
    -

    Consommations ({{user}})

    +

    Consommations ({{user}})

    @@ -28,7 +28,7 @@ {% for c in consumptions %} - {{c.product}} + {% if perms.gestion.view_product %}{{ c.product.name }}{% else %}{{c.product.name}}{% endif %} {{c.quantity}} {{c.amount}} {{c.paymentMethod}} diff --git a/users/templates/users/all_menus.html b/users/templates/users/all_menus.html index 0e3921d..b412d90 100644 --- a/users/templates/users/all_menus.html +++ b/users/templates/users/all_menus.html @@ -10,7 +10,7 @@ {% block content %}
    -

    Consommations de menus ({{user}})

    +

    Consommations de menus ({{user}})

    diff --git a/users/templates/users/group_profile.html b/users/templates/users/group_profile.html index 3d5ebf0..17f89cc 100644 --- a/users/templates/users/group_profile.html +++ b/users/templates/users/group_profile.html @@ -72,7 +72,7 @@ {% for user in group.user_set.all %} - {{ user }} + {{user}} Profil Retirer diff --git a/users/templates/users/profile.html b/users/templates/users/profile.html index caea4fd..778dac2 100644 --- a/users/templates/users/profile.html +++ b/users/templates/users/profile.html @@ -124,7 +124,7 @@ {% for c in lastConsumptions %} - {{c.product}} + {% if perms.gestion.view_product %}{{ c.product.name }}{% else %}{{c.product}}{% endif %} {{c.quantity}} {{c.amount}} € {{c.paymentMethod}} diff --git a/users/templates/users/superusers_index.html b/users/templates/users/superusers_index.html index 157aebf..9c4a5ff 100644 --- a/users/templates/users/superusers_index.html +++ b/users/templates/users/superusers_index.html @@ -23,7 +23,7 @@ {% for user in superusers %} - {{ user }} + {{user}} Profil Retirer des superusers diff --git a/users/templates/users/users_index.html b/users/templates/users/users_index.html index 7cc5545..7bc806d 100644 --- a/users/templates/users/users_index.html +++ b/users/templates/users/users_index.html @@ -25,7 +25,7 @@ {% for user in users %} - {{ user }} + {{user}} Profil {% if perms.auth.change_user %} {{ user.is_active | yesno:"Désa,A"}}ctiver From be65967e3c33772d39e841dd0f82979632796750 Mon Sep 17 00:00:00 2001 From: Nanoy Date: Thu, 17 Jan 2019 23:16:43 +0100 Subject: [PATCH 04/15] Ajour d'icones --- gestion/templates/gestion/kegs_list.html | 16 +++++------ gestion/templates/gestion/manage.html | 6 ++-- gestion/templates/gestion/menus_list.html | 4 +-- gestion/templates/gestion/pintes_list.html | 10 +++---- .../templates/gestion/pintes_user_list.html | 2 +- gestion/templates/gestion/products_list.html | 4 +-- gestion/views.py | 26 ++++++++--------- .../preferences/cotisations_index.html | 4 +-- .../preferences/general_preferences.html | 10 +++---- .../preferences/payment_methods_index.html | 4 +-- preferences/views.py | 8 +++--- templates/form.html | 2 +- templates/nav.html | 2 +- users/templates/users/admins_index.html | 6 ++-- users/templates/users/group_profile.html | 12 ++++---- users/templates/users/groups_index.html | 4 +-- users/templates/users/index.html | 2 +- users/templates/users/profile.html | 22 +++++++-------- users/templates/users/schools_index.html | 4 +-- users/templates/users/superusers_index.html | 8 +++--- users/templates/users/users_index.html | 8 +++--- users/views.py | 28 +++++++++---------- 22 files changed, 96 insertions(+), 96 deletions(-) diff --git a/gestion/templates/gestion/kegs_list.html b/gestion/templates/gestion/kegs_list.html index 2589099..764d7e8 100644 --- a/gestion/templates/gestion/kegs_list.html +++ b/gestion/templates/gestion/kegs_list.html @@ -13,13 +13,13 @@

    Liste des fûts actifs

    {% if perms.gestion.add_keg %} - Créer un fût + Créer un fût {% endif %} {% if perms.gestion.open_keg %} - Percuter un fût + Percuter un fût {% endif %} {% if perms.gestion.close_keg %} - Fermer un fût + Fermer un fût {% endif %}

    @@ -48,7 +48,7 @@ {{ kegH.amountSold }} € {{ kegH.keg.amount }} € Voir - {% if perms.gestion.close_keg %}Fermer {% endif %}{% if perms.gestion.change_keg %}Modifier{% endif %} + {% if perms.gestion.close_keg %} Fermer {% endif %}{% if perms.gestion.change_keg %} Modifier{% endif %} {% endfor %} @@ -60,13 +60,13 @@

    Liste des fûts inactifs

    {% if perms.gestion.add_keg %} - Créer un fût + Créer un fût {% endif %} {% if perms.gestion.open_keg %} - Percuter un fût + Percuter un fût {% endif %} {% if perms.gestion.close_keg %} - Fermer un fût + Fermer un fût {% endif %}

    @@ -91,7 +91,7 @@ {{ keg.capacity }} L {{ keg.amount }} € Voir - {% if perms.gestion.open_keg %}{% if keg.stockHold > 0 %}Percuter {% endif %}{% endif %}{% if perms.gestion.change_keg %}Modifier{% endif %} + {% if perms.gestion.open_keg %}{% if keg.stockHold > 0 %} Percuter {% endif %}{% endif %}{% if perms.gestion.change_keg %} Modifier{% endif %} {% endfor %} diff --git a/gestion/templates/gestion/manage.html b/gestion/templates/gestion/manage.html index 1f9c6ee..0292bf4 100644 --- a/gestion/templates/gestion/manage.html +++ b/gestion/templates/gestion/manage.html @@ -61,7 +61,7 @@
    - Annuler

    + Annuler

    {{gestion_form}}
    @@ -214,7 +214,7 @@ {% csrf_token %} {{reload_form}}
    - +
    {% endif %} @@ -227,7 +227,7 @@ {% csrf_token %} {{refund_form}}
    - +
    {% endif %} diff --git a/gestion/templates/gestion/menus_list.html b/gestion/templates/gestion/menus_list.html index 3cecb99..22ef402 100644 --- a/gestion/templates/gestion/menus_list.html +++ b/gestion/templates/gestion/menus_list.html @@ -10,7 +10,7 @@

    Liste des menus

    - Créer un menu

    + Créer un menu

    @@ -31,7 +31,7 @@ - + {% endfor %} diff --git a/gestion/templates/gestion/pintes_list.html b/gestion/templates/gestion/pintes_list.html index 88c0b95..937e13d 100644 --- a/gestion/templates/gestion/pintes_list.html +++ b/gestion/templates/gestion/pintes_list.html @@ -14,7 +14,7 @@

    Général

    {% if perms.gestion.add_pinte %} - Créer une ou plusieurs pintes

    + Créer une ou plusieurs pintes

    {% endif %} Il a y actuellement {{ taken_pintes.count|add:free_pintes.count }} pintes, parmis lesquelles {{ free_pintes.count }} sont rendues et {{ taken_pintes.count }} ne sont pas rendues. @@ -37,10 +37,10 @@ {% for pinte in taken_pintes %} - - + + - + {% endfor %} @@ -64,7 +64,7 @@ {% for pinte in free_pintes %} - + {% endfor %} diff --git a/gestion/templates/gestion/pintes_user_list.html b/gestion/templates/gestion/pintes_user_list.html index a827972..eb5a2ae 100644 --- a/gestion/templates/gestion/pintes_user_list.html +++ b/gestion/templates/gestion/pintes_user_list.html @@ -23,7 +23,7 @@ {% for user in users %} - + {% endfor %} diff --git a/gestion/templates/gestion/products_list.html b/gestion/templates/gestion/products_list.html index ce840fd..f986d20 100644 --- a/gestion/templates/gestion/products_list.html +++ b/gestion/templates/gestion/products_list.html @@ -11,7 +11,7 @@

    Liste des produits

    {% if perms.gestion.add_product %} - Créer un produit

    + Créer un produit

    {% endif %}
    {{ menu.barcode }} {% 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 %}{% if perms.gestion.change_menu %}{% if menu.is_active %} Désa{% else %} A{% endif %}ctiver Modifier{% endif %}
    {{ pinte.pk }}{{ pinte.current_owner }}{{ pinte.previous_owner }}{% if pinte.current_owner %}{{ pinte.current_owner }}{% endif %}{% if pinte.previous_owner %}{{ pinte.previous_owner }}{% endif %} {{ pinte.last_update_date }}{% if perms.gestion.change_pinte %} Libérer{% endif %}{% if perms.gestion.change_pinte %} Libérer{% endif %}
    {{ pinte.pk }}{{ pinte.previous_owner }}{% if pinte.previous_owner %}{{ pinte.previous_owner }}{% endif %} {{ pinte.last_update_date }}
    {{user}}Profil Profil {{ user.pintes_owned_currently.count }}
    @@ -41,7 +41,7 @@ - + {% endfor %} diff --git a/gestion/views.py b/gestion/views.py index 525b202..af72595 100644 --- a/gestion/views.py +++ b/gestion/views.py @@ -341,7 +341,7 @@ def addProduct(request): product = form.save() messages.success(request, "Le produit a bien été ajouté") return redirect(reverse('gestion:productProfile', kwargs={'pk':product.pk})) - return render(request, "form.html", {"form": form, "form_title": "Ajout d'un produit", "form_button": "Ajouter"}) + return render(request, "form.html", {"form": form, "form_title": "Ajout d'un produit", "form_button": "Ajouter", "form_button_icon": "plus-square"}) @active_required @login_required @@ -374,7 +374,7 @@ def editProduct(request, pk): form.save() messages.success(request, "Le produit a bien été modifié") return redirect(reverse('gestion:productProfile', kwargs={'pk':product.pk})) - return render(request, "form.html", {"form": form, "form_title": "Modification d'un produit", "form_button": "Modifier"}) + return render(request, "form.html", {"form": form, "form_title": "Modification d'un produit", "form_button": "Modifier", "form_button_icon": "pencil-alt"}) @active_required @login_required @@ -420,7 +420,7 @@ def searchProduct(request): form = SearchProductForm(request.POST or None) if(form.is_valid()): return redirect(reverse('gestion:productProfile', kwargs={'pk': form.cleaned_data['product'].pk })) - return render(request, "form.html", {"form": form, "form_title":"Rechercher un produit", "form_button": "Rechercher"}) + return render(request, "form.html", {"form": form, "form_title":"Rechercher un produit", "form_button": "Rechercher", "form_button_icon": "search"}) @active_required @login_required @@ -526,7 +526,7 @@ def addKeg(request): keg = form.save() messages.success(request, "Le fût " + keg.name + " a bien été ajouté") return redirect(reverse('gestion:kegsList')) - return render(request, "form.html", {"form":form, "form_title": "Ajout d'un fût", "form_button": "Ajouter"}) + return render(request, "form.html", {"form":form, "form_title": "Ajout d'un fût", "form_button": "Ajouter", "form_button_icon": "plus-square"}) @active_required @login_required @@ -559,7 +559,7 @@ def editKeg(request, pk): form.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"}) + return render(request, "form.html", {"form": form, "form_title": "Modification d'un fût", "form_button": "Modifier", "form_button_icon": "pencil-alt"}) @active_required @login_required @@ -598,7 +598,7 @@ def openKeg(request): keg.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"}) + return render(request, "form.html", {"form": form, "form_title":"Percutage d'un fût", "form_button":"Percuter", "form_button_icon": "fill-drip"}) @active_required @login_required @@ -660,7 +660,7 @@ def closeKeg(request): keg.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"}) + return render(request, "form.html", {"form": form, "form_title":"Fermeture d'un fût", "form_button":"Fermer le fût", "form_button_icon": "fill"}) @active_required @login_required @@ -785,7 +785,7 @@ def addMenu(request): menu = form.save() messages.success(request, "Le menu " + menu.name + " a bien été ajouté") return redirect(reverse('gestion:menusList')) - return render(request, "form.html", {"form":form, "form_title": "Ajout d'un menu", "form_button": "Ajouter", "extra_css": extra_css}) + return render(request, "form.html", {"form":form, "form_title": "Ajout d'un menu", "form_button": "Ajouter", "form_button_icon": "plus-square", "extra_css": extra_css}) @active_required @login_required @@ -819,7 +819,7 @@ def edit_menu(request, pk): form.save() messages.success(request, "Le menu a bien été modifié") return redirect(reverse('gestion:menusList')) - return render(request, "form.html", {"form": form, "form_title": "Modification d'un menu", "form_button": "Modifier", "extra_css": extra_css}) + return render(request, "form.html", {"form": form, "form_title": "Modification d'un menu", "form_button": "Modifier", "form_button_icon": "pencil-alt", "extra_css": extra_css}) @active_required @login_required @@ -847,7 +847,7 @@ def searchMenu(request): if(form.is_valid()): menu = form.cleaned_data['menu'] return redirect(reverse('gestion:editMenu', kwargs={'pk':menu.pk})) - return render(request, "form.html", {"form": form, "form_title": "Recherche d'un menu", "form_button": "Modifier"}) + return render(request, "form.html", {"form": form, "form_title": "Recherche d'un menu", "form_button": "Modifier", "form_button_icon": "search"}) @active_required @login_required @@ -989,7 +989,7 @@ def add_pintes(request): i += 1 messages.success(request, str(i) + " pinte(s) a(ont) été ajoutée(s)") return redirect(reverse('gestion:productsIndex')) - return render(request, "form.html", {"form": form, "form_title": "Ajouter des pintes", "form_button": "Ajouter"}) + return render(request, "form.html", {"form": form, "form_title": "Ajouter des pintes", "form_button": "Ajouter", "form_button_icon": "plus-square"}) @active_required @login_required @@ -1008,7 +1008,7 @@ def release_pintes(request): i += 1 messages.success(request, str(i) + " pinte(s) a(ont) été libérée(s)") return redirect(reverse('gestion:productsIndex')) - return render(request, "form.html", {"form": form, "form_title": "Libérer des pintes", "form_button": "Libérer"}) + return render(request, "form.html", {"form": form, "form_title": "Libérer des pintes", "form_button": "Libérer", "form_button_icon": "glass-whiskey"}) @active_required @login_required @@ -1072,4 +1072,4 @@ def gen_releve(request): now = datetime.datetime.now() return render_to_pdf(request, 'gestion/releve.tex', {"consumptions": consumptions, "reloads": reloads, "refunds": refunds, "cotisations": cotisations, "begin": begin, "end": end, "now": now, "value_especes": value_especes, "value_lydia": value_lydia, "value_cheque": value_cheque}, filename="releve.pdf") else: - return render(request, "form.html", {"form": form, "form_title": "Génération d'un relevé", "form_button": "Générer"}) + return render(request, "form.html", {"form": form, "form_title": "Génération d'un relevé", "form_button": "Générer", "form_button_icon": "file-pdf"}) diff --git a/preferences/templates/preferences/cotisations_index.html b/preferences/templates/preferences/cotisations_index.html index eacb342..8ede5a4 100644 --- a/preferences/templates/preferences/cotisations_index.html +++ b/preferences/templates/preferences/cotisations_index.html @@ -11,7 +11,7 @@

    Liste des cotisations

    {% if perms.preferences.add_cotisation %} - Créer une cotisation

    + Créer une cotisation

    {% endif %}
    {{ product.is_active | yesno:"Oui, Non"}} {{ product.deg }} {{ product.volume }} clProfil {% if perms.gestion.change_product %}{% if product.is_active %}Désa{% else %}A{% endif %}ctiver Modifier{% endif %} Profil {% if perms.gestion.change_product %}{% if product.is_active %} Désa{% else %} A{% endif %}ctiver Modifier{% endif %}
    @@ -27,7 +27,7 @@ - + {% endfor %} diff --git a/preferences/templates/preferences/general_preferences.html b/preferences/templates/preferences/general_preferences.html index f8ee3ba..988dea2 100644 --- a/preferences/templates/preferences/general_preferences.html +++ b/preferences/templates/preferences/general_preferences.html @@ -26,7 +26,7 @@
    - +
    @@ -51,7 +51,7 @@
    - +
    @@ -89,7 +89,7 @@
    - +
    @@ -115,7 +115,7 @@
    - +
    @@ -135,7 +135,7 @@
    - +
    diff --git a/preferences/templates/preferences/payment_methods_index.html b/preferences/templates/preferences/payment_methods_index.html index b79ea22..31e6454 100644 --- a/preferences/templates/preferences/payment_methods_index.html +++ b/preferences/templates/preferences/payment_methods_index.html @@ -11,7 +11,7 @@

    Liste des moyens de paiement

    {% if perms.preferences.add_paymentmethod %} - Créer un moyen de paiement

    + Créer un moyen de paiement

    {% endif %}
    {{ cotisation.duration }} jours {{ cotisation.amount }} €{% if perms.preferences.change_cotisation %}Modifier {% endif %}{% if perms.preferences.delete_cotisation %}Supprimer{% endif %}{% if perms.preferences.change_cotisation %} Modifier {% endif %}{% if perms.preferences.delete_cotisation %} Supprimer{% endif %}
    @@ -35,7 +35,7 @@ - + {% endfor %} diff --git a/preferences/views.py b/preferences/views.py index b9bba9c..7c4f4a4 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -84,7 +84,7 @@ def addCotisation(request): cotisation = form.save() messages.success(request, "La cotisation (" + str(cotisation.duration) + " jours, " + str(cotisation.amount) + "€) a bien été créée") return redirect(reverse('preferences:cotisationsIndex')) - return render(request, "form.html", {"form": form, "form_title": "Création d'une cotisation", "form_button": "Créer"}) + return render(request, "form.html", {"form": form, "form_title": "Création d'une cotisation", "form_button": "Créer", "form_button_icon": "plus-square"}) @active_required @login_required @@ -117,7 +117,7 @@ def editCotisation(request, pk): cotisation = form.save() messages.success(request, "La cotisation (" + str(cotisation.duration) + " jours, " + str(cotisation.amount) + "€) a bien été modifiée") return redirect(reverse('preferences:cotisationsIndex')) - return render(request, "form.html", {"form": form, "form_title": "Modification d'une cotisation", "form_button": "Modifier"}) + return render(request, "form.html", {"form": form, "form_title": "Modification d'une cotisation", "form_button": "Modifier", "form_button_icon": "pencil-alt"}) @active_required @login_required @@ -184,7 +184,7 @@ def addPaymentMethod(request): paymentMethod = form.save() messages.success(request, "Le moyen de paiement " + paymentMethod.name + " a bien été crée") return redirect(reverse('preferences:paymentMethodsIndex')) - return render(request, "form.html", {"form": form, "form_title": "Création d'un moyen de paiement", "form_button": "Créer"}) + return render(request, "form.html", {"form": form, "form_title": "Création d'un moyen de paiement", "form_button": "Créer", "form_button_icon": "plus-square"}) @active_required @login_required @@ -217,7 +217,7 @@ def editPaymentMethod(request, pk): paymentMethod = form.save() messages.success(request, "Le moyen de paiment " + paymentMethod.name + " a bien été modifié") return redirect(reverse('preferences:paymentMethodsIndex')) - return render(request, "form.html", {"form": form, "form_title": "Modification d'un moyen de paiement", "form_button": "Modifier"}) + return render(request, "form.html", {"form": form, "form_title": "Modification d'un moyen de paiement", "form_button": "Modifier", "form_button_icon": "pencil-alt"}) @active_required @login_required diff --git a/templates/form.html b/templates/form.html index 592927a..be760ba 100644 --- a/templates/form.html +++ b/templates/form.html @@ -16,7 +16,7 @@ {% csrf_token %} {{ form }}
    - + diff --git a/templates/nav.html b/templates/nav.html index 188251a..d19985f 100644 --- a/templates/nav.html +++ b/templates/nav.html @@ -45,5 +45,5 @@ Deconnexion {% else %} -Connexion +Connexion {% endif %} diff --git a/users/templates/users/admins_index.html b/users/templates/users/admins_index.html index 8c1e04c..f021b2b 100644 --- a/users/templates/users/admins_index.html +++ b/users/templates/users/admins_index.html @@ -10,7 +10,7 @@

    Liste des admins

    - Ajouter un admin

    + Ajouter un admin

    {{ 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 %}{% if perms.preferences.change_paymentmethod %} Modifier {% endif %}{% if perms.preferences.delete_paymentmethod %} Supprimer{% endif %}
    @@ -24,8 +24,8 @@ {% for user in admins %} - - + + {% endfor %} diff --git a/users/templates/users/group_profile.html b/users/templates/users/group_profile.html index 17f89cc..056f38f 100644 --- a/users/templates/users/group_profile.html +++ b/users/templates/users/group_profile.html @@ -21,12 +21,12 @@
    {% if perms.auth.change_group %} {% endif %} {% if perms.auth.delete_group %} {% endif %}
    @@ -49,7 +49,7 @@ - + {% endfor %} @@ -72,9 +72,9 @@ {% for user in group.user_set.all %} - - - + + + {% endfor %} diff --git a/users/templates/users/groups_index.html b/users/templates/users/groups_index.html index 2bc9ddb..a43f9df 100644 --- a/users/templates/users/groups_index.html +++ b/users/templates/users/groups_index.html @@ -11,7 +11,7 @@

    Liste des groupes de droit

    {% if perms.auth.add_group %} - Ajouter un groupe de droit

    + Ajouter un groupe de droit

    {% endif %}
    {{ user }}{ {% if user.is_superuser %}(superuser){% endif %}Profil{% if not user.is_superuser %}Retirer des admins{% endif %} Profil{% if not user.is_superuser %} Retirer des admins{% endif %}
    {{perm.codename}} {{perm.name}}Enlever le droit Enlever le droit
    {{user}}ProfilRetirer{{user}} Profil Retirer
    @@ -29,7 +29,7 @@ - + {% endfor %} diff --git a/users/templates/users/index.html b/users/templates/users/index.html index 451c552..3c53a5a 100644 --- a/users/templates/users/index.html +++ b/users/templates/users/index.html @@ -107,7 +107,7 @@ {% csrf_token %} {{export_form}}
    - + {% endif %} diff --git a/users/templates/users/profile.html b/users/templates/users/profile.html index 778dac2..0eae992 100644 --- a/users/templates/users/profile.html +++ b/users/templates/users/profile.html @@ -52,19 +52,19 @@ @@ -129,7 +129,7 @@ - + {%endfor%} @@ -163,7 +163,7 @@ - + {%endfor%} @@ -196,7 +196,7 @@ {% if perms.gestion.delete_reload %} - + {% endif %} {% endfor %} @@ -211,7 +211,7 @@

    {{ self | yesno:"Mes cotisations,Cotisations"}}

    - Ajouter une cotisation

    + Ajouter une cotisation

    {{ group.name }} {{ group.permissions.count }} {{ group.user_set.count }}Voir {% if perms.auth.change_group %}Éditer {% endif %}{% if perms.auth.delete_group %}Supprimer{% endif %} Voir {% if perms.auth.change_group %} Éditer {% endif %}{% if perms.auth.delete_group %} Supprimer{% endif %}
    {{c.amount}} € {{c.paymentMethod}} {{c.date}}{% if perms.gestion.delete_consumptionhistory %}Annuler{% endif %}{% if perms.gestion.delete_consumptionhistory %} Annuler{% endif %}
    {{m.amount}} € {{m.paymentMethod}} {{m.date}}{% if perms.gestion.delete_menuhistory %}Annuler{% endif %}{% if perms.gestion.delete_menuhistory %} Annuler{% endif %}
    {{reload.PaymentMethod}} {{reload.date}}Annuler Annuler
    @@ -234,7 +234,7 @@ - + {% endfor %} @@ -248,7 +248,7 @@

    {{ self | yesno:"Mes accès gracieux,Accès gracieux"}}

    - Ajouter un accès à titre gracieux

    + Ajouter un accès à titre gracieux

    {{cotisation.paymentMethod}} {{cotisation.endDate}} {{cotisation.valid}}{% if perms.users.validate_cotisationHistory %}Valider Invalider{% endif %}{% if perms.users.validate_cotisationHistory %} Valider Invalider{% endif %}
    diff --git a/users/templates/users/schools_index.html b/users/templates/users/schools_index.html index 47c7065..913e543 100644 --- a/users/templates/users/schools_index.html +++ b/users/templates/users/schools_index.html @@ -10,7 +10,7 @@

    Liste des écoles

    - Créer une école

    + Créer une école

    @@ -23,7 +23,7 @@ {% for school in schools %} - + {% endfor %} diff --git a/users/templates/users/superusers_index.html b/users/templates/users/superusers_index.html index 9c4a5ff..02fa984 100644 --- a/users/templates/users/superusers_index.html +++ b/users/templates/users/superusers_index.html @@ -10,7 +10,7 @@

    Liste des superusers

    - Ajouter un superuser

    + Ajouter un superuser

    {{ school }}{% if perms.gestion.change_school %}Modifier {% endif %}{% if perms.gestion.delete_school %}Supprimer{% endif %}{% if perms.gestion.change_school %} Modifier {% endif %}{% if perms.gestion.delete_school %} Supprimer{% endif %}
    @@ -23,9 +23,9 @@ {% for user in superusers %} - - - + + + {% endfor %} diff --git a/users/templates/users/users_index.html b/users/templates/users/users_index.html index 7bc806d..c0c73b0 100644 --- a/users/templates/users/users_index.html +++ b/users/templates/users/users_index.html @@ -10,7 +10,7 @@

    Liste des utilisateurs

    - Créer un utilisateur

    + Créer un utilisateur

    {{user}}ProfilRetirer des superusers{{user}} Profil Retirer des superusers
    @@ -25,10 +25,10 @@ {% for user in users %} - - + + {% if perms.auth.change_user %} - + {% endif %} {% endfor %} diff --git a/users/views.py b/users/views.py index ca37f2f..0836054 100644 --- a/users/views.py +++ b/users/views.py @@ -52,7 +52,7 @@ def loginView(request): return redirect(reverse('users:profile', kwargs={'pk':request.user.pk})) 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"}) + return render(request, "form.html", {"form_entete": "Connexion", "form": form, "form_title": "Connexion", "form_button": "Se connecter", "form_button_icon": "sign-in-alt"}) @active_required @login_required @@ -225,7 +225,7 @@ def createUser(request): user.save() 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 l'utilisateur"}) + return render(request, "form.html", {"form_entete": "Gestion des utilisateurs", "form":form, "form_title":"Création d'un nouvel utilisateur", "form_button":"Créer l'utilisateur", "form_button_icon": "user-plus"}) @active_required @login_required @@ -252,7 +252,7 @@ def searchUser(request): form = SelectUserForm(request.POST or None) if(form.is_valid()): return redirect(reverse('users:profile', kwargs={"pk":form.cleaned_data['user'].pk})) - return render(request, "form.html", {"form_entete": "Gestion des utilisateurs", "form": form, "form_title": "Rechercher un utilisateur", "form_button": "Afficher le profil"}) + return render(request, "form.html", {"form_entete": "Gestion des utilisateurs", "form": form, "form_title": "Rechercher un utilisateur", "form_button": "Afficher le profil", "form_button_icon": "search"}) @active_required @login_required @@ -305,7 +305,7 @@ def editGroups(request, pk): messages.success(request, "Les groupes de l'utilisateur " + user.username + " ont bien été enregistrés.") return redirect(reverse('users:profile', kwargs={'pk':pk})) extra_css = "#id_groups{height:200px;}" - return render(request, "form.html", {"form_entete": "Gestion de l'utilisateur " + user.username, "form": form, "form_title": "Modification des groupes", "form_button": "Enregistrer", "extra_css": extra_css}) + return render(request, "form.html", {"form_entete": "Gestion de l'utilisateur " + user.username, "form": form, "form_title": "Modification des groupes", "form_button": "Enregistrer", "form_button_icon": "pencil-alt", "extra_css": extra_css}) @active_required @login_required @@ -345,7 +345,7 @@ def editPassword(request, pk): return redirect(reverse('users:profile', kwargs={'pk':pk})) else: messages.error(request, "Le mot de passe actuel est incorrect") - return render(request, "form.html", {"form_entete": "Modification de mon compte", "form": form, "form_title": "Modification de mon mot de passe", "form_button": "Modifier mon mot de passe"}) + return render(request, "form.html", {"form_entete": "Modification de mon compte", "form": form, "form_title": "Modification de mon mot de passe", "form_button": "Modifier mon mot de passe", "form_button_icon": "pencil-alt"}) @active_required @login_required @@ -379,7 +379,7 @@ def editUser(request, pk): user.save() messages.success(request, "Les modifications ont bien été enregistrées") 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"}) + 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 @@ -583,7 +583,7 @@ def createGroup(request): group = form.save() messages.success(request, "Le groupe " + form.cleaned_data['name'] + " a bien été crée.") return redirect(reverse('users:groupProfile', kwargs={'pk': group.pk})) - return render(request, "form.html", {"form_entete": "Gestion des utilisateurs", "form":form, "form_title": "Création d'un groupe de droit", "form_button": "Créer le groupe de droit"}) + return render(request, "form.html", {"form_entete": "Gestion des utilisateurs", "form":form, "form_title": "Création d'un groupe de droit", "form_button": "Créer le groupe de droit", "form_button_icon": "plus-square"}) @active_required @login_required @@ -617,7 +617,7 @@ def editGroup(request, pk): form.save() messages.success(request, "Le groupe " + group.name + " a bien été modifié.") return redirect(reverse('users:groupProfile', kwargs={'pk': group.pk})) - return render(request, "form.html", {"form_entete": "Gestion des utilisateurs", "form": form, "form_title": "Modification du groupe de droit " + group.name, "form_button": "Modifier le groupe de droit", "extra_css":extra_css}) + return render(request, "form.html", {"form_entete": "Gestion des utilisateurs", "form": form, "form_title": "Modification du groupe de droit " + group.name, "form_button": "Modifier le groupe de droit", "form_button_icon": "pencil-alt", "extra_css":extra_css}) @active_required @login_required @@ -736,7 +736,7 @@ def addAdmin(request): user.save() messages.success(request, "L'utilisateur " + user.username + " a bien été rajouté aux admins") return redirect(reverse('users:adminsIndex')) - return render(request, "form.html", {"form": form, "form_title": "Ajout d'un admin", "form_button":"Ajouter l'utilisateur aux admins"}) + return render(request, "form.html", {"form": form, "form_title": "Ajout d'un admin", "form_button": "Ajouter l'utilisateur aux admins", "form_button_icon": "user-plus"}) @active_required @login_required @@ -814,7 +814,7 @@ def addSuperuser(request): user.save() messages.success(request, "L'utilisateur " + user.username + " a bien été rajouté aux superusers") return redirect(reverse('users:superusersIndex')) - return render(request, "form.html", {"form_entete": "Gestion des superusers", "form": form, "form_title": "Ajout d'un superuser", "form_button":"Ajouter l'utilisateur aux superusers"}) + return render(request, "form.html", {"form_entete": "Gestion des superusers", "form": form, "form_title": "Ajout d'un superuser", "form_button":"Ajouter l'utilisateur aux superusers", "form_button_icon": "user-plus"}) @active_required @login_required @@ -888,7 +888,7 @@ def addCotisationHistory(request, pk): cotisation.save() messages.success(request, "La cotisation a bien été ajoutée") return redirect(reverse('users:profile',kwargs={'pk':user.pk})) - return render(request, "form.html",{"form": form, "form_title": "Ajout d'une cotisation pour l'utilisateur " + str(user), "form_button": "Ajouter"}) + return render(request, "form.html",{"form": form, "form_title": "Ajout d'une cotisation pour l'utilisateur " + str(user), "form_button": "Ajouter", "form_button_icon": "plus-square"}) @active_required @login_required @@ -969,7 +969,7 @@ def addWhiteListHistory(request, pk): whiteList.save() messages.success(request, "L'accès gracieux a bien été ajouté") return redirect(reverse('users:profile', kwargs={'pk':user.pk})) - return render(request, "form.html", {"form": form, "form_title": "Ajout d'un accès gracieux pour " + user.username, "form_button": "Ajouter"}) + return render(request, "form.html", {"form": form, "form_title": "Ajout d'un accès gracieux pour " + user.username, "form_button": "Ajouter", "form_button_icon": "plus-square"}) ########## Schools ########## @@ -1019,7 +1019,7 @@ def createSchool(request): form.save() messages.success(request, "L'école a bien été créée") return redirect(reverse('users:schoolsIndex')) - return render(request, "form.html", {"form": form, "form_title": "Création d'une école", "form_button": "Créer"}) + return render(request, "form.html", {"form": form, "form_title": "Création d'une école", "form_button": "Créer", "form_button_icon": "plus-square"}) @active_required @login_required @@ -1052,7 +1052,7 @@ def editSchool(request, pk): form.save() messages.success(request, "L'école a bien été modifiée") return redirect(reverse('users:schoolsIndex')) - return render(request, "form.html", {"form": form, "form_title": "Modification de l'école " + str(school), "form_button": "Modifier"}) + return render(request, "form.html", {"form": form, "form_title": "Modification de l'école " + str(school), "form_button": "Modifier", "form_button": "pencil-alt"}) @active_required @login_required From 53d4acae01aa3cea9d2922f22fc0f917f1239064 Mon Sep 17 00:00:00 2001 From: Nanoy Date: Thu, 17 Jan 2019 23:25:56 +0100 Subject: [PATCH 05/15] Changement des widgtes --- gestion/forms.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gestion/forms.py b/gestion/forms.py index 34d18da..eb11709 100644 --- a/gestion/forms.py +++ b/gestion/forms.py @@ -16,20 +16,21 @@ class ReloadForm(forms.ModelForm): class Meta: model = Reload fields = ("customer", "amount", "PaymentMethod") - widgets = {'customer': autocomplete.ModelSelect2(url='users:active-users-autocomplete', attrs={'data-minimum-input-length':2})} + widgets = {'customer': autocomplete.ModelSelect2(url='users:active-users-autocomplete', attrs={'data-minimum-input-length':2}), 'amount': forms.TextInput} class RefundForm(forms.ModelForm): class Meta: model = Refund fields = ("customer", "amount") - widgets = {'customer': autocomplete.ModelSelect2(url='users:active-users-autocomplete', attrs={'data-minimum-input-length':2})} + widgets = {'customer': autocomplete.ModelSelect2(url='users:active-users-autocomplete', attrs={'data-minimum-input-length':2}), 'amount': forms.TextInput} class ProductForm(forms.ModelForm): class Meta: model = Product fields = "__all__" + widgets = {'amount': forms.TextInput} class KegForm(forms.ModelForm): def __init__(self, *args, **kwargs): @@ -41,11 +42,13 @@ class KegForm(forms.ModelForm): class Meta: model = Keg exclude = ("is_active", ) + widgets = {'amount': forms.TextInput} class MenuForm(forms.ModelForm): class Meta: model = Menu fields = "__all__" + widgets = {'amount': forms.TextInput} class SearchProductForm(forms.Form): product = forms.ModelChoiceField(queryset=Product.objects.all(), required=True, label="Produit", widget=autocomplete.ModelSelect2(url='gestion:products-autocomplete', attrs={'data-minimum-input-length':2})) From 130a42ae731662647895b3bb04888487c30dbc77 Mon Sep 17 00:00:00 2001 From: Nanoy Date: Fri, 18 Jan 2019 15:38:48 +0100 Subject: [PATCH 06/15] Ajout classement produit --- gestion/models.py | 16 ++++++++++ gestion/templates/gestion/ranking.html | 43 +++++++++++++++++++++++++- gestion/views.py | 7 ++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/gestion/models.py b/gestion/models.py index a9fc147..9921418 100644 --- a/gestion/models.py +++ b/gestion/models.py @@ -45,6 +45,22 @@ class Product(models.Model): def __str__(self): return self.name + def user_ranking(self, pk): + user = User.objects.get(pk=pk) + consumptions = ConsumptionHistory.objects.filter(customer=user).filter(product=self) + # add menu + nb = 0 + for consumption in consumptions: + nb += consumption.quantity + return (user, nb) + + @property + def ranking(self): + users = User.objects.all() + ranking = [self.user_ranking(user.pk) for user in users] + ranking.sort(key=lambda x:x[1], reverse=True) + return ranking[0:25] + def isPinte(id): product = Product.objects.get(id=id) diff --git a/gestion/templates/gestion/ranking.html b/gestion/templates/gestion/ranking.html index 204fc50..fb9d4eb 100644 --- a/gestion/templates/gestion/ranking.html +++ b/gestion/templates/gestion/ranking.html @@ -1,10 +1,11 @@ {% extends "base.html" %} {%load static %} {%block entete%}Classement{%endblock%} -{% block nav %} +{% block navbar %} {% endblock %} {% block content %} @@ -70,4 +71,44 @@ +
    +
    +
    +
    +

    Classement par produit

    +
    +
    +
    +
    + {% csrf_token %} + {{form}} +

    + + + {% if product_ranking %} +
    {{user}}Profil{{user}} Profil{{ user.is_active | yesno:"Désa,A"}}ctiver{% if user.is_active %} Désactiver{% else %} Activer{% endif %}
    + + + + + + + + + {% for customer in product_ranking %} + + + + + + {%endfor%} + +
    PlacePseudoQuantités consommées
    {{ forloop.counter }}{{ customer.0.username }}{{ customer.1 }}
    + {% endif %} +
    +
    + + +
    +{{form.media}} {%endblock%} \ No newline at end of file diff --git a/gestion/views.py b/gestion/views.py index af72595..696da59 100644 --- a/gestion/views.py +++ b/gestion/views.py @@ -939,7 +939,12 @@ def ranking(request): alcohol = customer.profile.alcohol list.append([customer, alcohol]) bestDrinkers = sorted(list, key=lambda x: x[1], reverse=True)[:25] - return render(request, "gestion/ranking.html", {"bestBuyers": bestBuyers, "bestDrinkers": bestDrinkers}) + form = SearchProductForm(request.POST or None) + if(form.is_valid()): + product_ranking = form.cleaned_data['product'].ranking + else: + product_ranking = None + return render(request, "gestion/ranking.html", {"bestBuyers": bestBuyers, "bestDrinkers": bestDrinkers, "product_ranking": product_ranking, "form": form}) ########## Pinte monitoring ########## From 8265a7535bd9e9f98dfb8d82288b3cf5ef1752fe Mon Sep 17 00:00:00 2001 From: Nanoy Date: Sat, 19 Jan 2019 23:39:48 +0100 Subject: [PATCH 07/15] Page d'accueil --- coopeV3/urls.py | 1 + coopeV3/views.py | 12 +++++-- preferences/forms.py | 1 + .../migrations/0006_auto_20190119_2326.py | 23 ++++++++++++++ preferences/models.py | 1 + .../preferences/general_preferences.html | 9 ++++++ templates/home.html | 31 +++++++++++++++++++ templates/nav.html | 9 ++++-- 8 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 preferences/migrations/0006_auto_20190119_2326.py create mode 100644 templates/home.html diff --git a/coopeV3/urls.py b/coopeV3/urls.py index 83de9b1..bce380c 100644 --- a/coopeV3/urls.py +++ b/coopeV3/urls.py @@ -20,6 +20,7 @@ from . import views urlpatterns = [ path('', views.home, name="home"), + path('home', views.homepage, name="homepage"), path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/', admin.site.urls), path('users/', include('users.urls')), diff --git a/coopeV3/views.py b/coopeV3/views.py index e1da5b8..84734f9 100644 --- a/coopeV3/views.py +++ b/coopeV3/views.py @@ -1,11 +1,19 @@ -from django.shortcuts import redirect +from django.shortcuts import redirect, render from django.urls import reverse +from preferences.models import GeneralPreferences +from gestion.models import Keg + def home(request): if request.user.is_authenticated: if(request.user.has_perm('gestion.can_manage')): return redirect(reverse('gestion:manage')) else: - return redirect(reverse('users:profile', kwargs={'pk': request.user.pk})) + return redirect(reverse('homepage')) else: return redirect(reverse('users:login')) + +def homepage(request): + gp, _ = GeneralPreferences.objects.get_or_create(pk=1) + kegs = Keg.objects.filter(is_active=True) + return render(request, "home.html", {"home_text": gp.home_text, "kegs": kegs}) diff --git a/preferences/forms.py b/preferences/forms.py index d6224c6..6e92522 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -36,5 +36,6 @@ class GeneralPreferencesForm(forms.ModelForm): 'treasurer': forms.TextInput(attrs={'placeholder': 'Trésorier'}), 'brewer': forms.TextInput(attrs={'placeholder': 'Maître brasseur'}), 'grocer': forms.TextInput(attrs={'placeholder': 'Epic épicier'}), + 'home_text': forms.Textarea(attrs={'placeholder': 'Ce message sera affiché sur la page d\'accueil'}) } diff --git a/preferences/migrations/0006_auto_20190119_2326.py b/preferences/migrations/0006_auto_20190119_2326.py new file mode 100644 index 0000000..0762048 --- /dev/null +++ b/preferences/migrations/0006_auto_20190119_2326.py @@ -0,0 +1,23 @@ +# Generated by Django 2.1 on 2019-01-19 22:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0005_auto_20190106_0513'), + ] + + operations = [ + migrations.AddField( + model_name='generalpreferences', + name='home_text', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='historicalgeneralpreferences', + name='home_text', + field=models.TextField(blank=True), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index dd1cc7b..e5eada9 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -34,6 +34,7 @@ class GeneralPreferences(models.Model): use_pinte_monitoring = models.BooleanField(default=False) lost_pintes_allowed = models.PositiveIntegerField(default=0) floating_buttons = models.BooleanField(default=False) + home_text = models.TextField(blank=True) history = HistoricalRecords() class Cotisation(models.Model): diff --git a/preferences/templates/preferences/general_preferences.html b/preferences/templates/preferences/general_preferences.html index 988dea2..44acc57 100644 --- a/preferences/templates/preferences/general_preferences.html +++ b/preferences/templates/preferences/general_preferences.html @@ -127,12 +127,21 @@

    Autre

    +
    +

    Boutons flottants

    {{form.floating_buttons}}
    +
    +

    Texte de la page d'accueil

    +
    +
    + {{form.home_text}} +
    +
    diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..b05a045 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} +{% block entete %}Accueil{% endblock %} +{% block navbar %} + +{% endblock %} +{% block content %} +
    +
    +

    Accueil

    +
    +
    + {{ home_text }} +
    +
    +
    +
    +

    Les pressions du moment

    +
    +
    + Les bières pressions actuellement en Coopé : +
      + {% for keg in kegs %} +
    • {{keg}} ({% if keg.pinte %} Pinte : {{keg.pinte.amount}}€,{% endif %}{% if keg.demi %} Demi : {{keg.demi.amount}}€,{% endif %}{% if keg.galopin %} Galopin : {{keg.galopin.amount}}€{% endif %})
    • + {% endfor %} +
    +
    +
    +{% endblock %} diff --git a/templates/nav.html b/templates/nav.html index d19985f..0c636e5 100644 --- a/templates/nav.html +++ b/templates/nav.html @@ -1,3 +1,6 @@ + + Accueil + {% if request.user.is_authenticated %} Mon profil @@ -18,11 +21,11 @@ {% endif %} +
    Classement
    {% if perms.preferences.change_generalpreferences %} -
    Admin
    {% endif %} @@ -45,5 +48,7 @@ Deconnexion {% else %} - Connexion + + Connexion + {% endif %} From 5a18899dc6a6e34bd8a78f670f1d1bd4f68b5e3b Mon Sep 17 00:00:00 2001 From: Nanoy Date: Sun, 20 Jan 2019 09:28:11 +0100 Subject: [PATCH 08/15] Coope-runner --- coopeV3/urls.py | 1 + coopeV3/views.py | 3 + staticfiles/css/runner.css | 136 + .../default_100_percent/100-disabled.png | Bin 0 -> 382 bytes .../default_100_percent/100-error-offline.png | Bin 0 -> 196 bytes .../100-offline-sprite.png | Bin 0 -> 10330 bytes .../100-offline-sprite.xcf | Bin 0 -> 48215 bytes staticfiles/runner.js | 2715 +++++++++++++++++ templates/404.html | 26 + templates/base.html | 4 +- templates/coope-runner.html | 86 + 11 files changed, 2970 insertions(+), 1 deletion(-) create mode 100644 staticfiles/css/runner.css create mode 100644 staticfiles/runner-assets/default_100_percent/100-disabled.png create mode 100644 staticfiles/runner-assets/default_100_percent/100-error-offline.png create mode 100644 staticfiles/runner-assets/default_100_percent/100-offline-sprite.png create mode 100644 staticfiles/runner-assets/default_100_percent/100-offline-sprite.xcf create mode 100644 staticfiles/runner.js create mode 100644 templates/coope-runner.html diff --git a/coopeV3/urls.py b/coopeV3/urls.py index bce380c..ec4148f 100644 --- a/coopeV3/urls.py +++ b/coopeV3/urls.py @@ -21,6 +21,7 @@ from . import views urlpatterns = [ path('', views.home, name="home"), path('home', views.homepage, name="homepage"), + path('coope-runner', views.coope_runner, name="coope-runner"), path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/', admin.site.urls), path('users/', include('users.urls')), diff --git a/coopeV3/views.py b/coopeV3/views.py index 84734f9..612a342 100644 --- a/coopeV3/views.py +++ b/coopeV3/views.py @@ -17,3 +17,6 @@ def homepage(request): gp, _ = GeneralPreferences.objects.get_or_create(pk=1) kegs = Keg.objects.filter(is_active=True) return render(request, "home.html", {"home_text": gp.home_text, "kegs": kegs}) + +def coope_runner(request): + return render(request, "coope-runner.html") diff --git a/staticfiles/css/runner.css b/staticfiles/css/runner.css new file mode 100644 index 0000000..b7e52b8 --- /dev/null +++ b/staticfiles/css/runner.css @@ -0,0 +1,136 @@ +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +html, body { + padding: 0; + margin: 0; + width: 100%; + height: 100%; +} + +.icon { + -webkit-user-select: none; + user-select: none; + display: inline-block; +} + +.icon-offline { + content: -webkit-image-set( url(../runner-assets/default_100_percent/100-error-offline.png) 1x, url(../runner-assets/default_200_percent/200-error-offline.png) 2x); + position: relative; +} + +.hidden { + display: none; +} + + +/* Offline page */ + +.offline .interstitial-wrapper { + color: #2b2b2b; + font-size: 1em; + line-height: 1.55; + margin: 0 auto; + max-width: 600px; + padding-top: 100px; + width: 100%; +} + +.offline .runner-container { + height: 150px; + max-width: 600px; + overflow: hidden; + position: absolute; + top: 35px; + width: 44px; +} + +.offline .runner-canvas { + height: 150px; + max-width: 600px; + opacity: 1; + overflow: hidden; + position: absolute; + top: 0; + z-index: 2; +} + +.offline .controller { + background: rgba(247, 247, 247, .1); + height: 100vh; + left: 0; + position: absolute; + top: 0; + width: 100vw; + z-index: 1; +} + +#offline-resources { + display: none; +} + +@media (max-width: 420px) { + .suggested-left > #control-buttons, .suggested-right > #control-buttons { + float: none; + } + .snackbar { + left: 0; + bottom: 0; + width: 100%; + border-radius: 0; + } +} + +@media (max-height: 350px) { + h1 { + margin: 0 0 15px; + } + .icon-offline { + margin: 0 0 10px; + } + .interstitial-wrapper { + margin-top: 5%; + } + .nav-wrapper { + margin-top: 30px; + } +} + +@media (min-width: 600px) and (max-width: 736px) and (orientation: landscape) { + .offline .interstitial-wrapper { + margin-left: 0; + margin-right: 0; + } +} + +@media (min-width: 420px) and (max-width: 736px) and (min-height: 240px) and (max-height: 420px) and (orientation:landscape) { + .interstitial-wrapper { + margin-bottom: 100px; + } +} + +@media (min-height: 240px) and (orientation: landscape) { + .offline .interstitial-wrapper { + margin-bottom: 90px; + } + .icon-offline { + margin-bottom: 20px; + } +} + +@media (max-height: 320px) and (orientation: landscape) { + .icon-offline { + margin-bottom: 0; + } + .offline .runner-container { + top: 10px; + } +} + +@media (max-width: 240px) { + .interstitial-wrapper { + overflow: inherit; + padding: 0 8px; + } +} diff --git a/staticfiles/runner-assets/default_100_percent/100-disabled.png b/staticfiles/runner-assets/default_100_percent/100-disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..526075e6d55359bb31f0ba3b1154524a04dea4cf GIT binary patch literal 382 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 wza;5aF)k1Xe3f 1; + + /** @const */ + var IS_IOS = /iPad|iPhone|iPod/.test(window.navigator.platform); + + /** @const */ + var IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS; + + /** @const */ + var IS_TOUCH_ENABLED = 'ontouchstart' in window; + + /** + * Default game configuration. + * @enum {number} + */ + Runner.config = { + ACCELERATION: 0.001, + BG_CLOUD_SPEED: 0.2, + BOTTOM_PAD: 10, + CLEAR_TIME: 3000, + CLOUD_FREQUENCY: 0.5, + GAMEOVER_CLEAR_TIME: 750, + GAP_COEFFICIENT: 0.6, + GRAVITY: 0.6, + INITIAL_JUMP_VELOCITY: 12, + INVERT_FADE_DURATION: 12000, + INVERT_DISTANCE: 700, + MAX_BLINK_COUNT: 3, + MAX_CLOUDS: 6, + MAX_OBSTACLE_LENGTH: 3, + MAX_OBSTACLE_DUPLICATION: 2, + MAX_SPEED: 13, + MIN_JUMP_HEIGHT: 35, + MOBILE_SPEED_COEFFICIENT: 1.2, + RESOURCE_TEMPLATE_ID: 'audio-resources', + SPEED: 6, + SPEED_DROP_COEFFICIENT: 3 + }; + + + /** + * Default dimensions. + * @enum {string} + */ + Runner.defaultDimensions = { + WIDTH: DEFAULT_WIDTH, + HEIGHT: 150 + }; + + + /** + * CSS class names. + * @enum {string} + */ + Runner.classes = { + CANVAS: 'runner-canvas', + CONTAINER: 'runner-container', + CRASHED: 'crashed', + ICON: 'icon-offline', + INVERTED: 'inverted', + SNACKBAR: 'snackbar', + SNACKBAR_SHOW: 'snackbar-show', + TOUCH_CONTROLLER: 'controller' + }; + + + /** + * Sprite definition layout of the spritesheet. + * @enum {Object} + */ + Runner.spriteDefinition = { + LDPI: { + CACTUS_LARGE: { x: 332, y: 2 }, + CACTUS_SMALL: { x: 228, y: 2 }, + CLOUD: { x: 86, y: 2 }, + HORIZON: { x: 2, y: 54 }, + MOON: { x: 484, y: 2 }, + PTERODACTYL: { x: 134, y: 2 }, + RESTART: { x: 2, y: 2 }, + TEXT_SPRITE: { x: 655, y: 2 }, + TREX: { x: 848, y: 2 }, + STAR: { x: 645, y: 2 } + }, + HDPI: { + CACTUS_LARGE: { x: 652, y: 2 }, + CACTUS_SMALL: { x: 446, y: 2 }, + CLOUD: { x: 166, y: 2 }, + HORIZON: { x: 2, y: 104 }, + MOON: { x: 954, y: 2 }, + PTERODACTYL: { x: 260, y: 2 }, + RESTART: { x: 2, y: 2 }, + TEXT_SPRITE: { x: 1294, y: 2 }, + TREX: { x: 1678, y: 2 }, + STAR: { x: 1276, y: 2 } + } + }; + + + /** + * Sound FX. Reference to the ID of the audio tag on interstitial page. + * @enum {string} + */ + Runner.sounds = { + BUTTON_PRESS: 'offline-sound-press', + HIT: 'offline-sound-hit', + SCORE: 'offline-sound-reached' + }; + + + /** + * Key code mapping. + * @enum {Object} + */ + Runner.keycodes = { + JUMP: { '38': 1, '32': 1 }, // Up, spacebar + DUCK: { '40': 1 }, // Down + RESTART: { '13': 1 } // Enter + }; + + + /** + * Runner event names. + * @enum {string} + */ + Runner.events = { + ANIM_END: 'webkitAnimationEnd', + CLICK: 'click', + KEYDOWN: 'keydown', + KEYUP: 'keyup', + MOUSEDOWN: 'mousedown', + MOUSEUP: 'mouseup', + RESIZE: 'resize', + TOUCHEND: 'touchend', + TOUCHSTART: 'touchstart', + VISIBILITY: 'visibilitychange', + BLUR: 'blur', + FOCUS: 'focus', + LOAD: 'load' + }; + + + Runner.prototype = { + /** + * Whether the easter egg has been disabled. CrOS enterprise enrolled devices. + * @return {boolean} + */ + isDisabled: function () { + // return loadTimeData && loadTimeData.valueExists('disabledEasterEgg'); + return false; + }, + + /** + * For disabled instances, set up a snackbar with the disabled message. + */ + setupDisabledRunner: function () { + this.containerEl = document.createElement('div'); + this.containerEl.className = Runner.classes.SNACKBAR; + this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg'); + this.outerContainerEl.appendChild(this.containerEl); + + // Show notification when the activation key is pressed. + document.addEventListener(Runner.events.KEYDOWN, function (e) { + if (Runner.keycodes.JUMP[e.keyCode]) { + this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW); + document.querySelector('.icon').classList.add('icon-disabled'); + } + }.bind(this)); + }, + + /** + * Setting individual settings for debugging. + * @param {string} setting + * @param {*} value + */ + updateConfigSetting: function (setting, value) { + if (setting in this.config && value != undefined) { + this.config[setting] = value; + + switch (setting) { + case 'GRAVITY': + case 'MIN_JUMP_HEIGHT': + case 'SPEED_DROP_COEFFICIENT': + this.tRex.config[setting] = value; + break; + case 'INITIAL_JUMP_VELOCITY': + this.tRex.setJumpVelocity(value); + break; + case 'SPEED': + this.setSpeed(value); + break; + } + } + }, + + /** + * Cache the appropriate image sprite from the page and get the sprite sheet + * definition. + */ + loadImages: function () { + if (IS_HIDPI) { + Runner.imageSprite = document.getElementById('offline-resources-1x'); + this.spriteDef = Runner.spriteDefinition.HDPI; + } else { + Runner.imageSprite = document.getElementById('offline-resources-1x'); + this.spriteDef = Runner.spriteDefinition.LDPI; + } + + if (Runner.imageSprite.complete) { + this.init(); + } else { + // If the images are not yet loaded, add a listener. + Runner.imageSprite.addEventListener(Runner.events.LOAD, + this.init.bind(this)); + } + }, + + /** + * Load and decode base 64 encoded sounds. + */ + loadSounds: function () { + if (!IS_IOS) { + this.audioContext = new AudioContext(); + + var resourceTemplate = + document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content; + + for (var sound in Runner.sounds) { + var soundSrc = + resourceTemplate.getElementById(Runner.sounds[sound]).src; + soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1); + var buffer = decodeBase64ToArrayBuffer(soundSrc); + + // Async, so no guarantee of order in array. + this.audioContext.decodeAudioData(buffer, function (index, audioData) { + this.soundFx[index] = audioData; + }.bind(this, sound)); + } + } + }, + + /** + * Sets the game speed. Adjust the speed accordingly if on a smaller screen. + * @param {number} opt_speed + */ + setSpeed: function (opt_speed) { + var speed = opt_speed || this.currentSpeed; + + // Reduce the speed on smaller mobile screens. + if (this.dimensions.WIDTH < DEFAULT_WIDTH) { + var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH * + this.config.MOBILE_SPEED_COEFFICIENT; + this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed; + } else if (opt_speed) { + this.currentSpeed = opt_speed; + } + }, + + /** + * Game initialiser. + */ + init: function () { + // Hide the static icon. + document.querySelector('.' + Runner.classes.ICON).style.visibility = + 'hidden'; + + this.adjustDimensions(); + this.setSpeed(); + + this.containerEl = document.createElement('div'); + this.containerEl.className = Runner.classes.CONTAINER; + + // Player canvas container. + this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH, + this.dimensions.HEIGHT, Runner.classes.PLAYER); + + this.canvasCtx = this.canvas.getContext('2d'); + this.canvasCtx.fillStyle = '#f7f7f7'; + this.canvasCtx.fill(); + Runner.updateCanvasScaling(this.canvas); + + // Horizon contains clouds, obstacles and the ground. + this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions, + this.config.GAP_COEFFICIENT); + + // Distance meter + this.distanceMeter = new DistanceMeter(this.canvas, + this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH); + + // Draw t-rex + this.tRex = new Trex(this.canvas, this.spriteDef.TREX); + + this.outerContainerEl.appendChild(this.containerEl); + + if (IS_MOBILE) { + this.createTouchController(); + } + + this.startListening(); + this.update(); + + window.addEventListener(Runner.events.RESIZE, + this.debounceResize.bind(this)); + }, + + /** + * Create the touch controller. A div that covers whole screen. + */ + createTouchController: function () { + this.touchController = document.createElement('div'); + this.touchController.className = Runner.classes.TOUCH_CONTROLLER; + this.outerContainerEl.appendChild(this.touchController); + }, + + /** + * Debounce the resize event. + */ + debounceResize: function () { + if (!this.resizeTimerId_) { + this.resizeTimerId_ = + setInterval(this.adjustDimensions.bind(this), 250); + } + }, + + /** + * Adjust game space dimensions on resize. + */ + adjustDimensions: function () { + clearInterval(this.resizeTimerId_); + this.resizeTimerId_ = null; + + var boxStyles = window.getComputedStyle(this.outerContainerEl); + var padding = Number(boxStyles.paddingLeft.substr(0, + boxStyles.paddingLeft.length - 2)); + + this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2; + + // Redraw the elements back onto the canvas. + if (this.canvas) { + this.canvas.width = this.dimensions.WIDTH; + this.canvas.height = this.dimensions.HEIGHT; + + Runner.updateCanvasScaling(this.canvas); + + this.distanceMeter.calcXPos(this.dimensions.WIDTH); + this.clearCanvas(); + this.horizon.update(0, 0, true); + this.tRex.update(0); + + // Outer container and distance meter. + if (this.playing || this.crashed || this.paused) { + this.containerEl.style.width = this.dimensions.WIDTH + 'px'; + this.containerEl.style.height = this.dimensions.HEIGHT + 'px'; + this.distanceMeter.update(0, Math.ceil(this.distanceRan)); + this.stop(); + } else { + this.tRex.draw(0, 0); + } + + // Game over panel. + if (this.crashed && this.gameOverPanel) { + this.gameOverPanel.updateDimensions(this.dimensions.WIDTH); + this.gameOverPanel.draw(); + } + } + }, + + /** + * Play the game intro. + * Canvas container width expands out to the full width. + */ + playIntro: function () { + if (!this.activated && !this.crashed) { + this.playingIntro = true; + this.tRex.playingIntro = true; + + // CSS animation definition. + var keyframes = '@-webkit-keyframes intro { ' + + 'from { width:' + Trex.config.WIDTH + 'px }' + + 'to { width: ' + this.dimensions.WIDTH + 'px }' + + '}'; + + // create a style sheet to put the keyframe rule in + // and then place the style sheet in the html head + var sheet = document.createElement('style'); + sheet.innerHTML = keyframes; + document.head.appendChild(sheet); + + this.containerEl.addEventListener(Runner.events.ANIM_END, + this.startGame.bind(this)); + + this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both'; + this.containerEl.style.width = this.dimensions.WIDTH + 'px'; + + // if (this.touchController) { + // this.outerContainerEl.appendChild(this.touchController); + // } + this.playing = true; + this.activated = true; + } else if (this.crashed) { + this.restart(); + } + }, + + + /** + * Update the game status to started. + */ + startGame: function () { + this.runningTime = 0; + this.playingIntro = false; + this.tRex.playingIntro = false; + this.containerEl.style.webkitAnimation = ''; + this.playCount++; + + // Handle tabbing off the page. Pause the current game. + document.addEventListener(Runner.events.VISIBILITY, + this.onVisibilityChange.bind(this)); + + window.addEventListener(Runner.events.BLUR, + this.onVisibilityChange.bind(this)); + + window.addEventListener(Runner.events.FOCUS, + this.onVisibilityChange.bind(this)); + }, + + clearCanvas: function () { + this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH, + this.dimensions.HEIGHT); + }, + + /** + * Update the game frame and schedules the next one. + */ + update: function () { + this.updatePending = false; + + var now = getTimeStamp(); + var deltaTime = now - (this.time || now); + this.time = now; + + if (this.playing) { + this.clearCanvas(); + + if (this.tRex.jumping) { + this.tRex.updateJump(deltaTime); + } + + this.runningTime += deltaTime; + var hasObstacles = this.runningTime > this.config.CLEAR_TIME; + + // First jump triggers the intro. + if (this.tRex.jumpCount == 1 && !this.playingIntro) { + this.playIntro(); + } + + // The horizon doesn't move until the intro is over. + if (this.playingIntro) { + this.horizon.update(0, this.currentSpeed, hasObstacles); + } else { + deltaTime = !this.activated ? 0 : deltaTime; + this.horizon.update(deltaTime, this.currentSpeed, hasObstacles, + this.inverted); + } + + // Check for collisions. + var collision = hasObstacles && + checkForCollision(this.horizon.obstacles[0], this.tRex); + + if (!collision) { + this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame; + + if (this.currentSpeed < this.config.MAX_SPEED) { + this.currentSpeed += this.config.ACCELERATION; + } + } else { + this.gameOver(); + } + + var playAchievementSound = this.distanceMeter.update(deltaTime, + Math.ceil(this.distanceRan)); + + if (playAchievementSound) { + this.playSound(this.soundFx.SCORE); + } + + // Night mode. + if (this.invertTimer > this.config.INVERT_FADE_DURATION) { + this.invertTimer = 0; + this.invertTrigger = false; + this.invert(); + } else if (this.invertTimer) { + this.invertTimer += deltaTime; + } else { + var actualDistance = + this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan)); + + if (actualDistance > 0) { + this.invertTrigger = !(actualDistance % + this.config.INVERT_DISTANCE); + + if (this.invertTrigger && this.invertTimer === 0) { + this.invertTimer += deltaTime; + this.invert(); + } + } + } + } + + if (this.playing || (!this.activated && + this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) { + this.tRex.update(deltaTime); + this.scheduleNextUpdate(); + } + }, + + /** + * Event handler. + */ + handleEvent: function (e) { + return (function (evtType, events) { + switch (evtType) { + case events.KEYDOWN: + case events.TOUCHSTART: + case events.MOUSEDOWN: + this.onKeyDown(e); + break; + case events.KEYUP: + case events.TOUCHEND: + case events.MOUSEUP: + this.onKeyUp(e); + break; + } + }.bind(this))(e.type, Runner.events); + }, + + /** + * Bind relevant key / mouse / touch listeners. + */ + startListening: function () { + // Keys. + document.addEventListener(Runner.events.KEYDOWN, this); + document.addEventListener(Runner.events.KEYUP, this); + + if (IS_MOBILE) { + // Mobile only touch devices. + this.touchController.addEventListener(Runner.events.TOUCHSTART, this); + this.touchController.addEventListener(Runner.events.TOUCHEND, this); + this.containerEl.addEventListener(Runner.events.TOUCHSTART, this); + } else { + // Mouse. + document.addEventListener(Runner.events.MOUSEDOWN, this); + document.addEventListener(Runner.events.MOUSEUP, this); + } + }, + + /** + * Remove all listeners. + */ + stopListening: function () { + document.removeEventListener(Runner.events.KEYDOWN, this); + document.removeEventListener(Runner.events.KEYUP, this); + + if (IS_MOBILE) { + this.touchController.removeEventListener(Runner.events.TOUCHSTART, this); + this.touchController.removeEventListener(Runner.events.TOUCHEND, this); + this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this); + } else { + document.removeEventListener(Runner.events.MOUSEDOWN, this); + document.removeEventListener(Runner.events.MOUSEUP, this); + } + }, + + /** + * Process keydown. + * @param {Event} e + */ + onKeyDown: function (e) { + // Prevent native page scrolling whilst tapping on mobile. + if (IS_MOBILE && this.playing) { + e.preventDefault(); + } + + if (e.target != this.detailsButton) { + if (!this.crashed && (Runner.keycodes.JUMP[e.keyCode] || + e.type == Runner.events.TOUCHSTART)) { + if (!this.playing) { + this.loadSounds(); + this.playing = true; + this.update(); + if (window.errorPageController) { + errorPageController.trackEasterEgg(); + } + } + // Play sound effect and jump on starting the game for the first time. + if (!this.tRex.jumping && !this.tRex.ducking) { + this.playSound(this.soundFx.BUTTON_PRESS); + this.tRex.startJump(this.currentSpeed); + } + } + + if (this.crashed && e.type == Runner.events.TOUCHSTART && + e.currentTarget == this.containerEl) { + this.restart(); + } + } + + if (this.playing && !this.crashed && Runner.keycodes.DUCK[e.keyCode]) { + e.preventDefault(); + if (this.tRex.jumping) { + // Speed drop, activated only when jump key is not pressed. + this.tRex.setSpeedDrop(); + } else if (!this.tRex.jumping && !this.tRex.ducking) { + // Duck. + this.tRex.setDuck(true); + } + } + }, + + + /** + * Process key up. + * @param {Event} e + */ + onKeyUp: function (e) { + var keyCode = String(e.keyCode); + var isjumpKey = Runner.keycodes.JUMP[keyCode] || + e.type == Runner.events.TOUCHEND || + e.type == Runner.events.MOUSEDOWN; + + if (this.isRunning() && isjumpKey) { + this.tRex.endJump(); + } else if (Runner.keycodes.DUCK[keyCode]) { + this.tRex.speedDrop = false; + this.tRex.setDuck(false); + } else if (this.crashed) { + // Check that enough time has elapsed before allowing jump key to restart. + var deltaTime = getTimeStamp() - this.time; + + if (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) || + (deltaTime >= this.config.GAMEOVER_CLEAR_TIME && + Runner.keycodes.JUMP[keyCode])) { + this.restart(); + } + } else if (this.paused && isjumpKey) { + // Reset the jump state + this.tRex.reset(); + this.play(); + } + }, + + /** + * Returns whether the event was a left click on canvas. + * On Windows right click is registered as a click. + * @param {Event} e + * @return {boolean} + */ + isLeftClickOnCanvas: function (e) { + return e.button != null && e.button < 2 && + e.type == Runner.events.MOUSEUP && e.target == this.canvas; + }, + + /** + * RequestAnimationFrame wrapper. + */ + scheduleNextUpdate: function () { + if (!this.updatePending) { + this.updatePending = true; + this.raqId = requestAnimationFrame(this.update.bind(this)); + } + }, + + /** + * Whether the game is running. + * @return {boolean} + */ + isRunning: function () { + return !!this.raqId; + }, + + /** + * Game over state. + */ + gameOver: function () { + this.playSound(this.soundFx.HIT); + vibrate(200); + + this.stop(); + this.crashed = true; + this.distanceMeter.acheivement = false; + + this.tRex.update(100, Trex.status.CRASHED); + + // Game over panel. + if (!this.gameOverPanel) { + this.gameOverPanel = new GameOverPanel(this.canvas, + this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART, + this.dimensions); + } else { + this.gameOverPanel.draw(); + } + + // Update the high score. + if (this.distanceRan > this.highestScore) { + this.highestScore = Math.ceil(this.distanceRan); + this.distanceMeter.setHighScore(this.highestScore); + } + + // Reset the time clock. + this.time = getTimeStamp(); + }, + + stop: function () { + this.playing = false; + this.paused = true; + cancelAnimationFrame(this.raqId); + this.raqId = 0; + }, + + play: function () { + if (!this.crashed) { + this.playing = true; + this.paused = false; + this.tRex.update(0, Trex.status.RUNNING); + this.time = getTimeStamp(); + this.update(); + } + }, + + restart: function () { + if (!this.raqId) { + this.playCount++; + this.runningTime = 0; + this.playing = true; + this.crashed = false; + this.distanceRan = 0; + this.setSpeed(this.config.SPEED); + this.time = getTimeStamp(); + this.containerEl.classList.remove(Runner.classes.CRASHED); + this.clearCanvas(); + this.distanceMeter.reset(this.highestScore); + this.horizon.reset(); + this.tRex.reset(); + this.playSound(this.soundFx.BUTTON_PRESS); + this.invert(true); + this.update(); + } + }, + + /** + * Pause the game if the tab is not in focus. + */ + onVisibilityChange: function (e) { + if (document.hidden || document.webkitHidden || e.type == 'blur' || + document.visibilityState != 'visible') { + this.stop(); + } else if (!this.crashed) { + this.tRex.reset(); + this.play(); + } + }, + + /** + * Play a sound. + * @param {SoundBuffer} soundBuffer + */ + playSound: function (soundBuffer) { + if (soundBuffer) { + var sourceNode = this.audioContext.createBufferSource(); + sourceNode.buffer = soundBuffer; + sourceNode.connect(this.audioContext.destination); + sourceNode.start(0); + } + }, + + /** + * Inverts the current page / canvas colors. + * @param {boolean} Whether to reset colors. + */ + invert: function (reset) { + if (reset) { + document.body.classList.toggle(Runner.classes.INVERTED, false); + this.invertTimer = 0; + this.inverted = false; + } else { + this.inverted = document.body.classList.toggle(Runner.classes.INVERTED, + this.invertTrigger); + } + } + }; + + + /** + * Updates the canvas size taking into + * account the backing store pixel ratio and + * the device pixel ratio. + * + * See article by Paul Lewis: + * http://www.html5rocks.com/en/tutorials/canvas/hidpi/ + * + * @param {HTMLCanvasElement} canvas + * @param {number} opt_width + * @param {number} opt_height + * @return {boolean} Whether the canvas was scaled. + */ + Runner.updateCanvasScaling = function (canvas, opt_width, opt_height) { + var context = canvas.getContext('2d'); + + // Query the various pixel ratios + var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1; + var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1; + var ratio = devicePixelRatio / backingStoreRatio; + + // Upscale the canvas if the two ratios don't match + if (devicePixelRatio !== backingStoreRatio) { + var oldWidth = opt_width || canvas.width; + var oldHeight = opt_height || canvas.height; + + canvas.width = oldWidth * ratio; + canvas.height = oldHeight * ratio; + + canvas.style.width = oldWidth + 'px'; + canvas.style.height = oldHeight + 'px'; + + // Scale the context to counter the fact that we've manually scaled + // our canvas element. + context.scale(ratio, ratio); + return true; + } else if (devicePixelRatio == 1) { + // Reset the canvas width / height. Fixes scaling bug when the page is + // zoomed and the devicePixelRatio changes accordingly. + canvas.style.width = canvas.width + 'px'; + canvas.style.height = canvas.height + 'px'; + } + return false; + }; + + + /** + * Get random number. + * @param {number} min + * @param {number} max + * @param {number} + */ + function getRandomNum(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + + /** + * Vibrate on mobile devices. + * @param {number} duration Duration of the vibration in milliseconds. + */ + function vibrate(duration) { + if (IS_MOBILE && window.navigator.vibrate) { + window.navigator.vibrate(duration); + } + } + + + /** + * Create canvas element. + * @param {HTMLElement} container Element to append canvas to. + * @param {number} width + * @param {number} height + * @param {string} opt_classname + * @return {HTMLCanvasElement} + */ + function createCanvas(container, width, height, opt_classname) { + var canvas = document.createElement('canvas'); + canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' + + opt_classname : Runner.classes.CANVAS; + canvas.width = width; + canvas.height = height; + container.appendChild(canvas); + + return canvas; + } + + + /** + * Decodes the base 64 audio to ArrayBuffer used by Web Audio. + * @param {string} base64String + */ + function decodeBase64ToArrayBuffer(base64String) { + var len = (base64String.length / 4) * 3; + var str = atob(base64String); + var arrayBuffer = new ArrayBuffer(len); + var bytes = new Uint8Array(arrayBuffer); + + for (var i = 0; i < len; i++) { + bytes[i] = str.charCodeAt(i); + } + return bytes.buffer; + } + + + /** + * Return the current timestamp. + * @return {number} + */ + function getTimeStamp() { + return IS_IOS ? new Date().getTime() : performance.now(); + } + + + //****************************************************************************** + + + /** + * Game over panel. + * @param {!HTMLCanvasElement} canvas + * @param {Object} textImgPos + * @param {Object} restartImgPos + * @param {!Object} dimensions Canvas dimensions. + * @constructor + */ + function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) { + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.canvasDimensions = dimensions; + this.textImgPos = textImgPos; + this.restartImgPos = restartImgPos; + this.draw(); + }; + + + /** + * Dimensions used in the panel. + * @enum {number} + */ + GameOverPanel.dimensions = { + TEXT_X: 0, + TEXT_Y: 13, + TEXT_WIDTH: 191, + TEXT_HEIGHT: 11, + RESTART_WIDTH: 36, + RESTART_HEIGHT: 32 + }; + + + GameOverPanel.prototype = { + /** + * Update the panel dimensions. + * @param {number} width New canvas width. + * @param {number} opt_height Optional new canvas height. + */ + updateDimensions: function (width, opt_height) { + this.canvasDimensions.WIDTH = width; + if (opt_height) { + this.canvasDimensions.HEIGHT = opt_height; + } + }, + + /** + * Draw the panel. + */ + draw: function () { + var dimensions = GameOverPanel.dimensions; + + var centerX = this.canvasDimensions.WIDTH / 2; + + // Game over text. + var textSourceX = dimensions.TEXT_X; + var textSourceY = dimensions.TEXT_Y; + var textSourceWidth = dimensions.TEXT_WIDTH; + var textSourceHeight = dimensions.TEXT_HEIGHT; + + var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2)); + var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3); + var textTargetWidth = dimensions.TEXT_WIDTH; + var textTargetHeight = dimensions.TEXT_HEIGHT; + + var restartSourceWidth = dimensions.RESTART_WIDTH; + var restartSourceHeight = dimensions.RESTART_HEIGHT; + var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2); + var restartTargetY = this.canvasDimensions.HEIGHT / 2; + + if (IS_HIDPI) { + textSourceY *= 2; + textSourceX *= 2; + textSourceWidth *= 2; + textSourceHeight *= 2; + restartSourceWidth *= 2; + restartSourceHeight *= 2; + } + + textSourceX += this.textImgPos.x; + textSourceY += this.textImgPos.y; + + // Game over text from sprite. + this.canvasCtx.drawImage(Runner.imageSprite, + textSourceX, textSourceY, textSourceWidth, textSourceHeight, + textTargetX, textTargetY, textTargetWidth, textTargetHeight); + + // Restart button. + this.canvasCtx.drawImage(Runner.imageSprite, + this.restartImgPos.x, this.restartImgPos.y, + restartSourceWidth, restartSourceHeight, + restartTargetX, restartTargetY, dimensions.RESTART_WIDTH, + dimensions.RESTART_HEIGHT); + } + }; + + + //****************************************************************************** + + /** + * Check for a collision. + * @param {!Obstacle} obstacle + * @param {!Trex} tRex T-rex object. + * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing + * collision boxes. + * @return {Array} + */ + function checkForCollision(obstacle, tRex, opt_canvasCtx) { + var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos; + + // Adjustments are made to the bounding box as there is a 1 pixel white + // border around the t-rex and obstacles. + var tRexBox = new CollisionBox( + tRex.xPos + 1, + tRex.yPos + 1, + tRex.config.WIDTH - 2, + tRex.config.HEIGHT - 2); + + var obstacleBox = new CollisionBox( + obstacle.xPos + 1, + obstacle.yPos + 1, + obstacle.typeConfig.width * obstacle.size - 2, + obstacle.typeConfig.height - 2); + + // Debug outer box + if (opt_canvasCtx) { + drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox); + } + + // Simple outer bounds check. + if (boxCompare(tRexBox, obstacleBox)) { + var collisionBoxes = obstacle.collisionBoxes; + var tRexCollisionBoxes = tRex.ducking ? + Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING; + + // Detailed axis aligned box check. + for (var t = 0; t < tRexCollisionBoxes.length; t++) { + for (var i = 0; i < collisionBoxes.length; i++) { + // Adjust the box to actual positions. + var adjTrexBox = + createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox); + var adjObstacleBox = + createAdjustedCollisionBox(collisionBoxes[i], obstacleBox); + var crashed = boxCompare(adjTrexBox, adjObstacleBox); + + // Draw boxes for debug. + if (opt_canvasCtx) { + drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox); + } + + if (crashed) { + return [adjTrexBox, adjObstacleBox]; + } + } + } + } + return false; + }; + + + /** + * Adjust the collision box. + * @param {!CollisionBox} box The original box. + * @param {!CollisionBox} adjustment Adjustment box. + * @return {CollisionBox} The adjusted collision box object. + */ + function createAdjustedCollisionBox(box, adjustment) { + return new CollisionBox( + box.x + adjustment.x, + box.y + adjustment.y, + box.width, + box.height); + }; + + + /** + * Draw the collision boxes for debug. + */ + function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) { + canvasCtx.save(); + canvasCtx.strokeStyle = '#f00'; + canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height); + + canvasCtx.strokeStyle = '#0f0'; + canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y, + obstacleBox.width, obstacleBox.height); + canvasCtx.restore(); + }; + + + /** + * Compare two collision boxes for a collision. + * @param {CollisionBox} tRexBox + * @param {CollisionBox} obstacleBox + * @return {boolean} Whether the boxes intersected. + */ + function boxCompare(tRexBox, obstacleBox) { + var crashed = false; + var tRexBoxX = tRexBox.x; + var tRexBoxY = tRexBox.y; + + var obstacleBoxX = obstacleBox.x; + var obstacleBoxY = obstacleBox.y; + + // Axis-Aligned Bounding Box method. + if (tRexBox.x < obstacleBoxX + obstacleBox.width && + tRexBox.x + tRexBox.width > obstacleBoxX && + tRexBox.y < obstacleBox.y + obstacleBox.height && + tRexBox.height + tRexBox.y > obstacleBox.y) { + crashed = true; + } + + return crashed; + }; + + + //****************************************************************************** + + /** + * Collision box object. + * @param {number} x X position. + * @param {number} y Y Position. + * @param {number} w Width. + * @param {number} h Height. + */ + function CollisionBox(x, y, w, h) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + }; + + + //****************************************************************************** + + /** + * Obstacle. + * @param {HTMLCanvasCtx} canvasCtx + * @param {Obstacle.type} type + * @param {Object} spritePos Obstacle position in sprite. + * @param {Object} dimensions + * @param {number} gapCoefficient Mutipler in determining the gap. + * @param {number} speed + * @param {number} opt_xOffset + */ + function Obstacle(canvasCtx, type, spriteImgPos, dimensions, + gapCoefficient, speed, opt_xOffset) { + + this.canvasCtx = canvasCtx; + this.spritePos = spriteImgPos; + this.typeConfig = type; + this.gapCoefficient = gapCoefficient; + this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH); + this.dimensions = dimensions; + this.remove = false; + this.xPos = dimensions.WIDTH + (opt_xOffset || 0); + this.yPos = 0; + this.width = 0; + this.collisionBoxes = []; + this.gap = 0; + this.speedOffset = 0; + + // For animated obstacles. + this.currentFrame = 0; + this.timer = 0; + + this.init(speed); + }; + + /** + * Coefficient for calculating the maximum gap. + * @const + */ + Obstacle.MAX_GAP_COEFFICIENT = 1.5; + + /** + * Maximum obstacle grouping count. + * @const + */ + Obstacle.MAX_OBSTACLE_LENGTH = 3, + + + Obstacle.prototype = { + /** + * Initialise the DOM for the obstacle. + * @param {number} speed + */ + init: function (speed) { + this.cloneCollisionBoxes(); + + // Only allow sizing if we're at the right speed. + if (this.size > 1 && this.typeConfig.multipleSpeed > speed) { + this.size = 1; + } + + this.width = this.typeConfig.width * this.size; + + // Check if obstacle can be positioned at various heights. + if (Array.isArray(this.typeConfig.yPos)) { + var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile : + this.typeConfig.yPos; + this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)]; + } else { + this.yPos = this.typeConfig.yPos; + } + + this.draw(); + + // Make collision box adjustments, + // Central box is adjusted to the size as one box. + // ____ ______ ________ + // _| |-| _| |-| _| |-| + // | |<->| | | |<--->| | | |<----->| | + // | | 1 | | | | 2 | | | | 3 | | + // |_|___|_| |_|_____|_| |_|_______|_| + // + if (this.size > 1) { + this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width - + this.collisionBoxes[2].width; + this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width; + } + + // For obstacles that go at a different speed from the horizon. + if (this.typeConfig.speedOffset) { + this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset : + -this.typeConfig.speedOffset; + } + + this.gap = this.getGap(this.gapCoefficient, speed); + }, + + /** + * Draw and crop based on size. + */ + draw: function () { + var sourceWidth = this.typeConfig.width; + var sourceHeight = this.typeConfig.height; + + if (IS_HIDPI) { + sourceWidth = sourceWidth * 2; + sourceHeight = sourceHeight * 2; + } + + // X position in sprite. + var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) + + this.spritePos.x; + + // Animation frames. + if (this.currentFrame > 0) { + sourceX += sourceWidth * this.currentFrame; + } + + this.canvasCtx.drawImage(Runner.imageSprite, + sourceX, this.spritePos.y, + sourceWidth * this.size, sourceHeight, + this.xPos, this.yPos, + this.typeConfig.width * this.size, this.typeConfig.height); + }, + + /** + * Obstacle frame update. + * @param {number} deltaTime + * @param {number} speed + */ + update: function (deltaTime, speed) { + if (!this.remove) { + if (this.typeConfig.speedOffset) { + speed += this.speedOffset; + } + this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime); + + // Update frame + if (this.typeConfig.numFrames) { + this.timer += deltaTime; + if (this.timer >= this.typeConfig.frameRate) { + this.currentFrame = + this.currentFrame == this.typeConfig.numFrames - 1 ? + 0 : this.currentFrame + 1; + this.timer = 0; + } + } + this.draw(); + + if (!this.isVisible()) { + this.remove = true; + } + } + }, + + /** + * Calculate a random gap size. + * - Minimum gap gets wider as speed increses + * @param {number} gapCoefficient + * @param {number} speed + * @return {number} The gap size. + */ + getGap: function (gapCoefficient, speed) { + var minGap = Math.round(this.width * speed + + this.typeConfig.minGap * gapCoefficient); + var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT); + return getRandomNum(minGap, maxGap); + }, + + /** + * Check if obstacle is visible. + * @return {boolean} Whether the obstacle is in the game area. + */ + isVisible: function () { + return this.xPos + this.width > 0; + }, + + /** + * Make a copy of the collision boxes, since these will change based on + * obstacle type and size. + */ + cloneCollisionBoxes: function () { + var collisionBoxes = this.typeConfig.collisionBoxes; + + for (var i = collisionBoxes.length - 1; i >= 0; i--) { + this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x, + collisionBoxes[i].y, collisionBoxes[i].width, + collisionBoxes[i].height); + } + } + }; + + + /** + * Obstacle definitions. + * minGap: minimum pixel space betweeen obstacles. + * multipleSpeed: Speed at which multiples are allowed. + * speedOffset: speed faster / slower than the horizon. + * minSpeed: Minimum speed which the obstacle can make an appearance. + */ + Obstacle.types = [ + { + type: 'CACTUS_SMALL', + width: 17, + height: 35, + yPos: 105, + multipleSpeed: 4, + minGap: 120, + minSpeed: 0, + collisionBoxes: [ + new CollisionBox(0, 7, 5, 27), + new CollisionBox(4, 0, 6, 34), + new CollisionBox(10, 4, 7, 14) + ] + }, + { + type: 'CACTUS_LARGE', + width: 25, + height: 50, + yPos: 90, + multipleSpeed: 7, + minGap: 120, + minSpeed: 0, + collisionBoxes: [ + new CollisionBox(0, 12, 7, 38), + new CollisionBox(8, 0, 7, 49), + new CollisionBox(13, 10, 10, 38) + ] + }, + { + type: 'PTERODACTYL', + width: 46, + height: 40, + yPos: [100, 75, 50], // Variable height. + yPosMobile: [100, 50], // Variable height mobile. + multipleSpeed: 999, + minSpeed: 8.5, + minGap: 150, + collisionBoxes: [ + new CollisionBox(15, 15, 16, 5), + new CollisionBox(18, 21, 24, 6), + new CollisionBox(2, 14, 4, 3), + new CollisionBox(6, 10, 4, 7), + new CollisionBox(10, 8, 6, 9) + ], + numFrames: 2, + frameRate: 1000 / 6, + speedOffset: .8 + } + ]; + + + //****************************************************************************** + /** + * T-rex game character. + * @param {HTMLCanvas} canvas + * @param {Object} spritePos Positioning within image sprite. + * @constructor + */ + function Trex(canvas, spritePos) { + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.spritePos = spritePos; + this.xPos = 0; + this.yPos = 0; + // Position when on the ground. + this.groundYPos = 0; + this.currentFrame = 0; + this.currentAnimFrames = []; + this.blinkDelay = 0; + this.blinkCount = 0; + this.animStartTime = 0; + this.timer = 0; + this.msPerFrame = 1000 / FPS; + this.config = Trex.config; + // Current status. + this.status = Trex.status.WAITING; + + this.jumping = false; + this.ducking = false; + this.jumpVelocity = 0; + this.reachedMinHeight = false; + this.speedDrop = false; + this.jumpCount = 0; + this.jumpspotX = 0; + + this.init(); + }; + + + /** + * T-rex player config. + * @enum {number} + */ + Trex.config = { + DROP_VELOCITY: -5, + GRAVITY: 0.6, + HEIGHT: 47, + HEIGHT_DUCK: 25, + INIITAL_JUMP_VELOCITY: -10, + INTRO_DURATION: 1500, + MAX_JUMP_HEIGHT: 30, + MIN_JUMP_HEIGHT: 30, + SPEED_DROP_COEFFICIENT: 3, + SPRITE_WIDTH: 262, + START_X_POS: 50, + WIDTH: 44, + WIDTH_DUCK: 59 + }; + + + /** + * Used in collision detection. + * @type {Array} + */ + Trex.collisionBoxes = { + DUCKING: [ + new CollisionBox(1, 18, 55, 25) + ], + RUNNING: [ + new CollisionBox(22, 0, 17, 16), + new CollisionBox(1, 18, 30, 9), + new CollisionBox(10, 35, 14, 8), + new CollisionBox(1, 24, 29, 5), + new CollisionBox(5, 30, 21, 4), + new CollisionBox(9, 34, 15, 4) + ] + }; + + + /** + * Animation states. + * @enum {string} + */ + Trex.status = { + CRASHED: 'CRASHED', + DUCKING: 'DUCKING', + JUMPING: 'JUMPING', + RUNNING: 'RUNNING', + WAITING: 'WAITING' + }; + + /** + * Blinking coefficient. + * @const + */ + Trex.BLINK_TIMING = 7000; + + + /** + * Animation config for different states. + * @enum {Object} + */ + Trex.animFrames = { + WAITING: { + frames: [44, 0], + msPerFrame: 1000 / 3 + }, + RUNNING: { + frames: [88, 132], + msPerFrame: 1000 / 12 + }, + CRASHED: { + frames: [220], + msPerFrame: 1000 / 60 + }, + JUMPING: { + frames: [0], + msPerFrame: 1000 / 60 + }, + DUCKING: { + frames: [264, 323], + msPerFrame: 1000 / 8 + } + }; + + + Trex.prototype = { + /** + * T-rex player initaliser. + * Sets the t-rex to blink at random intervals. + */ + init: function () { + this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT - + Runner.config.BOTTOM_PAD; + this.yPos = this.groundYPos; + this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT; + + this.draw(0, 0); + this.update(0, Trex.status.WAITING); + }, + + /** + * Setter for the jump velocity. + * The approriate drop velocity is also set. + */ + setJumpVelocity: function (setting) { + this.config.INIITAL_JUMP_VELOCITY = -setting; + this.config.DROP_VELOCITY = -setting / 2; + }, + + /** + * Set the animation status. + * @param {!number} deltaTime + * @param {Trex.status} status Optional status to switch to. + */ + update: function (deltaTime, opt_status) { + this.timer += deltaTime; + + // Update the status. + if (opt_status) { + this.status = opt_status; + this.currentFrame = 0; + this.msPerFrame = Trex.animFrames[opt_status].msPerFrame; + this.currentAnimFrames = Trex.animFrames[opt_status].frames; + + if (opt_status == Trex.status.WAITING) { + this.animStartTime = getTimeStamp(); + this.setBlinkDelay(); + } + } + + // Game intro animation, T-rex moves in from the left. + if (this.playingIntro && this.xPos < this.config.START_X_POS) { + this.xPos += Math.round((this.config.START_X_POS / + this.config.INTRO_DURATION) * deltaTime); + } + + if (this.status == Trex.status.WAITING) { + this.blink(getTimeStamp()); + } else { + this.draw(this.currentAnimFrames[this.currentFrame], 0); + } + + // Update the frame position. + if (this.timer >= this.msPerFrame) { + this.currentFrame = this.currentFrame == + this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1; + this.timer = 0; + } + + // Speed drop becomes duck if the down key is still being pressed. + if (this.speedDrop && this.yPos == this.groundYPos) { + this.speedDrop = false; + this.setDuck(true); + } + }, + + /** + * Draw the t-rex to a particular position. + * @param {number} x + * @param {number} y + */ + draw: function (x, y) { + var sourceX = x; + var sourceY = y; + var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ? + this.config.WIDTH_DUCK : this.config.WIDTH; + var sourceHeight = this.config.HEIGHT; + + if (IS_HIDPI) { + sourceX *= 2; + sourceY *= 2; + sourceWidth *= 2; + sourceHeight *= 2; + } + + // Adjustments for sprite sheet position. + sourceX += this.spritePos.x; + sourceY += this.spritePos.y; + + // Ducking. + if (this.ducking && this.status != Trex.status.CRASHED) { + this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY, + sourceWidth, sourceHeight, + this.xPos, this.yPos, + this.config.WIDTH_DUCK, this.config.HEIGHT); + } else { + // Crashed whilst ducking. Trex is standing up so needs adjustment. + if (this.ducking && this.status == Trex.status.CRASHED) { + this.xPos++; + } + // Standing / running + this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY, + sourceWidth, sourceHeight, + this.xPos, this.yPos, + this.config.WIDTH, this.config.HEIGHT); + } + }, + + /** + * Sets a random time for the blink to happen. + */ + setBlinkDelay: function () { + this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING); + }, + + /** + * Make t-rex blink at random intervals. + * @param {number} time Current time in milliseconds. + */ + blink: function (time) { + var deltaTime = time - this.animStartTime; + + if (deltaTime >= this.blinkDelay) { + this.draw(this.currentAnimFrames[this.currentFrame], 0); + + if (this.currentFrame == 1) { + // Set new random delay to blink. + this.setBlinkDelay(); + this.animStartTime = time; + this.blinkCount++; + } + } + }, + + /** + * Initialise a jump. + * @param {number} speed + */ + startJump: function (speed) { + if (!this.jumping) { + this.update(0, Trex.status.JUMPING); + // Tweak the jump velocity based on the speed. + this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10); + this.jumping = true; + this.reachedMinHeight = false; + this.speedDrop = false; + } + }, + + /** + * Jump is complete, falling down. + */ + endJump: function () { + if (this.reachedMinHeight && + this.jumpVelocity < this.config.DROP_VELOCITY) { + this.jumpVelocity = this.config.DROP_VELOCITY; + } + }, + + /** + * Update frame for a jump. + * @param {number} deltaTime + * @param {number} speed + */ + updateJump: function (deltaTime, speed) { + var msPerFrame = Trex.animFrames[this.status].msPerFrame; + var framesElapsed = deltaTime / msPerFrame; + + // Speed drop makes Trex fall faster. + if (this.speedDrop) { + this.yPos += Math.round(this.jumpVelocity * + this.config.SPEED_DROP_COEFFICIENT * framesElapsed); + } else { + this.yPos += Math.round(this.jumpVelocity * framesElapsed); + } + + this.jumpVelocity += this.config.GRAVITY * framesElapsed; + + // Minimum height has been reached. + if (this.yPos < this.minJumpHeight || this.speedDrop) { + this.reachedMinHeight = true; + } + + // Reached max height + if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) { + this.endJump(); + } + + // Back down at ground level. Jump completed. + if (this.yPos > this.groundYPos) { + this.reset(); + this.jumpCount++; + } + + this.update(deltaTime); + }, + + /** + * Set the speed drop. Immediately cancels the current jump. + */ + setSpeedDrop: function () { + this.speedDrop = true; + this.jumpVelocity = 1; + }, + + /** + * @param {boolean} isDucking. + */ + setDuck: function (isDucking) { + if (isDucking && this.status != Trex.status.DUCKING) { + this.update(0, Trex.status.DUCKING); + this.ducking = true; + } else if (this.status == Trex.status.DUCKING) { + this.update(0, Trex.status.RUNNING); + this.ducking = false; + } + }, + + /** + * Reset the t-rex to running at start of game. + */ + reset: function () { + this.yPos = this.groundYPos; + this.jumpVelocity = 0; + this.jumping = false; + this.ducking = false; + this.update(0, Trex.status.RUNNING); + this.midair = false; + this.speedDrop = false; + this.jumpCount = 0; + } + }; + + + //****************************************************************************** + + /** + * Handles displaying the distance meter. + * @param {!HTMLCanvasElement} canvas + * @param {Object} spritePos Image position in sprite. + * @param {number} canvasWidth + * @constructor + */ + function DistanceMeter(canvas, spritePos, canvasWidth) { + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.image = Runner.imageSprite; + this.spritePos = spritePos; + this.x = 0; + this.y = 5; + + this.currentDistance = 0; + this.maxScore = 0; + this.highScore = 0; + this.container = null; + + this.digits = []; + this.acheivement = false; + this.defaultString = ''; + this.flashTimer = 0; + this.flashIterations = 0; + this.invertTrigger = false; + + this.config = DistanceMeter.config; + this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS; + this.init(canvasWidth); + }; + + + /** + * @enum {number} + */ + DistanceMeter.dimensions = { + WIDTH: 10, + HEIGHT: 13, + DEST_WIDTH: 11 + }; + + + /** + * Y positioning of the digits in the sprite sheet. + * X position is always 0. + * @type {Array} + */ + DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120]; + + + /** + * Distance meter config. + * @enum {number} + */ + DistanceMeter.config = { + // Number of digits. + MAX_DISTANCE_UNITS: 5, + + // Distance that causes achievement animation. + ACHIEVEMENT_DISTANCE: 100, + + // Used for conversion from pixel distance to a scaled unit. + COEFFICIENT: 0.025, + + // Flash duration in milliseconds. + FLASH_DURATION: 1000 / 4, + + // Flash iterations for achievement animation. + FLASH_ITERATIONS: 3 + }; + + + DistanceMeter.prototype = { + /** + * Initialise the distance meter to '00000'. + * @param {number} width Canvas width in px. + */ + init: function (width) { + var maxDistanceStr = ''; + + this.calcXPos(width); + this.maxScore = this.maxScoreUnits; + for (var i = 0; i < this.maxScoreUnits; i++) { + this.draw(i, 0); + this.defaultString += '0'; + maxDistanceStr += '9'; + } + + this.maxScore = parseInt(maxDistanceStr); + }, + + /** + * Calculate the xPos in the canvas. + * @param {number} canvasWidth + */ + calcXPos: function (canvasWidth) { + this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH * + (this.maxScoreUnits + 1)); + }, + + /** + * Draw a digit to canvas. + * @param {number} digitPos Position of the digit. + * @param {number} value Digit value 0-9. + * @param {boolean} opt_highScore Whether drawing the high score. + */ + draw: function (digitPos, value, opt_highScore) { + var sourceWidth = DistanceMeter.dimensions.WIDTH; + var sourceHeight = DistanceMeter.dimensions.HEIGHT; + var sourceX = DistanceMeter.dimensions.WIDTH * value; + var sourceY = 0; + + var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH; + var targetY = this.y; + var targetWidth = DistanceMeter.dimensions.WIDTH; + var targetHeight = DistanceMeter.dimensions.HEIGHT; + + // For high DPI we 2x source values. + if (IS_HIDPI) { + sourceWidth *= 2; + sourceHeight *= 2; + sourceX *= 2; + } + + sourceX += this.spritePos.x; + sourceY += this.spritePos.y; + + this.canvasCtx.save(); + + if (opt_highScore) { + // Left of the current score. + var highScoreX = this.x - (this.maxScoreUnits * 2) * + DistanceMeter.dimensions.WIDTH; + this.canvasCtx.translate(highScoreX, this.y); + } else { + this.canvasCtx.translate(this.x, this.y); + } + + this.canvasCtx.drawImage(this.image, sourceX, sourceY, + sourceWidth, sourceHeight, + targetX, targetY, + targetWidth, targetHeight + ); + + this.canvasCtx.restore(); + }, + + /** + * Covert pixel distance to a 'real' distance. + * @param {number} distance Pixel distance ran. + * @return {number} The 'real' distance ran. + */ + getActualDistance: function (distance) { + return distance ? Math.round(distance * this.config.COEFFICIENT) : 0; + }, + + /** + * Update the distance meter. + * @param {number} distance + * @param {number} deltaTime + * @return {boolean} Whether the acheivement sound fx should be played. + */ + update: function (deltaTime, distance) { + var paint = true; + var playSound = false; + + if (!this.acheivement) { + distance = this.getActualDistance(distance); + // Score has gone beyond the initial digit count. + if (distance > this.maxScore && this.maxScoreUnits == + this.config.MAX_DISTANCE_UNITS) { + this.maxScoreUnits++; + this.maxScore = parseInt(this.maxScore + '9'); + } else { + this.distance = 0; + } + + if (distance > 0) { + // Acheivement unlocked + if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) { + // Flash score and play sound. + this.acheivement = true; + this.flashTimer = 0; + playSound = true; + } + + // Create a string representation of the distance with leading 0. + var distanceStr = (this.defaultString + + distance).substr(-this.maxScoreUnits); + this.digits = distanceStr.split(''); + } else { + this.digits = this.defaultString.split(''); + } + } else { + // Control flashing of the score on reaching acheivement. + if (this.flashIterations <= this.config.FLASH_ITERATIONS) { + this.flashTimer += deltaTime; + + if (this.flashTimer < this.config.FLASH_DURATION) { + paint = false; + } else if (this.flashTimer > + this.config.FLASH_DURATION * 2) { + this.flashTimer = 0; + this.flashIterations++; + } + } else { + this.acheivement = false; + this.flashIterations = 0; + this.flashTimer = 0; + } + } + + // Draw the digits if not flashing. + if (paint) { + for (var i = this.digits.length - 1; i >= 0; i--) { + this.draw(i, parseInt(this.digits[i])); + } + } + + this.drawHighScore(); + return playSound; + }, + + /** + * Draw the high score. + */ + drawHighScore: function () { + this.canvasCtx.save(); + this.canvasCtx.globalAlpha = .8; + for (var i = this.highScore.length - 1; i >= 0; i--) { + this.draw(i, parseInt(this.highScore[i], 10), true); + } + this.canvasCtx.restore(); + }, + + /** + * Set the highscore as a array string. + * Position of char in the sprite: H - 10, I - 11. + * @param {number} distance Distance ran in pixels. + */ + setHighScore: function (distance) { + distance = this.getActualDistance(distance); + var highScoreStr = (this.defaultString + + distance).substr(-this.maxScoreUnits); + + this.highScore = ['10', '11', ''].concat(highScoreStr.split('')); + }, + + /** + * Reset the distance meter back to '00000'. + */ + reset: function () { + this.update(0); + this.acheivement = false; + } + }; + + + //****************************************************************************** + + /** + * Cloud background item. + * Similar to an obstacle object but without collision boxes. + * @param {HTMLCanvasElement} canvas Canvas element. + * @param {Object} spritePos Position of image in sprite. + * @param {number} containerWidth + */ + function Cloud(canvas, spritePos, containerWidth) { + this.canvas = canvas; + this.canvasCtx = this.canvas.getContext('2d'); + this.spritePos = spritePos; + this.containerWidth = containerWidth; + this.xPos = containerWidth; + this.yPos = 0; + this.remove = false; + this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP, + Cloud.config.MAX_CLOUD_GAP); + + this.init(); + }; + + + /** + * Cloud object config. + * @enum {number} + */ + Cloud.config = { + HEIGHT: 14, + MAX_CLOUD_GAP: 400, + MAX_SKY_LEVEL: 30, + MIN_CLOUD_GAP: 100, + MIN_SKY_LEVEL: 71, + WIDTH: 46 + }; + + + Cloud.prototype = { + /** + * Initialise the cloud. Sets the Cloud height. + */ + init: function () { + this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL, + Cloud.config.MIN_SKY_LEVEL); + this.draw(); + }, + + /** + * Draw the cloud. + */ + draw: function () { + this.canvasCtx.save(); + var sourceWidth = Cloud.config.WIDTH; + var sourceHeight = Cloud.config.HEIGHT; + + if (IS_HIDPI) { + sourceWidth = sourceWidth * 2; + sourceHeight = sourceHeight * 2; + } + + this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x, + this.spritePos.y, + sourceWidth, sourceHeight, + this.xPos, this.yPos, + Cloud.config.WIDTH, Cloud.config.HEIGHT); + + this.canvasCtx.restore(); + }, + + /** + * Update the cloud position. + * @param {number} speed + */ + update: function (speed) { + if (!this.remove) { + this.xPos -= Math.ceil(speed); + this.draw(); + + // Mark as removeable if no longer in the canvas. + if (!this.isVisible()) { + this.remove = true; + } + } + }, + + /** + * Check if the cloud is visible on the stage. + * @return {boolean} + */ + isVisible: function () { + return this.xPos + Cloud.config.WIDTH > 0; + } + }; + + + //****************************************************************************** + + /** + * Nightmode shows a moon and stars on the horizon. + */ + function NightMode(canvas, spritePos, containerWidth) { + this.spritePos = spritePos; + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.xPos = containerWidth - 50; + this.yPos = 30; + this.currentPhase = 0; + this.opacity = 0; + this.containerWidth = containerWidth; + this.stars = []; + this.drawStars = false; + this.placeStars(); + }; + + /** + * @enum {number} + */ + NightMode.config = { + FADE_SPEED: 0.035, + HEIGHT: 40, + MOON_SPEED: 0.25, + NUM_STARS: 2, + STAR_SIZE: 9, + STAR_SPEED: 0.3, + STAR_MAX_Y: 70, + WIDTH: 20 + }; + + NightMode.phases = [140, 120, 100, 60, 40, 20, 0]; + + NightMode.prototype = { + /** + * Update moving moon, changing phases. + * @param {boolean} activated Whether night mode is activated. + * @param {number} delta + */ + update: function (activated, delta) { + // Moon phase. + if (activated && this.opacity == 0) { + this.currentPhase++; + + if (this.currentPhase >= NightMode.phases.length) { + this.currentPhase = 0; + } + } + + // Fade in / out. + if (activated && (this.opacity < 1 || this.opacity == 0)) { + this.opacity += NightMode.config.FADE_SPEED; + } else if (this.opacity > 0) { + this.opacity -= NightMode.config.FADE_SPEED; + } + + // Set moon positioning. + if (this.opacity > 0) { + this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED); + + // Update stars. + if (this.drawStars) { + for (var i = 0; i < NightMode.config.NUM_STARS; i++) { + this.stars[i].x = this.updateXPos(this.stars[i].x, + NightMode.config.STAR_SPEED); + } + } + this.draw(); + } else { + this.opacity = 0; + this.placeStars(); + } + this.drawStars = true; + }, + + updateXPos: function (currentPos, speed) { + if (currentPos < -NightMode.config.WIDTH) { + currentPos = this.containerWidth; + } else { + currentPos -= speed; + } + return currentPos; + }, + + draw: function () { + var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 : + NightMode.config.WIDTH; + var moonSourceHeight = NightMode.config.HEIGHT; + var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase]; + var moonOutputWidth = moonSourceWidth; + var starSize = NightMode.config.STAR_SIZE; + var starSourceX = Runner.spriteDefinition.LDPI.STAR.x; + + if (IS_HIDPI) { + moonSourceWidth *= 2; + moonSourceHeight *= 2; + moonSourceX = this.spritePos.x + + (NightMode.phases[this.currentPhase] * 2); + starSize *= 2; + starSourceX = Runner.spriteDefinition.HDPI.STAR.x; + } + + this.canvasCtx.save(); + this.canvasCtx.globalAlpha = this.opacity; + + // Stars. + if (this.drawStars) { + for (var i = 0; i < NightMode.config.NUM_STARS; i++) { + this.canvasCtx.drawImage(Runner.imageSprite, + starSourceX, this.stars[i].sourceY, starSize, starSize, + Math.round(this.stars[i].x), this.stars[i].y, + NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE); + } + } + + // Moon. + this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX, + this.spritePos.y, moonSourceWidth, moonSourceHeight, + Math.round(this.xPos), this.yPos, + moonOutputWidth, NightMode.config.HEIGHT); + + this.canvasCtx.globalAlpha = 1; + this.canvasCtx.restore(); + }, + + // Do star placement. + placeStars: function () { + var segmentSize = Math.round(this.containerWidth / + NightMode.config.NUM_STARS); + + for (var i = 0; i < NightMode.config.NUM_STARS; i++) { + this.stars[i] = {}; + this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1)); + this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y); + + if (IS_HIDPI) { + this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y + + NightMode.config.STAR_SIZE * 2 * i; + } else { + this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y + + NightMode.config.STAR_SIZE * i; + } + } + }, + + reset: function () { + this.currentPhase = 0; + this.opacity = 0; + this.update(false); + } + + }; + + + //****************************************************************************** + + /** + * Horizon Line. + * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon. + * @param {HTMLCanvasElement} canvas + * @param {Object} spritePos Horizon position in sprite. + * @constructor + */ + function HorizonLine(canvas, spritePos) { + this.spritePos = spritePos; + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.sourceDimensions = {}; + this.dimensions = HorizonLine.dimensions; + this.sourceXPos = [this.spritePos.x, this.spritePos.x + + this.dimensions.WIDTH]; + this.xPos = []; + this.yPos = 0; + this.bumpThreshold = 0.5; + + this.setSourceDimensions(); + this.draw(); + }; + + + /** + * Horizon line dimensions. + * @enum {number} + */ + HorizonLine.dimensions = { + WIDTH: 600, + HEIGHT: 12, + YPOS: 127 + }; + + + HorizonLine.prototype = { + /** + * Set the source dimensions of the horizon line. + */ + setSourceDimensions: function () { + + for (var dimension in HorizonLine.dimensions) { + if (IS_HIDPI) { + if (dimension != 'YPOS') { + this.sourceDimensions[dimension] = + HorizonLine.dimensions[dimension] * 2; + } + } else { + this.sourceDimensions[dimension] = + HorizonLine.dimensions[dimension]; + } + this.dimensions[dimension] = HorizonLine.dimensions[dimension]; + } + + this.xPos = [0, HorizonLine.dimensions.WIDTH]; + this.yPos = HorizonLine.dimensions.YPOS; + }, + + /** + * Return the crop x position of a type. + */ + getRandomType: function () { + return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0; + }, + + /** + * Draw the horizon line. + */ + draw: function () { + this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0], + this.spritePos.y, + this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT, + this.xPos[0], this.yPos, + this.dimensions.WIDTH, this.dimensions.HEIGHT); + + this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1], + this.spritePos.y, + this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT, + this.xPos[1], this.yPos, + this.dimensions.WIDTH, this.dimensions.HEIGHT); + }, + + /** + * Update the x position of an indivdual piece of the line. + * @param {number} pos Line position. + * @param {number} increment + */ + updateXPos: function (pos, increment) { + var line1 = pos; + var line2 = pos == 0 ? 1 : 0; + + this.xPos[line1] -= increment; + this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH; + + if (this.xPos[line1] <= -this.dimensions.WIDTH) { + this.xPos[line1] += this.dimensions.WIDTH * 2; + this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH; + this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x; + } + }, + + /** + * Update the horizon line. + * @param {number} deltaTime + * @param {number} speed + */ + update: function (deltaTime, speed) { + var increment = Math.floor(speed * (FPS / 1000) * deltaTime); + + if (this.xPos[0] <= 0) { + this.updateXPos(0, increment); + } else { + this.updateXPos(1, increment); + } + this.draw(); + }, + + /** + * Reset horizon to the starting position. + */ + reset: function () { + this.xPos[0] = 0; + this.xPos[1] = HorizonLine.dimensions.WIDTH; + } + }; + + + //****************************************************************************** + + /** + * Horizon background class. + * @param {HTMLCanvasElement} canvas + * @param {Object} spritePos Sprite positioning. + * @param {Object} dimensions Canvas dimensions. + * @param {number} gapCoefficient + * @constructor + */ + function Horizon(canvas, spritePos, dimensions, gapCoefficient) { + this.canvas = canvas; + this.canvasCtx = this.canvas.getContext('2d'); + this.config = Horizon.config; + this.dimensions = dimensions; + this.gapCoefficient = gapCoefficient; + this.obstacles = []; + this.obstacleHistory = []; + this.horizonOffsets = [0, 0]; + this.cloudFrequency = this.config.CLOUD_FREQUENCY; + this.spritePos = spritePos; + this.nightMode = null; + + // Cloud + this.clouds = []; + this.cloudSpeed = this.config.BG_CLOUD_SPEED; + + // Horizon + this.horizonLine = null; + this.init(); + }; + + + /** + * Horizon config. + * @enum {number} + */ + Horizon.config = { + BG_CLOUD_SPEED: 0.2, + BUMPY_THRESHOLD: .3, + CLOUD_FREQUENCY: .5, + HORIZON_HEIGHT: 16, + MAX_CLOUDS: 6 + }; + + + Horizon.prototype = { + /** + * Initialise the horizon. Just add the line and a cloud. No obstacles. + */ + init: function () { + this.addCloud(); + this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON); + this.nightMode = new NightMode(this.canvas, this.spritePos.MOON, + this.dimensions.WIDTH); + }, + + /** + * @param {number} deltaTime + * @param {number} currentSpeed + * @param {boolean} updateObstacles Used as an override to prevent + * the obstacles from being updated / added. This happens in the + * ease in section. + * @param {boolean} showNightMode Night mode activated. + */ + update: function (deltaTime, currentSpeed, updateObstacles, showNightMode) { + this.runningTime += deltaTime; + this.horizonLine.update(deltaTime, currentSpeed); + this.nightMode.update(showNightMode); + this.updateClouds(deltaTime, currentSpeed); + + if (updateObstacles) { + this.updateObstacles(deltaTime, currentSpeed); + } + }, + + /** + * Update the cloud positions. + * @param {number} deltaTime + * @param {number} currentSpeed + */ + updateClouds: function (deltaTime, speed) { + var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed; + var numClouds = this.clouds.length; + + if (numClouds) { + for (var i = numClouds - 1; i >= 0; i--) { + this.clouds[i].update(cloudSpeed); + } + + var lastCloud = this.clouds[numClouds - 1]; + + // Check for adding a new cloud. + if (numClouds < this.config.MAX_CLOUDS && + (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap && + this.cloudFrequency > Math.random()) { + this.addCloud(); + } + + // Remove expired clouds. + this.clouds = this.clouds.filter(function (obj) { + return !obj.remove; + }); + } else { + this.addCloud(); + } + }, + + /** + * Update the obstacle positions. + * @param {number} deltaTime + * @param {number} currentSpeed + */ + updateObstacles: function (deltaTime, currentSpeed) { + // Obstacles, move to Horizon layer. + var updatedObstacles = this.obstacles.slice(0); + + for (var i = 0; i < this.obstacles.length; i++) { + var obstacle = this.obstacles[i]; + obstacle.update(deltaTime, currentSpeed); + + // Clean up existing obstacles. + if (obstacle.remove) { + updatedObstacles.shift(); + } + } + this.obstacles = updatedObstacles; + + if (this.obstacles.length > 0) { + var lastObstacle = this.obstacles[this.obstacles.length - 1]; + + if (lastObstacle && !lastObstacle.followingObstacleCreated && + lastObstacle.isVisible() && + (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) < + this.dimensions.WIDTH) { + this.addNewObstacle(currentSpeed); + lastObstacle.followingObstacleCreated = true; + } + } else { + // Create new obstacles. + this.addNewObstacle(currentSpeed); + } + }, + + removeFirstObstacle: function () { + this.obstacles.shift(); + }, + + /** + * Add a new obstacle. + * @param {number} currentSpeed + */ + addNewObstacle: function (currentSpeed) { + var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1); + var obstacleType = Obstacle.types[obstacleTypeIndex]; + + // Check for multiples of the same type of obstacle. + // Also check obstacle is available at current speed. + if (this.duplicateObstacleCheck(obstacleType.type) || + currentSpeed < obstacleType.minSpeed) { + this.addNewObstacle(currentSpeed); + } else { + var obstacleSpritePos = this.spritePos[obstacleType.type]; + + this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType, + obstacleSpritePos, this.dimensions, + this.gapCoefficient, currentSpeed, obstacleType.width)); + + this.obstacleHistory.unshift(obstacleType.type); + + if (this.obstacleHistory.length > 1) { + this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION); + } + } + }, + + /** + * Returns whether the previous two obstacles are the same as the next one. + * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION. + * @return {boolean} + */ + duplicateObstacleCheck: function (nextObstacleType) { + var duplicateCount = 0; + + for (var i = 0; i < this.obstacleHistory.length; i++) { + duplicateCount = this.obstacleHistory[i] == nextObstacleType ? + duplicateCount + 1 : 0; + } + return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION; + }, + + /** + * Reset the horizon layer. + * Remove existing obstacles and reposition the horizon line. + */ + reset: function () { + this.obstacles = []; + this.horizonLine.reset(); + this.nightMode.reset(); + }, + + /** + * Update the canvas width and scaling. + * @param {number} width Canvas width. + * @param {number} height Canvas height. + */ + resize: function (width, height) { + this.canvas.width = width; + this.canvas.height = height; + }, + + /** + * Add a new cloud to the horizon. + */ + addCloud: function () { + this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD, + this.dimensions.WIDTH)); + } + }; +})(); + + +function onDocumentLoad() { + new Runner('.interstitial-wrapper'); +} + +document.addEventListener('DOMContentLoaded', onDocumentLoad); diff --git a/templates/404.html b/templates/404.html index b09fd6d..5f83c26 100644 --- a/templates/404.html +++ b/templates/404.html @@ -1,4 +1,5 @@ {% extends 'base.html' %} +{% load static %} {% block entete %}Page introuvable{% endblock %} {% block navbar %}
      @@ -6,6 +7,30 @@
    {% endblock %} {% block content %} +

    Erreur 404

    @@ -13,5 +38,6 @@
    Une erreur s'est produite lors de l'accès à cette page (la page que vous demandez n'existe pas). Vous pouvez revenir à l'accueil en cliquant ici.
    + {% endblock %} diff --git a/templates/base.html b/templates/base.html index d9c654f..98c4c04 100644 --- a/templates/base.html +++ b/templates/base.html @@ -8,7 +8,9 @@ - + + {% block extra_css %}{% endblock %} + {% block extra_script %}{% endblock %} diff --git a/templates/coope-runner.html b/templates/coope-runner.html new file mode 100644 index 0000000..5493d4d --- /dev/null +++ b/templates/coope-runner.html @@ -0,0 +1,86 @@ +{% load static %} + + + + + + + + + + + + + + +
    +

    Press up arrow to start

    +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    + + + + + \ No newline at end of file From 75329f510df704ec19b776331aea81e99145fbf1 Mon Sep 17 00:00:00 2001 From: Nanoy Date: Sun, 20 Jan 2019 12:14:53 +0100 Subject: [PATCH 09/15] Automatic logout --- coopeV3/templatetags/vip.py | 6 +++++ .../migrations/0007_auto_20190120_1208.py | 23 +++++++++++++++++++ preferences/models.py | 1 + .../preferences/general_preferences.html | 8 +++++++ templates/base.html | 11 +++++++++ 5 files changed, 49 insertions(+) create mode 100644 preferences/migrations/0007_auto_20190120_1208.py diff --git a/coopeV3/templatetags/vip.py b/coopeV3/templatetags/vip.py index 3d49afa..436cb03 100644 --- a/coopeV3/templatetags/vip.py +++ b/coopeV3/templatetags/vip.py @@ -39,3 +39,9 @@ def global_message(): gp,_ = GeneralPreferences.objects.get_or_create(pk=1) messages = gp.global_message.split("\n") return random.choice(messages) + +@register.simple_tag +def logout_time(): + gp, _ = GeneralPreferences.objects.get_or_create(pk=1) + logout_time = gp.automatic_logout_time + return logout_time \ No newline at end of file diff --git a/preferences/migrations/0007_auto_20190120_1208.py b/preferences/migrations/0007_auto_20190120_1208.py new file mode 100644 index 0000000..18d808a --- /dev/null +++ b/preferences/migrations/0007_auto_20190120_1208.py @@ -0,0 +1,23 @@ +# Generated by Django 2.1 on 2019-01-20 11:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0006_auto_20190119_2326'), + ] + + operations = [ + migrations.AddField( + model_name='generalpreferences', + name='automatic_logout_time', + field=models.PositiveIntegerField(null=True), + ), + migrations.AddField( + model_name='historicalgeneralpreferences', + name='automatic_logout_time', + field=models.PositiveIntegerField(null=True), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index e5eada9..f4d1ce6 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -35,6 +35,7 @@ class GeneralPreferences(models.Model): lost_pintes_allowed = models.PositiveIntegerField(default=0) floating_buttons = models.BooleanField(default=False) home_text = models.TextField(blank=True) + automatic_logout_time = models.PositiveIntegerField(null=True) history = HistoricalRecords() class Cotisation(models.Model): diff --git a/preferences/templates/preferences/general_preferences.html b/preferences/templates/preferences/general_preferences.html index 44acc57..5c1e047 100644 --- a/preferences/templates/preferences/general_preferences.html +++ b/preferences/templates/preferences/general_preferences.html @@ -136,6 +136,14 @@
    +

    Déconnexion automatique

    +
    +
    + {{form.automatic_logout_time}} + +
    +
    +

    Texte de la page d'accueil

    diff --git a/templates/base.html b/templates/base.html index 98c4c04..b4c0ed2 100644 --- a/templates/base.html +++ b/templates/base.html @@ -44,5 +44,16 @@ {% include 'footer.html'%}
    + {% if request.user.is_authenticated %} + + {% endif %} From cf038ae4938c372ed1b882a5e4fd689c28a4ce2d Mon Sep 17 00:00:00 2001 From: Nanoy Date: Sun, 20 Jan 2019 14:08:14 +0100 Subject: [PATCH 10/15] Belles couleurs --- templates/base.html | 1 + users/templates/users/profile.html | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/templates/base.html b/templates/base.html index b4c0ed2..18222ac 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,6 +9,7 @@ + {% block extra_css %}{% endblock %} {% block extra_script %}{% endblock %} diff --git a/users/templates/users/profile.html b/users/templates/users/profile.html index 0eae992..62cf9c3 100644 --- a/users/templates/users/profile.html +++ b/users/templates/users/profile.html @@ -76,6 +76,16 @@ From 424da8261c790f6273b75823b1fc5b327833d061 Mon Sep 17 00:00:00 2001 From: Nanoy Date: Tue, 22 Jan 2019 20:27:18 +0100 Subject: [PATCH 11/15] Proprification de order et easy_cotis --- gestion/templates/gestion/manage.html | 13 ++ gestion/views.py | 224 ++++++++++++++++---------- preferences/urls.py | 3 +- preferences/views.py | 15 +- staticfiles/manage.js | 61 ++++++- templates/base.html | 1 - users/templates/users/profile.html | 2 +- users/views.py | 2 +- 8 files changed, 221 insertions(+), 100 deletions(-) diff --git a/gestion/templates/gestion/manage.html b/gestion/templates/gestion/manage.html index 0292bf4..3b4c3f0 100644 --- a/gestion/templates/gestion/manage.html +++ b/gestion/templates/gestion/manage.html @@ -115,6 +115,19 @@
    + + {% for cotisation in cotisations %} + {% if forloop.counter0|divisibleby:4 %} + + {% endif %} + + {% if forloop.counter|divisibleby:4 %} + + {% endif %} + {% endfor %} + {% if not bieresPression|divisibleby:4 %} + + {% endif %} {% for product in bieresPression %} {% if forloop.counter0|divisibleby:4 %} diff --git a/gestion/views.py b/gestion/views.py index 696da59..9bf39bc 100644 --- a/gestion/views.py +++ b/gestion/views.py @@ -7,19 +7,20 @@ from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.decorators import login_required, permission_required from django.utils import timezone from django.http import HttpResponseRedirect +from django.db import transaction + +from datetime import datetime, timedelta from django_tex.views import render_to_pdf - from coopeV3.acl import active_required, acl_or, admin_required import simplejson as json from dal import autocomplete from decimal import * -import datetime from .forms import ReloadForm, RefundForm, ProductForm, KegForm, MenuForm, GestionForm, SearchMenuForm, SearchProductForm, SelectPositiveKegForm, SelectActiveKegForm, PinteForm, GenerateReleveForm from .models import Product, Menu, Keg, ConsumptionHistory, KegHistory, Consumption, MenuHistory, Pinte, Reload, Refund -from preferences.models import PaymentMethod, GeneralPreferences +from preferences.models import PaymentMethod, GeneralPreferences, Cotisation from users.models import CotisationHistory @active_required @@ -77,6 +78,7 @@ def manage(request): menus = Menu.objects.filter(is_active=True) kegs = Keg.objects.filter(is_active=True) gp, _ = GeneralPreferences.objects.get_or_create(pk=1) + cotisations = Cotisation.objects.all() floating_buttons = gp.floating_buttons for keg in kegs: if(keg.pinte): @@ -97,6 +99,7 @@ def manage(request): "menus": menus, "pay_buttons": pay_buttons, "floating_buttons": floating_buttons, + "cotisations": cotisations }) @csrf_exempt @@ -107,90 +110,135 @@ def order(request): """ Process the given order. Called by a js/JQuery script. """ - if("user" not in request.POST or "paymentMethod" not in request.POST or "amount" not in request.POST or "order" not in request.POST): - return HttpResponse("Erreur du POST") - else: - user = get_object_or_404(User, pk=request.POST['user']) - paymentMethod = get_object_or_404(PaymentMethod, pk=request.POST['paymentMethod']) - amount = Decimal(request.POST['amount']) - order = json.loads(request.POST["order"]) - menus = json.loads(request.POST["menus"]) - listPintes = json.loads(request.POST["listPintes"]) - gp,_ = GeneralPreferences.objects.get_or_create(pk=1) - if (not order) and (not menus): - return HttpResponse("Pas de commande") - adherentRequired = False - for o in order: - product = get_object_or_404(Product, pk=o["pk"]) - adherentRequired = adherentRequired or product.adherentRequired - for m in menus: - menu = get_object_or_404(Menu, pk=m["pk"]) - adherentRequired = adherentRequired or menu.adherent_required - if(adherentRequired and not user.profile.is_adherent): - return HttpResponse("N'est pas adhérent et devrait l'être") - # Partie un peu complexe : je libère toutes les pintes de la commande, puis je test - # s'il a trop de pintes non rendues, puis je réalloue les pintes - for pinte in listPintes: - allocate(pinte, None) - if(gp.lost_pintes_allowed and user.profile.nb_pintes >= gp.lost_pintes_allowed): - return HttpResponse("Impossible de réaliser la commande : l'utilisateur a perdu trop de pintes.") - for pinte in listPintes: - allocate(pinte, user) - if(paymentMethod.affect_balance): - if(user.profile.balance < amount): - return HttpResponse("Solde inférieur au prix de la commande") + error_message = "Impossible d'effectuer la transaction. Toute opération abandonnée. Veuillez contacter le président ou le trésorier" + try: + with transaction.atomic(): + if("user" not in request.POST or "paymentMethod" not in request.POST or "amount" not in request.POST or "order" not in request.POST): + raise Exception("Erreur du post") else: - user.profile.debit += amount - user.save() - for o in order: - product = get_object_or_404(Product, pk=o["pk"]) - quantity = int(o["quantity"]) - if(product.category == Product.P_PRESSION): - keg = get_object_or_404(Keg, pinte=product) - if(not keg.is_active): - return HttpResponse("Une erreur inconnue s'est produite. Veuillez contacter le trésorier ou le président") - kegHistory = get_object_or_404(KegHistory, keg=keg, isCurrentKegHistory=True) - kegHistory.quantitySold += Decimal(quantity * 0.5) - kegHistory.amountSold += Decimal(quantity * product.amount) - kegHistory.save() - elif(product.category == Product.D_PRESSION): - keg = get_object_or_404(Keg, demi=product) - if(not keg.is_active): - return HttpResponse("Une erreur inconnue s'est produite. Veuillez contacter le trésorier ou le président") - kegHistory = get_object_or_404(KegHistory, keg=keg, isCurrentKegHistory=True) - kegHistory.quantitySold += Decimal(quantity * 0.25) - kegHistory.amountSold += Decimal(quantity * product.amount) - kegHistory.save() - elif(product.category == Product.G_PRESSION): - keg = get_object_or_404(Keg, galopin=product) - if(not keg.is_active): - return HttpResponse("Une erreur inconnue s'est produite. Veuillez contacter le trésorier ou le président") - kegHistory = get_object_or_404(KegHistory, keg=keg, isCurrentKegHistory=True) - kegHistory.quantitySold += Decimal(quantity * 0.125) - kegHistory.amountSold += Decimal(quantity * product.amount) - kegHistory.save() - else: - if(product.stockHold > 0): - product.stockHold -= 1 - product.save() - consumption, _ = Consumption.objects.get_or_create(customer=user, product=product) - consumption.quantity += quantity - consumption.save() - ch = ConsumptionHistory(customer=user, quantity=quantity, paymentMethod=paymentMethod, product=product, amount=Decimal(quantity*product.amount), coopeman=request.user) - ch.save() - for m in menus: - menu = get_object_or_404(Menu, pk=m["pk"]) - quantity = int(m["quantity"]) - mh = MenuHistory(customer=user, quantity=quantity, paymentMethod=paymentMethod, menu=menu, amount=int(quantity*menu.amount), coopeman=request.user) - mh.save() - for article in menu.articles.all(): - consumption, _ = Consumption.objects.get_or_create(customer=user, product=article) - consumption.quantity += quantity - consumption.save() - if(article.stockHold > 0): - article.stockHold -= 1 - article.save() - return HttpResponse("La commande a bien été effectuée") + user = get_object_or_404(User, pk=request.POST['user']) + paymentMethod = get_object_or_404(PaymentMethod, pk=request.POST['paymentMethod']) + amount = Decimal(request.POST['amount']) + order = json.loads(request.POST["order"]) + menus = json.loads(request.POST["menus"]) + listPintes = json.loads(request.POST["listPintes"]) + cotisations = json.loads(request.POST['cotisations']) + gp,_ = GeneralPreferences.objects.get_or_create(pk=1) + if (not order) and (not menus) and (not cotisations): + error_message = "Pas de commande" + raise Exception(error_message) + 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(paymentMethod.affect_balance): + if(user.profile.balance >= cotisation_history.cotisation.amount): + user.profile.debit += cotisation_history.cotisation.amount + else: + error_message = "Solde insuffisant" + raise Exception(error_message) + cotisation_history.user = user + cotisation_history.coopeman = request.user + cotisation_history.amount = cotisation.amount + cotisation_history.duration = cotisation.duration + cotisation_history.paymentMethod = paymentMethod + if(user.profile.cotisationEnd and user.profile.cotisationEnd > timezone.now()): + cotisation_history.endDate = user.profile.cotisationEnd + timedelta(days=cotisation.duration) + else: + cotisation_history.endDate = timezone.now() + timedelta(days=cotisation.duration) + user.profile.cotisationEnd = cotisation_history.endDate + user.save() + cotisation_history.save() + adherentRequired = False + for o in order: + product = get_object_or_404(Product, pk=o["pk"]) + adherentRequired = adherentRequired or product.adherentRequired + for m in menus: + menu = get_object_or_404(Menu, pk=m["pk"]) + adherentRequired = adherentRequired or menu.adherent_required + if(adherentRequired and not user.profile.is_adherent): + error_message = "N'est pas adhérent et devrait l'être." + raise Exception(error_message) + # Partie un peu complexe : je libère toutes les pintes de la commande, puis je test + # s'il a trop de pintes non rendues, puis je réalloue les pintes + for pinte in listPintes: + allocate(pinte, None) + if(gp.use_pinte_monitoring and gp.lost_pintes_allowed and user.profile.nb_pintes >= gp.lost_pintes_allowed): + error_message = "Impossible de réaliser la commande : l'utilisateur a perdu trop de pintes." + raise Exception(error_message) + for pinte in listPintes: + allocate(pinte, user) + if(paymentMethod.affect_balance): + if(user.profile.balance < amount): + error_message = "Solde inférieur au prix de la commande" + raise Exception(error_message) + else: + user.profile.debit += amount + user.save() + for o in order: + product = get_object_or_404(Product, pk=o["pk"]) + quantity = int(o["quantity"]) + if(product.category == Product.P_PRESSION): + keg = get_object_or_404(Keg, pinte=product) + if(not keg.is_active): + raise Exception("Fût non actif") + kegHistory = get_object_or_404(KegHistory, keg=keg, isCurrentKegHistory=True) + kegHistory.quantitySold += Decimal(quantity * 0.5) + kegHistory.amountSold += Decimal(quantity * product.amount) + kegHistory.save() + elif(product.category == Product.D_PRESSION): + keg = get_object_or_404(Keg, demi=product) + if(not keg.is_active): + raise Exception("Fût non actif") + kegHistory = get_object_or_404(KegHistory, keg=keg, isCurrentKegHistory=True) + kegHistory.quantitySold += Decimal(quantity * 0.25) + kegHistory.amountSold += Decimal(quantity * product.amount) + kegHistory.save() + elif(product.category == Product.G_PRESSION): + keg = get_object_or_404(Keg, galopin=product) + if(not keg.is_active): + raise Exception("Fût non actif") + kegHistory = get_object_or_404(KegHistory, keg=keg, isCurrentKegHistory=True) + kegHistory.quantitySold += Decimal(quantity * 0.125) + kegHistory.amountSold += Decimal(quantity * product.amount) + kegHistory.save() + else: + if(product.stockHold > 0): + product.stockHold -= 1 + product.save() + consumption, _ = Consumption.objects.get_or_create(customer=user, product=product) + consumption.quantity += quantity + consumption.save() + ch = ConsumptionHistory(customer=user, quantity=quantity, paymentMethod=paymentMethod, product=product, amount=Decimal(quantity*product.amount), coopeman=request.user) + ch.save() + if(user.profile.balance > Decimal(product.amount * quantity)): + user.profile.debit += Decimal(product.amount*quantity) + else: + error_message = "Solde insuffisant" + raise Exception(error_message) + for m in menus: + menu = get_object_or_404(Menu, pk=m["pk"]) + quantity = int(m["quantity"]) + mh = MenuHistory(customer=user, quantity=quantity, paymentMethod=paymentMethod, menu=menu, amount=int(quantity*menu.amount), coopeman=request.user) + mh.save() + if(user.profile.balance > Decimal(menu.amount * quantity)): + user.profile.debit += Decimal(menu.amount*quantity) + else: + error_message = "Solde insuffisant" + raise Exception(error_message) + for article in menu.articles.all(): + consumption, _ = Consumption.objects.get_or_create(customer=user, product=article) + consumption.quantity += quantity + consumption.save() + if(article.stockHold > 0): + article.stockHold -= 1 + article.save() + return HttpResponse("La commande a bien été effectuée") + except Exception as e: + print(e) + print("test") + return HttpResponse(error_message) @active_required @login_required @@ -899,7 +947,7 @@ def get_menu(request, pk): for article in menu.articles: if article.category == Product.P_PRESSION: nb_pintes +=1 - data = json.dumps({"pk": menu.pk, "barcode" : menu.barcode, "name": menu.name, "amount" : menu.amount, needQuantityButton: False, "nb_pintes": nb_pintes}) + data = json.dumps({"pk": menu.pk, "barcode" : menu.barcode, "name": menu.name, "amount" : menu.amount, "needQuantityButton": False, "nb_pintes": nb_pintes}) return HttpResponse(data, content_type='application/json') class MenusAutocomplete(autocomplete.Select2QuerySetView): @@ -1074,7 +1122,7 @@ def gen_releve(request): value_lydia += cot.amount elif pm == cheque: value_cheque += cot.amount - now = datetime.datetime.now() + now = datetime.now() return render_to_pdf(request, 'gestion/releve.tex', {"consumptions": consumptions, "reloads": reloads, "refunds": refunds, "cotisations": cotisations, "begin": begin, "end": end, "now": now, "value_especes": value_especes, "value_lydia": value_lydia, "value_cheque": value_cheque}, filename="releve.pdf") else: return render(request, "form.html", {"form": form, "form_title": "Génération d'un relevé", "form_button": "Générer", "form_button_icon": "file-pdf"}) diff --git a/preferences/urls.py b/preferences/urls.py index dd7d3b8..2b2bac4 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -15,4 +15,5 @@ urlpatterns = [ path('deletePaymentMethod/', views.deletePaymentMethod, name="deletePaymentMethod"), path('inactive', views.inactive, name="inactive"), path('getConfig', views.get_config, name="getConfig"), -] + path('getCotisation/', views.get_cotisation, name="getCotisation") +,] diff --git a/preferences/views.py b/preferences/views.py index 7c4f4a4..e8bae46 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -1,4 +1,4 @@ -import json +import simplejson as json from django.shortcuts import render, redirect, get_object_or_404 from django.contrib import messages @@ -135,6 +135,19 @@ def deleteCotisation(request,pk): messages.success(request, message) return redirect(reverse('preferences:cotisationsIndex')) +@active_required +@login_required +@permission_required('preferences.view_cotisation') +def get_cotisation(request, pk): + """ + Get a cotisation by pk + + ``pk`` + The primary key of the cotisation + """ + cotisation = get_object_or_404(Cotisation, pk=pk) + data = json.dumps({"pk": cotisation.pk, "duration": cotisation.duration, "amount" : cotisation.amount, "needQuantityButton": False}) + return HttpResponse(data, content_type='application/json') ########## Payment Methods ########## diff --git a/staticfiles/manage.js b/staticfiles/manage.js index c4c176f..a85f2cc 100644 --- a/staticfiles/manage.js +++ b/staticfiles/manage.js @@ -1,10 +1,11 @@ total = 0 products = [] menus = [] +cotisations = [] paymentMethod = null balance = 0 username = "" -id = 0 +id_user = 0 listPintes = [] nbPintes = 0; use_pinte_monitoring = false; @@ -29,9 +30,15 @@ function get_menu(id){ }); } +function get_cotisation(id){ + res = $.get("../preferences/getCotisation/" + id, function(data){ + add_cotisation(data.pk, "", data.duration, data.amount, data.needQuantityButton); + }); +} + function add_product(pk, barcode, name, amount, needQuantityButton){ exist = false - index = -1 + index = -1; for(k=0;k < products.length; k++){ if(products[k].pk == pk){ exist = true @@ -71,15 +78,36 @@ function add_menu(pk, barcode, name, amount){ generate_html(); } +function add_cotisation(pk, barcode, duration, amount){ + exist = false; + index = -1; + for(k=0; k < cotisations.length; k++){ + if(cotisations[k].pk == pk){ + exist = true; + index = k; + } + } + if(exist){ + cotisations[index].quantity += 1; + }else{ + cotisations.push({"pk": pk, "barcode": barcode, "duration": duration, "amount": amount, "quantity":1}); + } + generate_html(); +} + function generate_html(){ html = ""; + for(k=0;k'; + } for(k=0;k'; + html += ''; } for(k=0; k'; + html += ''; } $("#items").html(html) updateTotal(); @@ -93,6 +121,9 @@ function updateTotal(){ for(k=0; k + @@ -47,6 +48,7 @@ + @@ -78,6 +80,7 @@ + @@ -90,6 +93,7 @@ + diff --git a/templates/home.html b/templates/home.html index b05a045..4e8a3ff 100644 --- a/templates/home.html +++ b/templates/home.html @@ -23,7 +23,7 @@ Les bières pressions actuellement en Coopé :
      {% for keg in kegs %} -
    • {{keg}} ({% if keg.pinte %} Pinte : {{keg.pinte.amount}}€,{% endif %}{% if keg.demi %} Demi : {{keg.demi.amount}}€,{% endif %}{% if keg.galopin %} Galopin : {{keg.galopin.amount}}€{% endif %})
    • +
    • {{keg}} ({% if keg.pinte %} Pinte : {{keg.pinte.amount}}€,{% endif %}{% if keg.demi %} Demi : {{keg.demi.amount}}€,{% endif %}{% if keg.galopin %} Galopin : {{keg.galopin.amount}}€{% endif %}) : {{keg.pinte.deg}}°
    • {% endfor %}
    From a28d9660dd8e09f0751585c139b2692aed68f7a5 Mon Sep 17 00:00:00 2001 From: Nanoy Date: Wed, 23 Jan 2019 10:42:54 +0100 Subject: [PATCH 13/15] Fix invalidation --- users/templates/users/profile.html | 1 - users/views.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/users/templates/users/profile.html b/users/templates/users/profile.html index 27b9b26..fcdb7bf 100644 --- a/users/templates/users/profile.html +++ b/users/templates/users/profile.html @@ -80,7 +80,6 @@ console.log(baseColor) $.get("http://www.thecolorapi.com/scheme?rgb=" + baseColor + "&mode=analogic&count={{products | length}}", function( data ) { colors = data.colors - console.log(colors) var bgColor = [] for(var i = 0; i < colors.length; i++){ color = colors[i] diff --git a/users/views.py b/users/views.py index 71f59ae..8777b55 100644 --- a/users/views.py +++ b/users/views.py @@ -922,7 +922,7 @@ def invalidateCotisationHistory(request, pk): user = cotisationHistory.user user.profile.cotisationEnd = user.profile.cotisationEnd - timedelta(days=cotisationHistory.duration) if(cotisationHistory.paymentMethod.affect_balance): - user.profile.balance += cotisation.amount + user.profile.debit -= cotisationHistory.cotisation.amount user.save() messages.success(request, "La cotisation a bien été invalidée") return HttpResponseRedirect(request.META.get('HTTP_REFERER')) From dac7a8312b432e8078d5af3f3be15ecc8ecdc020 Mon Sep 17 00:00:00 2001 From: Nanoy Date: Wed, 23 Jan 2019 10:49:33 +0100 Subject: [PATCH 14/15] startswith devient contains --- gestion/views.py | 10 +++++----- users/views.py | 13 +++++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/gestion/views.py b/gestion/views.py index 9bf39bc..fe76b11 100644 --- a/gestion/views.py +++ b/gestion/views.py @@ -532,7 +532,7 @@ class ProductsAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): qs = Product.objects.all() if self.q: - qs = qs.filter(name__istartswith=self.q) + qs = qs.filter(name__contains=self.q) return qs class ActiveProductsAutocomplete(autocomplete.Select2QuerySetView): @@ -542,7 +542,7 @@ class ActiveProductsAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): qs = Product.objects.filter(is_active=True) if self.q: - qs = qs.filter(name__istartswith=self.q) + qs = qs.filter(name__contains=self.q) return qs ########## Kegs ########## @@ -790,7 +790,7 @@ class KegActiveAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): qs = Keg.objects.filter(is_active = True) if self.q: - qs = qs.filter(name__istartswith=self.q) + qs = qs.filter(name__contains=self.q) return qs class KegPositiveAutocomplete(autocomplete.Select2QuerySetView): @@ -800,7 +800,7 @@ class KegPositiveAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): qs = Keg.objects.filter(stockHold__gt = 0) if self.q: - qs = qs.filter(name__istartswith=self.q) + qs = qs.filter(name__contains=self.q) return qs ########## Menus ########## @@ -957,7 +957,7 @@ class MenusAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): qs = Menu.objects.all() if self.q: - qs = qs.filter(name__istartswith=self.q) + qs = qs.filter(name__contains=self.q) return qs ########## Ranking ########## diff --git a/users/views.py b/users/views.py index 8777b55..a2c3a0c 100644 --- a/users/views.py +++ b/users/views.py @@ -1079,7 +1079,7 @@ class AllUsersAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): qs = User.objects.all() if self.q: - qs = qs.filter(Q(username__istartswith=self.q) | Q(first_name__istartswith=self.q) | Q(last_name__istartswith=self.q)) + qs = qs.filter(Q(username__contains=self.q) | Q(first_name__contains=self.q) | Q(last_name__contains=self.q)) return qs class ActiveUsersAutocomplete(autocomplete.Select2QuerySetView): @@ -1089,7 +1089,7 @@ class ActiveUsersAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): qs = User.objects.filter(is_active=True) if self.q: - qs = qs.filter(Q(username__istartswith=self.q) | Q(first_name__istartswith=self.q) | Q(last_name__istartswith=self.q)) + qs = qs.filter(Q(username__contains=self.q) | Q(first_name__contains=self.q) | Q(last_name__contains=self.q)) return qs class AdherentAutocomplete(autocomplete.Select2QuerySetView): @@ -1098,8 +1098,13 @@ class AdherentAutocomplete(autocomplete.Select2QuerySetView): """ def get_queryset(self): qs = User.objects.all() + pks = [x.pk for x in qs if x.is_adherent] + qs = User.objects.filter(pk__in=pks) + if self.q: + qs = qs.filter(Q(username__contains=self.q) | Q(first_name__contains=self.q) | Q(last_name__contains=self.q)) return qs + class NonSuperUserAutocomplete(autocomplete.Select2QuerySetView): """ Autocomplete for non-superuser users @@ -1107,7 +1112,7 @@ class NonSuperUserAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): qs = User.objects.filter(is_superuser=False) if self.q: - qs = qs.filter(Q(username__istartswith=self.q) | Q(first_name__istartswith=self.q) | Q(last_name__istartswith=self.q)) + qs = qs.filter(Q(username__contains=self.q) | Q(first_name__contains=self.q) | Q(last_name__contains=self.q)) return qs class NonAdminUserAutocomplete(autocomplete.Select2QuerySetView): @@ -1117,5 +1122,5 @@ class NonAdminUserAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): qs = User.objects.filter(is_staff=False) if self.q: - qs = qs.filter(Q(username__istartswith=self.q) | Q(first_name__istartswith=self.q) | Q(last_name__istartswith=self.q)) + qs = qs.filter(Q(username__contains=self.q) | Q(first_name__contains=self.q) | Q(last_name__contains=self.q)) return qs \ No newline at end of file From a64fc2bb2843fd48f333f35da1fc368dccd948d4 Mon Sep 17 00:00:00 2001 From: Nanoy Date: Wed, 23 Jan 2019 10:57:00 +0100 Subject: [PATCH 15/15] Informations de version --- CHANGELOG.md | 12 ++++++++++++ templates/footer.html | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 006f2e1..734e76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## v3.3.0 +* Ajout d'icônes +* Le . est utilisé pour les décimaux +* Ajout de liens vers les profils de produits et utilisateurs +* Ajout de cotisations dans les transactions +* Ajout d'une page d'accueil. Les pressions du moment y sont affichées +* Belles couleurs sur le diagramme +* Verouillage automatique de la caisse +* Classement par produit +* Fix invalidation +* Recherche plus intuitive (le startswith devient contains) +* Easter egg sur 404 ## v3.2.2 * Fix cotisation cancer ## v3.2.1 diff --git a/templates/footer.html b/templates/footer.html index 608943c..cca478e 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -39,6 +39,6 @@
  • Facebook
  • - +
    Cotisations
    Bières pression
    ' + String(cotisation.amount) + ' €' + String(Number((cotisation.quantity * cotisation.amount).toFixed(2))) + ' €
    ' + product.name + '' + String(product.amount) + '' + String(Number((product.quantity * product.amount).toFixed(2))) + '
    ' + product.barcode + '' + product.name + '' + String(product.amount) + ' €' + String(Number((product.quantity * product.amount).toFixed(2))) + ' €
    ' + menu.name + '' + String(menu.amount) + '' + String(Number((menu.quantity * menu.amount).toFixed(2))) + '
    ' + menu.barcode + '' + menu.name + '' + String(menu.amount) + ' €' + String(Number((menu.quantity * menu.amount).toFixed(2))) + ' €
    Quantité vendue Montant vendu Prix du fûtDegré Historique Administrer
    {{ kegH.quantitySold }} L {{ kegH.amountSold }} € {{ kegH.keg.amount }} €{{ kegH.keg.pinte.deg }}° Voir {% if perms.gestion.close_keg %} Fermer {% endif %}{% if perms.gestion.change_keg %} Modifier{% endif %}
    Code barre Capacité Prix du fûtDegré Historique Administrer
    {{ keg.barcode }} {{ keg.capacity }} L {{ keg.amount }} €{{ keg.pinte.deg }}° Voir {% if perms.gestion.open_keg %}{% if keg.stockHold > 0 %} Percuter {% endif %}{% endif %}{% if perms.gestion.change_keg %} Modifier{% endif %}