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

View File

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

View File

@ -80,6 +80,7 @@ TrackFormSet = modelformset_factory(
"tags",
"album",
],
can_delete=True,
extra=0,
)
"""Track formset used in EpisodeUpdateView."""
@ -94,6 +95,7 @@ SoundFormSet = modelformset_factory(
"is_downloadable",
"duration",
],
can_delete=True,
extra=0,
)
"""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 .program import Program, ProgramChildQuerySet, ProgramQuerySet, Stream
from .schedule import Schedule
from .sound import Sound, SoundQuerySet, Track
from .sound import Sound, SoundQuerySet
from .station import Port, Station, StationQuerySet
from .track import Track
from .user_settings import UserSettings
__all__ = (

View File

@ -62,6 +62,7 @@ class Episode(Page):
podcast["name"] = f"{self.title} - {archive_index}"
else:
podcast["name"] = self.title
archive_index += 1
podcasts[index]["cover"] = cover
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 .diffusion import Diffusion
from .sound import Sound, Track
from .sound import Sound
from .station import Station
from .track import Track
from .page import Renderable
logger = logging.getLogger("aircox")

View File

@ -1,3 +1,5 @@
import os
from django.contrib.auth.models import Group, Permission, User
from django.db import transaction
from django.db.models import signals
@ -11,6 +13,7 @@ from .episode import Episode
from .page import Page
from .program import Program
from .schedule import Schedule
from .sound import Sound
# 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)
def diffusion_post_delete(sender, instance, *args, **kwargs):
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.utils import timezone as tz
from django.utils.translation import gettext_lazy as _
from taggit.managers import TaggableManager
from aircox.conf import settings
@ -16,7 +15,7 @@ from .program import Program
logger = logging.getLogger("aircox")
__all__ = ("Sound", "SoundQuerySet", "Track")
__all__ = ("Sound", "SoundQuerySet")
class SoundQuerySet(models.QuerySet):
@ -33,7 +32,7 @@ class SoundQuerySet(models.QuerySet):
return self.filter(episode__diffusion__id=id)
def available(self):
return self.exclude(type=Sound.TYPE_REMOVED)
return self.exclude(is_removed=False)
def public(self):
"""Return sounds available as podcasts."""
@ -85,12 +84,10 @@ class Sound(models.Model):
TYPE_OTHER = 0x00
TYPE_ARCHIVE = 0x01
TYPE_EXCERPT = 0x02
TYPE_REMOVED = 0x03
TYPE_CHOICES = (
(TYPE_OTHER, _("other")),
(TYPE_ARCHIVE, _("archive")),
(TYPE_EXCERPT, _("excerpt")),
(TYPE_REMOVED, _("removed")),
)
name = models.CharField(_("name"), max_length=64)
@ -116,6 +113,7 @@ class Sound(models.Model):
default=0,
help_text=_("position in the playlist"),
)
is_removed = models.BooleanField(_("removed"), default=False, help_text=_("file has been removed"))
def _upload_to(self, filename):
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.
"""
if not self.file_exists():
if self.type == self.TYPE_REMOVED:
if self.is_removed:
return
logger.debug("sound %s: has been removed", self.file.name)
self.type = self.TYPE_REMOVED
self.is_removed = True
return True
# not anymore removed
changed = False
if self.type == self.TYPE_REMOVED and self.program:
if self.is_removed and self.program:
changed = True
self.type = (
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):
super().__init__(*args, **kwargs)
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 }}
<a-tracklist-editor
:labels="{% track_inline_labels %}"
:init-data="{% track_inline_data formset=formset %}"
:labels="{% inline_labels %}"
:init-data="{% formset_inline_data formset=formset %}"
settings-url="{% url "api:user-settings" %}"
data-prefix="{{ formset.prefix }}-">
<template #title>

View File

@ -11,15 +11,15 @@ Context:
{% load aircox %}
{% 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 %}
<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 %}
<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 %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
{% 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 %}

View File

@ -29,7 +29,12 @@ Context:
<input type="hidden" name="{{ formset.prefix }}-TOTAL_FORMS"
:value="items.length || 0"/>
<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"
value="{{ formset.min_num }}"/>
<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 %}
<template v-slot:row-{{ name }}="{item,cell,value,attr,emit}">
<div class="field">
{% with full_name="'"|add:formset.prefix|add:"-' + cell.row + '-"|add:name|add:"'" %}
{% block row-field %}
<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>
{% endblock %}
{% endwith %}
<p v-for="error in item.error(attr)" class="help is-danger">
[[ error ]] !
</p>

View File

@ -1,11 +1,13 @@
{% extends "./list_editor.html" %}
{% block outer %}
{% with no_initial_form_count=True %}
{% with tag_id="inline-sounds" %}
{% with tag="a-sound-list-editor" %}
{{ block.super }}
{% endwith %}
{% endwith %}
{% endwith %}
{% 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):
kwargs.update(
{
"prefix": "tracks",
"queryset": self.get_tracklist_queryset(episode),
"initial": {
"episode": episode.id,
@ -78,6 +79,7 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
def get_soundlist_formset(self, episode, **kwargs):
kwargs.update(
{
"prefix": "sounds",
"queryset": self.get_soundlist_queryset(episode),
"initial": {
"program": episode.parent_id,
@ -102,20 +104,29 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
return forms.SoundCreateForm(**kwargs)
def get_context_data(self, **kwargs):
kwargs.update(
{
"soundlist_formset": self.get_soundlist_formset(self.object),
"tracklist_formset": self.get_tracklist_formset(self.object),
"sound_form": self.get_sound_form(self.object),
}
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)
return super().get_context_data(**kwargs)
def post(self, request, *args, **kwargs):
super().post(request, *args, **kwargs)
formset = self.get_formset(request.POST)
if formset.is_valid():
formset.save()
return super().form_valid(formset)
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:
return super().form_valid(formset) # form_invalid(formset)
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"
template_name = "aircox/page_form.html"
# FIXME: remove?
def get_page(self):
return self.object

View File

@ -42,6 +42,12 @@ class SoundViewSet(BaseAPIView, viewsets.ModelViewSet):
filter_backends = (drf_filters.DjangoFilterBackend,)
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):
"""Track viewset used for auto completion."""

View File

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

View File

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