mybuddy/core/models.py

702 lines
21 KiB
Python

# -*- coding: utf-8 -*-
import re
from datetime import timedelta
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models
from django.utils import timezone
from django.utils.text import format_lazy, slugify
from django.utils.translation import gettext_lazy as _
from taggit.managers import TaggableManager as TaggitTaggableManager
from taggit.models import GenericTaggedItemBase, TagBase
from babybuddy.site_settings import NapSettings
from core.utils import random_color
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
:param model: a model instance with 'start' and 'end' attributes
:param max_duration: maximum allowed duration between start and end time
:return:
"""
if model.start and model.end:
if model.start > model.end:
raise ValidationError(
_("Start time must come before end time."), code="end_before_start"
)
if model.end - model.start > max_duration:
raise ValidationError(_("Duration too long."), code="max_duration")
def validate_unique_period(queryset, model):
"""
Confirm that model's start and end date do not intersect with other
instances.
:param queryset: a queryset of instances to check against.
:param model: a model instance with 'start' and 'end' attributes
:return:
"""
if model.id:
queryset = queryset.exclude(id=model.id)
if model.start and model.end:
if queryset.filter(start__lt=model.end, end__gt=model.start):
raise ValidationError(
_("Another entry intersects the specified time period."),
code="period_intersection",
)
def validate_time(time, field_name):
"""
Confirm that a time is not in the future.
:param time: a timezone aware datetime instance.
:param field_name: the name of the field being checked.
:return:
"""
if time and time > timezone.localtime():
raise ValidationError(
{field_name: _("Date/time can not be in the future.")}, code="time_invalid"
)
class Tag(TagBase):
DARK_COLOR = "#101010"
LIGHT_COLOR = "#EFEFEF"
color = models.CharField(
verbose_name=_("Color"),
max_length=32,
default=random_color,
validators=[RegexValidator(r"^#[0-9a-fA-F]{6}$")],
)
last_used = models.DateTimeField(
verbose_name=_("Last used"),
default=timezone.now,
blank=False,
)
class Meta:
verbose_name = _("Tag")
verbose_name_plural = _("Tags")
@property
def complementary_color(self):
if not self.color:
return self.DARK_COLOR
r, g, b = [int(x, 16) for x in re.match("#(..)(..)(..)", self.color).groups()]
yiq = ((r * 299) + (g * 587) + (b * 114)) // 1000
if yiq >= 128:
return self.DARK_COLOR
else:
return self.LIGHT_COLOR
class Tagged(GenericTaggedItemBase):
tag = models.ForeignKey(
Tag,
verbose_name=_("Tag"),
on_delete=models.CASCADE,
related_name="%(app_label)s_%(class)s_items",
)
def save_base(self, *args, **kwargs):
"""
Update last_used of the used tag, whenever it is used in a
save-operation.
"""
self.tag.last_used = timezone.now()
self.tag.save()
return super().save_base(*args, **kwargs)
class TaggableManager(TaggitTaggableManager):
pass
class BMI(models.Model):
model_name = "bmi"
child = models.ForeignKey(
"Child", on_delete=models.CASCADE, related_name="bmi", verbose_name=_("Child")
)
bmi = models.FloatField(blank=False, null=False, verbose_name=_("BMI"))
date = models.DateField(
blank=False, default=timezone.localdate, null=False, verbose_name=_("Date")
)
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
tags = TaggableManager(blank=True, through=Tagged)
objects = models.Manager()
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-date"]
verbose_name = _("BMI")
verbose_name_plural = _("BMI")
def __str__(self):
return str(_("BMI"))
def clean(self):
validate_date(self.date, "date")
class Child(models.Model):
model_name = "child"
first_name = models.CharField(max_length=255, verbose_name=_("First name"))
last_name = models.CharField(
blank=True, max_length=255, verbose_name=_("Last name")
)
birth_date = models.DateField(blank=False, null=False, verbose_name=_("Birth date"))
slug = models.SlugField(
allow_unicode=True,
blank=False,
editable=False,
max_length=100,
unique=True,
verbose_name=_("Slug"),
)
picture = models.ImageField(
blank=True, null=True, upload_to="child/picture/", verbose_name=_("Picture")
)
objects = models.Manager()
cache_key_count = "core.child.count"
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["last_name", "first_name"]
verbose_name = _("Child")
verbose_name_plural = _("Children")
def __str__(self):
return self.name()
def save(self, *args, **kwargs):
self.slug = slugify(self, allow_unicode=True)
super(Child, self).save(*args, **kwargs)
cache.set(self.cache_key_count, Child.objects.count(), None)
def delete(self, using=None, keep_parents=False):
super(Child, self).delete(using, keep_parents)
cache.set(self.cache_key_count, Child.objects.count(), None)
def name(self, reverse=False):
if not self.last_name:
return self.first_name
if reverse:
return "{}, {}".format(self.last_name, self.first_name)
return "{} {}".format(self.first_name, self.last_name)
@classmethod
def count(cls):
"""Get a (cached) count of total number of Child instances."""
return cache.get_or_set(cls.cache_key_count, Child.objects.count, None)
class DiaperChange(models.Model):
model_name = "diaperchange"
child = models.ForeignKey(
"Child",
on_delete=models.CASCADE,
related_name="diaper_change",
verbose_name=_("Child"),
)
time = models.DateTimeField(
blank=False, default=timezone.localtime, null=False, verbose_name=_("Time")
)
wet = models.BooleanField(verbose_name=_("Wet"))
solid = models.BooleanField(verbose_name=_("Solid"))
color = models.CharField(
blank=True,
choices=[
("black", _("Black")),
("brown", _("Brown")),
("green", _("Green")),
("yellow", _("Yellow")),
],
max_length=255,
verbose_name=_("Color"),
)
amount = models.FloatField(blank=True, null=True, verbose_name=_("Amount"))
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
tags = TaggableManager(blank=True, through=Tagged)
objects = models.Manager()
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-time"]
verbose_name = _("Diaper Change")
verbose_name_plural = _("Diaper Changes")
def __str__(self):
return str(_("Diaper Change"))
def attributes(self):
attributes = []
if self.wet:
attributes.append(self._meta.get_field("wet").verbose_name)
if self.solid:
attributes.append(self._meta.get_field("solid").verbose_name)
if self.color:
attributes.append(self.get_color_display())
return attributes
def clean(self):
validate_time(self.time, "time")
class Feeding(models.Model):
model_name = "feeding"
child = models.ForeignKey(
"Child",
on_delete=models.CASCADE,
related_name="feeding",
verbose_name=_("Child"),
)
start = models.DateTimeField(
blank=False,
default=timezone.localtime,
null=False,
verbose_name=_("Start time"),
)
end = models.DateTimeField(
blank=False, default=timezone.localtime, null=False, verbose_name=_("End time")
)
duration = models.DurationField(
editable=False, null=True, verbose_name=_("Duration")
)
type = models.CharField(
choices=[
("breast milk", _("Breast milk")),
("formula", _("Formula")),
("fortified breast milk", _("Fortified breast milk")),
("solid food", _("Solid food")),
],
max_length=255,
verbose_name=_("Type"),
)
method = models.CharField(
choices=[
("bottle", _("Bottle")),
("left breast", _("Left breast")),
("right breast", _("Right breast")),
("both breasts", _("Both breasts")),
("parent fed", _("Parent fed")),
("self fed", _("Self fed")),
],
max_length=255,
verbose_name=_("Method"),
)
amount = models.FloatField(blank=True, null=True, verbose_name=_("Amount"))
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
tags = TaggableManager(blank=True, through=Tagged)
objects = models.Manager()
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-start"]
verbose_name = _("Feeding")
verbose_name_plural = _("Feedings")
def __str__(self):
return str(_("Feeding"))
def save(self, *args, **kwargs):
if self.start and self.end:
self.duration = self.end - self.start
super(Feeding, self).save(*args, **kwargs)
def clean(self):
validate_time(self.start, "start")
validate_duration(self)
validate_unique_period(Feeding.objects.filter(child=self.child), self)
class HeadCircumference(models.Model):
model_name = "head_circumference"
child = models.ForeignKey(
"Child",
on_delete=models.CASCADE,
related_name="head_circumference",
verbose_name=_("Child"),
)
head_circumference = models.FloatField(
blank=False, null=False, verbose_name=_("Head Circumference")
)
date = models.DateField(
blank=False, default=timezone.localdate, null=False, verbose_name=_("Date")
)
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
tags = TaggableManager(blank=True, through=Tagged)
objects = models.Manager()
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-date"]
verbose_name = _("Head Circumference")
verbose_name_plural = _("Head Circumference")
def __str__(self):
return str(_("Head Circumference"))
def clean(self):
validate_date(self.date, "date")
class Height(models.Model):
model_name = "height"
child = models.ForeignKey(
"Child",
on_delete=models.CASCADE,
related_name="height",
verbose_name=_("Child"),
)
height = models.FloatField(blank=False, null=False, verbose_name=_("Height"))
date = models.DateField(
blank=False, default=timezone.localdate, null=False, verbose_name=_("Date")
)
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
tags = TaggableManager(blank=True, through=Tagged)
objects = models.Manager()
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-date"]
verbose_name = _("Height")
verbose_name_plural = _("Height")
def __str__(self):
return str(_("Height"))
def clean(self):
validate_date(self.date, "date")
class Note(models.Model):
model_name = "note"
child = models.ForeignKey(
"Child", on_delete=models.CASCADE, related_name="note", verbose_name=_("Child")
)
note = models.TextField(verbose_name=_("Note"))
time = models.DateTimeField(
blank=False, default=timezone.localtime, verbose_name=_("Time")
)
tags = TaggableManager(blank=True, through=Tagged)
objects = models.Manager()
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-time"]
verbose_name = _("Note")
verbose_name_plural = _("Notes")
def __str__(self):
return str(_("Note"))
class Pumping(models.Model):
model_name = "pumping"
child = models.ForeignKey(
"Child",
on_delete=models.CASCADE,
related_name="pumping",
verbose_name=_("Child"),
)
start = models.DateTimeField(
blank=False,
default=timezone.localtime,
null=False,
verbose_name=_("Start time"),
)
end = models.DateTimeField(
blank=False,
default=timezone.localtime,
null=False,
verbose_name=_("End time"),
)
duration = models.DurationField(
editable=False,
null=True,
verbose_name=_("Duration"),
)
amount = models.FloatField(blank=False, null=False, verbose_name=_("Amount"))
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
tags = TaggableManager(blank=True, through=Tagged)
objects = models.Manager()
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-start"]
verbose_name = _("Pumping")
verbose_name_plural = _("Pumping")
def __str__(self):
return str(_("Pumping"))
def save(self, *args, **kwargs):
if self.start and self.end:
self.duration = self.end - self.start
super(Pumping, self).save(*args, **kwargs)
def clean(self):
validate_time(self.start, "start")
validate_duration(self)
validate_unique_period(Pumping.objects.filter(child=self.child), self)
class Sleep(models.Model):
model_name = "sleep"
child = models.ForeignKey(
"Child", on_delete=models.CASCADE, related_name="sleep", verbose_name=_("Child")
)
start = models.DateTimeField(
blank=False,
default=timezone.localtime,
null=False,
verbose_name=_("Start time"),
)
end = models.DateTimeField(
blank=False, default=timezone.localtime, null=False, verbose_name=_("End time")
)
nap = models.BooleanField(null=False, blank=True, verbose_name=_("Nap"))
duration = models.DurationField(
editable=False, null=True, verbose_name=_("Duration")
)
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
tags = TaggableManager(blank=True, through=Tagged)
objects = models.Manager()
settings = NapSettings(_("Nap settings"))
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-start"]
verbose_name = _("Sleep")
verbose_name_plural = _("Sleep")
def __str__(self):
return str(_("Sleep"))
def save(self, *args, **kwargs):
if self.nap is None:
self.nap = (
Sleep.settings.nap_start_min
<= timezone.localtime(self.start).time()
<= Sleep.settings.nap_start_max
)
if self.start and self.end:
self.duration = self.end - self.start
super(Sleep, self).save(*args, **kwargs)
def clean(self):
validate_time(self.start, "start")
validate_time(self.end, "end")
validate_duration(self)
validate_unique_period(Sleep.objects.filter(child=self.child), self)
class Temperature(models.Model):
model_name = "temperature"
child = models.ForeignKey(
"Child",
on_delete=models.CASCADE,
related_name="temperature",
verbose_name=_("Child"),
)
temperature = models.FloatField(
blank=False, null=False, verbose_name=_("Temperature")
)
time = models.DateTimeField(
blank=False, default=timezone.localtime, null=False, verbose_name=_("Time")
)
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
tags = TaggableManager(blank=True, through=Tagged)
objects = models.Manager()
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-time"]
verbose_name = _("Temperature")
verbose_name_plural = _("Temperature")
def __str__(self):
return str(_("Temperature"))
def clean(self):
validate_time(self.time, "time")
class Timer(models.Model):
model_name = "timer"
child = models.ForeignKey(
"Child",
blank=True,
null=True,
on_delete=models.CASCADE,
related_name="timers",
verbose_name=_("Child"),
)
name = models.CharField(
blank=True, max_length=255, null=True, verbose_name=_("Name")
)
start = models.DateTimeField(
default=timezone.now, blank=False, verbose_name=_("Start time")
)
active = models.BooleanField(default=True, editable=False, verbose_name=_("Active"))
user = models.ForeignKey(
"auth.User",
on_delete=models.CASCADE,
related_name="timers",
verbose_name=_("User"),
)
objects = models.Manager()
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-start"]
verbose_name = _("Timer")
verbose_name_plural = _("Timers")
def __str__(self):
return self.name or str(format_lazy(_("Timer #{id}"), id=self.id))
@property
def title_with_child(self):
"""Get Timer title with child name in parenthesis."""
title = str(self)
# Only actually add the name if there is more than one Child instance.
if title and self.child and Child.count() > 1:
title = format_lazy("{title} ({child})", title=title, child=self.child)
return title
@property
def user_username(self):
"""Get Timer user's name with a preference for the full name."""
if self.user.get_full_name():
return self.user.get_full_name()
return self.user.get_username()
def duration(self):
return timezone.now() - self.start
def restart(self):
"""Restart the timer."""
self.start = timezone.now()
self.save()
def stop(self):
"""Stop (delete) the timer."""
self.delete()
def save(self, *args, **kwargs):
self.name = self.name or None
super(Timer, self).save(*args, **kwargs)
def clean(self):
validate_time(self.start, "start")
class TummyTime(models.Model):
model_name = "tummytime"
child = models.ForeignKey(
"Child",
on_delete=models.CASCADE,
related_name="tummy_time",
verbose_name=_("Child"),
)
start = models.DateTimeField(
blank=False,
default=timezone.localtime,
null=False,
verbose_name=_("Start time"),
)
end = models.DateTimeField(
blank=False, default=timezone.localtime, null=False, verbose_name=_("End time")
)
duration = models.DurationField(
editable=False, null=True, verbose_name=_("Duration")
)
milestone = models.CharField(
blank=True, max_length=255, verbose_name=_("Milestone")
)
tags = TaggableManager(blank=True, through=Tagged)
objects = models.Manager()
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-start"]
verbose_name = _("Tummy Time")
verbose_name_plural = _("Tummy Time")
def __str__(self):
return str(_("Tummy Time"))
def save(self, *args, **kwargs):
if self.start and self.end:
self.duration = self.end - self.start
super(TummyTime, self).save(*args, **kwargs)
def clean(self):
validate_time(self.start, "start")
validate_time(self.end, "end")
validate_duration(self)
validate_unique_period(TummyTime.objects.filter(child=self.child), self)
class Weight(models.Model):
model_name = "weight"
child = models.ForeignKey(
"Child",
on_delete=models.CASCADE,
related_name="weight",
verbose_name=_("Child"),
)
weight = models.FloatField(blank=False, null=False, verbose_name=_("Weight"))
date = models.DateField(
blank=False, default=timezone.localdate, null=False, verbose_name=_("Date")
)
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
tags = TaggableManager(blank=True, through=Tagged)
objects = models.Manager()
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-date"]
verbose_name = _("Weight")
verbose_name_plural = _("Weight")
def __str__(self):
return str(_("Weight"))
def clean(self):
validate_date(self.date, "date")