mirror of https://github.com/snachodog/mybuddy.git
Add carousel of last 7 days of feedings to the todays feeding card (#450)
Add list of last 7 days of feedings to the todays feeding card Co-authored-by: Daniel Beard <daniel@medcrypt.co>
This commit is contained in:
parent
7178bf9fa4
commit
21b8a737c0
|
@ -311,8 +311,8 @@ class FeedingAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"child": 1,
|
"child": 1,
|
||||||
"start": "2017-11-18T14:00:00-05:00",
|
"start": "2017-11-18T19:00:00-05:00",
|
||||||
"end": "2017-11-18T14:15:00-05:00",
|
"end": "2017-11-18T19:15:00-05:00",
|
||||||
"duration": "00:15:00",
|
"duration": "00:15:00",
|
||||||
"type": "formula",
|
"type": "formula",
|
||||||
"method": "bottle",
|
"method": "bottle",
|
||||||
|
@ -333,7 +333,7 @@ class FeedingAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
|
||||||
self.endpoint, {"start_min": "2017-11-18T11:30:00-05:00"}
|
self.endpoint, {"start_min": "2017-11-18T11:30:00-05:00"}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data["count"], 2)
|
self.assertEqual(response.data["count"], 3)
|
||||||
|
|
||||||
def test_post(self):
|
def test_post(self):
|
||||||
data = {
|
data = {
|
||||||
|
|
|
@ -234,8 +234,8 @@
|
||||||
"fields":
|
"fields":
|
||||||
{
|
{
|
||||||
"child": 1,
|
"child": 1,
|
||||||
"start": "2017-11-18T14:00:00Z",
|
"start": "2017-11-18T14:00:00-05:00",
|
||||||
"end": "2017-11-18T14:30:00Z",
|
"end": "2017-11-18T14:30:00-05:00",
|
||||||
"duration": "00:30:00",
|
"duration": "00:30:00",
|
||||||
"type": "breast milk",
|
"type": "breast milk",
|
||||||
"method": "left breast",
|
"method": "left breast",
|
||||||
|
@ -248,8 +248,8 @@
|
||||||
"fields":
|
"fields":
|
||||||
{
|
{
|
||||||
"child": 1,
|
"child": 1,
|
||||||
"start": "2017-11-18T16:30:00Z",
|
"start": "2017-11-18T16:30:00-05:00",
|
||||||
"end": "2017-11-18T17:00:00Z",
|
"end": "2017-11-18T17:00:00-05:00",
|
||||||
"duration": "00:30:00",
|
"duration": "00:30:00",
|
||||||
"type": "breast milk",
|
"type": "breast milk",
|
||||||
"method": "right breast",
|
"method": "right breast",
|
||||||
|
@ -262,8 +262,8 @@
|
||||||
"fields":
|
"fields":
|
||||||
{
|
{
|
||||||
"child": 1,
|
"child": 1,
|
||||||
"start": "2017-11-18T19:00:00Z",
|
"start": "2017-11-18T19:00:00-05:00",
|
||||||
"end": "2017-11-18T19:15:00Z",
|
"end": "2017-11-18T19:15:00-05:00",
|
||||||
"duration": "00:15:00",
|
"duration": "00:15:00",
|
||||||
"type": "formula",
|
"type": "formula",
|
||||||
"method": "bottle",
|
"method": "bottle",
|
||||||
|
@ -271,6 +271,51 @@
|
||||||
"notes": "forgot vitamins :("
|
"notes": "forgot vitamins :("
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"model": "core.feeding",
|
||||||
|
"pk": 4,
|
||||||
|
"fields":
|
||||||
|
{
|
||||||
|
"child": 1,
|
||||||
|
"start": "2017-11-17T19:00:00-05:00",
|
||||||
|
"end": "2017-11-17T19:15:00-05:00",
|
||||||
|
"duration": "00:15:00",
|
||||||
|
"type": "formula",
|
||||||
|
"method": "bottle",
|
||||||
|
"amount": 0.25,
|
||||||
|
"notes": "forgot vitamins :("
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "core.feeding",
|
||||||
|
"pk": 5,
|
||||||
|
"fields":
|
||||||
|
{
|
||||||
|
"child": 1,
|
||||||
|
"start": "2017-11-11T19:00:00-05:00",
|
||||||
|
"end": "2017-11-11T19:15:00-05:00",
|
||||||
|
"duration": "00:15:00",
|
||||||
|
"type": "formula",
|
||||||
|
"method": "bottle",
|
||||||
|
"amount": 10.0,
|
||||||
|
"notes": "last day feedings"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "core.feeding",
|
||||||
|
"pk": 6,
|
||||||
|
"fields":
|
||||||
|
{
|
||||||
|
"child": 1,
|
||||||
|
"start": "2017-11-11T00:00:00-05:00",
|
||||||
|
"end": "2017-11-11T00:15:00-05:00",
|
||||||
|
"duration": "00:15:00",
|
||||||
|
"type": "formula",
|
||||||
|
"method": "bottle",
|
||||||
|
"amount": 10.0,
|
||||||
|
"notes": "oldest feeding at midnight"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"model": "core.note",
|
"model": "core.note",
|
||||||
"pk": 1,
|
"pk": 1,
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import datetime
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.utils import timesince, timezone
|
from django.utils import timesince, timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
@ -90,3 +92,25 @@ def seconds(duration):
|
||||||
return s
|
return s
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter()
|
||||||
|
def dayssince(value, today=None):
|
||||||
|
"""
|
||||||
|
Returns the days since passed datetime in a user friendly way. (e.g. today, yesterday, 2 days ago, ...)
|
||||||
|
:param value: a date instance
|
||||||
|
:param today: date to compare to (defaults to today)
|
||||||
|
:returns: the formatted string
|
||||||
|
"""
|
||||||
|
if today is None:
|
||||||
|
today = timezone.datetime.now().date()
|
||||||
|
|
||||||
|
delta = today - value
|
||||||
|
|
||||||
|
if delta < datetime.timedelta(days=1):
|
||||||
|
return "today"
|
||||||
|
if delta < datetime.timedelta(days=2):
|
||||||
|
return "yesterday"
|
||||||
|
|
||||||
|
# use standard timesince for anything beyond yesterday
|
||||||
|
return str(delta.days) + " days ago"
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import unittest
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.utils import timezone, formats
|
from django.utils import timezone, formats
|
||||||
|
@ -62,6 +64,37 @@ class TemplateTagsTestCase(TestCase):
|
||||||
self.assertEqual(duration.seconds(""), 0)
|
self.assertEqual(duration.seconds(""), 0)
|
||||||
self.assertRaises(TypeError, duration.seconds("not a delta"))
|
self.assertRaises(TypeError, duration.seconds("not a delta"))
|
||||||
|
|
||||||
|
def test_duration_dayssince(self):
|
||||||
|
# test with a few different dates that could be pathological
|
||||||
|
dates = [
|
||||||
|
timezone.datetime(2022, 1, 1, 0, 0, 1).date(), # new year
|
||||||
|
timezone.datetime(2021, 12, 31, 23, 59, 59).date(), # almost new year
|
||||||
|
timezone.datetime(
|
||||||
|
1969, 2, 1, 23, 59, 59
|
||||||
|
).date(), # old but middle of the year
|
||||||
|
]
|
||||||
|
for d in dates:
|
||||||
|
self.assertEqual(duration.dayssince(d, today=d), "today")
|
||||||
|
self.assertEqual(
|
||||||
|
duration.dayssince((d - timezone.timedelta(hours=5)), today=d), "today"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
duration.dayssince((d - timezone.timedelta(hours=24)), today=d),
|
||||||
|
"yesterday",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
duration.dayssince((d - timezone.timedelta(hours=24 * 2)), today=d),
|
||||||
|
"2 days ago",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
duration.dayssince((d - timezone.timedelta(hours=24 * 10)), today=d),
|
||||||
|
"10 days ago",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
duration.dayssince((d - timezone.timedelta(hours=24 * 60)), today=d),
|
||||||
|
"60 days ago",
|
||||||
|
)
|
||||||
|
|
||||||
def test_instance_add_url(self):
|
def test_instance_add_url(self):
|
||||||
child = Child.objects.create(
|
child = Child.objects.create(
|
||||||
first_name="Test", last_name="Child", birth_date=timezone.localdate()
|
first_name="Test", last_name="Child", birth_date=timezone.localdate()
|
||||||
|
|
|
@ -8,15 +8,42 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% if total %}
|
{% if feedings|length > 0 %}
|
||||||
{{ total }}
|
<div id="feeding-days-carousel" class="carousel slide" data-interval="false">
|
||||||
|
<div class="carousel-inner">
|
||||||
|
{% for feeding in feedings %}
|
||||||
|
<div class="carousel-item{% if forloop.counter == 1 %} active{% endif %}">
|
||||||
|
<div class="last-feeding-method text-center">
|
||||||
|
{% if feeding.total %}
|
||||||
|
{{ feeding.total }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans "None" %}
|
{% trans "None" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
</div>
|
||||||
|
<div class="text-center small">
|
||||||
{% block content %}
|
{% if feeding.count > 0 %} {{ feeding.count }} feedings {% endif %}
|
||||||
{% if count > 0 %}
|
</div>
|
||||||
{% blocktrans %}{{ count }} feeding entries{% endblocktrans %}
|
{% blocktrans trimmed with since=feeding.date.date|dayssince %}
|
||||||
|
<div class="text-center small text-muted">
|
||||||
|
{{ since }}
|
||||||
|
</div>
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if feedings|length > 1 %}
|
||||||
|
<a class="carousel-control-prev" href="#feeding-days-carousel" role="button" data-slide="prev">
|
||||||
|
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||||
|
<span class="sr-only">{% trans "Previous" %}</span>
|
||||||
|
</a>
|
||||||
|
<a class="carousel-control-next" href="#feeding-days-carousel" role="button" data-slide="next">
|
||||||
|
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||||
|
<span class="sr-only">{% trans "Next" %}</span>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% trans "None" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -107,31 +107,46 @@ def card_diaperchange_types(context, child, date=None):
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("cards/feeding_day.html", takes_context=True)
|
@register.inclusion_tag("cards/feeding_day.html", takes_context=True)
|
||||||
def card_feeding_day(context, child, date=None):
|
def card_feeding_day(context, child, end_date=None):
|
||||||
"""
|
"""
|
||||||
Filters Feeding instances to get total amount for a specific date.
|
Filters Feeding instances to get total amount for a specific date and for 7 days before
|
||||||
:param child: an instance of the Child model.
|
:param child: an instance of the Child model.
|
||||||
:param date: a Date object for the day to filter.
|
:param end_date: a Date object for the day to filter.
|
||||||
:returns: a dict with count and total amount for the Feeding instances.
|
:returns: a dict with count and total amount for the Feeding instances.
|
||||||
"""
|
"""
|
||||||
if not date:
|
if not end_date:
|
||||||
date = timezone.localtime().date()
|
end_date = timezone.localtime()
|
||||||
|
|
||||||
|
# push end_date to very end of that day
|
||||||
|
end_date = end_date.replace(hour=23, minute=59, second=59, microsecond=9999)
|
||||||
|
# we need a datetime to use the range helper in the model
|
||||||
|
start_date = end_date - timezone.timedelta(
|
||||||
|
days=8
|
||||||
|
) # end of the -8th day so we get the FULL 7th day
|
||||||
|
|
||||||
instances = models.Feeding.objects.filter(child=child).filter(
|
instances = models.Feeding.objects.filter(child=child).filter(
|
||||||
start__year=date.year, start__month=date.month, start__day=date.day
|
start__range=[start_date, end_date]
|
||||||
) | models.Feeding.objects.filter(child=child).filter(
|
|
||||||
end__year=date.year, end__month=date.month, end__day=date.day
|
|
||||||
)
|
)
|
||||||
|
|
||||||
total = sum([instance.amount for instance in instances if instance.amount])
|
# prepare the result list for the last 7 days
|
||||||
count = len(instances)
|
dates = [end_date - timezone.timedelta(days=i) for i in range(8)]
|
||||||
empty = len(instances) == 0 or total == 0
|
results = [{"date": d, "total": 0, "count": 0} for d in dates]
|
||||||
|
|
||||||
|
# do one pass over the data and add it to the appropriate day
|
||||||
|
for instance in instances:
|
||||||
|
# convert to local tz and push feed_date to end so we're comparing apples to apples for the date
|
||||||
|
feed_date = timezone.localtime(instance.start).replace(
|
||||||
|
hour=23, minute=59, second=59, microsecond=9999
|
||||||
|
)
|
||||||
|
idx = (end_date - feed_date).days
|
||||||
|
result = results[idx]
|
||||||
|
result["total"] += instance.amount if instance.amount is not None else 0
|
||||||
|
result["count"] += 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"feedings": results,
|
||||||
"type": "feeding",
|
"type": "feeding",
|
||||||
"total": total,
|
"empty": len(instances) == 0,
|
||||||
"count": count,
|
|
||||||
"empty": empty,
|
|
||||||
"hide_empty": _hide_empty(context),
|
"hide_empty": _hide_empty(context),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -156,8 +156,17 @@ class TemplateTagsTestCase(TestCase):
|
||||||
self.assertEqual(data["type"], "feeding")
|
self.assertEqual(data["type"], "feeding")
|
||||||
self.assertFalse(data["empty"])
|
self.assertFalse(data["empty"])
|
||||||
self.assertFalse(data["hide_empty"])
|
self.assertFalse(data["hide_empty"])
|
||||||
self.assertEqual(data["total"], 2.5)
|
# most recent day
|
||||||
self.assertEqual(data["count"], 3)
|
self.assertEqual(data["feedings"][0]["total"], 2.5)
|
||||||
|
self.assertEqual(data["feedings"][0]["count"], 3)
|
||||||
|
|
||||||
|
# yesterday
|
||||||
|
self.assertEqual(data["feedings"][1]["total"], 0.25)
|
||||||
|
self.assertEqual(data["feedings"][1]["count"], 1)
|
||||||
|
|
||||||
|
# last day
|
||||||
|
self.assertEqual(data["feedings"][-1]["total"], 20.0)
|
||||||
|
self.assertEqual(data["feedings"][-1]["count"], 2)
|
||||||
|
|
||||||
def test_card_feeding_last(self):
|
def test_card_feeding_last(self):
|
||||||
data = cards.card_feeding_last(self.context, self.child)
|
data = cards.card_feeding_last(self.context, self.child)
|
||||||
|
@ -246,7 +255,7 @@ class TemplateTagsTestCase(TestCase):
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "duration",
|
"type": "duration",
|
||||||
"stat": timezone.timedelta(0, 7200),
|
"stat": timezone.timedelta(days=1, seconds=46980),
|
||||||
"title": "Feeding frequency",
|
"title": "Feeding frequency",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue