make EpisodeUpdateView work

This commit is contained in:
bkfox 2024-03-25 23:48:25 +01:00
parent 1f716891ac
commit 8d4b4c5896
20 changed files with 180 additions and 104 deletions

View File

@ -32,6 +32,7 @@ class SoundInline(admin.TabularInline):
"is_good_quality", "is_good_quality",
"is_public", "is_public",
"is_downloadable", "is_downloadable",
"is_removed",
] ]
readonly_fields = ["type", "audio", "duration", "is_good_quality"] readonly_fields = ["type", "audio", "duration", "is_good_quality"]
extra = 0 extra = 0
@ -89,11 +90,7 @@ class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
related.short_description = _("Program / Episode") related.short_description = _("Program / Episode")
def audio(self, obj): def audio(self, obj):
return ( return mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url)) if not obj.is_removed else ""
mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url))
if obj.type != Sound.TYPE_REMOVED
else ""
)
audio.short_description = _("Audio") audio.short_description = _("Audio")

View File

@ -99,7 +99,7 @@ class SoundFile:
sound = Sound.objects.path(self.path).first() sound = Sound.objects.path(self.path).first()
if sound: if sound:
if keep_deleted: if keep_deleted:
sound.type = sound.TYPE_REMOVED sound.is_removed = True
sound.check_on_file() sound.check_on_file()
sound.save() sound.save()
return sound return sound

View File

@ -80,6 +80,7 @@ TrackFormSet = modelformset_factory(
"tags", "tags",
"album", "album",
], ],
can_delete=True,
extra=0, extra=0,
) )
"""Track formset used in EpisodeUpdateView.""" """Track formset used in EpisodeUpdateView."""
@ -94,6 +95,7 @@ SoundFormSet = modelformset_factory(
"is_downloadable", "is_downloadable",
"duration", "duration",
], ],
can_delete=True,
extra=0, extra=0,
) )
"""Sound formset used in EpisodeUpdateView.""" """Sound formset used in EpisodeUpdateView."""

View File

@ -6,8 +6,9 @@ from .log import Log, LogQuerySet
from .page import Category, Comment, NavItem, Page, PageQuerySet, StaticPage from .page import Category, Comment, NavItem, Page, PageQuerySet, StaticPage
from .program import Program, ProgramChildQuerySet, ProgramQuerySet, Stream from .program import Program, ProgramChildQuerySet, ProgramQuerySet, Stream
from .schedule import Schedule from .schedule import Schedule
from .sound import Sound, SoundQuerySet, Track from .sound import Sound, SoundQuerySet
from .station import Port, Station, StationQuerySet from .station import Port, Station, StationQuerySet
from .track import Track
from .user_settings import UserSettings from .user_settings import UserSettings
__all__ = ( __all__ = (

View File

@ -62,6 +62,7 @@ class Episode(Page):
podcast["name"] = f"{self.title} - {archive_index}" podcast["name"] = f"{self.title} - {archive_index}"
else: else:
podcast["name"] = self.title podcast["name"] = self.title
archive_index += 1
podcasts[index]["cover"] = cover podcasts[index]["cover"] = cover
podcasts[index]["page_url"] = self.get_absolute_url() podcasts[index]["page_url"] = self.get_absolute_url()

View File

@ -8,8 +8,9 @@ from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .diffusion import Diffusion from .diffusion import Diffusion
from .sound import Sound, Track from .sound import Sound
from .station import Station from .station import Station
from .track import Track
from .page import Renderable from .page import Renderable
logger = logging.getLogger("aircox") logger = logging.getLogger("aircox")

View File

@ -1,3 +1,5 @@
import os
from django.contrib.auth.models import Group, Permission, User from django.contrib.auth.models import Group, Permission, User
from django.db import transaction from django.db import transaction
from django.db.models import signals from django.db.models import signals
@ -11,6 +13,7 @@ from .episode import Episode
from .page import Page from .page import Page
from .program import Program from .program import Program
from .schedule import Schedule from .schedule import Schedule
from .sound import Sound
# Add a default group to a user when it is created. It also assigns a list # Add a default group to a user when it is created. It also assigns a list
@ -94,3 +97,15 @@ def schedule_pre_delete(sender, instance, *args, **kwargs):
@receiver(signals.post_delete, sender=Diffusion) @receiver(signals.post_delete, sender=Diffusion)
def diffusion_post_delete(sender, instance, *args, **kwargs): def diffusion_post_delete(sender, instance, *args, **kwargs):
Episode.objects.filter(diffusion__isnull=True, content__isnull=True, sound__isnull=True).delete() Episode.objects.filter(diffusion__isnull=True, content__isnull=True, sound__isnull=True).delete()
@receiver(signals.post_delete, sender=Sound)
def delete_file(sender, instance, *args, **kwargs):
"""Deletes file on `post_delete`"""
if not instance.file:
return
path = instance.file.path
qs = sender.objects.filter(file=path)
if not qs.exists() and os.path.exists(path):
os.remove(path)

View File

@ -6,7 +6,6 @@ from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils import timezone as tz from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from taggit.managers import TaggableManager
from aircox.conf import settings from aircox.conf import settings
@ -16,7 +15,7 @@ from .program import Program
logger = logging.getLogger("aircox") logger = logging.getLogger("aircox")
__all__ = ("Sound", "SoundQuerySet", "Track") __all__ = ("Sound", "SoundQuerySet")
class SoundQuerySet(models.QuerySet): class SoundQuerySet(models.QuerySet):
@ -33,7 +32,7 @@ class SoundQuerySet(models.QuerySet):
return self.filter(episode__diffusion__id=id) return self.filter(episode__diffusion__id=id)
def available(self): def available(self):
return self.exclude(type=Sound.TYPE_REMOVED) return self.exclude(is_removed=False)
def public(self): def public(self):
"""Return sounds available as podcasts.""" """Return sounds available as podcasts."""
@ -85,12 +84,10 @@ class Sound(models.Model):
TYPE_OTHER = 0x00 TYPE_OTHER = 0x00
TYPE_ARCHIVE = 0x01 TYPE_ARCHIVE = 0x01
TYPE_EXCERPT = 0x02 TYPE_EXCERPT = 0x02
TYPE_REMOVED = 0x03
TYPE_CHOICES = ( TYPE_CHOICES = (
(TYPE_OTHER, _("other")), (TYPE_OTHER, _("other")),
(TYPE_ARCHIVE, _("archive")), (TYPE_ARCHIVE, _("archive")),
(TYPE_EXCERPT, _("excerpt")), (TYPE_EXCERPT, _("excerpt")),
(TYPE_REMOVED, _("removed")),
) )
name = models.CharField(_("name"), max_length=64) name = models.CharField(_("name"), max_length=64)
@ -116,6 +113,7 @@ class Sound(models.Model):
default=0, default=0,
help_text=_("position in the playlist"), help_text=_("position in the playlist"),
) )
is_removed = models.BooleanField(_("removed"), default=False, help_text=_("file has been removed"))
def _upload_to(self, filename): def _upload_to(self, filename):
subdir = settings.SOUND_ARCHIVES_SUBDIR if self.type == self.TYPE_ARCHIVE else settings.SOUND_EXCERPTS_SUBDIR subdir = settings.SOUND_ARCHIVES_SUBDIR if self.type == self.TYPE_ARCHIVE else settings.SOUND_EXCERPTS_SUBDIR
@ -201,16 +199,16 @@ class Sound(models.Model):
Return True if there was changes. Return True if there was changes.
""" """
if not self.file_exists(): if not self.file_exists():
if self.type == self.TYPE_REMOVED: if self.is_removed:
return return
logger.debug("sound %s: has been removed", self.file.name) logger.debug("sound %s: has been removed", self.file.name)
self.type = self.TYPE_REMOVED self.is_removed = True
return True return True
# not anymore removed # not anymore removed
changed = False changed = False
if self.type == self.TYPE_REMOVED and self.program: if self.is_removed and self.program:
changed = True changed = True
self.type = ( self.type = (
self.TYPE_ARCHIVE if self.file.name.startswith(self.program.archives_path) else self.TYPE_EXCERPT self.TYPE_ARCHIVE if self.file.name.startswith(self.program.archives_path) else self.TYPE_EXCERPT
@ -240,65 +238,3 @@ class Sound(models.Model):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.__check_name() self.__check_name()
class Track(models.Model):
"""Track of a playlist of an object.
The position can either be expressed as the position in the playlist
or as the moment in seconds it started.
"""
episode = models.ForeignKey(
Episode,
models.CASCADE,
blank=True,
null=True,
verbose_name=_("episode"),
)
sound = models.ForeignKey(
Sound,
models.CASCADE,
blank=True,
null=True,
verbose_name=_("sound"),
)
position = models.PositiveSmallIntegerField(
_("order"),
default=0,
help_text=_("position in the playlist"),
)
timestamp = models.PositiveSmallIntegerField(
_("timestamp"),
blank=True,
null=True,
help_text=_("position (in seconds)"),
)
title = models.CharField(_("title"), max_length=128)
artist = models.CharField(_("artist"), max_length=128)
album = models.CharField(_("album"), max_length=128, null=True, blank=True)
tags = TaggableManager(verbose_name=_("tags"), blank=True)
year = models.IntegerField(_("year"), blank=True, null=True)
# FIXME: remove?
info = models.CharField(
_("information"),
max_length=128,
blank=True,
null=True,
help_text=_(
"additional informations about this track, such as " "the version, if is it a remix, features, etc."
),
)
class Meta:
verbose_name = _("Track")
verbose_name_plural = _("Tracks")
ordering = ("position",)
def __str__(self):
return "{self.artist} -- {self.title} -- {self.position}".format(self=self)
def save(self, *args, **kwargs):
if (self.sound is None and self.episode is None) or (self.sound is not None and self.episode is not None):
raise ValueError("sound XOR episode is required")
super().save(*args, **kwargs)

72
aircox/models/track.py Normal file
View File

@ -0,0 +1,72 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from taggit.managers import TaggableManager
from .episode import Episode
from .sound import Sound
__all__ = ("Track",)
class Track(models.Model):
"""Track of a playlist of an object.
The position can either be expressed as the position in the playlist
or as the moment in seconds it started.
"""
episode = models.ForeignKey(
Episode,
models.CASCADE,
blank=True,
null=True,
verbose_name=_("episode"),
)
sound = models.ForeignKey(
Sound,
models.CASCADE,
blank=True,
null=True,
verbose_name=_("sound"),
)
position = models.PositiveSmallIntegerField(
_("order"),
default=0,
help_text=_("position in the playlist"),
)
timestamp = models.PositiveSmallIntegerField(
_("timestamp"),
blank=True,
null=True,
help_text=_("position (in seconds)"),
)
title = models.CharField(_("title"), max_length=128)
artist = models.CharField(_("artist"), max_length=128)
album = models.CharField(_("album"), max_length=128, null=True, blank=True)
tags = TaggableManager(verbose_name=_("tags"), blank=True)
year = models.IntegerField(_("year"), blank=True, null=True)
# FIXME: remove?
info = models.CharField(
_("information"),
max_length=128,
blank=True,
null=True,
help_text=_(
"additional informations about this track, such as " "the version, if is it a remix, features, etc."
),
)
class Meta:
verbose_name = _("Track")
verbose_name_plural = _("Tracks")
ordering = ("position",)
def __str__(self):
return "{self.artist} -- {self.title} -- {self.position}".format(self=self)
def save(self, *args, **kwargs):
if (self.sound is None and self.episode is None) or (self.sound is not None and self.episode is not None):
raise ValueError("sound XOR episode is required")
super().save(*args, **kwargs)

File diff suppressed because one or more lines are too long

View File

@ -8,8 +8,8 @@
{{ admin_formset.non_form_errors }} {{ admin_formset.non_form_errors }}
<a-tracklist-editor <a-tracklist-editor
:labels="{% track_inline_labels %}" :labels="{% inline_labels %}"
:init-data="{% track_inline_data formset=formset %}" :init-data="{% formset_inline_data formset=formset %}"
settings-url="{% url "api:user-settings" %}" settings-url="{% url "api:user-settings" %}"
data-prefix="{{ formset.prefix }}-"> data-prefix="{{ formset.prefix }}-">
<template #title> <template #title>

View File

@ -11,15 +11,15 @@ Context:
{% load aircox %} {% load aircox %}
{% if field.is_hidden or hidden %} {% if field.is_hidden or hidden %}
<input type="hidden" name="{{ name }}" {% if vbind %}:value{% else %}value{% endif %}="{{ value|default:"" }}"> <input type="hidden" name="{{ name }}" value="{{ value|default:"" }}">
{% elif field|is_checkbox %} {% elif field|is_checkbox %}
<input type="checkbox" class="checkbox" name="{{ name }}" {% if vbind %}:checked="{{ value }}"{% elif value %}checked{% endif %}> <input type="checkbox" class="checkbox" name="{{ name }}" {% if value %}checked{% endif %}>
{% elif field|is_select %} {% elif field|is_select %}
<select name="{{ name }}" class="select" {% if vbind %}:value{% else %}value{% endif %}="{{ value|default:"" }}"> <select name="{{ name }}" class="select" value="{{ value|default:"" }}">
{% for value, label in field.widget.choices %} {% for value, label in field.widget.choices %}
<option value="{{ value }}">{{ label }}</option> <option value="{{ value }}">{{ label }}</option>
{% endfor %} {% endfor %}
</select> </select>
{% else %} {% else %}
<input type="text" class="input" name="{{ name }}" {% if vbind %}:value{% else %}value{% endif %}="{{ value|default:"" }}"> <input type="text" class="input" name="{{ name }}" value="{{ value|default:"" }}">
{% endif %} {% endif %}

View File

@ -29,7 +29,12 @@ Context:
<input type="hidden" name="{{ formset.prefix }}-TOTAL_FORMS" <input type="hidden" name="{{ formset.prefix }}-TOTAL_FORMS"
:value="items.length || 0"/> :value="items.length || 0"/>
<input type="hidden" name="{{ formset.prefix }}-INITIAL_FORMS" <input type="hidden" name="{{ formset.prefix }}-INITIAL_FORMS"
value="{{ formset.initial_form_count }}"/> {% if no_initial_form_count %}
:value="items.length || 0"
{% else %}
value="{{ formset.initial_form_count }}"
{% endif %}
/>
<input type="hidden" name="{{ formset.prefix }}-MIN_NUM_FORMS" <input type="hidden" name="{{ formset.prefix }}-MIN_NUM_FORMS"
value="{{ formset.min_num }}"/> value="{{ formset.min_num }}"/>
<input type="hidden" name="{{ formset.prefix }}-MAX_NUM_FORMS" <input type="hidden" name="{{ formset.prefix }}-MAX_NUM_FORMS"
@ -71,11 +76,13 @@ Context:
{% if not field.widget.is_hidden and not field.is_readonly %} {% if not field.widget.is_hidden and not field.is_readonly %}
<template v-slot:row-{{ name }}="{item,cell,value,attr,emit}"> <template v-slot:row-{{ name }}="{item,cell,value,attr,emit}">
<div class="field"> <div class="field">
{% with full_name="'"|add:formset.prefix|add:"-' + cell.row + '-"|add:name|add:"'" %}
{% block row-field %} {% block row-field %}
<div class="control"> <div class="control">
{% include "./form_field.html" with value="item.data."|add:name vbind=1 %} {% include "./v_form_field.html" with value="item.data."|add:name name=full_name %}
</div> </div>
{% endblock %} {% endblock %}
{% endwith %}
<p v-for="error in item.error(attr)" class="help is-danger"> <p v-for="error in item.error(attr)" class="help is-danger">
[[ error ]] ! [[ error ]] !
</p> </p>

View File

@ -1,11 +1,13 @@
{% extends "./list_editor.html" %} {% extends "./list_editor.html" %}
{% block outer %} {% block outer %}
{% with no_initial_form_count=True %}
{% with tag_id="inline-sounds" %} {% with tag_id="inline-sounds" %}
{% with tag="a-sound-list-editor" %} {% with tag="a-sound-list-editor" %}
{{ block.super }} {{ block.super }}
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
{% endwith %}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,24 @@
{% comment %}
Render a form field instance as field (to be used when no model instance is provided). Value is binded as vue, class to Bulma
Context:
- name: field name
- field: form field
- value: input ":v-model" attribute
- hidden: if True, hidden field
{% endcomment %}
{% load aircox %}
{% if field.is_hidden or hidden %}
<input type="hidden" :name="{{ name }}" :value="{{ value|default:"" }}">
{% elif field|is_checkbox %}
<input type="checkbox" class="checkbox" :name="{{ name }}" v-model="{{ value }}">
{% elif field|is_select %}
<select :name="{{ name }}" class="select" v-model="{{ value|default:"" }}">
{% for value, label in field.widget.choices %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
{% else %}
<input type="text" class="input" :name="{{ name }}" v-model="{{ value|default:"" }}">
{% endif %}

View File

@ -64,6 +64,7 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
def get_tracklist_formset(self, episode, **kwargs): def get_tracklist_formset(self, episode, **kwargs):
kwargs.update( kwargs.update(
{ {
"prefix": "tracks",
"queryset": self.get_tracklist_queryset(episode), "queryset": self.get_tracklist_queryset(episode),
"initial": { "initial": {
"episode": episode.id, "episode": episode.id,
@ -78,6 +79,7 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
def get_soundlist_formset(self, episode, **kwargs): def get_soundlist_formset(self, episode, **kwargs):
kwargs.update( kwargs.update(
{ {
"prefix": "sounds",
"queryset": self.get_soundlist_queryset(episode), "queryset": self.get_soundlist_queryset(episode),
"initial": { "initial": {
"program": episode.parent_id, "program": episode.parent_id,
@ -102,20 +104,29 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
return forms.SoundCreateForm(**kwargs) return forms.SoundCreateForm(**kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs.update( forms = (
{ ("soundlist_formset", self.get_soundlist_formset),
"soundlist_formset": self.get_soundlist_formset(self.object), ("tracklist_formset", self.get_tracklist_formset),
"tracklist_formset": self.get_tracklist_formset(self.object), ("sound_form", self.get_sound_form),
"sound_form": self.get_sound_form(self.object),
}
) )
for key, func in forms:
if key not in kwargs:
kwargs[key] = func(self.object)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
super().post(request, *args, **kwargs) resp = super().post(request, *args, **kwargs)
formset = self.get_formset(request.POST)
if formset.is_valid(): formsets = {
formset.save() "soundlist_formset": self.get_soundlist_formset(self.object, data=request.POST),
return super().form_valid(formset) "tracklist_formset": self.get_tracklist_formset(self.object, data=request.POST),
else: }
return super().form_valid(formset) # form_invalid(formset) invalid = False
for formset in formsets.values():
if not formset.is_valid():
invalid = True
else:
formset.save()
if invalid:
return self.get(request, **formsets)
return resp

View File

@ -196,7 +196,6 @@ class PageUpdateView(BaseView, UpdateView):
context_object_name = "page" context_object_name = "page"
template_name = "aircox/page_form.html" template_name = "aircox/page_form.html"
# FIXME: remove?
def get_page(self): def get_page(self):
return self.object return self.object

View File

@ -42,6 +42,12 @@ class SoundViewSet(BaseAPIView, viewsets.ModelViewSet):
filter_backends = (drf_filters.DjangoFilterBackend,) filter_backends = (drf_filters.DjangoFilterBackend,)
filterset_class = filters.SoundFilterSet filterset_class = filters.SoundFilterSet
def perform_create(self, serializer):
obj = serializer.save()
# FIXME: hack to avoid "TYPE_REMOVED" status
# -> file is saved to fs after object is saved to db
obj.save()
class TrackROViewSet(viewsets.ReadOnlyModelViewSet): class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
"""Track viewset used for auto completion.""" """Track viewset used for auto completion."""

View File

@ -3,7 +3,6 @@
<a-modal ref="modal" :title="labels && labels.add_sound"> <a-modal ref="modal" :title="labels && labels.add_sound">
<template #default> <template #default>
<a-file-upload ref="file-upload" :url="soundUploadUrl" :label="labels.select_file" submitLabel="" @load="uploadDone" <a-file-upload ref="file-upload" :url="soundUploadUrl" :label="labels.select_file" submitLabel="" @load="uploadDone"
> >
<template #preview="{upload}"> <template #preview="{upload}">
<slot name="upload-preview" :upload="upload"></slot> <slot name="upload-preview" :upload="upload"></slot>
@ -24,6 +23,7 @@
</template> </template>
</a-modal> </a-modal>
<slot name="top" :set="set" :items="set.items"></slot>
<a-rows :set="set" :columns="columns" <a-rows :set="set" :columns="columns"
:labels="initData.fields" :allow-create="true" :orderable="true" :labels="initData.fields" :allow-create="true" :orderable="true"
@move="listItemMove"> @move="listItemMove">

View File

@ -54,11 +54,13 @@ window.aircox = {
} }
}, },
onKeyPress(event) { onKeyPress(/*event*/) {
/*
if(event.key == " ") { if(event.key == " ") {
this.player.togglePlay() this.player.togglePlay()
event.stopPropagation() event.stopPropagation()
} }
*/
}, },
/** /**