Move timeline code to core and integrate with Child detail view.

This commit is contained in:
Christopher Charbonneau Wells 2017-11-07 13:15:48 -05:00
parent 1c5278cebf
commit 162f117cef
11 changed files with 121 additions and 132 deletions

View File

@ -3,3 +3,12 @@
max-width: 150px; max-width: 150px;
} }
} }
@include media-breakpoint-up(md) {
#view-child {
.child-detail-column {
position: fixed;
width: 100%;
}
}
}

View File

@ -118,7 +118,7 @@ $card-shadow: rgba(0, 0, 0, .175);
} }
} }
@media (max-width: 767px) { @include media-breakpoint-down(md) {
.timeline { .timeline {
&::before { &::before {
left: 40px; left: 40px;

View File

@ -9,7 +9,9 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="jumbotron text-center"> <div class="child-detail-column">
<div class="row">
<div class="col-lg-4 col-md-6 pb-3 text-center">
<img src="{% static 'babybuddy/img/core/child-placeholder.png' %}" class="child-photo img-fluid rounded-circle" /> <img src="{% static 'babybuddy/img/core/child-placeholder.png' %}" class="child-photo img-fluid rounded-circle" />
<div class="child-name display-4">{{ object }}</div> <div class="child-name display-4">{{ object }}</div>
<p class="lead"> <p class="lead">
@ -18,4 +20,42 @@
</p> </p>
{% include 'dashboard/child_button_group.html' %} {% include 'dashboard/child_button_group.html' %}
</div> </div>
</div>
</div>
<div class="row">
<div class="col-lg-8 offset-lg-4 col-md-6 offset-md-6">
<h3 class="text-center">
{% if date_previous %}
<a class="btn btn-sm btn-default" href="?date={{ date_previous|date:"Y-m-d" }}" aria-label="Previous">
<i class="icon icon-chevron-left" aria-hidden="true"></i>
<span class="sr-only">Previous</span>
</a>
{% endif %}
{{ date|date }}
{% if date_next %}
<a class="btn btn-sm btn-default" href="?date={{ date_next|date:"Y-m-d" }}" aria-label="Next">
<i class="icon icon-chevron-right" aria-hidden="true"></i>
<span class="sr-only">Next</span>
</a>
{% endif %}
</h3>
<ul class="timeline m-auto">
{% for object in timeline_objects %}
<li{% cycle "" ' class="timeline-inverted"' %}>
<div class="timeline-badge {% if object.type == "start" %}bg-success{% elif object.type == "end" %}bg-danger{% else %}bg-info{% endif %}">
<i class="icon icon-{{ object.model_name }}"></i>
</div>
<div class="card text-right">
<div class="card-body">
{{ object.event }}
</div>
<div class="card-footer text-muted">
{{ object.time|timesince }} ago ({{ object.time|time }})
</div>
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -6,9 +6,9 @@ from django.utils import timezone
from core.models import DiaperChange, Feeding, Sleep, TummyTime from core.models import DiaperChange, Feeding, Sleep, TummyTime
def timeline(child, date): def get_objects(child, date):
""" """
Create a time-sorted dictionary for all events for a child. Create a time-sorted dictionary of all events for a child.
:param child: an instance of a Child. :param child: an instance of a Child.
:param date: a DateTime instance for the day to be summarized. :param date: a DateTime instance for the day to be summarized.
:returns: a list of the day's events. :returns: a list of the day's events.

View File

@ -3,19 +3,18 @@ from __future__ import unicode_literals
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django_filters.views import FilterView from django_filters.views import FilterView
from .models import Child, DiaperChange, Feeding, Note, Sleep, Timer, TummyTime from core import forms, models, timeline
from .forms import (ChildForm, ChildDeleteForm, DiaperChangeForm, FeedingForm,
SleepForm, TimerForm, TummyTimeForm)
class ChildList(PermissionRequiredMixin, FilterView): class ChildList(PermissionRequiredMixin, FilterView):
model = Child model = models.Child
template_name = 'core/child_list.html' template_name = 'core/child_list.html'
permission_required = ('core.view_child',) permission_required = ('core.view_child',)
paginate_by = 10 paginate_by = 10
@ -23,34 +22,46 @@ class ChildList(PermissionRequiredMixin, FilterView):
class ChildAdd(PermissionRequiredMixin, CreateView): class ChildAdd(PermissionRequiredMixin, CreateView):
model = Child model = models.Child
permission_required = ('core.add_child',) permission_required = ('core.add_child',)
form_class = ChildForm form_class = forms.ChildForm
success_url = '/children' success_url = '/children'
class ChildDetail(PermissionRequiredMixin, DetailView): class ChildDetail(PermissionRequiredMixin, DetailView):
model = Child model = models.Child
permission_required = ('core.view_child',) permission_required = ('core.view_child',)
def get_context_data(self, **kwargs):
context = super(ChildDetail, self).get_context_data(**kwargs)
date = self.request.GET.get('date', str(timezone.localdate()))
date = timezone.datetime.strptime(date, '%Y-%m-%d')
date = timezone.localtime(timezone.make_aware(date))
context['timeline_objects'] = timeline.get_objects(self.object, date)
context['date'] = date
context['date_previous'] = date - timezone.timedelta(days=1)
if date.date() < timezone.localdate():
context['date_next'] = date + timezone.timedelta(days=1)
return context
class ChildUpdate(PermissionRequiredMixin, UpdateView): class ChildUpdate(PermissionRequiredMixin, UpdateView):
model = Child model = models.Child
permission_required = ('core.change_child',) permission_required = ('core.change_child',)
form_class = ChildForm form_class = forms.ChildForm
success_url = '/children' success_url = '/children'
class ChildDelete(PermissionRequiredMixin, UpdateView): class ChildDelete(PermissionRequiredMixin, UpdateView):
model = Child model = models.Child
form_class = ChildDeleteForm form_class = forms.ChildDeleteForm
template_name = 'core/child_confirm_delete.html' template_name = 'core/child_confirm_delete.html'
permission_required = ('core.delete_child',) permission_required = ('core.delete_child',)
success_url = '/children' success_url = '/children'
class DiaperChangeList(PermissionRequiredMixin, FilterView): class DiaperChangeList(PermissionRequiredMixin, FilterView):
model = DiaperChange model = models.DiaperChange
template_name = 'core/diaperchange_list.html' template_name = 'core/diaperchange_list.html'
permission_required = ('core.view_diaperchange',) permission_required = ('core.view_diaperchange',)
paginate_by = 10 paginate_by = 10
@ -58,27 +69,27 @@ class DiaperChangeList(PermissionRequiredMixin, FilterView):
class DiaperChangeAdd(PermissionRequiredMixin, CreateView): class DiaperChangeAdd(PermissionRequiredMixin, CreateView):
model = DiaperChange model = models.DiaperChange
permission_required = ('core.add_diaperchange',) permission_required = ('core.add_diaperchange',)
form_class = DiaperChangeForm form_class = forms.DiaperChangeForm
success_url = '/changes' success_url = '/changes'
class DiaperChangeUpdate(PermissionRequiredMixin, UpdateView): class DiaperChangeUpdate(PermissionRequiredMixin, UpdateView):
model = DiaperChange model = models.DiaperChange
permission_required = ('core.change_diaperchange',) permission_required = ('core.change_diaperchange',)
form_class = DiaperChangeForm form_class = forms.DiaperChangeForm
success_url = '/changes' success_url = '/changes'
class DiaperChangeDelete(PermissionRequiredMixin, DeleteView): class DiaperChangeDelete(PermissionRequiredMixin, DeleteView):
model = DiaperChange model = models.DiaperChange
permission_required = ('core.delete_diaperchange',) permission_required = ('core.delete_diaperchange',)
success_url = '/changes' success_url = '/changes'
class FeedingList(PermissionRequiredMixin, FilterView): class FeedingList(PermissionRequiredMixin, FilterView):
model = Feeding model = models.Feeding
template_name = 'core/feeding_list.html' template_name = 'core/feeding_list.html'
permission_required = ('core.view_feeding',) permission_required = ('core.view_feeding',)
paginate_by = 10 paginate_by = 10
@ -86,9 +97,9 @@ class FeedingList(PermissionRequiredMixin, FilterView):
class FeedingAdd(PermissionRequiredMixin, CreateView): class FeedingAdd(PermissionRequiredMixin, CreateView):
model = Feeding model = models.Feeding
permission_required = ('core.add_feeding',) permission_required = ('core.add_feeding',)
form_class = FeedingForm form_class = forms.FeedingForm
success_url = '/feedings' success_url = '/feedings'
def get_form_kwargs(self): def get_form_kwargs(self):
@ -99,20 +110,20 @@ class FeedingAdd(PermissionRequiredMixin, CreateView):
class FeedingUpdate(PermissionRequiredMixin, UpdateView): class FeedingUpdate(PermissionRequiredMixin, UpdateView):
model = Feeding model = models.Feeding
permission_required = ('core.change_feeding',) permission_required = ('core.change_feeding',)
form_class = FeedingForm form_class = forms.FeedingForm
success_url = '/feedings' success_url = '/feedings'
class FeedingDelete(PermissionRequiredMixin, DeleteView): class FeedingDelete(PermissionRequiredMixin, DeleteView):
model = Feeding model = models.Feeding
permission_required = ('core.delete_feeding',) permission_required = ('core.delete_feeding',)
success_url = '/feedings' success_url = '/feedings'
class NoteList(PermissionRequiredMixin, FilterView): class NoteList(PermissionRequiredMixin, FilterView):
model = Note model = models.Note
template_name = 'core/note_list.html' template_name = 'core/note_list.html'
permission_required = ('core.view_note',) permission_required = ('core.view_note',)
paginate_by = 10 paginate_by = 10
@ -120,27 +131,27 @@ class NoteList(PermissionRequiredMixin, FilterView):
class NoteAdd(PermissionRequiredMixin, CreateView): class NoteAdd(PermissionRequiredMixin, CreateView):
model = Note model = models.Note
permission_required = ('core.add_note',) permission_required = ('core.add_note',)
fields = ['child', 'note'] fields = ['child', 'note']
success_url = '/notes' success_url = '/notes'
class NoteUpdate(PermissionRequiredMixin, UpdateView): class NoteUpdate(PermissionRequiredMixin, UpdateView):
model = Note model = models.Note
permission_required = ('core.change_note',) permission_required = ('core.change_note',)
fields = ['child', 'note'] fields = ['child', 'note']
success_url = '/notes' success_url = '/notes'
class NoteDelete(PermissionRequiredMixin, DeleteView): class NoteDelete(PermissionRequiredMixin, DeleteView):
model = Note model = models.Note
permission_required = ('core.delete_note',) permission_required = ('core.delete_note',)
success_url = '/notes' success_url = '/notes'
class SleepList(PermissionRequiredMixin, FilterView): class SleepList(PermissionRequiredMixin, FilterView):
model = Sleep model = models.Sleep
template_name = 'core/sleep_list.html' template_name = 'core/sleep_list.html'
permission_required = ('core.view_sleep',) permission_required = ('core.view_sleep',)
paginate_by = 10 paginate_by = 10
@ -148,9 +159,9 @@ class SleepList(PermissionRequiredMixin, FilterView):
class SleepAdd(PermissionRequiredMixin, CreateView): class SleepAdd(PermissionRequiredMixin, CreateView):
model = Sleep model = models.Sleep
permission_required = ('core.add_sleep',) permission_required = ('core.add_sleep',)
form_class = SleepForm form_class = forms.SleepForm
success_url = '/sleep' success_url = '/sleep'
def get_form_kwargs(self): def get_form_kwargs(self):
@ -161,20 +172,20 @@ class SleepAdd(PermissionRequiredMixin, CreateView):
class SleepUpdate(PermissionRequiredMixin, UpdateView): class SleepUpdate(PermissionRequiredMixin, UpdateView):
model = Sleep model = models.Sleep
permission_required = ('core.change_sleep',) permission_required = ('core.change_sleep',)
form_class = SleepForm form_class = forms.SleepForm
success_url = '/sleep' success_url = '/sleep'
class SleepDelete(PermissionRequiredMixin, DeleteView): class SleepDelete(PermissionRequiredMixin, DeleteView):
model = Sleep model = models.Sleep
permission_required = ('core.delete_sleep',) permission_required = ('core.delete_sleep',)
success_url = '/sleep' success_url = '/sleep'
class TimerList(PermissionRequiredMixin, FilterView): class TimerList(PermissionRequiredMixin, FilterView):
model = Timer model = models.Timer
template_name = 'core/timer_list.html' template_name = 'core/timer_list.html'
permission_required = ('core.view_timer',) permission_required = ('core.view_timer',)
paginate_by = 10 paginate_by = 10
@ -182,14 +193,14 @@ class TimerList(PermissionRequiredMixin, FilterView):
class TimerDetail(PermissionRequiredMixin, DetailView): class TimerDetail(PermissionRequiredMixin, DetailView):
model = Timer model = models.Timer
permission_required = ('core.view_timer',) permission_required = ('core.view_timer',)
class TimerAdd(PermissionRequiredMixin, CreateView): class TimerAdd(PermissionRequiredMixin, CreateView):
model = Timer model = models.Timer
permission_required = ('core.add_timer',) permission_required = ('core.add_timer',)
form_class = TimerForm form_class = forms.TimerForm
success_url = '/timers' success_url = '/timers'
def get_form_kwargs(self): def get_form_kwargs(self):
@ -199,9 +210,9 @@ class TimerAdd(PermissionRequiredMixin, CreateView):
class TimerUpdate(PermissionRequiredMixin, UpdateView): class TimerUpdate(PermissionRequiredMixin, UpdateView):
model = Timer model = models.Timer
permission_required = ('core.change_timer',) permission_required = ('core.change_timer',)
form_class = TimerForm form_class = forms.TimerForm
success_url = '/timers' success_url = '/timers'
def get_form_kwargs(self): def get_form_kwargs(self):
@ -218,7 +229,7 @@ class TimerAddQuick(PermissionRequiredMixin, RedirectView):
permission_required = ('core.add_timer',) permission_required = ('core.add_timer',)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
instance = Timer.objects.create(user=request.user) instance = models.Timer.objects.create(user=request.user)
instance.save() instance.save()
self.url = request.GET.get( self.url = request.GET.get(
'next', reverse('timer-detail', args={instance.id})) 'next', reverse('timer-detail', args={instance.id}))
@ -229,7 +240,7 @@ class TimerRestart(PermissionRequiredMixin, RedirectView):
permission_required = ('core.change_timer',) permission_required = ('core.change_timer',)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
instance = Timer.objects.get(id=kwargs['pk']) instance = models.Timer.objects.get(id=kwargs['pk'])
instance.restart() instance.restart()
return super(TimerRestart, self).get(request, *args, **kwargs) return super(TimerRestart, self).get(request, *args, **kwargs)
@ -241,7 +252,7 @@ class TimerStop(PermissionRequiredMixin, RedirectView):
permission_required = ('core.change_timer',) permission_required = ('core.change_timer',)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
instance = Timer.objects.get(id=kwargs['pk']) instance = models.Timer.objects.get(id=kwargs['pk'])
instance.stop() instance.stop()
return super(TimerStop, self).get(request, *args, **kwargs) return super(TimerStop, self).get(request, *args, **kwargs)
@ -250,13 +261,13 @@ class TimerStop(PermissionRequiredMixin, RedirectView):
class TimerDelete(PermissionRequiredMixin, DeleteView): class TimerDelete(PermissionRequiredMixin, DeleteView):
model = Timer model = models.Timer
permission_required = ('core.delete_timer',) permission_required = ('core.delete_timer',)
success_url = '/' success_url = '/'
class TummyTimeList(PermissionRequiredMixin, FilterView): class TummyTimeList(PermissionRequiredMixin, FilterView):
model = TummyTime model = models.TummyTime
template_name = 'core/tummytime_list.html' template_name = 'core/tummytime_list.html'
permission_required = ('core.view_tummytime',) permission_required = ('core.view_tummytime',)
paginate_by = 10 paginate_by = 10
@ -264,9 +275,9 @@ class TummyTimeList(PermissionRequiredMixin, FilterView):
class TummyTimeAdd(PermissionRequiredMixin, CreateView): class TummyTimeAdd(PermissionRequiredMixin, CreateView):
model = TummyTime model = models.TummyTime
permission_required = ('core.add_tummytime',) permission_required = ('core.add_tummytime',)
form_class = TummyTimeForm form_class = forms.TummyTimeForm
success_url = '/tummy-time' success_url = '/tummy-time'
def get_form_kwargs(self): def get_form_kwargs(self):
@ -277,13 +288,13 @@ class TummyTimeAdd(PermissionRequiredMixin, CreateView):
class TummyTimeUpdate(PermissionRequiredMixin, UpdateView): class TummyTimeUpdate(PermissionRequiredMixin, UpdateView):
model = TummyTime model = models.TummyTime
permission_required = ('core.change_tummytime',) permission_required = ('core.change_tummytime',)
form_class = TummyTimeForm form_class = forms.TummyTimeForm
success_url = '/tummy-time' success_url = '/tummy-time'
class TummyTimeDelete(PermissionRequiredMixin, DeleteView): class TummyTimeDelete(PermissionRequiredMixin, DeleteView):
model = TummyTime model = models.TummyTime
permission_required = ('core.delete_tummytime',) permission_required = ('core.delete_tummytime',)
success_url = '/tummy-time' success_url = '/tummy-time'

View File

@ -18,7 +18,6 @@
<a class="dropdown-item" href="{% url 'reports:report-diaperchange-lifetimes-child' object.slug %}">Diaper Lifetimes</a> <a class="dropdown-item" href="{% url 'reports:report-diaperchange-lifetimes-child' object.slug %}">Diaper Lifetimes</a>
<a class="dropdown-item" href="{% url 'reports:report-sleep-pattern-child' object.slug %}">Sleep Pattern</a> <a class="dropdown-item" href="{% url 'reports:report-sleep-pattern-child' object.slug %}">Sleep Pattern</a>
<a class="dropdown-item" href="{% url 'reports:report-sleep-totals-child' object.slug %}">Sleep Totals</a> <a class="dropdown-item" href="{% url 'reports:report-sleep-totals-child' object.slug %}">Sleep Totals</a>
<a class="dropdown-item" href="{% url 'reports:report-timeline-child' object.slug %}">Timeline</a>
</div> </div>
</div> </div>

View File

@ -2,4 +2,3 @@ from .diaperchange_lifetimes import diaperchange_lifetimes # NOQA
from .diaperchange_types import diaperchange_types # NOQA from .diaperchange_types import diaperchange_types # NOQA
from .sleep_pattern import sleep_pattern # NOQA from .sleep_pattern import sleep_pattern # NOQA
from .sleep_totals import sleep_totals # NOQA from .sleep_totals import sleep_totals # NOQA
from .timeline import timeline # NOQA

View File

@ -1,42 +0,0 @@
{% extends 'reports/report_base.html' %}
{% load static %}
{% block title %}Timeline - {{ object }}{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item active" aria-current="page">Timeline</li>
{% endblock %}
{% block content %}
<h1 class="text-center">Timeline</h1>
<h2 class="text-center text-muted">{{ object }}</h2>
<h3 class="text-center">
<a class="btn btn-sm btn-default" href="?date={{ date_previous|date:"Y-m-d" }}" aria-label="Previous">
<i class="icon icon-chevron-left" aria-hidden="true"></i>
<span class="sr-only">Previous</span>
</a>
{{ date|date }}
<a class="btn btn-sm btn-default" href="?date={{ date_next|date:"Y-m-d" }}" aria-label="Next">
<i class="icon icon-chevron-right" aria-hidden="true"></i>
<span class="sr-only">Next</span>
</a>
</h3>
<ul class="timeline m-auto">
{% for object in objects %}
<li{% cycle "" ' class="timeline-inverted"' %}>
<div class="timeline-badge {% if object.type == "start" %}bg-success{% elif object.type == "end" %}bg-danger{% else %}bg-info{% endif %}">
<i class="icon icon-{{ object.model_name }}"></i>
</div>
<div class="card text-right">
<div class="card-body">
{{ object.event }}
</div>
<div class="card-footer text-muted">
{{ object.time|timesince }} ago ({{ object.time|time }})
</div>
</div>
</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -44,6 +44,3 @@ class ViewsTestCase(TestCase):
self.assertEqual(page.status_code, 200) self.assertEqual(page.status_code, 200)
page = self.c.get('{}/sleep/totals/'.format(base_url)) page = self.c.get('{}/sleep/totals/'.format(base_url))
self.assertEqual(page.status_code, 200) self.assertEqual(page.status_code, 200)
page = self.c.get('{}/timeline/'.format(base_url))
self.assertEqual(page.status_code, 200)

View File

@ -19,8 +19,4 @@ urlpatterns = [
url(r'^children/(?P<slug>[^/.]+)/reports/sleep/totals/$', url(r'^children/(?P<slug>[^/.]+)/reports/sleep/totals/$',
views.SleepTotalsChildReport.as_view(), views.SleepTotalsChildReport.as_view(),
name='report-sleep-totals-child'), name='report-sleep-totals-child'),
url(r'^children/(?P<slug>[^/.]+)/reports/timeline/$',
views.TimelineChildReport.as_view(),
name='report-timeline-child'),
] ]

View File

@ -3,12 +3,11 @@ from __future__ import unicode_literals
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.utils import timezone
from core.models import Child, DiaperChange, Sleep from core.models import Child, DiaperChange, Sleep
from .graphs import (diaperchange_types, diaperchange_lifetimes, sleep_pattern, from .graphs import (diaperchange_types, diaperchange_lifetimes, sleep_pattern,
sleep_totals, timeline) sleep_totals)
class DiaperChangeLifetimesChildReport(PermissionRequiredMixin, DetailView): class DiaperChangeLifetimesChildReport(PermissionRequiredMixin, DetailView):
@ -89,22 +88,3 @@ class SleepTotalsChildReport(PermissionRequiredMixin, DetailView):
if instances: if instances:
context['html'], context['javascript'] = sleep_totals(instances) context['html'], context['javascript'] = sleep_totals(instances)
return context return context
class TimelineChildReport(PermissionRequiredMixin, DetailView):
"""Chronological daily view of events (non-graph).
"""
model = Child
permission_required = ('core.view_child',)
template_name = 'reports/timeline.html'
def get_context_data(self, **kwargs):
context = super(TimelineChildReport, self).get_context_data(**kwargs)
date = self.request.GET.get('date', str(timezone.localdate()))
date = timezone.datetime.strptime(date, '%Y-%m-%d')
date = timezone.localtime(timezone.make_aware(date))
context['objects'] = timeline(self.object, date)
context['date'] = date
context['date_previous'] = date - timezone.timedelta(days=1)
context['date_next'] = date + timezone.timedelta(days=1)
return context