M2M list editor; users list & group mgt; add missing files
This commit is contained in:
parent
ae176dc623
commit
8f1ec9cbc1
|
@ -176,5 +176,8 @@ class Settings(BaseSettings):
|
|||
IMPORT_PLAYLIST_CSV_TEXT_QUOTE = '"'
|
||||
"""Text delimiter of csv text files."""
|
||||
|
||||
ALLOW_COMMENTS = True
|
||||
"""Allow comments."""
|
||||
|
||||
|
||||
settings = Settings("AIRCOX")
|
||||
|
|
|
@ -13,6 +13,7 @@ __all__ = (
|
|||
"SoundFilterSet",
|
||||
"TrackFilterSet",
|
||||
"UserFilterSet",
|
||||
"GroupFilterSet",
|
||||
"UserGroupFilterSet",
|
||||
)
|
||||
|
||||
|
@ -90,6 +91,17 @@ class UserFilterSet(filters.FilterSet):
|
|||
)
|
||||
|
||||
|
||||
class GroupFilterSet(filters.FilterSet):
|
||||
search = filters.CharFilter(field_name="search", method="search_filter")
|
||||
no_user = filters.NumberFilter(field_name="no_user", method="no_user_filter")
|
||||
|
||||
def no_user_filter(self, queryset, name, value):
|
||||
return queryset.exclude(user__in=[value])
|
||||
|
||||
def search_filter(self, queryset, name, value):
|
||||
return queryset.filter(Q(name__icontains=value) | Q(program__title__icontains=value))
|
||||
|
||||
|
||||
class UserGroupFilterSet(filters.FilterSet):
|
||||
class Meta:
|
||||
model = User.groups.through
|
||||
|
|
|
@ -4,6 +4,7 @@ from .episode import EpisodeForm, EpisodeSoundFormSet
|
|||
from .program import ProgramForm
|
||||
from .page import CommentForm, ImageForm, PageForm, ChildPageForm
|
||||
from .sound import SoundForm, SoundCreateForm
|
||||
from .track import TrackFormSet
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
@ -18,4 +19,5 @@ __all__ = (
|
|||
ChildPageForm,
|
||||
SoundForm,
|
||||
SoundCreateForm,
|
||||
TrackFormSet,
|
||||
)
|
||||
|
|
89
aircox/forms/widgets.py
Normal file
89
aircox/forms/widgets.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
from itertools import chain
|
||||
from functools import cached_property
|
||||
|
||||
from django import forms, http
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
__all__ = (
|
||||
"VueWidget",
|
||||
"VueAutoComplete",
|
||||
)
|
||||
|
||||
|
||||
class VueWidget(forms.Widget):
|
||||
binds = None
|
||||
"""Dict of `{attribute: value}` attrs set as bindings."""
|
||||
events = None
|
||||
"""Dict of `{event: value}` attrs set as events."""
|
||||
v_model = ""
|
||||
"""ES6 Model instance to bind to (`v-model`)."""
|
||||
|
||||
def __init__(self, *args, binds=None, events=None, v_model=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.binds = binds or []
|
||||
self.events = events or []
|
||||
|
||||
@cached_property
|
||||
def vue_attrs(self):
|
||||
"""Dict of Vue specific attributes."""
|
||||
binds, events = self.binds, self.events
|
||||
if isinstance(binds, dict):
|
||||
binds = binds.items()
|
||||
if isinstance(events, dict):
|
||||
events = events.items()
|
||||
|
||||
return dict(
|
||||
chain(
|
||||
((":" + key, value) for key, value in binds),
|
||||
(("@" + key, value) for key, value in events),
|
||||
)
|
||||
)
|
||||
|
||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||
extra_attrs = extra_attrs or {}
|
||||
extra_attrs.update(self.vue_attrs)
|
||||
return super().build_attrs(base_attrs, extra_attrs)
|
||||
|
||||
|
||||
class VueAutoComplete(VueWidget, forms.TextInput):
|
||||
"""Autocomplete Vue component."""
|
||||
|
||||
template_name = "aircox/widgets/autocomplete.html"
|
||||
|
||||
url: str = ""
|
||||
"""Url to autocomplete API view.
|
||||
|
||||
If it has query parameters, does not generate it based on lookup
|
||||
(see `get_url()` doc).
|
||||
"""
|
||||
lookup: str = ""
|
||||
"""Field name used as lookup (instead as provided one)."""
|
||||
params: http.QueryDict
|
||||
|
||||
def __init__(self, url_name, *args, lookup=None, params=None, **kwargs):
|
||||
self.url_name = url_name
|
||||
self.lookup = lookup
|
||||
self.params = params
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
context["url"] = self.get_url(name, self.lookup, self.params)
|
||||
return context
|
||||
|
||||
def get_url(self, name, lookup, params=None):
|
||||
"""Return url to autocomplete API. When query parameters are not
|
||||
provided generate them using `?{lookup}=${query}&field={name}` (where
|
||||
`${query} is Vue `a-autocomplete` specific).
|
||||
|
||||
:param str name: field name (not used by default)
|
||||
:param str lookup: lookup query parameter
|
||||
:param http.QueryDict params: additional mutable parameter
|
||||
"""
|
||||
url = reverse(self.url_name)
|
||||
query = http.QueryDict(mutable=True)
|
||||
if params:
|
||||
query.update(params)
|
||||
query.update({lookup: "${query}"})
|
||||
return f"{url}?{query.urlencode()}"
|
31
aircox/migrations/0022_set_group_ownership.py
Normal file
31
aircox/migrations/0022_set_group_ownership.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from django.db import migrations, models, transaction
|
||||
|
||||
|
||||
def init_groups_and_permissions(app, schema_editor):
|
||||
from aircox import permissions
|
||||
|
||||
Program = app.get_model("aircox", "Program")
|
||||
|
||||
with transaction.atomic():
|
||||
for program in Program.objects.all():
|
||||
permissions.program.init(program)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
dependencies = [
|
||||
("aircox", "0021_alter_schedule_timezone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(init_groups_and_permissions),
|
||||
migrations.AlterField(
|
||||
model_name="program",
|
||||
name="editors_group",
|
||||
field=models.ForeignKey(
|
||||
on_delete=models.deletion.CASCADE,
|
||||
to="auth.group",
|
||||
verbose_name="editors",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -305,9 +305,9 @@ class StaticPage(BasePage):
|
|||
)
|
||||
|
||||
def get_related_view(self):
|
||||
from ..views import attached
|
||||
from ..views.page import attached_views
|
||||
|
||||
return self.attach_to and attached.get(self.attach_to) or None
|
||||
return self.attach_to and attached_views.get(self.attach_to) or None
|
||||
|
||||
def get_absolute_url(self):
|
||||
if self.attach_to:
|
||||
|
|
28
aircox/serializers/auth.py
Normal file
28
aircox/serializers/auth.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from django.contrib.auth.models import User, Group
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
__all__ = ("UserSerializer", "GroupSerializer", "UserGroupSerializer")
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
exclude = ("password",)
|
||||
model = User
|
||||
|
||||
|
||||
class GroupSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
exclude = ("permissions",)
|
||||
model = Group
|
||||
|
||||
|
||||
class UserGroupSerializer(serializers.ModelSerializer):
|
||||
group = GroupSerializer(read_only=True)
|
||||
user = UserSerializer(read_only=True)
|
||||
user_id = serializers.IntegerField()
|
||||
group_id = serializers.IntegerField()
|
||||
|
||||
class Meta:
|
||||
model = User.groups.through
|
||||
fields = ("id", "group_id", "user_id", "group", "user")
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,34 +0,0 @@
|
|||
{% extends "aircox/base.html" %}
|
||||
{% load i18n aircox %}
|
||||
|
||||
{% block head_title %}
|
||||
{% block title %}{{ user.username }}{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content-container %}
|
||||
<div class="container content page-content">
|
||||
<h2 class="subtitle">Mon Profil</h2>
|
||||
{% translate "Username" %} : {{ user.username|title }}<br/>
|
||||
<!-- Connexion: {{ user.last_login }} -->
|
||||
|
||||
<h2 class="subtitle is-1">Mes émissions</h2>
|
||||
{% if programs|length %}
|
||||
<ul>
|
||||
{% for p in programs %}
|
||||
<li>{{ p.title }} :
|
||||
|
||||
<a href="{% url 'program-detail' slug=p.slug %}">
|
||||
<span title="{% translate 'View' %} {{ page }}">{% translate 'View' %}</span>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'program-edit' pk=p.pk %}">
|
||||
<span title="{% translate 'Edit' %} {{ page }}">{% translate 'Edit' %} </span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{% trans 'You are not listed as a program editor yet' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -21,44 +21,11 @@
|
|||
{% endblock %}
|
||||
</section>
|
||||
|
||||
|
||||
{% block list-pagination %}
|
||||
{% if is_paginated %}
|
||||
<hr/>
|
||||
{% update_query request.GET.copy page=None as GET %}
|
||||
{% with GET.urlencode as GET %}
|
||||
<nav class="nav-urls is-centered" role="pagination" aria-label="{% translate "pagination" %}">
|
||||
<ul class="urls">
|
||||
{% if page_obj.has_previous %}
|
||||
{% comment %}Translators: Bottom of the list, "previous page"{% endcomment %}
|
||||
<a href="?{{ GET }}&page={{ page_obj.previous_page_number }}" class="left"
|
||||
title="{% translate "Previous" %}"
|
||||
aria-label="{% translate "Previous" %}">
|
||||
<span class="icon"><i class="fa fa-chevron-left"></i></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span>
|
||||
{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
{% comment %}Translators: Bottom of the list, "Nextpage"{% endcomment %}
|
||||
<a href="?{{ GET }}&page={{ page_obj.next_page_number }}" class="right"
|
||||
title="{% translate "Next" %}"
|
||||
aria-label="{% translate "Next" %}">
|
||||
<span class="icon"><i class="fa fa-chevron-right"></i></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% include "./widgets/list_pagination.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
|
||||
{% block head-title %}
|
||||
{% block title %}
|
||||
{% if page and page.title %}{{ page.title }} —{% endif %}
|
||||
{% if page and page.title %}{{ page.title }}{% endif %}
|
||||
{% endblock %}
|
||||
{% if page and page.title %}—{% endif %}
|
||||
{{ station.name }}
|
||||
{% endblock %}
|
||||
|
|
|
@ -25,17 +25,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if comments %}
|
||||
<div>
|
||||
<h2 class="title is-2 mb-3">{% translate "Last Comments" %}</h2>
|
||||
<div class="box box-shadow p-3" style="max-height: 35rem; overflow-y:auto;">
|
||||
{% for object in comments|slice:"0:25" %}
|
||||
{% page_widget "item" object admin=True is_tiny=True %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<h2 class="title is-2 mb-3">{% translate "Programs" %}</h2>
|
||||
<div class="box box-shadow p-3" style="max-height: 35rem; overflow-y:auto;">
|
||||
|
@ -47,5 +36,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if comments %}
|
||||
<div>
|
||||
<h2 class="title is-2 mb-3">{% translate "Last Comments" %}</h2>
|
||||
<div class="box box-shadow p-3" style="max-height: 35rem; overflow-y:auto;">
|
||||
{% for object in comments|slice:"0:25" %}
|
||||
{% page_widget "item" object admin=True is_tiny=True %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
53
aircox/templates/aircox/dashboard/user_list.html
Normal file
53
aircox/templates/aircox/dashboard/user_list.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
{% extends "./base.html" %}
|
||||
{% load i18n aircox %}
|
||||
|
||||
{% block title %}{% translate "Users" %}{% endblock %}
|
||||
|
||||
|
||||
{% block content-container %}
|
||||
<div class="container">
|
||||
<table class="table is-stripped is-fullwidth">
|
||||
<thead>
|
||||
<td>{% trans "User" %}</td>
|
||||
<td>{% trans "Programs" %}</td>
|
||||
<td></td>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for obj in object_list %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if obj.first_name or obj.last_name %}
|
||||
{{ obj.first_name }} {{ obj.last_name }}
|
||||
—
|
||||
{% endif %}
|
||||
<i>{{ obj.username }}</i>
|
||||
</td>
|
||||
<td>
|
||||
{% for p in obj.programs %}
|
||||
<a href="{% url "program-edit" p.pk %}">{{ p.title }}</a>
|
||||
{% if not forloop.last %}
|
||||
<br/>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{% if user.is_superuser %}
|
||||
<button type="button" class="button secondary"
|
||||
@click="$refs['user-groups-modal'].open({id: {{ obj.id }}, name: '{{ obj.username }}' })">
|
||||
{% trans "Groups" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% include "aircox/widgets/list_pagination.html" %}
|
||||
|
||||
{% if user.is_superuser %}
|
||||
{% include "./widgets/user_groups.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -3,12 +3,28 @@
|
|||
<a-modal ref="group-users-modal">
|
||||
<template #title="{item}">[[ item?.name ]]</template>
|
||||
<template #default="{item}">
|
||||
<a-group-users v-if="item" ref="group-users"
|
||||
<a-many-to-many-edit 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}&not_in_group=' + item.id"
|
||||
:initials="{group_id: item.id }"
|
||||
/>
|
||||
|
||||
:source_id="item.id" source_field="group"
|
||||
target_field="user"
|
||||
:autocomplete="{url:'{% url 'api:user-autocomplete' %}?search=${query}&not_in_group=' + item.id, 'label-field':'username', 'value-field':'id'}"
|
||||
>
|
||||
<template #items-title>{% translate "Members" %}</template>
|
||||
|
||||
<template #item="{item}">
|
||||
<b class="mr-3">[[ item.data.user.username ]]</b>
|
||||
<span>[[ item.data.user.first_name ]] [[ item.data.user.last_name ]]</span>
|
||||
</template>
|
||||
|
||||
<template #autocomplete-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-many-to-many-edit>
|
||||
</template>
|
||||
<template #footer="{item, close}">
|
||||
<button type="button" class="button" @click="$refs['group-users'].save(); close()">
|
||||
|
|
30
aircox/templates/aircox/dashboard/widgets/user_groups.html
Normal file
30
aircox/templates/aircox/dashboard/widgets/user_groups.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% load i18n %}
|
||||
|
||||
<a-modal ref="user-groups-modal">
|
||||
<template #title="{item}">[[ item?.username ]]</template>
|
||||
<template #default="{item}">
|
||||
<a-many-to-many-edit v-if="item" ref="user-groups"
|
||||
:url="'{% url 'api:usergroup-list' %}?user=' + item.id"
|
||||
commit-url="{% url 'api:usergroup-commit' %}"
|
||||
|
||||
:source_id="item.id" source_field="user"
|
||||
target_field="group"
|
||||
:autocomplete="{url:'{% url 'api:group-autocomplete' %}?search=${query}&no_user=' + item.id, 'label-field':'name', 'value-field':'id'}"
|
||||
>
|
||||
<template #items-title>{% translate "Groups" %}</template>
|
||||
|
||||
<template #item="{item}">
|
||||
[[ item.data.group.name ]]
|
||||
</template>
|
||||
|
||||
<template #autocomplete-item="{item}">
|
||||
[[ item.name ]]
|
||||
</template>
|
||||
</a-many-to-many-edit>
|
||||
</template>
|
||||
<template #footer="{item, close}">
|
||||
<button type="button" class="button" @click="$refs['group-users'].save(); close()">
|
||||
Save
|
||||
</button>
|
||||
</template>
|
||||
</a-modal>
|
|
@ -1,18 +1,21 @@
|
|||
{% extends "./dashboard/base.html" %}
|
||||
{% load static aircox_admin i18n %}
|
||||
{% load static aircox aircox_admin i18n %}
|
||||
|
||||
{% block init-scripts %}
|
||||
aircox.labels = {% inline_labels %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block header-cover %}
|
||||
<div class="flex-column">
|
||||
<img src="{{ cover }}" ref="cover" class="cover">
|
||||
<button type="button" class="button" @click="$refs['cover-select'].open()">
|
||||
{% translate "Change cover" %}
|
||||
</button>
|
||||
</div>
|
||||
{% block header-cover %}{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% if not object %}
|
||||
{% with view.model|verbose_name as model %}
|
||||
{% blocktranslate %}Create a {{model}}{% endblocktranslate %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{{ block.super }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -52,34 +55,69 @@ aircox.labels = {% inline_labels %}
|
|||
<form method="post" enctype="multipart/form-data">
|
||||
{% block page-form %}
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{% if field.name == "cover" %}
|
||||
<input type="hidden" name="{{ field.name }}" value="{{ field.value }}" ref="cover-input"/>
|
||||
{% else %}
|
||||
<div class="field {% if field.name != "content" %}is-horizontal{% endif %}">
|
||||
<label class="label">{{ field.label }}</label>
|
||||
<div class="control clear-unset">
|
||||
{% if field.name == "pub_date" %}
|
||||
<input type="datetime-local" class="input" name="{{ field.name }}"
|
||||
value="{{ field.value|date:"Y-m-d" }}T{{ field.value|date:"H:i" }}"/>
|
||||
{% elif field.name == "content" %}
|
||||
<textarea name="{{ field.name }}" class="is-fullwidth height-25">{{ field.value|striptags|safe }}</textarea>
|
||||
{% else %}
|
||||
{% include "aircox/forms/form_field.html" with field=field.field name=field.name value=field.initial %}
|
||||
<div class="flex-row">
|
||||
<div class="flex-grow-3">
|
||||
{% for field in form %}
|
||||
{% if field.name not in "cover,content" %}
|
||||
<div class="field {% if field.name != "content" %}is-horizontal{% endif %}">
|
||||
<label class="label">{{ field.label }}</label>
|
||||
<div class="control clear-unset">
|
||||
{% if field.name == "pub_date" %}
|
||||
<input type="datetime-local" class="input" name="{{ field.name }}"
|
||||
value="{{ field.value|date:"Y-m-d" }}T{{ field.value|date:"H:i" }}"/>
|
||||
{% else %}
|
||||
{% include "aircox/forms/form_field.html" with field=field.field name=field.name value=field.initial %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="help">{{ field.help_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<p class="help is-danger">{{ field.errors }}</p>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<p class="help">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
{% with form.cover as field %}
|
||||
{% block page-form-cover %}
|
||||
<input type="hidden" name="{{ field.name }}" value="{{ field.value }}" ref="cover-input"/>
|
||||
{% spaceless %}
|
||||
<figure class="header-cover">
|
||||
<img src="{{ cover }}" ref="cover" class="cover">
|
||||
<button type="button" class="button" @click="$refs['cover-select'].open()">
|
||||
{% translate "Change cover" %}
|
||||
</button>
|
||||
</figure>
|
||||
{% endspaceless %}
|
||||
{% endblock %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<p class="help">{{ field.help_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<p class="help is-danger">{{ field.errors }}</p>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<p class="help">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% with form.content as field %}
|
||||
{% block page-form-content %}
|
||||
<div>
|
||||
<div class="field {% if field.name != "content" %}is-horizontal{% endif %}">
|
||||
<label class="label">{{ field.label }}</label>
|
||||
<div class="control clear-unset">
|
||||
<textarea name="{{ field.name }}" class="is-fullwidth height-25">{{ field.value|default:""|striptags|safe }}</textarea>
|
||||
</div>
|
||||
<p class="help">{{ field.help_text }}</p>
|
||||
</div>
|
||||
{% if field.errors %}
|
||||
<p class="help is-danger">{{ field.errors }}</p>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<p class="help">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endwith %}
|
||||
|
||||
{% endblock %}
|
||||
<hr/>
|
||||
<div class="flex-row">
|
||||
<div class="flex-grow-1">{% block page-form-actions %}{% endblock %}</div>
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block page-form-actions %}
|
||||
{% if request.user.is_superuser %}
|
||||
{% if object and object.pk and request.user.is_superuser %}
|
||||
<button type="button"
|
||||
class="button secondary"
|
||||
@click="$refs['group-users-modal'].open({id: {{ object.editors_group_id }}, name: '{{ object.editors_group.name }}' })">Editors</button>
|
||||
@click="$refs['group-users-modal'].open({id: {{ object.editors_group_id }}, name: '{{ object.editors_group.name }}' })">{% translate "Editors" %}</button>
|
||||
|
||||
{% include "./dashboard/widgets/group_users.html" %}
|
||||
{{ block.super }}
|
||||
|
|
|
@ -9,8 +9,9 @@ content.
|
|||
|
||||
{% block head-title %}
|
||||
{% block title %}
|
||||
{% if page and page.title %}{{ page.title }} —{% endif %}
|
||||
{% if page and page.title %}{{ page.title }}{% endif %}
|
||||
{% endblock %}
|
||||
{% if page and page.title %}—{% endif %}
|
||||
{{ station.name }}
|
||||
{% endblock %}
|
||||
|
||||
|
|
39
aircox/templates/aircox/widgets/list_pagination.html
Normal file
39
aircox/templates/aircox/widgets/list_pagination.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
{% comment %}
|
||||
Context:
|
||||
- is_paginated: if True, page is paginated
|
||||
- page_obj: page object from list view;
|
||||
{% endcomment %}
|
||||
{% load i18n aircox %}
|
||||
|
||||
{% if is_paginated %}
|
||||
<hr/>
|
||||
{% update_query request.GET.copy page=None as GET %}
|
||||
{% with GET.urlencode as GET %}
|
||||
<nav class="nav-urls is-centered" role="pagination" aria-label="{% translate "pagination" %}">
|
||||
<ul class="urls">
|
||||
{% if page_obj.has_previous %}
|
||||
{% comment %}Translators: Bottom of the list, "previous page"{% endcomment %}
|
||||
<a href="?{{ GET }}&page={{ page_obj.previous_page_number }}" class="left"
|
||||
title="{% translate "Previous" %}"
|
||||
aria-label="{% translate "Previous" %}">
|
||||
<span class="icon"><i class="fa fa-chevron-left"></i></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span>
|
||||
{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
{% comment %}Translators: Bottom of the list, "Nextpage"{% endcomment %}
|
||||
<a href="?{{ GET }}&page={{ page_obj.next_page_number }}" class="right"
|
||||
title="{% translate "Next" %}"
|
||||
aria-label="{% translate "Next" %}">
|
||||
<span class="icon"><i class="fa fa-chevron-right"></i></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
{% endwith %}
|
||||
{% endif %}
|
|
@ -1,4 +1,4 @@
|
|||
{% load i18n %}
|
||||
{% load aircox i18n %}
|
||||
<div class="dropdown is-hoverable is-right">
|
||||
<div class="dropdown-trigger">
|
||||
<button class="button square" aria-haspopup="true" aria-controls="dropdown-menu" type="button">
|
||||
|
@ -13,7 +13,23 @@
|
|||
<a class="dropdown-item" href="{% url "dashboard" %}" data-force-reload="1">
|
||||
{% translate "Dashboard" %}
|
||||
</a>
|
||||
{% if user|has_perm:"list_user" %}
|
||||
<a class="dropdown-item" href="{% url "user-list" %}" data-force-reload="1">
|
||||
{% translate "Users" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% comment %}
|
||||
{% block edit-menu %}
|
||||
{% if request.user|has_perm:"aircox.create_program" %}
|
||||
<a class="dropdown-item" href="{% url "program-create" %}">
|
||||
{% translate "Create Program" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endcomment %}
|
||||
{% if user.is_superuser %}
|
||||
<hr class="dropdown-divider" />
|
||||
{% block admin-menu %}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% load aircox i18n %}
|
||||
{% block user-actions-container %}
|
||||
{% has_perm page page.program.change_permission_codename simple=True as can_edit %}
|
||||
{% has_obj_perm page page.program.change_permission_codename simple=True as can_edit %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
{{ object.get_status_display }}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
{% extends "aircox/forms/formset.html" %}
|
||||
{% load aircox %}
|
||||
|
||||
{% block row-control %}
|
||||
{% if name == 'user' %}
|
||||
{% form_field field "inputName" value %}
|
||||
{% else %}
|
||||
{{ block.super }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -75,17 +75,19 @@ def do_get_tracks(obj):
|
|||
return obj.track_set.all()
|
||||
|
||||
|
||||
@register.simple_tag(name="has_perm", takes_context=True)
|
||||
def do_has_perm(context, obj, perm, user=None, simple=False):
|
||||
@register.filter(name="has_perm")
|
||||
def do_has_perm(user, perm):
|
||||
"""Return True if user has permission."""
|
||||
return user.has_perm(perm)
|
||||
|
||||
|
||||
@register.simple_tag(name="has_obj_perm", takes_context=True)
|
||||
def do_has_obj_perm(context, obj, perm, user=None):
|
||||
"""Return True if ``user.has_perm('[APP].[perm]_[MODEL]')``"""
|
||||
user = user or context["request"].user
|
||||
if not obj:
|
||||
return
|
||||
if user is None:
|
||||
user = context["request"].user
|
||||
if simple:
|
||||
return user.has_perm("aircox.{}".format(perm)) or user.is_superuser
|
||||
else:
|
||||
return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name))
|
||||
return False
|
||||
return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name))
|
||||
|
||||
|
||||
@register.filter(name="is_diffusion")
|
||||
|
|
|
@ -22,6 +22,7 @@ register_converter(WeekConverter, "week")
|
|||
|
||||
router = DefaultRouter()
|
||||
router.register("user", viewsets.UserViewSet, basename="user")
|
||||
router.register("group", viewsets.GroupViewSet, basename="group")
|
||||
router.register("usergroup", viewsets.UserGroupViewSet, basename="usergroup")
|
||||
|
||||
router.register("images", viewsets.ImageViewSet, basename="image")
|
||||
|
@ -31,7 +32,7 @@ router.register("comment", viewsets.CommentViewSet, basename="comment")
|
|||
|
||||
|
||||
api = [
|
||||
path("logs/", views.LogListAPIView.as_view(), name="live"),
|
||||
path("logs/", views.log.LogListAPIView.as_view(), name="live"),
|
||||
path(
|
||||
"user/settings/",
|
||||
viewsets.UserSettingsViewSet.as_view({"get": "retrieve", "post": "update", "put": "update"}),
|
||||
|
@ -41,30 +42,30 @@ api = [
|
|||
|
||||
|
||||
urls = [
|
||||
path("", views.HomeView.as_view(), name="home"),
|
||||
path("", views.home.HomeView.as_view(), name="home"),
|
||||
path("api/", include((api, "aircox"), namespace="api")),
|
||||
# ---- ---- objects views
|
||||
# ---- articles
|
||||
path(
|
||||
_("articles/<slug:slug>/"),
|
||||
views.ArticleDetailView.as_view(),
|
||||
views.article.ArticleDetailView.as_view(),
|
||||
name="article-detail",
|
||||
),
|
||||
path(
|
||||
_("articles/"),
|
||||
views.ArticleListView.as_view(model=models.Article),
|
||||
views.article.ArticleListView.as_view(model=models.article.Article),
|
||||
name="article-list",
|
||||
),
|
||||
path(
|
||||
_("articles/c/<slug:category_slug>/"),
|
||||
views.ArticleListView.as_view(model=models.Article),
|
||||
views.article.ArticleListView.as_view(model=models.article.Article),
|
||||
name="article-list",
|
||||
),
|
||||
# ---- timetable
|
||||
path(_("timetable/"), views.TimeTableView.as_view(), name="timetable-list"),
|
||||
path(_("timetable/"), views.diffusion.TimeTableView.as_view(), name="timetable-list"),
|
||||
path(
|
||||
_("timetable/<date:date>/"),
|
||||
views.TimeTableView.as_view(),
|
||||
views.diffusion.TimeTableView.as_view(),
|
||||
name="timetable-list",
|
||||
),
|
||||
# ---- pages
|
||||
|
@ -95,38 +96,41 @@ urls = [
|
|||
name="static-page-list",
|
||||
),
|
||||
# ---- programs
|
||||
path(_("programs/"), views.ProgramListView.as_view(), name="program-list"),
|
||||
path(_("programs/c/<slug:category_slug>/"), views.ProgramListView.as_view(), name="program-list"),
|
||||
path(_("programs/"), views.program.ProgramListView.as_view(), name="program-list"),
|
||||
path(_("programs/c/<slug:category_slug>/"), views.program.ProgramListView.as_view(), name="program-list"),
|
||||
path(
|
||||
_("programs/<slug:slug>"),
|
||||
views.ProgramDetailView.as_view(),
|
||||
views.program.ProgramDetailView.as_view(),
|
||||
name="program-detail",
|
||||
),
|
||||
path(_("programs/<slug:parent_slug>/articles/"), views.ArticleListView.as_view(), name="article-list"),
|
||||
path(_("programs/<slug:parent_slug>/podcasts/"), views.PodcastListView.as_view(), name="podcast-list"),
|
||||
path(_("programs/<slug:parent_slug>/episodes/"), views.EpisodeListView.as_view(), name="episode-list"),
|
||||
path(_("programs/<slug:parent_slug>/diffusions/"), views.DiffusionListView.as_view(), name="diffusion-list"),
|
||||
path(_("programs/<slug:parent_slug>/articles/"), views.article.ArticleListView.as_view(), name="article-list"),
|
||||
path(_("programs/<slug:parent_slug>/podcasts/"), views.episode.PodcastListView.as_view(), name="podcast-list"),
|
||||
path(_("programs/<slug:parent_slug>/episodes/"), views.episode.EpisodeListView.as_view(), name="episode-list"),
|
||||
path(
|
||||
_("programs/<slug:parent_slug>/diffusions/"), views.diffusion.DiffusionListView.as_view(), name="diffusion-list"
|
||||
),
|
||||
path(
|
||||
_("programs/<slug:parent_slug>/publications/"),
|
||||
views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.Target.PAGES),
|
||||
name="page-list",
|
||||
),
|
||||
# ---- episodes
|
||||
path(_("programs/episodes/"), views.EpisodeListView.as_view(), name="episode-list"),
|
||||
path(_("programs/episodes/c/<slug:category_slug>/"), views.EpisodeListView.as_view(), name="episode-list"),
|
||||
path(_("programs/episodes/"), views.episode.EpisodeListView.as_view(), name="episode-list"),
|
||||
path(_("programs/episodes/c/<slug:category_slug>/"), views.episode.EpisodeListView.as_view(), name="episode-list"),
|
||||
path(
|
||||
_("programs/episodes/<slug:slug>/"),
|
||||
views.EpisodeDetailView.as_view(),
|
||||
views.episode.EpisodeDetailView.as_view(),
|
||||
name="episode-detail",
|
||||
),
|
||||
path(_("podcasts/"), views.PodcastListView.as_view(), name="podcast-list"),
|
||||
path(_("podcasts/c/<slug:category_slug>/"), views.PodcastListView.as_view(), name="podcast-list"),
|
||||
path(_("podcasts/"), views.episode.PodcastListView.as_view(), name="podcast-list"),
|
||||
path(_("podcasts/c/<slug:category_slug>/"), views.episode.PodcastListView.as_view(), name="podcast-list"),
|
||||
# ---- dashboard
|
||||
path(_("dashboard/"), views.dashboard.DashboardView.as_view(), name="dashboard"),
|
||||
path(_("dashboard/program/<pk>/"), views.ProgramUpdateView.as_view(), name="program-edit"),
|
||||
path(_("dashboard/episodes/<pk>/"), views.EpisodeUpdateView.as_view(), name="episode-edit"),
|
||||
path(_("dashboard/program/<pk>/"), views.program.ProgramUpdateView.as_view(), name="program-edit"),
|
||||
path(_("dashboard/episodes/<pk>/"), views.episode.EpisodeUpdateView.as_view(), name="episode-edit"),
|
||||
path(_("dashboard/statistics/"), views.dashboard.StatisticsView.as_view(), name="dashboard-statistics"),
|
||||
path(_("dashboard/statistics/<date:date>/"), views.dashboard.StatisticsView.as_view(), name="dashboard-statistics"),
|
||||
path(_("dashboard/users/"), views.auth.UserListView.as_view(), name="user-list"),
|
||||
# ---- others
|
||||
path("errors/no-station/", views.errors.NoStationErrorView.as_view(), name="errors-no-station"),
|
||||
]
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
from . import admin, dashboard, errors
|
||||
from .article import ArticleDetailView, ArticleListView
|
||||
from . import admin, dashboard, errors, auth
|
||||
from . import article, program, episode, diffusion, log
|
||||
from . import home
|
||||
from .base import BaseAPIView, BaseView
|
||||
from .diffusion import DiffusionListView, TimeTableView
|
||||
from .episode import EpisodeDetailView, EpisodeListView, PodcastListView, EpisodeUpdateView
|
||||
from .home import HomeView
|
||||
from .log import LogListAPIView, LogListView
|
||||
from .page import (
|
||||
BasePageDetailView,
|
||||
BasePageListView,
|
||||
|
@ -12,47 +9,24 @@ from .page import (
|
|||
PageListView,
|
||||
PageUpdateView,
|
||||
)
|
||||
from .program import (
|
||||
ProgramDetailView,
|
||||
ProgramListView,
|
||||
ProgramUpdateView,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"admin",
|
||||
"dashboard",
|
||||
"errors",
|
||||
"attached",
|
||||
"dashboard",
|
||||
"auth",
|
||||
"article",
|
||||
"program",
|
||||
"episode",
|
||||
"diffusion",
|
||||
"log",
|
||||
"home",
|
||||
"BaseAPIView",
|
||||
"BaseView",
|
||||
"ArticleDetailView",
|
||||
"ArticleListView",
|
||||
"DiffusionListView",
|
||||
"TimeTableView",
|
||||
"EpisodeDetailView",
|
||||
"EpisodeListView",
|
||||
"PodcastListView",
|
||||
"EpisodeUpdateView",
|
||||
"HomeView",
|
||||
"LogListAPIView",
|
||||
"LogListView",
|
||||
"BasePageDetailView",
|
||||
"BasePageListView",
|
||||
"PageDetailView",
|
||||
"PageUpdateView",
|
||||
"PageListView",
|
||||
"ProgramDetailView",
|
||||
"ProgramListView",
|
||||
"ProgramUpdateView",
|
||||
)
|
||||
|
||||
|
||||
attached = {}
|
||||
|
||||
for key in __all__:
|
||||
view = globals().get(key)
|
||||
if key == "attached":
|
||||
continue
|
||||
if attach := getattr(view, "attach_to_value", None):
|
||||
attached[attach] = view
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
from ..models import Article, Program, StaticPage
|
||||
from .page import PageDetailView, PageListView
|
||||
from . import page
|
||||
|
||||
__all__ = ["ArticleDetailView", "ArticleListView"]
|
||||
|
||||
|
||||
class ArticleDetailView(PageDetailView):
|
||||
class ArticleDetailView(page.PageDetailView):
|
||||
model = Article
|
||||
|
||||
|
||||
class ArticleListView(PageListView):
|
||||
@page.attach
|
||||
class ArticleListView(page.PageListView):
|
||||
model = Article
|
||||
parent_model = Program
|
||||
attach_to_value = StaticPage.Target.ARTICLES
|
||||
|
|
28
aircox/views/auth.py
Normal file
28
aircox/views/auth.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.views.generic import ListView
|
||||
|
||||
from aircox.models import Program
|
||||
|
||||
|
||||
class UserListView(ListView):
|
||||
model = User
|
||||
queryset = User.objects.all().order_by("first_name").prefetch_related("groups")
|
||||
paginate_by = 100
|
||||
permission_required = [
|
||||
"auth.list_user",
|
||||
]
|
||||
template_name = "aircox/dashboard/user_list.html"
|
||||
|
||||
def get_users_programs(self, users):
|
||||
groups = {g for u in users for g in u.groups.all()}
|
||||
programs = Program.objects.filter(editors_group__in=groups)
|
||||
programs = {p.editors_group_id: p for p in programs}
|
||||
|
||||
for user in users:
|
||||
user.programs = [programs[g.id] for g in user.groups.all() if g.id in programs]
|
||||
|
||||
return programs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["programs"] = self.get_users_programs(self.object_list)
|
||||
return super().get_context_data(**kwargs)
|
|
@ -6,6 +6,7 @@ from django.views.generic import ListView
|
|||
from aircox.models import Diffusion, Log, StaticPage
|
||||
from .base import BaseView
|
||||
from .mixins import AttachedToMixin, GetDateMixin
|
||||
from .page import attach
|
||||
|
||||
__all__ = ("DiffusionListView", "TimeTableView")
|
||||
|
||||
|
@ -21,6 +22,7 @@ class DiffusionListView(BaseDiffusionListView):
|
|||
model = Diffusion
|
||||
|
||||
|
||||
@attach
|
||||
class TimeTableView(GetDateMixin, BaseDiffusionListView):
|
||||
model = Diffusion
|
||||
redirect_date_url = "timetable-list"
|
||||
|
|
|
@ -5,7 +5,7 @@ from aircox.models import Episode, Program, StaticPage, Track
|
|||
from aircox import forms, filters, permissions
|
||||
|
||||
from .mixins import VueFormDataMixin
|
||||
from .page import PageDetailView, PageListView, PageUpdateView
|
||||
from .page import attach, PageDetailView, PageListView, PageUpdateView
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
@ -33,6 +33,7 @@ class EpisodeDetailView(PageDetailView):
|
|||
return reverse("episode-list", kwargs={"parent_slug": self.object.parent.slug})
|
||||
|
||||
|
||||
@attach
|
||||
class EpisodeListView(PageListView):
|
||||
model = Episode
|
||||
filterset_class = filters.EpisodeFilters
|
||||
|
@ -40,6 +41,7 @@ class EpisodeListView(PageListView):
|
|||
attach_to_value = StaticPage.Target.EPISODES
|
||||
|
||||
|
||||
@attach
|
||||
class PodcastListView(EpisodeListView):
|
||||
attach_to_value = StaticPage.Target.PODCASTS
|
||||
queryset = Episode.objects.published().with_podcasts().order_by("-pub_date")
|
||||
|
|
|
@ -6,8 +6,10 @@ from django.views.generic import ListView
|
|||
from ..models import Diffusion, Episode, Log, Page, StaticPage
|
||||
from .base import BaseView
|
||||
from .mixins import AttachedToMixin
|
||||
from .page import attach
|
||||
|
||||
|
||||
@attach
|
||||
class HomeView(AttachedToMixin, BaseView, ListView):
|
||||
template_name = "aircox/home.html"
|
||||
attach_to_value = StaticPage.Target.HOME
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from django.http import HttpResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.views.generic.edit import UpdateView
|
||||
from django.views.generic.edit import CreateView, UpdateView
|
||||
from django.urls import reverse
|
||||
from honeypot.decorators import check_honeypot
|
||||
|
||||
from aircox.conf import settings
|
||||
from ..filters import PageFilters
|
||||
from ..forms import CommentForm
|
||||
from ..models import Comment, Category
|
||||
|
@ -12,6 +13,8 @@ from .base import BaseView
|
|||
from .mixins import AttachedToMixin, FiltersMixin, ParentMixin
|
||||
|
||||
__all__ = [
|
||||
"attached_views",
|
||||
"attach",
|
||||
"BasePageListView",
|
||||
"BasePageDetailView",
|
||||
"PageDetailView",
|
||||
|
@ -20,6 +23,16 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
attached_views = {}
|
||||
"""Register views by StaticPage.Target."""
|
||||
|
||||
|
||||
def attach(cls):
|
||||
"""Add decorated view class to `attached_views`"""
|
||||
attached_views[cls.attach_to_value] = cls
|
||||
return cls
|
||||
|
||||
|
||||
class BasePageMixin:
|
||||
category = None
|
||||
|
||||
|
@ -171,7 +184,7 @@ class PageDetailView(BasePageDetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_comment_form(self):
|
||||
if self.object.allow_comments:
|
||||
if settings.ALLOW_COMMENTS and self.object.allow_comments:
|
||||
return CommentForm()
|
||||
return None
|
||||
|
||||
|
@ -192,9 +205,15 @@ class PageDetailView(BasePageDetailView):
|
|||
return self.get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class PageUpdateView(BaseView, UpdateView):
|
||||
context_object_name = "page"
|
||||
|
||||
class PageCreateView(BaseView, CreateView):
|
||||
def get_page(self):
|
||||
return self.object
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.path
|
||||
|
||||
|
||||
class PageUpdateView(BaseView, UpdateView):
|
||||
def get_page(self):
|
||||
return self.object
|
||||
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
import random
|
||||
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin
|
||||
from django.urls import reverse
|
||||
|
||||
from aircox import models, forms, permissions
|
||||
from . import page
|
||||
from .mixins import VueFormDataMixin
|
||||
from .page import PageDetailView, PageListView, PageUpdateView
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ProgramDetailView",
|
||||
"ProgramDetailView",
|
||||
"ProgramCreateView",
|
||||
"ProgramUpdateView",
|
||||
)
|
||||
|
||||
|
||||
class ProgramDetailView(PageDetailView):
|
||||
class ProgramDetailView(page.PageDetailView):
|
||||
model = models.Program
|
||||
|
||||
def get_related_queryset(self):
|
||||
|
@ -44,7 +47,8 @@ class ProgramDetailView(PageDetailView):
|
|||
return super().get_template_names() + ["aircox/program_detail.html"]
|
||||
|
||||
|
||||
class ProgramListView(PageListView):
|
||||
@page.attach
|
||||
class ProgramListView(page.PageListView):
|
||||
model = models.Program
|
||||
attach_to_value = models.StaticPage.Target.PROGRAMS
|
||||
|
||||
|
@ -52,11 +56,18 @@ class ProgramListView(PageListView):
|
|||
return super().get_queryset().order_by("title")
|
||||
|
||||
|
||||
class ProgramUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
|
||||
class ProgramEditMixin(VueFormDataMixin):
|
||||
model = models.Program
|
||||
form_class = forms.ProgramForm
|
||||
queryset = models.Program.objects.select_related("editors_group")
|
||||
|
||||
|
||||
# FIXME: not used as long there is no complete administration mgt (schedule, etc.)
|
||||
class ProgramCreateView(PermissionRequiredMixin, ProgramEditMixin, page.PageCreateView):
|
||||
permission_required = "aircox.add_program"
|
||||
|
||||
|
||||
class ProgramUpdateView(UserPassesTestMixin, ProgramEditMixin, page.PageUpdateView):
|
||||
def test_func(self):
|
||||
obj = self.get_object()
|
||||
return permissions.program.can(self.request.user, "update", obj)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django_filters import rest_framework as drf_filters
|
||||
from rest_framework import status, viewsets, parsers, permissions
|
||||
from rest_framework.decorators import action
|
||||
|
@ -145,6 +145,13 @@ class UserViewSet(AutocompleteMixin, viewsets.ModelViewSet):
|
|||
queryset = User.objects.all().distinct().order_by("username")
|
||||
|
||||
|
||||
class GroupViewSet(AutocompleteMixin, viewsets.ModelViewSet):
|
||||
serializer_class = serializers.auth.GroupSerializer
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
filterset_class = filters.GroupFilterSet
|
||||
queryset = Group.objects.all().distinct().order_by("name")
|
||||
|
||||
|
||||
class UserGroupViewSet(ListCommitMixin, viewsets.ModelViewSet):
|
||||
serializer_class = serializers.auth.UserGroupSerializer
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
|
|
|
@ -283,7 +283,7 @@ export default {
|
|||
|
||||
mounted() {
|
||||
const form = this.$el.closest('form')
|
||||
form.addEventListener('reset', () => {
|
||||
form && form.addEventListener('reset', () => {
|
||||
this.inputValue = this.value;
|
||||
this.select(-1)
|
||||
})
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div class="a-group-users">
|
||||
<div class="a-m2m-edit">
|
||||
<table class="table is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Members
|
||||
<slot name="items-title"></slot>
|
||||
</th>
|
||||
<th style="width: 1rem">
|
||||
<span class="icon">
|
||||
|
@ -17,8 +17,9 @@
|
|||
<template v-for="item of items" :key="item.id">
|
||||
<tr :class="[item.created && 'has-text-info', item.deleted && 'has-text-danger']">
|
||||
<td>
|
||||
<b class="mr-3">{{ item.data.user.username }}</b>
|
||||
<span>{{ item.data.user.first_name }} {{ item.data.user.last_name }}</span>
|
||||
<slot name="item" :item="item">
|
||||
{{ item.data }}
|
||||
</slot>
|
||||
</td>
|
||||
<td class="align-center">
|
||||
<input type="checkbox" class="checkbox" @change="item.deleted = $event.target.checked">
|
||||
|
@ -30,18 +31,14 @@
|
|||
<div>
|
||||
<label>
|
||||
<span class="icon">
|
||||
<i class="fa fa-user"/>
|
||||
<i class="fa fa-plus"/>
|
||||
</span>
|
||||
Add user
|
||||
Add
|
||||
</label>
|
||||
<a-autocomplete ref="autocomplete" :url="searchUrl"
|
||||
label-field="username" value-field="id"
|
||||
@select="onUserSelect">
|
||||
<a-autocomplete ref="autocomplete" v-bind="autocomplete"
|
||||
@select="onSelect">
|
||||
<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>
|
||||
<slot name="autocomplete-item" :item="item">{{ item }}</slot>
|
||||
</template>
|
||||
</a-autocomplete>
|
||||
</div>
|
||||
|
@ -57,12 +54,14 @@ export default {
|
|||
model: {type: Function, default: Model },
|
||||
// List url
|
||||
url: String,
|
||||
// User autocomplete url
|
||||
searchUrl: String,
|
||||
// POST url
|
||||
commitUrl: String,
|
||||
// default values
|
||||
initials: {type: Object, default: () => ({})},
|
||||
// v-bind to autocomplete search box
|
||||
autocomplete: {type: Object },
|
||||
|
||||
source_id: Number,
|
||||
source_field: String,
|
||||
target_field: String,
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -73,24 +72,32 @@ export default {
|
|||
|
||||
computed: {
|
||||
items() { return this.set?.items || [] },
|
||||
user_ids() { return this.set?.items.map(i => i.data.user.id) },
|
||||
initials() {
|
||||
let obj = {}
|
||||
obj[this.source_id_attr] = this.source_id
|
||||
return obj
|
||||
},
|
||||
|
||||
source_id_attr() { return this.source_field + "_id" },
|
||||
target_id_attr() { return this.target_field + "_id" },
|
||||
target_ids() { return this.set?.items.map(i => i.data[this.target_id_attr]) },
|
||||
},
|
||||
|
||||
methods: {
|
||||
onUserSelect(index, item, value) {
|
||||
if(this.user_ids.indexOf(item.id) != -1)
|
||||
onSelect(index, item, value) {
|
||||
if(this.target_ids.indexOf(item.id) != -1)
|
||||
return
|
||||
|
||||
this.set.push({
|
||||
...this.initials,
|
||||
user: {...item},
|
||||
})
|
||||
let obj = {...this.initials}
|
||||
obj[this.target_field] = {...item}
|
||||
obj[this.target_id_attr] = item.id
|
||||
this.set.push(obj)
|
||||
this.$refs.autocomplete.reset()
|
||||
},
|
||||
|
||||
save() {
|
||||
this.set.commit(this.commitUrl, {
|
||||
getData: i => ({...this.initials, user_id: i.data.user.id})
|
||||
fields: [...Object.keys(this.initials), this.target_id_attr]
|
||||
})
|
||||
},
|
||||
},
|
|
@ -7,14 +7,14 @@ import AFormSet from './AFormSet.vue'
|
|||
import ATrackListEditor from './ATrackListEditor.vue'
|
||||
import ASoundListEditor from './ASoundListEditor.vue'
|
||||
|
||||
import AGroupUsers from "./AGroupUsers.vue"
|
||||
import AManyToManyEdit from "./AManyToManyEdit.vue"
|
||||
|
||||
import base from "./index.js"
|
||||
|
||||
|
||||
export const admin = {
|
||||
...base,
|
||||
AGroupUsers,
|
||||
AManyToManyEdit,
|
||||
AFileUpload, ASelectFile,
|
||||
AFormSet, ATrackListEditor, ASoundListEditor,
|
||||
AStatistics, AStreamer,
|
||||
|
|
|
@ -232,9 +232,14 @@ export class Set {
|
|||
|
||||
/**
|
||||
* Commit changes to server.
|
||||
* ref: `views.mixin.ListCommitMixin`
|
||||
* py-ref: `views.mixin.ListCommitMixin`
|
||||
*/
|
||||
commit(url, {getData=null, ...options}={}) {
|
||||
commit(url, {getData=null, fields=null, ...options}={}) {
|
||||
if(!getData && fields)
|
||||
getData = (i) => fields.reduce((r, f) => {
|
||||
r[f] = i.data[f]
|
||||
return r
|
||||
}, {})
|
||||
const createdItems = this.createdItems
|
||||
const body = {
|
||||
delete: this.deletedItems.map(i => i.id),
|
||||
|
|
|
@ -91,3 +91,8 @@ h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.modal .dropdown-menu {
|
||||
z-index: 50,
|
||||
}
|
||||
|
|
|
@ -88,6 +88,11 @@
|
|||
.flex-column { display: flex; flex-direction: column }
|
||||
.flex-grow-0 { flex-grow: 0 !important; }
|
||||
.flex-grow-1 { flex-grow: 1 !important; }
|
||||
.flex-grow-2 { flex-grow: 2 !important; }
|
||||
.flex-grow-3 { flex-grow: 3 !important; }
|
||||
.flex-grow-4 { flex-grow: 4 !important; }
|
||||
.flex-grow-5 { flex-grow: 5 !important; }
|
||||
.flex-grow-6 { flex-grow: 6 !important; }
|
||||
|
||||
.float-right { float: right }
|
||||
.float-left { float: left }
|
||||
|
|
Loading…
Reference in New Issue
Block a user