@ -1,12 +1,11 @@
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.forms.models import modelformset_factory
 | 
			
		||||
 | 
			
		||||
from filer.models.filemodels import File
 | 
			
		||||
 | 
			
		||||
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):
 | 
			
		||||
@ -40,18 +39,52 @@ class ProgramForm(PageForm):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EpisodeForm(PageForm):
 | 
			
		||||
    new_podcast = forms.FileField(required=False)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = models.Episode
 | 
			
		||||
        fields = PageForm.Meta.fields
 | 
			
		||||
 | 
			
		||||
    def save(self, commit=True):
 | 
			
		||||
        file_obj = self.cleaned_data["new_podcast"]
 | 
			
		||||
        if file_obj:
 | 
			
		||||
            obj, _ = File.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
 | 
			
		||||
            sound_file = SoundFile(obj.path)
 | 
			
		||||
            sound_file.sync(
 | 
			
		||||
                program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True
 | 
			
		||||
 | 
			
		||||
#    def save(self, commit=True):
 | 
			
		||||
#        file_obj = self.cleaned_data["new_podcast"]
 | 
			
		||||
#        if file_obj:
 | 
			
		||||
#            obj, _ = File.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
 | 
			
		||||
#            sound_file = SoundFile(obj.path)
 | 
			
		||||
#            sound_file.sync(
 | 
			
		||||
#                program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True
 | 
			
		||||
#            )
 | 
			
		||||
#        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,
 | 
			
		||||
)
 | 
			
		||||
        super().save(commit=commit)
 | 
			
		||||
"""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(
 | 
			
		||||
        _("public"),
 | 
			
		||||
        help_text=_("whether it is publicly available as podcast"),
 | 
			
		||||
        help_text=_("sound is available as podcast"),
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
    is_downloadable = models.BooleanField(
 | 
			
		||||
        _("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,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
  \**********************/
 | 
			
		||||
/***/ (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 %}
 | 
			
		||||
 | 
			
		||||
{% block page_form %}
 | 
			
		||||
<a-modal ref="sound-edit-modal" title="{% translate "Edit sound" %}">
 | 
			
		||||
    <template #default>
 | 
			
		||||
<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>
 | 
			
		||||
 | 
			
		||||
@ -11,37 +32,11 @@
 | 
			
		||||
    <template v-slot="{podcasts,page}">
 | 
			
		||||
        {{ block.super }}
 | 
			
		||||
        <hr/>
 | 
			
		||||
        {% include "./widgets/tracklist_editor.html" with formset=playlist_formset %}
 | 
			
		||||
        {% include "./widgets/tracklist_editor.html" with formset=tracklist_formset %}
 | 
			
		||||
        <hr/>
 | 
			
		||||
        <section class="container">
 | 
			
		||||
            <h3 class="title">{% translate "Sound files" %}</h3>
 | 
			
		||||
            <a-playlist v-if="page" :set="podcasts"
 | 
			
		||||
                    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>
 | 
			
		||||
            {% include "./widgets/playlist_editor.html" with formset=playlist_formset %}
 | 
			
		||||
        </section>
 | 
			
		||||
    </template>
 | 
			
		||||
</a-episode>
 | 
			
		||||
 | 
			
		||||
@ -75,6 +75,7 @@
 | 
			
		||||
            {{ field }}
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
        <p class="help">{{ field.help_text }}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {% 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 %}
 | 
			
		||||
{% load aircox aircox_admin static i18n %}
 | 
			
		||||
 | 
			
		||||
{% with formset.form.fields as fields %}
 | 
			
		||||
{% with formset.form.base_fields as fields %}
 | 
			
		||||
<div id="inline-tracks">
 | 
			
		||||
    {{ formset.non_form_errors }}
 | 
			
		||||
    <!-- formset.management_form -->
 | 
			
		||||
 | 
			
		||||
    <a-tracklist-editor
 | 
			
		||||
            :labels="{% track_inline_labels %}"
 | 
			
		||||
            :init-data="{% track_inline_data formset=formset %}"
 | 
			
		||||
            :default-columns="[{% for f in fields %}{% if f != "position" %}'{{ f }}',{% endif %}{% endfor %}]"
 | 
			
		||||
            :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>
 | 
			
		||||
@ -47,25 +47,26 @@ Context:
 | 
			
		||||
                    :name="'{{ formset.prefix }}-' + row + '-id'"
 | 
			
		||||
                    :value="item.data.id || item.id"/>
 | 
			
		||||
 | 
			
		||||
                {% for field in fields %}
 | 
			
		||||
                {% if field != 'position' and field.widget.is_hidden %}
 | 
			
		||||
                {% for name, field in fields.items %}
 | 
			
		||||
                {% if name != 'position' and field.widget.is_hidden %}
 | 
			
		||||
                <input type="hidden"
 | 
			
		||||
                    :name="'{{ formset.prefix }}-' + row + '-{{ field.name }}'"
 | 
			
		||||
                    :name="'{{ formset.prefix }}-' + row + '-{{ name }}'"
 | 
			
		||||
                    v-model="item.data[attr]"/>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </td>
 | 
			
		||||
        </template>
 | 
			
		||||
        {% for field in fields %}
 | 
			
		||||
        {% for name, field in fields.items %}
 | 
			
		||||
        {% 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">
 | 
			
		||||
                <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 }}'"
 | 
			
		||||
                        url="{% url 'api:track-autocomplete' %}?{{ name }}=${query}&field={{ name }}"
 | 
			
		||||
                        :name="'{{ formset.prefix }}-' + cell.row + '-{{ name }}'"
 | 
			
		||||
                        v-model="item.data[attr]"
 | 
			
		||||
                        title="{{ field }}"
 | 
			
		||||
                        title="{{ name }}"
 | 
			
		||||
                        @change="emit('change', col)"/>
 | 
			
		||||
                <p v-for="error in item.error(attr)" class="help is-danger">
 | 
			
		||||
                    [[ error ]] !
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import json
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
from django import template
 | 
			
		||||
from django import template, forms
 | 
			
		||||
from django.contrib.admin.templatetags.admin_urls import admin_urlname
 | 
			
		||||
from django.template.loader import render_to_string
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
@ -148,3 +148,15 @@ def do_edit_view(obj):
 | 
			
		||||
@register.filter(name="detail_view")
 | 
			
		||||
def do_detail_view(obj):
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
__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()
 | 
			
		||||
@ -17,14 +17,24 @@ def do_get_admin_tools():
 | 
			
		||||
    return admin.site.get_tools()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag(name="track_inline_data", takes_context=True)
 | 
			
		||||
def do_track_inline_data(context, formset):
 | 
			
		||||
    """Return initial data for playlist editor as dict. Keys are:
 | 
			
		||||
@register.simple_tag(name="formset_inline_data", takes_context=True)
 | 
			
		||||
def do_formset_inline_data(context, formset):
 | 
			
		||||
    """Return initial data of formset as dict (used by TrackListEditor and
 | 
			
		||||
    PlaylistEditor). Keys are:
 | 
			
		||||
 | 
			
		||||
    - ``items``: list of items. Extra keys:
 | 
			
		||||
        - ``__error__``: dict of form fields errors
 | 
			
		||||
    - ``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 = []
 | 
			
		||||
    for form in formset.forms:
 | 
			
		||||
        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)
 | 
			
		||||
        items.append(item)
 | 
			
		||||
 | 
			
		||||
    data = {"items": items}
 | 
			
		||||
    data = {"items": items, "fields": fields}
 | 
			
		||||
    user = context["request"].user
 | 
			
		||||
    settings = getattr(user, "aircox_settings", None)
 | 
			
		||||
    data["settings"] = settings and UserSettingsSerializer(settings).data
 | 
			
		||||
@ -44,22 +54,19 @@ def do_track_inline_data(context, formset):
 | 
			
		||||
    return source
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
track_inline_labels_ = {
 | 
			
		||||
    "artist": _("Artist"),
 | 
			
		||||
    "album": _("Album"),
 | 
			
		||||
    "title": _("Title"),
 | 
			
		||||
    "tags": _("Tags"),
 | 
			
		||||
    "year": _("Year"),
 | 
			
		||||
inline_labels_ = {
 | 
			
		||||
    # list editor
 | 
			
		||||
    "add_item": _("Add an item"),
 | 
			
		||||
    "remove_item": _("Remove"),
 | 
			
		||||
    "save_settings": _("Save Settings"),
 | 
			
		||||
    "discard_changes": _("Discard changes"),
 | 
			
		||||
    # track list
 | 
			
		||||
    "columns": _("Columns"),
 | 
			
		||||
    "add_track": _("Add a track"),
 | 
			
		||||
    "remove_track": _("Remove"),
 | 
			
		||||
    "timestamp": _("Timestamp"),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag(name="track_inline_labels")
 | 
			
		||||
def do_track_inline_labels():
 | 
			
		||||
@register.simple_tag(name="inline_labels")
 | 
			
		||||
def do_inline_labels():
 | 
			
		||||
    """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.forms.models import modelformset_factory
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from aircox.forms import EpisodeForm
 | 
			
		||||
from aircox.models import Episode, Program, StaticPage, Track
 | 
			
		||||
from aircox import forms
 | 
			
		||||
from ..filters import EpisodeFilters
 | 
			
		||||
from .page import PageListView
 | 
			
		||||
from .program import ProgramPageDetailView, BaseProgramMixin
 | 
			
		||||
@ -49,18 +48,9 @@ class PodcastListView(EpisodeListView):
 | 
			
		||||
 | 
			
		||||
class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
 | 
			
		||||
    model = Episode
 | 
			
		||||
    form_class = EpisodeForm
 | 
			
		||||
    form_class = forms.EpisodeForm
 | 
			
		||||
    template_name = "aircox/episode_form.html"
 | 
			
		||||
 | 
			
		||||
    playlist_fields = (
 | 
			
		||||
        "position",
 | 
			
		||||
        "artist",
 | 
			
		||||
        "title",
 | 
			
		||||
        "tags",
 | 
			
		||||
        "album",
 | 
			
		||||
    )
 | 
			
		||||
    """Playlist editor's ordered fields."""
 | 
			
		||||
 | 
			
		||||
    def test_func(self):
 | 
			
		||||
        program = self.get_object().program
 | 
			
		||||
        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):
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
    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):
 | 
			
		||||
        kwargs["queryset"] = self.get_playlist_queryset(episode)
 | 
			
		||||
        TrackFormSet = modelformset_factory(Track, fields=self.playlist_fields, extra=0)
 | 
			
		||||
        return TrackFormSet(**kwargs)
 | 
			
		||||
        kwargs.update(
 | 
			
		||||
            {
 | 
			
		||||
                "queryset": self.get_playlist_queryset(episode),
 | 
			
		||||
                "initial": {
 | 
			
		||||
                    "program": episode.parent_id,
 | 
			
		||||
                    "episode": episode.id,
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        return forms.SoundFormSet(**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)
 | 
			
		||||
 | 
			
		||||
    def post(self, request, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ import './index.js'
 | 
			
		||||
 | 
			
		||||
import App from './app';
 | 
			
		||||
import {admin as components} from './components'
 | 
			
		||||
import Track from './track'
 | 
			
		||||
 | 
			
		||||
const AdminApp = {
 | 
			
		||||
    ...App,
 | 
			
		||||
@ -12,7 +11,6 @@ const AdminApp = {
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            ...super.data,
 | 
			
		||||
            Track,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,12 +6,17 @@
 | 
			
		||||
                <div class="modal-card-title">
 | 
			
		||||
                    <slot name="title">{{ title }}</slot>
 | 
			
		||||
                </div>
 | 
			
		||||
                <button type="button" class="delete square" aria-label="close" @click="close">
 | 
			
		||||
                    <span class="icon">
 | 
			
		||||
                        <i class="fa fa-close"></i>
 | 
			
		||||
                    </span>
 | 
			
		||||
                </button>
 | 
			
		||||
            </header>
 | 
			
		||||
            <section class="modal-card-body">
 | 
			
		||||
                <slot name="default"></slot>
 | 
			
		||||
                <slot name="default" :item="item"></slot>
 | 
			
		||||
            </section>
 | 
			
		||||
            <div class="modal-card-foot align-right">
 | 
			
		||||
                <slot name="footer" :close="close"></slot>
 | 
			
		||||
                <slot name="footer" :item="item" :close="close"></slot>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </section>
 | 
			
		||||
@ -24,13 +29,24 @@ export default {
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            ///! If true, modal is open
 | 
			
		||||
            active: false,
 | 
			
		||||
            ///! Item or data passed down to slots.
 | 
			
		||||
            item: null,
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        open() { this.active = true; },
 | 
			
		||||
        close() { this.active = false; },
 | 
			
		||||
        ///! Open modal dialog. Set provided `item` to dialog's one.
 | 
			
		||||
        open(item=null) {
 | 
			
		||||
            this.active = true
 | 
			
		||||
            this.item = item
 | 
			
		||||
        },
 | 
			
		||||
        ///! Close modal and reset item to null.
 | 
			
		||||
        close() {
 | 
			
		||||
            this.active = false
 | 
			
		||||
            this.item = null
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -37,10 +37,6 @@
 | 
			
		||||
                <span class="fas fa-pause" v-if="playing"></span>
 | 
			
		||||
                <span class="fas fa-play" v-else></span>
 | 
			
		||||
            </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' : '']">
 | 
			
		||||
                <slot name="content" :loaded="loaded" :live="live" :current="current"></slot>
 | 
			
		||||
            </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]">
 | 
			
		||||
            <!-- upload -->
 | 
			
		||||
            <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>
 | 
			
		||||
                    <input type="file" ref="uploadFile" :name="uploadFieldName" @change="onSubmit"/>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,7 @@
 | 
			
		||||
 | 
			
		||||
        </section>
 | 
			
		||||
        <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"
 | 
			
		||||
                    :orderable="true" @move="listItemMove" @colmove="columnMove"
 | 
			
		||||
                    @cell="onCellEvent">
 | 
			
		||||
@ -49,8 +49,8 @@
 | 
			
		||||
                    <td class="align-right pr-0">
 | 
			
		||||
                        <button type="button" class="button square"
 | 
			
		||||
                                @click.stop="items.splice(data.row,1)"
 | 
			
		||||
                                :title="labels.remove_track"
 | 
			
		||||
                                :aria-label="labels.remove_track">
 | 
			
		||||
                                :title="labels.remove_item"
 | 
			
		||||
                                :aria-label="labels.remove_item">
 | 
			
		||||
                            <span class="icon"><i class="fa fa-trash" /></span>
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </td>
 | 
			
		||||
@ -82,8 +82,8 @@
 | 
			
		||||
                </button>
 | 
			
		||||
                <button type="button" class="button square is-primary p-2" v-if="page == Page.List"
 | 
			
		||||
                        @click="this.set.push(new this.set.model())"
 | 
			
		||||
                        :title="labels.add_track"
 | 
			
		||||
                        :aria-label="labels.add_track"
 | 
			
		||||
                        :title="labels.add_item"
 | 
			
		||||
                        :aria-label="labels.add_item"
 | 
			
		||||
                        >
 | 
			
		||||
                    <span class="icon"><i class="fa fa-plus"/></span>
 | 
			
		||||
                </button>
 | 
			
		||||
@ -99,7 +99,7 @@
 | 
			
		||||
                    <table class="table is-bordered"
 | 
			
		||||
                            style="vertical-align: middle">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <a-row :columns="columns" :item="labels"
 | 
			
		||||
                            <a-row :columns="columns" :item="initData.fields"
 | 
			
		||||
                                    @move="formatMove" :orderable="true">
 | 
			
		||||
                                <template v-slot:cell-after="{cell}">
 | 
			
		||||
                                    <td style="cursor:pointer;" v-if="cell.col < columns.length-1">
 | 
			
		||||
@ -149,8 +149,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
 | 
			
		||||
import {Set} from '../model'
 | 
			
		||||
import Track from '../track'
 | 
			
		||||
import Model, {Set} from '../model'
 | 
			
		||||
 | 
			
		||||
import AActionButton from './AActionButton'
 | 
			
		||||
import ARow from './ARow'
 | 
			
		||||
@ -165,6 +164,7 @@ export const Page = {
 | 
			
		||||
export default {
 | 
			
		||||
    components: { AActionButton, ARow, ARows, AModal },
 | 
			
		||||
    props: {
 | 
			
		||||
        ///! initial data as: {items: [], fields: {column_name: label, settings: {}}
 | 
			
		||||
        initData: Object,
 | 
			
		||||
        dataPrefix: String,
 | 
			
		||||
        labels: Object,
 | 
			
		||||
@ -182,7 +182,7 @@ export default {
 | 
			
		||||
        return {
 | 
			
		||||
            Page: Page,
 | 
			
		||||
            page: Page.Text,
 | 
			
		||||
            set: new Set(Track),
 | 
			
		||||
            set: new Set(Model),
 | 
			
		||||
            extraData: {},
 | 
			
		||||
            settings,
 | 
			
		||||
            savedSettings: cloneDeep(settings),
 | 
			
		||||
 | 
			
		||||
@ -7,15 +7,16 @@ import AList from './AList'
 | 
			
		||||
import APage from './APage'
 | 
			
		||||
import APlayer from './APlayer'
 | 
			
		||||
import APlaylist from './APlaylist'
 | 
			
		||||
import ATracklistEditor from './ATracklistEditor'
 | 
			
		||||
import AProgress from './AProgress'
 | 
			
		||||
import ASoundItem from './ASoundItem'
 | 
			
		||||
import ASwitch from './ASwitch'
 | 
			
		||||
import AStatistics from './AStatistics'
 | 
			
		||||
import AStreamer from './AStreamer'
 | 
			
		||||
 | 
			
		||||
import AModal from "./AModal"
 | 
			
		||||
import ASelectFile from "./ASelectFile"
 | 
			
		||||
import AStatistics from './AStatistics'
 | 
			
		||||
import AStreamer from './AStreamer'
 | 
			
		||||
import ATracklistEditor from './ATracklistEditor'
 | 
			
		||||
import APlaylistEditor from './APlaylistEditor'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Core components
 | 
			
		||||
@ -35,5 +36,5 @@ export const admin = {
 | 
			
		||||
 | 
			
		||||
export const dashboard = {
 | 
			
		||||
    ...base,
 | 
			
		||||
    AActionButton, ASelectFile, AModal, ATracklistEditor,
 | 
			
		||||
    AActionButton, ASelectFile, AModal, ATracklistEditor, APlaylistEditor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,13 +8,11 @@ const DashboardApp = {
 | 
			
		||||
    ...App,
 | 
			
		||||
    components: {...App.components, ...components},
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            editPageContent: null,
 | 
			
		||||
            modalItem: null,
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    */
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        ...App.methods,
 | 
			
		||||
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
import Model from './model'
 | 
			
		||||
 | 
			
		||||
export default class Track extends Model {
 | 
			
		||||
    static getId(data) { return data.pk }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user