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:
Benjamin Häublein 2021-05-14 05:28:39 +02:00 committed by GitHub
parent fe568876c7
commit 1dca1cc050
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 336 additions and 126 deletions

View File

@ -14,7 +14,7 @@ class SettingsInline(admin.StackedInline):
can_delete = False
fieldsets = (
(_('Dashboard'), {
'fields': ('dashboard_refresh_rate',)
'fields': ('dashboard_refresh_rate', 'dashboard_hide_empty',)
}),
)

View File

@ -41,4 +41,9 @@ class UserPasswordForm(PasswordChangeForm):
class UserSettingsForm(forms.ModelForm):
class Meta:
model = Settings
fields = ['dashboard_refresh_rate', 'language', 'timezone']
fields = [
'dashboard_refresh_rate',
'dashboard_hide_empty',
'language',
'timezone'
]

View File

@ -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'),
),
]

View File

@ -34,6 +34,11 @@ class Settings(models.Model):
(timezone.timedelta(minutes=15), _('15 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(
choices=settings.LANGUAGES,
default=settings.LANGUAGE_CODE,

View File

@ -64,6 +64,11 @@
{% include 'babybuddy/form_field.html' %}
{% endwith %}
</div>
<div class="form-group row">
{% with form_settings.dashboard_hide_empty as field %}
{% include 'babybuddy/form_field.html' %}
{% endwith %}
</div>
</fieldset>
<fieldset>
<legend>{% trans "API" %}</legend>

View File

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
import datetime
from django.contrib.auth.models import User
from django.core.management import call_command
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(timezone.get_current_timezone_name(),
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))

View File

@ -1,3 +1,4 @@
{% if not empty or not hide_empty %}
<div class="card card-dashboard card-{{ type }}">
<div class="card-header">
<i class="icon icon-{{ type }} pull-left" aria-hidden="true"></i>
@ -9,3 +10,4 @@
</div>
{% block listgroup %}{% endblock %}
</div>
{% endif %}

View File

@ -1,11 +1,13 @@
{% load duration i18n %}
{% if not empty or not hide_empty %}
<div class="card card-dashboard card-statistics">
<div class="card-header">
<i class="icon icon-graph pull-left" aria-hidden="true"></i>
{% trans "Statistics" %}
</div>
<div class="card-body text-center">
{% if stats|length > 0 %}
<div id="statistics-carousel" class="carousel slide" data-interval="false">
<div class="carousel-inner">
{% for stat in stats %}
@ -36,5 +38,9 @@
<span class="sr-only">{% trans "Next" %}</span>
</a>
</div>
{% else %}
<span class="card-title"><strong>{% trans "No data yet" %}</strong></span>
{% endif %}
</div>
</div>
{% endif %}

View File

@ -9,12 +9,15 @@ from datetime import date, datetime, time
from core import models
register = template.Library()
@register.inclusion_tag('cards/diaperchange_last.html')
def card_diaperchange_last(child):
def _hide_empty(context):
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.
:param child: an instance of the Child model.
@ -22,11 +25,18 @@ def card_diaperchange_last(child):
"""
instance = models.DiaperChange.objects.filter(
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')
def card_diaperchange_types(child, date=None):
@register.inclusion_tag('cards/diaperchange_types.html', takes_context=True)
def card_diaperchange_types(context, child, date=None):
"""
Creates a break down of wet and solid Diaper Change instances for the past
seven days.
@ -51,6 +61,8 @@ def card_diaperchange_types(child, date=None):
instances = models.DiaperChange.objects.filter(child=child) \
.filter(time__gt=min_date).filter(time__lt=max_date).order_by('-time')
empty = len(instances) == 0
for instance in instances:
key = (max_date - instance.time).days
if instance.wet:
@ -65,11 +77,17 @@ def card_diaperchange_types(child, date=None):
stats[key]['wet_pct'] = info['wet'] / 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')
def card_feeding_day(child, date=None):
@register.inclusion_tag('cards/feeding_day.html', takes_context=True)
def card_feeding_day(context, child, date=None):
"""
Filters Feeding instances to get total amount for a specific date.
:param child: an instance of the Child model.
@ -78,6 +96,7 @@ def card_feeding_day(child, date=None):
"""
if not date:
date = timezone.localtime().date()
instances = models.Feeding.objects.filter(child=child).filter(
start__year=date.year,
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])
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')
def card_feeding_last(child):
@register.inclusion_tag('cards/feeding_last.html', takes_context=True)
def card_feeding_last(context, child):
"""
Information about the most recent feeding.
:param child: an instance of the Child model.
@ -102,11 +128,18 @@ def card_feeding_last(child):
"""
instance = models.Feeding.objects.filter(child=child) \
.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')
def card_feeding_last_method(child):
@register.inclusion_tag('cards/feeding_last_method.html', takes_context=True)
def card_feeding_last_method(context, child):
"""
Information about the three most recent feeding methods.
: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) \
.order_by('-end')[:3]
empty = len(instances) == 0
# 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')
def card_sleep_last(child):
@register.inclusion_tag('cards/sleep_last.html', takes_context=True)
def card_sleep_last(context, child):
"""
Information about the most recent sleep entry.
:param child: an instance of the Child model.
@ -127,11 +167,18 @@ def card_sleep_last(child):
"""
instance = models.Sleep.objects.filter(child=child) \
.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')
def card_sleep_day(child, date=None):
@register.inclusion_tag('cards/sleep_day.html', takes_context=True)
def card_sleep_day(context, child, date=None):
"""
Filters Sleep instances to get count and total values for a specific date.
:param child: an instance of the Child model.
@ -147,6 +194,7 @@ def card_sleep_day(child, date=None):
end__year=date.year,
end__month=date.month,
end__day=date.day)
empty = len(instances) == 0
total = timezone.timedelta(seconds=0)
for instance in instances:
@ -161,11 +209,17 @@ def card_sleep_day(child, date=None):
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')
def card_sleep_naps_day(child, date=None):
@register.inclusion_tag('cards/sleep_naps_day.html', takes_context=True)
def card_sleep_naps_day(context, child, date=None):
"""
Filters Sleep instances categorized as naps and generates statistics for a
specific date.
@ -182,14 +236,18 @@ def card_sleep_naps_day(child, date=None):
end__year=date.year,
end__month=date.month,
end__day=date.day)
empty = len(instances) == 0
return {
'type': 'sleep',
'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')
def card_statistics(child):
@register.inclusion_tag('cards/statistics.html', takes_context=True)
def card_statistics(context, child):
"""
Statistics data for all models.
:param child: an instance of the Child model.
@ -198,12 +256,14 @@ def card_statistics(child):
stats = []
changes = _diaperchange_statistics(child)
if changes:
stats.append({
'type': 'duration',
'stat': changes['btwn_average'],
'title': _('Diaper change frequency')})
feedings = _feeding_statistics(child)
if feedings:
for item in feedings:
stats.append({
'type': 'duration',
@ -211,6 +271,7 @@ def card_statistics(child):
'title': item['title']})
naps = _nap_statistics(child)
if naps:
stats.append({
'type': 'duration',
'stat': naps['average'],
@ -221,6 +282,7 @@ def card_statistics(child):
'title': _('Average naps per day')})
sleep = _sleep_statistics(child)
if sleep:
stats.append({
'type': 'duration',
'stat': sleep['average'],
@ -231,12 +293,19 @@ def card_statistics(child):
'title': _('Average awake duration')})
weight = _weight_statistics(child)
if weight:
stats.append({
'type': 'float',
'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):
@ -247,6 +316,8 @@ def _diaperchange_statistics(child):
"""
instances = models.DiaperChange.objects.filter(child=child) \
.order_by('time')
if len(instances) == 0:
return False
changes = {
'btwn_total': timezone.timedelta(0),
'btwn_count': instances.count() - 1,
@ -292,6 +363,8 @@ def _feeding_statistics(child):
timespan['btwn_average'] = 0.0
instances = models.Feeding.objects.filter(child=child).order_by('start')
if len(instances) == 0:
return False
last_instance = None
for instance in instances:
@ -317,6 +390,8 @@ def _nap_statistics(child):
:returns: a dictionary of statistics.
"""
instances = models.Sleep.naps.filter(child=child).order_by('start')
if len(instances) == 0:
return False
naps = {
'total': instances.aggregate(Sum('duration'))['duration__sum'],
'count': instances.count(),
@ -340,6 +415,9 @@ def _sleep_statistics(child):
:returns: a dictionary of statistics.
"""
instances = models.Sleep.objects.filter(child=child).order_by('start')
if len(instances) == 0:
return False
sleep = {
'total': instances.aggregate(Sum('duration'))['duration__sum'],
'count': instances.count(),
@ -371,6 +449,9 @@ def _weight_statistics(child):
weight = {'change_weekly': 0.0}
instances = models.Weight.objects.filter(child=child).order_by('-date')
if len(instances) == 0:
return False
newest = instances.first()
oldest = instances.last()
@ -382,8 +463,8 @@ def _weight_statistics(child):
return weight
@register.inclusion_tag('cards/timer_list.html')
def card_timer_list(child=None):
@register.inclusion_tag('cards/timer_list.html', takes_context=True)
def card_timer_list(context, child=None):
"""
Filters for currently active Timer instances, optionally by child.
:param child: an instance of the Child model.
@ -397,11 +478,18 @@ def card_timer_list(child=None):
).order_by('-start')
else:
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')
def card_tummytime_last(child):
@register.inclusion_tag('cards/tummytime_last.html', takes_context=True)
def card_tummytime_last(context, child):
"""
Filters the most recent tummy time.
:param child: an instance of the Child model.
@ -409,11 +497,18 @@ def card_tummytime_last(child):
"""
instance = models.TummyTime.objects.filter(child=child) \
.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')
def card_tummytime_day(child, date=None):
@register.inclusion_tag('cards/tummytime_day.html', takes_context=True)
def card_tummytime_day(context, child, date=None):
"""
Filters Tummy Time instances and generates statistics for a specific date.
:param child: an instance of the Child model.
@ -425,14 +520,20 @@ def card_tummytime_day(child, date=None):
instances = models.TummyTime.objects.filter(
child=child, end__year=date.year, end__month=date.month,
end__day=date.day).order_by('-end')
empty = len(instances) == 0
stats = {
'total': timezone.timedelta(seconds=0),
'count': instances.count()
}
for instance in instances:
stats['total'] += timezone.timedelta(seconds=instance.duration.seconds)
return {
'type': 'tummytime',
'stats': stats,
'instances': instances,
'last': instances.first()}
'last': instances.first(),
'empty': empty,
'hide_empty': _hide_empty(context)
}

View File

@ -10,6 +10,11 @@ from core import models
from dashboard.templatetags import cards
class MockUserRequest:
def __init__(self, user):
self.user = user
class TemplateTagsTestCase(TestCase):
fixtures = ['tests.json']
@ -17,6 +22,7 @@ class TemplateTagsTestCase(TestCase):
def setUpClass(cls):
super(TemplateTagsTestCase, cls).setUpClass()
cls.child = models.Child.objects.first()
cls.context = {'request': MockUserRequest(User.objects.first())}
# Ensure timezone matches the one defined by fixtures.
user_timezone = Settings.objects.first().timezone
@ -26,14 +32,26 @@ class TemplateTagsTestCase(TestCase):
date = timezone.localtime().strptime('2017-11-18', '%Y-%m-%d')
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):
data = cards.card_diaperchange_last(self.child)
data = cards.card_diaperchange_last(self.context, self.child)
self.assertEqual(data['type'], 'diaperchange')
self.assertFalse(data['empty'])
self.assertFalse(data['hide_empty'])
self.assertIsInstance(data['change'], models.DiaperChange)
self.assertEqual(data['change'], models.DiaperChange.objects.first())
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')
stats = {
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)
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.assertFalse(data['empty'])
self.assertFalse(data['hide_empty'])
self.assertEqual(data['total'], 2.5)
self.assertEqual(data['count'], 3)
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.assertFalse(data['empty'])
self.assertFalse(data['hide_empty'])
self.assertIsInstance(data['feeding'], models.Feeding)
self.assertEqual(data['feeding'], models.Feeding.objects.first())
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.assertFalse(data['empty'])
self.assertFalse(data['hide_empty'])
self.assertEqual(len(data['feedings']), 3)
for feeding in data['feedings']:
self.assertIsInstance(feeding, models.Feeding)
@ -69,25 +93,38 @@ class TemplateTagsTestCase(TestCase):
models.Feeding.objects.first().method)
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.assertFalse(data['empty'])
self.assertFalse(data['hide_empty'])
self.assertIsInstance(data['sleep'], models.Sleep)
self.assertEqual(data['sleep'], models.Sleep.objects.first())
def test_card_sleep_day(self):
data = cards.card_sleep_day(self.child, self.date)
def test_card_sleep_last_empty(self):
models.Sleep.objects.all().delete()
data = cards.card_sleep_last(self.context, self.child)
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['count'], 4)
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.assertFalse(data['empty'])
self.assertFalse(data['hide_empty'])
self.assertEqual(data['total'], timezone.timedelta(0, 9000))
self.assertEqual(data['count'], 2)
def test_card_statistics(self):
data = cards.card_statistics(self.child)
data = cards.card_statistics(self.context, self.child)
stats = [
{
'title': 'Diaper change frequency',
@ -139,6 +176,8 @@ class TemplateTagsTestCase(TestCase):
]
self.assertEqual(data['stats'], stats)
self.assertFalse(data['empty'])
self.assertFalse(data['hide_empty'])
def test_card_timer_list(self):
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.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.assertTrue(timers['no_child'] in data['instances'])
self.assertTrue(timers['child'] 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.assertTrue(timers['no_child'] in data['instances'])
self.assertTrue(timers['child_two'] in data['instances'])
self.assertFalse(timers['child'] in data['instances'])
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.assertFalse(data['empty'])
self.assertFalse(data['hide_empty'])
self.assertIsInstance(data['tummytime'], models.TummyTime)
self.assertEqual(data['tummytime'], models.TummyTime.objects.first())
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.assertFalse(data['empty'])
self.assertFalse(data['hide_empty'])
self.assertIsInstance(data['instances'].first(), models.TummyTime)
self.assertIsInstance(data['last'], models.TummyTime)
stats = {'count': 3, 'total': timezone.timedelta(0, 300)}