#! /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: