work on sound list
This commit is contained in:
parent
f41cc3ce0c
commit
70a55607a5
|
@ -53,6 +53,12 @@ class SoundFilterSet(filters.FilterSet):
|
||||||
episode = filters.NumberFilter(field_name="episode_id")
|
episode = filters.NumberFilter(field_name="episode_id")
|
||||||
search = filters.CharFilter(field_name="search", method="search_filter")
|
search = filters.CharFilter(field_name="search", method="search_filter")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Sound
|
||||||
|
fields = {
|
||||||
|
"episode": ["in", "exact", "isnull"],
|
||||||
|
}
|
||||||
|
|
||||||
def search_filter(self, queryset, name, value):
|
def search_filter(self, queryset, name, value):
|
||||||
return queryset.search(value)
|
return queryset.search(value)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.forms.models import modelformset_factory
|
||||||
from aircox import models
|
from aircox import models
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("CommentForm", "PageForm", "ProgramForm", "EpisodeForm", "SoundForm", "TrackFormSet")
|
__all__ = ("CommentForm", "PageForm", "ProgramForm", "EpisodeForm", "SoundForm", "SoundFormSet", "TrackFormSet")
|
||||||
|
|
||||||
|
|
||||||
class CommentForm(forms.ModelForm):
|
class CommentForm(forms.ModelForm):
|
||||||
|
@ -60,7 +60,15 @@ class SoundForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Sound
|
model = models.Sound
|
||||||
fields = ["name", "program", "episode", "type", "position", "duration", "is_public", "is_downloadable"]
|
fields = ["name", "program", "episode", "file", "type", "position", "duration", "is_public", "is_downloadable"]
|
||||||
|
|
||||||
|
|
||||||
|
class SoundCreateForm(forms.ModelForm):
|
||||||
|
"""SoundForm used in EpisodeUpdateView."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Sound
|
||||||
|
fields = ["name", "episode", "program", "file", "type", "is_public", "is_downloadable"]
|
||||||
|
|
||||||
|
|
||||||
TrackFormSet = modelformset_factory(
|
TrackFormSet = modelformset_factory(
|
||||||
|
@ -84,6 +92,7 @@ SoundFormSet = modelformset_factory(
|
||||||
"type",
|
"type",
|
||||||
"is_public",
|
"is_public",
|
||||||
"is_downloadable",
|
"is_downloadable",
|
||||||
|
"duration",
|
||||||
],
|
],
|
||||||
extra=0,
|
extra=0,
|
||||||
)
|
)
|
||||||
|
|
File diff suppressed because one or more lines are too long
25
aircox/templates/aircox/dashboard/form_field.html
Normal file
25
aircox/templates/aircox/dashboard/form_field.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{% 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 ":value" attribute
|
||||||
|
- vbind: if True, use ":value" instead of "value"
|
||||||
|
- hidden: if True, hidden field
|
||||||
|
{% endcomment %}
|
||||||
|
{% load aircox %}
|
||||||
|
|
||||||
|
{% if field.is_hidden or hidden %}
|
||||||
|
<input type="hidden" name="{{ name }}" {% if vbind %}:value{% else %}value{% endif %}="{{ value|default:"" }}">
|
||||||
|
{% elif field|is_checkbox %}
|
||||||
|
<input type="checkbox" class="checkbox" name="{{ name }}" {% if vbind %}:checked="{{ value }}"{% elif value %}checked{% endif %}>
|
||||||
|
{% elif field|is_select %}
|
||||||
|
<select name="{{ name }}" class="select" {% if vbind %}:value{% else %}value{% endif %}="{{ 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:"" }}">
|
||||||
|
{% endif %}
|
|
@ -1,24 +1,31 @@
|
||||||
{% comment %}
|
{% comment %}
|
||||||
|
Base template for list editor based on formsets (tracklist_editor, playlist_editor).
|
||||||
|
|
||||||
Context:
|
Context:
|
||||||
- formset: formset
|
- tag_id: id of parent component
|
||||||
|
- tag: vue component tag (a-playlist-editor, etc.)
|
||||||
|
- formset: formset used to render the list editor
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load aircox aircox_admin static i18n %}
|
{% load aircox aircox_admin static i18n %}
|
||||||
|
|
||||||
{% with formset.form.base_fields as fields %}
|
{% with formset.form.base_fields as fields %}
|
||||||
<div id="inline-sounds">
|
{% block outer %}
|
||||||
|
<div id="{{ tag_id }}">
|
||||||
{{ formset.non_form_errors }}
|
{{ formset.non_form_errors }}
|
||||||
<!-- formset.management_form -->
|
<!-- formset.management_form -->
|
||||||
|
|
||||||
<a-playlist-editor
|
<{{ tag }}
|
||||||
|
{% block tag-attrs %}
|
||||||
:labels="{% inline_labels %}"
|
:labels="{% inline_labels %}"
|
||||||
:init-data="{% formset_inline_data formset=formset %}"
|
:init-data="{% formset_inline_data formset=formset %}"
|
||||||
:default-columns="[{% for f in fields.keys %}{% if f != "position" %}'{{ f }}',{% endif %}{% endfor %}]"
|
:default-columns="[{% for f in fields.keys %}{% if f != "position" %}'{{ f }}',{% endif %}{% endfor %}]"
|
||||||
settings-url="{% url "api:user-settings" %}"
|
settings-url="{% url "api:user-settings" %}"
|
||||||
data-prefix="{{ formset.prefix }}-">
|
data-prefix="{{ formset.prefix }}-"
|
||||||
<template #title>
|
{% endblock %}>
|
||||||
<h3 class="title is-2">{% trans "Playlist" %}</h3>
|
{% block inner %}
|
||||||
</template>
|
|
||||||
<template #top="{items}">
|
<template #top="{items}">
|
||||||
|
{% block top %}
|
||||||
<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"
|
||||||
|
@ -27,16 +34,20 @@ Context:
|
||||||
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"
|
||||||
value="{{ formset.max_num }}"/>
|
value="{{ formset.max_num }}"/>
|
||||||
|
{% endblock %}
|
||||||
</template>
|
</template>
|
||||||
<template #rows-header-head>
|
<template #rows-header-head>
|
||||||
<th style="max-width:2em" title="{% trans "Sound Position" %}"
|
{% block rows-header-head %}
|
||||||
aria-description="{% trans "Sound Position" %}">
|
<th style="max-width:2em" title="{{ fields.position.help_text }}"
|
||||||
|
aria-description="{{ fields.position.help_text }}">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fa fa-arrow-down-1-9"></i>
|
<i class="fa fa-arrow-down-1-9"></i>
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
|
{% endblock %}
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:row-head="{item,row}">
|
<template v-slot:row-head="{item,row}">
|
||||||
|
{% block row-head %}
|
||||||
<td>
|
<td>
|
||||||
[[ row+1 ]]
|
[[ row+1 ]]
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
|
@ -54,14 +65,17 @@ Context:
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
|
{% endblock %}
|
||||||
</template>
|
</template>
|
||||||
{% for name, field in fields.items %}
|
{% for name, field in fields.items %}
|
||||||
{% 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">
|
||||||
|
{% block row-field %}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{% include "./form_field.html" with field=field name=name value="item.data."|add:name %}
|
{% include "./form_field.html" with value="item.data."|add:name vbind=1 %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
<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>
|
||||||
|
@ -69,6 +83,8 @@ Context:
|
||||||
</template>
|
</template>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</a-playlist-editor>
|
{% endblock %}
|
||||||
|
</{{ tag }}>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
{% endwith %}
|
{% endwith %}
|
40
aircox/templates/aircox/dashboard/soundlist_editor.html
Normal file
40
aircox/templates/aircox/dashboard/soundlist_editor.html
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{% extends "./list_editor.html" %}
|
||||||
|
|
||||||
|
{% block outer %}
|
||||||
|
{% with tag_id="inline-sounds" %}
|
||||||
|
{% with tag="a-sound-list-editor" %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block tag-attrs %}
|
||||||
|
{{ block.super }}
|
||||||
|
sound-list-url="{% url "api:sound-list" %}?program={{ object.pk }}&episode__isnull"
|
||||||
|
sound-upload-url="{% url "api:sound-list" %}"
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block inner %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
<template #upload-form>
|
||||||
|
{% for field in sound_form %}
|
||||||
|
{% with field.name as name %}
|
||||||
|
{% with field.initial as value %}
|
||||||
|
{% with field.field as field %}
|
||||||
|
{% if name in "episode,program" %}
|
||||||
|
{% include "./form_field.html" with value=value hidden=True %}
|
||||||
|
{% elif name != "file" %}
|
||||||
|
<div class="field is-horizontal">
|
||||||
|
<label class="label mr-3">{{ field.label }}</label>
|
||||||
|
{% include "./form_field.html" with value=value %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
{% endblock %}
|
19
aircox/templates/aircox/dashboard/tracklist_editor.html
Normal file
19
aircox/templates/aircox/dashboard/tracklist_editor.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "./list_editor.html" %}
|
||||||
|
|
||||||
|
{% block outer %}
|
||||||
|
{% with tag_id="inline-tracks" %}
|
||||||
|
{% with tag="a-track-list-editor" %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block row-field %}
|
||||||
|
<a-autocomplete
|
||||||
|
:input-class="['input', item.error(attr) ? 'is-danger' : 'half-field']"
|
||||||
|
url="{% url 'api:track-autocomplete' %}?{{ name }}=${query}&field={{ name }}"
|
||||||
|
:name="'{{ formset.prefix }}-' + cell.row + '-{{ name }}'"
|
||||||
|
v-model="item.data[attr]"
|
||||||
|
title="{{ name }}"
|
||||||
|
@change="emit('change', col)"/>
|
||||||
|
{% endblock %}
|
|
@ -2,41 +2,15 @@
|
||||||
{% load static i18n humanize honeypot aircox %}
|
{% load static i18n humanize honeypot aircox %}
|
||||||
|
|
||||||
{% block page_form %}
|
{% block page_form %}
|
||||||
<a-modal ref="sound-edit-modal" v-if="item" title="{% translate "Edit sound" %}">
|
|
||||||
<template #default="{item}">
|
|
||||||
{% for field in sound_form %}
|
|
||||||
{% if field.name in "episode,program" %}
|
|
||||||
<input type="hidden" name="{{ field.name }}" value="{{ field.value }}" />
|
|
||||||
{% else %}
|
|
||||||
<div class="field">
|
|
||||||
{% if field|is_checkbox %}
|
|
||||||
<label class="label">
|
|
||||||
<input type="text" name="{{ field.name }}" :value="item.{{ field.name }}">
|
|
||||||
{{ field.label }}
|
|
||||||
</label>
|
|
||||||
{% else %}
|
|
||||||
<label class="label">{{ field.label }}</label>
|
|
||||||
<div class="control">
|
|
||||||
<input type="text" name="{{ field.name }}" :value="item.{{ field.name }}">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p class="help">{{ field.help_text }}</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</template>
|
|
||||||
</a-modal>
|
|
||||||
|
|
||||||
<a-episode :page="{title: "{{ object.title }}", podcasts: {{ object.sounds|json }}}">
|
<a-episode :page="{title: "{{ object.title }}", podcasts: {{ object.sounds|json }}}">
|
||||||
<template v-slot="{podcasts,page}">
|
<template v-slot="{podcasts,page}">
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<hr/>
|
<hr/>
|
||||||
{% include "./widgets/tracklist_editor.html" with formset=tracklist_formset %}
|
{% include "./dashboard/tracklist_editor.html" with formset=tracklist_formset %}
|
||||||
<hr/>
|
<hr/>
|
||||||
<section class="container">
|
<section class="container">
|
||||||
<h3 class="title">{% translate "Sound files" %}</h3>
|
<h3 class="title">{% translate "Sound files" %}</h3>
|
||||||
{% include "./widgets/playlist_editor.html" with formset=playlist_formset %}
|
{% include "./dashboard/soundlist_editor.html" with formset=soundlist_formset %}
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</a-episode>
|
</a-episode>
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
{% 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 ":value" attribute
|
|
||||||
{% endcomment %}
|
|
||||||
{% load aircox %}
|
|
||||||
|
|
||||||
{% if field|is_checkbox %}
|
|
||||||
<input type="checkbox" class="checkbox" name="{{ name }}" :checked="{{ value }}">
|
|
||||||
{% elif field|is_select %}
|
|
||||||
<select name="{{ name }}" class="select" :value="{{ value }}">
|
|
||||||
{% for value, label in field.widget.choices %}
|
|
||||||
<option value="{{ value }}">{{ label }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
{% else %}
|
|
||||||
<input type="text" class="input" name="{{ name }}" :value="{{ value }}">
|
|
||||||
{% endif %}
|
|
|
@ -1,80 +0,0 @@
|
||||||
{% comment %}
|
|
||||||
Context:
|
|
||||||
- formset: playlist's track formset
|
|
||||||
|
|
||||||
{% endcomment %}
|
|
||||||
{% load aircox aircox_admin static i18n %}
|
|
||||||
|
|
||||||
{% with formset.form.base_fields as fields %}
|
|
||||||
<div id="inline-tracks">
|
|
||||||
{{ formset.non_form_errors }}
|
|
||||||
<!-- formset.management_form -->
|
|
||||||
|
|
||||||
<a-tracklist-editor
|
|
||||||
:labels="{% inline_labels %}"
|
|
||||||
:init-data="{% formset_inline_data formset=formset %}"
|
|
||||||
:default-columns="[{% for f in fields.keys %}{% if f != "position" %}'{{ f }}',{% endif %}{% endfor %}]"
|
|
||||||
settings-url="{% url "api:user-settings" %}"
|
|
||||||
data-prefix="{{ formset.prefix }}-">
|
|
||||||
<template #title>
|
|
||||||
<h3 class="title is-2">{% trans "Playlist" %}</h3>
|
|
||||||
</template>
|
|
||||||
<template #top="{items}">
|
|
||||||
<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 }}"/>
|
|
||||||
<input type="hidden" name="{{ formset.prefix }}-MIN_NUM_FORMS"
|
|
||||||
value="{{ formset.min_num }}"/>
|
|
||||||
<input type="hidden" name="{{ formset.prefix }}-MAX_NUM_FORMS"
|
|
||||||
value="{{ formset.max_num }}"/>
|
|
||||||
</template>
|
|
||||||
<template #rows-header-head>
|
|
||||||
<th style="max-width:2em" title="{% trans "Track Position" %}"
|
|
||||||
aria-description="{% trans "Track Position" %}">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="fa fa-arrow-down-1-9"></i>
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
</template>
|
|
||||||
<template v-slot:row-head="{item,row}">
|
|
||||||
<td>
|
|
||||||
[[ row+1 ]]
|
|
||||||
<input type="hidden"
|
|
||||||
:name="'{{ formset.prefix }}-' + row + '-position'"
|
|
||||||
:value="row"/>
|
|
||||||
<input t-if="item.data.id" type="hidden"
|
|
||||||
:name="'{{ formset.prefix }}-' + row + '-id'"
|
|
||||||
:value="item.data.id || item.id"/>
|
|
||||||
|
|
||||||
{% for name, field in fields.items %}
|
|
||||||
{% if name != 'position' and field.widget.is_hidden %}
|
|
||||||
<input type="hidden"
|
|
||||||
:name="'{{ formset.prefix }}-' + row + '-{{ name }}'"
|
|
||||||
v-model="item.data[attr]"/>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
</template>
|
|
||||||
{% for name, field in fields.items %}
|
|
||||||
{% if not field.widget.is_hidden and not field.is_readonly %}
|
|
||||||
---
|
|
||||||
<template v-slot:row-{{ name }}="{item,cell,value,attr,emit}">
|
|
||||||
<div class="field">
|
|
||||||
<a-autocomplete
|
|
||||||
:input-class="['input', item.error(attr) ? 'is-danger' : 'half-field']"
|
|
||||||
url="{% url 'api:track-autocomplete' %}?{{ name }}=${query}&field={{ name }}"
|
|
||||||
:name="'{{ formset.prefix }}-' + cell.row + '-{{ name }}'"
|
|
||||||
v-model="item.data[attr]"
|
|
||||||
title="{{ name }}"
|
|
||||||
@change="emit('change', col)"/>
|
|
||||||
<p v-for="error in item.error(attr)" class="help is-danger">
|
|
||||||
[[ error ]] !
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</a-tracklist-editor>
|
|
||||||
</div>
|
|
||||||
{% endwith %}
|
|
|
@ -40,6 +40,10 @@ def do_formset_inline_data(context, formset):
|
||||||
item = {name: form[name].value() for name in form.fields.keys()}
|
item = {name: form[name].value() for name in form.fields.keys()}
|
||||||
item["__errors__"] = form.errors
|
item["__errors__"] = form.errors
|
||||||
|
|
||||||
|
# hack for sound list
|
||||||
|
if duration := item.get("duration"):
|
||||||
|
item["duration"] = duration.strftime("%H:%M")
|
||||||
|
|
||||||
# hack for playlist editor
|
# hack for playlist editor
|
||||||
tags = item.get("tags")
|
tags = item.get("tags")
|
||||||
if tags and not isinstance(tags, str):
|
if tags and not isinstance(tags, str):
|
||||||
|
@ -60,9 +64,13 @@ inline_labels_ = {
|
||||||
"remove_item": _("Remove"),
|
"remove_item": _("Remove"),
|
||||||
"save_settings": _("Save Settings"),
|
"save_settings": _("Save Settings"),
|
||||||
"discard_changes": _("Discard changes"),
|
"discard_changes": _("Discard changes"),
|
||||||
|
"select_file": _("Select a file"),
|
||||||
|
"submit": _("Submit"),
|
||||||
# track list
|
# track list
|
||||||
"columns": _("Columns"),
|
"columns": _("Columns"),
|
||||||
"timestamp": _("Timestamp"),
|
"timestamp": _("Timestamp"),
|
||||||
|
# sound list
|
||||||
|
"add_sound": _("Add a sound"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from aircox.models import Episode, Program, StaticPage, Track
|
from aircox.models import Episode, Program, StaticPage, Sound, Track
|
||||||
from aircox import forms
|
from aircox import forms
|
||||||
from ..filters import EpisodeFilters
|
from ..filters import EpisodeFilters
|
||||||
from .page import PageListView
|
from .page import PageListView
|
||||||
|
@ -72,13 +72,13 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
|
||||||
)
|
)
|
||||||
return forms.TrackFormSet(**kwargs)
|
return forms.TrackFormSet(**kwargs)
|
||||||
|
|
||||||
def get_playlist_queryset(self, episode):
|
def get_soundlist_queryset(self, episode):
|
||||||
return episode.sound_set.all()
|
return episode.sound_set.all()
|
||||||
|
|
||||||
def get_playlist_formset(self, episode, **kwargs):
|
def get_soundlist_formset(self, episode, **kwargs):
|
||||||
kwargs.update(
|
kwargs.update(
|
||||||
{
|
{
|
||||||
"queryset": self.get_playlist_queryset(episode),
|
"queryset": self.get_soundlist_queryset(episode),
|
||||||
"initial": {
|
"initial": {
|
||||||
"program": episode.parent_id,
|
"program": episode.parent_id,
|
||||||
"episode": episode.id,
|
"episode": episode.id,
|
||||||
|
@ -87,11 +87,26 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
|
||||||
)
|
)
|
||||||
return forms.SoundFormSet(**kwargs)
|
return forms.SoundFormSet(**kwargs)
|
||||||
|
|
||||||
|
def get_sound_form(self, episode, **kwargs):
|
||||||
|
kwargs.update(
|
||||||
|
{
|
||||||
|
"initial": {
|
||||||
|
"program": episode.parent_id,
|
||||||
|
"episode": episode.pk,
|
||||||
|
"name": episode.title,
|
||||||
|
"is_public": True,
|
||||||
|
"type": Sound.TYPE_ARCHIVE,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return forms.SoundCreateForm(**kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs.update(
|
kwargs.update(
|
||||||
{
|
{
|
||||||
"playlist_formset": self.get_playlist_formset(self.object),
|
"soundlist_formset": self.get_soundlist_formset(self.object),
|
||||||
"tracklist_formset": self.get_tracklist_formset(self.object),
|
"tracklist_formset": self.get_tracklist_formset(self.object),
|
||||||
|
"sound_form": self.get_sound_form(self.object),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
from django_filters import rest_framework as drf_filters
|
from django_filters import rest_framework as drf_filters
|
||||||
from rest_framework import status, viewsets
|
from rest_framework import status, viewsets, parsers, permissions
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.parsers import MultiPartParser
|
|
||||||
|
|
||||||
from filer.models.imagemodels import Image
|
from filer.models.imagemodels import Image
|
||||||
|
|
||||||
|
@ -20,7 +18,7 @@ __all__ = (
|
||||||
|
|
||||||
|
|
||||||
class ImageViewSet(viewsets.ModelViewSet):
|
class ImageViewSet(viewsets.ModelViewSet):
|
||||||
parsers = (MultiPartParser,)
|
parsers = (parsers.MultiPartParser,)
|
||||||
serializer_class = admin.ImageSerializer
|
serializer_class = admin.ImageSerializer
|
||||||
queryset = Image.objects.all().order_by("-uploaded_at")
|
queryset = Image.objects.all().order_by("-uploaded_at")
|
||||||
filter_backends = (drf_filters.DjangoFilterBackend,)
|
filter_backends = (drf_filters.DjangoFilterBackend,)
|
||||||
|
@ -37,6 +35,8 @@ class ImageViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
class SoundViewSet(BaseAPIView, viewsets.ModelViewSet):
|
class SoundViewSet(BaseAPIView, viewsets.ModelViewSet):
|
||||||
|
parsers = (parsers.MultiPartParser,)
|
||||||
|
permissions = (permissions.IsAuthenticatedOrReadOnly,)
|
||||||
serializer_class = SoundSerializer
|
serializer_class = SoundSerializer
|
||||||
queryset = models.Sound.objects.available().order_by("-pk")
|
queryset = models.Sound.objects.available().order_by("-pk")
|
||||||
filter_backends = (drf_filters.DjangoFilterBackend,)
|
filter_backends = (drf_filters.DjangoFilterBackend,)
|
||||||
|
@ -47,7 +47,7 @@ class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
"""Track viewset used for auto completion."""
|
"""Track viewset used for auto completion."""
|
||||||
|
|
||||||
serializer_class = admin.TrackSerializer
|
serializer_class = admin.TrackSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = (permissions.IsAuthenticated,)
|
||||||
filter_backends = (drf_filters.DjangoFilterBackend,)
|
filter_backends = (drf_filters.DjangoFilterBackend,)
|
||||||
filterset_class = filters.TrackFilterSet
|
filterset_class = filters.TrackFilterSet
|
||||||
queryset = models.Track.objects.all()
|
queryset = models.Track.objects.all()
|
||||||
|
@ -70,7 +70,7 @@ class UserSettingsViewSet(viewsets.ViewSet):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = admin.UserSettingsSerializer
|
serializer_class = admin.UserSettingsSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = (permissions.IsAuthenticated,)
|
||||||
|
|
||||||
def get_serializer(self, instance=None, **kwargs):
|
def get_serializer(self, instance=None, **kwargs):
|
||||||
return self.serializer_class(instance=instance, context={"user": self.request.user}, **kwargs)
|
return self.serializer_class(instance=instance, context={"user": self.request.user}, **kwargs)
|
||||||
|
|
110
assets/src/components/AFileUpload.vue
Normal file
110
assets/src/components/AFileUpload.vue
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
<template>
|
||||||
|
<div ref="list" class="a-select-file-list">
|
||||||
|
<form ref="form" class="flex-column" v-if="state == STATE.DEFAULT">
|
||||||
|
<slot name="form"></slot>
|
||||||
|
<div class="field is-horizontal">
|
||||||
|
<label class="label">{{ label }}</label>
|
||||||
|
<input type="file" ref="uploadFile" :name="fieldName" @change="onFileChange"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row align-right" v-if="submitLabel">
|
||||||
|
<button type="button" class="button small" @click="submit">
|
||||||
|
{{ submitLabel }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="flex-column" v-else>
|
||||||
|
<slot name="preview" :fileUrl="fileUrl" :file="file" :loaded="loaded" :total="total"></slot>
|
||||||
|
<div class="flex-row">
|
||||||
|
<progress :max="total" :value="loaded"/>
|
||||||
|
<button type="button" class="button small square ml-2" @click="abort">
|
||||||
|
<span class="icon small">
|
||||||
|
<i class="fa fa-close"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import {getCsrf} from "../model"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emit: ["fileChange", "load"],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
url: { type: String },
|
||||||
|
fieldName: { type: String, default: "file" },
|
||||||
|
label: { type: String, default: "Select a file" },
|
||||||
|
submitLabel: { type: String, default: "Upload" },
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
STATE: {
|
||||||
|
DEFAULT: 0,
|
||||||
|
UPLOADING: 1,
|
||||||
|
},
|
||||||
|
state: 0,
|
||||||
|
upload: {},
|
||||||
|
file: null,
|
||||||
|
fileUrl: null,
|
||||||
|
total: 0,
|
||||||
|
loaded: 0,
|
||||||
|
request: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
abort() {
|
||||||
|
this.request && this.request.abort()
|
||||||
|
},
|
||||||
|
|
||||||
|
onFileChange() {
|
||||||
|
const [file] = this.$refs.uploadFile.files
|
||||||
|
if(!file)
|
||||||
|
return
|
||||||
|
this._setUploadFile(file)
|
||||||
|
this.$emit("fileChange", {upload: this, file: this.file, fileUrl: this.fileUrl})
|
||||||
|
},
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
const req = new XMLHttpRequest()
|
||||||
|
req.open("POST", this.url)
|
||||||
|
req.upload.addEventListener("progress", (e) => this.onUploadProgress(e))
|
||||||
|
req.addEventListener("load", (e) => this.onUploadDone(e))
|
||||||
|
req.addEventListener("abort", (e) => this.onUploadDone(e))
|
||||||
|
req.addEventListener("error", (e) => this.onUploadDone(e))
|
||||||
|
|
||||||
|
const formData = new FormData(this.$refs.form);
|
||||||
|
formData.append('csrfmiddlewaretoken', getCsrf())
|
||||||
|
req.send(formData)
|
||||||
|
|
||||||
|
this._resetUpload(this.STATE.UPLOADING, false, req)
|
||||||
|
},
|
||||||
|
|
||||||
|
onUploadProgress(event) {
|
||||||
|
this.loaded = event.loaded
|
||||||
|
this.total = event.total
|
||||||
|
},
|
||||||
|
|
||||||
|
onUploadDone(event) {
|
||||||
|
this.$emit("load", event)
|
||||||
|
this._resetUpload(this.STATE.DEFAULT, true)
|
||||||
|
},
|
||||||
|
|
||||||
|
_setUploadFile(file) {
|
||||||
|
this.file = file
|
||||||
|
this.fileURL = file && URL.createObjectURL(file)
|
||||||
|
},
|
||||||
|
|
||||||
|
_resetUpload(state, resetFile=false, request=null) {
|
||||||
|
this.state = state
|
||||||
|
this.loaded = 0
|
||||||
|
this.total = 0
|
||||||
|
this.request = request
|
||||||
|
if(resetFile)
|
||||||
|
this.file = null
|
||||||
|
}
|
||||||
|
|
||||||
|
},}
|
||||||
|
</script>
|
|
@ -1,5 +1,29 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="a-playlist-editor">
|
<div class="a-playlist-editor">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
<template #form>
|
||||||
|
<slot name="upload-form"></slot>
|
||||||
|
</template>
|
||||||
|
</a-file-upload>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button type="button" class="button"
|
||||||
|
@click.stop="$refs['file-upload'].submit()">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-upload"></i>
|
||||||
|
</span>
|
||||||
|
<span>{{ labels.submit }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
<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">
|
||||||
|
@ -21,7 +45,7 @@
|
||||||
<span class="icon"><i class="fa fa-rotate" /></span>
|
<span class="icon"><i class="fa fa-rotate" /></span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="button square is-primary p-2"
|
<button type="button" class="button square is-primary p-2"
|
||||||
@click="this.set.push(new this.set.model())"
|
@click="$refs.modal.open()"
|
||||||
:title="labels.add_sound"
|
:title="labels.add_sound"
|
||||||
:aria-label="labels.add_sound"
|
:aria-label="labels.add_sound"
|
||||||
>
|
>
|
||||||
|
@ -38,16 +62,19 @@ import Model, {Set} from '../model'
|
||||||
|
|
||||||
// import AActionButton from './AActionButton'
|
// import AActionButton from './AActionButton'
|
||||||
import ARows from './ARows'
|
import ARows from './ARows'
|
||||||
// import AModal from "./AModal"
|
import AModal from "./AModal"
|
||||||
|
import AFileUpload from "./AFileUpload"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {ARows},
|
components: {ARows, AModal, AFileUpload},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
initData: Object,
|
initData: Object,
|
||||||
dataPrefix: String,
|
dataPrefix: String,
|
||||||
labels: Object,
|
labels: Object,
|
||||||
settingsUrl: String,
|
settingsUrl: String,
|
||||||
|
soundListUrl: String,
|
||||||
|
soundUploadUrl: String,
|
||||||
columns: {
|
columns: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => ['name', "type", 'is_public', 'is_downloadable']
|
default: () => ['name', "type", 'is_public', 'is_downloadable']
|
||||||
|
@ -89,6 +116,14 @@ export default {
|
||||||
// if(settings)
|
// if(settings)
|
||||||
// this.settingsSaved(settings)
|
// this.settingsSaved(settings)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
uploadDone(event) {
|
||||||
|
const req = event.target
|
||||||
|
if(req.status == 201) {
|
||||||
|
const item = JSON.parse(req.response)
|
||||||
|
this.set.push(item)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,12 @@ import ASoundItem from './ASoundItem'
|
||||||
import ASwitch from './ASwitch'
|
import ASwitch from './ASwitch'
|
||||||
|
|
||||||
import AModal from "./AModal"
|
import AModal from "./AModal"
|
||||||
|
import AFileUpload from "./AFileUpload"
|
||||||
import ASelectFile from "./ASelectFile"
|
import ASelectFile from "./ASelectFile"
|
||||||
import AStatistics from './AStatistics'
|
import AStatistics from './AStatistics'
|
||||||
import AStreamer from './AStreamer'
|
import AStreamer from './AStreamer'
|
||||||
import ATracklistEditor from './ATracklistEditor'
|
import ATrackListEditor from './ATrackListEditor'
|
||||||
import APlaylistEditor from './APlaylistEditor'
|
import ASoundListEditor from './ASoundListEditor'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core components
|
* Core components
|
||||||
|
@ -31,10 +32,10 @@ export default base
|
||||||
|
|
||||||
export const admin = {
|
export const admin = {
|
||||||
...base,
|
...base,
|
||||||
AStatistics, AStreamer, ATracklistEditor
|
AStatistics, AStreamer, ATrackListEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dashboard = {
|
export const dashboard = {
|
||||||
...base,
|
...base,
|
||||||
AActionButton, ASelectFile, AModal, ATracklistEditor, APlaylistEditor
|
AActionButton, AFileUpload, ASelectFile, AModal, ATrackListEditor, ASoundListEditor
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user