Add weight tracking.

This commit is contained in:
Christopher Charbonneau Wells 2017-11-09 21:15:09 -05:00
parent f947d37285
commit 067be4bf07
17 changed files with 380 additions and 59 deletions

View File

@ -5,8 +5,7 @@ from rest_framework import serializers
from django.contrib.auth.models import User
from core.models import (Child, DiaperChange, Feeding, Note, Sleep, Timer,
TummyTime)
from core import models
class UserSerializer(serializers.ModelSerializer):
@ -17,7 +16,7 @@ class UserSerializer(serializers.ModelSerializer):
class ChildSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Child
model = models.Child
fields = ('first_name', 'last_name', 'birth_date', 'slug')
lookup_field = 'slug'
@ -26,7 +25,7 @@ class DiaperChangeSerializer(serializers.HyperlinkedModelSerializer):
child = ChildSerializer()
class Meta:
model = DiaperChange
model = models.DiaperChange
fields = ('child', 'time', 'wet', 'solid', 'color')
@ -34,7 +33,7 @@ class FeedingSerializer(serializers.HyperlinkedModelSerializer):
child = ChildSerializer()
class Meta:
model = Feeding
model = models.Feeding
fields = ('child', 'start', 'end', 'duration', 'type', 'method',
'amount')
@ -43,7 +42,7 @@ class NoteSerializer(serializers.HyperlinkedModelSerializer):
child = ChildSerializer()
class Meta:
model = Note
model = models.Note
fields = ('child', 'note', 'time')
@ -51,7 +50,7 @@ class SleepSerializer(serializers.HyperlinkedModelSerializer):
child = ChildSerializer()
class Meta:
model = Sleep
model = models.Sleep
fields = ('child', 'start', 'end', 'duration')
@ -59,7 +58,7 @@ class TimerSerializer(serializers.HyperlinkedModelSerializer):
user = UserSerializer()
class Meta:
model = Timer
model = models.Timer
fields = ('name', 'start', 'end', 'duration', 'active', 'user')
@ -67,5 +66,13 @@ class TummyTimeSerializer(serializers.HyperlinkedModelSerializer):
child = ChildSerializer()
class Meta:
model = TummyTime
model = models.TummyTime
fields = ('child', 'start', 'end', 'duration', 'milestone')
class WeightSerializer(serializers.HyperlinkedModelSerializer):
child = ChildSerializer()
class Meta:
model = models.Weight
fields = ('child', 'weight', 'date')

View File

@ -4,17 +4,18 @@ from __future__ import unicode_literals
from django.conf.urls import url, include
from rest_framework import routers
from .views import (ChildViewSet, DiaperChangeViewSet, FeedingViewSet,
NoteViewSet, SleepViewSet, TimerViewSet, TummyTimeViewSet)
from . import views
router = routers.DefaultRouter()
router.register(r'children', ChildViewSet)
router.register(r'changes', DiaperChangeViewSet)
router.register(r'feedings', FeedingViewSet)
router.register(r'notes', NoteViewSet)
router.register(r'sleep', SleepViewSet)
router.register(r'timers', TimerViewSet)
router.register(r'tummy-times', TummyTimeViewSet)
router.register(r'children', views.ChildViewSet)
router.register(r'changes', views.DiaperChangeViewSet)
router.register(r'feedings', views.FeedingViewSet)
router.register(r'notes', views.NoteViewSet)
router.register(r'sleep', views.SleepViewSet)
router.register(r'timers', views.TimerViewSet)
router.register(r'tummy-times', views.TummyTimeViewSet)
router.register(r'weight', views.WeightViewSet)
urlpatterns = [
url(r'^api/', include(router.urls)),

View File

@ -3,52 +3,55 @@ from __future__ import unicode_literals
from rest_framework import viewsets
from core.models import (Child, DiaperChange, Feeding, Note, Sleep, Timer,
TummyTime)
from core import models
from .serializers import (ChildSerializer, DiaperChangeSerializer,
FeedingSerializer, NoteSerializer, SleepSerializer,
TimerSerializer, TummyTimeSerializer,)
from . import serializers
class ChildViewSet(viewsets.ModelViewSet):
queryset = Child.objects.all()
serializer_class = ChildSerializer
queryset = models.Child.objects.all()
serializer_class = serializers.ChildSerializer
lookup_field = 'slug'
filter_fields = ('first_name', 'last_name', 'slug')
class DiaperChangeViewSet(viewsets.ModelViewSet):
queryset = DiaperChange.objects.all()
serializer_class = DiaperChangeSerializer
queryset = models.DiaperChange.objects.all()
serializer_class = serializers.DiaperChangeSerializer
filter_fields = ('child', 'wet', 'solid', 'color')
class FeedingViewSet(viewsets.ModelViewSet):
queryset = Feeding.objects.all()
serializer_class = FeedingSerializer
queryset = models.Feeding.objects.all()
serializer_class = serializers.FeedingSerializer
filter_fields = ('child', 'type', 'method')
class NoteViewSet(viewsets.ModelViewSet):
queryset = Note.objects.all()
serializer_class = NoteSerializer
queryset = models.Note.objects.all()
serializer_class = serializers.NoteSerializer
filter_fields = ('child',)
class SleepViewSet(viewsets.ModelViewSet):
queryset = Sleep.objects.all()
serializer_class = SleepSerializer
queryset = models.Sleep.objects.all()
serializer_class = serializers.SleepSerializer
filter_fields = ('child',)
class TimerViewSet(viewsets.ModelViewSet):
queryset = Timer.objects.all()
serializer_class = TimerSerializer
queryset = models.Timer.objects.all()
serializer_class = serializers.TimerSerializer
filter_fields = ('active', 'user')
class TummyTimeViewSet(viewsets.ModelViewSet):
queryset = TummyTime.objects.all()
serializer_class = TummyTimeSerializer
queryset = models.TummyTime.objects.all()
serializer_class = serializers.TummyTimeSerializer
filter_fields = ('child',)
class WeightViewSet(viewsets.ModelViewSet):
queryset = models.Weight.objects.all()
serializer_class = serializers.WeightSerializer
filter_fields = ('child',)

View File

@ -10,7 +10,7 @@ from django.utils import timezone
from faker import Factory
from core.models import Child, DiaperChange, Feeding, Note, Sleep, TummyTime
from core import models
class Command(BaseCommand):
@ -42,7 +42,7 @@ class Command(BaseCommand):
# User first day of data that will created for birth date.
birth_date = (timezone.localtime() - timedelta(days=days))
for i in range(0, children):
child = Child.objects.create(
child = models.Child.objects.create(
first_name=self.faker.first_name(),
last_name=self.faker.last_name(),
birth_date=birth_date
@ -67,7 +67,7 @@ class Command(BaseCommand):
if solid:
wet = False
color = choice(
DiaperChange._meta.get_field('color').choices)[0]
models.DiaperChange._meta.get_field('color').choices)[0]
else:
wet = True
color = ''
@ -75,7 +75,7 @@ class Command(BaseCommand):
time = date + timedelta(minutes=randint(0, 60 * 24))
if time < now:
DiaperChange.objects.create(
models.DiaperChange.objects.create(
child=child,
time=time,
wet=wet,
@ -85,7 +85,8 @@ class Command(BaseCommand):
last_end = date
while last_end < date + timedelta(days=1):
method = choice(Feeding._meta.get_field('method').choices)[0]
method = choice(models.Feeding._meta.get_field(
'method').choices)[0]
if method is 'bottle':
amount = Decimal('%d.%d' % (randint(0, 6), randint(0, 9)))
else:
@ -96,11 +97,11 @@ class Command(BaseCommand):
if end > now:
break
Feeding.objects.create(
models.Feeding.objects.create(
child=child,
start=start,
end=end,
type=choice(Feeding._meta.get_field('type').choices)[0],
type=choice(models.Feeding._meta.get_field('type').choices)[0],
method=method,
amount=amount
).save()
@ -109,7 +110,8 @@ class Command(BaseCommand):
last_end = date
# Adjust last_end if the last sleep entry crossed in to date.
last_entry = Sleep.objects.filter(child=child).order_by('end').last()
last_entry = models.Sleep.objects.filter(
child=child).order_by('end').last()
if last_entry:
last_entry_end = timezone.localtime(last_entry.end)
if last_entry_end > last_end:
@ -124,7 +126,8 @@ class Command(BaseCommand):
if end > now:
break
Sleep.objects.create(child=child, start=start, end=end).save()
models.Sleep.objects.create(
child=child, start=start, end=end).save()
last_end = end
last_end = date
@ -139,7 +142,7 @@ class Command(BaseCommand):
if end > now:
break
TummyTime.objects.create(
models.TummyTime.objects.create(
child=child,
start=start,
end=end,
@ -147,8 +150,14 @@ class Command(BaseCommand):
).save()
last_end = end
models.Weight.objects.create(
child=child,
weight=Decimal('%d.%d' % (randint(3, 15), randint(0, 9))),
date=date.date()
).save()
note = self.faker.sentence()
Note.objects.create(
models.Note.objects.create(
child=child,
note=note,
time=date + timedelta(minutes=randint(0, 60 * 24))

View File

@ -103,3 +103,7 @@
.icon-user {
@extend .fa-user;
}
.icon-weight {
@extend .fa-balance-scale;
}

View File

@ -43,6 +43,11 @@
<i class="icon icon-tummytime" aria-hidden="true"></i> Tummy Time
</a>
{% endif %}
{% if perms.core.add_weight %}
<a class="dropdown-item p-2" href="{% url 'weight-add' %}">
<i class="icon icon-weight" aria-hidden="true"></i> Weight
</a>
{% endif %}
</div>
</div>
@ -91,6 +96,16 @@
href="{% url 'note-add' %}"><i class="icon icon-add" aria-hidden="true"></i> Note</a>
{% endif %}
{% if perms.core.view_weight %}
<a class="dropdown-item{% if request.path == '/weight/' %} active{% endif %}" href="{% url 'weight-list' %}">
<i class="icon icon-weight" aria-hidden="true"></i> Weight
</a>
{% endif %}
{% if perms.core.add_weight %}
<a class="dropdown-item pl-5{% if request.path == '/weight/add/' %} active{% endif %}"
href="{% url 'weight-add' %}"><i class="icon icon-add" aria-hidden="true"></i> Weight entry</a>
{% endif %}
</div>
</li>

View File

@ -3,25 +3,24 @@ from __future__ import unicode_literals
from django.contrib import admin
from .models import (Child, DiaperChange, Feeding, Note, Sleep, Timer,
TummyTime)
from core import models
@admin.register(Child)
@admin.register(models.Child)
class ChildAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'birth_date', 'slug')
list_filter = ('last_name',)
search_fields = ('first_name', 'last_name', 'birth_date',)
@admin.register(DiaperChange)
@admin.register(models.DiaperChange)
class DiaperChangeAdmin(admin.ModelAdmin):
list_display = ('child', 'time', 'wet', 'solid', 'color')
list_filter = ('child', 'wet', 'solid', 'color')
search_fields = ('child__first_name', 'child__last_name',)
@admin.register(Feeding)
@admin.register(models.Feeding)
class FeedingAdmin(admin.ModelAdmin):
list_display = ('start', 'end', 'duration', 'child', 'type', 'method',
'amount')
@ -30,29 +29,36 @@ class FeedingAdmin(admin.ModelAdmin):
'method',)
@admin.register(Note)
@admin.register(models.Note)
class NoteAdmin(admin.ModelAdmin):
list_display = ('time', 'child', 'note',)
list_filter = ('child',)
search_fields = ('child__last_name',)
@admin.register(Sleep)
@admin.register(models.Sleep)
class SleepAdmin(admin.ModelAdmin):
list_display = ('start', 'end', 'duration', 'child', 'nap')
list_filter = ('child',)
search_fields = ('child__first_name', 'child__last_name',)
@admin.register(Timer)
@admin.register(models.Timer)
class TimerAdmin(admin.ModelAdmin):
list_display = ('name', 'start', 'end', 'duration', 'active', 'user')
list_filter = ('active', 'user')
search_fields = ('name', 'user')
@admin.register(TummyTime)
@admin.register(models.TummyTime)
class TummyTimeAdmin(admin.ModelAdmin):
list_display = ('start', 'end', 'duration', 'child', 'milestone',)
list_filter = ('child',)
search_fields = ('child__first_name', 'child__last_name', 'milestone',)
@admin.register(models.Weight)
class WeightAdmin(admin.ModelAdmin):
list_display = ('child', 'weight', 'date',)
list_filter = ('child',)
search_fields = ('child__first_name', 'child__last_name', 'weight',)

View File

@ -205,3 +205,19 @@ class TummyTimeForm(forms.ModelForm):
timer.stop(instance.end)
instance.save()
return instance
class WeightForm(forms.ModelForm):
class Meta:
model = models.Weight
fields = ['child', 'weight', 'date']
widgets = {
'date': forms.DateInput(attrs={
'class': 'datepicker-input',
'data-target': '#datetimepicker_date',
}),
}
def __init__(self, *args, **kwargs):
kwargs = set_default_child(kwargs)
super(WeightForm, self).__init__(*args, **kwargs)

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-10 02:07
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0002_auto_20171028_1257'),
]
operations = [
migrations.CreateModel(
name='Weight',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('weight', models.FloatField()),
('date', models.DateField()),
('child', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='weight', to='core.Child')),
],
options={
'default_permissions': ('view', 'add', 'change', 'delete'),
'verbose_name_plural': 'Weight',
'ordering': ['-date'],
},
),
]

View File

@ -10,6 +10,19 @@ from django.template.defaultfilters import slugify
from django.utils import timezone
def validate_date(date, field_name):
"""
Confirm that a date is not in the future.
:param date: a timezone aware date instance.
:param field_name: the name of the field being checked.
:return:
"""
if date and date > timezone.localdate():
raise ValidationError(
{field_name: 'Date can not be in the future.'},
code='date_invalid')
def validate_duration(model, max_duration=timedelta(hours=24)):
"""
Basic sanity checks for models with a duration
@ -325,3 +338,23 @@ class TummyTime(models.Model):
validate_duration(self)
validate_unique_period(
TummyTime.objects.filter(child=self.child), self)
class Weight(models.Model):
model_name = 'weight'
child = models.ForeignKey('Child', related_name='weight')
weight = models.FloatField(blank=False, null=False)
date = models.DateField(blank=False, null=False)
objects = models.Manager()
class Meta:
default_permissions = ('view', 'add', 'change', 'delete')
ordering = ['-date']
verbose_name_plural = 'Weight'
def __str__(self):
return 'Weight'
def clean(self):
validate_date(self.date, 'date')

View File

@ -0,0 +1,18 @@
{% extends 'babybuddy/page.html' %}
{% load widget_tweaks %}
{% block title %}Delete a Weight Entry{% endblock %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'weight-list' %}">Weight</a></li>
<li class="breadcrumb-item active" aria-current="page">Delete</li>
{% endblock %}
{% block content %}
<form role="form" method="post">
{% csrf_token %}
<h1>Are you sure you want to delete <span class="text-info">{{ object }}</span>?</h1>
<input type="submit" value="Delete" class="btn btn-danger" />
<a href="{% url 'weight-list' %}" class="btn btn-default">Cancel</a>
</form>
{% endblock %}

View File

@ -0,0 +1,38 @@
{% extends 'babybuddy/page.html' %}
{% block title %}
{% if object %}
{{ object }}
{% else %}
Add a Weight Entry
{% endif %}
{% endblock %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'weight-list' %}">Weight</a></li>
{% if object %}
<li class="breadcrumb-item active" aria-current="page">Update</li>
{% else %}
<li class="breadcrumb-item active" aria-current="page">Add a Weight Entry</li>
{% endif %}
{% endblock %}
{% block content %}
{% if object %}
<h1>Update <span class="text-info">{{ object }}</span></h1>
{% else %}
<h1>Add a Weight Entry</h1>
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$(function () {
$('#datetimepicker_date').datetimepicker({
defaultDate: 'now',
format: 'YYYY-MM-DD'
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,63 @@
{% extends 'babybuddy/page.html' %}
{% load widget_tweaks %}
{% block title %}Weight{% endblock %}
{% block breadcrumbs %}
<li class="breadcrumb-item active" aria-current="page">Weight</li>
{% endblock %}
{% block content %}
<h1>Weight</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-inverse">
<tr>
<th>Child</th>
<th>Weight</th>
<th>Date</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr>
<th scope="row"><a href="{% url 'child' object.child.slug %}">{{ object.child }}</a></th>
<td>{{ object.weight }}</td>
<td>{{ object.date }}</td>
<td class="text-center">
<div class="btn-group btn-group-sm" role="group" aria-label="Actions">
{% if perms.core.change_weight %}
<a href="{% url 'weight-update' object.id %}" class="btn btn-primary">
<i class="icon icon-update" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.core.delete_weight %}
<a href="{% url 'weight-delete' object.id %}" class="btn btn-danger">
<i class="icon icon-delete" aria-hidden="true"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<th colspan="4">No weight entries found.</th>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include 'babybuddy/paginator.html' %}
{% if perms.core.add_weight %}
<a href="{% url 'weight-add' %}" class="btn btn-sm btn-success">
<i class="icon icon-weight" aria-hidden="true"></i> Add a Weight Entry
</a>
{% endif %}
{% endblock %}

View File

@ -180,6 +180,31 @@ class FormsTestCase(TestCase):
page = self.c.post('/tummy-time/{}/'.format(entry.id), params)
self.assertEqual(page.status_code, 302)
def test_weight_forms(self):
params = {
'child': 1,
'weight': '8.5',
'date': '2000-01-01'
}
entry = models.Weight.objects.first()
page = self.c.post('/weight/{}/'.format(entry.id), params)
self.assertEqual(page.status_code, 302)
def test_validate_date(self):
future = (timezone.localdate() + timezone.timedelta(days=1))
params = {
'child': 1,
'weight': '8.5',
'date': future.strftime('%Y-%m-%d')
}
entry = models.Weight.objects.first()
page = self.c.post('/weight/{}/'.format(entry.id), params)
self.assertEqual(page.status_code, 200)
self.assertFormError(page, 'form', 'date',
'Date can not be in the future.')
def test_validate_duration(self):
params = {
'child': 1,

View File

@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.test import TestCase
from django.test import Client as HttpClient
from django.contrib.auth.models import User
from django.core.management import call_command
from django.test import TestCase
from django.test import Client as HttpClient
from django.utils import timezone
from faker import Factory
@ -40,6 +41,11 @@ class ViewsTestCase(TestCase):
entry = models.Child.objects.first()
page = self.c.get('/children/{}/'.format(entry.slug))
self.assertEqual(page.status_code, 200)
page = self.c.get(
'/children/{}/'.format(entry.slug),
{'date': timezone.localdate() - timezone.timedelta(days=1)})
self.assertEqual(page.status_code, 200)
page = self.c.get('/children/{}/edit/'.format(entry.slug))
self.assertEqual(page.status_code, 200)
page = self.c.get('/children/{}/delete/'.format(entry.slug))
@ -124,3 +130,15 @@ class ViewsTestCase(TestCase):
self.assertEqual(page.status_code, 200)
page = self.c.get('/tummy-time/{}/delete/'.format(entry.id))
self.assertEqual(page.status_code, 200)
def test_weight_views(self):
page = self.c.get('/weight/')
self.assertEqual(page.status_code, 200)
page = self.c.get('/weight/add/')
self.assertEqual(page.status_code, 200)
entry = models.Weight.objects.first()
page = self.c.get('/weight/{}/'.format(entry.id))
self.assertEqual(page.status_code, 200)
page = self.c.get('/weight/{}/delete/'.format(entry.id))
self.assertEqual(page.status_code, 200)

View File

@ -68,4 +68,11 @@ urlpatterns = [
name='tummytime-update'),
url(r'^tummy-time/(?P<pk>[0-9]+)/delete/$',
views.TummyTimeDelete.as_view(), name='tummytime-delete'),
url(r'^weight/$', views.WeightList.as_view(), name='weight-list'),
url(r'^weight/add/$', views.WeightAdd.as_view(), name='weight-add'),
url(r'^weight/(?P<pk>[0-9]+)/$', views.WeightUpdate.as_view(),
name='weight-update'),
url(r'^weight/(?P<pk>[0-9]+)/delete/$', views.WeightDelete.as_view(),
name='weight-delete'),
]

View File

@ -298,3 +298,31 @@ class TummyTimeDelete(PermissionRequiredMixin, DeleteView):
model = models.TummyTime
permission_required = ('core.delete_tummytime',)
success_url = '/tummy-time'
class WeightList(PermissionRequiredMixin, FilterView):
model = models.Weight
template_name = 'core/weight_list.html'
permission_required = ('core.view_weight',)
paginate_by = 10
filter_fields = ('child',)
class WeightAdd(PermissionRequiredMixin, CreateView):
model = models.Weight
permission_required = ('core.add_weight',)
form_class = forms.WeightForm
success_url = '/weight'
class WeightUpdate(PermissionRequiredMixin, UpdateView):
model = models.Weight
permission_required = ('core.change_weight',)
fields = ['child', 'weight', 'date']
success_url = '/weight'
class WeightDelete(PermissionRequiredMixin, DeleteView):
model = models.Weight
permission_required = ('core.delete_weight',)
success_url = '/weight'