#132 | #121: backoffice / dev-1.0-121 (#131)

cfr #121

Co-authored-by: Christophe Siraut <d@tobald.eu.org>
Co-authored-by: bkfox <thomas bkfox net>
Co-authored-by: Thomas Kairos <thomas@bkfox.net>
Reviewed-on: rc/aircox#131
Co-authored-by: Chris Tactic <ctactic@noreply.git.radiocampus.be>
Co-committed-by: Chris Tactic <ctactic@noreply.git.radiocampus.be>
This commit is contained in:
2024-04-28 22:02:09 +02:00
committed by Thomas Kairos
parent 1e17a1334a
commit 55123c386d
348 changed files with 124397 additions and 17879 deletions

View File

@ -1,42 +1,32 @@
from . import admin, errors
from .article import ArticleDetailView, ArticleListView
from . import admin, dashboard, errors, auth
from . import article, program, episode, diffusion, log
from . import home
from .base import BaseAPIView, BaseView
from .diffusion import DiffusionListView
from .episode import EpisodeDetailView, EpisodeListView
from .home import HomeView
from .log import LogListAPIView, LogListView
from .page import (
BasePageDetailView,
BasePageListView,
PageDetailView,
PageListView,
PageUpdateView,
)
from .program import (
ProgramDetailView,
ProgramListView,
ProgramPageDetailView,
ProgramPageListView,
)
__all__ = (
"admin",
"errors",
"ArticleDetailView",
"ArticleListView",
"dashboard",
"auth",
"article",
"program",
"episode",
"diffusion",
"log",
"home",
"BaseAPIView",
"BaseView",
"DiffusionListView",
"EpisodeDetailView",
"EpisodeListView",
"HomeView",
"LogListAPIView",
"LogListView",
"BasePageDetailView",
"BasePageListView",
"PageDetailView",
"PageUpdateView",
"PageListView",
"ProgramDetailView",
"ProgramListView",
"ProgramPageDetailView",
"ProgramPageListView",
)

View File

@ -19,7 +19,7 @@ class AdminMixin(LoginRequiredMixin, UserPassesTestMixin):
return self.request.station
def test_func(self):
return self.request.user.is_staff
return self.request.user.is_admin
def get_context_data(self, **kwargs):
kwargs.update(admin.site.each_context(self.request))
@ -31,7 +31,7 @@ class AdminMixin(LoginRequiredMixin, UserPassesTestMixin):
class StatisticsView(AdminMixin, LogListView, ListView):
template_name = "admin/aircox/statistics.html"
redirect_date_url = "admin:tools-stats"
# redirect_date_url = "admin:tools-stats"
title = _("Statistics")
date = None

View File

@ -1,20 +1,15 @@
from ..models import Article, Program, StaticPage
from .page import PageDetailView, PageListView
from . import page
__all__ = ["ArticleDetailView", "ArticleListView"]
class ArticleDetailView(PageDetailView):
has_sidebar = True
class ArticleDetailView(page.PageDetailView):
model = Article
def get_sidebar_queryset(self):
qs = Article.objects.published().select_related("cover").order_by("-pub_date")
return qs
class ArticleListView(PageListView):
@page.attach
class ArticleListView(page.PageListView):
model = Article
has_headline = True
parent_model = Program
attach_to_value = StaticPage.ATTACH_TO_ARTICLES
attach_to_value = StaticPage.Target.ARTICLES

28
aircox/views/auth.py Normal file
View File

@ -0,0 +1,28 @@
from django.contrib.auth.models import User
from django.views.generic import ListView
from aircox.models import Program
class UserListView(ListView):
model = User
queryset = User.objects.all().order_by("first_name").prefetch_related("groups")
paginate_by = 100
permission_required = [
"auth.list_user",
]
template_name = "aircox/dashboard/user_list.html"
def get_users_programs(self, users):
groups = {g for u in users for g in u.groups.all()}
programs = Program.objects.filter(editors_group__in=groups)
programs = {p.editors_group_id: p for p in programs}
for user in users:
user.programs = [programs[g.id] for g in user.groups.all() if g.id in programs]
return programs
def get_context_data(self, **kwargs):
kwargs["programs"] = self.get_users_programs(self.object_list)
return super().get_context_data(**kwargs)

View File

@ -2,18 +2,14 @@ from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views.generic.base import ContextMixin, TemplateResponseMixin
from ..models import Page
__all__ = ("BaseView", "BaseAPIView")
class BaseView(TemplateResponseMixin, ContextMixin):
has_sidebar = True
"""Show side navigation."""
has_filters = False
"""Show filters nav."""
list_count = 5
"""Item count for small lists displayed on page."""
related_count = 4
related_carousel_count = 8
title = ""
@property
def station(self):
@ -22,12 +18,33 @@ class BaseView(TemplateResponseMixin, ContextMixin):
# def get_queryset(self):
# return super().get_queryset().station(self.station)
def get_sidebar_queryset(self):
"""Return a queryset of items to render on the side nav."""
return Page.objects.select_subclasses().published().order_by("-pub_date")
def get_nav_menu(self):
menu = []
for item in self.station.navitem_set.all():
try:
if item.page:
view = item.page.get_related_view()
secondary = view and view.get_secondary_nav()
else:
secondary = None
menu.append((item, secondary))
except:
import traceback
def get_sidebar_url(self):
return reverse("page-list")
traceback.print_exc()
raise
return menu
def get_secondary_nav(self):
return None
def get_related_queryset(self):
"""Return a queryset of related pages or None."""
return None
def get_related_url(self):
"""Return an url to the list of related pages."""
return None
def get_page(self):
return None
@ -35,22 +52,20 @@ class BaseView(TemplateResponseMixin, ContextMixin):
def get_context_data(self, **kwargs):
kwargs.setdefault("station", self.station)
kwargs.setdefault("page", self.get_page())
kwargs.setdefault("has_filters", self.has_filters)
has_sidebar = kwargs.setdefault("has_sidebar", self.has_sidebar)
if has_sidebar and "sidebar_object_list" not in kwargs:
sidebar_object_list = self.get_sidebar_queryset()
if sidebar_object_list is not None:
kwargs["sidebar_object_list"] = sidebar_object_list[: self.list_count]
kwargs["sidebar_list_url"] = self.get_sidebar_url()
if "audio_streams" not in kwargs:
kwargs["audio_streams"] = self.station.streams
if "model" not in kwargs:
model = getattr(self, "model", None) or hasattr(self, "object") and type(self.object)
kwargs["model"] = model
page = kwargs.get("page")
if page:
kwargs.setdefault("title", page.display_title)
kwargs.setdefault("cover", page.cover and page.cover.url)
elif self.title:
kwargs.setdefault("title", self.title)
if "nav_menu" not in kwargs:
kwargs["nav_menu"] = self.get_nav_menu()
return super().get_context_data(**kwargs)
def dispatch(self, *args, **kwargs):

57
aircox/views/dashboard.py Normal file
View File

@ -0,0 +1,57 @@
from django.db.models import Q
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.utils.translation import gettext_lazy as _
from django.views.generic.base import TemplateView
from aircox import models
from aircox.controllers.log_archiver import LogArchiver
from .base import BaseView
from .log import LogListView
__all__ = ("DashboardBaseView", "DashboardView", "StatisticsView")
class DashboardBaseView(LoginRequiredMixin, UserPassesTestMixin, BaseView):
title = _("Dashboard")
def test_func(self):
user = self.request.user
return user.is_staff or user.is_superuser
class DashboardView(DashboardBaseView, TemplateView):
template_name = "aircox/dashboard/dashboard.html"
def get_context_data(self, **kwargs):
programs = models.Program.objects.editor(self.request.user)
comments = models.Comment.objects.filter(
Q(page__in=programs) | Q(page__episode__parent__in=programs) | Q(page__article__parent__in=programs)
)
kwargs.update(
{
"subtitle": self.request.user.get_username(),
"programs": programs.order_by("title"),
"comments": comments.order_by("-date"),
"next_diffs": models.Diffusion.objects.editor(self.request.user)
.select_related("episode")
.after()
.order_by("start"),
}
)
return super().get_context_data(**kwargs)
class StatisticsView(DashboardBaseView, LogListView):
template_name = "aircox/dashboard/statistics.html"
date = None
# redirect_date_url = "dashboard-statistics"
# TOOD: test_func & perms check
def get_object_list(self, logs, full=False):
if not logs.exists():
logs = LogArchiver().load(self.station, self.date) if self.date else []
objs = super().get_object_list(logs, True)
return objs

View File

@ -1,30 +1,57 @@
import datetime
from django.urls import reverse
from django.views.generic import ListView
from aircox.models import Diffusion, StaticPage
from aircox.models import Diffusion, Log, StaticPage
from .base import BaseView
from .mixins import AttachedToMixin, GetDateMixin
from .page import attach
__all__ = ("DiffusionListView",)
__all__ = ("DiffusionListView", "TimeTableView")
class DiffusionListView(GetDateMixin, AttachedToMixin, BaseView, ListView):
class BaseDiffusionListView(AttachedToMixin, BaseView, ListView):
model = Diffusion
queryset = Diffusion.objects.on_air().order_by("-start")
class DiffusionListView(BaseDiffusionListView):
"""View for timetables."""
model = Diffusion
has_filters = True
redirect_date_url = "diffusion-list"
attach_to_value = StaticPage.ATTACH_TO_DIFFUSIONS
@attach
class TimeTableView(GetDateMixin, BaseDiffusionListView):
model = Diffusion
redirect_date_url = "timetable-list"
attach_to_value = StaticPage.Target.TIMETABLE
template_name = "aircox/timetable_list.html"
def get_date(self):
date = super().get_date()
return date if date is not None else datetime.date.today()
def get_queryset(self):
return super().get_queryset().date(self.date).order_by("start")
def get_logs(self, date):
return Log.objects.on_air().date(self.date).filter(track__isnull=False)
def get_context_data(self, **kwargs):
def get_queryset(self):
return super().get_queryset().date(self.date)
@classmethod
def get_secondary_nav(cls):
date = datetime.date.today()
start = date - datetime.timedelta(days=date.weekday())
dates = [start + datetime.timedelta(days=i) for i in range(0, 7)]
return tuple((date.strftime("%A %d"), reverse("timetable-list", kwargs={"date": date})) for date in dates)
def get_context_data(self, object_list=None, **kwargs):
start = self.date - datetime.timedelta(days=self.date.weekday())
dates = [start + datetime.timedelta(days=i) for i in range(0, 7)]
return super().get_context_data(date=self.date, dates=dates, **kwargs)
if object_list is None:
logs = self.get_logs(self.date)
object_list = Log.merge_diffusions(logs, self.object_list, group_logs=True)
object_list = list(reversed(object_list))
return super().get_context_data(date=self.date, dates=dates, object_list=object_list, **kwargs)

View File

@ -1,27 +1,138 @@
from ..filters import EpisodeFilters
from ..models import Episode, Program, StaticPage
from .page import PageListView
from .program import ProgramPageDetailView
from django.contrib.auth.mixins import UserPassesTestMixin
from django.urls import reverse
from aircox.models import Episode, Program, StaticPage, Track
from aircox import forms, filters, permissions
from .mixins import VueFormDataMixin
from .page import attach, PageDetailView, PageListView, PageUpdateView
__all__ = (
"EpisodeDetailView",
"EpisodeListView",
"PodcastListView",
"EpisodeUpdateView",
)
class EpisodeDetailView(ProgramPageDetailView):
class EpisodeDetailView(PageDetailView):
model = Episode
def can_edit(self, obj):
return permissions.program.can(self.request.user, "update", obj)
def get_context_data(self, **kwargs):
if "tracks" not in kwargs:
kwargs["tracks"] = self.object.track_set.order_by("position")
return super().get_context_data(**kwargs)
def get_related_queryset(self):
return (
self.get_queryset().parent(self.object.parent).exclude(pk=self.object.pk).published().order_by("-pub_date")
)
def get_related_url(self):
return reverse("episode-list", kwargs={"parent_slug": self.object.parent.slug})
@attach
class EpisodeListView(PageListView):
model = Episode
filterset_class = EpisodeFilters
item_template_name = "aircox/widgets/episode_item.html"
has_headline = True
filterset_class = filters.EpisodeFilters
parent_model = Program
attach_to_value = StaticPage.ATTACH_TO_EPISODES
attach_to_value = StaticPage.Target.EPISODES
@attach
class PodcastListView(EpisodeListView):
attach_to_value = StaticPage.Target.PODCASTS
queryset = Episode.objects.published().with_podcasts().order_by("-pub_date")
class EpisodeUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
model = Episode
form_class = forms.EpisodeForm
template_name = "aircox/episode_form.html"
def test_func(self):
obj = self.get_object()
return permissions.program.can(self.request.user, "update", obj)
def get_tracklist_queryset(self, episode):
return Track.objects.filter(episode=episode).order_by("position")
def get_tracklist_formset(self, episode, **kwargs):
kwargs.update(
{
"prefix": "tracks",
"queryset": self.get_tracklist_queryset(episode),
"initial": [
{
"episode": episode.id,
}
],
}
)
return forms.TrackFormSet(**kwargs)
def get_soundlist_queryset(self, episode):
return episode.episodesound_set.all().select_related("sound").order_by("position")
def get_soundlist_formset(self, episode, **kwargs):
kwargs.update(
{
"prefix": "sounds",
"queryset": self.get_soundlist_queryset(episode),
"initial": [
{
"episode": episode.id,
}
],
}
)
return forms.EpisodeSoundFormSet(**kwargs)
def get_sound_form(self, episode, **kwargs):
kwargs.update(
{
"initial": {
"program": episode.parent_id,
"name": episode.title,
"is_public": True,
},
}
)
return forms.SoundCreateForm(**kwargs)
def get_context_data(self, **kwargs):
forms = (
("soundlist_formset", self.get_soundlist_formset),
("tracklist_formset", self.get_tracklist_formset),
("sound_form", self.get_sound_form),
)
for key, func in forms:
if key not in kwargs:
kwargs[key] = func(self.object)
for key in ("soundlist_formset", "tracklist_formset"):
formset = kwargs[key]
kwargs[f"{key}_data"] = self.get_formset_data(formset, {"episode": self.object.id})
return super().get_context_data(**kwargs)
def post(self, request, *args, **kwargs):
resp = super().post(request, *args, **kwargs)
formsets = {
"soundlist_formset": self.get_soundlist_formset(self.object, data=request.POST),
"tracklist_formset": self.get_tracklist_formset(self.object, data=request.POST),
}
invalid = False
for formset in formsets.values():
if not formset.is_valid():
invalid = True
else:
formset.save()
if invalid:
return self.get(request, **formsets)
return resp

View File

@ -1,57 +1,84 @@
from datetime import date
from datetime import date, datetime, timedelta
from django.utils import timezone as tz
from django.views.generic import ListView
from ..models import Diffusion, Log, Page, StaticPage
from ..models import Diffusion, Episode, Log, Page, StaticPage
from .base import BaseView
from .mixins import AttachedToMixin
from .page import attach
class HomeView(BaseView, ListView):
@attach
class HomeView(AttachedToMixin, BaseView, ListView):
template_name = "aircox/home.html"
attach_to_value = StaticPage.Target.HOME
model = Diffusion
attach_to_value = StaticPage.ATTACH_TO_HOME
queryset = Diffusion.objects.on_air().select_related("episode")
logs_count = 5
publications_count = 5
has_filters = False
queryset = Diffusion.objects.on_air().select_related("episode").order_by("-start")
publications_queryset = Page.objects.select_subclasses().published().order_by("-pub_date")
podcasts_queryset = Episode.objects.published().with_podcasts().order_by("-pub_date")
def get_queryset(self):
return super().get_queryset().date(date.today())
now = datetime.now()
return super().get_queryset().after(now - timedelta(hours=24)).before(now).order_by("-start")
def get_logs(self, diffusions):
today = date.today()
logs = Log.objects.on_air().date(today).filter(track__isnull=False)
now = datetime.now()
# diffs = Diffusion.objects.on_air().date(today)
return Log.merge_diffusions(logs, diffusions, self.logs_count)
object_list = self.object_list
diffs = list(object_list[: self.related_count])
logs = Log.objects.on_air().filter(track__isnull=False, date__lte=now)
if diffs:
min_date = diffs[-1].start - timedelta(hours=1)
logs = logs.after(min_date)
else:
logs = logs.date(today)
return Log.merge_diffusions(
logs, object_list, diff_count=self.related_count, count=self.related_count + 2, group_logs=True
)
def get_next_diffs(self):
now = tz.now()
current_diff = Diffusion.objects.on_air().now(now).first()
next_diffs = Diffusion.objects.on_air().after(now)
query = Diffusion.objects.on_air().select_related("episode")
current_diff = query.now(now).first()
next_diffs = query.after(now)
if current_diff:
diffs = [current_diff] + list(next_diffs.exclude(pk=current_diff.pk)[:2])
diffs = [current_diff] + list(next_diffs.exclude(pk=current_diff.pk)[:9])
else:
diffs = next_diffs[:3]
diffs = next_diffs[: self.related_carousel_count]
return diffs
def get_last_publications(self):
def get_publications(self):
# note: with postgres db, possible to use distinct()
qs = Page.objects.select_subclasses().published().order_by("-pub_date")
qs = self.publications_queryset.all()
parents = set()
items = []
for publication in qs:
parent_id = publication.parent_id
parent_id = getattr(publication, "parent_id", None)
if parent_id is not None and parent_id in parents:
continue
items.append(publication)
if len(items) == self.publications_count:
if len(items) == self.related_count:
break
return items
def get_podcasts(self):
return self.podcasts_queryset.all()[: self.related_carousel_count]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["logs"] = self.get_logs(context["object_list"])
context["next_diffs"] = self.get_next_diffs()
context["last_publications"] = self.get_last_publications()[:5]
return context
next_diffs = self.get_next_diffs()
current_diff = next_diffs and next_diffs[0]
kwargs.update(
{
"object": current_diff.episode,
"diffusion": current_diff,
"logs": self.get_logs(self.object_list),
"next_diffs": next_diffs,
"publications": self.get_publications(),
"podcasts": self.get_podcasts(),
}
)
return super().get_context_data(**kwargs)

View File

@ -6,45 +6,43 @@ from django.views.decorators.cache import cache_page
from django.views.generic import ListView
from rest_framework.generics import ListAPIView
from ..models import Diffusion, Log, StaticPage
from ..models import Diffusion, Log
from ..serializers import LogInfo, LogInfoSerializer
from .base import BaseAPIView, BaseView
from .mixins import AttachedToMixin, GetDateMixin
__all__ = ["LogListMixin", "LogListView"]
__all__ = ("LogListMixin", "LogListView", "LogListAPIView")
class LogListMixin(GetDateMixin):
model = Log
min_date = None
max_date = None
def get_date(self):
date = super().get_date()
def get_date(self, param):
date = super().get_date(param)
if date is not None and not self.request.user.is_staff:
return min(date, datetime.date.today())
return date
def filter_qs(self, query):
if self.min_date:
query = query.after(self.min_date)
if self.max_date:
query = query.before(self.max_date)
if not self.min_date and not self.max_date and self.date:
return query.date(self.date)
return query
def get_queryset(self):
# only get logs for tracks: log for diffusion will be retrieved
# by the diffusions' queryset.
qs = super().get_queryset().on_air().filter(track__isnull=False).filter(date__lte=tz.now())
return (
qs.date(self.date)
if self.date is not None
else qs.after(self.min_date)
if self.min_date is not None
else qs
)
query = super().get_queryset().on_air().filter(track__isnull=False).filter(date__lte=tz.now())
return self.filter_qs(query)
def get_diffusions_queryset(self):
qs = Diffusion.objects.station(self.station).on_air().filter(start__lte=tz.now())
return (
qs.date(self.date)
if self.date is not None
else qs.after(self.min_date)
if self.min_date is not None
else qs
)
query = Diffusion.objects.station(self.station).on_air().filter(start__lte=tz.now()).before()
return self.filter_qs(query)
def get_object_list(self, logs, full=False):
"""Return diffusions merged to the provided logs iterable.
@ -62,13 +60,31 @@ class LogListView(AttachedToMixin, BaseView, LogListMixin, ListView):
`request.GET`, defaults to today)."""
redirect_date_url = "log-list"
has_filters = True
attach_to_value = StaticPage.ATTACH_TO_LOGS
date_delta = tz.timedelta(days=7)
def get_date(self):
date = super().get_date()
def get_date(self, param):
date = super().get_date(param)
return datetime.date.today() if date is None else date
def get(self, request, **kwargs):
min_date = self.get_date("min_date")
max_date = self.get_date("max_date")
# ensure right values for min and max
min_date, max_date = min(min_date, max_date), max(min_date, max_date)
# limit logs list size using date delta
if min_date and max_date:
max_date = min(min_date + self.date_delta, max_date)
elif min_date:
max_date = min_date + self.date_delta
elif max_date:
min_date = max_date - self.date_delta
self.min_date = min_date
self.max_date = max_date
return super().get(request, **kwargs)
def get_context_data(self, **kwargs):
today = datetime.date.today()
# `super()...` must be called before updating kwargs, in order
@ -76,6 +92,9 @@ class LogListView(AttachedToMixin, BaseView, LogListMixin, ListView):
kwargs = super().get_context_data(**kwargs)
kwargs.update(
{
"min_date": self.min_date or today,
"max_date": self.max_date or today,
"today": datetime.date.today(),
"date": self.date,
"dates": (today - datetime.timedelta(days=i) for i in range(0, 7)),
"object_list": self.get_object_list(self.object_list),
@ -101,8 +120,8 @@ class LogListAPIView(LogListMixin, BaseAPIView, ListAPIView):
def list(self, *args, **kwargs):
return super().list(*args, **kwargs)
def get_date(self):
date = super().get_date()
def get_date(self, param):
date = super().get_date(param)
if date is None:
self.min_date = tz.now() - tz.timedelta(minutes=30)
return date

View File

@ -12,9 +12,9 @@ class GetDateMixin:
date = None
redirect_date_url = None
def get_date(self):
date = self.request.GET.get("date")
return str_to_date(date, "-") if date is not None else self.kwargs["date"] if "date" in self.kwargs else None
def get_date(self, param="date"):
date = self.request.GET.get(param)
return str_to_date(date, "-") if date else self.kwargs[param] if param in self.kwargs else None
def get(self, *args, **kwargs):
if self.redirect_date_url and self.request.GET.get("date"):
@ -23,7 +23,7 @@ class GetDateMixin:
date=self.request.GET["date"].replace("-", "/"),
)
self.date = self.get_date()
self.date = self.get_date("date")
return super().get(*args, **kwargs)
@ -44,16 +44,16 @@ class ParentMixin:
parent = None
"""Parent page object."""
def get_parent(self, request, *args, **kwargs):
def get_parent(self, request, **kwargs):
if self.parent_model is None or self.parent_url_kwarg not in kwargs:
return
lookup = {self.parent_field: kwargs[self.parent_url_kwarg]}
return get_object_or_404(self.parent_model.objects.select_related("cover"), **lookup)
def get(self, request, *args, **kwargs):
self.parent = self.get_parent(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
def dispatch(self, request, *args, **kwargs):
self.parent = self.get_parent(request, **kwargs)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
if self.parent is not None:
@ -61,9 +61,10 @@ class ParentMixin:
return super().get_queryset()
def get_context_data(self, **kwargs):
self.parent = kwargs.setdefault("parent", self.parent)
if self.parent is not None:
kwargs.setdefault("cover", self.parent.cover)
parent = kwargs.setdefault("parent", self.parent)
if parent is not None and parent.cover:
kwargs.setdefault("cover", parent.cover.url)
return super().get_context_data(**kwargs)
@ -107,3 +108,42 @@ class FiltersMixin:
params = self.request.GET.copy()
kwargs["get_params"] = params.pop("page", True) and params
return super().get_context_data(**kwargs)
class VueFormDataMixin:
"""Provide form information as data to be used with vue components."""
# Note: values corresponds to AFormSet expected one
def get_form_items(self, formset):
return [form.initial for form in formset.forms]
def get_form_field_data(self, form, values=None):
"""Return form fields as data."""
model = form.Meta.model
fields = ((name, field, model._meta.get_field(name)) for name, field in form.base_fields.items())
return [
{
"name": name,
"label": str(m_field.verbose_name).capitalize(),
"help": str(m_field.help_text).capitalize(),
"hidden": field.widget.is_hidden,
"value": values and values.get(name),
}
for name, field, m_field in fields
]
def get_formset_data(self, formset, field_values=None, **kwargs):
"""Return formset as data object."""
return {
"prefix": formset.prefix,
"management": {
"initial_forms": formset.initial_form_count(),
"min_num_forms": formset.min_num,
"max_num_forms": formset.max_num,
},
"fields": self.get_form_field_data(formset.form, field_values),
"initial_extra": formset.initial_extra and formset.initial_extra[0],
"initials": self.get_form_items(formset),
**kwargs,
}

View File

@ -1,72 +1,119 @@
from django.http import Http404, HttpResponse
from django.http import HttpResponse
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse
from honeypot.decorators import check_honeypot
from aircox.conf import settings
from ..filters import PageFilters
from ..forms import CommentForm
from ..models import Comment
from ..utils import Redirect
from ..models import Comment, Category
from .base import BaseView
from .mixins import AttachedToMixin, FiltersMixin, ParentMixin
__all__ = [
"attached_views",
"attach",
"BasePageListView",
"BasePageDetailView",
"PageDetailView",
"PageListView",
"PageUpdateView",
]
class BasePageListView(AttachedToMixin, ParentMixin, BaseView, ListView):
attached_views = {}
"""Register views by StaticPage.Target."""
def attach(cls):
"""Add decorated view class to `attached_views`"""
attached_views[cls.attach_to_value] = cls
return cls
class BasePageMixin:
category = None
def get_queryset(self):
qs = super().get_queryset().select_subclasses().select_related("cover")
if self.request.user.is_authenticated:
return qs
return qs.published()
def get_category(self, page, **kwargs):
if page:
if getattr(page, "category_id", None):
return page.category
if getattr(page, "parent_id", None):
return self.get_category(page.parent_subclass)
if slug := self.kwargs.get("category_slug"):
return Category.objects.get(slug=slug)
return None
def get_context_data(self, *args, **kwargs):
kwargs.setdefault("category", self.category)
return super().get_context_data(*args, **kwargs)
class BasePageListView(AttachedToMixin, BasePageMixin, ParentMixin, BaseView, ListView):
"""Base view class for BasePage list."""
template_name = "aircox/basepage_list.html"
item_template_name = "aircox/widgets/page_item.html"
has_sidebar = True
paginate_by = 30
has_headline = True
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def get(self, *args, **kwargs):
self.category = self.get_category(self.parent)
return super().get(*args, **kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses().published().select_related("cover")
query = super().get_queryset()
if self.category:
query = query.filter(category=self.category)
return query
def get_context_data(self, **kwargs):
kwargs.setdefault("item_template_name", self.item_template_name)
kwargs.setdefault("has_headline", self.has_headline)
return super().get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
parent = context.get("parent")
if not context.get("page"):
if not context.get("title"):
model = self.model._meta.verbose_name_plural
title = _("{model}")
context["title"] = title.format(model=model, parent=parent)
if not context.get("cover") and parent and parent.cover:
context["cover"] = parent.cover.url
return context
class BasePageDetailView(BaseView, DetailView):
class BasePageDetailView(BasePageMixin, BaseView, DetailView):
"""Base view class for BasePage."""
template_name = "aircox/basepage_detail.html"
template_name = "aircox/public.html"
context_object_name = "page"
has_filters = False
def get_queryset(self):
return super().get_queryset().select_related("cover")
# This should not exists: it allows mapping not published pages
# or it should be only used for trashed pages.
def not_published_redirect(self, page):
"""When a page is not published, redirect to the returned url instead
of an HTTP 404 code."""
return None
def get_context_data(self, **kwargs):
if self.object.cover:
kwargs.setdefault("cover", self.object.cover.url)
if self.object.title:
kwargs.setdefault("title", self.object.display_title)
return super().get_context_data(**kwargs)
def get_object(self):
if getattr(self, "object", None):
return self.object
obj = super().get_object()
if not obj.is_published:
redirect_url = self.not_published_redirect(obj)
if redirect_url:
raise Redirect(redirect_url)
raise Http404("%s not found" % self.model._meta.verbose_name)
self.category = self.get_category(obj)
return obj
def get_page(self):
@ -78,7 +125,6 @@ class PageListView(FiltersMixin, BasePageListView):
filterset_class = PageFilters
template_name = None
has_filters = True
categories = None
filters = None
@ -92,15 +138,21 @@ class PageListView(FiltersMixin, BasePageListView):
def get_queryset(self):
qs = super().get_queryset().select_related("category").order_by("-pub_date")
cat_ids = self.model.objects.published().values_list("category_id", flat=True)
self.categories = Category.objects.filter(id__in=cat_ids)
return qs
def get_context_data(self, **kwargs):
kwargs["categories"] = (
self.model.objects.published()
.filter(category__isnull=False)
.values_list("category__title", "category__id")
.distinct()
@classmethod
def get_secondary_nav(cls):
cat_ids = cls.model.objects.published().values_list("category_id", flat=True)
categories = Category.objects.filter(id__in=cat_ids)
return tuple(
(category.title, reverse(cls.model.list_url_name, kwargs={"category_slug": category.slug}))
for category in categories
)
def get_context_data(self, **kwargs):
kwargs["categories"] = self.categories
return super().get_context_data(**kwargs)
@ -109,7 +161,10 @@ class PageDetailView(BasePageDetailView):
template_name = None
context_object_name = "page"
has_filters = False
def can_edit(self, object):
"""Return True if user can edit current page."""
return False
def get_template_names(self):
return super().get_template_names() + ["aircox/page_detail.html"]
@ -118,11 +173,26 @@ class PageDetailView(BasePageDetailView):
return super().get_queryset().select_related("category")
def get_context_data(self, **kwargs):
if self.object.allow_comments and "comment_form" not in kwargs:
kwargs["comment_form"] = CommentForm()
if "comment_form" not in kwargs:
kwargs["comment_form"] = self.get_comment_form()
kwargs["comments"] = Comment.objects.filter(page=self.object).order_by("-date")
if parent_subclass := getattr(self.object, "parent_subclass", None):
kwargs["parent"] = parent_subclass
if "related_objects" not in kwargs:
related = self.get_related_queryset()
if related:
related = related[: self.related_count]
kwargs["related_objects"] = related
kwargs["can_edit"] = self.can_edit(self.object)
return super().get_context_data(**kwargs)
def get_comment_form(self):
if settings.ALLOW_COMMENTS and self.object.allow_comments:
return CommentForm()
return None
@classmethod
def as_view(cls, *args, **kwargs):
view = super(PageDetailView, cls).as_view(*args, **kwargs)
@ -138,3 +208,19 @@ class PageDetailView(BasePageDetailView):
comment.page = self.object
comment.save()
return self.get(request, *args, **kwargs)
class PageCreateView(BaseView, CreateView):
def get_page(self):
return self.object
def get_success_url(self):
return self.request.path
class PageUpdateView(BaseView, UpdateView):
def get_page(self):
return self.object
def get_success_url(self):
return self.request.path

View File

@ -1,60 +1,76 @@
import random
from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin
from django.urls import reverse
from ..models import Page, Program, StaticPage
from .mixins import ParentMixin
from .page import PageDetailView, PageListView
__all__ = ["ProgramPageDetailView", "ProgramDetailView", "ProgramPageListView"]
from aircox import models, forms, permissions
from . import page
from .mixins import VueFormDataMixin
class BaseProgramMixin:
def get_program(self):
return self.object
__all__ = (
"ProgramDetailView",
"ProgramDetailView",
"ProgramCreateView",
"ProgramUpdateView",
)
def get_sidebar_url(self):
return reverse("program-page-list", kwargs={"parent_slug": self.program.slug})
class ProgramDetailView(page.PageDetailView):
model = models.Program
def can_edit(self, obj):
return permissions.program.can(self.request.user, "update", obj)
def get_related_queryset(self):
queryset = (
self.get_queryset()
.filter(category_id=self.object.category_id)
.exclude(pk=self.object.pk)
.published()
.order_by("-pub_date")[:50]
)
return random.sample(list(queryset), min(len(queryset), self.related_count))
def get_related_url(self):
return reverse("program-list") + f"?category__id={self.object.category_id}"
def get_context_data(self, **kwargs):
self.program = self.get_program()
kwargs["program"] = self.program
return super().get_context_data(**kwargs)
episodes = models.Episode.objects.program(self.object).published().order_by("-pub_date")
podcasts = episodes.with_podcasts()
articles = models.Article.objects.parent(self.object).published().order_by("-pub_date")
return super().get_context_data(
articles=articles[: self.related_count],
episodes=episodes[: self.related_count],
podcasts=podcasts[: self.related_count],
**kwargs,
)
def get_template_names(self):
return super().get_template_names() + ["aircox/program_detail.html"]
class ProgramDetailView(BaseProgramMixin, PageDetailView):
model = Program
@page.attach
class ProgramListView(page.PageListView):
model = models.Program
attach_to_value = models.StaticPage.Target.PROGRAMS
def get_sidebar_queryset(self):
return super().get_sidebar_queryset().filter(parent=self.program)
def get_queryset(self):
return super().get_queryset().order_by("title")
class ProgramListView(PageListView):
model = Program
attach_to_value = StaticPage.ATTACH_TO_PROGRAMS
class ProgramEditMixin(VueFormDataMixin):
model = models.Program
form_class = forms.ProgramForm
queryset = models.Program.objects.select_related("editors_group")
# FIXME: not used
class ProgramPageDetailView(BaseProgramMixin, ParentMixin, PageDetailView):
"""Base view class for a page that is displayed as a program's child
page."""
parent_model = Program
def get_program(self):
self.parent = self.object.program
return self.object.program
def get_sidebar_queryset(self):
return super().get_sidebar_queryset().filter(parent=self.program)
# FIXME: not used as long there is no complete administration mgt (schedule, etc.)
class ProgramCreateView(PermissionRequiredMixin, ProgramEditMixin, page.PageCreateView):
permission_required = "aircox.add_program"
class ProgramPageListView(BaseProgramMixin, PageListView):
model = Page
parent_model = Program
queryset = Page.objects.select_subclasses()
def get_program(self):
return self.parent
def get_context_data(self, **kwargs):
kwargs.setdefault("sidebar_url_parent", None)
return super().get_context_data(**kwargs)
class ProgramUpdateView(UserPassesTestMixin, ProgramEditMixin, page.PageUpdateView):
def test_func(self):
obj = self.get_object()
return permissions.program.can(self.request.user, "update", obj)