This commit is contained in:
bkfox 2024-03-15 20:54:52 +01:00
commit c79f040fa1
19 changed files with 1589 additions and 161 deletions

View File

@ -1,7 +1,12 @@
from django import forms from django import forms
from django.forms import ModelForm from django.forms import ModelForm, ImageField, FileField
from .models import Comment 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.controllers.sound_file import SoundFile
class CommentForm(ModelForm): class CommentForm(ModelForm):
@ -16,3 +21,38 @@ class CommentForm(ModelForm):
class Meta: class Meta:
model = Comment model = Comment
fields = ["nickname", "email", "content"] fields = ["nickname", "email", "content"]
class ProgramForm(ModelForm):
content = RichTextField()
new_cover = ImageField(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
fields = ["content"]
def save(self, commit=True):
file_obj = self.cleaned_data["new_podcast"]
if file_obj:
obj, _ = File.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
sound_file = SoundFile(obj.path)
sound_file.sync(
program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True
)
super().save(commit=commit)

File diff suppressed because it is too large Load Diff

View File

@ -125,6 +125,7 @@ class Program(Page):
editors, created = Group.objects.get_or_create(name=self.editors_group_name) editors, created = Group.objects.get_or_create(name=self.editors_group_name)
if created: if created:
self.editors = editors self.editors = editors
super().save()
permission, _ = Permission.objects.get_or_create( permission, _ = Permission.objects.get_or_create(
name=f"change program {self.title}", name=f"change program {self.title}",
codename=self.change_permission_codename, codename=self.change_permission_codename,
@ -159,7 +160,6 @@ class Program(Page):
Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_)))) Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_))))
self.set_group_ownership() self.set_group_ownership()
super().save(*kargs, **kwargs)
class ProgramChildQuerySet(PageQuerySet): class ProgramChildQuerySet(PageQuerySet):

View File

@ -81,6 +81,9 @@ class Station(models.Model):
max_length=64, max_length=64,
default=_("Music stream"), default=_("Music stream"),
) )
legal_label = models.CharField(
_("Legal label"), max_length=64, blank=True, default="", help_text=_("Displayed at the bottom of pages.")
)
objects = StationQuerySet.as_manager() objects = StationQuerySet.as_manager()

View File

@ -51,7 +51,7 @@ Usefull context:
<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">
{% block nav-primary %} {% block primary-nav %}
<a class="nav-brand" href="{% url "home" %}"> <a class="nav-brand" href="{% url "home" %}">
<img src="{{ station.logo.url }}"> <img src="{{ station.logo.url }}">
</a> </a>
@ -60,7 +60,7 @@ Usefull context:
aria-label="{% translate "Main menu" %}"> aria-label="{% translate "Main menu" %}">
</a-switch> </a-switch>
<div class="nav-menu"> <div class="nav-menu">
{% block nav-primary-menu %} {% block primary-nav-menu %}
{% nav_items "top" css_class="nav-item" active_class="active" as items %} {% nav_items "top" css_class="nav-item" active_class="active" as items %}
{% for item, render in items %} {% for item, render in items %}
{{ render }} {{ render }}
@ -71,11 +71,9 @@ Usefull context:
</a> </a>
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="nav-item" href="{% url "profile" %}" target="new"> <a class="nav-item" href="{% url "logout" %}" title="{% translate "Disconnect" %}"
{% translate "Profile" %} aria-label="{% translate "Disconnect" %}">
</a> <i class="fa fa-power-off"></i>
<a class="nav-item" href="{% url 'logout' %}">
<i title="{% translate 'disconnect' %}" class="fa fa-power-off"></i>
</a> </a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@ -98,8 +96,6 @@ Usefull context:
{% endblock %} {% endblock %}
{% endspaceless %} {% endspaceless %}
{% block header-container %} {% block header-container %}
{% if page or cover or title %} {% if page or cover or title %}
<header class="container header preview preview-header {% if cover %}has-cover{% endif %}"> <header class="container header preview preview-header {% if cover %}has-cover{% endif %}">
@ -146,6 +142,24 @@ Usefull context:
{% endblock %} {% endblock %}
</main> </main>
{% endblock %} {% endblock %}
{% block footer-container %}
<footer class="page-footer">
{% block footer %}
{% if request.station and request.station.legal_label %}
{{ request.station.legal_label }} &mdash;
{% endif %}
<a class="nav-item" href="{% url "profile" %}" target="new"
title="{% translate "Profile" %}">
<span class="small icon">
<i class="fa fa-account">
</span>
</a>
{% endblock %}
</footer>
{% endblock %}
</div> </div>
{% block player-container %} {% block player-container %}
<div id="player">{% include "aircox/widgets/player.html" %}</div> <div id="player">{% include "aircox/widgets/player.html" %}</div>

View File

@ -4,15 +4,19 @@
{% if user.is_authenticated and can_edit %} {% if user.is_authenticated and can_edit %}
{% with request.resolver_match.view_name as view_name %} {% with request.resolver_match.view_name as view_name %}
&nbsp; &nbsp;
{% if view_name in 'program-edit,bla' %} {% if view_name in 'page-edit,program-edit,episode-edit' %}
<!-- <a href="{% url view_name|detail_view page.slug %}" target="_self">
<a href="{% url 'program-detail' page.slug %}" target="_self"> <small>
<span title="{% translate 'View' %} {{ page }}">{% translate 'View' %} 👁 </span> <span title="{% translate 'View' %} {{ page }}">{% translate 'View' %} </span>
<i class="fa-regular fa-eye"></i>
</small>
</a> </a>
-->
{% else %} {% else %}
<a href="{% url view_name|edit_view page.pk %}" target="_self"> <a href="{% url view_name|edit_view page.pk %}" target="_self">
<span title="{% translate 'Edit' %} {{ page }}">{% translate 'Edit' %} 🖉 </span> <small>
<span title="{% translate 'Edit' %} {{ page }}">{% translate 'Edit' %} </span>
<i class="fa-solid fa-pencil"></i>
</small>
</a> </a>
{% endif %} {% endif %}
{% endwith %} {% endwith %}

View File

@ -23,10 +23,10 @@
<table> <table>
{{ form.as_table }} {{ form.as_table }}
{% render_honeypot_field "website" %} {% render_honeypot_field "website" %}
</table> </table>
<br/> <br/>
<input type="submit" value="Update" class="button is-success"> <input type="submit" value="Update" class="button is-success">
<hr>
{% include "aircox/playlist_inline.html" %} {% include "aircox/playlist_inline.html" %}

View File

@ -16,12 +16,23 @@
<section class="container"> <section class="container">
<div> <div>
<form method="post" enctype="multipart/form-data">{% csrf_token %} <form method="post" enctype="multipart/form-data">{% csrf_token %}
<table> {% csrf_token %}
{{ form.as_table }} {% for field in form %}
{% render_honeypot_field "website" %} <div class="field is-horizontal">
</table> <label class="label">{{ field.label }}</label>
<br/> <div class="control">{{ field }}</div>
<input type="submit" value="Update" class="button is-success"> </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> </form>
</div> </div>
</section> </section>

View File

@ -143,3 +143,8 @@ def do_verbose_name(obj, plural=False):
@register.filter(name="edit_view") @register.filter(name="edit_view")
def do_edit_view(obj): def do_edit_view(obj):
return "%s-edit" % obj.split("-")[0] return "%s-edit" % obj.split("-")[0]
@register.filter(name="detail_view")
def do_detail_view(obj):
return "%s-detail" % obj.split("-")[0]

View File

@ -1,6 +1,7 @@
from datetime import time, timedelta from datetime import time, timedelta
import itertools import itertools
import logging import logging
import os
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -162,3 +163,10 @@ def tracks(episode, sound):
@pytest.fixture @pytest.fixture
def user(): def user():
return User.objects.create_user(username="user1", password="bar") return User.objects.create_user(username="user1", password="bar")
@pytest.fixture
def png_content():
image_file = "{}/image.png".format(os.path.dirname(__file__))
with open(image_file, "rb") as fh:
return fh.read()

BIN
aircox/tests/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -7,12 +7,6 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from aircox.models import Program from aircox.models import Program
png_content = (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde"
+ b"\x00\x00\x00\x0cIDATx\x9cc`\xf8\xcf\x00\x00\x02\x02\x01\x00{\t\x81x\x00\x00\x00\x00IEND\xaeB`\x82"
)
@pytest.mark.django_db() @pytest.mark.django_db()
def test_edit_program(user, client, program): def test_edit_program(user, client, program):
client.force_login(user) client.force_login(user)
@ -29,7 +23,7 @@ def test_edit_program(user, client, program):
@pytest.mark.django_db() @pytest.mark.django_db()
def test_add_cover(user, client, program): def test_add_cover(user, client, program, png_content):
assert program.cover is None assert program.cover is None
user.groups.add(program.editors) user.groups.add(program.editors)
client.force_login(user) client.force_login(user)
@ -58,16 +52,16 @@ def test_edit_tracklist(user, client, program, episode, tracks):
tracklist = [t.id for t in episode.track_set.all().order_by("position")] tracklist = [t.id for t in episode.track_set.all().order_by("position")]
tracklist_details_reversed = [(t.id, t.artist, t.title) for t in episode.track_set.all().order_by("-position")] tracklist_details_reversed = [(t.id, t.artist, t.title) for t in episode.track_set.all().order_by("-position")]
tracklist_details_reversed = list(chain(*tracklist_details_reversed)) tracklist_details_reversed = list(chain(*tracklist_details_reversed))
data = """{"website": [""], "content": ["foobar"], "new_podcast": [""], "form-TOTAL_FORMS": ["3"], data = """{{"website": [""], "content": ["foobar"], "new_podcast": [""], "form-TOTAL_FORMS": ["3"],
"form-INITIAL_FORMS": ["3"], "form-MIN_NUM_FORMS": ["0"], "form-MAX_NUM_FORMS": ["1000"], "form-0-position": ["0"], "form-INITIAL_FORMS": ["3"], "form-MIN_NUM_FORMS": ["0"], "form-MAX_NUM_FORMS": ["1000"], "form-0-position": ["0"],
"form-0-id": ["%s"], "form-0-": ["", "", "", "", "", ""], "form-0-artist": ["%s"], "form-0-title": ["%s"], "form-0-id": ["{}"], "form-0-": ["", "", "", "", "", ""], "form-0-artist": ["{}"], "form-0-title": ["{}"],
"form-0-tags": [""], "form-0-album": [""], "form-0-year": [""], "form-1-position": ["1"], "form-1-id": ["%s"], "form-0-tags": [""], "form-0-album": [""], "form-0-year": [""], "form-1-position": ["1"], "form-1-id": ["{}"],
"form-1-": ["", "", "", "", "", ""], "form-1-artist": ["%s"], "form-1-title": ["%s"], "form-1-tags": [""], "form-1-": ["", "", "", "", "", ""], "form-1-artist": ["{}"], "form-1-title": ["{}"], "form-1-tags": [""],
"form-1-album": [""], "form-1-year": [""], "form-2-position": ["2"], "form-2-id": ["%s"], "form-2-": ["", "", "", "form-1-album": [""], "form-1-year": [""], "form-2-position": ["2"], "form-2-id": ["{}"], "form-2-": ["", "", "",
"", "", ""], "form-2-artist": ["%s"], "form-2-title": ["%s"], "form-2-tags": [""], "form-2-album": [""], "", "", ""], "form-2-artist": ["{}"], "form-2-title": ["{}"], "form-2-tags": [""], "form-2-album": [""],
"form-2-year": [""]}""" % tuple( "form-2-year": [""]}}""".format(
tracklist_details_reversed *tracklist_details_reversed
) )
r = client.post(reverse("episode-edit", kwargs={"pk": episode.pk}), json.loads(data), follow=True) r = client.post(reverse("episode-edit", kwargs={"pk": episode.pk}), json.loads(data), follow=True)
assert r.status_code == 200 assert r.status_code == 200
assert [t.id for t in episode.track_set.all().order_by("position")] == list(reversed(tracklist)) assert set(episode.track_set.all().values_list("id", flat=True)) == set(tracklist)

View File

@ -132,6 +132,6 @@ urls = [
views.errors.NoStationErrorView.as_view(), views.errors.NoStationErrorView.as_view(),
name="errors-no-station", name="errors-no-station",
), ),
path("gestion/", views.profile, name="profile"), path(_("manage/"), views.ProfileView.as_view(), name="profile"),
path("accounts/profile/", views.profile, name="profile"), path("accounts/profile/", views.ProfileView.as_view(), name="profile"),
] ]

View File

@ -11,7 +11,7 @@ from .page import (
PageDetailView, PageDetailView,
PageListView, PageListView,
) )
from .profile import profile from .profile import ProfileView
from .program import ( from .program import (
ProgramDetailView, ProgramDetailView,
ProgramListView, ProgramListView,
@ -40,7 +40,7 @@ __all__ = (
"BasePageListView", "BasePageListView",
"PageDetailView", "PageDetailView",
"PageListView", "PageListView",
"profile", "ProfileView",
"ProgramDetailView", "ProgramDetailView",
"ProgramListView", "ProgramListView",
"ProgramPageDetailView", "ProgramPageDetailView",

View File

@ -1,16 +1,10 @@
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.forms import ModelForm, FileField
from django.forms.models import modelformset_factory from django.forms.models import modelformset_factory
from django.urls import reverse from django.urls import reverse
from ckeditor.fields import RichTextField from aircox.forms import EpisodeForm
from filer.models.filemodels import File from aircox.models import Episode, Program, StaticPage, Track
from aircox.controllers.sound_file import SoundFile
from aircox.models import Track
from ..filters import EpisodeFilters from ..filters import EpisodeFilters
from ..models import Episode, Program, StaticPage
from .page import PageListView from .page import PageListView
from .program import ProgramPageDetailView, BaseProgramMixin from .program import ProgramPageDetailView, BaseProgramMixin
from .page import PageUpdateView from .page import PageUpdateView
@ -53,25 +47,6 @@ class PodcastListView(EpisodeListView):
queryset = Episode.objects.published().with_podcasts().order_by("-pub_date") queryset = Episode.objects.published().with_podcasts().order_by("-pub_date")
class EpisodeForm(ModelForm):
content = RichTextField()
new_podcast = FileField(required=False)
class Meta:
model = Episode
fields = ["content"]
def save(self, commit=True):
file_obj = self.cleaned_data["new_podcast"]
if file_obj:
obj, _ = File.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
sound_file = SoundFile(obj.path)
sound_file.sync(
program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True
)
super().save(commit=commit)
class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView): class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
model = Episode model = Episode
form_class = EpisodeForm form_class = EpisodeForm

View File

@ -16,6 +16,7 @@ __all__ = [
"BasePageDetailView", "BasePageDetailView",
"PageDetailView", "PageDetailView",
"PageListView", "PageListView",
"PageUpdateView",
] ]

View File

@ -1,15 +1,15 @@
from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin
from django.template.response import TemplateResponse from django.views.generic.base import TemplateView
from aircox.models import Program from aircox.models import Program
from aircox.views import BaseView
@login_required class ProfileView(LoginRequiredMixin, BaseView, TemplateView):
def profile(request): template_name = "accounts/profile.html"
programs = []
ugroups = request.user.groups.all() def get_context_data(self, **kwargs):
for p in Program.objects.all(): groups = self.request.user.groups.all()
if p.editors in ugroups: programs = Program.objects.filter(editors__in=groups)
programs.append(p) kwargs.update({"user": self.request.user, "programs": programs})
context = {"user": request.user, "programs": programs} return super().get_context_data(**kwargs)
return TemplateResponse(request, "accounts/profile.html", context)

View File

@ -1,14 +1,10 @@
import random import random
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.forms import ModelForm, ImageField
from django.urls import reverse from django.urls import reverse
from ckeditor.fields import RichTextField from aircox.forms import ProgramForm
from filer.models.imagemodels import Image from aircox.models import Article, Episode, Page, Program, StaticPage
from ..models import Article, Episode, Page, Program, StaticPage
from .mixins import ParentMixin from .mixins import ParentMixin
from .page import PageDetailView, PageListView, PageUpdateView from .page import PageDetailView, PageListView, PageUpdateView
@ -56,22 +52,6 @@ class ProgramDetailView(BaseProgramMixin, PageDetailView):
return super().get_template_names() + ["aircox/program_detail.html"] return super().get_template_names() + ["aircox/program_detail.html"]
class ProgramForm(ModelForm):
content = RichTextField()
new_cover = ImageField(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 ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView): class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
model = Program model = Program
form_class = ProgramForm form_class = ProgramForm