Compare commits
	
		
			4 Commits
		
	
	
		
			3d8d7d8438
			...
			0f1ca3017e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0f1ca3017e | |||
| c4ed89329a | |||
| cb4f8d60f8 | |||
| 6c5746ecb2 | 
@ -86,8 +86,8 @@ class Settings(BaseSettings):
 | 
			
		||||
            # TODO include content_type in order to avoid clash with potential
 | 
			
		||||
            #      extra applications
 | 
			
		||||
            # aircox
 | 
			
		||||
            "change_program",
 | 
			
		||||
            "change_episode",
 | 
			
		||||
            "view_program",
 | 
			
		||||
            "view_episode",
 | 
			
		||||
            "change_diffusion",
 | 
			
		||||
            "add_comment",
 | 
			
		||||
            "change_comment",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										25
									
								
								aircox/migrations/0015_program_editors.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								aircox/migrations/0015_program_editors.py
									
									
									
									
									
										Normal 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",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -3,6 +3,8 @@ import os
 | 
			
		||||
import shutil
 | 
			
		||||
 | 
			
		||||
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.models import F
 | 
			
		||||
from django.db.models.functions import Concat, Substr
 | 
			
		||||
@ -58,6 +60,7 @@ class Program(Page):
 | 
			
		||||
        default=True,
 | 
			
		||||
        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()
 | 
			
		||||
    detail_url_name = "program-detail"
 | 
			
		||||
@ -80,6 +83,14 @@ class Program(Page):
 | 
			
		||||
    def excerpts_path(self):
 | 
			
		||||
        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):
 | 
			
		||||
        super().__init__(*kargs, **kwargs)
 | 
			
		||||
        if self.slug:
 | 
			
		||||
@ -109,6 +120,18 @@ class Program(Page):
 | 
			
		||||
        os.makedirs(path, exist_ok=True)
 | 
			
		||||
        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:
 | 
			
		||||
        verbose_name = _("Program")
 | 
			
		||||
        verbose_name_plural = _("Programs")
 | 
			
		||||
@ -134,6 +157,9 @@ class Program(Page):
 | 
			
		||||
            shutil.move(abspath, self.abspath)
 | 
			
		||||
            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):
 | 
			
		||||
    def station(self, station=None, id=None):
 | 
			
		||||
 | 
			
		||||
@ -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)]
 | 
			
		||||
    models.Track.objects.bulk_create(items)
 | 
			
		||||
    return items
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def user():
 | 
			
		||||
    return User.objects.create_user(username="user1", password="bar")
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										37
									
								
								aircox/tests/test_permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								aircox/tests/test_permissions.py
									
									
									
									
									
										Normal 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
 | 
			
		||||
@ -92,6 +92,11 @@ urls = [
 | 
			
		||||
        views.ProgramDetailView.as_view(),
 | 
			
		||||
        name="program-detail",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        _("program/<slug:slug>/edit/"),
 | 
			
		||||
        views.ProgramUpdateView.as_view(),
 | 
			
		||||
        name="program-edit",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        _("programs/<slug:parent_slug>/episodes/"),
 | 
			
		||||
        views.EpisodeListView.as_view(),
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ from .program import (
 | 
			
		||||
    ProgramListView,
 | 
			
		||||
    ProgramPageDetailView,
 | 
			
		||||
    ProgramPageListView,
 | 
			
		||||
    ProgramUpdateView,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
@ -39,4 +40,5 @@ __all__ = (
 | 
			
		||||
    "ProgramListView",
 | 
			
		||||
    "ProgramPageDetailView",
 | 
			
		||||
    "ProgramPageListView",
 | 
			
		||||
    "ProgramUpdateView",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -138,3 +138,7 @@ class PageDetailView(BasePageDetailView):
 | 
			
		||||
        comment.page = self.object
 | 
			
		||||
        comment.save()
 | 
			
		||||
        return self.get(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PageUpdateView(PageDetailView):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,10 @@
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.mixins import UserPassesTestMixin
 | 
			
		||||
 | 
			
		||||
from ..models import Page, Program, StaticPage
 | 
			
		||||
from .mixins import ParentMixin
 | 
			
		||||
from .page import PageDetailView, PageListView
 | 
			
		||||
from .page import PageDetailView, PageListView, PageUpdateView
 | 
			
		||||
 | 
			
		||||
__all__ = ["ProgramPageDetailView", "ProgramDetailView", "ProgramPageListView"]
 | 
			
		||||
 | 
			
		||||
@ -27,6 +29,17 @@ class ProgramDetailView(BaseProgramMixin, PageDetailView):
 | 
			
		||||
        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):
 | 
			
		||||
    model = Program
 | 
			
		||||
    attach_to_value = StaticPage.ATTACH_TO_PROGRAMS
 | 
			
		||||
 | 
			
		||||
@ -40,3 +40,9 @@ LOGGING = {
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CACHES = {
 | 
			
		||||
    "default": {
 | 
			
		||||
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user