manage program editors
This commit is contained in:
		@ -1,5 +1,7 @@
 | 
				
			|||||||
import django_filters as filters
 | 
					from django.contrib.auth.models import User
 | 
				
			||||||
 | 
					from django.db.models import Q
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					import django_filters as filters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import models
 | 
					from . import models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -10,6 +12,8 @@ __all__ = (
 | 
				
			|||||||
    "ImageFilterSet",
 | 
					    "ImageFilterSet",
 | 
				
			||||||
    "SoundFilterSet",
 | 
					    "SoundFilterSet",
 | 
				
			||||||
    "TrackFilterSet",
 | 
					    "TrackFilterSet",
 | 
				
			||||||
 | 
					    "UserFilterSet",
 | 
				
			||||||
 | 
					    "UserGroupFilterSet",
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -67,3 +71,26 @@ class TrackFilterSet(filters.FilterSet):
 | 
				
			|||||||
    artist = filters.CharFilter(field_name="artist", lookup_expr="icontains")
 | 
					    artist = filters.CharFilter(field_name="artist", lookup_expr="icontains")
 | 
				
			||||||
    album = filters.CharFilter(field_name="album", lookup_expr="icontains")
 | 
					    album = filters.CharFilter(field_name="album", lookup_expr="icontains")
 | 
				
			||||||
    title = filters.CharFilter(field_name="title", lookup_expr="icontains")
 | 
					    title = filters.CharFilter(field_name="title", lookup_expr="icontains")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UserFilterSet(filters.FilterSet):
 | 
				
			||||||
 | 
					    search = filters.CharFilter(field_name="search", method="search_filter")
 | 
				
			||||||
 | 
					    in_group = filters.NumberFilter(field_name="in_group", method="in_group_filter")
 | 
				
			||||||
 | 
					    not_in_group = filters.NumberFilter(field_name="not_in_group", method="not_in_group_filter")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def in_group_filter(self, queryset, name, value):
 | 
				
			||||||
 | 
					        return queryset.filter(groups__in=[value])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def not_in_group_filter(self, queryset, name, value):
 | 
				
			||||||
 | 
					        return queryset.exclude(groups__in=[value])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def search_filter(self, queryset, name, value):
 | 
				
			||||||
 | 
					        return queryset.filter(
 | 
				
			||||||
 | 
					            Q(username__icontains=value) | Q(first_name__icontains=value) | Q(last_name__icontains=value)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UserGroupFilterSet(filters.FilterSet):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = User.groups.through
 | 
				
			||||||
 | 
					        fields = ["group", "user"]
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
from . import widgets
 | 
					from . import widgets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .auth import UserGroupFormSet
 | 
					 | 
				
			||||||
from .episode import EpisodeForm, EpisodeSoundFormSet
 | 
					from .episode import EpisodeForm, EpisodeSoundFormSet
 | 
				
			||||||
from .program import ProgramForm
 | 
					from .program import ProgramForm
 | 
				
			||||||
from .page import CommentForm, ImageForm, PageForm, ChildPageForm
 | 
					from .page import CommentForm, ImageForm, PageForm, ChildPageForm
 | 
				
			||||||
@ -19,5 +18,4 @@ __all__ = (
 | 
				
			|||||||
    ChildPageForm,
 | 
					    ChildPageForm,
 | 
				
			||||||
    SoundForm,
 | 
					    SoundForm,
 | 
				
			||||||
    SoundCreateForm,
 | 
					    SoundCreateForm,
 | 
				
			||||||
    UserGroupFormSet,
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,20 +0,0 @@
 | 
				
			|||||||
from django import forms
 | 
					 | 
				
			||||||
from django.forms.models import modelformset_factory
 | 
					 | 
				
			||||||
from django.contrib.auth.models import User
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from aircox.forms import widgets
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__all__ = ("UserGroupFormSet",)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
UserGroupFormSet = modelformset_factory(
 | 
					 | 
				
			||||||
    User.groups.through,
 | 
					 | 
				
			||||||
    fields=("group", "user"),
 | 
					 | 
				
			||||||
    widgets={
 | 
					 | 
				
			||||||
        "group": forms.HiddenInput(),
 | 
					 | 
				
			||||||
        "user": widgets.VueAutoComplete("api:usergroup-autocomplete", lookup="username"),
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    extra=0,
 | 
					 | 
				
			||||||
    can_delete=True,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
@ -30,17 +30,10 @@ Usefull context:
 | 
				
			|||||||
        <script type="module" src="{{vue_url}}"></script>
 | 
					        <script type="module" src="{{vue_url}}"></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <link rel="stylesheet" type="text/css" href="{% static "fontawesome-free/css/all.min.css" %}"/>
 | 
					        <link rel="stylesheet" type="text/css" href="{% static "fontawesome-free/css/all.min.css" %}"/>
 | 
				
			||||||
        <link rel="stylesheet" type="text/css" href="{% static "aircox/style.css" %}"/>
 | 
					        <link rel="stylesheet" type="text/css" href="{% static "aircox/index.css" %}"/>
 | 
				
			||||||
        <script type="module" src="{% static "aircox/public.js" %}"></script>
 | 
					        <link rel="stylesheet" type="text/css" href="{% static "aircox/public.css" %}"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {% comment %}
 | 
					        <script type="module" src="{% if app_js_url %}{{ app_js_url }}{% else %}{% static "aircox/public.js" %}{% endif %}"></script>
 | 
				
			||||||
        <link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-common.css" %}"/>
 | 
					 | 
				
			||||||
        <link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-vendors.css" %}"/>
 | 
					 | 
				
			||||||
        <link rel="stylesheet" type="text/css" href="{% static "aircox/css/public.css" %}"/>
 | 
					 | 
				
			||||||
        <script src="{% static "aircox/js/chunk-common.js" %}"></script>
 | 
					 | 
				
			||||||
        <script src="{% static "aircox/js/chunk-vendors.js" %}"></script>
 | 
					 | 
				
			||||||
        <script src="{% static "aircox/js/public.js" %}"></script>
 | 
					 | 
				
			||||||
        {% endcomment %}
 | 
					 | 
				
			||||||
        {% endblock %}
 | 
					        {% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <title>
 | 
					        <title>
 | 
				
			||||||
 | 
				
			|||||||
@ -2,9 +2,9 @@
 | 
				
			|||||||
{% load static i18n %}
 | 
					{% load static i18n %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block assets %}
 | 
					{% block assets %}
 | 
				
			||||||
 | 
					{% static "aircox/admin.js" as app_js_url %}
 | 
				
			||||||
{{ block.super }}
 | 
					{{ block.super }}
 | 
				
			||||||
<script src="{% static "aircox/js/dashboard.js" %}"></script>
 | 
					<link rel="stylesheet" type="text/css" href="{% static "aircox/admin.css" %}"/>
 | 
				
			||||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/admin.css" %}"/>
 | 
					 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block head-title %}
 | 
					{% block head-title %}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										18
									
								
								aircox/templates/aircox/dashboard/widgets/group_users.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								aircox/templates/aircox/dashboard/widgets/group_users.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					{% load i18n %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<a-modal ref="group-users-modal">
 | 
				
			||||||
 | 
					    <template #title="{item}">[[ item?.name ]]</template>
 | 
				
			||||||
 | 
					    <template #default="{item}">
 | 
				
			||||||
 | 
					        <a-group-users v-if="item" ref="group-users"
 | 
				
			||||||
 | 
					            :url="'{% url 'api:usergroup-list' %}?group=' + item.id"
 | 
				
			||||||
 | 
					            commit-url="{% url 'api:usergroup-commit' %}"
 | 
				
			||||||
 | 
					            :search-url="'{% url 'api:user-autocomplete' %}?search=${query}&group=' + item.id"
 | 
				
			||||||
 | 
					            :initials="{group_id: item.id }"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					    <template #footer="{item, close}">
 | 
				
			||||||
 | 
					        <button type="button" class="button" @click="$refs['group-users'].save(); close()">
 | 
				
			||||||
 | 
					            Save
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					</a-modal>
 | 
				
			||||||
@ -18,11 +18,10 @@ Context:
 | 
				
			|||||||
    {{ formset.non_form_errors }}
 | 
					    {{ formset.non_form_errors }}
 | 
				
			||||||
    <!-- formset.management_form -->
 | 
					    <!-- formset.management_form -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <{{ tag|default:"a-form-set" }}
 | 
					    <{{ tag|default:"a-form-set" }} ref="formset"
 | 
				
			||||||
            {% block tag-attrs %}
 | 
					            {% block tag-attrs %}
 | 
				
			||||||
            :form-data="{{ formset_data|json }}"
 | 
					            :form-data="{{ formset_data|json }}"
 | 
				
			||||||
            :labels="window.aircox.labels"
 | 
					            :labels="window.aircox.labels"
 | 
				
			||||||
            :init-data="{% formset_inline_data formset=formset %}"
 | 
					 | 
				
			||||||
            :columns="[{% for n, f in fields.items %}{% if not f.widget.is_hidden %}'{{ n }}',{% endif %}{% endfor %} ]"
 | 
					            :columns="[{% for n, f in fields.items %}{% if not f.widget.is_hidden %}'{{ n }}',{% endif %}{% endfor %} ]"
 | 
				
			||||||
            settings-url="{% url "api:user-settings" %}"
 | 
					            settings-url="{% url "api:user-settings" %}"
 | 
				
			||||||
            data-prefix="{{ formset.prefix }}-"
 | 
					            data-prefix="{{ formset.prefix }}-"
 | 
				
			||||||
@ -40,7 +39,7 @@ Context:
 | 
				
			|||||||
        </template>
 | 
					        </template>
 | 
				
			||||||
        {% for name, field in fields.items %}
 | 
					        {% for name, field in fields.items %}
 | 
				
			||||||
        {% if not field.widget.is_hidden and not field.is_readonly %}
 | 
					        {% if not field.widget.is_hidden and not field.is_readonly %}
 | 
				
			||||||
        <template v-slot:control-{{ name }}="{item,cell,value,attr,emit,inputName}">
 | 
					        <template v-slot:control-{{ name }}="{context,item,cell,value,attr,emit,inputName}">
 | 
				
			||||||
            {% block row-control %}
 | 
					            {% block row-control %}
 | 
				
			||||||
            {% include "./v_form_field.html" with value="item.data."|add:name name="inputName" %}
 | 
					            {% include "./v_form_field.html" with value="item.data."|add:name name="inputName" %}
 | 
				
			||||||
            {% endblock %}
 | 
					            {% endblock %}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,6 @@
 | 
				
			|||||||
{% extends "./dashboard/base.html" %}
 | 
					{% extends "./dashboard/base.html" %}
 | 
				
			||||||
{% load static aircox_admin i18n %}
 | 
					{% load static aircox_admin i18n %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block assets %}
 | 
					 | 
				
			||||||
{{ block.super }}
 | 
					 | 
				
			||||||
<script src="{% static "aircox/js/dashboard.js" %}"></script>
 | 
					 | 
				
			||||||
{% endblock %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{% block init-scripts %}
 | 
					{% block init-scripts %}
 | 
				
			||||||
aircox.labels = {% inline_labels %}
 | 
					aircox.labels = {% inline_labels %}
 | 
				
			||||||
{{ block.super }}
 | 
					{{ block.super }}
 | 
				
			||||||
@ -86,9 +81,11 @@ aircox.labels = {% inline_labels %}
 | 
				
			|||||||
    {% endblock %}
 | 
					    {% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <hr/>
 | 
					    <hr/>
 | 
				
			||||||
    <div class="has-text-right">
 | 
					    <div class="flex-row">
 | 
				
			||||||
        <button type="submit" class="button">{% translate "Update" %}</button>
 | 
					        <div class="flex-grow-1">{% block page-form-actions %}{% endblock %}</div>
 | 
				
			||||||
    </div>
 | 
					        <div class="has-text-right">
 | 
				
			||||||
 | 
					            <button type="submit" class="button">{% translate "Update" %}</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
</section>
 | 
					</section>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
				
			|||||||
@ -6,13 +6,13 @@
 | 
				
			|||||||
  {{ form.media }}
 | 
					  {{ form.media }}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block page-form %}
 | 
					{% block page-form-actions %}
 | 
				
			||||||
//////
 | 
					{% if request.user.is_superuser %}
 | 
				
			||||||
{{ block.super }}
 | 
					<button type="button"
 | 
				
			||||||
 | 
					    class="button secondary"
 | 
				
			||||||
 | 
					    @click="$refs['group-users-modal'].open({id: {{ object.editors_group_id }}, name: '{{ object.editors_group.name }}' })">Editors</button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% if editors_formset %}
 | 
					{% include "./dashboard/widgets/group_users.html" %}
 | 
				
			||||||
<hr/>
 | 
					{{ block.super }}
 | 
				
			||||||
<h2 class="title is-2">{% translate "Editors" %}</h2>
 | 
					 | 
				
			||||||
{% include "./widgets/usergroup_formset.html" with formset=editors_formset formset_data=editors_formset_data tag_id="usergroup_formset" %}
 | 
					 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
<a-autocomplete
 | 
					<a-autocomplete
 | 
				
			||||||
    url="{{url}}"
 | 
					    url="{{url}}"
 | 
				
			||||||
    name="{{ widget.name }}"{% if widget.value != None %} model-value="{{ widget.value|stringformat:'s' }}"{% endif %}
 | 
					    {% if ":name" not in widget.attrs %}name="{{ name|default:widget.name }}"{% endif %}{% if widget.value != None %} model-value="{{ widget.value|stringformat:'s' }}"{% endif %}
 | 
				
			||||||
    {% include "django/forms/widgets/attrs.html" %} />
 | 
					    {% include "django/forms/widgets/attrs.html" %} {{ extra|default:"" }}/>
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block row-control %}
 | 
					{% block row-control %}
 | 
				
			||||||
{% if name == 'user' %}
 | 
					{% if name == 'user' %}
 | 
				
			||||||
    {% form_field field name value %}
 | 
					    {% form_field field "inputName" value %}
 | 
				
			||||||
{% else %}
 | 
					{% else %}
 | 
				
			||||||
    {{ block.super }}
 | 
					    {{ block.super }}
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
 | 
				
			|||||||
@ -45,7 +45,7 @@ def do_formset_inline_data(context, formset):
 | 
				
			|||||||
        # hack for sound list
 | 
					        # hack for sound list
 | 
				
			||||||
        if duration := item.get("duration"):
 | 
					        if duration := item.get("duration"):
 | 
				
			||||||
            item["duration"] = duration.strftime("%H:%M")
 | 
					            item["duration"] = duration.strftime("%H:%M")
 | 
				
			||||||
        if sound := getattr(form.instance, "sound"):
 | 
					        if sound := getattr(form.instance, "sound", None):
 | 
				
			||||||
            item["name"] = sound.name
 | 
					            item["name"] = sound.name
 | 
				
			||||||
            fields["name"] = str(_("Sound")).capitalize()
 | 
					            fields["name"] = str(_("Sound")).capitalize()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -55,7 +55,7 @@ def do_formset_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, "fields": fields}
 | 
					    data = {"items": items, "fields": fields, "initial": formset.initial and formset.initial[0]}
 | 
				
			||||||
    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
 | 
				
			||||||
 | 
				
			|||||||
@ -21,11 +21,13 @@ register_converter(WeekConverter, "week")
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router = DefaultRouter()
 | 
					router = DefaultRouter()
 | 
				
			||||||
 | 
					router.register("user", viewsets.UserViewSet, basename="user")
 | 
				
			||||||
 | 
					router.register("usergroup", viewsets.UserGroupViewSet, basename="usergroup")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.register("images", viewsets.ImageViewSet, basename="image")
 | 
					router.register("images", viewsets.ImageViewSet, basename="image")
 | 
				
			||||||
router.register("sound", viewsets.SoundViewSet, basename="sound")
 | 
					router.register("sound", viewsets.SoundViewSet, basename="sound")
 | 
				
			||||||
router.register("track", viewsets.TrackROViewSet, basename="track")
 | 
					router.register("track", viewsets.TrackROViewSet, basename="track")
 | 
				
			||||||
router.register("comment", viewsets.CommentViewSet, basename="comment")
 | 
					router.register("comment", viewsets.CommentViewSet, basename="comment")
 | 
				
			||||||
router.register("usergroup", viewsets.UserGroupViewSet, basename="usergroup")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
api = [
 | 
					api = [
 | 
				
			||||||
 | 
				
			|||||||
@ -115,6 +115,9 @@ class VueFormDataMixin:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # Note: values corresponds to AFormSet expected one
 | 
					    # Note: values corresponds to AFormSet expected one
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_form_items(self, formset):
 | 
				
			||||||
 | 
					        return [form.initial for form in formset.forms]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_form_field_data(self, form, values=None):
 | 
					    def get_form_field_data(self, form, values=None):
 | 
				
			||||||
        """Return form fields as data."""
 | 
					        """Return form fields as data."""
 | 
				
			||||||
        model = form.Meta.model
 | 
					        model = form.Meta.model
 | 
				
			||||||
@ -140,5 +143,7 @@ class VueFormDataMixin:
 | 
				
			|||||||
                "max_num_forms": formset.max_num,
 | 
					                "max_num_forms": formset.max_num,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "fields": self.get_form_field_data(formset.form, field_values),
 | 
					            "fields": self.get_form_field_data(formset.form, field_values),
 | 
				
			||||||
 | 
					            "initial_extra": formset.initial_extra and formset.initial_extra[0],
 | 
				
			||||||
 | 
					            "initials": self.get_form_items(formset),
 | 
				
			||||||
            **kwargs,
 | 
					            **kwargs,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
import random
 | 
					import random
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib.auth.models import User
 | 
					 | 
				
			||||||
from django.contrib.auth.mixins import UserPassesTestMixin
 | 
					from django.contrib.auth.mixins import UserPassesTestMixin
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -61,31 +60,3 @@ class ProgramUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
 | 
				
			|||||||
    def test_func(self):
 | 
					    def test_func(self):
 | 
				
			||||||
        obj = self.get_object()
 | 
					        obj = self.get_object()
 | 
				
			||||||
        return permissions.program.can(self.request.user, "update", obj)
 | 
					        return permissions.program.can(self.request.user, "update", obj)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_editors_queryset(self, program):
 | 
					 | 
				
			||||||
        # TODO: provide username in formset initials
 | 
					 | 
				
			||||||
        return User.groups.through.objects.filter(group_id=program.editors_group_id).order_by("user__username")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_editors_formset(self, program, **kwargs):
 | 
					 | 
				
			||||||
        return forms.UserGroupFormSet(
 | 
					 | 
				
			||||||
            **{
 | 
					 | 
				
			||||||
                **kwargs,
 | 
					 | 
				
			||||||
                "prefix": "editors",
 | 
					 | 
				
			||||||
                "queryset": self.get_editors_queryset(program),
 | 
					 | 
				
			||||||
                "initial": {
 | 
					 | 
				
			||||||
                    "group": program.editors_group_id,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_context_data(self, editors_formset=None, **kwargs):
 | 
					 | 
				
			||||||
        # TODO: use group and permission system
 | 
					 | 
				
			||||||
        if self.request.user.is_superuser:
 | 
					 | 
				
			||||||
            if editors_formset is None:
 | 
					 | 
				
			||||||
                editors_formset = self.get_editors_formset(self.object)
 | 
					 | 
				
			||||||
            kwargs["editors_formset_data"] = self.get_formset_data(
 | 
					 | 
				
			||||||
                editors_formset, {"group": self.object.editors_group_id}
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        context = super().get_context_data(editors_formset=editors_formset, **kwargs)
 | 
					 | 
				
			||||||
        return context
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -40,6 +40,49 @@ class AutocompleteMixin:
 | 
				
			|||||||
        return self.list(request)
 | 
					        return self.list(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ListCommitMixin:
 | 
				
			||||||
 | 
					    @action(name="commit", detail=False, methods=["POST"])
 | 
				
			||||||
 | 
					    def commit(self, request):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Data:
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "delete": [pk],
 | 
				
			||||||
 | 
					                "update": [{pk, **object}],
 | 
				
			||||||
 | 
					                "create": [object_data]
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Return:
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "deleted": [pk],
 | 
				
			||||||
 | 
					                "updated": [object],
 | 
				
			||||||
 | 
					                "created": [object],
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        queryset = self.get_queryset()
 | 
				
			||||||
 | 
					        resp = {"deleted": [], "updated": [], "created": []}
 | 
				
			||||||
 | 
					        if ids := request.data.get("delete"):
 | 
				
			||||||
 | 
					            q = queryset.filter(id__in=ids)
 | 
				
			||||||
 | 
					            resp["deleted"] = list(q.values_list("id", flat=True))
 | 
				
			||||||
 | 
					            q.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # TODO: bulk save and update
 | 
				
			||||||
 | 
					        if items := request.data.get("update"):
 | 
				
			||||||
 | 
					            resp["updated"] = self._commit_save_many(items)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if items := request.data.get("create"):
 | 
				
			||||||
 | 
					            resp["created"] = self._commit_save_many(items)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response(data=resp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _commit_save_many(self, data):
 | 
				
			||||||
 | 
					        ser = self.get_serializer(data=data, many=True)
 | 
				
			||||||
 | 
					        ser.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        items = ser.save()
 | 
				
			||||||
 | 
					        ser = self.get_serializer(items, many=True)
 | 
				
			||||||
 | 
					        return ser.data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ImageViewSet(viewsets.ModelViewSet):
 | 
					class ImageViewSet(viewsets.ModelViewSet):
 | 
				
			||||||
    parsers = (parsers.MultiPartParser,)
 | 
					    parsers = (parsers.MultiPartParser,)
 | 
				
			||||||
    permissions = (permissions.IsAuthenticatedOrReadOnly,)
 | 
					    permissions = (permissions.IsAuthenticatedOrReadOnly,)
 | 
				
			||||||
@ -84,7 +127,6 @@ class TrackROViewSet(AutocompleteMixin, viewsets.ReadOnlyModelViewSet):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    serializer_class = serializers.admin.TrackSerializer
 | 
					    serializer_class = serializers.admin.TrackSerializer
 | 
				
			||||||
    permission_classes = (permissions.IsAuthenticated,)
 | 
					    permission_classes = (permissions.IsAuthenticated,)
 | 
				
			||||||
    filter_backends = (drf_filters.DjangoFilterBackend,)
 | 
					 | 
				
			||||||
    filterset_class = filters.TrackFilterSet
 | 
					    filterset_class = filters.TrackFilterSet
 | 
				
			||||||
    queryset = models.Track.objects.all()
 | 
					    queryset = models.Track.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -96,10 +138,19 @@ class CommentViewSet(viewsets.ModelViewSet):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- admin
 | 
					# --- admin
 | 
				
			||||||
class UserGroupViewSet(AutocompleteMixin, viewsets.ModelViewSet):
 | 
					class UserViewSet(AutocompleteMixin, viewsets.ModelViewSet):
 | 
				
			||||||
 | 
					    serializer_class = serializers.auth.UserSerializer
 | 
				
			||||||
 | 
					    permission_classes = (permissions.IsAdminUser,)
 | 
				
			||||||
 | 
					    filterset_class = filters.UserFilterSet
 | 
				
			||||||
 | 
					    queryset = User.objects.all().distinct().order_by("username")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UserGroupViewSet(ListCommitMixin, viewsets.ModelViewSet):
 | 
				
			||||||
    serializer_class = serializers.auth.UserGroupSerializer
 | 
					    serializer_class = serializers.auth.UserGroupSerializer
 | 
				
			||||||
    permission_classes = (permissions.IsAdminUser,)
 | 
					    permission_classes = (permissions.IsAdminUser,)
 | 
				
			||||||
    queryset = User.groups.through.objects.all().distinct().order_by("user__username")
 | 
					    filterset_class = filters.UserGroupFilterSet
 | 
				
			||||||
 | 
					    model = User.groups.through
 | 
				
			||||||
 | 
					    queryset = model.objects.all().distinct().order_by("user__username")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserSettingsViewSet(viewsets.ViewSet):
 | 
					class UserSettingsViewSet(viewsets.ViewSet):
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import './styles/admin.scss'
 | 
				
			|||||||
import './index.js'
 | 
					import './index.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import App from './app';
 | 
					import App from './app';
 | 
				
			||||||
import {admin as components} from './components'
 | 
					import components from './components/admin.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AdminApp = {
 | 
					const AdminApp = {
 | 
				
			||||||
    ...App,
 | 
					    ...App,
 | 
				
			||||||
 | 
				
			|||||||
@ -17,10 +17,22 @@ const App = {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    methods: {
 | 
					    methods: {
 | 
				
			||||||
 | 
					        //! Delete elements from DOM using provided selector.
 | 
				
			||||||
        deleteElements(sel) {
 | 
					        deleteElements(sel) {
 | 
				
			||||||
            for(var el of document.querySelectorAll(sel))
 | 
					            for(var el of document.querySelectorAll(sel))
 | 
				
			||||||
                el.parentNode.removeChild(el)
 | 
					                el.parentNode.removeChild(el)
 | 
				
			||||||
        }
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        //! File has been selected
 | 
				
			||||||
 | 
					        //! TODO: replace using regular ref and bindings.
 | 
				
			||||||
 | 
					        fileSelected(select, input, preview) {
 | 
				
			||||||
 | 
					            const item = this.$refs[select].item
 | 
				
			||||||
 | 
					            if(item) {
 | 
				
			||||||
 | 
					                this.$refs[input].value = item.id
 | 
				
			||||||
 | 
					                if(preview)
 | 
				
			||||||
 | 
					                    preview.src = item.file
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -20,24 +20,23 @@
 | 
				
			|||||||
            <span class="is-inline-block" v-if="selected">
 | 
					            <span class="is-inline-block" v-if="selected">
 | 
				
			||||||
                <slot name="button" :index="selectedIndex" :item="selected"
 | 
					                <slot name="button" :index="selectedIndex" :item="selected"
 | 
				
			||||||
                    :value-field="valueField" :labelField="labelField">
 | 
					                    :value-field="valueField" :labelField="labelField">
 | 
				
			||||||
                {{ labelField && selected.data[labelField] || selected }}
 | 
					                {{ selectedLabel }}
 | 
				
			||||||
                </slot>
 | 
					                </slot>
 | 
				
			||||||
            </span>
 | 
					            </span>
 | 
				
			||||||
        </a>
 | 
					        </a>
 | 
				
			||||||
        <div :class="dropdownClass">
 | 
					        <div :class="dropdownClass">
 | 
				
			||||||
            <div class="dropdown-menu is-fullwidth">
 | 
					            <div class="dropdown-menu is-fullwidth">
 | 
				
			||||||
                <div class="dropdown-content" style="overflow: hidden">
 | 
					                <div class="dropdown-content" style="overflow: hidden">
 | 
				
			||||||
                    <a v-for="(item, index) in items" :key="item.id"
 | 
					                    <span v-for="(item, index) in items" :key="item.id"
 | 
				
			||||||
                        href="#" :data-autocomplete-index="index"
 | 
					                        :data-autocomplete-index="index"
 | 
				
			||||||
                        @click="select(index, false, false)"
 | 
					                        @click="select(index, false, false)"
 | 
				
			||||||
                        :class="['dropdown-item', (index == this.cursor) ? 'is-active':'']"
 | 
					                        :class="['dropdown-item', (index == this.cursor) ? 'is-active':'']"
 | 
				
			||||||
                        :title="labelField && item.data[labelField] || item"
 | 
					 | 
				
			||||||
                        tabindex="-1">
 | 
					                        tabindex="-1">
 | 
				
			||||||
                        <slot name="item" :index="index" :item="item" :value-field="valueField"
 | 
					                        <slot name="item" :index="index" :item="item" :value-field="valueField"
 | 
				
			||||||
                            :labelField="labelField">
 | 
					                            :labelField="labelField">
 | 
				
			||||||
                        {{ labelField && item.data[labelField] || item }}
 | 
					                        {{ getValue(item, labelField) || item }}
 | 
				
			||||||
                        </slot>
 | 
					                        </slot>
 | 
				
			||||||
                    </a>
 | 
					                    </span>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@ -56,12 +55,14 @@ export default {
 | 
				
			|||||||
    props: {
 | 
					    props: {
 | 
				
			||||||
        //! Search URL (where `${query}` is replaced by search term)
 | 
					        //! Search URL (where `${query}` is replaced by search term)
 | 
				
			||||||
        url: String,
 | 
					        url: String,
 | 
				
			||||||
 | 
					        //! Extra GET url parameters
 | 
				
			||||||
 | 
					        urlParams: Object,
 | 
				
			||||||
        //! Items' model
 | 
					        //! Items' model
 | 
				
			||||||
        model: Function,
 | 
					        model: Function,
 | 
				
			||||||
        //! Input tag class
 | 
					        //! Input tag class
 | 
				
			||||||
        inputClass: Array,
 | 
					        inputClass: Array,
 | 
				
			||||||
        //! input text placeholder
 | 
					        //! input text placeholder
 | 
				
			||||||
        placeholder: String,
 | 
					        placeholder: Object,
 | 
				
			||||||
        //! input form field name
 | 
					        //! input form field name
 | 
				
			||||||
        name: String,
 | 
					        name: String,
 | 
				
			||||||
        //! Field on items to use as label
 | 
					        //! Field on items to use as label
 | 
				
			||||||
@ -105,6 +106,20 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    computed: {
 | 
					    computed: {
 | 
				
			||||||
 | 
					        fullUrl() {
 | 
				
			||||||
 | 
					            if(!this.urlParams)
 | 
				
			||||||
 | 
					               return this.url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const url = new URL(this.url, window.location.origin)
 | 
				
			||||||
 | 
					            const params = new URLSearchParams(url.searchParams)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for(var key in this.urlParams)
 | 
				
			||||||
 | 
					               params.set(key, this.urlParams[key])
 | 
				
			||||||
 | 
					            const join = this.url.indexOf("?") >= 0 ? "&" : "?"
 | 
				
			||||||
 | 
					            url.search = params.toString()
 | 
				
			||||||
 | 
					            return url.href
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        isFetching() { return !!this.promise },
 | 
					        isFetching() { return !!this.promise },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        selected() {
 | 
					        selected() {
 | 
				
			||||||
@ -136,12 +151,34 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    methods: {
 | 
					    methods: {
 | 
				
			||||||
 | 
					        reset() {
 | 
				
			||||||
 | 
					            this.inputValue = ""
 | 
				
			||||||
 | 
					            this.selectedIndex = -1
 | 
				
			||||||
 | 
					            this.items = []
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // TODO: move to utils/data
 | 
				
			||||||
 | 
					        getValue(data, path=null) {
 | 
				
			||||||
 | 
					            if(!data)
 | 
				
			||||||
 | 
					                return null
 | 
				
			||||||
 | 
					            if(!path)
 | 
				
			||||||
 | 
					                return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const paths = path.split('.')
 | 
				
			||||||
 | 
					            for(const key of paths) {
 | 
				
			||||||
 | 
					                if(key in data)
 | 
				
			||||||
 | 
					                    data = data[key]
 | 
				
			||||||
 | 
					                else return null;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return data
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        itemValue(item) {
 | 
					        itemValue(item) {
 | 
				
			||||||
            return this.valueField ? item && item[this.valueField] : item;
 | 
					            return this.valueField ? this.getValue(item, this.valueField) : item;
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        itemLabel(item) {
 | 
					        itemLabel(item) {
 | 
				
			||||||
            return this.labelField ? item && item[this.labelField] : item;
 | 
					            return this.labelField ? this.getValue(item, this.labelField) : item;
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        hide() {
 | 
					        hide() {
 | 
				
			||||||
@ -183,7 +220,7 @@ export default {
 | 
				
			|||||||
            if(!this.items.length)
 | 
					            if(!this.items.length)
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var index = event.relatedTarget && Math.parseInt(event.relatedTarget.dataset.autocompleteIndex);
 | 
					            var index = event.relatedTarget && Math.floor(event.relatedTarget.dataset.autocompleteIndex);
 | 
				
			||||||
            if(index !== undefined && index !== null)
 | 
					            if(index !== undefined && index !== null)
 | 
				
			||||||
                this.select(index, false, false)
 | 
					                this.select(index, false, false)
 | 
				
			||||||
            this.cursor = -1;
 | 
					            this.cursor = -1;
 | 
				
			||||||
@ -227,7 +264,7 @@ export default {
 | 
				
			|||||||
                return
 | 
					                return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.query = query
 | 
					            this.query = query
 | 
				
			||||||
            var url = this.url.replace('${query}', query)
 | 
					            var url = this.fullUrl.replace('${query}', query).replace('%24%7Bquery%7D', query)
 | 
				
			||||||
            var promise = this.model ? this.model.fetch(url, {many:true})
 | 
					            var promise = this.model ? this.model.fetch(url, {many:true})
 | 
				
			||||||
                                     : fetch(url, Model.getOptions()).then(d => d.json())
 | 
					                                     : fetch(url, Model.getOptions()).then(d => d.json())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@
 | 
				
			|||||||
                :value="value"/>
 | 
					                :value="value"/>
 | 
				
			||||||
        </template>
 | 
					        </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <a-rows ref="rows" :set="set"
 | 
					        <a-rows ref="rows" :set="set" :context="this"
 | 
				
			||||||
                :columns="visibleFields" :columnsOrderable="columnsOrderable"
 | 
					                :columns="visibleFields" :columnsOrderable="columnsOrderable"
 | 
				
			||||||
                :orderable="orderable" @move="moveItem" @colmove="onColumnMove"
 | 
					                :orderable="orderable" @move="moveItem" @colmove="onColumnMove"
 | 
				
			||||||
                @cell="e => $emit('cell', e)">
 | 
					                @cell="e => $emit('cell', e)">
 | 
				
			||||||
@ -45,7 +45,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            <template v-for="(field,slot) of fieldSlots" v-bind:key="field.name"
 | 
					            <template v-for="(field,slot) of fieldSlots" v-bind:key="field.name"
 | 
				
			||||||
                    v-slot:[slot]="data">
 | 
					                    v-slot:[slot]="data">
 | 
				
			||||||
                <slot :name="slot" v-bind="data" :field="field" :input-name="_prefix + data.cell.row + '-' + field.name">
 | 
					                    <slot :name="slot" v-bind="data" :field="field" :input-name="_prefix + data.cell.row + '-' + field.name">
 | 
				
			||||||
                    <div class="field">
 | 
					                    <div class="field">
 | 
				
			||||||
                        <div class="control">
 | 
					                        <div class="control">
 | 
				
			||||||
                            <slot :name="'control-' + field.name" v-bind="data" :field="field" :input-name="_prefix + data.cell.row + '-' + field.name"/>
 | 
					                            <slot :name="'control-' + field.name" v-bind="data" :field="field" :input-name="_prefix + data.cell.row + '-' + field.name"/>
 | 
				
			||||||
@ -118,8 +118,6 @@
 | 
				
			|||||||
            formData: Object,
 | 
					            formData: Object,
 | 
				
			||||||
            //! Model class used for item's set
 | 
					            //! Model class used for item's set
 | 
				
			||||||
            model: {type: Function, default: Model},
 | 
					            model: {type: Function, default: Model},
 | 
				
			||||||
            //! initial data set load at mount
 | 
					 | 
				
			||||||
            initials: Array,
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data() {
 | 
					        data() {
 | 
				
			||||||
@ -184,7 +182,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            //! Reset forms to initials
 | 
					            //! Reset forms to initials
 | 
				
			||||||
            reset() {
 | 
					            reset() {
 | 
				
			||||||
                this.load(this.initials || [], true)
 | 
					                this.load(this.formData?.initials || [], true)
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										102
									
								
								assets/src/components/AGroupUsers.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								assets/src/components/AGroupUsers.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div class="a-group-users">
 | 
				
			||||||
 | 
					        <table class="table is-fullwidth">
 | 
				
			||||||
 | 
					            <thead>
 | 
				
			||||||
 | 
					                <tr>
 | 
				
			||||||
 | 
					                    <th>
 | 
				
			||||||
 | 
					                        Members
 | 
				
			||||||
 | 
					                    </th>
 | 
				
			||||||
 | 
					                    <th style="width: 1rem">
 | 
				
			||||||
 | 
					                        <span class="icon">
 | 
				
			||||||
 | 
					                            <i class="fa fa-trash"/>
 | 
				
			||||||
 | 
					                        </span>
 | 
				
			||||||
 | 
					                    </th>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					            </thead>
 | 
				
			||||||
 | 
					            <tbody>
 | 
				
			||||||
 | 
					                <template v-for="item of items" :key="item.id">
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td>
 | 
				
			||||||
 | 
					                            <b class="mr-3">{{ item.data.user.username }}</b>
 | 
				
			||||||
 | 
					                            <span class="text-light">{{ item.data.user.first_name }} {{ item.data.user.last_name }}</span>
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                        <td class="align-center">
 | 
				
			||||||
 | 
					                            <input type="checkbox" class="checkbox" @change="item.deleted = $event.target.checked">
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					            </tbody>
 | 
				
			||||||
 | 
					        </table>
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					            <label>
 | 
				
			||||||
 | 
					                <span class="icon">
 | 
				
			||||||
 | 
					                    <i class="fa fa-user"/>
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					                Add user
 | 
				
			||||||
 | 
					            </label>
 | 
				
			||||||
 | 
					            <a-autocomplete ref="autocomplete" :url="searchUrl"
 | 
				
			||||||
 | 
					                label-field="username" value-field="id"
 | 
				
			||||||
 | 
					                @select="onUserSelect">
 | 
				
			||||||
 | 
					                <template #item="{item}">
 | 
				
			||||||
 | 
					                    <b class="mr-3">{{ item.username }}</b>
 | 
				
			||||||
 | 
					                    <span class="text-light">{{ item.first_name }} {{ item.last_name }}</span>
 | 
				
			||||||
 | 
					                    —
 | 
				
			||||||
 | 
					                    <i>{{ item.email }}</i>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					            </a-autocomplete>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import Model, { Set } from "../model.js"
 | 
				
			||||||
 | 
					import AAutocomplete from "./AAutocomplete.vue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					    components: {AAutocomplete},
 | 
				
			||||||
 | 
					    props: {
 | 
				
			||||||
 | 
					        model: {type: Function, default: Model },
 | 
				
			||||||
 | 
					        // List url
 | 
				
			||||||
 | 
					        url: String,
 | 
				
			||||||
 | 
					        // User autocomplete url
 | 
				
			||||||
 | 
					        searchUrl: String,
 | 
				
			||||||
 | 
					        // POST url
 | 
				
			||||||
 | 
					        commitUrl: String,
 | 
				
			||||||
 | 
					        // default values
 | 
				
			||||||
 | 
					        initials: {type: Object, default: () => ({})},
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    data() {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            set: new Set(this.model, {url: this.url, unique: true}),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    computed: {
 | 
				
			||||||
 | 
					        items() { return this.set?.items || [] },
 | 
				
			||||||
 | 
					        user_ids() { return this.set?.items.map(i => i.data.user.id) },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    methods: {
 | 
				
			||||||
 | 
					        onUserSelect(index, item, value) {
 | 
				
			||||||
 | 
					            if(this.user_ids.indexOf(item.id) != -1)
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.set.push({
 | 
				
			||||||
 | 
					                ...this.initials,
 | 
				
			||||||
 | 
					                user: {...item},
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            this.$refs.autocomplete.reset()
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        save() {
 | 
				
			||||||
 | 
					            this.set.commit(this.commitUrl, {
 | 
				
			||||||
 | 
					                getData: i => ({...this.initials, user_id: i.data.user.id})
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mounted() {
 | 
				
			||||||
 | 
					        this.set.fetch()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
@ -4,9 +4,9 @@
 | 
				
			|||||||
        <div class="modal-card">
 | 
					        <div class="modal-card">
 | 
				
			||||||
            <header class="modal-card-head">
 | 
					            <header class="modal-card-head">
 | 
				
			||||||
                <div class="modal-card-title">
 | 
					                <div class="modal-card-title">
 | 
				
			||||||
                    <slot name="title">{{ title }}</slot>
 | 
					                    <slot name="title" :item="item">{{ title }}</slot>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <slot name="bar"></slot>
 | 
					                <slot name="bar" :item="item"></slot>
 | 
				
			||||||
                <button type="button" class="delete square" aria-label="close" @click="close">
 | 
					                <button type="button" class="delete square" aria-label="close" @click="close">
 | 
				
			||||||
                    <span class="icon">
 | 
					                    <span class="icon">
 | 
				
			||||||
                        <i class="fa fa-close"></i>
 | 
					                        <i class="fa fa-close"></i>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,25 +1,25 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <tr>
 | 
					    <tr>
 | 
				
			||||||
        <slot name="head" :item="item" :row="row"/>
 | 
					        <slot name="head" :context="context" :item="item" :row="row"/>
 | 
				
			||||||
        <template v-for="(attr,col) in columns" :key="col">
 | 
					        <template v-for="(attr,col) in columns" :key="col">
 | 
				
			||||||
            <slot name="cell-before" :item="item" :cell="cells[col]"
 | 
					            <slot name="cell-before" :context="context" :item="item" :cell="cells[col]"
 | 
				
			||||||
                    :attr="attr"/>
 | 
					                    :attr="attr"/>
 | 
				
			||||||
            <component :is="cellTag" :class="['cell', 'cell-' + attr]" :data-col="col"
 | 
					            <component :is="cellTag" :class="['cell', 'cell-' + attr]" :data-col="col"
 | 
				
			||||||
                    :draggable="orderable"
 | 
					                    :draggable="orderable"
 | 
				
			||||||
                    @dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">
 | 
					                    @dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">
 | 
				
			||||||
                <slot :name="attr" :item="item" :cell="cells[col]"
 | 
					                <slot :name="attr" :context="context" :item="item" :cell="cells[col]"
 | 
				
			||||||
                        :data="itemData" :attr="attr" :emit="cellEmit"
 | 
					                        :data="itemData" :attr="attr" :emit="cellEmit"
 | 
				
			||||||
                        :value="itemData && itemData[attr]">
 | 
					                        :value="itemData && itemData[attr]">
 | 
				
			||||||
                    {{ itemData && itemData[attr] }}
 | 
					                    {{ itemData && itemData[attr] }}
 | 
				
			||||||
                </slot>
 | 
					                </slot>
 | 
				
			||||||
                <slot name="cell" :item="item" :cell="cells[col]"
 | 
					                <slot name="cell" :context="context" :item="item" :cell="cells[col]"
 | 
				
			||||||
                        :data="itemData" :attr="attr" :emit="cellEmit"
 | 
					                        :data="itemData" :attr="attr" :emit="cellEmit"
 | 
				
			||||||
                        :value="itemData && itemData[attr]"/>
 | 
					                        :value="itemData && itemData[attr]"/>
 | 
				
			||||||
            </component>
 | 
					            </component>
 | 
				
			||||||
            <slot name="cell-after" :item="item" :col="col" :cell="cells[col]"
 | 
					            <slot name="cell-after" :context="context" :item="item" :col="col" :cell="cells[col]"
 | 
				
			||||||
                    :attr="attr"/>
 | 
					                    :attr="attr"/>
 | 
				
			||||||
        </template>
 | 
					        </template>
 | 
				
			||||||
        <slot name="tail" :item="item" :row="row"/>
 | 
					        <slot name="tail" :context="context" :item="item" :row="row"/>
 | 
				
			||||||
    </tr>
 | 
					    </tr>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
@ -30,6 +30,8 @@ export default {
 | 
				
			|||||||
    emits: ['move', 'cell'],
 | 
					    emits: ['move', 'cell'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    props: {
 | 
					    props: {
 | 
				
			||||||
 | 
					        //! Context object
 | 
				
			||||||
 | 
					        context: {type: Object, default: () => ({})},
 | 
				
			||||||
        //! Item to display in row
 | 
					        //! Item to display in row
 | 
				
			||||||
        item: {type: Object, default: () => ({})},
 | 
					        item: {type: Object, default: () => ({})},
 | 
				
			||||||
        //! Columns to display, as items' attributes
 | 
					        //! Columns to display, as items' attributes
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <table class="table is-stripped is-fullwidth">
 | 
					    <table class="table is-stripped is-fullwidth">
 | 
				
			||||||
        <thead>
 | 
					        <thead>
 | 
				
			||||||
            <a-row :columns="columnNames"
 | 
					            <a-row :context="context" :columns="columnNames"
 | 
				
			||||||
                    :orderable="columnsOrderable" cellTag="th"
 | 
					                    :orderable="columnsOrderable" cellTag="th"
 | 
				
			||||||
                    @move="moveColumn">
 | 
					                    @move="moveColumn">
 | 
				
			||||||
                <template v-if="$slots['header-head']" v-slot:head="data">
 | 
					                <template v-if="$slots['header-head']" v-slot:head="data">
 | 
				
			||||||
@ -26,7 +26,7 @@
 | 
				
			|||||||
            <slot name="head"/>
 | 
					            <slot name="head"/>
 | 
				
			||||||
            <template v-for="(item,row) in items" :key="row">
 | 
					            <template v-for="(item,row) in items" :key="row">
 | 
				
			||||||
                <!-- data-index comes from AList component drag & drop -->
 | 
					                <!-- data-index comes from AList component drag & drop -->
 | 
				
			||||||
                <a-row :item="item" :cell="{row}" :columns="columnNames" :data-index="row"
 | 
					                <a-row :context="context" :item="item" :cell="{row}" :columns="columnNames" :data-index="row"
 | 
				
			||||||
                        :data-row="row"
 | 
					                        :data-row="row"
 | 
				
			||||||
                        :draggable="orderable"
 | 
					                        :draggable="orderable"
 | 
				
			||||||
                        @dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop"
 | 
					                        @dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop"
 | 
				
			||||||
@ -54,6 +54,9 @@ const Component = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    props: {
 | 
					    props: {
 | 
				
			||||||
        ...AList.props,
 | 
					        ...AList.props,
 | 
				
			||||||
 | 
					        //! Context object
 | 
				
			||||||
 | 
					        context: {type: Object, default: () => ({})},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        //! Ordered list of columns, as objects with:
 | 
					        //! Ordered list of columns, as objects with:
 | 
				
			||||||
        //! - name: item attribute value
 | 
					        //! - name: item attribute value
 | 
				
			||||||
        //! - label: display label
 | 
					        //! - label: display label
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										23
									
								
								assets/src/components/admin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								assets/src/components/admin.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import AFileUpload from "./AFileUpload.vue"
 | 
				
			||||||
 | 
					import ASelectFile from "./ASelectFile.vue"
 | 
				
			||||||
 | 
					import AStatistics from './AStatistics.vue'
 | 
				
			||||||
 | 
					import AStreamer from './AStreamer.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import AFormSet from './AFormSet.vue'
 | 
				
			||||||
 | 
					import ATrackListEditor from './ATrackListEditor.vue'
 | 
				
			||||||
 | 
					import ASoundListEditor from './ASoundListEditor.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import AGroupUsers from "./AGroupUsers.vue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import base from "./index.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const admin = {
 | 
				
			||||||
 | 
					    ...base,
 | 
				
			||||||
 | 
					    AGroupUsers,
 | 
				
			||||||
 | 
					    AFileUpload, ASelectFile,
 | 
				
			||||||
 | 
					    AFormSet, ATrackListEditor, ASoundListEditor,
 | 
				
			||||||
 | 
					    AStatistics, AStreamer,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default admin
 | 
				
			||||||
@ -1,7 +1,8 @@
 | 
				
			|||||||
import AActionButton from './AActionButton.vue'
 | 
					 | 
				
			||||||
import AAutocomplete from './AAutocomplete.vue'
 | 
					import AAutocomplete from './AAutocomplete.vue'
 | 
				
			||||||
import ACarousel from './ACarousel.vue'
 | 
					import AModal from "./AModal.vue"
 | 
				
			||||||
 | 
					import AActionButton from './AActionButton.vue'
 | 
				
			||||||
import ADropdown from "./ADropdown.vue"
 | 
					import ADropdown from "./ADropdown.vue"
 | 
				
			||||||
 | 
					import ACarousel from './ACarousel.vue'
 | 
				
			||||||
import AEpisode from './AEpisode.vue'
 | 
					import AEpisode from './AEpisode.vue'
 | 
				
			||||||
import AList from './AList.vue'
 | 
					import AList from './AList.vue'
 | 
				
			||||||
import APage from './APage.vue'
 | 
					import APage from './APage.vue'
 | 
				
			||||||
@ -11,31 +12,15 @@ import AProgress from './AProgress.vue'
 | 
				
			|||||||
import ASoundItem from './ASoundItem.vue'
 | 
					import ASoundItem from './ASoundItem.vue'
 | 
				
			||||||
import ASwitch from './ASwitch.vue'
 | 
					import ASwitch from './ASwitch.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import AModal from "./AModal.vue"
 | 
					 | 
				
			||||||
import AFileUpload from "./AFileUpload.vue"
 | 
					 | 
				
			||||||
import ASelectFile from "./ASelectFile.vue"
 | 
					 | 
				
			||||||
import AStatistics from './AStatistics.vue'
 | 
					 | 
				
			||||||
import AStreamer from './AStreamer.vue'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import AFormSet from './AFormSet.vue'
 | 
					 | 
				
			||||||
import ATrackListEditor from './ATrackListEditor.vue'
 | 
					 | 
				
			||||||
import ASoundListEditor from './ASoundListEditor.vue'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Core components
 | 
					 * Core components
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const base = {
 | 
					export const base = {
 | 
				
			||||||
    AAutocomplete, ACarousel, ADropdown, AEpisode, AList, APage, APlayer, APlaylist,
 | 
					    AActionButton, AAutocomplete, AModal,
 | 
				
			||||||
 | 
					    ACarousel, ADropdown, AEpisode, AList, APage, APlayer, APlaylist,
 | 
				
			||||||
    AProgress, ASoundItem, ASwitch,
 | 
					    AProgress, ASoundItem, ASwitch,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default base
 | 
					export default base
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const admin = {
 | 
					 | 
				
			||||||
    ...base,
 | 
					 | 
				
			||||||
    AActionButton, AFileUpload, ASelectFile, AModal,
 | 
					 | 
				
			||||||
    AFormSet, ATrackListEditor, ASoundListEditor,
 | 
					 | 
				
			||||||
    AStatistics, AStreamer,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -113,7 +113,7 @@ export default class Model {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Update instance's data with provided data. Return None
 | 
					     * Set instance's data with provided data. Return None
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    commit(data) {
 | 
					    commit(data) {
 | 
				
			||||||
        this.data = data;
 | 
					        this.data = data;
 | 
				
			||||||
@ -121,11 +121,17 @@ export default class Model {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Update model data, without reset previous value
 | 
					     * Update model data, without reset previous value.
 | 
				
			||||||
 | 
					     * Item is marked as updated.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    update(data) {
 | 
					    update(data) {
 | 
				
			||||||
        this.data = {...this.data, ...data}
 | 
					        this.data = {...this.data, ...data}
 | 
				
			||||||
        this.id = this.constructor.getId(this.data)
 | 
					        this.id = this.constructor.getId(this.data)
 | 
				
			||||||
 | 
					        this.updated = true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    delete() {
 | 
				
			||||||
 | 
					        this.deleted = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -177,8 +183,24 @@ export class Set {
 | 
				
			|||||||
            this.push(item, {args: args, save: false});
 | 
					            this.push(item, {args: args, save: false});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //! Return total items count
 | 
				
			||||||
    get length() { return this.items.length }
 | 
					    get length() { return this.items.length }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //! Return a list of items marked as deleted
 | 
				
			||||||
 | 
					    get deletedItems() {
 | 
				
			||||||
 | 
					        return this.items.filter(i => i.deleted)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //! Return a list of created items
 | 
				
			||||||
 | 
					    get createdItems() {
 | 
				
			||||||
 | 
					        return this.items.filter(i => !i.deleted && !i.id)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //! Return a list of updated items
 | 
				
			||||||
 | 
					    get updatedItems() {
 | 
				
			||||||
 | 
					        return this.items.filter(i => i.updated)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Fetch multiple items from server
 | 
					     * Fetch multiple items from server
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
@ -190,6 +212,58 @@ export class Set {
 | 
				
			|||||||
                              .map(d => new model(d, {url: url, ...args})))
 | 
					                              .map(d => new model(d, {url: url, ...args})))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fetch({url=null, reset=false, ...options}={}, args=null) {
 | 
				
			||||||
 | 
					        url = url || this.url
 | 
				
			||||||
 | 
					        options = this.model.getOptions(options)
 | 
				
			||||||
 | 
					        return fetch(url, options)
 | 
				
			||||||
 | 
					            .then(response => response.json())
 | 
				
			||||||
 | 
					            .then(data =>
 | 
				
			||||||
 | 
					                (data instanceof Array ? data : data.results)
 | 
				
			||||||
 | 
					                .map(d => new this.model(d, {url: url, ...args}))
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .then(data => {
 | 
				
			||||||
 | 
					                if(reset)
 | 
				
			||||||
 | 
					                    this.items = data
 | 
				
			||||||
 | 
					                else
 | 
				
			||||||
 | 
					                    // TODO: remove duplicate
 | 
				
			||||||
 | 
					                    this.items = [...this.items, ...data]
 | 
				
			||||||
 | 
					                return data
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Commit changes to server.
 | 
				
			||||||
 | 
					     * ref: `views.mixin.ListCommitMixin`
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    commit(url, {getData=null, ...options}={}) {
 | 
				
			||||||
 | 
					        const createdItems = this.createdItems
 | 
				
			||||||
 | 
					        const body = {
 | 
				
			||||||
 | 
					            delete: this.deletedItems.map(i => i.id),
 | 
				
			||||||
 | 
					            update: this.updatedItems.map(getData),
 | 
				
			||||||
 | 
					            create: createdItems.map(getData),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if(!body.delete && !body.update && !body.create)
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        getData = getData || ((i) => i.data);
 | 
				
			||||||
 | 
					        options = this.model.getOptions(options)
 | 
				
			||||||
 | 
					        options.method = "POST"
 | 
				
			||||||
 | 
					        options.body = JSON.stringify(body)
 | 
				
			||||||
 | 
					        return fetch(url, options)
 | 
				
			||||||
 | 
					            .then(response => response.json())
 | 
				
			||||||
 | 
					            .then(data => {
 | 
				
			||||||
 | 
					                const {created, updated, deleted} = data
 | 
				
			||||||
 | 
					                if(createdItems)
 | 
				
			||||||
 | 
					                    this.items = this.items.filter(i => createdItems.indexOf(i) == -1)
 | 
				
			||||||
 | 
					                if(deleted)
 | 
				
			||||||
 | 
					                    this.items = this.items.filter(i => deleted.indexOf(i.id) == -1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.extend(created)
 | 
				
			||||||
 | 
					                this.extend(updated)
 | 
				
			||||||
 | 
					                return data
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Load list from localStorage
 | 
					     * Load list from localStorage
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
@ -234,22 +308,30 @@ export class Set {
 | 
				
			|||||||
                                        : this.items.findIndex(x => x.id == pred.id);
 | 
					                                        : this.items.findIndex(x => x.id == pred.id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    extend(items, options) {
 | 
				
			||||||
 | 
					        items.forEach(i => this.push(i, options))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Add item to set, return index.
 | 
					     * Add item to set, return index.
 | 
				
			||||||
 | 
					     * If item already exists, replace it.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    push(item, {args={},save=true}={}) {
 | 
					    push(item, {args={},save=true}={}) {
 | 
				
			||||||
        item = item instanceof this.model ? item : new this.model(item, args);
 | 
					        item = item instanceof this.model ? item : new this.model(item, args);
 | 
				
			||||||
        if(this.unique) {
 | 
					        let index = -1
 | 
				
			||||||
            let index = this.findIndex(item);
 | 
					        if(this.unique && item.id) {
 | 
				
			||||||
 | 
					            index = this.findIndex(item);
 | 
				
			||||||
            if(index > -1)
 | 
					            if(index > -1)
 | 
				
			||||||
                return index;
 | 
					                this.items[index] = item
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if(this.max && this.items.length >= this.max)
 | 
					        if(index == -1) {
 | 
				
			||||||
            this.items.splice(0,this.items.length-this.max)
 | 
					            if(this.max && this.items.length >= this.max)
 | 
				
			||||||
 | 
					                this.items.splice(0,this.items.length-this.max)
 | 
				
			||||||
        this.items.push(item);
 | 
					            this.items.push(item)
 | 
				
			||||||
        save && this.save();
 | 
					            index = this.items.length-1
 | 
				
			||||||
        return this.items.length-1;
 | 
					        }
 | 
				
			||||||
 | 
					        save && this.save()
 | 
				
			||||||
 | 
					        return index;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
 | 
				
			|||||||
@ -309,9 +309,9 @@
 | 
				
			|||||||
.preview-header {
 | 
					.preview-header {
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &:not(.no-cover) {
 | 
					    /*&:not(.no-cover) {
 | 
				
			||||||
        min-height: var(--header-height);
 | 
					        min-height: var(--header-height);
 | 
				
			||||||
    }
 | 
					    }*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &.no-cover {
 | 
					    &.no-cover {
 | 
				
			||||||
        height: unset;
 | 
					        height: unset;
 | 
				
			||||||
 | 
				
			|||||||
@ -21,6 +21,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    &.x { padding-right: 0px !important; }
 | 
					    &.x { padding-right: 0px !important; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					.align-center {
 | 
				
			||||||
 | 
					    text-align: center !important;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.clear-left { clear: left !important }
 | 
					.clear-left { clear: left !important }
 | 
				
			||||||
.clear-right { clear: right !important }
 | 
					.clear-right { clear: right !important }
 | 
				
			||||||
 | 
				
			|||||||
@ -25,6 +25,8 @@ export default defineConfig({
 | 
				
			|||||||
                globals: {
 | 
					                globals: {
 | 
				
			||||||
                    vue: 'Vue',
 | 
					                    vue: 'Vue',
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
 | 
					                assetFileNames: "[name].[ext]",
 | 
				
			||||||
 | 
					                chunkFileNames: "[name].js",
 | 
				
			||||||
                entryFileNames: "[name].js",
 | 
					                entryFileNames: "[name].js",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            plugins: [commonjs()],
 | 
					            plugins: [commonjs()],
 | 
				
			||||||
 | 
				
			|||||||
@ -254,4 +254,8 @@ WSGI_APPLICATION = "instance.wsgi.application"
 | 
				
			|||||||
LOGOUT_REDIRECT_URL = "/"
 | 
					LOGOUT_REDIRECT_URL = "/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
REST_FRAMEWORK = {"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 50}
 | 
					REST_FRAMEWORK = {
 | 
				
			||||||
 | 
					    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
 | 
				
			||||||
 | 
					    "PAGE_SIZE": 50,
 | 
				
			||||||
 | 
					    "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user