Compare commits

..

24 Commits

Author SHA1 Message Date
1aababe2ae docs: update user manual with simplified program management for animators 2024-02-06 09:57:21 +01:00
0dd961e0bb db: create program editors groups 2024-02-06 09:57:20 +01:00
f9da318a38 db: add missing migration on schedule timezone 2024-02-06 09:57:20 +01:00
26fa426416 views: avoid failing on missing parent cover 2024-02-06 09:40:45 +01:00
71f4d2473e episode-form: add tracks inline formset 2024-02-06 09:40:45 +01:00
2e9ebaded2 templatetags: display edit-links for admins 2024-02-06 09:40:45 +01:00
c6a4196319 templates: update after merging branch 118-design 2024-02-06 09:40:37 +01:00
be224d0efb templatetags: return on none type object 2024-02-05 10:29:58 +01:00
89f80ad103 templates: add in-context edition links 2024-02-05 10:29:58 +01:00
6d556fcd5d db: migrations merge 2024-02-05 10:29:55 +01:00
4201d50f4b templates: update container block names 2024-02-05 10:24:48 +01:00
6c942f36fa templatetags: avoid failing on nav_items when no station is defined 2024-02-05 10:24:48 +01:00
d51b9ee58b signals: disable schedule_pre_save when using loaddata 2024-02-05 10:24:48 +01:00
1a27ae2a76 misc: add in-site episode management for animators 2024-02-05 10:24:46 +01:00
e5862ee59b templates: set document type to html, prevent quicks mode 2024-02-05 10:22:16 +01:00
8f88b15536 ProgramUpdateView: use ckeditor RichTextField 2024-02-05 10:22:16 +01:00
10dfe3811b context_processors: prevent a null station error when no default station is defined 2024-02-05 10:22:16 +01:00
f71c201020 views/program: allow changing program cover 2024-02-05 10:22:16 +01:00
0812f3a0a1 misc: add a profile view for authenticated users 2024-02-05 10:22:14 +01:00
ad2ed17c34 misc: use the django authentication system 2024-02-05 10:19:05 +01:00
9db69580e0 misc: move station and audio_streams to context_processors (in order to have them available in accounts views) 2024-02-05 10:19:05 +01:00
4ead6b154b misc: edit programs in site 2024-02-05 10:19:05 +01:00
811cc97e07 templatetags: parametrize has_perm() in order to enable aircox namespace permissions 2024-02-05 10:19:05 +01:00
b794e24d0c models/program: link to editor groups 2024-02-05 10:19:05 +01:00
12 changed files with 87 additions and 81 deletions

View File

@ -1,12 +1,7 @@
from django import forms from django import forms
from django.forms import ModelForm, ImageField, FileField from django.forms import ModelForm
from ckeditor.fields import RichTextField from .models import Comment
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):
@ -21,38 +16,3 @@ 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)

View File

@ -125,7 +125,6 @@ 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,
@ -160,6 +159,7 @@ 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

@ -59,6 +59,8 @@ def do_get_tracks(obj):
@register.simple_tag(name="has_perm", takes_context=True) @register.simple_tag(name="has_perm", takes_context=True)
def do_has_perm(context, obj, perm, user=None, simple=False): def do_has_perm(context, obj, perm, user=None, simple=False):
"""Return True if ``user.has_perm('[APP].[perm]_[MODEL]')``""" """Return True if ``user.has_perm('[APP].[perm]_[MODEL]')``"""
if not obj:
return
if user is None: if user is None:
user = context["request"].user user = context["request"].user
if simple: if simple:
@ -102,6 +104,8 @@ def do_player_live_attr(context):
@register.simple_tag(name="nav_items", takes_context=True) @register.simple_tag(name="nav_items", takes_context=True)
def do_nav_items(context, menu, **kwargs): def do_nav_items(context, menu, **kwargs):
"""Render navigation items for the provided menu name.""" """Render navigation items for the provided menu name."""
if not getattr(context["request"], "station"):
return []
station, request = context["station"], context["request"] station, request = context["station"], context["request"]
return [(item, item.render(request, **kwargs)) for item in station.navitem_set.filter(menu=menu)] return [(item, item.render(request, **kwargs)) for item in station.navitem_set.filter(menu=menu)]

View File

@ -1,7 +1,6 @@
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
@ -163,10 +162,3 @@ 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()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

View File

@ -7,6 +7,12 @@ 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)
@ -23,7 +29,7 @@ def test_edit_program(user, client, program):
@pytest.mark.django_db() @pytest.mark.django_db()
def test_add_cover(user, client, program, png_content): def test_add_cover(user, client, program):
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)
@ -52,16 +58,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": ["{}"], "form-0-": ["", "", "", "", "", ""], "form-0-artist": ["{}"], "form-0-title": ["{}"], "form-0-id": ["%s"], "form-0-": ["", "", "", "", "", ""], "form-0-artist": ["%s"], "form-0-title": ["%s"],
"form-0-tags": [""], "form-0-album": [""], "form-0-year": [""], "form-1-position": ["1"], "form-1-id": ["{}"], "form-0-tags": [""], "form-0-album": [""], "form-0-year": [""], "form-1-position": ["1"], "form-1-id": ["%s"],
"form-1-": ["", "", "", "", "", ""], "form-1-artist": ["{}"], "form-1-title": ["{}"], "form-1-tags": [""], "form-1-": ["", "", "", "", "", ""], "form-1-artist": ["%s"], "form-1-title": ["%s"], "form-1-tags": [""],
"form-1-album": [""], "form-1-year": [""], "form-2-position": ["2"], "form-2-id": ["{}"], "form-2-": ["", "", "", "form-1-album": [""], "form-1-year": [""], "form-2-position": ["2"], "form-2-id": ["%s"], "form-2-": ["", "", "",
"", "", ""], "form-2-artist": ["{}"], "form-2-title": ["{}"], "form-2-tags": [""], "form-2-album": [""], "", "", ""], "form-2-artist": ["%s"], "form-2-title": ["%s"], "form-2-tags": [""], "form-2-album": [""],
"form-2-year": [""]}}""".format( "form-2-year": [""]}""" % tuple(
*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 set(episode.track_set.all().values_list("id", flat=True)) == set(tracklist) assert [t.id for t in episode.track_set.all().order_by("position")] == list(reversed(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.ProfileView.as_view(), name="profile"), path("gestion/", views.profile, name="profile"),
path("accounts/profile/", views.ProfileView.as_view(), name="profile"), path("accounts/profile/", views.profile, name="profile"),
] ]

View File

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

View File

@ -1,10 +1,16 @@
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 aircox.forms import EpisodeForm from ckeditor.fields import RichTextField
from aircox.models import Episode, Program, StaticPage, Track from filer.models.filemodels import File
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
@ -47,6 +53,25 @@ 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,7 +16,6 @@ __all__ = [
"BasePageDetailView", "BasePageDetailView",
"PageDetailView", "PageDetailView",
"PageListView", "PageListView",
"PageUpdateView",
] ]

View File

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

View File

@ -1,10 +1,14 @@
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 aircox.forms import ProgramForm from ckeditor.fields import RichTextField
from aircox.models import Article, Episode, Page, Program, StaticPage from filer.models.imagemodels import Image
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
@ -52,6 +56,22 @@ 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