make EpisodeUpdateView work
This commit is contained in:
parent
1f716891ac
commit
8d4b4c5896
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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__ = (
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
72
aircox/models/track.py
Normal 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
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
||||
|
|
24
aircox/templates/aircox/dashboard/v_form_field.html
Normal file
24
aircox/templates/aircox/dashboard/v_form_field.html
Normal 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 %}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -54,11 +54,13 @@ window.aircox = {
|
|||
}
|
||||
},
|
||||
|
||||
onKeyPress(event) {
|
||||
onKeyPress(/*event*/) {
|
||||
/*
|
||||
if(event.key == " ") {
|
||||
this.player.togglePlay()
|
||||
event.stopPropagation()
|
||||
}
|
||||
*/
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue
Block a user