From e533966f55582eea9977f6861821da3597c856a8 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 20 May 2020 17:44:07 +0200 Subject: [PATCH 1/4] Add abv and volume to Article model --- kfet/forms.py | 4 +++ kfet/migrations/0072_auto_20200515_1747.py | 33 ++++++++++++++++++++++ kfet/models.py | 7 +++++ 3 files changed, 44 insertions(+) create mode 100644 kfet/migrations/0072_auto_20200515_1747.py diff --git a/kfet/forms.py b/kfet/forms.py index 9419d9f8..cca3f8c5 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -288,6 +288,8 @@ class ArticleForm(forms.ModelForm): "hidden", "price", "stock", + "abv", + "volume", "category", "box_type", "box_capacity", @@ -301,6 +303,8 @@ class ArticleRestrictForm(ArticleForm): "is_sold", "hidden", "price", + "abv", + "volume", "category", "box_type", "box_capacity", diff --git a/kfet/migrations/0072_auto_20200515_1747.py b/kfet/migrations/0072_auto_20200515_1747.py new file mode 100644 index 00000000..7dbe2a95 --- /dev/null +++ b/kfet/migrations/0072_auto_20200515_1747.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.12 on 2020-05-15 15:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("kfet", "0071_promo_2020"), + ] + + operations = [ + migrations.AddField( + model_name="article", + name="abv", + field=models.DecimalField( + decimal_places=1, + default=0, + max_digits=6, + verbose_name="Degré d'alcool (en %)", + ), + ), + migrations.AddField( + model_name="article", + name="volume", + field=models.DecimalField( + decimal_places=1, + default=0, + max_digits=6, + verbose_name="Volume d'une unité (en cL)", + ), + ), + ] diff --git a/kfet/models.py b/kfet/models.py index 2eacf06f..cee80705 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -520,6 +520,13 @@ class Article(models.Model): "capacité du contenant", blank=True, null=True, default=None ) + abv = models.DecimalField( + _("Degré d'alcool (en %)"), max_digits=6, default=0, decimal_places=1 + ) + volume = models.DecimalField( + _("Volume d'une unité (en cL)"), max_digits=6, default=0, decimal_places=1 + ) + def __str__(self): return "%s - %s" % (self.category.name, self.name) -- 2.45.1 From 39a2d309b5aa52f09866fe393b539d598f68ad4f Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 20 May 2020 17:44:24 +0200 Subject: [PATCH 2/4] Fetch and display if drunk --- kfet/static/kfet/css/kpsul.css | 8 ++++++++ kfet/static/kfet/js/account.js | 16 ++++++++++++++++ kfet/templates/kfet/base.html | 1 + kfet/templates/kfet/kpsul.html | 1 + kfet/views.py | 26 +++++++++++++++++++++++++- 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/kfet/static/kfet/css/kpsul.css b/kfet/static/kfet/css/kpsul.css index 1616ce3e..166ef5f5 100644 --- a/kfet/static/kfet/css/kpsul.css +++ b/kfet/static/kfet/css/kpsul.css @@ -112,6 +112,14 @@ input[type=number]::-webkit-outer-spin-button { right:0; } +#account #alcohol-warning { + padding: 5px; + position: absolute; + top:0; + right:0; + font-size:35px; +} + @media (min-width: 600px) { #account_form input { font-size:60px; } diff --git a/kfet/static/kfet/js/account.js b/kfet/static/kfet/js/account.js index 5ce3c8cd..cf1e06dc 100644 --- a/kfet/static/kfet/js/account.js +++ b/kfet/static/kfet/js/account.js @@ -11,6 +11,7 @@ var Account = Backbone.Model.extend({ 'departement': '', 'nickname': '', 'trigramme': '', + 'n_units': 0, }, url: function () { @@ -45,6 +46,7 @@ var AccountView = Backbone.View.extend({ el: '#account', input: '#id_trigramme', buttons: '.buttons', + alcohol_warning: '#alcohol-warning', props: _.keys(Account.prototype.defaults), @@ -98,6 +100,15 @@ var AccountView = Backbone.View.extend({ return buttons }, + get_alcohol_warning: function () { + n_units = parseFloat(this.model.get("n_units")).toFixed(2); + + if (n_units < 4) + return ""; + + return `` + }, + render: function () { for (let prop of this.props) { var selector = "#account-" + prop; @@ -106,6 +117,7 @@ var AccountView = Backbone.View.extend({ this.$el.attr("data-balance", this.attr_data_balance()); this.$(this.buttons).html(this.get_buttons()); + this.$(this.alcohol_warning).html(this.get_alcohol_warning()); }, reset: function () { @@ -124,6 +136,10 @@ var LIQView = AccountView.extend({ return ""; }, + get_alcohol_warning: function () { + return "" + }, + attr_data_balance: function () { return 'ok'; } diff --git a/kfet/templates/kfet/base.html b/kfet/templates/kfet/base.html index 9b75af03..40b3b51b 100644 --- a/kfet/templates/kfet/base.html +++ b/kfet/templates/kfet/base.html @@ -17,6 +17,7 @@ + {# JS #} diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 7b292087..5660e70b 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -86,6 +86,7 @@
+
diff --git a/kfet/views.py b/kfet/views.py index b6c49f72..602f800e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2,7 +2,7 @@ import ast import heapq import statistics from collections import defaultdict -from datetime import timedelta +from datetime import datetime, time, timedelta from decimal import Decimal from typing import List from urllib.parse import urlencode @@ -916,6 +916,29 @@ def account_read_json(request, trigramme): account = get_object_or_404(Account, trigramme=trigramme) if not account.readable: raise Http404 + + # Calcul des unités d'alcool consommées + # 1UA = 10g d'alcool (éthanol) pur + alcohol_density = 0.8 + now = timezone.localtime(timezone.now()) + # une soirée va de XXh à 06h + if time(18) <= now.time() or now.time() <= time(6): + begin_time = datetime.combine(now.date(), time(18)) + # si on est après minuit, il faut retrancher un jour + if now.time() <= time(6): + begin_time -= timedelta(days=1) + + qs = Operation.objects.filter( + group__on_acc=account, type=Operation.PURCHASE, group__at__gte=begin_time, + ).annotate( + units=F("article__volume") * F("article__abv") * alcohol_density / 100 + ) + # Sum retourne None sur un queryset vide : + # https://docs.djangoproject.com/en/3.0/ref/models/querysets/#aggregation-functions + n_units = qs.aggregate(agg=Sum("units"))["agg"] or 0 + else: + n_units = 0 + data = { "id": account.pk, "name": account.name, @@ -927,6 +950,7 @@ def account_read_json(request, trigramme): "departement": account.departement, "nickname": account.nickname, "trigramme": account.trigramme, + "n_units": n_units, } return JsonResponse(data) -- 2.45.1 From 55f22f2250aefcc629a8a246844bed02d8c646b4 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 20 May 2020 17:48:47 +0200 Subject: [PATCH 3/4] Change party start to 21h --- kfet/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 602f800e..f9d39885 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -922,8 +922,8 @@ def account_read_json(request, trigramme): alcohol_density = 0.8 now = timezone.localtime(timezone.now()) # une soirée va de XXh à 06h - if time(18) <= now.time() or now.time() <= time(6): - begin_time = datetime.combine(now.date(), time(18)) + if time(21) <= now.time() or now.time() <= time(6): + begin_time = datetime.combine(now.date(), time(21)) # si on est après minuit, il faut retrancher un jour if now.time() <= time(6): begin_time -= timedelta(days=1) -- 2.45.1 From 60831045b54de0c186a096fe212685e94d180ad0 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 10 Jun 2020 22:23:03 +0200 Subject: [PATCH 4/4] Tests + timezone fix --- kfet/tests/test_views.py | 97 +++++++++++++++++++++++++++++++++++++++- kfet/views.py | 6 ++- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index bcd9a9b4..dbf90b3b 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -1,5 +1,5 @@ import json -from datetime import datetime, timedelta +from datetime import timedelta from decimal import Decimal from unittest import mock @@ -1365,6 +1365,8 @@ class ArticleCreateViewTests(ViewTestCaseMixin, TestCase): "category": self.category.pk, "stock": 5, "price": "2.5", + "volume": "33", + "abv": "6.3", } def setUp(self): @@ -1451,6 +1453,8 @@ class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase): "price": "3.5", "box_type": "carton", # 'hidden': not checked + "volume": "33", + "abv": "6.3", } def setUp(self): @@ -4220,6 +4224,74 @@ class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase): def url_expected(self): return "/k-fet/accounts/{}/.json".format(self.accounts["user"].trigramme) + def setUp(self): + super().setUp() + + # A Checkout, curently usable, balance=100 + self.checkout = Checkout.objects.create( + created_by=self.accounts["team"], + name="Checkout", + valid_from=self.now - timedelta(days=7), + valid_to=self.now + timedelta(days=7), + balance=Decimal("100.00"), + ) + # An Article, price=2.5, stock=20 + self.article = Article.objects.create( + category=ArticleCategory.objects.create(name="Category"), + name="Article", + price=Decimal("2.5"), + stock=20, + volume=Decimal("33"), + abv=Decimal("7.3"), + ) + + now = timezone.localtime(self.now) + + ope_party1 = OperationGroup.objects.create( + on_acc=self.accounts["user"], + checkout=self.checkout, + at=now.replace(hour=1), + ) + + ope_party2 = OperationGroup.objects.create( + on_acc=self.accounts["user"], + checkout=self.checkout, + at=now.replace(day=now.day - 1, hour=22), + ) + + ope_noparty = OperationGroup.objects.create( + on_acc=self.accounts["user"], + checkout=self.checkout, + at=now.replace(day=now.day - 1, hour=19), + ) + + Operation.objects.create( + group=ope_party1, + type=Operation.PURCHASE, + article=self.article, + article_nb=2, + ) + + Operation.objects.create( + group=ope_party2, + type=Operation.PURCHASE, + article=self.article, + article_nb=1, + ) + Operation.objects.create( + group=ope_party2, + type=Operation.PURCHASE, + article=self.article, + article_nb=1, + ) + + Operation.objects.create( + group=ope_noparty, + type=Operation.PURCHASE, + article=self.article, + article_nb=3, + ) + def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) @@ -4243,10 +4315,33 @@ class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase): "nickname", "promo", "trigramme", + "n_units", ] ), ) + @mock.patch("django.utils.timezone.now") + def test_without_party(self, mock_now): + now = timezone.localtime(self.now) + mock_now.return_value = now.replace(hour=20).astimezone(timezone.utc) + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + content = json.loads(r.content.decode("utf-8")) + + self.assertEqual(content["n_units"], 0) + + @mock.patch("django.utils.timezone.now") + def test_with_party(self, mock_now): + now = timezone.localtime(self.now) + mock_now.return_value = now.replace(hour=5).astimezone(timezone.utc) + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + content = json.loads(r.content.decode("utf-8")) + + self.assertAlmostEqual(float(content["n_units"]), 3 * 33 * 7.3 * 0.8 / 100) + class SettingsListViewTests(ViewTestCaseMixin, TestCase): url_name = "kfet.settings" diff --git a/kfet/views.py b/kfet/views.py index f9d39885..18e442f2 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -923,13 +923,15 @@ def account_read_json(request, trigramme): now = timezone.localtime(timezone.now()) # une soirée va de XXh à 06h if time(21) <= now.time() or now.time() <= time(6): - begin_time = datetime.combine(now.date(), time(21)) + begin_time = now.replace(hour=21, minute=0, second=0, microsecond=0) # si on est après minuit, il faut retrancher un jour if now.time() <= time(6): begin_time -= timedelta(days=1) qs = Operation.objects.filter( - group__on_acc=account, type=Operation.PURCHASE, group__at__gte=begin_time, + group__on_acc=account, + type=Operation.PURCHASE, + group__at__gte=begin_time.astimezone(timezone.utc), ).annotate( units=F("article__volume") * F("article__abv") * alcohol_density / 100 ) -- 2.45.1