mirror of https://github.com/snachodog/mybuddy.git
New statistic graph: Diaper change intervals
Fix formatting Fix comment Remove debug Remove debug Change naming of variable Add new statistics graph: Feeding intervals Fix naming Add simple HTTP status code tests for new graphs Remove number of feedings from graph Remove obsolete yaxis2 Fix calculation of intervals and change scale to hours instead of minutes
This commit is contained in:
parent
8d8006ab71
commit
26e8a1f689
|
@ -2,8 +2,10 @@ from .bmi_change import bmi_change # NOQA
|
||||||
from .diaperchange_amounts import diaperchange_amounts # NOQA
|
from .diaperchange_amounts import diaperchange_amounts # NOQA
|
||||||
from .diaperchange_lifetimes import diaperchange_lifetimes # NOQA
|
from .diaperchange_lifetimes import diaperchange_lifetimes # NOQA
|
||||||
from .diaperchange_types import diaperchange_types # NOQA
|
from .diaperchange_types import diaperchange_types # NOQA
|
||||||
|
from .diaperchange_intervals import diaperchange_intervals # NOQA
|
||||||
from .feeding_amounts import feeding_amounts # NOQA
|
from .feeding_amounts import feeding_amounts # NOQA
|
||||||
from .feeding_duration import feeding_duration # NOQA
|
from .feeding_duration import feeding_duration # NOQA
|
||||||
|
from .feeding_intervals import feeding_intervals # NOQA
|
||||||
from .head_circumference_change import head_circumference_change # NOQA
|
from .head_circumference_change import head_circumference_change # NOQA
|
||||||
from .height_change import height_change # NOQA
|
from .height_change import height_change # NOQA
|
||||||
from .pumping_amounts import pumping_amounts # NOQA
|
from .pumping_amounts import pumping_amounts # NOQA
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from django.db.models import Count, Case, When
|
||||||
|
from django.db.models.functions import TruncDate
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from django.utils.translation import get_language
|
||||||
|
|
||||||
|
import plotly.offline as plotly
|
||||||
|
import plotly.graph_objs as go
|
||||||
|
|
||||||
|
from core.utils import duration_parts
|
||||||
|
|
||||||
|
from reports import utils
|
||||||
|
|
||||||
|
|
||||||
|
def diaperchange_intervals(changes):
|
||||||
|
"""
|
||||||
|
Create a graph showing intervals of diaper changes.
|
||||||
|
:param changes: a QuerySet of Diaper Change instances.
|
||||||
|
:returns: a tuple of the the graph's html and javascript.
|
||||||
|
"""
|
||||||
|
|
||||||
|
changes = changes.order_by("time")
|
||||||
|
intervals = []
|
||||||
|
intervals_solid = []
|
||||||
|
intervals_wet = []
|
||||||
|
last_change = changes.first()
|
||||||
|
for change in changes[1:]:
|
||||||
|
interval = change.time - last_change.time
|
||||||
|
if interval.total_seconds() > 0:
|
||||||
|
intervals.append(interval)
|
||||||
|
if change.solid:
|
||||||
|
intervals_solid.append(interval)
|
||||||
|
if change.wet:
|
||||||
|
intervals_wet.append(interval)
|
||||||
|
last_change = change
|
||||||
|
|
||||||
|
trace_solid = go.Scatter(
|
||||||
|
name=_("Solid"),
|
||||||
|
line=dict(shape="spline"),
|
||||||
|
x=list(changes.values_list("time", flat=True)),
|
||||||
|
y=[i.total_seconds() / 3600 for i in intervals_solid],
|
||||||
|
hoverinfo="text",
|
||||||
|
text=[_duration_string_hms(i) for i in intervals_solid],
|
||||||
|
)
|
||||||
|
|
||||||
|
trace_wet = go.Scatter(
|
||||||
|
name=_("Wet"),
|
||||||
|
line=dict(shape="spline"),
|
||||||
|
x=list(changes.values_list("time", flat=True)),
|
||||||
|
y=[i.total_seconds() / 3600 for i in intervals_wet],
|
||||||
|
hoverinfo="text",
|
||||||
|
text=[_duration_string_hms(i) for i in intervals_wet],
|
||||||
|
)
|
||||||
|
|
||||||
|
trace_total = go.Scatter(
|
||||||
|
name=_("Total"),
|
||||||
|
line=dict(shape="spline"),
|
||||||
|
x=list(changes.values_list("time", flat=True)),
|
||||||
|
y=[i.total_seconds() / 3600 for i in intervals],
|
||||||
|
hoverinfo="text",
|
||||||
|
text=[_duration_string_hms(i) for i in intervals],
|
||||||
|
)
|
||||||
|
|
||||||
|
layout_args = utils.default_graph_layout_options()
|
||||||
|
layout_args["barmode"] = "stack"
|
||||||
|
layout_args["title"] = _("<b>Diaper Change Intervals</b>")
|
||||||
|
layout_args["xaxis"]["title"] = _("Date")
|
||||||
|
layout_args["xaxis"]["rangeselector"] = utils.rangeselector_date()
|
||||||
|
layout_args["yaxis"]["title"] = _("Interval (hours)")
|
||||||
|
|
||||||
|
fig = go.Figure(
|
||||||
|
{
|
||||||
|
"data": [trace_solid, trace_wet, trace_total],
|
||||||
|
"layout": go.Layout(**layout_args),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
output = plotly.plot(
|
||||||
|
fig,
|
||||||
|
output_type="div",
|
||||||
|
include_plotlyjs=False,
|
||||||
|
config={"locale": get_language()},
|
||||||
|
)
|
||||||
|
return utils.split_graph_output(output)
|
||||||
|
|
||||||
|
|
||||||
|
def _duration_string_hms(duration):
|
||||||
|
"""
|
||||||
|
Format a duration string with hours, minutes and seconds. This is
|
||||||
|
intended to fit better in smaller spaces on a graph.
|
||||||
|
:returns: a string of the form Xm.
|
||||||
|
"""
|
||||||
|
h, m, s = duration_parts(duration)
|
||||||
|
return "{}h{}m{}s".format(h, m, s)
|
|
@ -0,0 +1,58 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from django.db.models import Count, Sum
|
||||||
|
from django.db.models.functions import TruncDate
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
import plotly.offline as plotly
|
||||||
|
import plotly.graph_objs as go
|
||||||
|
|
||||||
|
from core.utils import duration_parts
|
||||||
|
|
||||||
|
from reports import utils
|
||||||
|
|
||||||
|
|
||||||
|
def feeding_intervals(instances):
|
||||||
|
"""
|
||||||
|
Create a graph showing intervals of feeding instances over time.
|
||||||
|
|
||||||
|
:param instances: a QuerySet of Feeding instances.
|
||||||
|
:returns: a tuple of the the graph's html and javascript.
|
||||||
|
"""
|
||||||
|
totals = instances.annotate(count=Count("id")).order_by("start")
|
||||||
|
|
||||||
|
intervals = []
|
||||||
|
last_feeding = totals.first()
|
||||||
|
for feeding in totals[1:]:
|
||||||
|
interval = feeding.start - last_feeding.start
|
||||||
|
if interval.total_seconds() > 0:
|
||||||
|
intervals.append(interval)
|
||||||
|
last_feeding = feeding
|
||||||
|
|
||||||
|
trace_avg = go.Scatter(
|
||||||
|
name=_("Interval"),
|
||||||
|
line=dict(shape="spline"),
|
||||||
|
x=list(totals.values_list("start", flat=True)),
|
||||||
|
y=[i.total_seconds() / 3600 for i in intervals],
|
||||||
|
hoverinfo="text",
|
||||||
|
text=[_duration_string_hms(i) for i in intervals],
|
||||||
|
)
|
||||||
|
|
||||||
|
layout_args = utils.default_graph_layout_options()
|
||||||
|
layout_args["title"] = _("<b>Feeding intervals</b>")
|
||||||
|
layout_args["xaxis"]["title"] = _("Date")
|
||||||
|
layout_args["xaxis"]["rangeselector"] = utils.rangeselector_date()
|
||||||
|
layout_args["yaxis"]["title"] = _("Feeding interval (hours)")
|
||||||
|
|
||||||
|
fig = go.Figure({"data": [trace_avg], "layout": go.Layout(**layout_args)})
|
||||||
|
output = plotly.plot(fig, output_type="div", include_plotlyjs=False)
|
||||||
|
return utils.split_graph_output(output)
|
||||||
|
|
||||||
|
|
||||||
|
def _duration_string_hms(duration):
|
||||||
|
"""
|
||||||
|
Format a duration string with hours, minutes and seconds. This is
|
||||||
|
intended to fit better in smaller spaces on a graph.
|
||||||
|
:returns: a string of the form Xm.
|
||||||
|
"""
|
||||||
|
h, m, s = duration_parts(duration)
|
||||||
|
return "{}h{}m{}s".format(h, m, s)
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends 'reports/report_base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Diaper Change Intervals" %} - {{ object }}{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ block.super }}
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{% trans "Diaper Change Intervals" %}</li>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends 'reports/report_base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Feeding Intervals" %} - {{ object }}{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ block.super }}
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{% trans "Feeding Intervals" %}</li>
|
||||||
|
{% endblock %}
|
|
@ -20,10 +20,12 @@
|
||||||
<a href="{% url 'reports:report-diaperchange-amounts-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Diaper Change Amounts" %}</a>
|
<a href="{% url 'reports:report-diaperchange-amounts-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Diaper Change Amounts" %}</a>
|
||||||
<a href="{% url 'reports:report-diaperchange-types-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Diaper Change Types" %}</a>
|
<a href="{% url 'reports:report-diaperchange-types-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Diaper Change Types" %}</a>
|
||||||
<a href="{% url 'reports:report-diaperchange-lifetimes-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Diaper Lifetimes" %}</a>
|
<a href="{% url 'reports:report-diaperchange-lifetimes-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Diaper Lifetimes" %}</a>
|
||||||
|
<a href="{% url 'reports:report-diaperchange-intervals-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Diaper Intervals" %}</a>
|
||||||
<a href="{% url 'reports:report-feeding-amounts-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Feeding Amounts" %}</a>
|
<a href="{% url 'reports:report-feeding-amounts-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Feeding Amounts" %}</a>
|
||||||
<a href="{% url 'reports:report-feeding-duration-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Feeding Durations (Average)" %}</a>
|
<a href="{% url 'reports:report-feeding-duration-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Feeding Durations (Average)" %}</a>
|
||||||
<a href="{% url 'reports:report-head-circumference-change-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Head Circumference" %}</a>
|
<a href="{% url 'reports:report-head-circumference-change-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Head Circumference" %}</a>
|
||||||
<a href="{% url 'reports:report-height-change-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Height" %}</a>
|
<a href="{% url 'reports:report-height-change-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Height" %}</a>
|
||||||
|
<a href="{% url 'reports:report-feeding-intervals-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Feeding Intervals" %}</a>
|
||||||
<a href="{% url 'reports:report-pumping-amounts-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Pumping Amounts" %}</a>
|
<a href="{% url 'reports:report-pumping-amounts-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Pumping Amounts" %}</a>
|
||||||
<a href="{% url 'reports:report-sleep-pattern-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Sleep Pattern" %}</a>
|
<a href="{% url 'reports:report-sleep-pattern-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Sleep Pattern" %}</a>
|
||||||
<a href="{% url 'reports:report-sleep-totals-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Sleep Totals" %}</a>
|
<a href="{% url 'reports:report-sleep-totals-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Sleep Totals" %}</a>
|
||||||
|
|
|
@ -46,11 +46,15 @@ class ViewsTestCase(TestCase):
|
||||||
self.assertEqual(page.status_code, 200)
|
self.assertEqual(page.status_code, 200)
|
||||||
page = self.c.get("{}/changes/types/".format(base_url))
|
page = self.c.get("{}/changes/types/".format(base_url))
|
||||||
self.assertEqual(page.status_code, 200)
|
self.assertEqual(page.status_code, 200)
|
||||||
|
page = self.c.get("{}/changes/intervals/".format(base_url))
|
||||||
|
self.assertEqual(page.status_code, 200)
|
||||||
|
|
||||||
page = self.c.get("{}/feeding/amounts/".format(base_url))
|
page = self.c.get("{}/feeding/amounts/".format(base_url))
|
||||||
self.assertEqual(page.status_code, 200)
|
self.assertEqual(page.status_code, 200)
|
||||||
page = self.c.get("{}/feeding/duration/".format(base_url))
|
page = self.c.get("{}/feeding/duration/".format(base_url))
|
||||||
self.assertEqual(page.status_code, 200)
|
self.assertEqual(page.status_code, 200)
|
||||||
|
page = self.c.get("{}/feeding/intervals/".format(base_url))
|
||||||
|
self.assertEqual(page.status_code, 200)
|
||||||
|
|
||||||
page = self.c.get("{}/head-circumference/head-circumference/".format(base_url))
|
page = self.c.get("{}/head-circumference/head-circumference/".format(base_url))
|
||||||
self.assertEqual(page.status_code, 200)
|
self.assertEqual(page.status_code, 200)
|
||||||
|
|
|
@ -31,6 +31,11 @@ urlpatterns = [
|
||||||
views.DiaperChangeTypesChildReport.as_view(),
|
views.DiaperChangeTypesChildReport.as_view(),
|
||||||
name="report-diaperchange-types-child",
|
name="report-diaperchange-types-child",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"children/<str:slug>/reports/changes/intervals/",
|
||||||
|
views.DiaperChangeIntervalsChildReport.as_view(),
|
||||||
|
name="report-diaperchange-intervals-child",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"children/<str:slug>/reports/feeding/amounts/",
|
"children/<str:slug>/reports/feeding/amounts/",
|
||||||
views.FeedingAmountsChildReport.as_view(),
|
views.FeedingAmountsChildReport.as_view(),
|
||||||
|
@ -56,6 +61,11 @@ urlpatterns = [
|
||||||
views.PumpingAmounts.as_view(),
|
views.PumpingAmounts.as_view(),
|
||||||
name="report-pumping-amounts-child",
|
name="report-pumping-amounts-child",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"children/<str:slug>/reports/feeding/intervals/",
|
||||||
|
views.FeedingIntervalsChildReport.as_view(),
|
||||||
|
name="report-feeding-intervals-child",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"children/<str:slug>/reports/sleep/pattern/",
|
"children/<str:slug>/reports/sleep/pattern/",
|
||||||
views.SleepPatternChildReport.as_view(),
|
views.SleepPatternChildReport.as_view(),
|
||||||
|
|
|
@ -80,7 +80,7 @@ class DiaperChangeTypesChildReport(PermissionRequiredMixin, DetailView):
|
||||||
|
|
||||||
model = models.Child
|
model = models.Child
|
||||||
permission_required = ("core.view_child",)
|
permission_required = ("core.view_child",)
|
||||||
template_name = "reports/diaperchange_types.html"
|
template_name = "reports/diaperchange_intervals.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(DiaperChangeTypesChildReport, self).get_context_data(**kwargs)
|
context = super(DiaperChangeTypesChildReport, self).get_context_data(**kwargs)
|
||||||
|
@ -91,6 +91,26 @@ class DiaperChangeTypesChildReport(PermissionRequiredMixin, DetailView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class DiaperChangeIntervalsChildReport(PermissionRequiredMixin, DetailView):
|
||||||
|
"""
|
||||||
|
Graph of diaper change intervals.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = models.Child
|
||||||
|
permission_required = ("core.view_child",)
|
||||||
|
template_name = "reports/diaperchange_intervals.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(DiaperChangeIntervalsChildReport, self).get_context_data(
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
child = context["object"]
|
||||||
|
changes = models.DiaperChange.objects.filter(child=child)
|
||||||
|
if changes:
|
||||||
|
context["html"], context["js"] = graphs.diaperchange_intervals(changes)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class FeedingAmountsChildReport(PermissionRequiredMixin, DetailView):
|
class FeedingAmountsChildReport(PermissionRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Graph of daily feeding amounts over time.
|
Graph of daily feeding amounts over time.
|
||||||
|
@ -137,6 +157,23 @@ class FeedingDurationChildReport(PermissionRequiredMixin, DetailView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class FeedingIntervalsChildReport(PermissionRequiredMixin, DetailView):
|
||||||
|
"""
|
||||||
|
Graph of diaper change intervals.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = models.Child
|
||||||
|
permission_required = ("core.view_child",)
|
||||||
|
template_name = "reports/feeding_intervals.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(FeedingIntervalsChildReport, self).get_context_data(**kwargs)
|
||||||
|
child = context["object"]
|
||||||
|
instances = models.Feeding.objects.filter(child=child)
|
||||||
|
if instances:
|
||||||
|
context["html"], context["js"] = graphs.feeding_intervals(instances)
|
||||||
|
return context
|
||||||
|
|
||||||
class HeadCircumferenceChangeChildReport(PermissionRequiredMixin, DetailView):
|
class HeadCircumferenceChangeChildReport(PermissionRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Graph of head circumference change over time.
|
Graph of head circumference change over time.
|
||||||
|
|
Loading…
Reference in New Issue