work on page form; add image selector
This commit is contained in:
parent
c74ec6fb16
commit
eb5bdcf167
|
@ -1,14 +1,23 @@
|
|||
import django_filters as filters
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Episode, Page
|
||||
from . import models
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PageFilters",
|
||||
"EpisodeFilters",
|
||||
"ImageFilterSet",
|
||||
"SoundFilterSet",
|
||||
"TrackFilterSet",
|
||||
)
|
||||
|
||||
|
||||
class PageFilters(filters.FilterSet):
|
||||
q = filters.CharFilter(method="search_filter", label=_("Search"))
|
||||
|
||||
class Meta:
|
||||
model = Page
|
||||
model = models.Page
|
||||
fields = {
|
||||
"category__id": ["in", "exact"],
|
||||
"pub_date": ["exact", "gte", "lte"],
|
||||
|
@ -22,10 +31,33 @@ class EpisodeFilters(PageFilters):
|
|||
podcast = filters.BooleanFilter(method="podcast_filter", label=_("Podcast"))
|
||||
|
||||
class Meta:
|
||||
model = Episode
|
||||
model = models.Episode
|
||||
fields = PageFilters.Meta.fields.copy()
|
||||
|
||||
def podcast_filter(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(sound__is_public=True).distinct()
|
||||
return queryset.filter(sound__isnull=True)
|
||||
|
||||
|
||||
class ImageFilterSet(filters.FilterSet):
|
||||
search = filters.CharFilter(field_name="search", method="search_filter")
|
||||
|
||||
def search_filter(self, queryset, name, value):
|
||||
return queryset.filter(original_filename__icontains=value)
|
||||
|
||||
|
||||
class SoundFilterSet(filters.FilterSet):
|
||||
station = filters.NumberFilter(field_name="program__station__id")
|
||||
program = filters.NumberFilter(field_name="program_id")
|
||||
episode = filters.NumberFilter(field_name="episode_id")
|
||||
search = filters.CharFilter(field_name="search", method="search_filter")
|
||||
|
||||
def search_filter(self, queryset, name, value):
|
||||
return queryset.search(value)
|
||||
|
||||
|
||||
class TrackFilterSet(filters.FilterSet):
|
||||
artist = filters.CharFilter(field_name="artist", lookup_expr="icontains")
|
||||
album = filters.CharFilter(field_name="album", lookup_expr="icontains")
|
||||
title = filters.CharFilter(field_name="title", lookup_expr="icontains")
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
from django import forms
|
||||
from django.forms import ModelForm, ImageField, FileField
|
||||
|
||||
from ckeditor.fields import RichTextField
|
||||
from filer.models.imagemodels import Image
|
||||
from filer.models.filemodels import File
|
||||
|
||||
from aircox.models import Comment, Episode, Program
|
||||
from aircox import models
|
||||
from aircox.controllers.sound_file import SoundFile
|
||||
|
||||
|
||||
class CommentForm(ModelForm):
|
||||
__all__ = ("CommentForm", "PageForm", "ProgramForm", "EpisodeForm")
|
||||
|
||||
|
||||
class CommentForm(forms.ModelForm):
|
||||
nickname = forms.CharField()
|
||||
email = forms.EmailField(required=False)
|
||||
content = forms.CharField(widget=forms.Textarea())
|
||||
|
@ -19,32 +19,31 @@ class CommentForm(ModelForm):
|
|||
content.widget.attrs.update({"class": "textarea"})
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
model = models.Comment
|
||||
fields = ["nickname", "email", "content"]
|
||||
|
||||
|
||||
class ProgramForm(ModelForm):
|
||||
content = RichTextField()
|
||||
new_cover = ImageField(required=False)
|
||||
class ImageForm(forms.Form):
|
||||
file = forms.ImageField()
|
||||
|
||||
|
||||
class PageForm(forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ("title", "status", "cover", "content")
|
||||
model = models.Page
|
||||
|
||||
|
||||
class ProgramForm(PageForm):
|
||||
class Meta:
|
||||
fields = PageForm.Meta.fields
|
||||
model = models.Program
|
||||
|
||||
|
||||
class EpisodeForm(PageForm):
|
||||
new_podcast = forms.FileField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Program
|
||||
fields = ["content"]
|
||||
|
||||
def save(self, commit=True):
|
||||
file_obj = self.cleaned_data["new_cover"]
|
||||
if file_obj:
|
||||
obj, _ = Image.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
|
||||
self.instance.cover = obj
|
||||
super().save(commit=commit)
|
||||
|
||||
|
||||
class EpisodeForm(ModelForm):
|
||||
content = RichTextField()
|
||||
new_podcast = FileField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Episode
|
||||
model = models.Episode
|
||||
fields = ["content"]
|
||||
|
||||
def save(self, commit=True):
|
||||
|
|
|
@ -127,9 +127,9 @@ class Program(Page):
|
|||
self.editors = editors
|
||||
super().save()
|
||||
permission, _ = Permission.objects.get_or_create(
|
||||
name=f"change program {self.title}",
|
||||
codename=self.change_permission_codename,
|
||||
content_type=ContentType.objects.get_for_model(self),
|
||||
defaults={"name": f"change program {self.title}"},
|
||||
)
|
||||
if permission not in editors.permissions.all():
|
||||
editors.permissions.add(permission)
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from filer.models.imagemodels import Image
|
||||
from taggit.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from ..models import Track, UserSettings
|
||||
|
||||
__all__ = ("TrackSerializer", "UserSettingsSerializer")
|
||||
__all__ = ("ImageSerializer", "TrackSerializer", "UserSettingsSerializer")
|
||||
|
||||
|
||||
class ImageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Image
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
|
||||
|
|
|
@ -121,6 +121,10 @@
|
|||
color: var(--heading-hg-fg);
|
||||
}
|
||||
|
||||
.panels .panel:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
|
@ -525,6 +529,34 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.a-select-file > *:not(:last-child) {
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.a-select-file .upload-preview {
|
||||
max-width: 100%;
|
||||
}
|
||||
.a-select-file .a-select-file-list {
|
||||
max-height: 30rem;
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.a-select-file .file-preview {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.a-select-file .file-preview:hover {
|
||||
box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.a-select-file .file-preview.active {
|
||||
box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.a-select-file .file-preview img {
|
||||
width: 100%;
|
||||
max-height: 10rem;
|
||||
}
|
||||
|
||||
/* Bulma Utilities */
|
||||
.button {
|
||||
-moz-appearance: none;
|
||||
|
|
|
@ -6643,6 +6643,11 @@ a.tag:hover {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.text-light {
|
||||
weight: 400;
|
||||
color: var(--text-color-light);
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
justify-content: left;
|
||||
|
@ -6665,6 +6670,10 @@ a.tag:hover {
|
|||
clear: both !important;
|
||||
}
|
||||
|
||||
.clear-unset {
|
||||
clear: unset !important;
|
||||
}
|
||||
|
||||
.d-inline {
|
||||
display: inline !important;
|
||||
}
|
||||
|
@ -6866,11 +6875,10 @@ input.half-field:not(:active):not(:hover) {
|
|||
}
|
||||
|
||||
:root {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 1rem;
|
||||
background-color: var(--body-bg);
|
||||
}
|
||||
|
||||
|
@ -6878,22 +6886,24 @@ body.mobile .grid {
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1280px) {
|
||||
html {
|
||||
font-size: 18px !important;
|
||||
@media screen and (max-width: 900px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
html {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
:root {
|
||||
--header-height: 20rem;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
@media screen and (max-width: 1280px) {
|
||||
html {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1280px) {
|
||||
html {
|
||||
font-size: 24px !important;
|
||||
}
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
|
||||
|
@ -6903,3 +6913,8 @@ h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
|
|||
.container:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-cover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
|
@ -1211,7 +1211,7 @@
|
|||
background-color: var(--vc-bg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-hg-color: transparent;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.vc-container,
|
||||
|
|
|
@ -121,6 +121,10 @@
|
|||
color: var(--heading-hg-fg);
|
||||
}
|
||||
|
||||
.panels .panel:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
|
@ -525,6 +529,34 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.a-select-file > *:not(:last-child) {
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.a-select-file .upload-preview {
|
||||
max-width: 100%;
|
||||
}
|
||||
.a-select-file .a-select-file-list {
|
||||
max-height: 30rem;
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.a-select-file .file-preview {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.a-select-file .file-preview:hover {
|
||||
box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.a-select-file .file-preview.active {
|
||||
box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.a-select-file .file-preview img {
|
||||
width: 100%;
|
||||
max-height: 10rem;
|
||||
}
|
||||
|
||||
/* Bulma Utilities */
|
||||
.file-cta,
|
||||
.file-name, .select select, .textarea, .input {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,29 +0,0 @@
|
|||
/* global CKEDITOR, django */
|
||||
/* Modified in order to be manually loaded after vue.js */
|
||||
|
||||
function initialiseCKEditor() {
|
||||
var textareas = Array.prototype.slice.call(
|
||||
document.querySelectorAll("textarea[data-type=ckeditortype]"),
|
||||
)
|
||||
for (var i = 0; i < textareas.length; ++i) {
|
||||
var t = textareas[i]
|
||||
if (
|
||||
t.getAttribute("data-processed") == "0" &&
|
||||
t.id.indexOf("__prefix__") == -1
|
||||
) {
|
||||
t.setAttribute("data-processed", "1")
|
||||
var ext = JSON.parse(t.getAttribute("data-external-plugin-resources"))
|
||||
for (var j = 0; j < ext.length; ++j) {
|
||||
CKEDITOR.plugins.addExternal(ext[j][0], ext[j][1], ext[j][2])
|
||||
}
|
||||
CKEDITOR.replace(t.id, JSON.parse(t.getAttribute("data-config")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initialiseCKEditorInInlinedForms() {
|
||||
if (typeof django === "object" && django.jQuery) {
|
||||
django.jQuery(document).on("formset:added", initialiseCKEditor)
|
||||
}
|
||||
}
|
||||
//})()
|
|
@ -7,9 +7,6 @@ Usefull context:
|
|||
- cover: image cover
|
||||
- site: current website
|
||||
- model: view model or displayed `object`'s
|
||||
- sidebar_object_list: item to display in sidebar
|
||||
- sidebar_url_name: url name sidebar item complete list
|
||||
- sidebar_url_parent: parent page for sidebar items complete list
|
||||
{% endcomment %}
|
||||
<html>
|
||||
<head>
|
||||
|
@ -100,16 +97,19 @@ Usefull context:
|
|||
{% if page or cover or title %}
|
||||
<header class="container header preview preview-header {% if cover %}has-cover{% endif %}">
|
||||
{% block header %}
|
||||
{% if cover %}
|
||||
<figure class="header-cover">
|
||||
<img src="{{ cover }}" class="cover">
|
||||
{% block header-cover %}
|
||||
{% if cover %}
|
||||
<img src="{{ cover }}" ref="cover" class="cover">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</figure>
|
||||
{% endif %}
|
||||
<div class="headings preview-card-headings">
|
||||
{% block headings %}
|
||||
<div>
|
||||
{% block title-container %}
|
||||
<h1 class="title is-1 {% block title-class %}{% endblock %}">{% block title %}{{ title|default:"" }}{% endblock %}</h1>
|
||||
{% include "aircox/edit-link.html" %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div>
|
||||
{% spaceless %}
|
||||
|
@ -146,17 +146,26 @@ Usefull context:
|
|||
{% block footer-container %}
|
||||
<footer class="page-footer">
|
||||
{% block footer %}
|
||||
{% if request.station and request.station.legal_label %}
|
||||
{{ request.station.legal_label }} —
|
||||
{% endif %}
|
||||
{% comment %}
|
||||
{% nav_items "footer" css_class="nav-item" active_class="active" as items %}
|
||||
{% for item, render in items %}
|
||||
{{ render }}
|
||||
{% endfor %}
|
||||
{% endcomment %}
|
||||
|
||||
{% if not request.user.is_authenticated %}
|
||||
<a class="nav-item" href="{% url "profile" %}" target="new"
|
||||
title="{% translate "Profile" %}">
|
||||
<span class="small icon">
|
||||
<i class="fa fa-account">
|
||||
<i class="fa fa-user"></i>
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% if request.station and request.station.legal_label %}
|
||||
{{ request.station.legal_label }} —
|
||||
{% endif %}
|
||||
</footer>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
{% load aircox i18n %}
|
||||
{% block user-actions-container %}
|
||||
{% has_perm page page.program.change_permission_codename simple=True as can_edit %}
|
||||
{% if user.is_authenticated and can_edit %}
|
||||
{% with request.resolver_match.view_name as view_name %}
|
||||
|
||||
{% if view_name in 'page-edit,program-edit,episode-edit' %}
|
||||
<a href="{% url view_name|detail_view page.slug %}" target="_self">
|
||||
<small>
|
||||
<span title="{% translate 'View' %} {{ page }}">{% translate 'View' %} </span>
|
||||
<i class="fa-regular fa-eye"></i>
|
||||
</small>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url view_name|edit_view page.pk %}" target="_self">
|
||||
<small>
|
||||
<span title="{% translate 'Edit' %} {{ page }}">{% translate 'Edit' %} </span>
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</small>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -23,6 +23,12 @@ Context:
|
|||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block title-container %}
|
||||
{{ block.super }}
|
||||
{% block page-actions %}
|
||||
{% include "aircox/widgets/page_actions.html" %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ block.super }}
|
||||
|
|
71
aircox/templates/aircox/page_form.html
Normal file
71
aircox/templates/aircox/page_form.html
Normal file
|
@ -0,0 +1,71 @@
|
|||
{% extends "./page_detail.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block header-cover %}
|
||||
<img src="{{ cover }}" ref="cover" class="cover">
|
||||
{% endblock %}
|
||||
|
||||
{% block content-container %}
|
||||
<section class="container active">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="field">
|
||||
{% if field.name == "cover" %}
|
||||
<input type="hidden" name="{{ field.name }}" value="{{ field.pk }}" ref="coverField"/>
|
||||
{% else %}
|
||||
<label class="label">{{ field.label }}</label>
|
||||
<div class="control clear-unset">
|
||||
{% if field.name == "pub_date" %}
|
||||
<input type="datetime-local" 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">{{ field.value|striptags|safe }}</textarea>
|
||||
{% else %}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</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 %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="has-text-right">
|
||||
<button type="submit" class="button">{% translate "Update" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="cover-modal" class="container page-edit-panel">
|
||||
<h3 class="title">{% translate "Change cover" %}</h3>
|
||||
<a-select-file list-url="{% url "api:image-list" %}" upload-url="{% url "api:image-list" %}"
|
||||
prev-label="{% translate "Show previous" %}"
|
||||
next-label="{% translate "Show next" %}"
|
||||
>
|
||||
<template #upload-preview="{item}">
|
||||
<template v-if="item">
|
||||
<img :src="item.src" class="upload-preview"/>
|
||||
</template>
|
||||
</template>
|
||||
<template #default="{item}">
|
||||
<div class="flex-column">
|
||||
<div class="flex-grow-1">
|
||||
<img :src="item.file"/>
|
||||
</div>
|
||||
<label class="label">[[ item.name || item.original_filename ]]</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer="{item}">
|
||||
<button type="button" class="button float-right"
|
||||
@click="(event) => {$refs.cover.src = item.file; $refs.coverField.value = item.id}">
|
||||
{% translate "Select" %}
|
||||
</button>
|
||||
</template>
|
||||
</a-select-file>
|
||||
</section>
|
||||
{% endblock %}
|
30
aircox/templates/aircox/widgets/page_actions.html
Normal file
30
aircox/templates/aircox/widgets/page_actions.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% load aircox i18n %}
|
||||
{% block user-actions-container %}
|
||||
{% has_perm page page.program.change_permission_codename simple=True as can_edit %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
{{ object.get_status_display }}
|
||||
({{ object.pub_date|date:"d/m/Y H:i" }})
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_authenticated and can_edit %}
|
||||
{% with request.resolver_match.view_name as view_name %}
|
||||
|
||||
{% if "-edit" in view_name %}
|
||||
<a href="{% url view_name|detail_view page.slug %}" target="_self" title="{% translate 'View' %} {{ page }}">
|
||||
<span class="icon">
|
||||
<i class="fa-regular fa-eye"></i>
|
||||
</span>
|
||||
<span>{% translate 'View' %} </span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url view_name|edit_view page.pk %}" target="_self" title="{% translate 'Edit' %} {{ page }}">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
<span>{% translate 'Edit' %} </span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -2,7 +2,7 @@ from django.urls import include, path, register_converter
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from . import models, views, viewsets
|
||||
from . import forms, models, views, viewsets
|
||||
from .converters import DateConverter, PagePathConverter, WeekConverter
|
||||
|
||||
__all__ = ["api", "urls"]
|
||||
|
@ -21,6 +21,7 @@ register_converter(WeekConverter, "week")
|
|||
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register("images", viewsets.ImageViewSet, basename="image")
|
||||
router.register("sound", viewsets.SoundViewSet, basename="sound")
|
||||
router.register("track", viewsets.TrackROViewSet, basename="track")
|
||||
|
||||
|
@ -132,6 +133,12 @@ urls = [
|
|||
views.errors.NoStationErrorView.as_view(),
|
||||
name="errors-no-station",
|
||||
),
|
||||
path(_("manage/"), views.ProfileView.as_view(), name="profile"),
|
||||
# ---- backoffice
|
||||
path(_("edit/"), views.ProfileView.as_view(), name="profile"),
|
||||
path(
|
||||
_("edit/programs/<slug:slug>"),
|
||||
views.PageUpdateView.as_view(model=models.Program, form_class=forms.ProgramForm),
|
||||
name="program-update",
|
||||
),
|
||||
path("accounts/profile/", views.ProfileView.as_view(), name="profile"),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from . import admin, errors
|
||||
from .article import ArticleDetailView, ArticleListView
|
||||
from .base import BaseAPIView, BaseView
|
||||
from .diffusion import DiffusionListView, TimeTableView
|
||||
from .episode import EpisodeDetailView, EpisodeListView, PodcastListView, EpisodeUpdateView
|
||||
from .home import HomeView
|
||||
|
@ -10,6 +9,7 @@ from .page import (
|
|||
BasePageListView,
|
||||
PageDetailView,
|
||||
PageListView,
|
||||
PageUpdateView,
|
||||
)
|
||||
from .profile import ProfileView
|
||||
from .program import (
|
||||
|
@ -20,13 +20,12 @@ from .program import (
|
|||
ProgramUpdateView,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"admin",
|
||||
"errors",
|
||||
"ArticleDetailView",
|
||||
"ArticleListView",
|
||||
"BaseAPIView",
|
||||
"BaseView",
|
||||
"DiffusionListView",
|
||||
"TimeTableView",
|
||||
"EpisodeDetailView",
|
||||
|
@ -39,6 +38,7 @@ __all__ = (
|
|||
"BasePageDetailView",
|
||||
"BasePageListView",
|
||||
"PageDetailView",
|
||||
"PageUpdateView",
|
||||
"PageListView",
|
||||
"ProfileView",
|
||||
"ProgramDetailView",
|
||||
|
|
|
@ -24,7 +24,10 @@ class BasePageMixin:
|
|||
category = None
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_subclasses().published().select_related("cover")
|
||||
qs = super().get_queryset().select_subclasses().select_related("cover")
|
||||
if self.request.user.is_authenticated:
|
||||
return qs
|
||||
return qs.published()
|
||||
|
||||
def get_category(self, page, **kwargs):
|
||||
if page:
|
||||
|
@ -153,8 +156,8 @@ class PageDetailView(BasePageDetailView):
|
|||
return super().get_queryset().select_related("category")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
if self.object.allow_comments and "comment_form" not in kwargs:
|
||||
kwargs["comment_form"] = CommentForm()
|
||||
if "comment_form" not in kwargs:
|
||||
kwargs["comment_form"] = self.get_comment_form()
|
||||
kwargs["comments"] = Comment.objects.filter(page=self.object).order_by("-date")
|
||||
|
||||
if self.object.parent_subclass:
|
||||
|
@ -167,6 +170,11 @@ class PageDetailView(BasePageDetailView):
|
|||
kwargs["related_objects"] = related
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_comment_form(self):
|
||||
if self.object.allow_comments:
|
||||
return CommentForm()
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, *args, **kwargs):
|
||||
view = super(PageDetailView, cls).as_view(*args, **kwargs)
|
||||
|
@ -186,6 +194,14 @@ class PageDetailView(BasePageDetailView):
|
|||
|
||||
class PageUpdateView(BaseView, UpdateView):
|
||||
context_object_name = "page"
|
||||
template_name = "aircox/page_form.html"
|
||||
|
||||
# FIXME: remove?
|
||||
def get_page(self):
|
||||
return self.object
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.path
|
||||
|
||||
def get_comment_form(self):
|
||||
return None
|
||||
|
|
|
@ -56,9 +56,6 @@ class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
|
|||
model = Program
|
||||
form_class = ProgramForm
|
||||
|
||||
def get_sidebar_queryset(self):
|
||||
return super().get_sidebar_queryset().filter(parent=self.program)
|
||||
|
||||
def test_func(self):
|
||||
program = self.get_object()
|
||||
return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
|
||||
|
@ -94,7 +91,3 @@ class ProgramPageListView(BaseProgramMixin, PageListView):
|
|||
|
||||
def get_program(self):
|
||||
return self.parent
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.setdefault("sidebar_url_parent", None)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
|
|
@ -1,44 +1,46 @@
|
|||
from django_filters import rest_framework as filters
|
||||
from django_filters import rest_framework as drf_filters
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
|
||||
from .models import Sound, Track
|
||||
from filer.models.imagemodels import Image
|
||||
|
||||
from . import models, forms, filters
|
||||
from .serializers import SoundSerializer, admin
|
||||
from .views import BaseAPIView
|
||||
|
||||
__all__ = (
|
||||
"SoundFilter",
|
||||
"ImageViewSet",
|
||||
"SoundViewSet",
|
||||
"TrackFilter",
|
||||
"TrackROViewSet",
|
||||
"UserSettingsViewSet",
|
||||
)
|
||||
|
||||
|
||||
class SoundFilter(filters.FilterSet):
|
||||
station = filters.NumberFilter(field_name="program__station__id")
|
||||
program = filters.NumberFilter(field_name="program_id")
|
||||
episode = filters.NumberFilter(field_name="episode_id")
|
||||
search = filters.CharFilter(field_name="search", method="search_filter")
|
||||
class ImageViewSet(viewsets.ModelViewSet):
|
||||
parsers = (MultiPartParser,)
|
||||
serializer_class = admin.ImageSerializer
|
||||
queryset = Image.objects.all().order_by("-uploaded_at")
|
||||
filter_backends = (drf_filters.DjangoFilterBackend,)
|
||||
filterset_class = filters.ImageFilterSet
|
||||
|
||||
def search_filter(self, queryset, name, value):
|
||||
return queryset.search(value)
|
||||
def create(self, request, **kwargs):
|
||||
# FIXME: to be replaced by regular DRF
|
||||
form = forms.ImageForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
file = form.cleaned_data["file"]
|
||||
Image.objects.create(original_filename=file.name, file=file)
|
||||
return Response({"status": "ok"})
|
||||
return Response({"status": "error", "errors": form.errors})
|
||||
|
||||
|
||||
class SoundViewSet(BaseAPIView, viewsets.ModelViewSet):
|
||||
serializer_class = SoundSerializer
|
||||
queryset = Sound.objects.available().order_by("-pk")
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filterset_class = SoundFilter
|
||||
|
||||
|
||||
# --- admin
|
||||
class TrackFilter(filters.FilterSet):
|
||||
artist = filters.CharFilter(field_name="artist", lookup_expr="icontains")
|
||||
album = filters.CharFilter(field_name="album", lookup_expr="icontains")
|
||||
title = filters.CharFilter(field_name="title", lookup_expr="icontains")
|
||||
queryset = models.Sound.objects.available().order_by("-pk")
|
||||
filter_backends = (drf_filters.DjangoFilterBackend,)
|
||||
filterset_class = filters.SoundFilterSet
|
||||
|
||||
|
||||
class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
@ -46,9 +48,9 @@ class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
|
||||
serializer_class = admin.TrackSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filterset_class = TrackFilter
|
||||
queryset = Track.objects.all()
|
||||
filter_backends = (drf_filters.DjangoFilterBackend,)
|
||||
filterset_class = filters.TrackFilterSet
|
||||
queryset = models.Track.objects.all()
|
||||
|
||||
@action(name="autocomplete", detail=False)
|
||||
def autocomplete(self, request):
|
||||
|
@ -60,6 +62,7 @@ class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
return self.list(request)
|
||||
|
||||
|
||||
# --- admin
|
||||
class UserSettingsViewSet(viewsets.ViewSet):
|
||||
"""User's settings specific to aircox.
|
||||
|
||||
|
|
|
@ -86,3 +86,8 @@ h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
|
|||
.container:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-cover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
|
@ -138,6 +138,11 @@
|
|||
}
|
||||
|
||||
|
||||
// ---- panels
|
||||
.panels {
|
||||
.panel:not(.active) { display: none; }
|
||||
}
|
||||
|
||||
// ---- button
|
||||
@mixin button {
|
||||
.button, a.button, button.button {
|
||||
|
@ -701,3 +706,40 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/// ----------------
|
||||
.a-select-file {
|
||||
> *:not(:last-child) {
|
||||
margin-bottom: v.$mp-3;
|
||||
}
|
||||
|
||||
.upload-preview {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.a-select-file-list {
|
||||
max-height: 30rem;
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: v.$mp-3;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0em 0em 1em rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: 0em 0em 1em rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-height: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
@use "./vars" as v;
|
||||
|
||||
.text-light { weight: 400; color: var(--text-color-light); }
|
||||
|
||||
// ---- layout
|
||||
.align-left { text-align: left; justify-content: left; }
|
||||
.align-right { text-align: right; justify-content: right; }
|
||||
|
@ -7,6 +9,7 @@
|
|||
.clear-left { clear: left !important }
|
||||
.clear-right { clear: right !important }
|
||||
.clear-both { clear: both !important }
|
||||
.clear-unset { clear: unset !important }
|
||||
|
||||
.d-inline { display: inline !important; }
|
||||
.d-block { display: block !important; }
|
||||
|
@ -20,6 +23,7 @@
|
|||
|
||||
.ws-nowrap { white-space: nowrap; }
|
||||
|
||||
|
||||
// ---- grid
|
||||
@mixin grid {
|
||||
display: grid;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
|
||||
// ---- main theme & layout
|
||||
|
||||
.page {
|
||||
padding-bottom: 5rem;
|
||||
|
||||
|
|
124
assets/src/components/ASelectFile.vue
Normal file
124
assets/src/components/ASelectFile.vue
Normal file
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<div class="a-select-file">
|
||||
<div class="a-select-file-list" ref="list">
|
||||
<div class="flex-column file-preview">
|
||||
<div class="field flex-grow-1" v-if="!uploadFile">
|
||||
<label class="label">{{ uploadLabel }}</label>
|
||||
<input type="file" @change="previewFile"/>
|
||||
</div>
|
||||
<slot name="upload-preview" :item="uploadFile"></slot>
|
||||
<div v-if="uploadFile">
|
||||
<button class="button secondary" @click="removeUpload">
|
||||
<span class="icon">
|
||||
<i class="fa fa-trash"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button class="button float-right" @click="doUpload">
|
||||
<span class="icon">
|
||||
<i class="fa fa-upload"></i>
|
||||
</span>
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="prevUrl">
|
||||
<a href="#" @click="load(prevUrl)">
|
||||
{{ prevLabel }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<template v-for="item in items" v-bind:key="item.id">
|
||||
<div :class="['file-preview', this.item && item.id == this.item.id && 'active']" @click="select(item)">
|
||||
<slot :item="item"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="nextUrl">
|
||||
<a href="#" @click="load(nextUrl)">
|
||||
{{ nextLabel }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="a-select-footer">
|
||||
<slot name="footer" :item="item" :items="items"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {getCsrf} from "../model"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
name: { type: String },
|
||||
prevLabel: { type: String, default: "Prev" },
|
||||
nextLabel: { type: String, default: "Next" },
|
||||
listUrl: { type: String },
|
||||
uploadLabel: { type: String, default: "Upload a file" },
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
item: null,
|
||||
items: [],
|
||||
uploadFile: null,
|
||||
uploadUrl: null,
|
||||
uploadFieldName: null,
|
||||
uploadCSRF: null,
|
||||
nextUrl: "",
|
||||
prevUrl: "",
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
previewFile(event) {
|
||||
const [file] = event.target.files
|
||||
this.uploadFile = file && {
|
||||
file: file,
|
||||
src: URL.createObjectURL(file)
|
||||
}
|
||||
},
|
||||
|
||||
removeUpload() {
|
||||
this.uploadFile = null;
|
||||
},
|
||||
|
||||
doUpload() {
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.uploadFile.file)
|
||||
formData.append('original_filename', this.uploadFile.file.name)
|
||||
formData.append('csrfmiddlewaretoken', getCsrf())
|
||||
fetch(this.listUrl, {
|
||||
method: "POST",
|
||||
body: formData
|
||||
}).then(
|
||||
() => {
|
||||
this.uploadFile = null;
|
||||
this.load()
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
load(url) {
|
||||
fetch(url || this.listUrl).then(
|
||||
response => response.ok ? response.json() : Promise.reject(response)
|
||||
).then(data => {
|
||||
this.nextUrl = data.next
|
||||
this.prevUrl = data.previous
|
||||
this.items = data.results
|
||||
|
||||
this.$forceUpdate()
|
||||
this.$refs.list.scroll(0, 0)
|
||||
})
|
||||
},
|
||||
|
||||
select(item) {
|
||||
this.item = item;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.load()
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<button :title="ariaLabel"
|
||||
type="button"
|
||||
:aria-label="ariaLabel || label" :aria-description="ariaDescription"
|
||||
@click="toggle" :class="buttonClass">
|
||||
<slot name="default" :active="active">
|
||||
|
@ -49,17 +50,21 @@ export default {
|
|||
},
|
||||
|
||||
set(active) {
|
||||
const el = document.querySelector(this.el)
|
||||
if(active)
|
||||
el.classList.add(this.activeClass)
|
||||
else
|
||||
el.classList.remove(this.activeClass)
|
||||
if(this.el) {
|
||||
const el = document.querySelector(this.el)
|
||||
if(active)
|
||||
el.classList.add(this.activeClass)
|
||||
else
|
||||
el.classList.remove(this.activeClass)
|
||||
}
|
||||
this.active = active
|
||||
if(active)
|
||||
this.resetGroup()
|
||||
},
|
||||
|
||||
resetGroup() {
|
||||
if(!this.groupClass)
|
||||
return
|
||||
const els = document.querySelectorAll("." + this.groupClass)
|
||||
for(var el of els)
|
||||
if(el != this.$el)
|
||||
|
|
|
@ -12,13 +12,15 @@ import ASoundItem from './ASoundItem.vue'
|
|||
import ASwitch from './ASwitch.vue'
|
||||
import AStatistics from './AStatistics.vue'
|
||||
import AStreamer from './AStreamer.vue'
|
||||
import ASelectFile from "./ASelectFile.vue"
|
||||
|
||||
/**
|
||||
* Core components
|
||||
*/
|
||||
export const base = {
|
||||
AAutocomplete, ACarousel, ADropdown, AEpisode, AList, APage, APlayer, APlaylist,
|
||||
AProgress, ASoundItem, ASwitch
|
||||
AProgress, ASoundItem, ASwitch,
|
||||
ASelectFile,
|
||||
}
|
||||
|
||||
export default base
|
||||
|
|
|
@ -30,6 +30,7 @@ export default class Live {
|
|||
response.ok ? response.json()
|
||||
: Promise.reject(response)
|
||||
).then(data => {
|
||||
data = data.results
|
||||
data.forEach(item => {
|
||||
if(item.start) item.start = new Date(item.start)
|
||||
if(item.end) item.end = new Date(item.end)
|
||||
|
|
|
@ -252,3 +252,6 @@ TEMPLATES = [
|
|||
WSGI_APPLICATION = "instance.wsgi.application"
|
||||
|
||||
LOGOUT_REDIRECT_URL = "/"
|
||||
|
||||
|
||||
REST_FRAMEWORK = {"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 50}
|
||||
|
|
Loading…
Reference in New Issue
Block a user