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" %}
 | 
					{% extends "aircox/basepage_detail.html" %}
 | 
				
			||||||
{% load static i18n humanize honeypot aircox %}
 | 
					{% load static i18n humanize honeypot aircox %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
{% block head_extra %}
 | 
					{% 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 %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block init-scripts %}
 | 
					{% block init-scripts %}
 | 
				
			||||||
 | 
					    aircox.init(null, {hotReload:false, initPlayer:false, initApp:true})
 | 
				
			||||||
 | 
					    initialiseCKEditor()
 | 
				
			||||||
 | 
					    initialiseCKEditorInInlinedForms()
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block comments %}
 | 
					{% block comments %}
 | 
				
			||||||
@ -18,9 +23,15 @@
 | 
				
			|||||||
    <table>
 | 
					    <table>
 | 
				
			||||||
        {{ form.as_table }}
 | 
					        {{ form.as_table }}
 | 
				
			||||||
        {% render_honeypot_field "website" %}
 | 
					        {% render_honeypot_field "website" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
    <br/>
 | 
					    <br/>
 | 
				
			||||||
    <input type="submit" value="Update" class="button is-success">
 | 
					    <input type="submit" value="Update" class="button is-success">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {% include "aircox/playlist_inline.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <input type="submit" value="Update" class="button is-success">
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</section>
 | 
					</section>
 | 
				
			||||||
{% endblock %}
 | 
					{% 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)
 | 
					    user.groups.add(program.editors)
 | 
				
			||||||
    response = client.get(reverse("program-edit", kwargs={"pk": program.pk}))
 | 
					    response = client.get(reverse("program-edit", kwargs={"pk": program.pk}))
 | 
				
			||||||
    assert response.status_code == 200
 | 
					    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
 | 
					import pytest
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
from django.core.files.uploadedfile import SimpleUploadedFile
 | 
					from django.core.files.uploadedfile import SimpleUploadedFile
 | 
				
			||||||
@ -38,3 +40,34 @@ def test_add_cover(user, client, program):
 | 
				
			|||||||
    assert r.status_code == 200
 | 
					    assert r.status_code == 200
 | 
				
			||||||
    p = Program.objects.get(pk=program.pk)
 | 
					    p = Program.objects.get(pk=program.pk)
 | 
				
			||||||
    assert "cover1.png" in p.cover.url
 | 
					    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.contrib.auth.mixins import UserPassesTestMixin
 | 
				
			||||||
from django.forms import ModelForm, FileField
 | 
					from django.forms import ModelForm, FileField
 | 
				
			||||||
 | 
					from django.forms.models import modelformset_factory
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ckeditor.fields import RichTextField
 | 
					from ckeditor.fields import RichTextField
 | 
				
			||||||
from filer.models.filemodels import File
 | 
					from filer.models.filemodels import File
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from aircox.controllers.sound_file import SoundFile
 | 
					from aircox.controllers.sound_file import SoundFile
 | 
				
			||||||
 | 
					from aircox.models import Track
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..filters import EpisodeFilters
 | 
					from ..filters import EpisodeFilters
 | 
				
			||||||
from ..models import Episode, Program, StaticPage
 | 
					from ..models import Episode, Program, StaticPage
 | 
				
			||||||
@ -67,11 +69,13 @@ class EpisodeForm(ModelForm):
 | 
				
			|||||||
            sound_file.sync(
 | 
					            sound_file.sync(
 | 
				
			||||||
                program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True
 | 
					                program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					        super().save(commit=commit)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
 | 
					class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
 | 
				
			||||||
    model = Episode
 | 
					    model = Episode
 | 
				
			||||||
    form_class = EpisodeForm
 | 
					    form_class = EpisodeForm
 | 
				
			||||||
 | 
					    template_name = "aircox/episode_form.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_sidebar_queryset(self):
 | 
					    def get_sidebar_queryset(self):
 | 
				
			||||||
        return super().get_sidebar_queryset().filter(parent=self.program)
 | 
					        return super().get_sidebar_queryset().filter(parent=self.program)
 | 
				
			||||||
@ -82,3 +86,27 @@ 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_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