add missing files; improve dashboard; rewrite urls
This commit is contained in:
parent
a24318bc84
commit
1bd4e03f02
|
@ -3,7 +3,7 @@ from django.contrib import admin
|
|||
from django.forms import ModelForm
|
||||
|
||||
from aircox.models import Episode, EpisodeSound
|
||||
from .page import PageAdmin
|
||||
from .page import ChildPageAdmin
|
||||
from .sound import TrackInline
|
||||
from .diffusion import DiffusionInline
|
||||
|
||||
|
@ -15,14 +15,14 @@ class EpisodeAdminForm(ModelForm):
|
|||
|
||||
|
||||
@admin.register(Episode)
|
||||
class EpisodeAdmin(SortableAdminBase, PageAdmin):
|
||||
class EpisodeAdmin(SortableAdminBase, ChildPageAdmin):
|
||||
form = EpisodeAdminForm
|
||||
list_display = PageAdmin.list_display
|
||||
list_filter = tuple(f for f in PageAdmin.list_filter if f != "pub_date") + (
|
||||
list_display = ChildPageAdmin.list_display
|
||||
list_filter = tuple(f for f in ChildPageAdmin.list_filter if f != "pub_date") + (
|
||||
"diffusion__start",
|
||||
"pub_date",
|
||||
)
|
||||
search_fields = PageAdmin.search_fields + ("parent__title",)
|
||||
search_fields = ChildPageAdmin.search_fields + ("parent__title",)
|
||||
# readonly_fields = ('parent',)
|
||||
|
||||
inlines = [TrackInline, DiffusionInline]
|
||||
|
|
|
@ -21,7 +21,7 @@ class CategoryAdmin(admin.ModelAdmin):
|
|||
|
||||
|
||||
class BasePageAdmin(admin.ModelAdmin):
|
||||
list_display = ("cover_thumb", "title", "status", "parent")
|
||||
list_display = ("cover_thumb", "title", "status")
|
||||
list_display_links = ("cover_thumb", "title")
|
||||
list_editable = ("status",)
|
||||
list_filter = ("status",)
|
||||
|
@ -42,7 +42,9 @@ class BasePageAdmin(admin.ModelAdmin):
|
|||
(
|
||||
_("Publication Settings"),
|
||||
{
|
||||
"fields": ["status", "parent"],
|
||||
"fields": [
|
||||
"status",
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -54,23 +56,6 @@ class BasePageAdmin(admin.ModelAdmin):
|
|||
return mark_safe('<img src="{}"/>'.format(obj.cover.icons["64"]))
|
||||
return ""
|
||||
|
||||
def get_changeform_initial_data(self, request):
|
||||
data = super().get_changeform_initial_data(request)
|
||||
filters = QueryDict(request.GET.get("_changelist_filters", ""))
|
||||
data["parent"] = filters.get("parent", None)
|
||||
return data
|
||||
|
||||
def _get_common_context(self, query, extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
parent = query.get("parent", None)
|
||||
extra_context["parent"] = None if parent is None else Page.objects.get_subclass(id=parent)
|
||||
return extra_context
|
||||
|
||||
def render_change_form(self, request, context, *args, **kwargs):
|
||||
if context["original"] and "parent" not in context:
|
||||
context["parent"] = context["original"].parent
|
||||
return super().render_change_form(request, context, *args, **kwargs)
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
filters = QueryDict(request.GET.get("_changelist_filters", ""))
|
||||
extra_context = self._get_common_context(filters, extra_context)
|
||||
|
@ -88,12 +73,36 @@ class PageAdmin(BasePageAdmin):
|
|||
list_editable = BasePageAdmin.list_editable + ("category",)
|
||||
list_filter = BasePageAdmin.list_filter + ("category", "pub_date")
|
||||
search_fields = BasePageAdmin.search_fields + ("category__title",)
|
||||
fieldsets = deepcopy(BasePageAdmin.fieldsets)
|
||||
|
||||
fieldsets = deepcopy(BasePageAdmin.fieldsets)
|
||||
fieldsets[0][1]["fields"].insert(fieldsets[0][1]["fields"].index("slug") + 1, "category")
|
||||
fieldsets[1][1]["fields"] += ("featured", "allow_comments")
|
||||
|
||||
|
||||
class ChildPageAdmin(PageAdmin):
|
||||
list_display = PageAdmin.list_display + ("parent",)
|
||||
|
||||
fieldsets = deepcopy(PageAdmin.fieldsets)
|
||||
fieldsets[1][1]["fields"] += ("parent",)
|
||||
|
||||
def get_changeform_initial_data(self, request):
|
||||
data = super().get_changeform_initial_data(request)
|
||||
filters = QueryDict(request.GET.get("_changelist_filters", ""))
|
||||
data["parent"] = filters.get("parent", None)
|
||||
return data
|
||||
|
||||
def _get_common_context(self, query, extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
parent = query.get("parent", None)
|
||||
extra_context["parent"] = None if parent is None else Page.objects.get_subclass(id=parent)
|
||||
return extra_context
|
||||
|
||||
def render_change_form(self, request, context, *args, **kwargs):
|
||||
if context["original"] and "parent" not in context:
|
||||
context["parent"] = context["original"].parent
|
||||
return super().render_change_form(request, context, *args, **kwargs)
|
||||
|
||||
|
||||
@admin.register(StaticPage)
|
||||
class StaticPageAdmin(BasePageAdmin):
|
||||
list_display = BasePageAdmin.list_display + ("attach_to",)
|
||||
|
|
|
@ -27,6 +27,12 @@ class ImageForm(forms.Form):
|
|||
|
||||
|
||||
class PageForm(forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ("title", "category", "status", "cover", "content")
|
||||
model = models.Page
|
||||
|
||||
|
||||
class ChildPageForm(forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ("title", "status", "cover", "content")
|
||||
model = models.Page
|
||||
|
@ -41,7 +47,7 @@ class ProgramForm(PageForm):
|
|||
class EpisodeForm(PageForm):
|
||||
class Meta:
|
||||
model = models.Episode
|
||||
fields = PageForm.Meta.fields
|
||||
fields = ChildPageForm.Meta.fields
|
||||
|
||||
|
||||
class SoundForm(forms.ModelForm):
|
||||
|
|
|
@ -13,7 +13,7 @@ class Migration(migrations.Migration):
|
|||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="program",
|
||||
name="editors",
|
||||
name="editors_group",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
from django.db import migrations
|
||||
|
||||
from aircox.models import Program
|
||||
|
||||
|
||||
def set_group_ownership(*args):
|
||||
for program in Program.objects.all():
|
||||
program.set_group_ownership()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0021_alter_schedule_timezone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_group_ownership),
|
||||
]
|
|
@ -6,7 +6,8 @@ from django.db import migrations, models
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0022_set_group_ownership"),
|
||||
("filer", "0017_image__transparent"),
|
||||
("aircox", "0021_alter_schedule_timezone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
# Generated by Django 5.0 on 2024-04-10 08:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
children_infos = {}
|
||||
|
||||
|
||||
def get_children_infos(apps, schema_editor):
|
||||
Page = apps.get_model("aircox", "page")
|
||||
query = Page.objects.filter(parent__isnull=False).values("pk", "parent_id", "category_id", "parent__category_id")
|
||||
children_infos.update((r["pk"], r) for r in query)
|
||||
|
||||
|
||||
def restore_children_infos(apps, schema_editor):
|
||||
Episode = apps.get_model("aircox", "Episode")
|
||||
|
||||
pks = set(children_infos.keys())
|
||||
eps = _restore_for_objs(Episode.objects.filter(pk__in=pks))
|
||||
Episode.objects.bulk_update(eps, ("parent_id", "category_id"))
|
||||
print(f">> {len(eps)} episodes restored")
|
||||
|
||||
|
||||
def _restore_for_objs(objs):
|
||||
updated = []
|
||||
for obj in objs:
|
||||
info = children_infos.get(obj.pk)
|
||||
if info:
|
||||
obj.parent_id = info["parent_id"]
|
||||
obj.category_id = info["category_id"] or info["parent__category_id"]
|
||||
updated.append(obj)
|
||||
return updated
|
||||
|
||||
|
||||
def set_group_ownership(*args):
|
||||
for program in Program.objects.all():
|
||||
program.set_group_ownership()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0026_alter_sound_options_remove_sound_episode_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(get_children_infos),
|
||||
migrations.RemoveField(
|
||||
model_name="page",
|
||||
name="parent",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="staticpage",
|
||||
name="parent",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="station",
|
||||
name="path",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="article",
|
||||
name="parent",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="%(class)s_set",
|
||||
to="aircox.page",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="episode",
|
||||
name="parent",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="%(class)s_set",
|
||||
to="aircox.page",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(restore_children_infos),
|
||||
]
|
|
@ -1,12 +1,12 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .page import Page
|
||||
from .page import ChildPage
|
||||
from .program import ProgramChildQuerySet
|
||||
|
||||
__all__ = ("Article",)
|
||||
|
||||
|
||||
class Article(Page):
|
||||
class Article(ChildPage):
|
||||
detail_url_name = "article-detail"
|
||||
template_prefix = "article"
|
||||
|
||||
|
|
|
@ -17,6 +17,10 @@ __all__ = ("Diffusion", "DiffusionQuerySet")
|
|||
|
||||
|
||||
class DiffusionQuerySet(RerunQuerySet):
|
||||
def editor(self, user):
|
||||
episodes = Episode.objects.editor(user)
|
||||
return self.filter(episode__in=episodes)
|
||||
|
||||
def episode(self, episode=None, id=None):
|
||||
"""Diffusions for this episode."""
|
||||
return self.filter(episode=episode) if id is None else self.filter(episode__id=id)
|
||||
|
@ -200,7 +204,7 @@ class Diffusion(Rerun):
|
|||
@property
|
||||
def is_live(self):
|
||||
"""True if Diffusion is live (False if there are sounds files)."""
|
||||
return self.type == self.TYPE_ON_AIR and self.episode.episodesound_set.all().broadcast().empty()
|
||||
return self.type == self.TYPE_ON_AIR and not self.episode.episodesound_set.all().broadcast()
|
||||
|
||||
def is_date_in_range(self, date=None):
|
||||
"""Return true if the given date is in the diffusion's start-end
|
||||
|
|
|
@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from aircox.conf import settings
|
||||
|
||||
from .page import Page
|
||||
from .page import ChildPage
|
||||
from .program import ProgramChildQuerySet
|
||||
from .sound import Sound
|
||||
|
||||
|
@ -19,10 +19,11 @@ class EpisodeQuerySet(ProgramChildQuerySet):
|
|||
return self.filter(episodesound__sound__is_public=True).distinct()
|
||||
|
||||
|
||||
class Episode(Page):
|
||||
class Episode(ChildPage):
|
||||
objects = EpisodeQuerySet.as_manager()
|
||||
detail_url_name = "episode-detail"
|
||||
list_url_name = "episode-list"
|
||||
edit_url_name = "episode-edit"
|
||||
template_prefix = "episode"
|
||||
|
||||
@property
|
||||
|
@ -59,11 +60,6 @@ class Episode(Page):
|
|||
verbose_name = _("Episode")
|
||||
verbose_name_plural = _("Episodes")
|
||||
|
||||
def get_absolute_url(self):
|
||||
if not self.is_published:
|
||||
return self.program.get_absolute_url()
|
||||
return super().get_absolute_url()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.parent is None:
|
||||
raise ValueError("missing parent program")
|
||||
|
|
|
@ -86,14 +86,6 @@ class BasePage(Renderable, models.Model):
|
|||
(STATUS_TRASH, _("trash")),
|
||||
)
|
||||
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True,
|
||||
related_name="child_set",
|
||||
)
|
||||
title = models.CharField(max_length=100)
|
||||
slug = models.SlugField(_("slug"), max_length=120, blank=True, unique=True, db_index=True)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
|
@ -133,12 +125,6 @@ class BasePage(Renderable, models.Model):
|
|||
count = Page.objects.filter(slug__startswith=self.slug).count()
|
||||
if count:
|
||||
self.slug += "-" + str(count)
|
||||
|
||||
if self.parent:
|
||||
if self.parent == self:
|
||||
self.parent = None
|
||||
if not self.cover:
|
||||
self.cover = self.parent.cover
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
|
@ -160,14 +146,10 @@ class BasePage(Renderable, models.Model):
|
|||
|
||||
@property
|
||||
def display_title(self):
|
||||
if self.is_published:
|
||||
return self.title
|
||||
return self.parent and self.parent.title or ""
|
||||
return self.is_published and self.title or ""
|
||||
|
||||
@cached_property
|
||||
def display_headline(self):
|
||||
if not self.content or not self.is_published:
|
||||
return self.parent and self.parent.display_headline or ""
|
||||
content = bleach.clean(self.content, tags=[], strip=True)
|
||||
content = headline_clean_re.sub("\n", content)
|
||||
if content.startswith("\n"):
|
||||
|
@ -205,6 +187,7 @@ class BasePage(Renderable, models.Model):
|
|||
return cls(**cls.get_init_kwargs_from(page, **kwargs))
|
||||
|
||||
|
||||
# FIXME: rename
|
||||
class PageQuerySet(BasePageQuerySet):
|
||||
def published(self):
|
||||
return self.filter(status=Page.STATUS_PUBLISHED, pub_date__lte=tz.now())
|
||||
|
@ -232,8 +215,48 @@ class Page(BasePage):
|
|||
)
|
||||
|
||||
objects = PageQuerySet.as_manager()
|
||||
|
||||
detail_url_name = ""
|
||||
list_url_name = "page-list"
|
||||
edit_url_name = ""
|
||||
|
||||
@classmethod
|
||||
def get_list_url(cls, kwargs={}):
|
||||
return reverse(cls.list_url_name, kwargs=kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Publication")
|
||||
verbose_name_plural = _("Publications")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__initial_cover = self.cover
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.is_published and self.pub_date is None:
|
||||
self.pub_date = tz.now()
|
||||
elif not self.is_published:
|
||||
self.pub_date = None
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ChildPage(Page):
|
||||
parent = models.ForeignKey(Page, models.CASCADE, blank=True, null=True, db_index=True, related_name="%(class)s_set")
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def display_title(self):
|
||||
if self.is_published:
|
||||
return self.title
|
||||
return self.parent and self.parent.title or ""
|
||||
|
||||
@property
|
||||
def display_headline(self):
|
||||
if not self.content or not self.is_published:
|
||||
return self.parent and self.parent.display_headline or ""
|
||||
return super().display_headline
|
||||
|
||||
@cached_property
|
||||
def parent_subclass(self):
|
||||
|
@ -246,21 +269,13 @@ class Page(BasePage):
|
|||
return self.parent_subclass.get_absolute_url()
|
||||
return super().get_absolute_url()
|
||||
|
||||
@classmethod
|
||||
def get_list_url(cls, kwargs={}):
|
||||
return reverse(cls.list_url_name, kwargs=kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Publication")
|
||||
verbose_name_plural = _("Publications")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.is_published and self.pub_date is None:
|
||||
self.pub_date = tz.now()
|
||||
elif not self.is_published:
|
||||
self.pub_date = None
|
||||
|
||||
if self.parent and not self.category:
|
||||
if self.parent:
|
||||
if self.parent == self:
|
||||
self.parent = None
|
||||
if not self.cover:
|
||||
self.cover = self.parent.cover
|
||||
if not self.category:
|
||||
self.category = self.parent.category
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from django.conf import settings as conf
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import F
|
||||
from django.db.models.functions import Concat, Substr
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from aircox.conf import settings
|
||||
|
@ -15,13 +11,11 @@ from aircox.conf import settings
|
|||
from .page import Page, PageQuerySet
|
||||
from .station import Station
|
||||
|
||||
logger = logging.getLogger("aircox")
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ProgramQuerySet",
|
||||
"Program",
|
||||
"ProgramChildQuerySet",
|
||||
"ProgramQuerySet",
|
||||
"Stream",
|
||||
)
|
||||
|
||||
|
@ -34,6 +28,16 @@ class ProgramQuerySet(PageQuerySet):
|
|||
def active(self):
|
||||
return self.filter(active=True)
|
||||
|
||||
def editor(self, user):
|
||||
"""Return programs for which user is an editor.
|
||||
|
||||
Superuser is considered as editor of all groups.
|
||||
"""
|
||||
if user.is_superuser:
|
||||
return self
|
||||
groups = self.request.user.groups.all()
|
||||
return self.filter(editors_group__in=groups)
|
||||
|
||||
|
||||
class Program(Page):
|
||||
"""A Program can either be a Streamed or a Scheduled program.
|
||||
|
@ -60,11 +64,12 @@ class Program(Page):
|
|||
default=True,
|
||||
help_text=_("update later diffusions according to schedule changes"),
|
||||
)
|
||||
editors = models.ForeignKey(Group, models.CASCADE, blank=True, null=True, verbose_name=_("editors"))
|
||||
editors_group = models.ForeignKey(Group, models.CASCADE, blank=True, null=True, verbose_name=_("editors"))
|
||||
|
||||
objects = ProgramQuerySet.as_manager()
|
||||
detail_url_name = "program-detail"
|
||||
list_url_name = "program-list"
|
||||
edit_url_name = "program-edit"
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
|
@ -84,19 +89,10 @@ class Program(Page):
|
|||
def excerpts_path(self):
|
||||
return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
|
||||
|
||||
@property
|
||||
def editors_group_name(self):
|
||||
return f"{self.title} editors"
|
||||
|
||||
@property
|
||||
def change_permission_codename(self):
|
||||
return f"change_program_{self.slug}"
|
||||
|
||||
def __init__(self, *kargs, **kwargs):
|
||||
super().__init__(*kargs, **kwargs)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.slug:
|
||||
self.__initial_path = self.path
|
||||
self.__initial_cover = self.cover
|
||||
|
||||
@classmethod
|
||||
def get_from_path(cl, path):
|
||||
|
@ -121,18 +117,36 @@ class Program(Page):
|
|||
os.makedirs(path, exist_ok=True)
|
||||
return os.path.exists(path)
|
||||
|
||||
def set_group_ownership(self):
|
||||
editors, created = Group.objects.get_or_create(name=self.editors_group_name)
|
||||
def can_update(self, user):
|
||||
"""Return True if user can update program."""
|
||||
if user.is_superuser:
|
||||
return True
|
||||
perm = self._perm_update_codename.format(self=self)
|
||||
return user.has_perm("aircox." + perm)
|
||||
|
||||
# permissions and editor group format. Use of pk in codename makes it
|
||||
# consistent in case program title changes.
|
||||
_editor_group_name = "{self.title}: editors"
|
||||
_perm_update_codename = "program_{self.pk}_update"
|
||||
_perm_update_name = "{self.title}: update"
|
||||
|
||||
def init_editor_group(self):
|
||||
if not self.editors_group:
|
||||
name = self._editor_group_name.format(self=self)
|
||||
self.editors_group, created = Group.objects.get_or_create(name=name)
|
||||
else:
|
||||
created = False
|
||||
|
||||
if created:
|
||||
self.editors = editors
|
||||
super().save()
|
||||
if not self.pk:
|
||||
self.save(check_groups=False)
|
||||
permission, _ = Permission.objects.get_or_create(
|
||||
codename=self.change_permission_codename,
|
||||
codename=self._perm_update_codename.format(self=self),
|
||||
content_type=ContentType.objects.get_for_model(self),
|
||||
defaults={"name": f"change program {self.title}"},
|
||||
defaults={"name": self._perm_update_name.format(self=self)},
|
||||
)
|
||||
if permission not in editors.permissions.all():
|
||||
editors.permissions.add(permission)
|
||||
if permission not in self.editors_group.permissions.all():
|
||||
self.editors_group.permissions.add(permission)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Program")
|
||||
|
@ -141,29 +155,11 @@ class Program(Page):
|
|||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *kargs, **kwargs):
|
||||
from .sound import Sound
|
||||
|
||||
super().save(*kargs, **kwargs)
|
||||
|
||||
# TODO: move in signals
|
||||
path_ = getattr(self, "__initial_path", None)
|
||||
abspath = path_ and os.path.join(conf.MEDIA_ROOT, path_)
|
||||
if path_ is not None and path_ != self.path and os.path.exists(abspath) and not os.path.exists(self.abspath):
|
||||
logger.info(
|
||||
"program #%s's dir changed to %s - update it.",
|
||||
self.id,
|
||||
self.title,
|
||||
)
|
||||
|
||||
shutil.move(abspath, self.abspath)
|
||||
Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_))))
|
||||
|
||||
self.set_group_ownership()
|
||||
|
||||
|
||||
class ProgramChildQuerySet(PageQuerySet):
|
||||
def station(self, station=None, id=None):
|
||||
# lookup `__program` is due to parent being a page subclass (page is
|
||||
# concrete).
|
||||
return (
|
||||
self.filter(parent__program__station=station)
|
||||
if id is None
|
||||
|
@ -173,6 +169,10 @@ class ProgramChildQuerySet(PageQuerySet):
|
|||
def program(self, program=None, id=None):
|
||||
return self.parent(program, id)
|
||||
|
||||
def editor(self, user):
|
||||
programs = Program.objects.editor(user)
|
||||
return self.filter(parent__program__in=programs)
|
||||
|
||||
|
||||
class Stream(models.Model):
|
||||
"""When there are no program scheduled, it is possible to play sounds in
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from django.conf import settings as conf
|
||||
from django.contrib.auth.models import Group, Permission, User
|
||||
from django.db import transaction
|
||||
from django.db.models import signals
|
||||
from django.db.models import signals, F
|
||||
from django.db.models.functions import Concat, Substr
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone as tz
|
||||
|
||||
from aircox import utils
|
||||
from aircox.conf import settings
|
||||
from .article import Article
|
||||
from .diffusion import Diffusion
|
||||
from .episode import Episode
|
||||
from .page import Page
|
||||
|
@ -16,6 +21,9 @@ from .schedule import Schedule
|
|||
from .sound import Sound
|
||||
|
||||
|
||||
logger = logging.getLogger("aircox")
|
||||
|
||||
|
||||
# Add a default group to a user when it is created. It also assigns a list
|
||||
# of permissions to the group if it is created.
|
||||
#
|
||||
|
@ -42,24 +50,40 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
|
|||
instance.groups.add(group)
|
||||
|
||||
|
||||
# ---- page
|
||||
@receiver(signals.post_save, sender=Page)
|
||||
def page_post_save(sender, instance, created, *args, **kwargs):
|
||||
if not created and instance.cover and "raw" not in kwargs:
|
||||
Page.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
|
||||
def page_post_save__child_page_defaults(sender, instance, created, *args, **kwargs):
|
||||
initial_cover = getattr(instance, "__initial_cover", None)
|
||||
if initial_cover is None and instance.cover is not None:
|
||||
Episode.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
|
||||
Article.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
|
||||
|
||||
|
||||
# ---- program
|
||||
@receiver(signals.post_save, sender=Program)
|
||||
def program_post_save(sender, instance, created, *args, **kwargs):
|
||||
"""Clean-up later diffusions when a program becomes inactive."""
|
||||
def program_post_save__clean_later_episodes(sender, instance, created, *args, **kwargs):
|
||||
if not instance.active:
|
||||
Diffusion.objects.program(instance).after(tz.now()).delete()
|
||||
Episode.objects.parent(instance).filter(diffusion__isnull=True).delete()
|
||||
|
||||
cover = getattr(instance, "__initial_cover", None)
|
||||
if cover is None and instance.cover is not None:
|
||||
Episode.objects.parent(instance).filter(cover__isnull=True).update(cover=instance.cover)
|
||||
|
||||
@receiver(signals.post_save, sender=Program)
|
||||
def program_post_save__mv_sounds(sender, instance, created, *args, **kwargs):
|
||||
path_ = getattr(instance, "__initial_path", None)
|
||||
if path_ in (None, instance.path):
|
||||
return
|
||||
|
||||
abspath = path_ and os.path.join(conf.MEDIA_ROOT, path_)
|
||||
if os.path.exists(abspath) and not os.path.exists(instance.abspath):
|
||||
logger.info(
|
||||
f"program #{instance.pk}'s dir changed to {instance.title} - update it.", instance.id, instance.title
|
||||
)
|
||||
|
||||
shutil.move(abspath, instance.abspath)
|
||||
Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_))))
|
||||
|
||||
|
||||
# ---- schedule
|
||||
@receiver(signals.pre_save, sender=Schedule)
|
||||
def schedule_pre_save(sender, instance, *args, **kwargs):
|
||||
if getattr(instance, "pk") is not None and "raw" not in kwargs:
|
||||
|
@ -94,11 +118,13 @@ def schedule_pre_delete(sender, instance, *args, **kwargs):
|
|||
Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete()
|
||||
|
||||
|
||||
# ---- diffusion
|
||||
@receiver(signals.post_delete, sender=Diffusion)
|
||||
def diffusion_post_delete(sender, instance, *args, **kwargs):
|
||||
Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete()
|
||||
|
||||
|
||||
# ---- files
|
||||
@receiver(signals.post_delete, sender=Sound)
|
||||
def delete_file(sender, instance, *args, **kwargs):
|
||||
"""Deletes file on `post_delete`"""
|
||||
|
|
|
@ -169,6 +169,9 @@
|
|||
.preview .headings a:hover {
|
||||
color: var(--heading-link-hv-fg) !important;
|
||||
}
|
||||
.preview.tiny .title {
|
||||
font-size: calc(var(--preview-title-sz) * 0.8);
|
||||
}
|
||||
.preview.tiny .content {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
@ -256,6 +259,7 @@
|
|||
}
|
||||
.list-item .actions {
|
||||
text-align: right;
|
||||
align-items: center;
|
||||
}
|
||||
.list-item:not(.wide) .media {
|
||||
padding: 0.6rem;
|
||||
|
@ -9689,6 +9693,26 @@ a.tag:hover {
|
|||
height: 25em;
|
||||
}
|
||||
|
||||
.gap-1 {
|
||||
gap: 0.2rem !important;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.4rem !important;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 0.6rem !important;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 1.2rem !important;
|
||||
}
|
||||
|
||||
.gap-5 {
|
||||
gap: 1.6rem !important;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
@ -9819,6 +9843,13 @@ a.tag:hover {
|
|||
border-color: #b00 !important;
|
||||
}
|
||||
|
||||
.box-shadow:hover {
|
||||
box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.box-shadow.active {
|
||||
box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
input.half-field:not(:active):not(:hover) {
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
|
|
|
@ -169,6 +169,9 @@
|
|||
.preview .headings a:hover {
|
||||
color: var(--heading-link-hv-fg) !important;
|
||||
}
|
||||
.preview.tiny .title {
|
||||
font-size: calc(var(--preview-title-sz) * 0.8);
|
||||
}
|
||||
.preview.tiny .content {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
@ -256,6 +259,7 @@
|
|||
}
|
||||
.list-item .actions {
|
||||
text-align: right;
|
||||
align-items: center;
|
||||
}
|
||||
.list-item:not(.wide) .media {
|
||||
padding: 0.6rem;
|
||||
|
|
|
@ -86,9 +86,9 @@ Usefull context:
|
|||
{% endspaceless %}
|
||||
|
||||
{% block header-container %}
|
||||
{% if page or cover or title %}
|
||||
<header class="container header preview preview-header {% if cover %}has-cover{% endif %}">
|
||||
{% block header %}
|
||||
{% spaceless %}
|
||||
<figure class="header-cover">
|
||||
{% block header-cover %}
|
||||
{% if cover %}
|
||||
|
@ -96,6 +96,7 @@ Usefull context:
|
|||
{% endif %}
|
||||
{% endblock %}
|
||||
</figure>
|
||||
{% endspaceless %}
|
||||
<div class="headings preview-card-headings">
|
||||
{% block headings %}
|
||||
<div>
|
||||
|
@ -119,7 +120,6 @@ Usefull context:
|
|||
</div>
|
||||
{% endblock %}
|
||||
</header>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content-container %}
|
||||
|
@ -144,15 +144,6 @@ Usefull context:
|
|||
{{ render }}
|
||||
{% endfor %}
|
||||
{% endcomment %}
|
||||
|
||||
{% if not request.user.is_authenticated %}
|
||||
<a class="nav-item" href="{% url "profile" %}" target="new"
|
||||
title="{% translate "Profile" %}">
|
||||
<span class="small icon">
|
||||
<i class="fa fa-user"></i>
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% if request.station and request.station.legal_label %}
|
||||
|
|
8
aircox/templates/aircox/dashboard/base.html
Normal file
8
aircox/templates/aircox/dashboard/base.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% extends "../base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block head-title %}
|
||||
{% block title %}{{ block.super }}{% endblock %}
|
||||
—
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
50
aircox/templates/aircox/dashboard/dashboard.html
Normal file
50
aircox/templates/aircox/dashboard/dashboard.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
{% extends "./base.html" %}
|
||||
{% load i18n aircox %}
|
||||
|
||||
{% block subtitle %}
|
||||
<span class="icon">
|
||||
<i class="fa fa-user"></i>
|
||||
</span>
|
||||
{{ block.super }}
|
||||
{% if user.is_superuser %}
|
||||
— {% translate "administrator" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content-container %}
|
||||
<section class="container grid-2 gap-4">
|
||||
<div>
|
||||
<h2 class="title is-2">{% translate "Programs" %}</h2>
|
||||
<div class="box box-shadow p-3" style="max-height: 35rem; overflow-y:auto;">
|
||||
{% for object in programs %}
|
||||
{% page_widget "item" object admin=True is_tiny=True %}
|
||||
{% empty %}
|
||||
<div>{% translate "No diffusion to come" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="title is-2">{% translate "Next diffusions" %}</h2>
|
||||
<div class="box box-shadow p-3" style="max-height: 35rem; overflow-y:auto;">
|
||||
{% for object in next_diffs|slice:"0:25" %}
|
||||
{% page_widget "item" object.episode diffusion=object timetable=True admin=True is_tiny=True %}
|
||||
{% empty %}
|
||||
<div>{% translate "No diffusion to come" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="title is-2">{% translate "Last Comments" %}</h2>
|
||||
<div class="box box-shadow p-3" style="max-height: 35rem; overflow-y:auto;">
|
||||
{% for object in comments|slice:"0:25" %}
|
||||
{% page_widget "item" object admin=True is_tiny=True %}
|
||||
{% empty %}
|
||||
<div>{% translate "No diffusion to come" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div></section>
|
||||
{% endblock %}
|
|
@ -10,10 +10,13 @@
|
|||
<div class="dropdown-menu" id="dropdown-menu" role="menu" style="z-index:200">
|
||||
<div class="dropdown-content">
|
||||
{% block user-menu %}
|
||||
<a class="dropdown-item" href="{% url "dashboard" %}">
|
||||
{% translate "Dashboard" %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
{% if user.is_admin %}
|
||||
{% block admin-menu %}
|
||||
<a class="nav-item" href="{% url "admin:index" %}" target="new">
|
||||
<a class="dropdown-item" href="{% url "admin:index" %}" target="new">
|
||||
{% translate "Admin" %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
{% if object.podcasts %}
|
||||
{% spaceless %}
|
||||
<section class="container no-border">
|
||||
<h3 class="title">{% translate "Podcasts" %}</h3>
|
||||
<h2 class="title is-2">{% translate "Podcasts" %}</h2>
|
||||
<a-playlist v-if="page" :set="podcasts"
|
||||
name="{{ page.title }}"
|
||||
list-class="menu-list" item-class="menu-item"
|
||||
|
@ -25,7 +25,7 @@
|
|||
|
||||
{% if tracks %}
|
||||
<section class="container">
|
||||
<h3 class="title">{% translate "Playlist" %}</h3>
|
||||
<h2 class="title is-2">{% translate "Playlist" %}</h2>
|
||||
<table class="table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<hr/>
|
||||
{% include "./dashboard/tracklist_editor.html" with formset=tracklist_formset formset_data=tracklist_formset_data %}
|
||||
<hr/>
|
||||
<h3 class="title">{% translate "Podcasts" %}</h3>
|
||||
<h2 class="title is-2">{% translate "Podcasts" %}</h2>
|
||||
{% include "./dashboard/soundlist_editor.html" with formset=soundlist_formset formset_data=soundlist_formset_data %}
|
||||
</template>
|
||||
</a-episode>
|
||||
|
|
|
@ -37,7 +37,7 @@ Context:
|
|||
{% if related_objects %}
|
||||
<section class="container">
|
||||
{% with models=object|verbose_name:True %}
|
||||
<h3 class="title">{% blocktranslate %}Related {{models}}{% endblocktranslate %}</h3>
|
||||
<h2 class="title is-2">{% blocktranslate %}Related {{models}}{% endblocktranslate %}</h2>
|
||||
|
||||
{% include "./widgets/carousel.html" with objects=related_objects url_name=object.list_url_name url_category=object.category %}
|
||||
{% endwith %}
|
||||
|
@ -48,7 +48,7 @@ Context:
|
|||
{% block comments %}
|
||||
{% if comments %}
|
||||
<section class="container">
|
||||
<h2 class="title">{% translate "Comments" %}</h2>
|
||||
<h2 class="title is-2">{% translate "Comments" %}</h2>
|
||||
|
||||
{% for object in comments %}
|
||||
{% page_widget "item" object %}
|
||||
|
@ -58,7 +58,7 @@ Context:
|
|||
|
||||
{% if comment_form %}
|
||||
<section class="container">
|
||||
<h2 class="title">{% translate "Post a comment" %}</h2>
|
||||
<h2 class="title is-2">{% translate "Post a comment" %}</h2>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% render_honeypot_field "website" %}
|
||||
|
|
|
@ -2,20 +2,8 @@
|
|||
{% comment %}Detail page of a show{% endcomment %}
|
||||
{% load i18n aircox %}
|
||||
|
||||
{% block top-nav-tools %}
|
||||
{% has_perm page page.change_permission_codename simple=True as can_edit %}
|
||||
{% if can_edit %}
|
||||
<a class="navbar-item" href="{% url 'program-edit' page.pk %}" target="_self">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-pen"></i>
|
||||
</span>
|
||||
<span>{% translate "Edit" %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content-container %}
|
||||
{% with schedules=program.schedule_set.all %}
|
||||
{% with schedules=object.schedule_set.all %}
|
||||
{% if schedules %}
|
||||
<header class="container schedules">
|
||||
{% for schedule in schedules %}
|
||||
|
@ -49,7 +37,7 @@
|
|||
|
||||
{% if episodes %}
|
||||
<section class="container">
|
||||
<h3 class="title">{% translate "Last Episodes" %}</h3>
|
||||
<h2 class="title is-2">{% translate "Last Episodes" %}</h2>
|
||||
{% include "./widgets/carousel.html" with objects=episodes url_name="episode-list" url_parent=object url_label=_("All episodes") %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
@ -57,7 +45,7 @@
|
|||
|
||||
{% if articles %}
|
||||
<section class="container">
|
||||
<h3 class="title">{% translate "Last Articles" %}</h3>
|
||||
<h2 class="title is-2">{% translate "Last Articles" %}</h2>
|
||||
{% include "./widgets/carousel.html" with objects=articles url_name="article-list" url_parent=object url_label=_("All articles") %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
|
26
aircox/templates/aircox/widgets/diffusion_tags.html
Normal file
26
aircox/templates/aircox/widgets/diffusion_tags.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
{% comment %}
|
||||
Context:
|
||||
- object: diffusion
|
||||
{% endcomment %}
|
||||
{% load i18n %}
|
||||
{% if object.type == object.TYPE_ON_AIR %}
|
||||
<span class="tag is-info">
|
||||
<span class="icon is-small">
|
||||
{% if object.is_live %}
|
||||
<i class="fa fa-microphone"
|
||||
title="{% translate "Live diffusion" %}"></i>
|
||||
{% else %}
|
||||
<i class="fa fa-music"
|
||||
title="{% translate "Differed diffusion" %}"></i>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
{{ object.get_type_display }}
|
||||
</span>
|
||||
{% elif object.type == object.TYPE_CANCEL %}
|
||||
<span class="tag is-danger">
|
||||
{{ object.get_type_display }}</span>
|
||||
{% elif object.type == object.TYPE_UNCONFIRMED %}
|
||||
<span class="tag is-warning">
|
||||
{{ object.get_type_display }}</span>
|
||||
{% endif %}
|
|
@ -3,14 +3,8 @@
|
|||
|
||||
{% block outer %}
|
||||
{% with diffusion.is_now as is_active %}
|
||||
{% if admin %}
|
||||
{% with object|admin_url:"change" as url %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{{ block.super }}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
|
@ -29,10 +23,10 @@
|
|||
{% endblock %}
|
||||
|
||||
|
||||
{% block actions %}
|
||||
{{ block.super }}
|
||||
|
||||
{% block actions-container %}
|
||||
{% if admin and diffusion %}
|
||||
<div class="flex-row">
|
||||
<div class="flex-grow-1">
|
||||
{% if diffusion.type == diffusion.TYPE_ON_AIR %}
|
||||
<span class="tag is-info">
|
||||
<span class="icon is-small">
|
||||
|
@ -54,8 +48,16 @@
|
|||
<span class="tag is-warning">
|
||||
{{ diffusion.get_type_display }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ block.super }}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ block.super }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
{{ block.super }}
|
||||
{% if object.sound_set.count %}
|
||||
<button class="button action" @click="player.playButtonClick($event)"
|
||||
data-sounds="{{ object.podcasts|json }}">
|
||||
|
@ -65,4 +67,5 @@
|
|||
<label>{% translate "Listen" %}</label>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
{% block headings-container %}{{ block.super }}{% endblock %}
|
||||
{% block content-container %}
|
||||
<div class="media">
|
||||
{% if object.cover %}
|
||||
{% if object.cover and not no_cover %}
|
||||
<a href="{{ object.get_absolute_url }}"
|
||||
class="media-left preview-cover small"
|
||||
style="background-image: url({{ object.cover.url }})">
|
||||
|
@ -24,17 +24,9 @@
|
|||
{% endif %}
|
||||
<div class="media-content flex-column">
|
||||
<section class="content flex-grow-1">
|
||||
{% block content %}
|
||||
{% if content and with_content %}
|
||||
{% autoescape off %}
|
||||
{{ content|striptags|linebreaks }}
|
||||
{% endautoescape %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block content %}{{ block.super }}{% endblock %}
|
||||
</section>
|
||||
{% block actions-container %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
{% block actions-container %}{{ block.super }}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -4,15 +4,9 @@
|
|||
|
||||
{% block outer %}
|
||||
{% with cover|default:object.cover_url as cover %}
|
||||
{% if admin %}
|
||||
{% with object|admin_url:"change" as url %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with url|default:object.get_absolute_url as url %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -27,11 +21,12 @@
|
|||
|
||||
|
||||
{% block content %}
|
||||
{% if content %}
|
||||
{{ content }}
|
||||
{% elif object %}
|
||||
{% if not content and object %}
|
||||
{% with object.display_headline as content %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{{ block.super }}
|
||||
{{ object.display_headline }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -7,6 +7,10 @@ Content related context:
|
|||
- subtitle: subtitle
|
||||
- content: content to display
|
||||
|
||||
Components:
|
||||
- no_cover: don't show cover
|
||||
- no_content: don't show content
|
||||
|
||||
Styling related context:
|
||||
- is_active: add "active" css class
|
||||
- is_small: add "small" css class
|
||||
|
@ -18,7 +22,7 @@ Styling related context:
|
|||
{% endcomment %}
|
||||
|
||||
{% block outer %}
|
||||
<{{ tag|default:"article" }} class="preview {% if not cover %}no-cover {% endif %}{% if is_active %}active {% endif %}{% block tag-class %}{{ tag_class|default:"" }} {% endblock %}" {% block tag-extra %}{% endblock %}>
|
||||
<{{ tag|default:"article" }} class="preview {% if not cover %}no-cover {% endif %}{% if is_active %}active {% endif %}{% if is_tiny %}tiny{% elif is_small %}small{% endif %}{% block tag-class %}{{ tag_class|default:"" }} {% endblock %}" {% block tag-extra %}{% endblock %}>
|
||||
{% block inner %}
|
||||
{% block headings-container %}
|
||||
<header class="headings{% block headings-class %}{% endblock %}"{% block headings-tag-extra %}{% endblock %}>
|
||||
|
@ -40,7 +44,7 @@ Styling related context:
|
|||
{% block content-container %}
|
||||
<section class="content headings-container">
|
||||
{% block content %}
|
||||
{% if content and with_content %}
|
||||
{% if content and not no_content %}
|
||||
{% autoescape off %}
|
||||
{{ content|striptags|linebreaks }}
|
||||
{% endautoescape %}
|
||||
|
@ -50,9 +54,15 @@ Styling related context:
|
|||
{% endblock %}
|
||||
|
||||
{% block actions-container %}
|
||||
{% spaceless %}
|
||||
<div class="actions">
|
||||
{% block actions %}{% endblock %}
|
||||
{% block actions %}
|
||||
{% if admin and object.edit_url_name %}
|
||||
<a href="{% url object.edit_url_name pk=object.pk %}">{% translate "Edit" %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endspaceless %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
</{{ tag|default:"article" }}>
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.urls import include, path, register_converter
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from . import forms, models, views, viewsets
|
||||
from . import models, views, viewsets
|
||||
from .converters import DateConverter, PagePathConverter, WeekConverter
|
||||
|
||||
__all__ = ["api", "urls"]
|
||||
|
@ -70,7 +70,7 @@ urls = [
|
|||
name="page-list",
|
||||
),
|
||||
path(
|
||||
_("publications/c/<slug:category_slug>"),
|
||||
_("publications/c/<slug:category_slug>/"),
|
||||
views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.Target.PAGES),
|
||||
name="page-list",
|
||||
),
|
||||
|
@ -98,47 +98,29 @@ urls = [
|
|||
views.ProgramDetailView.as_view(),
|
||||
name="program-detail",
|
||||
),
|
||||
path(_("programs/<slug:parent_slug>/articles"), views.ArticleListView.as_view(), name="article-list"),
|
||||
path(_("programs/<slug:parent_slug>/podcasts"), views.PodcastListView.as_view(), name="podcast-list"),
|
||||
path(_("programs/<slug:parent_slug>/episodes"), views.EpisodeListView.as_view(), name="episode-list"),
|
||||
path(_("programs/<slug:parent_slug>/diffusions"), views.DiffusionListView.as_view(), name="diffusion-list"),
|
||||
path(_("programs/<slug:parent_slug>/articles/"), views.ArticleListView.as_view(), name="article-list"),
|
||||
path(_("programs/<slug:parent_slug>/podcasts/"), views.PodcastListView.as_view(), name="podcast-list"),
|
||||
path(_("programs/<slug:parent_slug>/episodes/"), views.EpisodeListView.as_view(), name="episode-list"),
|
||||
path(_("programs/<slug:parent_slug>/diffusions/"), views.DiffusionListView.as_view(), name="diffusion-list"),
|
||||
path(
|
||||
_("programs/<slug:parent_slug>/publications"),
|
||||
_("programs/<slug:parent_slug>/publications/"),
|
||||
views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.Target.PAGES),
|
||||
name="page-list",
|
||||
),
|
||||
# ---- episodes
|
||||
path(_("programs/episodes/"), views.EpisodeListView.as_view(), name="episode-list"),
|
||||
path(_("programs/episodes/c/<slug:category_slug>"), views.EpisodeListView.as_view(), name="episode-list"),
|
||||
path(_("programs/episodes/c/<slug:category_slug>/"), views.EpisodeListView.as_view(), name="episode-list"),
|
||||
path(
|
||||
_("programs/episodes/<slug:slug>"),
|
||||
_("programs/episodes/<slug:slug>/"),
|
||||
views.EpisodeDetailView.as_view(),
|
||||
name="episode-detail",
|
||||
),
|
||||
path(
|
||||
_("programs/episodes/<pk>/edit/"),
|
||||
views.EpisodeUpdateView.as_view(),
|
||||
name="episode-edit",
|
||||
),
|
||||
path(_("podcasts/"), views.PodcastListView.as_view(), name="podcast-list"),
|
||||
path(_("podcasts/c/<slug:category_slug>/"), views.PodcastListView.as_view(), name="podcast-list"),
|
||||
# ---- dashboard
|
||||
path(_("dashboard/"), views.dashboard.DashboardView.as_view(), name="dashboard"),
|
||||
path(_("dashboard/program/<pk>/"), views.ProgramUpdateView.as_view(), name="program-edit"),
|
||||
path(_("dashboard/episodes/<pk>/"), views.EpisodeUpdateView.as_view(), name="episode-edit"),
|
||||
# ---- others
|
||||
path(
|
||||
_("program/<pk>/edit/"),
|
||||
views.ProgramUpdateView.as_view(),
|
||||
name="program-edit",
|
||||
),
|
||||
path(
|
||||
"errors/no-station",
|
||||
views.errors.NoStationErrorView.as_view(),
|
||||
name="errors-no-station",
|
||||
),
|
||||
# ---- backoffice
|
||||
path(_("edit/"), views.ProfileView.as_view(), name="profile"),
|
||||
path(
|
||||
_("edit/programs/<slug:slug>"),
|
||||
views.PageUpdateView.as_view(model=models.Program, form_class=forms.ProgramForm),
|
||||
name="program-update",
|
||||
),
|
||||
path("accounts/profile/", views.ProfileView.as_view(), name="profile"),
|
||||
path("errors/no-station/", views.errors.NoStationErrorView.as_view(), name="errors-no-station"),
|
||||
]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from . import admin, errors
|
||||
from . import admin, dashboard, errors
|
||||
from .article import ArticleDetailView, ArticleListView
|
||||
from .base import BaseAPIView, BaseView
|
||||
from .diffusion import DiffusionListView, TimeTableView
|
||||
|
@ -12,19 +12,18 @@ from .page import (
|
|||
PageListView,
|
||||
PageUpdateView,
|
||||
)
|
||||
from .profile import ProfileView
|
||||
from .program import (
|
||||
ProgramDetailView,
|
||||
ProgramListView,
|
||||
ProgramPageDetailView,
|
||||
ProgramPageListView,
|
||||
ProgramUpdateView,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"admin",
|
||||
"dashboard",
|
||||
"errors",
|
||||
"attached",
|
||||
"BaseAPIView",
|
||||
"BaseView",
|
||||
"ArticleDetailView",
|
||||
|
@ -43,13 +42,9 @@ __all__ = (
|
|||
"PageDetailView",
|
||||
"PageUpdateView",
|
||||
"PageListView",
|
||||
"ProfileView",
|
||||
"ProgramDetailView",
|
||||
"ProgramListView",
|
||||
"ProgramPageDetailView",
|
||||
"ProgramPageListView",
|
||||
"ProgramUpdateView",
|
||||
"attached",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ __all__ = ("BaseView", "BaseAPIView")
|
|||
class BaseView(TemplateResponseMixin, ContextMixin):
|
||||
related_count = 4
|
||||
related_carousel_count = 8
|
||||
title = ""
|
||||
|
||||
@property
|
||||
def station(self):
|
||||
|
@ -58,8 +59,10 @@ class BaseView(TemplateResponseMixin, ContextMixin):
|
|||
|
||||
page = kwargs.get("page")
|
||||
if page:
|
||||
kwargs["title"] = page.display_title
|
||||
kwargs["cover"] = page.cover and page.cover.url
|
||||
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()
|
||||
|
|
31
aircox/views/dashboard.py
Normal file
31
aircox/views/dashboard.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from django.db.models import Q
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from aircox import models
|
||||
from .base import BaseView
|
||||
|
||||
|
||||
class DashboardView(LoginRequiredMixin, BaseView, TemplateView):
|
||||
template_name = "aircox/dashboard/dashboard.html"
|
||||
title = _("Dashboard")
|
||||
|
||||
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)
|
|
@ -5,9 +5,7 @@ from aircox.models import Episode, Program, StaticPage, Track
|
|||
from aircox import forms, filters
|
||||
|
||||
from .mixins import VueFormDataMixin
|
||||
from .page import PageListView
|
||||
from .program import ProgramPageDetailView, BaseProgramMixin
|
||||
from .page import PageUpdateView
|
||||
from .page import PageDetailView, PageListView, PageUpdateView
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
@ -18,7 +16,7 @@ __all__ = (
|
|||
)
|
||||
|
||||
|
||||
class EpisodeDetailView(ProgramPageDetailView):
|
||||
class EpisodeDetailView(PageDetailView):
|
||||
model = Episode
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -47,14 +45,14 @@ class PodcastListView(EpisodeListView):
|
|||
queryset = Episode.objects.published().with_podcasts().order_by("-pub_date")
|
||||
|
||||
|
||||
class EpisodeUpdateView(UserPassesTestMixin, VueFormDataMixin, BaseProgramMixin, PageUpdateView):
|
||||
class EpisodeUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
|
||||
model = Episode
|
||||
form_class = forms.EpisodeForm
|
||||
template_name = "aircox/episode_form.html"
|
||||
|
||||
def test_func(self):
|
||||
program = self.get_object().program
|
||||
return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
|
||||
obj = self.get_object().parent_subclass
|
||||
return obj.can_update(self.request.user)
|
||||
|
||||
def get_tracklist_queryset(self, episode):
|
||||
return Track.objects.filter(episode=episode).order_by("position")
|
||||
|
|
|
@ -54,7 +54,7 @@ class HomeView(AttachedToMixin, BaseView, ListView):
|
|||
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)
|
||||
|
|
|
@ -33,7 +33,7 @@ class BasePageMixin:
|
|||
if page:
|
||||
if getattr(page, "category_id", None):
|
||||
return page.category
|
||||
if page.parent_id:
|
||||
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)
|
||||
|
@ -160,8 +160,8 @@ class PageDetailView(BasePageDetailView):
|
|||
kwargs["comment_form"] = self.get_comment_form()
|
||||
kwargs["comments"] = Comment.objects.filter(page=self.object).order_by("-date")
|
||||
|
||||
if self.object.parent_subclass:
|
||||
kwargs["parent"] = self.object.parent_subclass
|
||||
if parent_subclass := getattr(self.object, "parent_subclass", None):
|
||||
kwargs["parent"] = parent_subclass
|
||||
|
||||
if "related_objects" not in kwargs:
|
||||
related = self.get_related_queryset()
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from aircox.models import Program
|
||||
from aircox.views import BaseView
|
||||
|
||||
|
||||
class ProfileView(LoginRequiredMixin, BaseView, TemplateView):
|
||||
template_name = "accounts/profile.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
groups = self.request.user.groups.all()
|
||||
programs = Program.objects.filter(editors__in=groups)
|
||||
kwargs.update({"user": self.request.user, "programs": programs})
|
||||
return super().get_context_data(**kwargs)
|
|
@ -4,24 +4,16 @@ from django.contrib.auth.mixins import UserPassesTestMixin
|
|||
from django.urls import reverse
|
||||
|
||||
from aircox.forms import ProgramForm
|
||||
from aircox.models import Article, Episode, Page, Program, StaticPage
|
||||
from .mixins import ParentMixin
|
||||
from aircox.models import Article, Episode, Program, StaticPage
|
||||
from .page import PageDetailView, PageListView, PageUpdateView
|
||||
|
||||
__all__ = ["ProgramPageDetailView", "ProgramDetailView", "ProgramPageListView"]
|
||||
__all__ = (
|
||||
"ProgramDetailView",
|
||||
"ProgramDetailView",
|
||||
)
|
||||
|
||||
|
||||
class BaseProgramMixin:
|
||||
def get_program(self):
|
||||
return self.object
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
self.program = self.get_program()
|
||||
kwargs["program"] = self.program
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class ProgramDetailView(BaseProgramMixin, PageDetailView):
|
||||
class ProgramDetailView(PageDetailView):
|
||||
model = Program
|
||||
|
||||
def get_related_queryset(self):
|
||||
|
@ -52,18 +44,6 @@ class ProgramDetailView(BaseProgramMixin, PageDetailView):
|
|||
return super().get_template_names() + ["aircox/program_detail.html"]
|
||||
|
||||
|
||||
class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
|
||||
model = Program
|
||||
form_class = ProgramForm
|
||||
|
||||
def test_func(self):
|
||||
program = self.get_object()
|
||||
return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("program-detail", kwargs={"slug": self.get_object().slug})
|
||||
|
||||
|
||||
class ProgramListView(PageListView):
|
||||
model = Program
|
||||
attach_to_value = StaticPage.Target.PROGRAMS
|
||||
|
@ -72,22 +52,13 @@ class ProgramListView(PageListView):
|
|||
return super().get_queryset().order_by("title")
|
||||
|
||||
|
||||
# FIXME: not used
|
||||
class ProgramPageDetailView(BaseProgramMixin, ParentMixin, PageDetailView):
|
||||
"""Base view class for a page that is displayed as a program's child
|
||||
page."""
|
||||
class ProgramUpdateView(UserPassesTestMixin, PageUpdateView):
|
||||
model = Program
|
||||
form_class = ProgramForm
|
||||
|
||||
parent_model = Program
|
||||
def test_func(self):
|
||||
obj = self.get_object()
|
||||
return obj.can_update(self.request.user)
|
||||
|
||||
def get_program(self):
|
||||
self.parent = self.object.program
|
||||
return self.object.program
|
||||
|
||||
|
||||
class ProgramPageListView(BaseProgramMixin, PageListView):
|
||||
model = Page
|
||||
parent_model = Program
|
||||
queryset = Page.objects.select_subclasses()
|
||||
|
||||
def get_program(self):
|
||||
return self.parent
|
||||
def get_success_url(self):
|
||||
return reverse("program-detail", kwargs={"slug": self.get_object().slug})
|
||||
|
|
19
aircox_streamer/conf.py
Normal file
19
aircox_streamer/conf.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
import os
|
||||
import tempfile
|
||||
|
||||
from aircox.conf import BaseSettings
|
||||
|
||||
|
||||
__all__ = ("Settings", "settings")
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
WORKING_DIR = os.path.join(tempfile.gettempdir(), "aircox")
|
||||
"""Parent working directory for all stations."""
|
||||
|
||||
def get_dir(self, station, *paths):
|
||||
"""Return working directory for the provided station."""
|
||||
return os.path.join(self.WORKING_DIR, station.slug.replace("-", "_"), *paths)
|
||||
|
||||
|
||||
settings = Settings("AIRCOX_STREAMER")
|
|
@ -261,9 +261,8 @@
|
|||
}
|
||||
|
||||
&.tiny {
|
||||
.content {
|
||||
font-size: v.$text-size;
|
||||
}
|
||||
.title { font-size: calc(var(--preview-title-sz) * 0.8); }
|
||||
.content { font-size: v.$text-size; }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -367,6 +366,7 @@
|
|||
|
||||
.actions {
|
||||
text-align: right;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:not(.wide) .media {
|
||||
|
|
|
@ -54,7 +54,16 @@
|
|||
.height-20 { height: 20em; }
|
||||
.height-25 { height: 25em; }
|
||||
|
||||
// ---- grid
|
||||
// ---- grid / flex
|
||||
|
||||
.gap-1 { gap: v.$mp-1 !important; }
|
||||
.gap-2 { gap: v.$mp-2 !important; }
|
||||
.gap-3 { gap: v.$mp-3 !important; }
|
||||
.gap-4 { gap: v.$mp-4 !important; }
|
||||
.gap-5 { gap: v.$mp-5 !important; }
|
||||
|
||||
|
||||
// ---- ---- grid
|
||||
@mixin grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
@ -70,7 +79,7 @@
|
|||
.grid-2 { @include grid; @include grid-2; }
|
||||
.grid-3 { @include grid; @include grid-3; }
|
||||
|
||||
// ---- flex
|
||||
// ---- ---- flex
|
||||
.flex-row { display: flex; flex-direction: row }
|
||||
.flex-column { display: flex; flex-direction: column }
|
||||
.flex-grow-0 { flex-grow: 0 !important; }
|
||||
|
@ -124,3 +133,14 @@
|
|||
background-color: v.$red !important;
|
||||
border-color: v.$red-dark !important;
|
||||
}
|
||||
|
||||
|
||||
.box-shadow {
|
||||
&:hover {
|
||||
box-shadow: 0em 0em 1em rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: 0em 0em 1em rgba(0,0,0,0.4);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user