Source code for berhoel.django.media.views

#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""Views from media.
"""

# Standard library imports.
import calendar
import datetime
from typing import Union
from functools import cached_property
from dataclasses import dataclass

# Django library imports.
from django.http import HttpResponseRedirect
from django.template import Template
from django.db.models import Min, Count
from django.shortcuts import HttpResponse
from django.views.generic import (
    ListView,
    CreateView,
    DeleteView,
    UpdateView,
    TemplateView,
)
from django.template.context import RequestContext

# Third party library imports.
import numpy as np
import pandas as pd
import plotly.offline as opy
import plotly.graph_objs as go

# Local library imports.
from . import forms, models
from .utils import Calendar
from .models import Seen

__date__ = "2021/01/02 20:04:30 hoel"
__author__ = "Berthold Höllmann"
__copyright__ = "Copyright © 2020 by Berthold Höllmann"
__credits__ = ["Berthold Höllmann"]
__maintainer__ = "Berthold Höllmann"
__email__ = "berhoel@gmail.com"

# Create your views here.


[docs]@dataclass class Section: title: str container: str = "container-xl"
[docs]class IndexView(ListView): template_name = "media/index.html" context_object_name = "seen_list" paginate_by = 30
[docs] @staticmethod def get_queryset() -> list: """Get all seen items.""" return models.Seen.objects.order_by("-date")
[docs] def get_context_data(self, *, object_list=None, **kwargs: dict) -> dict: ctx = super().get_context_data(object_list=object_list, **kwargs) ctx.update( { "section": Section("Seen Shows"), "media_types": models.MediaTypes, "watch_item_types": models.WatchItemTypes.__members__, "request": self.request, } ) return ctx
[docs]def calendar_view(request, **kwargs): def prev_month(date: datetime.date) -> Union[str, None]: "Calculate link to previous month." year, month = date.year, date.month if (month := month - 1) < 1: month = 12 year -= 1 target = datetime.date(year, month, 1) if mindate is not None and target <= mindate: return None return target.strftime("%Y/%m/") def next_month(date: datetime.date) -> Union[str, None]: "Calculate link to next month." year, month = date.year, date.month if (month := month + 1) > 12: month = 1 year += 1 target = datetime.date(year, month, 1) if target >= datetime.datetime.now().date(): return None return target.strftime("%Y/%m/") mindate = models.Seen.objects.aggregate(Min("date"))["date__min"] cal = Calendar() now = datetime.datetime.now() year = kwargs.get("year", now.year) month = kwargs.get("month", now.month) seens = Seen.objects.filter(date__year=year, date__month=month) context = RequestContext( request, { f"seens_per_day_d{i+1}": seens.filter(date__day=i + 1) for i in range(calendar.monthrange(year, month)[1]) }, ) context.update({"seens": seens}) act_date = datetime.date(year, month, 1) this_month = None if (year, month) == (now.year, now.month) else "" if mindate is not None and act_date > mindate: first_month = mindate.strftime("%Y/%m/") else: first_month = None context.update( { "watch_item_types": models.WatchItemTypes.__members__, "prev_month": prev_month(act_date), "next_month": next_month(act_date), "this_month": this_month, "first_month": first_month, "section": Section("Shows Overview", container="container-fluid"), } ) this_calendar = cal.formatmonth(act_date.year, act_date.month, withyear=True) context.update({"calendar": this_calendar}) return HttpResponse( Template( f"""<!-- calendar.html --> {{% extends 'media/base.html' %}}{{% csrf_token %}} {{% load crispy_forms_tags %}} {{% load fontawesome_5 %}} {{% block content %}} {{% spaceless %}} {{% if first_month is not None %}} <a href="{{% url 'calendar' %}}{{{{ first_month }}}}">First Month</a> | {{% endif %}} {{% if prev_month is not None %}} <a href="{{% url 'calendar' %}}{{{{ prev_month }}}}">Pervious Month</a> {{% endif %}} {{% if prev_month is not None and next_month is not None %}} | {{% endif %}} {{% if next_month is not None %}} <a href="{{% url 'calendar' %}}{{{{ next_month }}}}">Next Month</a> {{% endif %}} {{% if this_month is not None %}} | <a href="{{% url 'calendar' %}}{{{{ this_month }}}}">Current Month</a> {{% endif %}} {{% endspaceless %}} { this_calendar } {{% endblock content %}} <!-- calendar.html (END) --> """ ).render(context) )
[docs]class PersonCreateView(CreateView): model = models.Person fields = ("name",)
[docs] def get_context_data(self, **kwargs: dict) -> dict: ctx = super().get_context_data(**kwargs) ctx["section"] = Section("Create Person") return ctx
[docs]class PersonUpdateView(UpdateView): model = models.Person form_class = forms.PersonForm template_name = "media/person_update_form.html"
[docs] def get_success_url(self): return HttpResponseRedirect(self.request.META["HTTP_REFERER"])
[docs] def get_context_data(self, **kwargs: dict) -> dict: ctx = super().get_context_data(**kwargs) ctx["section"] = Section("Edit Person") return ctx
[docs]class SeenUpdateView(UpdateView): model = models.Seen form_class = forms.SeenForm
[docs] def get_success_url(self): return HttpResponseRedirect(self.request.META["HTTP_REFERER"])
[docs] def get_context_data(self, **kwargs: dict) -> dict: ctx = super().get_context_data(**kwargs) ctx["section"] = Section("Edit Seen") return ctx
[docs]class SeenDeleteView(DeleteView): model = models.Seen
[docs] def get_success_url(self): return self.request.META["HTTP_REFERER"]
[docs]class Graph(TemplateView): template_name = "media/graph.html" @cached_property def data_frame(self) -> pd.DataFrame: res = pd.DataFrame.from_records( ( models.Seen.objects.all() .values("date") .annotate(num=Count("date")) .order_by("date") ), index="date", ) res = res.reindex( pd.date_range(start=res.index[0], end=datetime.datetime.now().date()), fill_value=0, ) res.insert(res.shape[1], "avg_full", res.num.rolling(res.shape[0], 1).mean()) res.insert(res.shape[1], "avg_30", res.num.rolling(31, 1).mean()) return res @cached_property def minmax_dates(self) -> pd.DatetimeIndex: "Minimum and maximum of dates in dataset." return pd.DatetimeIndex((self.data_frame.index[0], self.data_frame.index[-1])) @cached_property def _v_stack(self): return np.vstack( [self.data_frame.index.view(int), np.ones(len(self.data_frame.index))] ).T @property def data_seen(self) -> go.Scatter: return go.Scatter( x=self.data_frame.index, y=self.data_frame.num, marker={"color": "red", "symbol": 0, "size": 2}, mode="markers", name="/ day", ) @property def data_seen_lp(self) -> go.Scatter: m, c = np.linalg.lstsq(self._v_stack, self.data_frame.num, rcond=None)[0] return go.Scatter( x=self.minmax_dates, y=m * self.minmax_dates.view(int) + c, line={"color": "red", "width": 1}, mode="lines", name="/ day (linear interp.)", ) @property def data_avg_full(self) -> go.Scatter: return go.Scatter( x=self.data_frame.index, y=self.data_frame.avg_full, line={"color": "blue", "width": 1}, mode="lines", name="full ⌀", ) @property def data_avg_full_lp(self) -> go.Scatter: m, c = np.linalg.lstsq(self._v_stack, self.data_frame.avg_full, rcond=None)[0] return go.Scatter( x=self.minmax_dates, y=m * self.minmax_dates.view(int) + c, line={"color": "blue", "width": 1}, mode="lines", name="full ⌀ (linear interp.)", ) @property def data_avg_30(self) -> go.Scatter: return go.Scatter( x=self.data_frame.index, y=self.data_frame.avg_30, line={"color": "green", "width": 1}, mode="lines", # mode='markers', name="30d ⌀", ) @property def data_avg_30_lp(self) -> go.Scatter: m, c = np.linalg.lstsq(self._v_stack, self.data_frame.avg_30, rcond=None)[0] return go.Scatter( x=self.minmax_dates, y=m * self.minmax_dates.view(int) + c, line={"color": "green", "width": 1}, mode="lines", # mode='markers', name="30d ⌀ (linear interp.)", )
[docs] def get_context_data(self, **kwargs) -> dict: context = super(Graph, self).get_context_data(**kwargs) layout = go.Layout( title="Overview", xaxis={ "title": "date", "range": self.minmax_dates, "rangeselector": { "buttons": [ { "count": 2, "label": "2m", "step": "month", "stepmode": "backward", }, { "count": 6, "label": "6m", "step": "month", "stepmode": "backward", }, { "count": 1, "label": "YTD", "step": "year", "stepmode": "todate", }, { "count": 1, "label": "1y", "step": "year", "stepmode": "backward", }, {"step": "all"}, ] }, "rangeslider": { "range": self.minmax_dates, "visible": True, }, "type": "date", }, yaxis={"title": "num seen"}, legend={ "x": 0, "y": 1, "traceorder": "normal", "font": {"family": "sans-serif", "size": 12, "color": "black"}, "bgcolor": "LightSteelBlue", "bordercolor": "Black", "borderwidth": 2, }, ) figure = go.Figure( data=( self.data_seen, self.data_seen_lp, self.data_avg_full, self.data_avg_full_lp, self.data_avg_30, self.data_avg_30_lp, ), layout=layout, ) figure.update_layout() context.update( { "graph": opy.plot(figure, auto_open=False, output_type="div"), "section": Section("Graphs", container="container-fluid"), } ) return context
# Local Variables: # mode: python # compile-command: "poetry run tox" # time-stamp-pattern: "35/__date__ = \"%:y/%02m/%02d %02H:%02M:%02S %u\"" # End: