Compare commits

...

4 Commits

11 changed files with 292 additions and 56 deletions

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
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):

View File

@ -1,67 +1,90 @@
{% extends "aircox/page_detail.html" %}
{% comment %}Detail page of a show{% endcomment %}
{% load i18n %}
{% extends "aircox/basepage_detail.html" %}
{% load static i18n humanize honeypot aircox %}
{% comment %}
Base template used to display a Page
{% include "aircox/program_sidebar.html" %}
Context:
- page: page
- parent: parent page
{% endcomment %}
{% block header_nav %}
{% block header_crumbs %}
{{ block.super }}
{% if page.category %}
{% if parent %} / {% endif %} {{ page.category.title }}
{% endif %}
{% endblock %}
{% block top-nav-tools %}
{% has_perm page page.change_permission_codename simple=True as can_edit %}
{% if can_edit %}
<a class="navbar-item" href="{% url 'program-edit' page.pk %}"
target="new">
<span class="icon is-small">
<i class="fa fa-pen"></i>
</span>&nbsp;
<span>{% translate "Edit" %}</span>
</a>
{% endif %}
{% endblock %}
{% block content %}
{% block main %}
{{ block.super }}
<br>
{% with has_headline=False %}
{% if articles %}
<section>
<h4 class="title is-4">{% translate "Articles" %}</h4>
{% for object in articles %}
{% include "aircox/widgets/page_item.html" %}
{% block comments %}
{% if comments or comment_form %}
<section class="mt-6">
<h4 class="title is-4">{% translate "Comments" %}</h4>
{% for comment in comments %}
<div class="media box">
<div class="media-content">
<p>
<strong class="mr-2">{{ comment.nickname }}</strong>
<time datetime="{{ comment.date }}" title="{{ comment.date }}">
<small>{{ comment.date|naturaltime }}</small>
</time>
<br>
{{ comment.content }}
</p>
</div>
</div>
{% endfor %}
<br>
<nav class="pagination is-centered">
<ul class="pagination-list">
<li>
<a href="{% url "article-list" parent_slug=program.slug %}"
class="pagination-link"
aria-label="{% translate "Show all program's articles" %}">
{% translate "More articles" %}
</a>
</li>
</ul>
</nav>
{% if comment_form %}
<form method="POST">
<h5 class="title is-5">{% translate "Post a comment" %}</h5>
{% csrf_token %}
{% render_honeypot_field "website" %}
{% for field in comment_form %}
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
{{ field.label_tag }}
</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">{{ field }}</p>
{% 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 %}
</div>
</div>
</div>
{% endfor %}
<div class="has-text-right">
<button type="reset" class="button is-danger">{% translate "Reset" %}</button>
<button type="submit" class="button is-success">{% translate "Post comment" %}</button>
</div>
</form>
{% endif %}
</section>
{% endif %}
{% endwith %}
{% endblock %}
{% block sidebar %}
<section>
<h4 class="title is-4">{% translate "Diffusions" %}</h4>
{% for schedule in program.schedule_set.all %}
{{ schedule.get_frequency_display }}
{% with schedule.start|date:"H:i" as start %}
{% with schedule.end|date:"H:i" as end %}
<time datetime="{{ start }}">{{ start }}</time>
&mdash;
<time datetime="{{ end }}">{{ end }}</time>
{% endwith %}
{% endwith %}
<small>
{% if schedule.initial %}
{% with schedule.initial.date as date %}
<span title="{% blocktranslate %}Rerun of {{ date }}{% endblocktranslate %}">
({% translate "Rerun" %})
</span>
{% endwith %}
{% endif %}
</small>
<br>
{% endfor %}
</section>
{{ block.super }}
{% endblock %}

View File

@ -0,0 +1,91 @@
{% extends "aircox/basepage_detail.html" %}
{% load static i18n humanize honeypot aircox %}
{% comment %}
Base template used to display a Page
Context:
- page: page
- parent: parent page
{% endcomment %}
{% block header_crumbs %}
{{ block.super }}
{% if page.category %}
{% if parent %} / {% endif %} {{ page.category.title }}
{% endif %}
{% endblock %}
{% block top-nav-tools %}
<a class="navbar-item" href="{% url 'program-detail' object.slug %}"
target="new">
<span class="icon is-small">
<i class="fa fa-eye"></i>
</span>&nbsp;
<span>{% translate "View" %}</span>
</a>
{% endblock %}
{% block main %}
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Update" class="button is-success">
</form>
{{ block.super }}
{% block comments %}
{% if comments or comment_form %}
<section class="mt-6">
<h4 class="title is-4">{% translate "Comments" %}</h4>
{% for comment in comments %}
<div class="media box">
<div class="media-content">
<p>
<strong class="mr-2">{{ comment.nickname }}</strong>
<time datetime="{{ comment.date }}" title="{{ comment.date }}">
<small>{{ comment.date|naturaltime }}</small>
</time>
<br>
{{ comment.content }}
</p>
</div>
</div>
{% endfor %}
{% if comment_form %}
<form method="POST">
<h5 class="title is-5">{% translate "Post a comment" %}</h5>
{% csrf_token %}
{% render_honeypot_field "website" %}
{% for field in comment_form %}
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
{{ field.label_tag }}
</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">{{ field }}</p>
{% 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 %}
</div>
</div>
</div>
{% endfor %}
<div class="has-text-right">
<button type="reset" class="button is-danger">{% translate "Reset" %}</button>
<button type="submit" class="button is-success">{% translate "Post comment" %}</button>
</div>
</form>
{% endif %}
</section>
{% endif %}
{% endblock %}
{% endblock %}

View File

@ -30,10 +30,13 @@ def do_get_tracks(obj):
@register.simple_tag(name="has_perm", takes_context=True)
def do_has_perm(context, obj, perm, user=None):
def do_has_perm(context, obj, perm, user=None, simple=False):
"""Return True if ``user.has_perm('[APP].[perm]_[MODEL]')``"""
if user is None:
user = context["request"].user
if simple:
return user.has_perm("aircox.{}".format(perm))
else:
return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name))

View File

@ -1,4 +1,6 @@
import pytest
from django.contrib.auth.models import User, Group
from django.urls import reverse
@pytest.mark.django_db()
@ -12,3 +14,23 @@ def test_no_admin(user, client):
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):
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={"pk": program.pk}))
assert response.status_code == 403
user.groups.add(program.editors)
response = client.get(reverse("program-edit", kwargs={"pk": program.pk}))
assert response.status_code == 200

View File

@ -0,0 +1,17 @@
import pytest
from django.urls import reverse
@pytest.mark.django_db()
def test_edit_program(user, client, program):
client.force_login(user)
response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
assert response.status_code == 200
assert b"fa-pen" not in response.content
user.groups.add(program.editors)
response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
assert b"fa-pen" in response.content
assert b"foobar" not in response.content
response = client.post(reverse("program-edit", kwargs={"pk": program.pk}), {"content": "foobar"})
response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
assert b"foobar" in response.content

View File

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

View File

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

View File

@ -1,6 +1,7 @@
from django.http import Http404, HttpResponse
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView
from django.views.generic.edit import UpdateView
from honeypot.decorators import check_honeypot
from ..filters import PageFilters
@ -138,3 +139,7 @@ class PageDetailView(BasePageDetailView):
comment.page = self.object
comment.save()
return self.get(request, *args, **kwargs)
class PageUpdateView(BaseView, UpdateView):
pass

View File

@ -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"]
@ -23,10 +25,25 @@ class BaseProgramMixin:
class ProgramDetailView(BaseProgramMixin, PageDetailView):
model = Program
def get_template_names(self):
return super().get_template_names() + ["aircox/program_detail.html"]
def get_sidebar_queryset(self):
return super().get_sidebar_queryset().filter(parent=self.program)
class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
model = Program
fields = ["content"]
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