Compare commits
2 Commits
07d72d799d
...
a2a399e531
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a2a399e531 | ||
![]() |
b28105c659 |
|
@ -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,
|
|
||||||
)
|
|
|
@ -1,12 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<title>Vue App</title>
|
|
||||||
<script defer src="js/chunk-vendors.js"></script><script defer src="js/chunk-common.js"></script><script defer src="js/admin.js"></script><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/chunk-common.css" rel="stylesheet"></head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -19,12 +19,21 @@ Usefull context:
|
||||||
<link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
|
<link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
|
||||||
|
|
||||||
{% block assets %}
|
{% block assets %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-common.css" %}"/>
|
{% static "vue/vue.esm-browser.js" as vue_url %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-vendors.css" %}"/>
|
<script type="importmap">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/public.css" %}"/>
|
{
|
||||||
<script src="{% static "aircox/js/chunk-common.js" %}"></script>
|
"imports": {
|
||||||
<script src="{% static "aircox/js/chunk-vendors.js" %}"></script>
|
"vue": "{{vue_url}}"
|
||||||
<script src="{% static "aircox/js/public.js" %}"></script>
|
}
|
||||||
|
}
|
||||||
|
</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 "aircox/index.css" %}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "aircox/public.css" %}"/>
|
||||||
|
|
||||||
|
<script type="module" src="{% if app_js_url %}{{ app_js_url }}{% else %}{% static "aircox/public.js" %}{% endif %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<title>
|
<title>
|
||||||
|
@ -47,6 +56,7 @@ Usefull context:
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
{% block app %}
|
||||||
<div class="navs">
|
<div class="navs">
|
||||||
{% block nav %}
|
{% block nav %}
|
||||||
<nav class="nav primary" role="navigation" aria-label="main navigation">
|
<nav class="nav primary" role="navigation" aria-label="main navigation">
|
||||||
|
@ -158,6 +168,7 @@ Usefull context:
|
||||||
<div id="player">{% include "aircox/widgets/player.html" %}</div>
|
<div id="player">{% include "aircox/widgets/player.html" %}</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -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,6 +1,7 @@
|
||||||
{% extends "./public.html" %}
|
{% extends "./public.html" %}
|
||||||
{% load i18n aircox %}
|
{% load i18n aircox %}
|
||||||
|
|
||||||
|
|
||||||
{% block head_title %}{{ station.name }}{% endblock %}
|
{% block head_title %}{{ station.name }}{% endblock %}
|
||||||
|
|
||||||
{% block title %}{% if page %}{{ block.super }}{% endif %}{% endblock %}
|
{% block title %}{% if page %}{{ block.super }}{% endif %}{% 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 @@
|
||||||
{% extends base_template|default:"./base.html" %}
|
{% extends "./base.html" %}
|
||||||
|
|
||||||
|
|
||||||
{% comment %}
|
{% comment %}
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
# aircox-assets
|
# aircox
|
||||||
|
|
||||||
## Project setup
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
```
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and hot-reloads for development
|
### Compile and Hot-Reload for Development
|
||||||
```
|
|
||||||
npm run serve
|
```sh
|
||||||
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and minifies for production
|
### Compile and Minify for Production
|
||||||
```
|
|
||||||
|
```sh
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lints and fixes files
|
|
||||||
```
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
||||||
### Customize configuration
|
|
||||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,19 +1,8 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
|
||||||
"module": "esnext",
|
|
||||||
"baseUrl": "./",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./src/*"]
|
||||||
"src/*"
|
}
|
||||||
]
|
},
|
||||||
},
|
"exclude": ["node_modules", "dist"]
|
||||||
"lib": [
|
|
||||||
"esnext",
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,37 @@
|
||||||
{
|
{
|
||||||
"name": "aircox-assets",
|
"name": "aircox",
|
||||||
"version": "0.1.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": true,
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"dev": "vite",
|
||||||
"build": "vue-cli-service build",
|
"build": "vite build",
|
||||||
"lint": "vue-cli-service lint"
|
"watch": "vite build --watch",
|
||||||
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.0.0",
|
"@fortawesome/fontawesome-free": "^6.0.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"v-calendar": "^3.1.2",
|
"v-calendar": "^3.1.2",
|
||||||
"vue": "^3.2.13"
|
"vite-plugin-babel-macros": "^1.0.6",
|
||||||
|
"vue": "^3.4.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.16",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@babel/eslint-parser": "^7.12.16",
|
|
||||||
"@vue/cli-plugin-babel": "~5.0.0",
|
|
||||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
|
||||||
"@vue/cli-service": "~5.0.0",
|
|
||||||
"bulma": "^0.9.4",
|
"bulma": "^0.9.4",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-vue": "^8.0.3",
|
"eslint-plugin-vue": "^8.0.3",
|
||||||
"sass": "^1.49.9",
|
"sass": "^1.49.9",
|
||||||
"sass-loader": "^12.6.0",
|
"vite": "^5.2.8"
|
||||||
"vue-cli": "^2.9.6",
|
|
||||||
"webpack-cli": "^5.1.4"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
"env": {
|
"env": {
|
||||||
"node": true
|
"node": true,
|
||||||
|
"es2022": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:vue/vue3-essential",
|
"plugin:vue/vue3-essential",
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 7.1 KiB |
|
@ -1 +0,0 @@
|
||||||
../node_modules/vue/dist/vue.esm-browser.js
|
|
|
@ -1 +0,0 @@
|
||||||
../node_modules/vue/dist/vue.esm-browser.prod.js
|
|
|
@ -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,
|
||||||
|
@ -11,7 +11,21 @@ const AdminApp = {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
...super.data,
|
...super.data,
|
||||||
|
modalItem: null,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
...App.methods,
|
||||||
|
|
||||||
|
fileSelected(select, input, preview) {
|
||||||
|
const item = this.$refs[select].item
|
||||||
|
if(item) {
|
||||||
|
this.$refs[input].value = item.id
|
||||||
|
if(preview)
|
||||||
|
preview.src = item.file
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default AdminApp;
|
export default AdminApp;
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {Set} from '../model';
|
import {Set} from '../model.js';
|
||||||
import Sound from '../sound';
|
import Sound from '../sound.js';
|
||||||
import APage from './APage';
|
import APage from './APage.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: APage,
|
extends: APage,
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import {getCsrf} from "../model"
|
import {getCsrf} from "../model.js"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emit: ["fileChange", "load", "abort", "error"],
|
emit: ["fileChange", "load", "abort", "error"],
|
||||||
|
|
|
@ -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,45 +1,26 @@
|
||||||
|
import AAutocomplete from './AAutocomplete.vue'
|
||||||
|
import AModal from "./AModal.vue"
|
||||||
import AActionButton from './AActionButton.vue'
|
import AActionButton from './AActionButton.vue'
|
||||||
import AAutocomplete from './AAutocomplete'
|
import ADropdown from "./ADropdown.vue"
|
||||||
import ACarousel from './ACarousel'
|
import ACarousel from './ACarousel.vue'
|
||||||
import ADropdown from "./ADropdown"
|
import AEpisode from './AEpisode.vue'
|
||||||
import AEpisode from './AEpisode'
|
import AList from './AList.vue'
|
||||||
import AList from './AList'
|
import APage from './APage.vue'
|
||||||
import APage from './APage'
|
import APlayer from './APlayer.vue'
|
||||||
import APlayer from './APlayer'
|
import APlaylist from './APlaylist.vue'
|
||||||
import APlaylist from './APlaylist'
|
import AProgress from './AProgress.vue'
|
||||||
import AProgress from './AProgress'
|
import ASoundItem from './ASoundItem.vue'
|
||||||
import ASoundItem from './ASoundItem'
|
import ASwitch from './ASwitch.vue'
|
||||||
import ASwitch from './ASwitch'
|
|
||||||
|
|
||||||
import AModal from "./AModal"
|
|
||||||
import AFileUpload from "./AFileUpload"
|
|
||||||
import ASelectFile from "./ASelectFile"
|
|
||||||
import AStatistics from './AStatistics'
|
|
||||||
import AStreamer from './AStreamer'
|
|
||||||
|
|
||||||
import AFormSet from './AFormSet'
|
|
||||||
import ATrackListEditor from './ATrackListEditor'
|
|
||||||
import ASoundListEditor from './ASoundListEditor'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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,
|
|
||||||
ATrackListEditor
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dashboard = {
|
|
||||||
...base,
|
|
||||||
AActionButton, AFileUpload, ASelectFile, AModal,
|
|
||||||
AFormSet, ATrackListEditor, ASoundListEditor,
|
|
||||||
AStatistics, AStreamer,
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
import './styles/admin.scss'
|
|
||||||
import './index.js'
|
|
||||||
|
|
||||||
import App from './app';
|
|
||||||
import {dashboard as components} from './components'
|
|
||||||
|
|
||||||
const DashboardApp = {
|
|
||||||
...App,
|
|
||||||
components: {...App.components, ...components},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
modalItem: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
...App.methods,
|
|
||||||
|
|
||||||
fileSelected(select, input, preview) {
|
|
||||||
const item = this.$refs[select].item
|
|
||||||
if(item) {
|
|
||||||
this.$refs[input].value = item.id
|
|
||||||
if(preview)
|
|
||||||
preview.src = item.file
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default DashboardApp;
|
|
||||||
|
|
||||||
|
|
||||||
window.App = DashboardApp
|
|
|
@ -3,6 +3,8 @@
|
||||||
* administration interface)
|
* administration interface)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'vue'
|
||||||
|
|
||||||
//-- aircox
|
//-- aircox
|
||||||
import App, {PlayerApp} from './app'
|
import App, {PlayerApp} from './app'
|
||||||
import VueLoader from './vueLoader'
|
import VueLoader from './vueLoader'
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,6 +2,4 @@ import "./styles/public.scss"
|
||||||
import './index.js'
|
import './index.js'
|
||||||
import App from './app.js'
|
import App from './app.js'
|
||||||
|
|
||||||
export default App
|
|
||||||
|
|
||||||
window.App = App
|
window.App = App
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@use "./vars" as v;
|
@use "./vars" as v;
|
||||||
|
|
||||||
// ---- text
|
// ---- text
|
||||||
.text-light { weight: 400; color: var(--text-color-light); }
|
.text-light { font-weight: 400; color: var(--text-color-light); }
|
||||||
|
|
||||||
.bigger { font-size: v.$text-size-bigger !important; }
|
.bigger { font-size: v.$text-size-bigger !important; }
|
||||||
.big { font-size: v.$text-size-big !important; }
|
.big { font-size: v.$text-size-big !important; }
|
||||||
|
@ -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 }
|
||||||
|
|
|
@ -398,7 +398,7 @@ nav li {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.header-cover:only-child {
|
.header-cover:only-child {
|
||||||
with: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: v.$screen-small) {
|
@media screen and (max-width: v.$screen-small) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@import 'v-calendar/style.css';
|
@import 'v-calendar/style.css';
|
||||||
@import '@fortawesome/fontawesome-free/css/all.min.css';
|
// @import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
|
|
||||||
// ---- bulma
|
// ---- bulma
|
||||||
$body-color: #000;
|
$body-color: #000;
|
||||||
|
|
44
assets/vite.config.js
Normal file
44
assets/vite.config.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { resolve } from 'path'
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
outDir: "../aircox/static/aircox/",
|
||||||
|
sourcemap: true,
|
||||||
|
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['vue',],
|
||||||
|
input: {
|
||||||
|
public: "src/public.js",
|
||||||
|
admin: "src/admin.js",
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
vue: 'Vue',
|
||||||
|
},
|
||||||
|
assetFileNames: "[name].[ext]",
|
||||||
|
chunkFileNames: "[name].js",
|
||||||
|
entryFileNames: "[name].js",
|
||||||
|
},
|
||||||
|
plugins: [commonjs()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
devSourcemap: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.ts', '.json', '.vue'],
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,23 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const { defineConfig } = require('@vue/cli-service')
|
|
||||||
module.exports = defineConfig({
|
|
||||||
transpileDependencies: true,
|
|
||||||
outputDir: path.resolve('../aircox/static/aircox'),
|
|
||||||
publicPath: './',
|
|
||||||
runtimeCompiler: true,
|
|
||||||
filenameHashing: false,
|
|
||||||
|
|
||||||
css: {
|
|
||||||
extract: true,
|
|
||||||
loaderOptions: {
|
|
||||||
sass: { sourceMap: true },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
pages: {
|
|
||||||
public: { entry: 'src/public.js' },
|
|
||||||
dashboard: { entry: 'src/dashboard.js' },
|
|
||||||
admin: { entry: 'src/admin.js' },
|
|
||||||
}
|
|
||||||
})
|
|
|
@ -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"],
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user