Compare commits

...

4 Commits

10 changed files with 126 additions and 3 deletions

View File

@ -86,8 +86,8 @@ class Settings(BaseSettings):
# TODO include content_type in order to avoid clash with potential # TODO include content_type in order to avoid clash with potential
# extra applications # extra applications
# aircox # aircox
"change_program", "view_program",
"change_episode", "view_episode",
"change_diffusion", "change_diffusion",
"add_comment", "add_comment",
"change_comment", "change_comment",

View File

@ -0,0 +1,25 @@
# Generated by Django 4.2.5 on 2023-10-18 13:50
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("aircox", "0014_alter_schedule_timezone"),
]
operations = [
migrations.AddField(
model_name="program",
name="editors",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="auth.group",
verbose_name="editors",
),
),
]

View File

@ -3,6 +3,8 @@ import os
import shutil import shutil
from django.conf import settings as conf from django.conf import settings as conf
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.db.models import F from django.db.models import F
from django.db.models.functions import Concat, Substr from django.db.models.functions import Concat, Substr
@ -58,6 +60,7 @@ class Program(Page):
default=True, default=True,
help_text=_("update later diffusions according to schedule changes"), help_text=_("update later diffusions according to schedule changes"),
) )
editors = models.ForeignKey(Group, models.CASCADE, blank=True, null=True, verbose_name=_("editors"))
objects = ProgramQuerySet.as_manager() objects = ProgramQuerySet.as_manager()
detail_url_name = "program-detail" detail_url_name = "program-detail"
@ -80,6 +83,14 @@ class Program(Page):
def excerpts_path(self): def excerpts_path(self):
return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR) return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
@property
def editors_group_name(self):
return "{self.title} editors"
@property
def change_permission_codename(self):
return f"change_program_{self.slug}"
def __init__(self, *kargs, **kwargs): def __init__(self, *kargs, **kwargs):
super().__init__(*kargs, **kwargs) super().__init__(*kargs, **kwargs)
if self.slug: if self.slug:
@ -109,6 +120,18 @@ class Program(Page):
os.makedirs(path, exist_ok=True) os.makedirs(path, exist_ok=True)
return os.path.exists(path) return os.path.exists(path)
def set_group_ownership(self):
editors, created = Group.objects.get_or_create(name=self.editors_group_name)
if created:
self.editors = editors
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),
)
if permission not in editors.permissions.all():
editors.permissions.add(permission)
class Meta: class Meta:
verbose_name = _("Program") verbose_name = _("Program")
verbose_name_plural = _("Programs") verbose_name_plural = _("Programs")
@ -134,6 +157,9 @@ class Program(Page):
shutil.move(abspath, self.abspath) shutil.move(abspath, self.abspath)
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()
super().save(*kargs, **kwargs)
class ProgramChildQuerySet(PageQuerySet): class ProgramChildQuerySet(PageQuerySet):
def station(self, station=None, id=None): def station(self, station=None, id=None):

View File

@ -157,3 +157,8 @@ def tracks(episode, sound):
items += [baker.prepare(models.Track, sound=sound, position=i, timestamp=i * 60) for i in range(0, 3)] items += [baker.prepare(models.Track, sound=sound, position=i, timestamp=i * 60) for i in range(0, 3)]
models.Track.objects.bulk_create(items) models.Track.objects.bulk_create(items)
return items return items
@pytest.fixture
def user():
return User.objects.create_user(username="user1", password="bar")

View File

@ -0,0 +1,37 @@
import pytest
from django.contrib.auth.models import User, Group
from django.urls import reverse
@pytest.mark.django_db()
def test_no_admin(user, client):
client.force_login(user)
response = client.get("/admin/")
assert response.status_code != 200
@pytest.mark.django_db()
def test_user_cannot_change_program_or_episode(user, client, program):
assert not user.has_perm("aircox.change_program")
assert not user.has_perm("aircox.change_episode")
@pytest.mark.django_db()
def test_group_can_change_program(user, client, program):
program_editors = program.editors
assert program_editors in Group.objects.all()
assert not user.has_perm("aircox.%s" % program.change_permission_codename)
user.groups.add(program_editors)
user = User.objects.get(pk=user.pk) # reload user in order to have permissions set
assert program_editors in user.groups.all()
assert user.has_perm("aircox.%s" % program.change_permission_codename)
@pytest.mark.django_db()
def test_group_change_program(user, client, program):
client.force_login(user)
response = client.get(reverse("program-edit", kwargs={"slug": program.slug}))
assert response.status_code == 403
user.groups.add(program.editors)
response = client.get(reverse("program-edit", kwargs={"slug": program.slug}))
assert response.status_code == 200

View File

@ -92,6 +92,11 @@ urls = [
views.ProgramDetailView.as_view(), views.ProgramDetailView.as_view(),
name="program-detail", name="program-detail",
), ),
path(
_("program/<slug:slug>/edit/"),
views.ProgramUpdateView.as_view(),
name="program-edit",
),
path( path(
_("programs/<slug:parent_slug>/episodes/"), _("programs/<slug:parent_slug>/episodes/"),
views.EpisodeListView.as_view(), views.EpisodeListView.as_view(),

View File

@ -16,6 +16,7 @@ from .program import (
ProgramListView, ProgramListView,
ProgramPageDetailView, ProgramPageDetailView,
ProgramPageListView, ProgramPageListView,
ProgramUpdateView,
) )
__all__ = ( __all__ = (
@ -39,4 +40,5 @@ __all__ = (
"ProgramListView", "ProgramListView",
"ProgramPageDetailView", "ProgramPageDetailView",
"ProgramPageListView", "ProgramPageListView",
"ProgramUpdateView",
) )

View File

@ -138,3 +138,7 @@ class PageDetailView(BasePageDetailView):
comment.page = self.object comment.page = self.object
comment.save() comment.save()
return self.get(request, *args, **kwargs) return self.get(request, *args, **kwargs)
class PageUpdateView(PageDetailView):
pass

View File

@ -1,8 +1,10 @@
from django.urls import reverse from django.urls import reverse
from django.contrib.auth.mixins import UserPassesTestMixin
from ..models import Page, Program, StaticPage from ..models import Page, Program, StaticPage
from .mixins import ParentMixin from .mixins import ParentMixin
from .page import PageDetailView, PageListView from .page import PageDetailView, PageListView, PageUpdateView
__all__ = ["ProgramPageDetailView", "ProgramDetailView", "ProgramPageListView"] __all__ = ["ProgramPageDetailView", "ProgramDetailView", "ProgramPageListView"]
@ -27,6 +29,17 @@ class ProgramDetailView(BaseProgramMixin, PageDetailView):
return super().get_sidebar_queryset().filter(parent=self.program) return super().get_sidebar_queryset().filter(parent=self.program)
class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
model = Program
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)
class ProgramListView(PageListView): class ProgramListView(PageListView):
model = Program model = Program
attach_to_value = StaticPage.ATTACH_TO_PROGRAMS attach_to_value = StaticPage.ATTACH_TO_PROGRAMS

View File

@ -40,3 +40,9 @@ LOGGING = {
}, },
}, },
} }
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
}
}