mirror of https://github.com/snachodog/mybuddy.git
Add option for hiding empty dashboard cards (#213)
* add option for hiding empty dashboard cards * rework add option for hiding empty dashboard cards missed statistics.html * don't exit early in cards * add forms test for dashboard_hide_empty * add tests for cards * fix early exit in card_diaperchange_latest * change dependency of migration * rename migration * introduce hiding of cards in templates * linting * add context to test_card_diaperchange_last * setup MockUserRequest * add context to all cards test cases * add test for settings_dashboard_hide_empty_on * change dashboard_hide_test, but it doesn't work * add test for _user_wants_hide * fix test_user_wants_hide user object, simpliy check for data['empty'] * add test for user_wants_hide to every card * linting * fix trailing whitespace * rename user_wants_hide to hide_empty * fix hidden statistics * add user.refresh_from_db to test case, add test case for dashboard_refresh_rate * Follow redirect and correct assertion Co-authored-by: jcgoette <jcgoette@gmail.com> Co-authored-by: Benjamin Häublein <benjaminh@debian.vm.hp> Co-authored-by: Christopher C. Wells <git@chris-wells.net>
This commit is contained in:
parent
fe568876c7
commit
1dca1cc050
|
@ -14,7 +14,7 @@ class SettingsInline(admin.StackedInline):
|
||||||
can_delete = False
|
can_delete = False
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(_('Dashboard'), {
|
(_('Dashboard'), {
|
||||||
'fields': ('dashboard_refresh_rate',)
|
'fields': ('dashboard_refresh_rate', 'dashboard_hide_empty',)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -41,4 +41,9 @@ class UserPasswordForm(PasswordChangeForm):
|
||||||
class UserSettingsForm(forms.ModelForm):
|
class UserSettingsForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Settings
|
model = Settings
|
||||||
fields = ['dashboard_refresh_rate', 'language', 'timezone']
|
fields = [
|
||||||
|
'dashboard_refresh_rate',
|
||||||
|
'dashboard_hide_empty',
|
||||||
|
'language',
|
||||||
|
'timezone'
|
||||||
|
]
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.1.5 on 2021-01-19 23:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('babybuddy', '0013_auto_20210411_1241'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='settings',
|
||||||
|
name='dashboard_hide_empty',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Hide Empty Dashboard Cards'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -34,6 +34,11 @@ class Settings(models.Model):
|
||||||
(timezone.timedelta(minutes=15), _('15 min.')),
|
(timezone.timedelta(minutes=15), _('15 min.')),
|
||||||
(timezone.timedelta(minutes=30), _('30 min.')),
|
(timezone.timedelta(minutes=30), _('30 min.')),
|
||||||
])
|
])
|
||||||
|
dashboard_hide_empty = models.BooleanField(
|
||||||
|
verbose_name=_('Hide Empty Dashboard Cards'),
|
||||||
|
default=False,
|
||||||
|
editable=True
|
||||||
|
)
|
||||||
language = models.CharField(
|
language = models.CharField(
|
||||||
choices=settings.LANGUAGES,
|
choices=settings.LANGUAGES,
|
||||||
default=settings.LANGUAGE_CODE,
|
default=settings.LANGUAGE_CODE,
|
||||||
|
|
|
@ -64,6 +64,11 @@
|
||||||
{% include 'babybuddy/form_field.html' %}
|
{% include 'babybuddy/form_field.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
{% with form_settings.dashboard_hide_empty as field %}
|
||||||
|
{% include 'babybuddy/form_field.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "API" %}</legend>
|
<legend>{% trans "API" %}</legend>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import datetime
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.test import Client as HttpClient, override_settings, TestCase
|
from django.test import Client as HttpClient, override_settings, TestCase
|
||||||
|
@ -132,3 +134,26 @@ class FormsTestCase(TestCase):
|
||||||
self.assertEqual(page.status_code, 200)
|
self.assertEqual(page.status_code, 200)
|
||||||
self.assertEqual(timezone.get_current_timezone_name(),
|
self.assertEqual(timezone.get_current_timezone_name(),
|
||||||
params['timezone'])
|
params['timezone'])
|
||||||
|
|
||||||
|
def test_user_settings_dashboard_hide_empty_on(self):
|
||||||
|
self.c.login(**self.credentials)
|
||||||
|
|
||||||
|
params = self.settings_template.copy()
|
||||||
|
params['dashboard_hide_empty'] = 'on'
|
||||||
|
|
||||||
|
page = self.c.post('/user/settings/', data=params, follow=True)
|
||||||
|
self.assertEqual(page.status_code, 200)
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
self.assertTrue(self.user.settings.dashboard_hide_empty)
|
||||||
|
|
||||||
|
def test_user_settings_dashboard_refresh_rate(self):
|
||||||
|
self.c.login(**self.credentials)
|
||||||
|
|
||||||
|
params = self.settings_template.copy()
|
||||||
|
params['dashboard_refresh_rate'] = '0:05:00'
|
||||||
|
|
||||||
|
page = self.c.post('/user/settings/', data=params, follow=True)
|
||||||
|
self.assertEqual(page.status_code, 200)
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
self.assertEqual(self.user.settings.dashboard_refresh_rate,
|
||||||
|
datetime.timedelta(seconds=300))
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
<div class="card card-dashboard card-{{ type }}">
|
{% if not empty or not hide_empty %}
|
||||||
<div class="card-header">
|
<div class="card card-dashboard card-{{ type }}">
|
||||||
<i class="icon icon-{{ type }} pull-left" aria-hidden="true"></i>
|
<div class="card-header">
|
||||||
{% block header %}{% endblock %}
|
<i class="icon icon-{{ type }} pull-left" aria-hidden="true"></i>
|
||||||
|
{% block header %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<span class="card-title"><strong>{% block title %}{% endblock %}</strong></span>
|
||||||
|
<div class="card-text"> {% block content %}{% endblock %} </div>
|
||||||
|
</div>
|
||||||
|
{% block listgroup %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
{% endif %}
|
||||||
<span class="card-title"><strong>{% block title %}{% endblock %}</strong></span>
|
|
||||||
<div class="card-text">{% block content %}{% endblock %}</div>
|
|
||||||
</div>
|
|
||||||
{% block listgroup %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -1,40 +1,46 @@
|
||||||
{% load duration i18n %}
|
{% load duration i18n %}
|
||||||
|
|
||||||
<div class="card card-dashboard card-statistics">
|
{% if not empty or not hide_empty %}
|
||||||
<div class="card-header">
|
<div class="card card-dashboard card-statistics">
|
||||||
<i class="icon icon-graph pull-left" aria-hidden="true"></i>
|
<div class="card-header">
|
||||||
{% trans "Statistics" %}
|
<i class="icon icon-graph pull-left" aria-hidden="true"></i>
|
||||||
</div>
|
{% trans "Statistics" %}
|
||||||
<div class="card-body text-center">
|
</div>
|
||||||
<div id="statistics-carousel" class="carousel slide" data-interval="false">
|
<div class="card-body text-center">
|
||||||
<div class="carousel-inner">
|
{% if stats|length > 0 %}
|
||||||
{% for stat in stats %}
|
<div id="statistics-carousel" class="carousel slide" data-interval="false">
|
||||||
<div class="carousel-item{% if forloop.counter == 1 %} active{% endif %}">
|
<div class="carousel-inner">
|
||||||
<span class="card-title">
|
{% for stat in stats %}
|
||||||
{% if stat.stat %}
|
<div class="carousel-item{% if forloop.counter == 1 %} active{% endif %}">
|
||||||
{% if stat.type == 'duration' %}
|
<span class="card-title">
|
||||||
{{ stat.stat|duration_string:'m' }}
|
{% if stat.stat %}
|
||||||
{% elif stat.type == 'float' %}
|
{% if stat.type == 'duration' %}
|
||||||
{{ stat.stat|floatformat }}
|
{{ stat.stat|duration_string:'m' }}
|
||||||
|
{% elif stat.type == 'float' %}
|
||||||
|
{{ stat.stat|floatformat }}
|
||||||
|
{% else %}
|
||||||
|
{{ stat.stat }}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ stat.stat }}
|
<em>{% trans "Not enough data" %}</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
</span>
|
||||||
<em>{% trans "Not enough data" %}</em>
|
<div class="card-text">{{ stat.title }}</div>
|
||||||
{% endif %}
|
</div>
|
||||||
</span>
|
{% endfor %}
|
||||||
<div class="card-text">{{ stat.title }}</div>
|
</div>
|
||||||
</div>
|
<a class="carousel-control-prev" href="#statistics-carousel" role="button" data-slide="prev">
|
||||||
{% endfor %}
|
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||||
|
<span class="sr-only">{% trans "Previous" %}</span>
|
||||||
|
</a>
|
||||||
|
<a class="carousel-control-next" href="#statistics-carousel" role="button" data-slide="next">
|
||||||
|
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||||
|
<span class="sr-only">{% trans "Next" %}</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a class="carousel-control-prev" href="#statistics-carousel" role="button" data-slide="prev">
|
{% else %}
|
||||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
<span class="card-title"><strong>{% trans "No data yet" %}</strong></span>
|
||||||
<span class="sr-only">{% trans "Previous" %}</span>
|
{% endif %}
|
||||||
</a>
|
|
||||||
<a class="carousel-control-next" href="#statistics-carousel" role="button" data-slide="next">
|
|
||||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
|
||||||
<span class="sr-only">{% trans "Next" %}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
|
|
|
@ -9,12 +9,15 @@ from datetime import date, datetime, time
|
||||||
|
|
||||||
from core import models
|
from core import models
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cards/diaperchange_last.html')
|
def _hide_empty(context):
|
||||||
def card_diaperchange_last(child):
|
return context['request'].user.settings.dashboard_hide_empty
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('cards/diaperchange_last.html', takes_context=True)
|
||||||
|
def card_diaperchange_last(context, child):
|
||||||
"""
|
"""
|
||||||
Information about the most recent diaper change.
|
Information about the most recent diaper change.
|
||||||
:param child: an instance of the Child model.
|
:param child: an instance of the Child model.
|
||||||
|
@ -22,11 +25,18 @@ def card_diaperchange_last(child):
|
||||||
"""
|
"""
|
||||||
instance = models.DiaperChange.objects.filter(
|
instance = models.DiaperChange.objects.filter(
|
||||||
child=child).order_by('-time').first()
|
child=child).order_by('-time').first()
|
||||||
return {'type': 'diaperchange', 'change': instance}
|
empty = not instance
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'diaperchange',
|
||||||
|
'change': instance,
|
||||||
|
'empty': empty,
|
||||||
|
'hide_empty': _hide_empty(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cards/diaperchange_types.html')
|
@register.inclusion_tag('cards/diaperchange_types.html', takes_context=True)
|
||||||
def card_diaperchange_types(child, date=None):
|
def card_diaperchange_types(context, child, date=None):
|
||||||
"""
|
"""
|
||||||
Creates a break down of wet and solid Diaper Change instances for the past
|
Creates a break down of wet and solid Diaper Change instances for the past
|
||||||
seven days.
|
seven days.
|
||||||
|
@ -51,6 +61,8 @@ def card_diaperchange_types(child, date=None):
|
||||||
|
|
||||||
instances = models.DiaperChange.objects.filter(child=child) \
|
instances = models.DiaperChange.objects.filter(child=child) \
|
||||||
.filter(time__gt=min_date).filter(time__lt=max_date).order_by('-time')
|
.filter(time__gt=min_date).filter(time__lt=max_date).order_by('-time')
|
||||||
|
empty = len(instances) == 0
|
||||||
|
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
key = (max_date - instance.time).days
|
key = (max_date - instance.time).days
|
||||||
if instance.wet:
|
if instance.wet:
|
||||||
|
@ -65,11 +77,17 @@ def card_diaperchange_types(child, date=None):
|
||||||
stats[key]['wet_pct'] = info['wet'] / total * 100
|
stats[key]['wet_pct'] = info['wet'] / total * 100
|
||||||
stats[key]['solid_pct'] = info['solid'] / total * 100
|
stats[key]['solid_pct'] = info['solid'] / total * 100
|
||||||
|
|
||||||
return {'type': 'diaperchange', 'stats': stats, 'total': week_total}
|
return {
|
||||||
|
'type': 'diaperchange',
|
||||||
|
'stats': stats,
|
||||||
|
'total': week_total,
|
||||||
|
'empty': empty,
|
||||||
|
'hide_empty': _hide_empty(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cards/feeding_day.html')
|
@register.inclusion_tag('cards/feeding_day.html', takes_context=True)
|
||||||
def card_feeding_day(child, date=None):
|
def card_feeding_day(context, child, date=None):
|
||||||
"""
|
"""
|
||||||
Filters Feeding instances to get total amount for a specific date.
|
Filters Feeding instances to get total amount for a specific date.
|
||||||
:param child: an instance of the Child model.
|
:param child: an instance of the Child model.
|
||||||
|
@ -78,6 +96,7 @@ def card_feeding_day(child, date=None):
|
||||||
"""
|
"""
|
||||||
if not date:
|
if not date:
|
||||||
date = timezone.localtime().date()
|
date = timezone.localtime().date()
|
||||||
|
|
||||||
instances = models.Feeding.objects.filter(child=child).filter(
|
instances = models.Feeding.objects.filter(child=child).filter(
|
||||||
start__year=date.year,
|
start__year=date.year,
|
||||||
start__month=date.month,
|
start__month=date.month,
|
||||||
|
@ -89,12 +108,19 @@ def card_feeding_day(child, date=None):
|
||||||
|
|
||||||
total = sum([instance.amount for instance in instances if instance.amount])
|
total = sum([instance.amount for instance in instances if instance.amount])
|
||||||
count = len(instances)
|
count = len(instances)
|
||||||
|
empty = len(instances) == 0
|
||||||
|
|
||||||
return {'type': 'feeding', 'total': total, 'count': count}
|
return {
|
||||||
|
'type': 'feeding',
|
||||||
|
'total': total,
|
||||||
|
'count': count,
|
||||||
|
'empty': empty,
|
||||||
|
'hide_empty': _hide_empty(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cards/feeding_last.html')
|
@register.inclusion_tag('cards/feeding_last.html', takes_context=True)
|
||||||
def card_feeding_last(child):
|
def card_feeding_last(context, child):
|
||||||
"""
|
"""
|
||||||
Information about the most recent feeding.
|
Information about the most recent feeding.
|
||||||
:param child: an instance of the Child model.
|
:param child: an instance of the Child model.
|
||||||
|
@ -102,11 +128,18 @@ def card_feeding_last(child):
|
||||||
"""
|
"""
|
||||||
instance = models.Feeding.objects.filter(child=child) \
|
instance = models.Feeding.objects.filter(child=child) \
|
||||||
.order_by('-end').first()
|
.order_by('-end').first()
|
||||||
return {'type': 'feeding', 'feeding': instance}
|
empty = not instance
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'feeding',
|
||||||
|
'feeding': instance,
|
||||||
|
'empty': empty,
|
||||||
|
'hide_empty': _hide_empty(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cards/feeding_last_method.html')
|
@register.inclusion_tag('cards/feeding_last_method.html', takes_context=True)
|
||||||
def card_feeding_last_method(child):
|
def card_feeding_last_method(context, child):
|
||||||
"""
|
"""
|
||||||
Information about the three most recent feeding methods.
|
Information about the three most recent feeding methods.
|
||||||
:param child: an instance of the Child model.
|
:param child: an instance of the Child model.
|
||||||
|
@ -114,12 +147,19 @@ def card_feeding_last_method(child):
|
||||||
"""
|
"""
|
||||||
instances = models.Feeding.objects.filter(child=child) \
|
instances = models.Feeding.objects.filter(child=child) \
|
||||||
.order_by('-end')[:3]
|
.order_by('-end')[:3]
|
||||||
|
empty = len(instances) == 0
|
||||||
|
|
||||||
# Results are reversed for carousel forward/back behavior.
|
# Results are reversed for carousel forward/back behavior.
|
||||||
return {'type': 'feeding', 'feedings': list(reversed(instances))}
|
return {
|
||||||
|
'type': 'feeding',
|
||||||
|
'feedings': list(reversed(instances)),
|
||||||
|
'empty': empty,
|
||||||
|
'hide_empty': _hide_empty(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cards/sleep_last.html')
|
@register.inclusion_tag('cards/sleep_last.html', takes_context=True)
|
||||||
def card_sleep_last(child):
|
def card_sleep_last(context, child):
|
||||||
"""
|
"""
|
||||||
Information about the most recent sleep entry.
|
Information about the most recent sleep entry.
|
||||||
:param child: an instance of the Child model.
|
:param child: an instance of the Child model.
|
||||||
|
@ -127,11 +167,18 @@ def card_sleep_last(child):
|
||||||
"""
|
"""
|
||||||
instance = models.Sleep.objects.filter(child=child) \
|
instance = models.Sleep.objects.filter(child=child) \
|
||||||
.order_by('-end').first()
|
.order_by('-end').first()
|
||||||
return {'type': 'sleep', 'sleep': instance}
|
empty = not instance
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'sleep',
|
||||||
|
'sleep': instance,
|
||||||
|
'empty': empty,
|
||||||
|
'hide_empty': _hide_empty(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cards/sleep_day.html')
|
@register.inclusion_tag('cards/sleep_day.html', takes_context=True)
|
||||||
def card_sleep_day(child, date=None):
|
def card_sleep_day(context, child, date=None):
|
||||||
"""
|
"""
|
||||||
Filters Sleep instances to get count and total values for a specific date.
|
Filters Sleep instances to get count and total values for a specific date.
|
||||||
:param child: an instance of the Child model.
|
:param child: an instance of the Child model.
|
||||||
|
@ -147,6 +194,7 @@ def card_sleep_day(child, date=None):
|
||||||
end__year=date.year,
|
end__year=date.year,
|
||||||
end__month=date.month,
|
end__month=date.month,
|
||||||
end__day=date.day)
|
end__day=date.day)
|
||||||
|
empty = len(instances) == 0
|
||||||
|
|
||||||
total = timezone.timedelta(seconds=0)
|
total = timezone.timedelta(seconds=0)
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
|
@ -161,11 +209,17 @@ def card_sleep_day(child, date=None):
|
||||||
|
|
||||||
count = len(instances)
|
count = len(instances)
|
||||||
|
|
||||||
return {'type': 'sleep', 'total': total, 'count': count}
|
return {
|
||||||
|
'type': 'sleep',
|
||||||
|
'total': total,
|
||||||
|
'count': count,
|
||||||
|
'empty': empty,
|
||||||
|
'hide_empty': _hide_empty(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cards/sleep_naps_day.html')
|
@register.inclusion_tag('cards/sleep_naps_day.html', takes_context=True)
|
||||||
def card_sleep_naps_day(child, date=None):
|
def card_sleep_naps_day(context, child, date=None):
|
||||||
"""
|
"""
|
||||||
Filters Sleep instances categorized as naps and generates statistics for a
|
Filters Sleep instances categorized as naps and generates statistics for a
|
||||||
specific date.
|
specific date.
|
||||||
|
@ -182,14 +236,18 @@ def card_sleep_naps_day(child, date=None):
|
||||||
end__year=date.year,
|
end__year=date.year,
|
||||||
end__month=date.month,
|
end__month=date.month,
|
||||||
end__day=date.day)
|
end__day=date.day)
|
||||||
|
empty = len(instances) == 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'type': 'sleep',
|
'type': 'sleep',
|
||||||
'total': instances.aggregate(Sum('duration'))['duration__sum'],
|
'total': instances.aggregate(Sum('duration'))['duration__sum'],
|
||||||
'count': len(instances)}
|
'count': len(instances),
|
||||||
|
'empty': empty, 'hide_empty': _hide_empty(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cards/statistics.html')
|
@register.inclusion_tag('cards/statistics.html', takes_context=True)
|
||||||
def card_statistics(child):
|
def card_statistics(context, child):
|
||||||
"""
|
"""
|
||||||
Statistics data for all models.
|
Statistics data for all models.
|
||||||
:param child: an instance of the Child model.
|
:param child: an instance of the Child model.
|
||||||
|
@ -198,45 +256,56 @@ def card_statistics(child):
|
||||||
stats = []
|
stats = []
|
||||||
|
|
||||||
changes = _diaperchange_statistics(child)
|
changes = _diaperchange_statistics(child)
|
||||||
stats.append({
|
if changes:
|
||||||
'type': 'duration',
|
|
||||||
'stat': changes['btwn_average'],
|
|
||||||
'title': _('Diaper change frequency')})
|
|
||||||
|
|
||||||
feedings = _feeding_statistics(child)
|
|
||||||
for item in feedings:
|
|
||||||
stats.append({
|
stats.append({
|
||||||
'type': 'duration',
|
'type': 'duration',
|
||||||
'stat': item['btwn_average'],
|
'stat': changes['btwn_average'],
|
||||||
'title': item['title']})
|
'title': _('Diaper change frequency')})
|
||||||
|
|
||||||
|
feedings = _feeding_statistics(child)
|
||||||
|
if feedings:
|
||||||
|
for item in feedings:
|
||||||
|
stats.append({
|
||||||
|
'type': 'duration',
|
||||||
|
'stat': item['btwn_average'],
|
||||||
|
'title': item['title']})
|
||||||
|
|
||||||
naps = _nap_statistics(child)
|
naps = _nap_statistics(child)
|
||||||
stats.append({
|
if naps:
|
||||||
'type': 'duration',
|
stats.append({
|
||||||
'stat': naps['average'],
|
'type': 'duration',
|
||||||
'title': _('Average nap duration')})
|
'stat': naps['average'],
|
||||||
stats.append({
|
'title': _('Average nap duration')})
|
||||||
'type': 'float',
|
stats.append({
|
||||||
'stat': naps['avg_per_day'],
|
'type': 'float',
|
||||||
'title': _('Average naps per day')})
|
'stat': naps['avg_per_day'],
|
||||||
|
'title': _('Average naps per day')})
|
||||||
|
|
||||||
sleep = _sleep_statistics(child)
|
sleep = _sleep_statistics(child)
|
||||||
stats.append({
|
if sleep:
|
||||||
'type': 'duration',
|
stats.append({
|
||||||
'stat': sleep['average'],
|
'type': 'duration',
|
||||||
'title': _('Average sleep duration')})
|
'stat': sleep['average'],
|
||||||
stats.append({
|
'title': _('Average sleep duration')})
|
||||||
'type': 'duration',
|
stats.append({
|
||||||
'stat': sleep['btwn_average'],
|
'type': 'duration',
|
||||||
'title': _('Average awake duration')})
|
'stat': sleep['btwn_average'],
|
||||||
|
'title': _('Average awake duration')})
|
||||||
|
|
||||||
weight = _weight_statistics(child)
|
weight = _weight_statistics(child)
|
||||||
stats.append({
|
if weight:
|
||||||
'type': 'float',
|
stats.append({
|
||||||
'stat': weight['change_weekly'],
|
'type': 'float',
|
||||||
'title': _('Weight change per week')})
|
'stat': weight['change_weekly'],
|
||||||
|
'title': _('Weight change per week')})
|
||||||
|
|
||||||
return {'stats': stats}
|
empty = len(stats) == 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'stats': stats,
|
||||||
|
'empty': empty,
|
||||||
|
'hide_empty': _hide_empty(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _diaperchange_statistics(child):
|
def _diaperchange_statistics(child):
|
||||||
|
@ -247,6 +316,8 @@ def _diaperchange_statistics(child):
|
||||||
"""
|
"""
|
||||||
instances = models.DiaperChange.objects.filter(child=child) \
|
instances = models.DiaperChange.objects.filter(child=child) \
|
||||||
.order_by('time')
|
.order_by('time')
|
||||||
|
if len(instances) == 0:
|
||||||
|
return False
|
||||||
changes = {
|
changes = {
|
||||||
'btwn_total': timezone.timedelta(0),
|
'btwn_total': timezone.timedelta(0),
|
||||||
'btwn_count': instances.count() - 1,
|
'btwn_count': instances.count() - 1,
|
||||||
|
@ -292,6 +363,8 @@ def _feeding_statistics(child):
|
||||||
timespan['btwn_average'] = 0.0
|
timespan['btwn_average'] = 0.0
|
||||||
|
|
||||||
instances = models.Feeding.objects.filter(child=child).order_by('start')
|
instances = models.Feeding.objects.filter(child=child).order_by('start')
|
||||||
|
if len(instances) == 0:
|
||||||
|
return False
|
||||||
last_instance = None
|
last_instance = None
|
||||||
|
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
|
@ -317,6 +390,8 @@ def _nap_statistics(child):
|
||||||
:returns: a dictionary of statistics.
|
:returns: a dictionary of statistics.
|
||||||
"""
|
"""
|
||||||
instances = models.Sleep.naps.filter(child=child).order_by('start')
|
instances = models.Sleep.naps.filter(child=child).order_by('start')
|
||||||
|
if len(instances) == 0:
|
||||||
|
return False
|
||||||
naps = {
|
naps = {
|
||||||
'total': instances.aggregate(Sum('duration'))['duration__sum'],
|
'total': instances.aggregate(Sum('duration'))['duration__sum'],
|
||||||
'count': instances.count(),
|
'count': instances.count(),
|
||||||
|
@ -340,6 +415,9 @@ def _sleep_statistics(child):
|
||||||
:returns: a dictionary of statistics.
|
:returns: a dictionary of statistics.
|
||||||
"""
|
"""
|
||||||
instances = models.Sleep.objects.filter(child=child).order_by('start')
|
instances = models.Sleep.objects.filter(child=child).order_by('start')
|
||||||
|
if len(instances) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
sleep = {
|
sleep = {
|
||||||
'total': instances.aggregate(Sum('duration'))['duration__sum'],
|
'total': instances.aggregate(Sum('duration'))['duration__sum'],
|
||||||
'count': instances.count(),
|
'count': instances.count(),
|
||||||
|
@ -371,6 +449,9 @@ def _weight_statistics(child):
|
||||||
weight = {'change_weekly': 0.0}
|
weight = {'change_weekly': 0.0}
|
||||||
|
|
||||||
instances = models.Weight.objects.filter(child=child).order_by('-date')
|
instances = models.Weight.objects.filter(child=child).order_by('-date')
|
||||||
|
if len(instances) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
newest = instances.first()
|
newest = instances.first()
|
||||||
oldest = instances.last()
|
oldest = instances.last()
|
||||||
|
|
||||||
|
@ -382,8 +463,8 @@ def _weight_statistics(child):
|
||||||
return weight
|
return weight
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cards/timer_list.html')
|
@register.inclusion_tag('cards/timer_list.html', takes_context=True)
|
||||||
def card_timer_list(child=None):
|
def card_timer_list(context, child=None):
|
||||||
"""
|
"""
|
||||||
Filters for currently active Timer instances, optionally by child.
|
Filters for currently active Timer instances, optionally by child.
|
||||||
:param child: an instance of the Child model.
|
:param child: an instance of the Child model.
|
||||||
|
@ -397,11 +478,18 @@ def card_timer_list(child=None):
|
||||||
).order_by('-start')
|
).order_by('-start')
|
||||||
else:
|
else:
|
||||||
instances = models.Timer.objects.filter(active=True).order_by('-start')
|
instances = models.Timer.objects.filter(active=True).order_by('-start')
|
||||||
return {'type': 'timer', 'instances': list(instances)}
|
empty = len(instances) == 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'timer',
|
||||||
|
'instances': list(instances),
|
||||||
|
'empty': empty,
|
||||||
|
'hide_empty': _hide_empty(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cards/tummytime_last.html')
|
@register.inclusion_tag('cards/tummytime_last.html', takes_context=True)
|
||||||
def card_tummytime_last(child):
|
def card_tummytime_last(context, child):
|
||||||
"""
|
"""
|
||||||
Filters the most recent tummy time.
|
Filters the most recent tummy time.
|
||||||
:param child: an instance of the Child model.
|
:param child: an instance of the Child model.
|
||||||
|
@ -409,11 +497,18 @@ def card_tummytime_last(child):
|
||||||
"""
|
"""
|
||||||
instance = models.TummyTime.objects.filter(child=child) \
|
instance = models.TummyTime.objects.filter(child=child) \
|
||||||
.order_by('-end').first()
|
.order_by('-end').first()
|
||||||
return {'type': 'tummytime', 'tummytime': instance}
|
empty = not instance
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'tummytime',
|
||||||
|
'tummytime': instance,
|
||||||
|
'empty': empty,
|
||||||
|
'hide_empty': _hide_empty(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cards/tummytime_day.html')
|
@register.inclusion_tag('cards/tummytime_day.html', takes_context=True)
|
||||||
def card_tummytime_day(child, date=None):
|
def card_tummytime_day(context, child, date=None):
|
||||||
"""
|
"""
|
||||||
Filters Tummy Time instances and generates statistics for a specific date.
|
Filters Tummy Time instances and generates statistics for a specific date.
|
||||||
:param child: an instance of the Child model.
|
:param child: an instance of the Child model.
|
||||||
|
@ -425,14 +520,20 @@ def card_tummytime_day(child, date=None):
|
||||||
instances = models.TummyTime.objects.filter(
|
instances = models.TummyTime.objects.filter(
|
||||||
child=child, end__year=date.year, end__month=date.month,
|
child=child, end__year=date.year, end__month=date.month,
|
||||||
end__day=date.day).order_by('-end')
|
end__day=date.day).order_by('-end')
|
||||||
|
empty = len(instances) == 0
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
'total': timezone.timedelta(seconds=0),
|
'total': timezone.timedelta(seconds=0),
|
||||||
'count': instances.count()
|
'count': instances.count()
|
||||||
}
|
}
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
stats['total'] += timezone.timedelta(seconds=instance.duration.seconds)
|
stats['total'] += timezone.timedelta(seconds=instance.duration.seconds)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'type': 'tummytime',
|
'type': 'tummytime',
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
'instances': instances,
|
'instances': instances,
|
||||||
'last': instances.first()}
|
'last': instances.first(),
|
||||||
|
'empty': empty,
|
||||||
|
'hide_empty': _hide_empty(context)
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,11 @@ from core import models
|
||||||
from dashboard.templatetags import cards
|
from dashboard.templatetags import cards
|
||||||
|
|
||||||
|
|
||||||
|
class MockUserRequest:
|
||||||
|
def __init__(self, user):
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
|
||||||
class TemplateTagsTestCase(TestCase):
|
class TemplateTagsTestCase(TestCase):
|
||||||
fixtures = ['tests.json']
|
fixtures = ['tests.json']
|
||||||
|
|
||||||
|
@ -17,6 +22,7 @@ class TemplateTagsTestCase(TestCase):
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super(TemplateTagsTestCase, cls).setUpClass()
|
super(TemplateTagsTestCase, cls).setUpClass()
|
||||||
cls.child = models.Child.objects.first()
|
cls.child = models.Child.objects.first()
|
||||||
|
cls.context = {'request': MockUserRequest(User.objects.first())}
|
||||||
|
|
||||||
# Ensure timezone matches the one defined by fixtures.
|
# Ensure timezone matches the one defined by fixtures.
|
||||||
user_timezone = Settings.objects.first().timezone
|
user_timezone = Settings.objects.first().timezone
|
||||||
|
@ -26,14 +32,26 @@ class TemplateTagsTestCase(TestCase):
|
||||||
date = timezone.localtime().strptime('2017-11-18', '%Y-%m-%d')
|
date = timezone.localtime().strptime('2017-11-18', '%Y-%m-%d')
|
||||||
cls.date = timezone.make_aware(date)
|
cls.date = timezone.make_aware(date)
|
||||||
|
|
||||||
|
def test_hide_empty(self):
|
||||||
|
request = MockUserRequest(User.objects.first())
|
||||||
|
request.user.settings.dashboard_hide_empty = True
|
||||||
|
context = {'request': request}
|
||||||
|
hide_empty = cards._hide_empty(context)
|
||||||
|
self.assertTrue(hide_empty)
|
||||||
|
|
||||||
def test_card_diaperchange_last(self):
|
def test_card_diaperchange_last(self):
|
||||||
data = cards.card_diaperchange_last(self.child)
|
data = cards.card_diaperchange_last(self.context, self.child)
|
||||||
self.assertEqual(data['type'], 'diaperchange')
|
self.assertEqual(data['type'], 'diaperchange')
|
||||||
|
self.assertFalse(data['empty'])
|
||||||
|
self.assertFalse(data['hide_empty'])
|
||||||
self.assertIsInstance(data['change'], models.DiaperChange)
|
self.assertIsInstance(data['change'], models.DiaperChange)
|
||||||
self.assertEqual(data['change'], models.DiaperChange.objects.first())
|
self.assertEqual(data['change'], models.DiaperChange.objects.first())
|
||||||
|
|
||||||
def test_card_diaperchange_types(self):
|
def test_card_diaperchange_types(self):
|
||||||
data = cards.card_diaperchange_types(self.child, self.date)
|
data = cards.card_diaperchange_types(
|
||||||
|
self.context,
|
||||||
|
self.child,
|
||||||
|
self.date)
|
||||||
self.assertEqual(data['type'], 'diaperchange')
|
self.assertEqual(data['type'], 'diaperchange')
|
||||||
stats = {
|
stats = {
|
||||||
0: {'wet_pct': 50.0, 'solid_pct': 50.0, 'solid': 1, 'wet': 1},
|
0: {'wet_pct': 50.0, 'solid_pct': 50.0, 'solid': 1, 'wet': 1},
|
||||||
|
@ -47,20 +65,26 @@ class TemplateTagsTestCase(TestCase):
|
||||||
self.assertEqual(data['stats'], stats)
|
self.assertEqual(data['stats'], stats)
|
||||||
|
|
||||||
def test_card_feeding_day(self):
|
def test_card_feeding_day(self):
|
||||||
data = cards.card_feeding_day(self.child, self.date)
|
data = cards.card_feeding_day(self.context, self.child, self.date)
|
||||||
self.assertEqual(data['type'], 'feeding')
|
self.assertEqual(data['type'], 'feeding')
|
||||||
|
self.assertFalse(data['empty'])
|
||||||
|
self.assertFalse(data['hide_empty'])
|
||||||
self.assertEqual(data['total'], 2.5)
|
self.assertEqual(data['total'], 2.5)
|
||||||
self.assertEqual(data['count'], 3)
|
self.assertEqual(data['count'], 3)
|
||||||
|
|
||||||
def test_card_feeding_last(self):
|
def test_card_feeding_last(self):
|
||||||
data = cards.card_feeding_last(self.child)
|
data = cards.card_feeding_last(self.context, self.child)
|
||||||
self.assertEqual(data['type'], 'feeding')
|
self.assertEqual(data['type'], 'feeding')
|
||||||
|
self.assertFalse(data['empty'])
|
||||||
|
self.assertFalse(data['hide_empty'])
|
||||||
self.assertIsInstance(data['feeding'], models.Feeding)
|
self.assertIsInstance(data['feeding'], models.Feeding)
|
||||||
self.assertEqual(data['feeding'], models.Feeding.objects.first())
|
self.assertEqual(data['feeding'], models.Feeding.objects.first())
|
||||||
|
|
||||||
def test_card_feeding_last_method(self):
|
def test_card_feeding_last_method(self):
|
||||||
data = cards.card_feeding_last_method(self.child)
|
data = cards.card_feeding_last_method(self.context, self.child)
|
||||||
self.assertEqual(data['type'], 'feeding')
|
self.assertEqual(data['type'], 'feeding')
|
||||||
|
self.assertFalse(data['empty'])
|
||||||
|
self.assertFalse(data['hide_empty'])
|
||||||
self.assertEqual(len(data['feedings']), 3)
|
self.assertEqual(len(data['feedings']), 3)
|
||||||
for feeding in data['feedings']:
|
for feeding in data['feedings']:
|
||||||
self.assertIsInstance(feeding, models.Feeding)
|
self.assertIsInstance(feeding, models.Feeding)
|
||||||
|
@ -69,25 +93,38 @@ class TemplateTagsTestCase(TestCase):
|
||||||
models.Feeding.objects.first().method)
|
models.Feeding.objects.first().method)
|
||||||
|
|
||||||
def test_card_sleep_last(self):
|
def test_card_sleep_last(self):
|
||||||
data = cards.card_sleep_last(self.child)
|
data = cards.card_sleep_last(self.context, self.child)
|
||||||
self.assertEqual(data['type'], 'sleep')
|
self.assertEqual(data['type'], 'sleep')
|
||||||
|
self.assertFalse(data['empty'])
|
||||||
|
self.assertFalse(data['hide_empty'])
|
||||||
self.assertIsInstance(data['sleep'], models.Sleep)
|
self.assertIsInstance(data['sleep'], models.Sleep)
|
||||||
self.assertEqual(data['sleep'], models.Sleep.objects.first())
|
self.assertEqual(data['sleep'], models.Sleep.objects.first())
|
||||||
|
|
||||||
def test_card_sleep_day(self):
|
def test_card_sleep_last_empty(self):
|
||||||
data = cards.card_sleep_day(self.child, self.date)
|
models.Sleep.objects.all().delete()
|
||||||
|
data = cards.card_sleep_last(self.context, self.child)
|
||||||
self.assertEqual(data['type'], 'sleep')
|
self.assertEqual(data['type'], 'sleep')
|
||||||
|
self.assertTrue(data['empty'])
|
||||||
|
self.assertFalse(data['hide_empty'])
|
||||||
|
|
||||||
|
def test_card_sleep_day(self):
|
||||||
|
data = cards.card_sleep_day(self.context, self.child, self.date)
|
||||||
|
self.assertEqual(data['type'], 'sleep')
|
||||||
|
self.assertFalse(data['empty'])
|
||||||
|
self.assertFalse(data['hide_empty'])
|
||||||
self.assertEqual(data['total'], timezone.timedelta(2, 7200))
|
self.assertEqual(data['total'], timezone.timedelta(2, 7200))
|
||||||
self.assertEqual(data['count'], 4)
|
self.assertEqual(data['count'], 4)
|
||||||
|
|
||||||
def test_card_sleep_naps_day(self):
|
def test_card_sleep_naps_day(self):
|
||||||
data = cards.card_sleep_naps_day(self.child, self.date)
|
data = cards.card_sleep_naps_day(self.context, self.child, self.date)
|
||||||
self.assertEqual(data['type'], 'sleep')
|
self.assertEqual(data['type'], 'sleep')
|
||||||
|
self.assertFalse(data['empty'])
|
||||||
|
self.assertFalse(data['hide_empty'])
|
||||||
self.assertEqual(data['total'], timezone.timedelta(0, 9000))
|
self.assertEqual(data['total'], timezone.timedelta(0, 9000))
|
||||||
self.assertEqual(data['count'], 2)
|
self.assertEqual(data['count'], 2)
|
||||||
|
|
||||||
def test_card_statistics(self):
|
def test_card_statistics(self):
|
||||||
data = cards.card_statistics(self.child)
|
data = cards.card_statistics(self.context, self.child)
|
||||||
stats = [
|
stats = [
|
||||||
{
|
{
|
||||||
'title': 'Diaper change frequency',
|
'title': 'Diaper change frequency',
|
||||||
|
@ -139,6 +176,8 @@ class TemplateTagsTestCase(TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(data['stats'], stats)
|
self.assertEqual(data['stats'], stats)
|
||||||
|
self.assertFalse(data['empty'])
|
||||||
|
self.assertFalse(data['hide_empty'])
|
||||||
|
|
||||||
def test_card_timer_list(self):
|
def test_card_timer_list(self):
|
||||||
user = User.objects.first()
|
user = User.objects.first()
|
||||||
|
@ -165,31 +204,35 @@ class TemplateTagsTestCase(TestCase):
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
data = cards.card_timer_list()
|
data = cards.card_timer_list(self.context)
|
||||||
self.assertIsInstance(data['instances'][0], models.Timer)
|
self.assertIsInstance(data['instances'][0], models.Timer)
|
||||||
self.assertEqual(len(data['instances']), 3)
|
self.assertEqual(len(data['instances']), 3)
|
||||||
|
|
||||||
data = cards.card_timer_list(child)
|
data = cards.card_timer_list(self.context, child)
|
||||||
self.assertIsInstance(data['instances'][0], models.Timer)
|
self.assertIsInstance(data['instances'][0], models.Timer)
|
||||||
self.assertTrue(timers['no_child'] in data['instances'])
|
self.assertTrue(timers['no_child'] in data['instances'])
|
||||||
self.assertTrue(timers['child'] in data['instances'])
|
self.assertTrue(timers['child'] in data['instances'])
|
||||||
self.assertFalse(timers['child_two'] in data['instances'])
|
self.assertFalse(timers['child_two'] in data['instances'])
|
||||||
|
|
||||||
data = cards.card_timer_list(child_two)
|
data = cards.card_timer_list(self.context, child_two)
|
||||||
self.assertIsInstance(data['instances'][0], models.Timer)
|
self.assertIsInstance(data['instances'][0], models.Timer)
|
||||||
self.assertTrue(timers['no_child'] in data['instances'])
|
self.assertTrue(timers['no_child'] in data['instances'])
|
||||||
self.assertTrue(timers['child_two'] in data['instances'])
|
self.assertTrue(timers['child_two'] in data['instances'])
|
||||||
self.assertFalse(timers['child'] in data['instances'])
|
self.assertFalse(timers['child'] in data['instances'])
|
||||||
|
|
||||||
def test_card_tummytime_last(self):
|
def test_card_tummytime_last(self):
|
||||||
data = cards.card_tummytime_last(self.child)
|
data = cards.card_tummytime_last(self.context, self.child)
|
||||||
self.assertEqual(data['type'], 'tummytime')
|
self.assertEqual(data['type'], 'tummytime')
|
||||||
|
self.assertFalse(data['empty'])
|
||||||
|
self.assertFalse(data['hide_empty'])
|
||||||
self.assertIsInstance(data['tummytime'], models.TummyTime)
|
self.assertIsInstance(data['tummytime'], models.TummyTime)
|
||||||
self.assertEqual(data['tummytime'], models.TummyTime.objects.first())
|
self.assertEqual(data['tummytime'], models.TummyTime.objects.first())
|
||||||
|
|
||||||
def test_card_tummytime_day(self):
|
def test_card_tummytime_day(self):
|
||||||
data = cards.card_tummytime_day(self.child, self.date)
|
data = cards.card_tummytime_day(self.context, self.child, self.date)
|
||||||
self.assertEqual(data['type'], 'tummytime')
|
self.assertEqual(data['type'], 'tummytime')
|
||||||
|
self.assertFalse(data['empty'])
|
||||||
|
self.assertFalse(data['hide_empty'])
|
||||||
self.assertIsInstance(data['instances'].first(), models.TummyTime)
|
self.assertIsInstance(data['instances'].first(), models.TummyTime)
|
||||||
self.assertIsInstance(data['last'], models.TummyTime)
|
self.assertIsInstance(data['last'], models.TummyTime)
|
||||||
stats = {'count': 3, 'total': timezone.timedelta(0, 300)}
|
stats = {'count': 3, 'total': timezone.timedelta(0, 300)}
|
||||||
|
|
Loading…
Reference in New Issue