work on playlist & tracklist
This commit is contained in:
parent
21f856e731
commit
f41cc3ce0c
|
@ -1,12 +1,11 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.forms.models import modelformset_factory
|
||||||
|
|
||||||
from filer.models.filemodels import File
|
|
||||||
|
|
||||||
from aircox import models
|
from aircox import models
|
||||||
from aircox.controllers.sound_file import SoundFile
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("CommentForm", "PageForm", "ProgramForm", "EpisodeForm")
|
__all__ = ("CommentForm", "PageForm", "ProgramForm", "EpisodeForm", "SoundForm", "TrackFormSet")
|
||||||
|
|
||||||
|
|
||||||
class CommentForm(forms.ModelForm):
|
class CommentForm(forms.ModelForm):
|
||||||
|
@ -40,18 +39,52 @@ class ProgramForm(PageForm):
|
||||||
|
|
||||||
|
|
||||||
class EpisodeForm(PageForm):
|
class EpisodeForm(PageForm):
|
||||||
new_podcast = forms.FileField(required=False)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Episode
|
model = models.Episode
|
||||||
fields = PageForm.Meta.fields
|
fields = PageForm.Meta.fields
|
||||||
|
|
||||||
def save(self, commit=True):
|
|
||||||
file_obj = self.cleaned_data["new_podcast"]
|
# def save(self, commit=True):
|
||||||
if file_obj:
|
# file_obj = self.cleaned_data["new_podcast"]
|
||||||
obj, _ = File.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
|
# if file_obj:
|
||||||
sound_file = SoundFile(obj.path)
|
# obj, _ = File.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
|
||||||
sound_file.sync(
|
# sound_file = SoundFile(obj.path)
|
||||||
program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True
|
# sound_file.sync(
|
||||||
)
|
# program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True
|
||||||
super().save(commit=commit)
|
# )
|
||||||
|
# super().save(commit=commit)
|
||||||
|
|
||||||
|
|
||||||
|
class SoundForm(forms.ModelForm):
|
||||||
|
"""SoundForm used in EpisodeUpdateView."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Sound
|
||||||
|
fields = ["name", "program", "episode", "type", "position", "duration", "is_public", "is_downloadable"]
|
||||||
|
|
||||||
|
|
||||||
|
TrackFormSet = modelformset_factory(
|
||||||
|
models.Track,
|
||||||
|
fields=[
|
||||||
|
"position",
|
||||||
|
"artist",
|
||||||
|
"title",
|
||||||
|
"tags",
|
||||||
|
"album",
|
||||||
|
],
|
||||||
|
extra=0,
|
||||||
|
)
|
||||||
|
"""Track formset used in EpisodeUpdateView."""
|
||||||
|
|
||||||
|
SoundFormSet = modelformset_factory(
|
||||||
|
models.Sound,
|
||||||
|
fields=[
|
||||||
|
"position",
|
||||||
|
"name",
|
||||||
|
"type",
|
||||||
|
"is_public",
|
||||||
|
"is_downloadable",
|
||||||
|
],
|
||||||
|
extra=0,
|
||||||
|
)
|
||||||
|
"""Sound formset used in EpisodeUpdateView."""
|
||||||
|
|
|
@ -148,12 +148,12 @@ class Sound(models.Model):
|
||||||
)
|
)
|
||||||
is_public = models.BooleanField(
|
is_public = models.BooleanField(
|
||||||
_("public"),
|
_("public"),
|
||||||
help_text=_("whether it is publicly available as podcast"),
|
help_text=_("sound is available as podcast"),
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
is_downloadable = models.BooleanField(
|
is_downloadable = models.BooleanField(
|
||||||
_("downloadable"),
|
_("downloadable"),
|
||||||
help_text=_("whether it can be publicly downloaded by visitors (sound must be " "public)"),
|
help_text=_("sound can be downloaded by visitors (sound must be public)"),
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
\**********************/
|
\**********************/
|
||||||
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _styles_admin_scss__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./styles/admin.scss */ \"./src/styles/admin.scss\");\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n/* harmony import */ var _track__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./track */ \"./src/track.js\");\n\n\n\n\n\nconst AdminApp = {\n ..._app__WEBPACK_IMPORTED_MODULE_2__[\"default\"],\n components: {\n ..._app__WEBPACK_IMPORTED_MODULE_2__[\"default\"].components,\n ..._components__WEBPACK_IMPORTED_MODULE_3__.admin\n },\n data() {\n return {\n ...super.data,\n Track: _track__WEBPACK_IMPORTED_MODULE_4__[\"default\"]\n };\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (AdminApp);\nwindow.App = AdminApp;\n\n//# sourceURL=webpack://aircox-assets/./src/admin.js?");
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _styles_admin_scss__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./styles/admin.scss */ \"./src/styles/admin.scss\");\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n\n\n\n\nconst AdminApp = {\n ..._app__WEBPACK_IMPORTED_MODULE_2__[\"default\"],\n components: {\n ..._app__WEBPACK_IMPORTED_MODULE_2__[\"default\"].components,\n ..._components__WEBPACK_IMPORTED_MODULE_3__.admin\n },\n data() {\n return {\n ...super.data\n };\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (AdminApp);\nwindow.App = AdminApp;\n\n//# sourceURL=webpack://aircox-assets/./src/admin.js?");
|
||||||
|
|
||||||
/***/ })
|
/***/ })
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -2,8 +2,29 @@
|
||||||
{% load static i18n humanize honeypot aircox %}
|
{% load static i18n humanize honeypot aircox %}
|
||||||
|
|
||||||
{% block page_form %}
|
{% block page_form %}
|
||||||
<a-modal ref="sound-edit-modal" title="{% translate "Edit sound" %}">
|
<a-modal ref="sound-edit-modal" v-if="item" title="{% translate "Edit sound" %}">
|
||||||
<template #default>
|
<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>
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
@ -11,37 +32,11 @@
|
||||||
<template v-slot="{podcasts,page}">
|
<template v-slot="{podcasts,page}">
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<hr/>
|
<hr/>
|
||||||
{% include "./widgets/tracklist_editor.html" with formset=playlist_formset %}
|
{% include "./widgets/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>
|
||||||
<a-playlist v-if="page" :set="podcasts"
|
{% include "./widgets/playlist_editor.html" with formset=playlist_formset %}
|
||||||
name="{{ page.title }}"
|
|
||||||
list-class="menu-list" item-class="menu-item"
|
|
||||||
:player="player" :actions="['play']"
|
|
||||||
@select="player.playItems('queue', $event.item)">
|
|
||||||
<template #after-title="{item}">
|
|
||||||
<span class="flex-grow-1">
|
|
||||||
[[ item.data.type_display ]]
|
|
||||||
<span v-if="item.data.is_public">
|
|
||||||
/
|
|
||||||
{% translate "public" %}
|
|
||||||
</span>
|
|
||||||
<span v-if="item.data.is_downloadable">
|
|
||||||
/
|
|
||||||
{% translate "downloadable" %}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template #actions="{item}">
|
|
||||||
<button type="button" class="button"
|
|
||||||
title="{% translate "Edit" %}">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="fa fa-edit"></i>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</a-playlist>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</a-episode>
|
</a-episode>
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
{{ field }}
|
{{ field }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help">{{ field.help_text }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
|
|
21
aircox/templates/aircox/widgets/form_field.html
Normal file
21
aircox/templates/aircox/widgets/form_field.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{% 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 %}
|
74
aircox/templates/aircox/widgets/playlist_editor.html
Normal file
74
aircox/templates/aircox/widgets/playlist_editor.html
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
{% comment %}
|
||||||
|
Context:
|
||||||
|
- formset: formset
|
||||||
|
{% endcomment %}
|
||||||
|
{% load aircox aircox_admin static i18n %}
|
||||||
|
|
||||||
|
{% with formset.form.base_fields as fields %}
|
||||||
|
<div id="inline-sounds">
|
||||||
|
{{ formset.non_form_errors }}
|
||||||
|
<!-- formset.management_form -->
|
||||||
|
|
||||||
|
<a-playlist-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 "Sound Position" %}"
|
||||||
|
aria-description="{% trans "Sound 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">
|
||||||
|
<div class="control">
|
||||||
|
{% include "./form_field.html" with field=field name=name value="item.data."|add:name %}
|
||||||
|
</div>
|
||||||
|
<p v-for="error in item.error(attr)" class="help is-danger">
|
||||||
|
[[ error ]] !
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</a-playlist-editor>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
|
@ -5,15 +5,15 @@ Context:
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% load aircox aircox_admin static i18n %}
|
{% load aircox aircox_admin static i18n %}
|
||||||
|
|
||||||
{% with formset.form.fields as fields %}
|
{% with formset.form.base_fields as fields %}
|
||||||
<div id="inline-tracks">
|
<div id="inline-tracks">
|
||||||
{{ formset.non_form_errors }}
|
{{ formset.non_form_errors }}
|
||||||
<!-- formset.management_form -->
|
<!-- formset.management_form -->
|
||||||
|
|
||||||
<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 %}"
|
||||||
:default-columns="[{% for f in fields %}{% 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>
|
<template #title>
|
||||||
|
@ -47,25 +47,26 @@ Context:
|
||||||
:name="'{{ formset.prefix }}-' + row + '-id'"
|
:name="'{{ formset.prefix }}-' + row + '-id'"
|
||||||
:value="item.data.id || item.id"/>
|
:value="item.data.id || item.id"/>
|
||||||
|
|
||||||
{% for field in fields %}
|
{% for name, field in fields.items %}
|
||||||
{% if field != 'position' and field.widget.is_hidden %}
|
{% if name != 'position' and field.widget.is_hidden %}
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
:name="'{{ formset.prefix }}-' + row + '-{{ field.name }}'"
|
:name="'{{ formset.prefix }}-' + row + '-{{ name }}'"
|
||||||
v-model="item.data[attr]"/>
|
v-model="item.data[attr]"/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
</template>
|
</template>
|
||||||
{% for field in fields %}
|
{% 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-{{ field }}="{item,cell,value,attr,emit}">
|
---
|
||||||
|
<template v-slot:row-{{ name }}="{item,cell,value,attr,emit}">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<a-autocomplete
|
<a-autocomplete
|
||||||
:input-class="['input', item.error(attr) ? 'is-danger' : 'half-field']"
|
:input-class="['input', item.error(attr) ? 'is-danger' : 'half-field']"
|
||||||
url="{% url 'api:track-autocomplete' %}?{{ field }}=${query}&field={{ field }}"
|
url="{% url 'api:track-autocomplete' %}?{{ name }}=${query}&field={{ name }}"
|
||||||
:name="'{{ formset.prefix }}-' + cell.row + '-{{ field }}'"
|
:name="'{{ formset.prefix }}-' + cell.row + '-{{ name }}'"
|
||||||
v-model="item.data[attr]"
|
v-model="item.data[attr]"
|
||||||
title="{{ field }}"
|
title="{{ name }}"
|
||||||
@change="emit('change', col)"/>
|
@change="emit('change', col)"/>
|
||||||
<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 ]] !
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from django import template
|
from django import template, forms
|
||||||
from django.contrib.admin.templatetags.admin_urls import admin_urlname
|
from django.contrib.admin.templatetags.admin_urls import admin_urlname
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
@ -148,3 +148,15 @@ def do_edit_view(obj):
|
||||||
@register.filter(name="detail_view")
|
@register.filter(name="detail_view")
|
||||||
def do_detail_view(obj):
|
def do_detail_view(obj):
|
||||||
return "%s-detail" % obj.split("-")[0]
|
return "%s-detail" % obj.split("-")[0]
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="is_checkbox")
|
||||||
|
def is_checkbox(field):
|
||||||
|
"""Return True if field is a checkbox."""
|
||||||
|
return isinstance(field.widget, forms.CheckboxInput)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="is_select")
|
||||||
|
def is_select(field):
|
||||||
|
"""Return True if field is a select."""
|
||||||
|
return isinstance(field.widget, forms.Select)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from aircox.serializers.admin import UserSettingsSerializer
|
from aircox.serializers.admin import UserSettingsSerializer
|
||||||
|
|
||||||
__all__ = ("register", "do_get_admin_tools", "do_track_inline_data")
|
__all__ = ("register", "do_get_admin_tools", "do_formset_inline_data", "do_inline_labels")
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
@ -17,14 +17,24 @@ def do_get_admin_tools():
|
||||||
return admin.site.get_tools()
|
return admin.site.get_tools()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(name="track_inline_data", takes_context=True)
|
@register.simple_tag(name="formset_inline_data", takes_context=True)
|
||||||
def do_track_inline_data(context, formset):
|
def do_formset_inline_data(context, formset):
|
||||||
"""Return initial data for playlist editor as dict. Keys are:
|
"""Return initial data of formset as dict (used by TrackListEditor and
|
||||||
|
PlaylistEditor). Keys are:
|
||||||
|
|
||||||
- ``items``: list of items. Extra keys:
|
- ``items``: list of items. Extra keys:
|
||||||
- ``__error__``: dict of form fields errors
|
- ``__error__``: dict of form fields errors
|
||||||
- ``settings``: user's settings
|
- ``settings``: user's settings
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# --- get fields labels
|
||||||
|
model = formset.form.Meta.model
|
||||||
|
fields = {}
|
||||||
|
for field_name in formset.form.Meta.fields:
|
||||||
|
field = model._meta.get_field(field_name)
|
||||||
|
fields[field_name] = str(field.verbose_name).capitalize()
|
||||||
|
|
||||||
|
# --- get items
|
||||||
items = []
|
items = []
|
||||||
for form in formset.forms:
|
for form in formset.forms:
|
||||||
item = {name: form[name].value() for name in form.fields.keys()}
|
item = {name: form[name].value() for name in form.fields.keys()}
|
||||||
|
@ -36,7 +46,7 @@ def do_track_inline_data(context, formset):
|
||||||
item["tags"] = ", ".join(tag.name for tag in tags)
|
item["tags"] = ", ".join(tag.name for tag in tags)
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
|
||||||
data = {"items": items}
|
data = {"items": items, "fields": fields}
|
||||||
user = context["request"].user
|
user = context["request"].user
|
||||||
settings = getattr(user, "aircox_settings", None)
|
settings = getattr(user, "aircox_settings", None)
|
||||||
data["settings"] = settings and UserSettingsSerializer(settings).data
|
data["settings"] = settings and UserSettingsSerializer(settings).data
|
||||||
|
@ -44,22 +54,19 @@ def do_track_inline_data(context, formset):
|
||||||
return source
|
return source
|
||||||
|
|
||||||
|
|
||||||
track_inline_labels_ = {
|
inline_labels_ = {
|
||||||
"artist": _("Artist"),
|
# list editor
|
||||||
"album": _("Album"),
|
"add_item": _("Add an item"),
|
||||||
"title": _("Title"),
|
"remove_item": _("Remove"),
|
||||||
"tags": _("Tags"),
|
|
||||||
"year": _("Year"),
|
|
||||||
"save_settings": _("Save Settings"),
|
"save_settings": _("Save Settings"),
|
||||||
"discard_changes": _("Discard changes"),
|
"discard_changes": _("Discard changes"),
|
||||||
|
# track list
|
||||||
"columns": _("Columns"),
|
"columns": _("Columns"),
|
||||||
"add_track": _("Add a track"),
|
|
||||||
"remove_track": _("Remove"),
|
|
||||||
"timestamp": _("Timestamp"),
|
"timestamp": _("Timestamp"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(name="track_inline_labels")
|
@register.simple_tag(name="inline_labels")
|
||||||
def do_track_inline_labels():
|
def do_inline_labels():
|
||||||
"""Return labels for columns in playlist editor as dict."""
|
"""Return labels for columns in playlist editor as dict."""
|
||||||
return json.dumps({k: str(v) for k, v in track_inline_labels_.items()})
|
return json.dumps({k: str(v) for k, v in inline_labels_.items()})
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
from django.forms.models import modelformset_factory
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from aircox.forms import EpisodeForm
|
|
||||||
from aircox.models import Episode, Program, StaticPage, Track
|
from aircox.models import Episode, Program, StaticPage, Track
|
||||||
|
from aircox import forms
|
||||||
from ..filters import EpisodeFilters
|
from ..filters import EpisodeFilters
|
||||||
from .page import PageListView
|
from .page import PageListView
|
||||||
from .program import ProgramPageDetailView, BaseProgramMixin
|
from .program import ProgramPageDetailView, BaseProgramMixin
|
||||||
|
@ -49,18 +48,9 @@ class PodcastListView(EpisodeListView):
|
||||||
|
|
||||||
class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
|
class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
|
||||||
model = Episode
|
model = Episode
|
||||||
form_class = EpisodeForm
|
form_class = forms.EpisodeForm
|
||||||
template_name = "aircox/episode_form.html"
|
template_name = "aircox/episode_form.html"
|
||||||
|
|
||||||
playlist_fields = (
|
|
||||||
"position",
|
|
||||||
"artist",
|
|
||||||
"title",
|
|
||||||
"tags",
|
|
||||||
"album",
|
|
||||||
)
|
|
||||||
"""Playlist editor's ordered fields."""
|
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
program = self.get_object().program
|
program = self.get_object().program
|
||||||
return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
|
return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
|
||||||
|
@ -68,16 +58,42 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse("episode-detail", kwargs={"slug": self.get_object().slug})
|
return reverse("episode-detail", kwargs={"slug": self.get_object().slug})
|
||||||
|
|
||||||
def get_playlist_queryset(self, episode):
|
def get_tracklist_queryset(self, episode):
|
||||||
return Track.objects.filter(episode=episode)
|
return Track.objects.filter(episode=episode)
|
||||||
|
|
||||||
|
def get_tracklist_formset(self, episode, **kwargs):
|
||||||
|
kwargs.update(
|
||||||
|
{
|
||||||
|
"queryset": self.get_tracklist_queryset(episode),
|
||||||
|
"initial": {
|
||||||
|
"episode": episode.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return forms.TrackFormSet(**kwargs)
|
||||||
|
|
||||||
|
def get_playlist_queryset(self, episode):
|
||||||
|
return episode.sound_set.all()
|
||||||
|
|
||||||
def get_playlist_formset(self, episode, **kwargs):
|
def get_playlist_formset(self, episode, **kwargs):
|
||||||
kwargs["queryset"] = self.get_playlist_queryset(episode)
|
kwargs.update(
|
||||||
TrackFormSet = modelformset_factory(Track, fields=self.playlist_fields, extra=0)
|
{
|
||||||
return TrackFormSet(**kwargs)
|
"queryset": self.get_playlist_queryset(episode),
|
||||||
|
"initial": {
|
||||||
|
"program": episode.parent_id,
|
||||||
|
"episode": episode.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return forms.SoundFormSet(**kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs["playlist_formset"] = self.get_playlist_formset(self.object)
|
kwargs.update(
|
||||||
|
{
|
||||||
|
"playlist_formset": self.get_playlist_formset(self.object),
|
||||||
|
"tracklist_formset": self.get_tracklist_formset(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):
|
||||||
|
|
|
@ -3,7 +3,6 @@ import './index.js'
|
||||||
|
|
||||||
import App from './app';
|
import App from './app';
|
||||||
import {admin as components} from './components'
|
import {admin as components} from './components'
|
||||||
import Track from './track'
|
|
||||||
|
|
||||||
const AdminApp = {
|
const AdminApp = {
|
||||||
...App,
|
...App,
|
||||||
|
@ -12,7 +11,6 @@ const AdminApp = {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
...super.data,
|
...super.data,
|
||||||
Track,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,17 @@
|
||||||
<div class="modal-card-title">
|
<div class="modal-card-title">
|
||||||
<slot name="title">{{ title }}</slot>
|
<slot name="title">{{ title }}</slot>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="delete square" aria-label="close" @click="close">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-close"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<section class="modal-card-body">
|
<section class="modal-card-body">
|
||||||
<slot name="default"></slot>
|
<slot name="default" :item="item"></slot>
|
||||||
</section>
|
</section>
|
||||||
<div class="modal-card-foot align-right">
|
<div class="modal-card-foot align-right">
|
||||||
<slot name="footer" :close="close"></slot>
|
<slot name="footer" :item="item" :close="close"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -24,13 +29,24 @@ export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
///! If true, modal is open
|
||||||
active: false,
|
active: false,
|
||||||
|
///! Item or data passed down to slots.
|
||||||
|
item: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
open() { this.active = true; },
|
///! Open modal dialog. Set provided `item` to dialog's one.
|
||||||
close() { this.active = false; },
|
open(item=null) {
|
||||||
|
this.active = true
|
||||||
|
this.item = item
|
||||||
|
},
|
||||||
|
///! Close modal and reset item to null.
|
||||||
|
close() {
|
||||||
|
this.active = false
|
||||||
|
this.item = null
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -37,10 +37,6 @@
|
||||||
<span class="fas fa-pause" v-if="playing"></span>
|
<span class="fas fa-pause" v-if="playing"></span>
|
||||||
<span class="fas fa-play" v-else></span>
|
<span class="fas fa-play" v-else></span>
|
||||||
</button>
|
</button>
|
||||||
<!--
|
|
||||||
<div class="media-cover" v-if="current && current.data.cover">
|
|
||||||
<img :src="current.data.cover" class="cover" />
|
|
||||||
</div> -->
|
|
||||||
<div :class="['a-player-bar-content', loaded && duration ? 'has-progress' : '']">
|
<div :class="['a-player-bar-content', loaded && duration ? 'has-progress' : '']">
|
||||||
<slot name="content" :loaded="loaded" :live="live" :current="current"></slot>
|
<slot name="content" :loaded="loaded" :live="live" :current="current"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
105
assets/src/components/APlaylistEditor.vue
Normal file
105
assets/src/components/APlaylistEditor.vue
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<template>
|
||||||
|
<div class="a-playlist-editor">
|
||||||
|
<a-rows :set="set" :columns="columns"
|
||||||
|
:labels="initData.fields" :allow-create="true" :orderable="true"
|
||||||
|
@move="listItemMove">
|
||||||
|
<template v-for="[name,slot] of rowsSlots" :key="slot"
|
||||||
|
v-slot:[slot]="data">
|
||||||
|
<slot v-if="name != 'row-tail'" :name="name" v-bind="data"/>
|
||||||
|
</template>
|
||||||
|
</a-rows>
|
||||||
|
|
||||||
|
<div class="flex-row">
|
||||||
|
<div class="flex-grow-1 flex-row">
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 align-right">
|
||||||
|
<button type="button" class="button square is-warning p-2"
|
||||||
|
@click="loadData({items: this.initData.items},true)"
|
||||||
|
:title="labels.discard_changes"
|
||||||
|
:aria-label="labels.discard_changes"
|
||||||
|
>
|
||||||
|
<span class="icon"><i class="fa fa-rotate" /></span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button square is-primary p-2"
|
||||||
|
@click="this.set.push(new this.set.model())"
|
||||||
|
:title="labels.add_sound"
|
||||||
|
:aria-label="labels.add_sound"
|
||||||
|
>
|
||||||
|
<span class="icon"><i class="fa fa-plus"/></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
// import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
|
||||||
|
import {cloneDeep} from 'lodash'
|
||||||
|
import Model, {Set} from '../model'
|
||||||
|
|
||||||
|
// import AActionButton from './AActionButton'
|
||||||
|
import ARows from './ARows'
|
||||||
|
// import AModal from "./AModal"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {ARows},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
initData: Object,
|
||||||
|
dataPrefix: String,
|
||||||
|
labels: Object,
|
||||||
|
settingsUrl: String,
|
||||||
|
columns: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ['name', "type", 'is_public', 'is_downloadable']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
set: new Set(Model),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
items() {
|
||||||
|
return this.set.items
|
||||||
|
},
|
||||||
|
|
||||||
|
rowsSlots() {
|
||||||
|
return Object.keys(this.$slots)
|
||||||
|
.filter(x => x.startsWith('row-') || x.startsWith('rows-'))
|
||||||
|
.map(x => [x, x.startsWith('rows-') ? x.slice(5) : x])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
listItemMove({from, to, set}) {
|
||||||
|
set.move(from, to);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load initial data
|
||||||
|
*/
|
||||||
|
loadData({items=[] /*, settings=null*/}, reset=false) {
|
||||||
|
if(reset) {
|
||||||
|
this.set.items = []
|
||||||
|
}
|
||||||
|
for(var index in items)
|
||||||
|
this.set.push(cloneDeep(items[index]))
|
||||||
|
// if(settings)
|
||||||
|
// this.settingsSaved(settings)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
initData(val) {
|
||||||
|
this.loadData(val)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.initData && this.loadData(this.initData)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -3,7 +3,7 @@
|
||||||
<div ref="list" :class="['a-select-file-list', listClass]">
|
<div ref="list" :class="['a-select-file-list', listClass]">
|
||||||
<!-- upload -->
|
<!-- upload -->
|
||||||
<form ref="uploadForm" class="flex-column" v-if="state == STATE.DEFAULT">
|
<form ref="uploadForm" class="flex-column" v-if="state == STATE.DEFAULT">
|
||||||
<div class="field flex-grow-1" v-if="!uploadFile">
|
<div class="field flex-grow-1">
|
||||||
<label class="label">{{ uploadLabel }}</label>
|
<label class="label">{{ uploadLabel }}</label>
|
||||||
<input type="file" ref="uploadFile" :name="uploadFieldName" @change="onSubmit"/>
|
<input type="file" ref="uploadFile" :name="uploadFieldName" @change="onSubmit"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
<section v-show="page == Page.List" class="panel">
|
<section v-show="page == Page.List" class="panel">
|
||||||
<a-rows :set="set" :columns="columns" :labels="labels"
|
<a-rows :set="set" :columns="columns" :labels="initData.fields"
|
||||||
:allow-create="true"
|
:allow-create="true"
|
||||||
:orderable="true" @move="listItemMove" @colmove="columnMove"
|
:orderable="true" @move="listItemMove" @colmove="columnMove"
|
||||||
@cell="onCellEvent">
|
@cell="onCellEvent">
|
||||||
|
@ -49,8 +49,8 @@
|
||||||
<td class="align-right pr-0">
|
<td class="align-right pr-0">
|
||||||
<button type="button" class="button square"
|
<button type="button" class="button square"
|
||||||
@click.stop="items.splice(data.row,1)"
|
@click.stop="items.splice(data.row,1)"
|
||||||
:title="labels.remove_track"
|
:title="labels.remove_item"
|
||||||
:aria-label="labels.remove_track">
|
:aria-label="labels.remove_item">
|
||||||
<span class="icon"><i class="fa fa-trash" /></span>
|
<span class="icon"><i class="fa fa-trash" /></span>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
@ -82,8 +82,8 @@
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="button square is-primary p-2" v-if="page == Page.List"
|
<button type="button" class="button square is-primary p-2" v-if="page == Page.List"
|
||||||
@click="this.set.push(new this.set.model())"
|
@click="this.set.push(new this.set.model())"
|
||||||
:title="labels.add_track"
|
:title="labels.add_item"
|
||||||
:aria-label="labels.add_track"
|
:aria-label="labels.add_item"
|
||||||
>
|
>
|
||||||
<span class="icon"><i class="fa fa-plus"/></span>
|
<span class="icon"><i class="fa fa-plus"/></span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
<table class="table is-bordered"
|
<table class="table is-bordered"
|
||||||
style="vertical-align: middle">
|
style="vertical-align: middle">
|
||||||
<tr>
|
<tr>
|
||||||
<a-row :columns="columns" :item="labels"
|
<a-row :columns="columns" :item="initData.fields"
|
||||||
@move="formatMove" :orderable="true">
|
@move="formatMove" :orderable="true">
|
||||||
<template v-slot:cell-after="{cell}">
|
<template v-slot:cell-after="{cell}">
|
||||||
<td style="cursor:pointer;" v-if="cell.col < columns.length-1">
|
<td style="cursor:pointer;" v-if="cell.col < columns.length-1">
|
||||||
|
@ -149,8 +149,7 @@
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
|
import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
|
||||||
import {Set} from '../model'
|
import Model, {Set} from '../model'
|
||||||
import Track from '../track'
|
|
||||||
|
|
||||||
import AActionButton from './AActionButton'
|
import AActionButton from './AActionButton'
|
||||||
import ARow from './ARow'
|
import ARow from './ARow'
|
||||||
|
@ -165,6 +164,7 @@ export const Page = {
|
||||||
export default {
|
export default {
|
||||||
components: { AActionButton, ARow, ARows, AModal },
|
components: { AActionButton, ARow, ARows, AModal },
|
||||||
props: {
|
props: {
|
||||||
|
///! initial data as: {items: [], fields: {column_name: label, settings: {}}
|
||||||
initData: Object,
|
initData: Object,
|
||||||
dataPrefix: String,
|
dataPrefix: String,
|
||||||
labels: Object,
|
labels: Object,
|
||||||
|
@ -182,7 +182,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
Page: Page,
|
Page: Page,
|
||||||
page: Page.Text,
|
page: Page.Text,
|
||||||
set: new Set(Track),
|
set: new Set(Model),
|
||||||
extraData: {},
|
extraData: {},
|
||||||
settings,
|
settings,
|
||||||
savedSettings: cloneDeep(settings),
|
savedSettings: cloneDeep(settings),
|
||||||
|
|
|
@ -7,15 +7,16 @@ import AList from './AList'
|
||||||
import APage from './APage'
|
import APage from './APage'
|
||||||
import APlayer from './APlayer'
|
import APlayer from './APlayer'
|
||||||
import APlaylist from './APlaylist'
|
import APlaylist from './APlaylist'
|
||||||
import ATracklistEditor from './ATracklistEditor'
|
|
||||||
import AProgress from './AProgress'
|
import AProgress from './AProgress'
|
||||||
import ASoundItem from './ASoundItem'
|
import ASoundItem from './ASoundItem'
|
||||||
import ASwitch from './ASwitch'
|
import ASwitch from './ASwitch'
|
||||||
import AStatistics from './AStatistics'
|
|
||||||
import AStreamer from './AStreamer'
|
|
||||||
|
|
||||||
import AModal from "./AModal"
|
import AModal from "./AModal"
|
||||||
import ASelectFile from "./ASelectFile"
|
import ASelectFile from "./ASelectFile"
|
||||||
|
import AStatistics from './AStatistics'
|
||||||
|
import AStreamer from './AStreamer'
|
||||||
|
import ATracklistEditor from './ATracklistEditor'
|
||||||
|
import APlaylistEditor from './APlaylistEditor'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core components
|
* Core components
|
||||||
|
@ -35,5 +36,5 @@ export const admin = {
|
||||||
|
|
||||||
export const dashboard = {
|
export const dashboard = {
|
||||||
...base,
|
...base,
|
||||||
AActionButton, ASelectFile, AModal, ATracklistEditor,
|
AActionButton, ASelectFile, AModal, ATracklistEditor, APlaylistEditor
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,11 @@ const DashboardApp = {
|
||||||
...App,
|
...App,
|
||||||
components: {...App.components, ...components},
|
components: {...App.components, ...components},
|
||||||
|
|
||||||
/*
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editPageContent: null,
|
modalItem: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
*/
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
...App.methods,
|
...App.methods,
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import Model from './model'
|
|
||||||
|
|
||||||
export default class Track extends Model {
|
|
||||||
static getId(data) { return data.pk }
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user