episode-form: add tracks inline formset
This commit is contained in:
		
							
								
								
									
										29
									
								
								aircox/static/aircox/js/ckeditor-init.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								aircox/static/aircox/js/ckeditor-init.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
/* global CKEDITOR, django */
 | 
			
		||||
/* Modified in order to be manually loaded after vue.js */
 | 
			
		||||
 | 
			
		||||
  function initialiseCKEditor() {
 | 
			
		||||
    var textareas = Array.prototype.slice.call(
 | 
			
		||||
      document.querySelectorAll("textarea[data-type=ckeditortype]"),
 | 
			
		||||
    )
 | 
			
		||||
    for (var i = 0; i < textareas.length; ++i) {
 | 
			
		||||
      var t = textareas[i]
 | 
			
		||||
      if (
 | 
			
		||||
        t.getAttribute("data-processed") == "0" &&
 | 
			
		||||
        t.id.indexOf("__prefix__") == -1
 | 
			
		||||
      ) {
 | 
			
		||||
        t.setAttribute("data-processed", "1")
 | 
			
		||||
        var ext = JSON.parse(t.getAttribute("data-external-plugin-resources"))
 | 
			
		||||
        for (var j = 0; j < ext.length; ++j) {
 | 
			
		||||
          CKEDITOR.plugins.addExternal(ext[j][0], ext[j][1], ext[j][2])
 | 
			
		||||
        }
 | 
			
		||||
        CKEDITOR.replace(t.id, JSON.parse(t.getAttribute("data-config")))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function initialiseCKEditorInInlinedForms() {
 | 
			
		||||
    if (typeof django === "object" && django.jQuery) {
 | 
			
		||||
      django.jQuery(document).on("formset:added", initialiseCKEditor)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
//})()
 | 
			
		||||
@ -1,12 +1,17 @@
 | 
			
		||||
{% extends "aircox/basepage_detail.html" %}
 | 
			
		||||
{% load static i18n humanize honeypot aircox %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block head_extra %}
 | 
			
		||||
  {{ form.media }}
 | 
			
		||||
  <script type="text/javascript" src="{% static "aircox/js/admin.js" %}"></script>
 | 
			
		||||
  <script type="text/javascript" src="{% static "aircox/js/ckeditor-init.js" %}"></script>
 | 
			
		||||
  <!-- <script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script> -->
 | 
			
		||||
  <script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block init-scripts %}
 | 
			
		||||
    aircox.init(null, {hotReload:false, initPlayer:false, initApp:true})
 | 
			
		||||
    initialiseCKEditor()
 | 
			
		||||
    initialiseCKEditorInInlinedForms()
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block comments %}
 | 
			
		||||
@ -18,9 +23,15 @@
 | 
			
		||||
    <table>
 | 
			
		||||
        {{ form.as_table }}
 | 
			
		||||
        {% render_honeypot_field "website" %}
 | 
			
		||||
 | 
			
		||||
    </table>
 | 
			
		||||
    <br/>
 | 
			
		||||
    <input type="submit" value="Update" class="button is-success">
 | 
			
		||||
 | 
			
		||||
    {% include "aircox/playlist_inline.html" %}
 | 
			
		||||
 | 
			
		||||
    <input type="submit" value="Update" class="button is-success">
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
</section>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										70
									
								
								aircox/templates/aircox/playlist_inline.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								aircox/templates/aircox/playlist_inline.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
			
		||||
{% comment %}Inline block to edit playlists{% endcomment %}
 | 
			
		||||
{% load aircox aircox_admin static i18n %}
 | 
			
		||||
 | 
			
		||||
<div id="inline-tracks" class="box mb-5">
 | 
			
		||||
    {{ formset.non_form_errors }}
 | 
			
		||||
    <!-- formset.management_form -->
 | 
			
		||||
 | 
			
		||||
    <a-playlist-editor
 | 
			
		||||
            :labels="{% track_inline_labels %}"
 | 
			
		||||
            :init-data="{% track_inline_data formset=formset %}"
 | 
			
		||||
            settings-url="{% url "api:user-settings" %}"
 | 
			
		||||
            data-prefix="{{ formset.prefix }}-">
 | 
			
		||||
        <template #title>
 | 
			
		||||
            <h5 class="title is-4">{% trans "Playlist" %}</h5>
 | 
			
		||||
        </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 field in fields %}
 | 
			
		||||
                {% if field != 'position' %}
 | 
			
		||||
                <input type="hidden"
 | 
			
		||||
                    :name="'{{ formset.prefix }}-' + row + '-{{ field.name }}'"
 | 
			
		||||
                    v-model="item.data[attr]"/>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </td>
 | 
			
		||||
        </template>
 | 
			
		||||
        {% for field in fields %}
 | 
			
		||||
        <template v-slot:row-{{ field }}="{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' %}?{{ field }}=${query}&field={{ field }}"
 | 
			
		||||
                        :name="'{{ formset.prefix }}-' + cell.row + '-{{ field }}'"
 | 
			
		||||
                        v-model="item.data[attr]"
 | 
			
		||||
                        title="{{ field }}"
 | 
			
		||||
                        @change="emit('change', col)"/>
 | 
			
		||||
                <p v-for="error in item.error(attr)" class="help is-danger">
 | 
			
		||||
                    [[ error ]] !
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </template>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
    </a-playlist-editor>
 | 
			
		||||
</div>
 | 
			
		||||
@ -34,3 +34,13 @@ def test_group_change_program(user, client, program):
 | 
			
		||||
    user.groups.add(program.editors)
 | 
			
		||||
    response = client.get(reverse("program-edit", kwargs={"pk": program.pk}))
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_group_change_episode(user, client, program, episode):
 | 
			
		||||
    client.force_login(user)
 | 
			
		||||
    response = client.get(reverse("episode-edit", kwargs={"pk": episode.pk}))
 | 
			
		||||
    assert response.status_code == 403
 | 
			
		||||
    user.groups.add(program.editors)
 | 
			
		||||
    response = client.get(reverse("episode-edit", kwargs={"pk": episode.pk}))
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
from itertools import chain
 | 
			
		||||
import json
 | 
			
		||||
import pytest
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.core.files.uploadedfile import SimpleUploadedFile
 | 
			
		||||
@ -38,3 +40,34 @@ def test_add_cover(user, client, program):
 | 
			
		||||
    assert r.status_code == 200
 | 
			
		||||
    p = Program.objects.get(pk=program.pk)
 | 
			
		||||
    assert "cover1.png" in p.cover.url
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_edit_tracklist(user, client, program, episode, tracks):
 | 
			
		||||
    user.groups.add(program.editors)
 | 
			
		||||
    client.force_login(user)
 | 
			
		||||
    episode.status = 0x10  # published
 | 
			
		||||
    episode.save()
 | 
			
		||||
    r = client.get(reverse("program-detail", kwargs={"slug": episode.program.slug}))
 | 
			
		||||
    assert r.status_code == 200
 | 
			
		||||
    r = client.get(reverse("episode-detail", kwargs={"slug": episode.slug}))
 | 
			
		||||
    assert r.status_code == 200
 | 
			
		||||
    r2 = client.get(reverse("episode-edit", kwargs={"pk": episode.pk}))
 | 
			
		||||
    assert r2.status_code == 200
 | 
			
		||||
 | 
			
		||||
    tracklist = [t.id for t in episode.track_set.all().order_by("position")]
 | 
			
		||||
    tracklist_details_reversed = [(t.id, t.artist, t.title) for t in episode.track_set.all().order_by("-position")]
 | 
			
		||||
    tracklist_details_reversed = list(chain(*tracklist_details_reversed))
 | 
			
		||||
    data = """{"website": [""], "content": ["foobar"], "new_podcast": [""], "form-TOTAL_FORMS": ["3"],
 | 
			
		||||
    "form-INITIAL_FORMS": ["3"], "form-MIN_NUM_FORMS": ["0"], "form-MAX_NUM_FORMS": ["1000"], "form-0-position": ["0"],
 | 
			
		||||
    "form-0-id": ["%s"], "form-0-": ["", "", "", "", "", ""], "form-0-artist": ["%s"], "form-0-title": ["%s"],
 | 
			
		||||
    "form-0-tags": [""], "form-0-album": [""], "form-0-year": [""], "form-1-position": ["1"], "form-1-id": ["%s"],
 | 
			
		||||
    "form-1-": ["", "", "", "", "", ""], "form-1-artist": ["%s"], "form-1-title": ["%s"], "form-1-tags": [""],
 | 
			
		||||
    "form-1-album": [""], "form-1-year": [""], "form-2-position": ["2"], "form-2-id": ["%s"], "form-2-": ["", "", "",
 | 
			
		||||
    "", "", ""], "form-2-artist": ["%s"], "form-2-title": ["%s"], "form-2-tags": [""], "form-2-album": [""],
 | 
			
		||||
    "form-2-year": [""]}""" % tuple(
 | 
			
		||||
        tracklist_details_reversed
 | 
			
		||||
    )
 | 
			
		||||
    r = client.post(reverse("episode-edit", kwargs={"pk": episode.pk}), json.loads(data), follow=True)
 | 
			
		||||
    assert r.status_code == 200
 | 
			
		||||
    assert [t.id for t in episode.track_set.all().order_by("position")] == list(reversed(tracklist))
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,13 @@
 | 
			
		||||
from django.contrib.auth.mixins import UserPassesTestMixin
 | 
			
		||||
from django.forms import ModelForm, FileField
 | 
			
		||||
from django.forms.models import modelformset_factory
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from ckeditor.fields import RichTextField
 | 
			
		||||
from filer.models.filemodels import File
 | 
			
		||||
 | 
			
		||||
from aircox.controllers.sound_file import SoundFile
 | 
			
		||||
from aircox.models import Track
 | 
			
		||||
 | 
			
		||||
from ..filters import EpisodeFilters
 | 
			
		||||
from ..models import Episode, Program, StaticPage
 | 
			
		||||
@ -67,11 +69,13 @@ class EpisodeForm(ModelForm):
 | 
			
		||||
            sound_file.sync(
 | 
			
		||||
                program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True
 | 
			
		||||
            )
 | 
			
		||||
        super().save(commit=commit)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
 | 
			
		||||
    model = Episode
 | 
			
		||||
    form_class = EpisodeForm
 | 
			
		||||
    template_name = "aircox/episode_form.html"
 | 
			
		||||
 | 
			
		||||
    def get_sidebar_queryset(self):
 | 
			
		||||
        return super().get_sidebar_queryset().filter(parent=self.program)
 | 
			
		||||
@ -82,3 +86,27 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse("episode-detail", kwargs={"slug": self.get_object().slug})
 | 
			
		||||
 | 
			
		||||
    def get_object(self, queryset=None):
 | 
			
		||||
        obj = Episode.objects.get(pk=self.kwargs["pk"])
 | 
			
		||||
        return obj
 | 
			
		||||
 | 
			
		||||
    def get_formset(self, *args, **kwargs):
 | 
			
		||||
        fields = ("position", "artist", "title", "tags", "album", "year", "info")
 | 
			
		||||
        TrackFormSet = modelformset_factory(Track, fields=fields, extra=0)
 | 
			
		||||
        return TrackFormSet(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
        context["fields"] = ("position", "artist", "title", "tags", "album", "year", "info")
 | 
			
		||||
        context["formset"] = self.get_formset(queryset=Track.objects.filter(episode=self.object))
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
        else:
 | 
			
		||||
            return super().form_valid(formset)  # form_invalid(formset)
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user