forked from rc/aircox
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:
@ -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",
|
||||
)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
28
aircox/views/auth.py
Normal 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)
|
@ -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
57
aircox/views/dashboard.py
Normal 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
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user