diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 078ab2f0..07fab727 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -1,6 +1,6 @@ name: Black formatter check -on: [push] +on: [push, pull_request] jobs: black: diff --git a/media/stregsystem/background.jpg b/media/stregsystem/background.jpg new file mode 100644 index 00000000..9662d30a Binary files /dev/null and b/media/stregsystem/background.jpg differ diff --git a/media/stregsystem/easter.jpg b/media/stregsystem/easter.jpg new file mode 100644 index 00000000..d091fc46 Binary files /dev/null and b/media/stregsystem/easter.jpg differ diff --git a/requirements.txt b/requirements.txt index e00317ad..4dce3e50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ Django==2.2.24 Pillow==8.3.2 Coverage==4.4.1 -pytz==2018.3 +pytz==2021.3 regex==2017.07.28 freezegun==0.3.15 Django-Select2==5.11.1 diff --git a/stregreport/views.py b/stregreport/views.py index 28992de0..eef48585 100644 --- a/stregreport/views.py +++ b/stregreport/views.py @@ -301,7 +301,7 @@ def ranks_for_year(request, year): 1859, ] coffee = [32, 35, 36, 39] - vitamin = [1850, 1851, 1852, 1863] + vitamin = [1850, 1851, 1852, 1863, 1880] FORMAT = '%d/%m/%Y kl. %H:%M' last_year = year - 1 diff --git a/stregsystem/admin.py b/stregsystem/admin.py index ae718c72..c873d7ec 100644 --- a/stregsystem/admin.py +++ b/stregsystem/admin.py @@ -129,6 +129,7 @@ class ProductAdmin(admin.ModelAdmin): "categories", "rooms", "alcohol_content_ml", + "caffeine_content_mg", ) readonly_fields = ("get_bought",) diff --git a/stregsystem/caffeine.py b/stregsystem/caffeine.py new file mode 100644 index 00000000..589a14e1 --- /dev/null +++ b/stregsystem/caffeine.py @@ -0,0 +1,56 @@ +from datetime import datetime, timedelta +from typing import List + +from django.utils import timezone + +CAFFEINE_IN_COFFEE = 70 +CAFFEINE_DEGRADATION_PR_HOUR = 0.12945 +CAFFEINE_TIME_INTERVAL = timedelta(days=1) + + +class Intake: + timestamp: datetime + mg: int + + def __init__(self, timestamp: datetime, mg: int): + self.timestamp = timestamp + self.mg = mg + + +def caffeine_mg_to_coffee_cups(mg: int) -> int: + return int(mg / CAFFEINE_IN_COFFEE) + + +# calculate current caffeine in body, takes list of intakes, applies caffeine degradation by using compound interest +def current_caffeine_in_body_compound_interest(intakes: List[Intake]) -> float: + """ + Given a list of Intakes (timestamp, mg), calculate caffeine mg content in blood at current time. + Assumes a bioavailability of 100%, immediate absorption in body, and caffeine half-life of 5 hours denoted by + CAFFEINE_DEGRADATION_PR_HOUR. + """ + # if no intakes withing given time interval, return 0mg in blood + if len(intakes) == 0: + return 0 + + # init last intake time to 24 hours and one minute ago to keep degradation within scope of intakes + last_intake_time = timezone.now() - timedelta(days=1, minutes=1) + mg_blood = 0 + + # append intake of 0 mg at timezone.now to intakes to calculate caffeine degradation from the latest intake to now + intakes.append(Intake(timezone.now(), 0)) + + # do compound interest on list of intakes + for intake in intakes: + # first do degradation of current caffeine in blood using compound rule (kn = k0 * (1 + r)^n), maxing to 0 + mg_blood = max( + mg_blood + * ((1 - CAFFEINE_DEGRADATION_PR_HOUR) ** ((intake.timestamp - last_intake_time) / timedelta(hours=1))), + 0, + ) + # swap curr timestamp with last intake time to calculate degradation timespan in next iteration + last_intake_time = intake.timestamp + + # finally, add current intake of caffeine to mg_blood + mg_blood += intake.mg + + return mg_blood diff --git a/stregsystem/fixtures/testdata.json b/stregsystem/fixtures/testdata.json index 4c813f8d..8cfbcb63 100644 --- a/stregsystem/fixtures/testdata.json +++ b/stregsystem/fixtures/testdata.json @@ -16,6 +16,13 @@ "notes": "This is a test user." } }, + { + "fields": { + "name": "kaffe" + }, + "model": "stregsystem.category", + "pk": "6" + }, { "model": "stregsystem.product", "pk": 1, @@ -28,12 +35,14 @@ }, { "model": "stregsystem.product", - "pk": 2, + "pk": 32, "fields": { "name": "Kold Kaffe", "price": 200, "active": true, - "deactivate_date": null + "deactivate_date": null, + "caffeine_content_mg": 70, + "categories": [6] } }, { @@ -72,7 +81,7 @@ "model": "stregsystem.oldprice", "pk": 2, "fields": { - "product": 2, + "product": 32, "price": 200, "changed_on": "2017-03-13T12:43:40.784+00:00" } @@ -81,7 +90,7 @@ "model": "stregsystem.oldprice", "pk": 3, "fields": { - "product": 2, + "product": 32, "price": 200, "changed_on": "2017-03-13T12:43:42.151+00:00" } @@ -118,7 +127,7 @@ "pk": 2, "fields": { "member": 1, - "product": 2, + "product": 32, "room": 1, "timestamp": "2017-03-13T12:54:12.423+00:00", "price": 200 diff --git a/stregsystem/forms.py b/stregsystem/forms.py index 709257b9..e33515eb 100644 --- a/stregsystem/forms.py +++ b/stregsystem/forms.py @@ -1,3 +1,5 @@ +import datetime + from django import forms from stregsystem.models import MobilePayment, Member @@ -31,3 +33,18 @@ class QRPaymentForm(forms.Form): class PurchaseForm(forms.Form): product_id = forms.IntegerField() + + +class RankingDateForm(forms.Form): + from_date = forms.DateField(widget=forms.SelectDateWidget(years=range(2000, datetime.date.today().year + 1))) + to_date = forms.DateField( + initial=datetime.date.today(), widget=forms.SelectDateWidget(years=range(2000, datetime.date.today().year + 1)) + ) + + # validate form. make sure that from_date is before to_date + def clean(self): + cleaned_data = super().clean() + if cleaned_data['from_date'] > cleaned_data['to_date']: + # raise forms.ValidationError('Fra dato skal være før eller lig til dato') + self.add_error('to_date', 'Fra dato skal være før eller lig til dato') + return cleaned_data diff --git a/stregsystem/migrations/0015_product_caffeine_content_mg.py b/stregsystem/migrations/0015_product_caffeine_content_mg.py new file mode 100644 index 00000000..93163ad8 --- /dev/null +++ b/stregsystem/migrations/0015_product_caffeine_content_mg.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2021-11-30 12:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stregsystem', '0014_mobilepayment_nullable_customername_20210908_1522'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='caffeine_content_mg', + field=models.IntegerField(default=0), + ), + ] diff --git a/stregsystem/models.py b/stregsystem/models.py index 9452b066..b4bed2c8 100644 --- a/stregsystem/models.py +++ b/stregsystem/models.py @@ -1,3 +1,4 @@ +import datetime from collections import Counter from email.utils import parseaddr @@ -8,6 +9,7 @@ from django.db.models import Count from django.utils import timezone +from stregsystem.caffeine import Intake, CAFFEINE_TIME_INTERVAL, current_caffeine_in_body_compound_interest from stregsystem.deprecated import deprecated from stregsystem.templatetags.stregsystem_extras import money from stregsystem.utils import ( @@ -287,6 +289,35 @@ def calculate_alcohol_promille(self): return bac + def calculate_caffeine_in_body(self) -> float: + # get list of last 24h caffeine intakes and calculate current body caffeine content + return current_caffeine_in_body_compound_interest( + [ + Intake(x.timestamp, x.product.caffeine_content_mg) + for x in self.sale_set.filter( + timestamp__gt=timezone.now() - CAFFEINE_TIME_INTERVAL, product__caffeine_content_mg__gt=0 + ).order_by('timestamp') + ] + ) + + def is_leading_coffee_addict(self): + coffee_category = [6] + + now = timezone.now() + start_of_week = now - datetime.timedelta(days=now.weekday()) - datetime.timedelta(hours=now.hour) + user_with_most_coffees_bought = ( + Member.objects.filter( + sale__timestamp__gt=start_of_week, + sale__timestamp__lte=now, + sale__product__categories__in=coffee_category, + ) + .annotate(Count('sale')) + .order_by('-sale__count', 'username') + .first() + ) + + return user_with_most_coffees_bought == self + class Payment(models.Model): # id automatisk... class Meta: @@ -311,7 +342,8 @@ def __unicode__(self): def __str__(self): return self.member.username + " " + str(self.timestamp) + ": " + money(self.amount) - def save(self, *args, **kwargs): + @transaction.atomic + def save(self, mbpayment=None, *args, **kwargs): if self.id: return # update -- should not be allowed else: @@ -321,7 +353,7 @@ def save(self, *args, **kwargs): self.member.save() if self.member.email != "" and self.amount != 0: if '@' in parseaddr(self.member.email)[1] and self.member.want_spam: - send_payment_mail(self.member, self.amount) + send_payment_mail(self.member, self.amount, mbpayment.comment if mbpayment else None) def log_from_mobile_payment(self, processed_mobile_payment, admin_user: User): LogEntry.objects.log_action( @@ -447,7 +479,7 @@ def process_submitted_mobile_payments(submitted_data, admin_user: User): payment = Payment(member=member, amount=payment_amount) payment.log_from_mobile_payment(processed_mobile_payment, admin_user) - payment.save() + payment.save(mbpayment=processed_mobile_payment) processed_mobile_payment.payment = payment processed_mobile_payment.member = member @@ -517,6 +549,7 @@ class Product(models.Model): # id automatisk... categories = models.ManyToManyField(Category, blank=True) rooms = models.ManyToManyField(Room, blank=True) alcohol_content_ml = models.FloatField(default=0.0, null=True) + caffeine_content_mg = models.IntegerField(default=0) @deprecated def __unicode__(self): diff --git a/stregsystem/static/stregsystem/beerflake.gif b/stregsystem/static/stregsystem/beerflake.gif new file mode 100644 index 00000000..af7de299 Binary files /dev/null and b/stregsystem/static/stregsystem/beerflake.gif differ diff --git a/stregsystem/static/stregsystem/beerflakeCursed.gif b/stregsystem/static/stregsystem/beerflakeCursed.gif new file mode 100644 index 00000000..b42f966a Binary files /dev/null and b/stregsystem/static/stregsystem/beerflakeCursed.gif differ diff --git a/stregsystem/static/stregsystem/easter.css b/stregsystem/static/stregsystem/easter.css new file mode 100644 index 00000000..f3b1167a --- /dev/null +++ b/stregsystem/static/stregsystem/easter.css @@ -0,0 +1,208 @@ +.kylle{ + position: fixed; + top: 72%; + right:-42%; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + animation-name: play; + animation-duration: 60s; + animation-timing-function: linear; + animation-iteration-count: infinite; + animation-play-state: running; +} + +@keyframes play { + 0%,80% {right:-80%;} + 100%{right:180%;} +} +.easter-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; +} + +@keyframes beerflakes-fall { + 0% { + top: -10% + } + 100% { + top: 100% + } +} + +@keyframes beerflakes-shake { + 0% { + transform: translateX(0px) + } + 50% { + transform: translateX(80px) + } + 100% { + transform: translateX(0px) + } +} + +.beerflake { + position: fixed; + top: -30%; + background: transparent; + cursor: default; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + animation-name: beerflakes-fall, beerflakes-shake; + animation-duration: 10s, 3s; + animation-timing-function: linear, ease-in-out; + animation-iteration-count: infinite, infinite; + animation-play-state: running, running; +} + +.beerflake:nth-of-type(0) { + left: 1%; + -webkit-animation-delay: 0s, 0s; + animation-delay: 0s, 0s +} + +.beerflake:nth-of-type(1) { + left: 10%; + -webkit-animation-delay: 1s, 1s; + animation-delay: 1s, 1s +} + +.beerflake:nth-of-type(2) { + left: 20%; + -webkit-animation-delay: 6s, .5s; + animation-delay: 6s, .5s +} + +.beerflake:nth-of-type(3) { + left: 30%; + -webkit-animation-delay: 4s, 2s; + animation-delay: 4s, 2s +} + +.beerflake:nth-of-type(4) { + left: 40%; + -webkit-animation-delay: 2s, 2s; + animation-delay: 2s, 2s +} + +.beerflake:nth-of-type(5) { + left: 50%; + -webkit-animation-delay: 8s, 3s; + animation-delay: 8s, 3s +} + +.beerflake:nth-of-type(6) { + left: 60%; + -webkit-animation-delay: 6s, 2s; + animation-delay: 6s, 2s +} + +.beerflake:nth-of-type(7) { + left: 70%; + -webkit-animation-delay: 2.5s, 1s; + animation-delay: 2.5s, 1s +} + +.beerflake:nth-of-type(8) { + left: 80%; + -webkit-animation-delay: 1s, 0s; + animation-delay: 1s, 0s +} + +.beerflake:nth-of-type(9) { + left: 90%; + -webkit-animation-delay: 7s, 4s; + animation-delay: 2.5s, 5.77s +} + +.beerflake:nth-of-type(10) { + left: 5%; + -webkit-animation-delay: 5.69s, 7.5s; + animation-delay: 7s, 5.77s +} + +.beerflake:nth-of-type(11) { + left: 15%; + -webkit-animation-delay: 12s, 12.9s; + animation-delay: 17s, 7.5s +} + +.beerflake:nth-of-type(12) { + left: 25%; + -webkit-animation-delay: 5.69s, 4s; + animation-delay: 2.5s, 4s +} + +.beerflake:nth-of-type(13) { + left: 35%; + -webkit-animation-delay: 7s, 6s; + animation-delay: 7s, 8s +} + +.beerflake:nth-of-type(14) { + left: 45%; + -webkit-animation-delay: 4s, 12.9s; + animation-delay: 5.69s, 7.5s +} + +.beerflake:nth-of-type(15) { + left: 55%; + -webkit-animation-delay: 2.5s, 1s; + animation-delay: 7s, 4s +} + +.beerflake:nth-of-type(16) { + left: 65%; + -webkit-animation-delay: 8s, 6s; + animation-delay: 2.5s, 3.2s +} + +.beerflake:nth-of-type(17) { + left: 75%; + -webkit-animation-delay: 7s, 2.5s; + animation-delay: 2.5s, 7.5s +} + +.beerflake:nth-of-type(18) { + left: 85%; + -webkit-animation-delay: 8.5s, 12.9s; + animation-delay: 5.69s, 4s +} + +.beerflake:nth-of-type(19) { + left: 95%; + -webkit-animation-delay: 2.5s, 6s; + animation-delay: 4s, 12.9s +} + +.beerflake:nth-of-type(20) { + left: 3%; + -webkit-animation-delay: 7s, 2.5s; + animation-delay: 13s, 4s +} + +.beerflake:nth-of-type(21) { + left: 13%; + -webkit-animation-delay: 5.77s, 6s; + animation-delay: 2.5s, 4s +} + +.beerflake:nth-of-type(22) { + left: 42%; + -webkit-animation-delay: 7s, 4s; + animation-delay: 5.69s, 7.5s +} + +.beerflake:nth-of-type(23) { + left: 69%; + -webkit-animation-delay: 2.5s, 4s; + animation-delay: 42s, 12.9s +} diff --git a/stregsystem/static/stregsystem/easter.js b/stregsystem/static/stregsystem/easter.js new file mode 100644 index 00000000..d7f44b69 --- /dev/null +++ b/stregsystem/static/stregsystem/easter.js @@ -0,0 +1,58 @@ +d = new Date(); + +if(d.getMonth() === 3){ + if(d.getHours() === 13 && d.getMinutes() === 37){ + for(let beerflakes=0; beerflakes < Math.min(d.getDate(), 24); beerflakes++){ + SpawnBeerflakeCursed(); + } + }else{ + for(let beerflakes=0; beerflakes < Math.min(d.getDate(), 24); beerflakes++){ + SpawnBeerflake(); + } + } + + const kylle = document.createElement('div'); + kylle.classList.add("kylle"); + const gif = document.createElement("img") + if(d.getHours() === 13 && d.getMinutes() === 37){ + gif.src="/static/stregsystem/kylleCursed.gif"; + kylle.setAttribute('style', 'top: 60%'); + }else{ + gif.src="/static/stregsystem/kylle.gif"; + } + kylle.appendChild(gif); + document.body.querySelector(".easter-container").appendChild(kylle); + + SetBodyEasterStyle(); +} + +function SpawnBeerflakeCursed () { + const beerflakeCursed = document.createElement('div'); + beerflakeCursed.classList.add("beerflake"); + const gif = document.createElement("img") + gif.src="/static/stregsystem/beerflakeCursed.gif"; + beerflakeCursed.appendChild(gif); + document.body.querySelector(".easter-container").appendChild(beerflakeCursed); +} + +function SpawnBeerflake () { + const beerflake = document.createElement('div'); + beerflake.classList.add("beerflake"); + const gif = document.createElement("img") + gif.src="/static/stregsystem/beerflake.gif"; + beerflake.appendChild(gif); + document.body.querySelector(".easter-container").appendChild(beerflake); +} + +function SetBodyEasterStyle() { + const bodyStyle = document.body.style; + bodyStyle.color = "black"; + bodyStyle.backgroundImage = "url(\"" + media_url + "stregsystem/easter.jpg\")"; + bodyStyle.backgroundRepeat = "repeat-x"; + bodyStyle.backgroundSize = "auto 100%"; + bodyStyle.padding = "0"; + bodyStyle.margin = "0"; + bodyStyle.width = "100vw"; + bodyStyle.height = "100vh"; + bodyStyle.position = "relative"; +} \ No newline at end of file diff --git a/stregsystem/static/stregsystem/kylle.gif b/stregsystem/static/stregsystem/kylle.gif new file mode 100644 index 00000000..87ea2c37 Binary files /dev/null and b/stregsystem/static/stregsystem/kylle.gif differ diff --git a/stregsystem/static/stregsystem/kylleCursed.gif b/stregsystem/static/stregsystem/kylleCursed.gif new file mode 100644 index 00000000..4da1a0f5 Binary files /dev/null and b/stregsystem/static/stregsystem/kylleCursed.gif differ diff --git a/stregsystem/static/stregsystem/santa-sled.gif b/stregsystem/static/stregsystem/santa-sled.gif new file mode 100644 index 00000000..91487ca2 Binary files /dev/null and b/stregsystem/static/stregsystem/santa-sled.gif differ diff --git a/stregsystem/static/stregsystem/snow.css b/stregsystem/static/stregsystem/snow.css new file mode 100644 index 00000000..d519ec36 --- /dev/null +++ b/stregsystem/static/stregsystem/snow.css @@ -0,0 +1,209 @@ +.santa{ + position: fixed; + top: 72%; + right:-42%; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + animation-name: play; + animation-duration: 60s; + animation-timing-function: linear; + animation-iteration-count: infinite; + animation-play-state: running; +} + +@keyframes play { + 0%,80% {right:-80%;} + 100%{right:180%;} +} +.snow-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; +} + +@keyframes snowflakes-fall { + 0% { + top: -10% + } + 100% { + top: 100% + } +} + +@keyframes snowflakes-shake { + 0% { + transform: translateX(0px) + } + 50% { + transform: translateX(80px) + } + 100% { + transform: translateX(0px) + } +} + +.snowflake { + --size: 1vw; + width: var(--size); + height: var(--size); + background: white; + border-radius: 50%; + position: fixed; + top: -10%; + z-index: 9999; + cursor: default; + animation-name: snowflakes-fall, snowflakes-shake; + animation-duration: 10s, 3s; + animation-timing-function: linear, ease-in-out; + animation-iteration-count: infinite, infinite; + animation-play-state: running, running +} + +.snowflake:nth-of-type(0) { + left: 1%; + -webkit-animation-delay: 0s, 0s; + animation-delay: 0s, 0s +} + +.snowflake:nth-of-type(1) { + left: 10%; + -webkit-animation-delay: 1s, 1s; + animation-delay: 1s, 1s +} + +.snowflake:nth-of-type(2) { + left: 20%; + -webkit-animation-delay: 6s, .5s; + animation-delay: 6s, .5s +} + +.snowflake:nth-of-type(3) { + left: 30%; + -webkit-animation-delay: 4s, 2s; + animation-delay: 4s, 2s +} + +.snowflake:nth-of-type(4) { + left: 40%; + -webkit-animation-delay: 2s, 2s; + animation-delay: 2s, 2s +} + +.snowflake:nth-of-type(5) { + left: 50%; + -webkit-animation-delay: 8s, 3s; + animation-delay: 8s, 3s +} + +.snowflake:nth-of-type(6) { + left: 60%; + -webkit-animation-delay: 6s, 2s; + animation-delay: 6s, 2s +} + +.snowflake:nth-of-type(7) { + left: 70%; + -webkit-animation-delay: 2.5s, 1s; + animation-delay: 2.5s, 1s +} + +.snowflake:nth-of-type(8) { + left: 80%; + -webkit-animation-delay: 1s, 0s; + animation-delay: 1s, 0s +} + +.snowflake:nth-of-type(9) { + left: 90%; + -webkit-animation-delay: 7s, 4s; + animation-delay: 2.5s, 5.77s +} + +.snowflake:nth-of-type(10) { + left: 5%; + -webkit-animation-delay: 5.69s, 7.5s; + animation-delay: 7s, 5.77s +} + +.snowflake:nth-of-type(11) { + left: 15%; + -webkit-animation-delay: 12s, 12.9s; + animation-delay: 17s, 7.5s +} + +.snowflake:nth-of-type(12) { + left: 25%; + -webkit-animation-delay: 5.69s, 4s; + animation-delay: 2.5s, 4s +} + +.snowflake:nth-of-type(13) { + left: 35%; + -webkit-animation-delay: 7s, 6s; + animation-delay: 7s, 8s +} + +.snowflake:nth-of-type(14) { + left: 45%; + -webkit-animation-delay: 4s, 12.9s; + animation-delay: 5.69s, 7.5s +} + +.snowflake:nth-of-type(15) { + left: 55%; + -webkit-animation-delay: 2.5s, 1s; + animation-delay: 7s, 4s +} + +.snowflake:nth-of-type(16) { + left: 65%; + -webkit-animation-delay: 8s, 6s; + animation-delay: 2.5s, 3.2s +} + +.snowflake:nth-of-type(17) { + left: 75%; + -webkit-animation-delay: 7s, 2.5s; + animation-delay: 2.5s, 7.5s +} + +.snowflake:nth-of-type(18) { + left: 85%; + -webkit-animation-delay: 8.5s, 12.9s; + animation-delay: 5.69s, 4s +} + +.snowflake:nth-of-type(19) { + left: 95%; + -webkit-animation-delay: 2.5s, 6s; + animation-delay: 4s, 12.9s +} + +.snowflake:nth-of-type(20) { + left: 3%; + -webkit-animation-delay: 7s, 2.5s; + animation-delay: 13s, 4s +} + +.snowflake:nth-of-type(21) { + left: 13%; + -webkit-animation-delay: 5.77s, 6s; + animation-delay: 2.5s, 4s +} + +.snowflake:nth-of-type(22) { + left: 42%; + -webkit-animation-delay: 7s, 4s; + animation-delay: 5.69s, 7.5s +} + +.snowflake:nth-of-type(23) { + left: 69%; + -webkit-animation-delay: 2.5s, 4s; + animation-delay: 42s, 12.9s +} diff --git a/stregsystem/static/stregsystem/snow.js b/stregsystem/static/stregsystem/snow.js new file mode 100644 index 00000000..226b9362 --- /dev/null +++ b/stregsystem/static/stregsystem/snow.js @@ -0,0 +1,35 @@ +d = new Date(); + +if(d.getMonth() === 11){ + for(let snowflakes=0; snowflakes < Math.min(d.getDate(), 24); snowflakes++){ + SpawnSnowflake(); + } + + const santa = document.createElement('div'); + santa.classList.add("santa"); + const gif = document.createElement("img") + gif.src="/static/stregsystem/santa-sled.gif"; + santa.appendChild(gif); + document.body.querySelector(".snow-container").appendChild(santa); + + SetBodyChristmasStyle(); +} + +function SpawnSnowflake () { + const snowflake = document.createElement('div'); + snowflake.classList.add("snowflake"); + document.body.querySelector(".snow-container").appendChild(snowflake); +} + +function SetBodyChristmasStyle() { + const bodyStyle = document.body.style; + bodyStyle.color = "white"; + bodyStyle.backgroundImage = "url(\"" + media_url + "stregsystem/background.jpg\")"; + bodyStyle.backgroundRepeat = "repeat-x"; + bodyStyle.backgroundSize = "auto 100%"; + bodyStyle.padding = "0"; + bodyStyle.margin = "0"; + bodyStyle.width = "100vw"; + bodyStyle.height = "100vh"; + bodyStyle.position = "relative" +} \ No newline at end of file diff --git a/stregsystem/static/stregsystem/stregsystem.css b/stregsystem/static/stregsystem/stregsystem.css index e0198918..8ae899fc 100644 --- a/stregsystem/static/stregsystem/stregsystem.css +++ b/stregsystem/static/stregsystem/stregsystem.css @@ -1,7 +1,6 @@ body { color: black; background-color: white; -/* text-align: center;*/ } h1 { color: red } diff --git a/stregsystem/templates/stregsystem/base.html b/stregsystem/templates/stregsystem/base.html index 12bcb35b..16229e0d 100644 --- a/stregsystem/templates/stregsystem/base.html +++ b/stregsystem/templates/stregsystem/base.html @@ -12,6 +12,8 @@ + + + +
- +
+
{% show_candle %} diff --git a/stregsystem/templates/stregsystem/index_sale.html b/stregsystem/templates/stregsystem/index_sale.html index 21a970e8..e834e954 100644 --- a/stregsystem/templates/stregsystem/index_sale.html +++ b/stregsystem/templates/stregsystem/index_sale.html @@ -16,7 +16,7 @@ for tilsammen {{cost|money}} kr. {% if give_multibuy_hint %} -

psssst. multibuy er enabled.

+

psssst. multibuy er enabled.

Du kunne have skrevet: {{sale_hints}} {% endif %} @@ -27,6 +27,15 @@

psssst. multibuy er enabled.


Din alkohol promille er ca. {{promille|floatformat:2}}‰ {% endif %} {% endif %} +{% if product_contains_caffeine %} + {% if is_coffee_master%} +
🏆Du er kaffemester i denne uge!🏆 + {% endif %} +
Du har {{caffeine | floatformat:0}}mg koffein i kroppen. + {% if cups is not 0 %} +
Det svarer til at drikke {{caffeine | caffeine_emoji_render}} {{ cups|pluralize:'kop kaffe,kopper kaffe i streg!' }} + {% endif %} +{% endif %} {% endautoescape %}

diff --git a/stregsystem/templates/stregsystem/menu.html b/stregsystem/templates/stregsystem/menu.html index b5c33eec..5af2f24b 100644 --- a/stregsystem/templates/stregsystem/menu.html +++ b/stregsystem/templates/stregsystem/menu.html @@ -29,6 +29,9 @@ Indsæt penge + + Rangliste + {% comment %} Fortryd køb @@ -62,6 +65,15 @@
Din alkohol promille er ca. {{promille|floatformat:2}}‰ {% endif %} {% endif %} +{% if caffeine %} +
Du har {{caffeine | floatformat:0}}mg koffein i kroppen. + {% if cups is not 0 %} +
Det svarer til at drikke {{caffeine | caffeine_emoji_render}} {{ cups|pluralize:'kop kaffe,kopper kaffe i streg!' }} + {% endif %} + {% if is_coffee_master %} +
🏆Du er kaffemester i denne uge!🏆 + {% endif %} +{% endif %}
{% endblock %} diff --git a/stregsystem/templates/stregsystem/menu_userrank.html b/stregsystem/templates/stregsystem/menu_userrank.html new file mode 100644 index 00000000..f9dcb254 --- /dev/null +++ b/stregsystem/templates/stregsystem/menu_userrank.html @@ -0,0 +1,54 @@ +{% extends "stregsystem/base.html" %} + +{% block title %}Treoens stregsystem : Rangliste {% endblock %} + +{% block content %} + + + +
+

{{ member.firstname }} {{ member.lastname }} ({{ member.email }})

+ Dit første køb var: {{ member_first_purchase }}

+
{% csrf_token %} + Fra: {{ form.from_date }}
+ Til: {{ form.to_date }}
+ +
+ {% if form.errors %} +

{{ form.errors }}

+ {% endif %} +

Din rangliste fra {{ from_date | date:'d-m-Y' }} til {{ to_date | date:'d-m-Y' }}:

+ + + + {% for header in rankings.keys %} + + {% endfor %} + + + + {% for _, rank in rankings.items %} + + {% endfor %} + + + + {% for _, rank in rankings.items %} + + {% endfor %} + +
{{ header }}
rank/total{{ rank.0.0 }}/{{ rank.0.1 }}
enheder/hverdag{{ rank.1 }}
+
+ Rank/total rækken viser din nuværende placering ud af det totale antal Fembers der har købt varer i den + givne kategori, i den angivne periode.
+ En hverdag er defineret som antallet af mandag til fredage mellem studiestart og project deadline i 2021:
+ ((to_time - from_time).days * 162.14 / 365)
+
+{% endblock %} + diff --git a/stregsystem/templatetags/stregsystem_extras.py b/stregsystem/templatetags/stregsystem_extras.py index 54d33b6a..62d174e2 100644 --- a/stregsystem/templatetags/stregsystem_extras.py +++ b/stregsystem/templatetags/stregsystem_extras.py @@ -2,9 +2,16 @@ from django.template.loader import get_template from django.utils import timezone +from stregsystem.caffeine import caffeine_mg_to_coffee_cups + register = template.Library() +@register.filter +def caffeine_emoji_render(caffeine: int): + return "☕" * caffeine_mg_to_coffee_cups(caffeine) + + def money(value): if value is None: value = 0 diff --git a/stregsystem/tests.py b/stregsystem/tests.py index 47351992..d681f8dc 100644 --- a/stregsystem/tests.py +++ b/stregsystem/tests.py @@ -20,6 +20,7 @@ from stregsystem import views as stregsystem_views from stregsystem.admin import CategoryAdmin, ProductAdmin, MemberForm, MemberAdmin from stregsystem.booze import ballmer_peak +from stregsystem.caffeine import CAFFEINE_DEGRADATION_PR_HOUR, CAFFEINE_IN_COFFEE from stregsystem.models import ( Category, GetTransaction, @@ -38,6 +39,7 @@ price_display, MobilePayment, ) +from stregsystem.templatetags.stregsystem_extras import caffeine_emoji_render from stregsystem.utils import mobile_payment_exact_match_member, strip_emoji, MobilePaytoolException @@ -1503,7 +1505,14 @@ def test_exact_guess(self): self.assertEqual(Member.objects.get(username__exact="marx"), mobile_payment_exact_match_member("marx")) def test_emoji_strip(self): - self.assertEqual(strip_emoji("Tilmeld Lichi 😎"), "Tilmeld Lichi ") + self.assertEqual(strip_emoji("Tilmeld Lichi 😎"), "Tilmeld Lichi") + + def test_emoji_strip_electric_boogaloo(self): + # Laurits bør få næse for at fremprovokoere dette case + self.assertEqual(strip_emoji("♂️Laurits♂️"), "Laurits") + + def test_emoji_retain_nordic(self): + self.assertEqual(strip_emoji("æøåäëö"), "æøåäëö") def test_emoji_retain(self): self.assertEqual(strip_emoji("Tilmeld Lichi"), "Tilmeld Lichi") @@ -1546,3 +1555,182 @@ def test_mobilepaytool_race_error_marx_jdoe(self): self.assertEqual(e.inconsistent_mbpayments_count, 2) self.assertEqual(e.inconsistent_transaction_ids, ["241E027449465355", "016E027417049990"]) raise e + + +class CaffeineCalculatorTest(TestCase): + def test_default_caffeine_is_zero(self): + product = Product.objects.create(name="some product", price=420.0, active=True) + + self.assertEqual(product.caffeine_content_mg, 0) + + def test_calculate_is_zero_if_no_caffeine_is_consumed(self): + user = Member.objects.create(username="test", gender='M', balance=100) + NOTcoffee = Product.objects.create(name="koffeinfri kaffe", price=2.0, caffeine_content_mg=0, active=True) + + user.sale_set.create(product=NOTcoffee, price=NOTcoffee.price) + + self.assertEqual(0, user.calculate_caffeine_in_body()) + + def test_calculate_is_non_zero_if_some_caffeine_is_consumed(self): + user = Member.objects.create(username="test", gender='M', balance=100) + coffee = Product.objects.create( + name="koffeinholdig kaffe", price=2.0, caffeine_content_mg=CAFFEINE_IN_COFFEE, active=True + ) + + user.sale_set.create(product=coffee, price=coffee.price) + + self.assertAlmostEqual(CAFFEINE_IN_COFFEE, user.calculate_caffeine_in_body(), delta=0.001) + + def test_caffeine_1_hour(self): + with freeze_time() as frozen_datetime: + user = Member.objects.create(username="test", gender='M', balance=100) + coffee = Product.objects.create( + name="Kaffe☕☕☕", price=1, caffeine_content_mg=CAFFEINE_IN_COFFEE, active=True + ) + + user.sale_set.create(product=coffee, price=coffee.price) + + frozen_datetime.tick(delta=datetime.timedelta(hours=1)) + + self.assertAlmostEqual( + CAFFEINE_IN_COFFEE * (1 - CAFFEINE_DEGRADATION_PR_HOUR) ** 1, + user.calculate_caffeine_in_body(), + delta=0.001, + ) # There could be a rounding error + + def test_caffeine_degradation_for_1_to_10_hours(self): + for hours in range(1, 10): + with freeze_time() as frozen_datetime: + user = Member.objects.create(username="test", gender='M', balance=100) + coffee = Product.objects.create( + name="Kaffe☕☕☕", price=1, caffeine_content_mg=CAFFEINE_IN_COFFEE, active=True + ) + + user.sale_set.create(product=coffee, price=coffee.price) + + frozen_datetime.tick(delta=datetime.timedelta(hours=hours)) + self.assertAlmostEqual( + CAFFEINE_IN_COFFEE * (1 - CAFFEINE_DEGRADATION_PR_HOUR) ** hours, + user.calculate_caffeine_in_body(), + delta=0.001, + ) + + def test_caffeine_half_time_twice_with_two_cups(self): + with freeze_time() as frozen_datetime: + user = Member.objects.create(username="test", gender='M', balance=100) + coffee = Product.objects.create( + name="Kaffe☕☕☕", price=1, caffeine_content_mg=CAFFEINE_IN_COFFEE, active=True + ) + + user.sale_set.create(product=coffee, price=coffee.price) + frozen_datetime.tick(delta=datetime.timedelta(hours=5)) + + self.assertAlmostEqual( + CAFFEINE_IN_COFFEE * (1 - CAFFEINE_DEGRADATION_PR_HOUR) ** 5, + user.calculate_caffeine_in_body(), + delta=0.001, + ) + + user.sale_set.create(product=coffee, price=coffee.price) + frozen_datetime.tick(delta=datetime.timedelta(hours=5)) + + # expected value is compound interest of 1 cup in mg for 5h, then rest + 1 cup in mg for additional 5h + self.assertAlmostEqual( + ((CAFFEINE_IN_COFFEE * (1 - CAFFEINE_DEGRADATION_PR_HOUR) ** 5) + CAFFEINE_IN_COFFEE) + * (1 - CAFFEINE_DEGRADATION_PR_HOUR) ** 5, + user.calculate_caffeine_in_body(), + delta=0.001, + ) + + def test_multiple_caffeine_intake_at_odd_times(self): + tick_one = datetime.timedelta(hours=2, minutes=24) + tick_two = datetime.timedelta(hours=1, minutes=13, seconds=16) + + with freeze_time() as frozen_datetime: + user = Member.objects.create(username="test", gender='M', balance=100) + coffee = Product.objects.create( + name="Kaffe☕☕☕", price=1, caffeine_content_mg=CAFFEINE_IN_COFFEE, active=True + ) + + # do sale and tick_one + user.sale_set.create(product=coffee, price=coffee.price) + frozen_datetime.tick(delta=tick_one) + + # calc and assert degradation of 1 cup during tick_one + expected_caffeine_in_body = float( + CAFFEINE_IN_COFFEE * (1 - CAFFEINE_DEGRADATION_PR_HOUR) ** (tick_one / datetime.timedelta(hours=1)), + ) + + self.assertAlmostEqual( + expected_caffeine_in_body, + user.calculate_caffeine_in_body(), + delta=0.001, + ) + + # do another sale and tick_two + user.sale_set.create(product=coffee, price=coffee.price) + frozen_datetime.tick(delta=tick_two) + + # first add consumed caffeine from second sale, then degrade, since no time passes after last consume + expected_caffeine_in_body += CAFFEINE_IN_COFFEE + + expected_caffeine_in_body = float( + expected_caffeine_in_body + * (1 - CAFFEINE_DEGRADATION_PR_HOUR) ** (tick_two / datetime.timedelta(hours=1)), + ) + + self.assertAlmostEqual( + expected_caffeine_in_body, + user.calculate_caffeine_in_body(), + delta=0.001, + ) + + def test_caffeine_str_is_correct_length(self): + user = Member.objects.create(username="test", gender='F', balance=100) + coffee = Product.objects.create(name="Kaffe☕☕☕", price=1, caffeine_content_mg=CAFFEINE_IN_COFFEE, active=True) + + # do five sales of coffee and assert that emoji renderer returns same amount of emoji + sales = 5 + with freeze_time() as _: + for _ in range(0, sales): + user.sale_set.create(product=coffee, price=coffee.price) + + # keep assert within scope of freeze_time to ensure degradation is not applied + self.assertEqual("☕" * sales, caffeine_emoji_render(user.calculate_caffeine_in_body())) + + def test_rich_guy_is_leading_coffee_addict(self): + coffee_addict = Member.objects.create(username="Anders", gender="M", balance=100) + average_developer = Member.objects.create(username="my-guy", gender="M", balance=50) + coffee_category = Category.objects.create(name="Caffeine☕☕☕", pk=6) + coffee_category.save() + coffee = Product.objects.create(name="Kaffe☕☕☕", price=1, caffeine_content_mg=70, active=True) + coffee.save() + coffee.categories.add(coffee_category) + + [coffee_addict.sale_set.create(product=coffee, price=coffee.price) for _ in range(5)] + [average_developer.sale_set.create(product=coffee, price=coffee.price) for _ in range(2)] + + self.assertTrue(coffee_addict.is_leading_coffee_addict()) + self.assertFalse(average_developer.is_leading_coffee_addict()) + + def test_if_sunday_is_in_week(self): + coffee_addict = Member.objects.create(username="Ida", gender="F", balance=100) + average_developer = Member.objects.create(username="my-gal", gender="F", balance=50) + coffee_category = Category.objects.create(name="Caffeine☕☕☕", pk=6) + coffee_category.save() + coffee = Product.objects.create(name="Kaffe☕☕☕", price=1, caffeine_content_mg=CAFFEINE_IN_COFFEE, active=True) + # matches coffee id in production. Will be implemented with categories later, when production have a coffee + # category + coffee.save() + coffee.categories.add(coffee_category) + + with freeze_time(timezone.datetime(year=2021, day=29, month=11, hour=8)) as monday: + coffee_addict.sale_set.create(product=coffee, price=coffee.price) + + with freeze_time(timezone.datetime(year=2021, day=5, month=12, hour=8)) as sunday: + + coffee_addict.sale_set.create(product=coffee, price=coffee.price) + average_developer.sale_set.create(product=coffee, price=coffee.price) + + self.assertTrue(coffee_addict.is_leading_coffee_addict()) + self.assertFalse(average_developer.is_leading_coffee_addict()) diff --git a/stregsystem/urls.py b/stregsystem/urls.py index e890b8aa..69906371 100644 --- a/stregsystem/urls.py +++ b/stregsystem/urls.py @@ -29,5 +29,6 @@ re_path(r'^(?P\d+)/user/(?P\d+)/$', views.menu_userinfo, name="userinfo"), re_path(r'^(?P\d+)/user/(?P\d+)/pay$', views.menu_userpay, name="userpay"), re_path(r'^(?P\d+)/user/(?P\d+)/tickets$', views.menu_ticketsview, name="usertickets"), + re_path(r'^(?P\d+)/user/(?P\d+)/rank$', views.menu_userrank, name="userrank"), re_path(r'^api/member/payment/qr$', views.qr_payment, name="payment_qr"), ] diff --git a/stregsystem/utils.py b/stregsystem/utils.py index fc31b244..83c6ccd7 100644 --- a/stregsystem/utils.py +++ b/stregsystem/utils.py @@ -93,7 +93,7 @@ def date_to_midnight(date): return timezone.make_aware(timezone.datetime(date.year, date.month, date.day, 0, 0)) -def send_payment_mail(member, amount): +def send_payment_mail(member, amount, mobilepay_comment): if hasattr(settings, 'TEST_MODE'): return msg = MIMEMultipart() @@ -103,7 +103,7 @@ def send_payment_mail(member, amount): formatted_amount = money(amount) - html = f""" + normal_html = f""" @@ -124,7 +124,38 @@ def send_payment_mail(member, amount): """ - msg.attach(MIMEText(html, 'html')) + from django.utils.html import escape + + shame_html = f""" + + + +

+ Hej {member.firstname}!

+ Vi har med stort besvær indsat pokkers {formatted_amount} stregdollars på din konto: "{member.username}".

+ Da du ikke skrev dit brugernavn korrekt, men i stedet skrev '{escape(mobilepay_comment)}' var de stakkels TREOer desværre nødt til at tage flere minutter ud af deres dag for at indsætte dine penge manuelt. + Vil du nyde godt af vores automatiske indbetaling kan du i fremtiden med fordel skrive dit brugernavn korrekt i MobilePay kommentaren: '{member.username}'. + Udnytter du vores QR-kode generator klarer den også denne komplicerede process for dig. + + Hvis du ikke ønsker at modtage flere mails som denne kan du skrive en mail til: treo@fklub.dk

+ Mvh,
+ TREOen
+ ====================================================================
+ Hello {member.firstname}!

+ We've had great trouble inserting {formatted_amount} stregdollars on your account: "{member.username}".

+ This is due to you not you not writing your username correctly, and instead writing '{escape(mobilepay_comment)}'. The poor TREOs had to take multiple minutes out of their day to insert your money manually. + If you want to utilise our automatic fill-up in future, you can write your username correctly in the MobilePay comment: '{member.username}' + Our QR-code generator also handles this very complicated process for you. + + If you do not desire to receive any more mails of this sort, please file a complaint to: treo@fklub.dk

+ Kind regards,
+ TREOen +

+ + + """ + + msg.attach(MIMEText(shame_html if mobilepay_comment else normal_html, 'html')) try: smtpObj = smtplib.SMTP('localhost', 25) @@ -178,16 +209,30 @@ def mobile_payment_exact_match_member(comment): def strip_emoji(text): # yoinked from https://stackoverflow.com/questions/33404752/removing-emojis-from-a-string-in-python - regrex_pattern = re.compile( - pattern="[" + emoj = re.compile( + "[" u"\U0001F600-\U0001F64F" # emoticons u"\U0001F300-\U0001F5FF" # symbols & pictographs u"\U0001F680-\U0001F6FF" # transport & map symbols u"\U0001F1E0-\U0001F1FF" # flags (iOS) + u"\U00002500-\U00002BEF" # chinese char + u"\U00002702-\U000027B0" + u"\U00002702-\U000027B0" + u"\U000024C2-\U0001F251" + u"\U0001f926-\U0001f937" + u"\U00010000-\U0010ffff" + u"\u2640-\u2642" + u"\u2600-\u2B55" + u"\u200d" + u"\u23cf" + u"\u23e9" + u"\u231a" + u"\ufe0f" # dingbats + u"\u3030" "]+", - flags=re.UNICODE, + re.UNICODE, ) - return regrex_pattern.sub(r'', text) + return re.sub(emoj, '', text).strip() def qr_code(data): diff --git a/stregsystem/views.py b/stregsystem/views.py index 696af782..865eb122 100644 --- a/stregsystem/views.py +++ b/stregsystem/views.py @@ -1,4 +1,10 @@ import datetime +from typing import List + +import pytz +from pytz import UTC + +from stregreport.views import fjule_party from django.core import management from django.forms import modelformset_factory, formset_factory @@ -6,7 +12,7 @@ from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import permission_required from django.conf import settings -from django.db.models import Q +from django.db.models import Q, Count from django import forms from django.http import HttpResponsePermanentRedirect, HttpResponseBadRequest from django.shortcuts import get_object_or_404, render @@ -27,6 +33,7 @@ Event, StregForbudError, MobilePayment, + Category, ) from stregsystem.utils import ( make_active_productlist_query, @@ -38,7 +45,8 @@ ) from .booze import ballmer_peak -from .forms import MobilePayToolForm, QRPaymentForm, PurchaseForm +from .caffeine import caffeine_mg_to_coffee_cups +from .forms import MobilePayToolForm, QRPaymentForm, PurchaseForm, RankingDateForm def __get_news(): @@ -126,13 +134,13 @@ def _multibuy_hint(now, member): return (False, None) -def quicksale(request, room, member, bought_ids): +def quicksale(request, room, member: Member, bought_ids): news = __get_news() product_list = __get_productlist(room.id) now = timezone.now() # Retrieve products and construct transaction - products = [] + products: List[Product] = [] try: for i in bought_ids: product = Product.objects.get( @@ -158,6 +166,11 @@ def quicksale(request, room, member, bought_ids): promille = member.calculate_alcohol_promille() is_ballmer_peaking, bp_minutes, bp_seconds = ballmer_peak(promille) + caffeine = member.calculate_caffeine_in_body() + cups = caffeine_mg_to_coffee_cups(caffeine) + product_contains_caffeine = any(product.caffeine_content_mg > 0 for product in products) + is_coffee_master = member.is_leading_coffee_addict() + cost = order.total give_multibuy_hint, sale_hints = _multibuy_hint(now, member) @@ -176,6 +189,10 @@ def usermenu(request, room, member, bought, from_sale=False): bp_seconds, ) = ballmer_peak(promille) + caffeine = member.calculate_caffeine_in_body() + cups = caffeine_mg_to_coffee_cups(caffeine) + is_coffee_master = member.is_leading_coffee_addict() + give_multibuy_hint, sale_hints = _multibuy_hint(timezone.now(), member) give_multibuy_hint = give_multibuy_hint and from_sale @@ -246,6 +263,74 @@ def menu_userpay(request, room_id, member_id): return render(request, 'stregsystem/menu_userpay.html', locals()) +def menu_userrank(request, room_id, member_id): + from_date = fjule_party(datetime.datetime.today().year - 1) + to_date = datetime.datetime.now(tz=pytz.timezone("Europe/Copenhagen")) + room = Room.objects.get(pk=room_id) + member = Member.objects.get(pk=member_id, active=True) + + def ranking(category_ids, from_d, to_d): + qs = ( + Member.objects.filter(sale__product__in=category_ids, sale__timestamp__gt=from_d, sale__timestamp__lte=to_d) + .annotate(Count('sale')) + .order_by('-sale__count', 'username') + ) + if member not in qs: + return 0, qs.count() + return list(qs).index(Member.objects.get(id=member.id)) + 1, int(qs.count()) + + def get_product_ids_for_category(category) -> list: + return list( + Product.objects.filter(categories__exact=Category.objects.get(name__exact=category)).values_list( + 'id', flat=True + ) + ) + + def category_per_uni_day(category_ids, from_d, to_d): + qs = Member.objects.filter( + id=member.id, + sale__product__in=category_ids, + sale__timestamp__gt=from_d, + sale__timestamp__lte=to_d, + ) + if member not in qs: + return 0 + else: + return "{:.2f}".format(qs.count() / ((to_d - from_d).days * 162.14 / 365)) # university workdays in 2021 + + # let user know when they first purchased a product + member_first_purchase = "Ikke endnu, køb en limfjordsporter!" + first_purchase = Sale.objects.filter(member=member_id).order_by('-timestamp') + if first_purchase.exists(): + member_first_purchase = first_purchase.last().timestamp + + form = RankingDateForm() + if request.method == "POST" and request.POST['custom-range']: + form = RankingDateForm(request.POST) + if form.is_valid(): + from_date = form.cleaned_data['from_date'] + to_date = form.cleaned_data['to_date'] + else: + # setup initial dates for form and results + form = RankingDateForm(initial={'from_date': from_date, 'to_date': to_date}) + + # get prod_ids for each category as dict {cat: [key1, key2])}, then flatten list of singleton + # dicts into one dict, lastly calculate member_id rating and units/weekday for category_ids + rankings = { + key: ( + ranking(category_ids, from_date, to_date), + category_per_uni_day(category_ids, from_date, to_date), + ) + for key, category_ids in { + k: v + for x in map(lambda x: {x: get_product_ids_for_category(x)}, list(Category.objects.all())) + for k, v in x.items() + }.items() + } + + return render(request, 'stregsystem/menu_userrank.html', locals()) + + def menu_sale(request, room_id, member_id, product_id=None): room = Room.objects.get(pk=room_id) news = __get_news()