from datetime import date from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey from django.db import models from django.utils.functional import SimpleLazyObject, empty class Period(object): DAY = 86400 # seconds WEEK = DAY * 7 DAYS_28 = DAY * 28 MONTH = DAY * 30 LIFETIME = 0 PERIOD_CHOICES = ( (Period.DAY, 'Day'), (Period.WEEK, 'Week'), (Period.DAYS_28, '28 days'), (Period.MONTH, 'Month'), (Period.LIFETIME, 'Lifetime')) class RegisterLazilyManagerMixin(object): _lazy_entries = [] def _register(self, defaults=None, **kwargs): """Fetch (update or create) an instance, lazily. We're doing this lazily, so that it becomes possible to define custom enums in your code, even before the Django ORM is fully initialized. Domain.objects.SHOPPING = Domain.objects.register( ref='shopping', name='Webshop') Domain.objects.USERS = Domain.objects.register( ref='users', name='User Accounts') """ f = lambda: self.update_or_create(defaults=defaults, **kwargs)[0] ret = SimpleLazyObject(f) self._lazy_entries.append(ret) return ret def clear_cache(self): """For testability""" for entry in self._lazy_entries: entry._wrapped = empty class DomainManager(RegisterLazilyManagerMixin, models.Manager): def register(self, ref, name=''): return super(DomainManager, self)._register( defaults={'name': name}, ref=ref) def get_by_natural_key(self, ref): return self.get(ref=ref) class Domain(models.Model): objects = DomainManager() ref = models.CharField( max_length=100, unique=True, help_text="Unique reference ID for this domain") name = models.CharField( max_length=100, blank=True, help_text="Short descriptive name") def __str__(self): return self.name or self.ref def natural_key(self): return [self.ref] class MetricManager(RegisterLazilyManagerMixin, models.Manager): def register(self, domain, ref, name='', description=''): return super(MetricManager, self)._register( defaults={'name': name, 'description': description}, domain=domain, ref=ref) def get_by_natural_key(self, domain, ref): return self.get(source=domain, ref=ref) class Metric(models.Model): objects = MetricManager() domain = models.ForeignKey(Domain, on_delete=models.PROTECT) ref = models.CharField( max_length=100, help_text="Unique reference ID for this metric within the domain") name = models.CharField( max_length=100, blank=True, help_text="Short descriptive name") description = models.TextField( blank=True, help_text="Description") class Meta: unique_together = ('domain', 'ref') def __str__(self): return self.name or self.ref def natural_key(self): return [self.source, self.ref] class AbstractStatisticQuerySet(models.QuerySet): order_field = None def narrow(self, metric=None, metrics=None, period=None): qs = self assert metric is None or metrics is None if metric is not None: metrics = [metric] if metrics is not None: qs = qs.filter(metric__in=metrics) if period: qs = qs.filter(period=period) return qs def record(self, metric, value, period, **kwargs): instance, _ = self.update_or_create( period=period, metric=metric, defaults={'value': value}, **kwargs) return instance def most_recent(self, **kwargs): return self.narrow(**kwargs).order_by('-' + self.order_field).first() class AbstractStatistic(models.Model): metric = models.ForeignKey(Metric, on_delete=models.PROTECT) value = models.BigIntegerField( # To support storing that no data is available, use: NULL null=True) period = models.IntegerField(choices=PERIOD_CHOICES) class Meta: abstract = True class ByObjectMixin(models.Model): object_type = models.ForeignKey(ContentType, on_delete=models.PROTECT) object_id = models.PositiveIntegerField() object = GenericForeignKey( 'object_type', 'object_id') class Meta: abstract = True class ByObjectQuerySetMixin(object): def record(self, **kwargs): object = kwargs.pop('object') ct = ContentType.objects.get_for_model(object) return super(ByObjectQuerySetMixin, self).record( object_id=object.pk, object_type=ct, **kwargs) def narrow(self, **kwargs): qs = self object = kwargs.pop('object', None) objects = kwargs.pop('objects', None) object_type = kwargs.pop('object_type', None) assert object is None or objects is None if object is not None: objects = [object] if object_type: qs = qs.filter(object_type=object_type) if type(objects) in (list, tuple, set): if not objects: qs = self.none() else: # Assumption: all objects are of same type ct = ContentType.objects.get_for_model(objects[0]) qs = qs.filter( object_type=ct, object_id__in=[s.pk for s in objects]) elif isinstance(objects, models.QuerySet): ct = ContentType.objects.get_for_model(objects.model) qs = qs.filter( object_type=ct, object_id__in=objects.values_list( 'id', flat=True)) elif objects is None: pass elif isinstance(objects, models.query.EmptyQuerySet): qs = self.none() else: raise NotImplementedError return super(ByObjectQuerySetMixin, qs).narrow(**kwargs) class ByDateMixin(models.Model): date = models.DateField(db_index=True) class Meta: abstract = True class ByDateQuerySetMixin(object): order_field = 'date' def record(self, **kwargs): dt = kwargs.pop('date', date.today()) return super(ByDateQuerySetMixin, self).record(date=dt, **kwargs) def narrow(self, **kwargs): """Up-to including""" from_date = kwargs.pop('from_date', None) to_date = kwargs.pop('to_date', None) date = kwargs.pop('date', None) qs = self if from_date: qs = qs.filter(date__gte=from_date) if to_date: qs = qs.filter(date__lte=to_date) if date: qs = qs.filter(date=date) return super(ByDateQuerySetMixin, qs).narrow(**kwargs) class StatisticByDateQuerySet( ByDateQuerySetMixin, AbstractStatisticQuerySet): pass class StatisticByDateAndObjectQuerySet( ByDateQuerySetMixin, ByObjectQuerySetMixin, AbstractStatisticQuerySet): pass class StatisticByDate(ByDateMixin, AbstractStatistic): objects = StatisticByDateQuerySet.as_manager() class Meta: unique_together = [ 'date', 'metric', 'period'] verbose_name = 'Statistic by date' verbose_name_plural = 'Statistics by date' def __str__(self): return '{date}: {value}'.format( date=self.date, value=self.value) class StatisticByDateAndObject( ByDateMixin, ByObjectMixin, AbstractStatistic): objects = StatisticByDateAndObjectQuerySet.as_manager() class Meta: unique_together = [ 'date', 'metric', 'object_type', 'object_id', 'period'] verbose_name = 'Statistic by date and object' verbose_name_plural = 'Statistics by date and object' def __str__(self): return '{date}: {value}'.format( date=self.date, value=self.value)