import datetime import logging import operator from collections import deque from django.db import models from django.utils import timezone as tz from django.utils.translation import gettext_lazy as _ from .diffusion import Diffusion from .sound import Sound from .station import Station from .track import Track from .page import Renderable logger = logging.getLogger("aircox") __all__ = ("Log", "LogQuerySet") class LogQuerySet(models.QuerySet): def station(self, station=None, id=None): return self.filter(station=station) if id is None else self.filter(station_id=id) def date(self, date): start = tz.datetime.combine(date, datetime.time()) end = tz.datetime.combine(date, datetime.time(23, 59, 59, 999)) return self.filter(date__range=(start, end)) # this filter does not work with mysql # return self.filter(date__date=date) def after(self, date): return self.filter(date__gte=date) if isinstance(date, tz.datetime) else self.filter(date__date__gte=date) def before(self, date): return self.filter(date__lte=date) if isinstance(date, tz.datetime) else self.filter(date__date__lte=date) def on_air(self): return self.filter(type=Log.TYPE_ON_AIR) def start(self): return self.filter(type=Log.TYPE_START) def with_diff(self, with_it=True): return self.filter(diffusion__isnull=not with_it) def with_sound(self, with_it=True): return self.filter(sound__isnull=not with_it) def with_track(self, with_it=True): return self.filter(track__isnull=not with_it) class Log(Renderable, models.Model): """Log sounds and diffusions that are played on the station. This only remember what has been played on the outputs, not on each source; Source designate here which source is responsible of that. """ template_prefix = "log" TYPE_STOP = 0x00 """Source has been stopped, e.g. manually.""" # Rule: \/ diffusion != null \/ sound != null TYPE_START = 0x01 """Diffusion or sound has been request to be played.""" TYPE_CANCEL = 0x02 """Diffusion has been canceled.""" # Rule: \/ sound != null /\ track == null # \/ sound == null /\ track != null # \/ sound == null /\ track == null /\ comment = sound_path TYPE_ON_AIR = 0x03 """Sound or diffusion occured on air.""" TYPE_OTHER = 0x04 """Other log.""" TYPE_CHOICES = ( (TYPE_STOP, _("stop")), (TYPE_START, _("start")), (TYPE_CANCEL, _("cancelled")), (TYPE_ON_AIR, _("on air")), (TYPE_OTHER, _("other")), ) station = models.ForeignKey( Station, models.CASCADE, verbose_name=_("station"), help_text=_("related station"), ) type = models.SmallIntegerField(_("type"), choices=TYPE_CHOICES) date = models.DateTimeField(_("date"), default=tz.now, db_index=True) source = models.CharField( # we use a CharField to avoid loosing logs information if the # source is removed max_length=64, blank=True, null=True, verbose_name=_("source"), help_text=_("Identifier of the log's source."), ) comment = models.CharField( max_length=512, blank=True, null=True, verbose_name=_("comment"), ) sound = models.ForeignKey( Sound, models.SET_NULL, blank=True, null=True, db_index=True, verbose_name=_("Sound"), ) track = models.ForeignKey( Track, models.SET_NULL, blank=True, null=True, db_index=True, verbose_name=_("Track"), ) diffusion = models.ForeignKey( Diffusion, models.SET_NULL, blank=True, null=True, db_index=True, verbose_name=_("Diffusion"), ) objects = LogQuerySet.as_manager() @property def related(self): return self.diffusion or self.sound or self.track # FIXME: required???? @property def local_date(self): """Return a version of self.date that is localized to self.timezone; This is needed since datetime are stored as UTC date and we want to get it as local time.""" return tz.localtime(self.date, tz.get_current_timezone()) # prepare for the future on crash + ease the use in merged lists with # diffusions @property def start(self): return self.date class Meta: verbose_name = _("Log") verbose_name_plural = _("Logs") def __str__(self): return "#{} ({}, {}, {})".format( self.pk, self.get_type_display(), self.source, self.local_date.strftime("%Y/%m/%d %H:%M%z"), ) @classmethod def __list_append(cls, object_list, items): object_list += [cls(obj) for obj in items] @classmethod def merge_diffusions(cls, logs, diffs, count=None, diff_count=None, group_logs=False): """Merge logs and diffusions together. `logs` can either be a queryset or a list ordered by `Log.date`. """ if isinstance(logs, models.QuerySet): logs = list(logs.order_by("-date")) diffs = diffs.on_air().order_by("-start") if diff_count: diffs = diffs[:diff_count] diffs = deque(diffs) object_list = [] while True: if not len(diffs): cls._append_logs(object_list, logs, len(logs), group=group_logs) break if not len(logs): object_list += diffs break diff = diffs.popleft() # - takes all logs after diff start index = cls._next_index(logs, diff.end, len(logs), pred=operator.le) cls._append_logs(object_list, logs, index, group=group_logs) if len(logs): # FIXME # - last log while diff is running # if logs[0].date > diff.start: # object_list.append(logs[0]) # - skips logs while diff is running index = cls._next_index(logs, diff.start, len(logs)) if index is not None and index > 0: logs = logs[index:] # - add diff object_list.append(diff) return object_list if count is None else object_list[:count] @classmethod def _next_index(cls, items, date, default, pred=operator.lt): iter = (i for i, v in enumerate(items) if pred(v.date, date)) return next(iter, default) @classmethod def _append_logs(cls, object_list, logs, count, group=False): logs = logs[:count] if not logs: return object_list if group: grouped = cls._group_logs_by_time(logs) object_list.extend(grouped) else: object_list += logs return object_list @classmethod def _group_logs_by_time(cls, logs): last_time = -1 cum = [] for log in logs: hour = log.date.time().hour if hour != last_time: if cum: yield cum cum = [] last_time = hour # reverse from lowest to highest date cum.insert(0, log) if cum: yield cum def print(self): r = [] if self.diffusion: r.append("diff: " + str(self.diffusion_id)) if self.sound: r.append("sound: " + str(self.sound_id)) if self.track: r.append("track: " + str(self.track_id)) logger.info( "log %s: %s%s", str(self), self.comment or "", " (" + ", ".join(r) + ")" if r else "", )