from django.db import models from django.utils import timezone import math from config.extras import DateUUIDField from config.extras import BREWFATHER_APP_ROOT import logging logger = logging.getLogger('django') def average(lst): return sum(lst) / len(lst) class AvailableYeastManager(models.Manager): """ Special manager for filtering out pitched yeast.""" def get_queryset(self): return super(AvailableYeastManager, self)\ .get_queryset().filter(pitched=False) class CustomModel(models.Model): """ Custom model class with default fields to use. """ created_date = models.DateTimeField(default=timezone.now) class Meta: abstract = True class Manufacturer(CustomModel): """ Store manufacturer data for various yeast strains.""" name = models.CharField(max_length=100) website = models.URLField(max_length=200, blank=True, null=True) def __str__(self): # Return a string that represents the instance return self.name class Strain(CustomModel): """ Store individual yeast strain data. :model:`yeast.Manufacturer`. """ name = models.CharField(max_length=100) long_name = models.CharField(max_length=100, blank=True) manufacturer = models.ForeignKey(Manufacturer, on_delete=models.PROTECT) @property def batches_available(self): return [x for x in Propogation.objects.all() if not x.consumed and x.strain == self] def __str__(self): # Return a string that represents the instance return '{}: {}'.format(self.manufacturer.name, self.name) class Storage(CustomModel): """ Data for methods of yeast storage. Used for calculating viability in :model:`yeast.Yeast`. """ name = models.CharField(max_length=100) viability_loss = models.DecimalField(max_digits=6, decimal_places=4) viability_interval = models.IntegerField(default=30) def __str__(self): # Return a string that represents the instance return self.name class Propogation(CustomModel): """ Stores a batch of :model:`yeast.Yeast` of a single :model:`yeast.Strain`. Can be a single purchased pack, or multiple vials to be frozen from a starter. """ BATCH_TYPES = { 'ST': 'Store', 'PR': 'Propogated', 'SL': 'Slurry', } id = DateUUIDField(primary_key=True) parent = models.ManyToManyField('Yeast', related_name='+', blank=True) production_date = models.DateField() strain = models.ForeignKey(Strain, on_delete=models.PROTECT, default=0) source = models.CharField(max_length=3, choices=BATCH_TYPES, default='ST') source_batch = models.ForeignKey( 'beer.Batch', null=True, blank=True, on_delete=models.PROTECT) notes = models.TextField(max_length=500, blank=True, null=True) def save(self, *args, **kwargs): super(Propogation, self).save(*args, **kwargs) if self.source_batch: relate_samples = [x for x in Yeast.objects.all() if x.pitched_batch == self.source_batch] for sample in relate_samples: self.parent.add(sample) @property def age(self): return int(average([x.age for x in Yeast.available.all() if x.propogation == self])) @property def max_viability(self): return int(max([x.viability for x in Yeast.available.all() if x.propogation == self]) * 100) @property def min_viability(self): return int(min([x.viability for x in Yeast.available.all() if x.propogation == self]) * 100) @property def generation(self): return int(average([x.generation_num for x in Yeast.available.all() if x.propogation == self])) @property def beer_name(self): if self.source_batch: return self.source_batch.brewfather_name @property def beer_num(self): if self.source_batch: return self.source_batch.brewfather_num @property def beer_url(self): if self.source_batch: return '{}/tabs/batches/batch/{}'.format( BREWFATHER_APP_ROOT, self.source_batch.brewfather_id ) @property def consumed(self): return not len(self.remaining_samples) > 0 @property def remaining_samples(self): return [x for x in Yeast.available.all() if x.propogation == self] @property def used_samples(self): return [x for x in Yeast.objects.all() if x.propogation == self and x.pitched] def __str__(self): # Return a string that represents the instance return '{} [{}]'.format( self.strain, self.production_date.strftime("%Y-%m-%d") ) class Yeast(CustomModel): """ Store an individual sample of yeast. """ id = DateUUIDField(primary_key=True) propogation = models.ForeignKey(Propogation, on_delete=models.CASCADE) generation_num = models.IntegerField(default=0) storage = models.ForeignKey(Storage, on_delete=models.CASCADE) cellcount = models.IntegerField(default=100) pitched = models.BooleanField(default=False) date_pitched = models.DateField(blank=True, null=True) pitched_batch = models.ForeignKey( 'beer.Batch', null=True, blank=True, on_delete=models.CASCADE) data_web = models.URLField(blank=True, null=True) lot_number = models.CharField(max_length=15, blank=True, null=True) notes = models.TextField(max_length=500, blank=True, null=True) objects = models.Manager() available = AvailableYeastManager() @property def name(self): return '{} {}'.format(self.id, self.propogation.strain.name) @property def age(self): """Return the age in days since the sample was propogated.""" if self.pitched: end_date = self.date_pitched else: end_date = timezone.now().date() return abs((self.propogation.production_date-end_date).days) @property def viability(self): """Return the viability based on age and storage method (:model:`yeast.Storage`). """ return 0.97 * math.exp(self.age * math.log(1-self.storage.viability_loss) / self.storage.viability_interval) def __str__(self): # Return a string that represents the instance return '{} {}'.format(self.id, self.propogation.strain.name)