From 92f9a08856018fbb6fb90cc2cbf774627124ee4d Mon Sep 17 00:00:00 2001 From: Christophe Siraut Date: Wed, 18 Oct 2023 15:43:17 +0200 Subject: [PATCH 1/9] aircox/conf: user cannot edit all programs/episode --- aircox/conf.py | 4 ++-- aircox/tests/conftest.py | 5 +++++ aircox/tests/test_permissions.py | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 aircox/tests/test_permissions.py diff --git a/aircox/conf.py b/aircox/conf.py index c54f8be..c66bd8d 100755 --- a/aircox/conf.py +++ b/aircox/conf.py @@ -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", diff --git a/aircox/tests/conftest.py b/aircox/tests/conftest.py index caf5564..a02a67f 100644 --- a/aircox/tests/conftest.py +++ b/aircox/tests/conftest.py @@ -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") diff --git a/aircox/tests/test_permissions.py b/aircox/tests/test_permissions.py new file mode 100644 index 0000000..54bf67a --- /dev/null +++ b/aircox/tests/test_permissions.py @@ -0,0 +1,14 @@ +import pytest + + +@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") -- 2.30.2 From b0afa0fd86faec0bd44cbc429b9ef74ee0afd5e5 Mon Sep 17 00:00:00 2001 From: Christophe Siraut Date: Wed, 18 Oct 2023 15:44:47 +0200 Subject: [PATCH 2/9] models/program: link to editor groups --- aircox/migrations/0015_program_editors.py | 25 ++++++++++++++++++++++ aircox/models/program.py | 26 +++++++++++++++++++++++ aircox/tests/test_permissions.py | 11 ++++++++++ 3 files changed, 62 insertions(+) create mode 100644 aircox/migrations/0015_program_editors.py diff --git a/aircox/migrations/0015_program_editors.py b/aircox/migrations/0015_program_editors.py new file mode 100644 index 0000000..9a3964f --- /dev/null +++ b/aircox/migrations/0015_program_editors.py @@ -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", + ), + ), + ] diff --git a/aircox/models/program.py b/aircox/models/program.py index 7a4fd16..7a6e6f3 100644 --- a/aircox/models/program.py +++ b/aircox/models/program.py @@ -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 f"{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): diff --git a/aircox/tests/test_permissions.py b/aircox/tests/test_permissions.py index 54bf67a..25866b3 100644 --- a/aircox/tests/test_permissions.py +++ b/aircox/tests/test_permissions.py @@ -1,4 +1,5 @@ import pytest +from django.contrib.auth.models import User, Group @pytest.mark.django_db() @@ -12,3 +13,13 @@ 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) -- 2.30.2 From a1d6f0ef4a37f6e445b8473998da6cb3e7ee6134 Mon Sep 17 00:00:00 2001 From: Christophe Siraut Date: Fri, 10 Nov 2023 08:28:23 +0100 Subject: [PATCH 3/9] templatetags: parametrize has_perm() in order to enable aircox namespace permissions --- aircox/templatetags/aircox.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aircox/templatetags/aircox.py b/aircox/templatetags/aircox.py index 285d451..81f2842 100644 --- a/aircox/templatetags/aircox.py +++ b/aircox/templatetags/aircox.py @@ -30,11 +30,14 @@ 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 - return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name)) + if simple: + return user.has_perm("aircox.{}".format(perm)) + else: + return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name)) @register.filter(name="is_diffusion") -- 2.30.2 From 0eeeb3bc0919e657720bbe85832834134bb39dd9 Mon Sep 17 00:00:00 2001 From: Christophe Siraut Date: Fri, 10 Nov 2023 11:23:52 +0100 Subject: [PATCH 4/9] templates: remove unused program_detail.html --- aircox/templates/aircox/program_detail.html | 67 --------------------- 1 file changed, 67 deletions(-) delete mode 100644 aircox/templates/aircox/program_detail.html diff --git a/aircox/templates/aircox/program_detail.html b/aircox/templates/aircox/program_detail.html deleted file mode 100644 index 267067b..0000000 --- a/aircox/templates/aircox/program_detail.html +++ /dev/null @@ -1,67 +0,0 @@ -{% extends "aircox/page_detail.html" %} -{% comment %}Detail page of a show{% endcomment %} -{% load i18n %} - -{% include "aircox/program_sidebar.html" %} - - -{% block header_nav %} -{% endblock %} - - -{% block content %} -{{ block.super }} -
-{% with has_headline=False %} -{% if articles %} -
-

{% translate "Articles" %}

- - {% for object in articles %} - {% include "aircox/widgets/page_item.html" %} - {% endfor %} - -
- -
-{% endif %} -{% endwith %} - -{% endblock %} - -{% block sidebar %} -
-

{% translate "Diffusions" %}

- {% 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 %} - - — - - {% endwith %} - {% endwith %} - - {% if schedule.initial %} - {% with schedule.initial.date as date %} - - ({% translate "Rerun" %}) - - {% endwith %} - {% endif %} - -
- {% endfor %} -
-{{ block.super }} -{% endblock %} -- 2.30.2 From a89117f69de3ea42e37264354b634542223b8ad9 Mon Sep 17 00:00:00 2001 From: Christophe Siraut Date: Fri, 10 Nov 2023 11:27:30 +0100 Subject: [PATCH 5/9] misc: edit programs in site --- aircox/templates/aircox/program_detail.html | 90 ++++++++++++++++++++ aircox/templates/aircox/program_form.html | 91 +++++++++++++++++++++ aircox/tests/test_permissions.py | 11 +++ aircox/tests/test_program.py | 17 ++++ aircox/urls.py | 5 ++ aircox/views/__init__.py | 2 + aircox/views/page.py | 5 ++ aircox/views/program.py | 19 ++++- 8 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 aircox/templates/aircox/program_detail.html create mode 100644 aircox/templates/aircox/program_form.html create mode 100644 aircox/tests/test_program.py diff --git a/aircox/templates/aircox/program_detail.html b/aircox/templates/aircox/program_detail.html new file mode 100644 index 0000000..fb2f154 --- /dev/null +++ b/aircox/templates/aircox/program_detail.html @@ -0,0 +1,90 @@ +{% 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 %} +{% has_perm page page.change_permission_codename simple=True as can_edit %} +{% if can_edit %} + + + +   + {% translate "Edit" %} + +{% endif %} +{% endblock %} + +{% block main %} +{{ block.super }} + +{% block comments %} +{% if comments or comment_form %} +
+

{% translate "Comments" %}

+ + {% for comment in comments %} +
+
+

+ {{ comment.nickname }} + +
+ {{ comment.content }} +

+
+
+ {% endfor %} + + {% if comment_form %} +
+
{% translate "Post a comment" %}
+ {% csrf_token %} + {% render_honeypot_field "website" %} + + {% for field in comment_form %} +
+
+ +
+
+
+

{{ field }}

+ {% if field.errors %} +

{{ field.errors }}

+ {% endif %} + {% if field.help_text %} +

{{ field.help_text|safe }}

+ {% endif %} +
+
+
+ {% endfor %} +
+ + +
+
+ {% endif %} +
+{% endif %} + +{% endblock %} +{% endblock %} diff --git a/aircox/templates/aircox/program_form.html b/aircox/templates/aircox/program_form.html new file mode 100644 index 0000000..b14fc3f --- /dev/null +++ b/aircox/templates/aircox/program_form.html @@ -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 %} + + + +   + {% translate "View" %} + +{% endblock %} + +{% block main %} +
{% csrf_token %} + {{ form.as_p }} + +
+{{ block.super }} + +{% block comments %} +{% if comments or comment_form %} +
+

{% translate "Comments" %}

+ + {% for comment in comments %} +
+
+

+ {{ comment.nickname }} + +
+ {{ comment.content }} +

+
+
+ {% endfor %} + + {% if comment_form %} +
+
{% translate "Post a comment" %}
+ {% csrf_token %} + {% render_honeypot_field "website" %} + + {% for field in comment_form %} +
+
+ +
+
+
+

{{ field }}

+ {% if field.errors %} +

{{ field.errors }}

+ {% endif %} + {% if field.help_text %} +

{{ field.help_text|safe }}

+ {% endif %} +
+
+
+ {% endfor %} +
+ + +
+
+ {% endif %} +
+{% endif %} + +{% endblock %} +{% endblock %} diff --git a/aircox/tests/test_permissions.py b/aircox/tests/test_permissions.py index 25866b3..a561f33 100644 --- a/aircox/tests/test_permissions.py +++ b/aircox/tests/test_permissions.py @@ -1,5 +1,6 @@ import pytest from django.contrib.auth.models import User, Group +from django.urls import reverse @pytest.mark.django_db() @@ -23,3 +24,13 @@ def test_group_can_change_program(user, client, program): 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 diff --git a/aircox/tests/test_program.py b/aircox/tests/test_program.py new file mode 100644 index 0000000..89a575e --- /dev/null +++ b/aircox/tests/test_program.py @@ -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 diff --git a/aircox/urls.py b/aircox/urls.py index 0ad7df6..8857297 100755 --- a/aircox/urls.py +++ b/aircox/urls.py @@ -92,6 +92,11 @@ urls = [ views.ProgramDetailView.as_view(), name="program-detail", ), + path( + _("program//edit/"), + views.ProgramUpdateView.as_view(), + name="program-edit", + ), path( _("programs//episodes/"), views.EpisodeListView.as_view(), diff --git a/aircox/views/__init__.py b/aircox/views/__init__.py index e4d9d4a..48e1191 100644 --- a/aircox/views/__init__.py +++ b/aircox/views/__init__.py @@ -16,6 +16,7 @@ from .program import ( ProgramListView, ProgramPageDetailView, ProgramPageListView, + ProgramUpdateView, ) __all__ = ( @@ -39,4 +40,5 @@ __all__ = ( "ProgramListView", "ProgramPageDetailView", "ProgramPageListView", + "ProgramUpdateView", ) diff --git a/aircox/views/page.py b/aircox/views/page.py index 7a0ef42..90ead86 100644 --- a/aircox/views/page.py +++ b/aircox/views/page.py @@ -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 diff --git a/aircox/views/program.py b/aircox/views/program.py index fb36d7d..2b2c9fe 100644 --- a/aircox/views/program.py +++ b/aircox/views/program.py @@ -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 -- 2.30.2 From d63d94909635ddb13ea07377472b5ab6701107b2 Mon Sep 17 00:00:00 2001 From: Christophe Siraut Date: Mon, 13 Nov 2023 13:41:16 +0100 Subject: [PATCH 6/9] misc: move station and audio_streams to context_processors (in order to have them available in accounts views) --- aircox/context_processors/__init__.py | 3 +++ aircox/tests/views/test_base.py | 2 -- aircox/views/base.py | 4 ---- instance/settings/base.py | 1 + 4 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 aircox/context_processors/__init__.py diff --git a/aircox/context_processors/__init__.py b/aircox/context_processors/__init__.py new file mode 100644 index 0000000..e79119a --- /dev/null +++ b/aircox/context_processors/__init__.py @@ -0,0 +1,3 @@ +def station(request): + station = request.station + return {"station": station, "audio_streams": station.streams} diff --git a/aircox/tests/views/test_base.py b/aircox/tests/views/test_base.py index 4be01a7..9c3c013 100644 --- a/aircox/tests/views/test_base.py +++ b/aircox/tests/views/test_base.py @@ -54,13 +54,11 @@ class TestBaseView: context = base_view.get_context_data() assert context == { "view": base_view, - "station": station, "page": None, # get_page() returns None "has_sidebar": base_view.has_sidebar, "has_filters": False, "sidebar_object_list": published_pages[: base_view.list_count], "sidebar_list_url": base_view.get_sidebar_url(), - "audio_streams": station.streams, "model": base_view.model, } diff --git a/aircox/views/base.py b/aircox/views/base.py index 6e6d597..c971dcc 100644 --- a/aircox/views/base.py +++ b/aircox/views/base.py @@ -33,7 +33,6 @@ class BaseView(TemplateResponseMixin, ContextMixin): return None def get_context_data(self, **kwargs): - kwargs.setdefault("station", self.station) kwargs.setdefault("page", self.get_page()) kwargs.setdefault("has_filters", self.has_filters) @@ -44,9 +43,6 @@ class BaseView(TemplateResponseMixin, ContextMixin): kwargs["sidebar_object_list"] = sidebar_object_list[: self.list_count] kwargs["sidebar_list_url"] = self.get_sidebar_url() - if "audio_streams" not in kwargs: - kwargs["audio_streams"] = self.station.streams - if "model" not in kwargs: model = getattr(self, "model", None) or hasattr(self, "object") and type(self.object) kwargs["model"] = model diff --git a/instance/settings/base.py b/instance/settings/base.py index d18dbd6..5881359 100755 --- a/instance/settings/base.py +++ b/instance/settings/base.py @@ -237,6 +237,7 @@ TEMPLATES = [ "django.template.context_processors.static", "django.template.context_processors.tz", "django.contrib.messages.context_processors.messages", + "aircox.context_processors.station", ), "loaders": ( "django.template.loaders.filesystem.Loader", -- 2.30.2 From 291949e6e844843dc36068f3ba93a23da2a36704 Mon Sep 17 00:00:00 2001 From: Christophe Siraut Date: Mon, 20 Nov 2023 12:24:23 +0100 Subject: [PATCH 7/9] misc: use the django authentication system --- aircox/templates/aircox/base.html | 7 +++++++ aircox/templates/registration/login.html | 20 ++++++++++++++++++++ aircox/tests/test_profile.py | 12 ++++++++++++ instance/settings/base.py | 2 ++ instance/urls.py | 1 + 5 files changed, 42 insertions(+) create mode 100644 aircox/templates/registration/login.html create mode 100644 aircox/tests/test_profile.py diff --git a/aircox/templates/aircox/base.html b/aircox/templates/aircox/base.html index 097b4c9..b72f4fd 100644 --- a/aircox/templates/aircox/base.html +++ b/aircox/templates/aircox/base.html @@ -68,6 +68,7 @@ Usefull context: diff --git a/aircox/templates/registration/login.html b/aircox/templates/registration/login.html new file mode 100644 index 0000000..179b47a --- /dev/null +++ b/aircox/templates/registration/login.html @@ -0,0 +1,20 @@ +{% extends "aircox/base.html" %} +{% load i18n aircox %} + +{% block main %} + +

{% trans "Log in" %}

+
+
+ {% csrf_token %} + + {{ form.as_table }} +
+
+ + +
+ +{{ block.super }} + +{% endblock %} diff --git a/aircox/tests/test_profile.py b/aircox/tests/test_profile.py new file mode 100644 index 0000000..90ace45 --- /dev/null +++ b/aircox/tests/test_profile.py @@ -0,0 +1,12 @@ +import pytest +from django.urls import reverse + + +@pytest.mark.django_db() +def test_authenticate(user, client, program): + r = client.get(reverse("login")) + assert r.status_code == 200 + assert b"id_username" in r.content + r = client.post(reverse("login"), kwargs={"username": "foo", "password": "bar"}) + assert b"errorlist" in r.content + assert client.login(username="user1", password="bar") diff --git a/instance/settings/base.py b/instance/settings/base.py index 5881359..850e831 100755 --- a/instance/settings/base.py +++ b/instance/settings/base.py @@ -249,3 +249,5 @@ TEMPLATES = [ WSGI_APPLICATION = "instance.wsgi.application" + +LOGOUT_REDIRECT_URL = "/" diff --git a/instance/urls.py b/instance/urls.py index af36db2..1acda1b 100755 --- a/instance/urls.py +++ b/instance/urls.py @@ -23,6 +23,7 @@ import aircox.urls urlpatterns = aircox.urls.urls + [ path("admin/", admin.site.urls), + path("accounts/", include("django.contrib.auth.urls")), path("ckeditor/", include("ckeditor_uploader.urls")), path("filer/", include("filer.urls")), ] -- 2.30.2 From 9e952735b8c9f2a3b7e1d29f714482d18abb8037 Mon Sep 17 00:00:00 2001 From: Christophe Siraut Date: Mon, 20 Nov 2023 14:19:44 +0100 Subject: [PATCH 8/9] misc: add a profile view for authenticated users --- aircox/templates/accounts/profile.html | 15 +++++++++++++++ aircox/templates/aircox/base.html | 2 +- aircox/tests/test_profile.py | 10 ++++++++++ aircox/urls.py | 2 ++ aircox/views/__init__.py | 2 ++ aircox/views/profile.py | 15 +++++++++++++++ 6 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 aircox/templates/accounts/profile.html create mode 100644 aircox/views/profile.py diff --git a/aircox/templates/accounts/profile.html b/aircox/templates/accounts/profile.html new file mode 100644 index 0000000..127f463 --- /dev/null +++ b/aircox/templates/accounts/profile.html @@ -0,0 +1,15 @@ +{% extends "aircox/base.html" %} +{% load i18n aircox %} + +{% block head_title %} + {% block title %}{{ user.username }}{% endblock %} +{% endblock %} + +{% block main %} +

Mes émissions

+ +{% endblock %} diff --git a/aircox/templates/aircox/base.html b/aircox/templates/aircox/base.html index b72f4fd..0306d2a 100644 --- a/aircox/templates/aircox/base.html +++ b/aircox/templates/aircox/base.html @@ -85,7 +85,7 @@ Usefull context: {% if user.is_authenticated %} {% endif %} diff --git a/aircox/tests/test_profile.py b/aircox/tests/test_profile.py index 90ace45..11129c7 100644 --- a/aircox/tests/test_profile.py +++ b/aircox/tests/test_profile.py @@ -10,3 +10,13 @@ def test_authenticate(user, client, program): r = client.post(reverse("login"), kwargs={"username": "foo", "password": "bar"}) assert b"errorlist" in r.content assert client.login(username="user1", password="bar") + + +@pytest.mark.django_db() +def test_profile_programs(user, client, program): + client.force_login(user) + r = client.get(reverse("profile")) + assert program.title not in r.content.decode("utf-8") + user.groups.add(program.editors) + r = client.get(reverse("profile")) + assert program.title in r.content.decode("utf-8") diff --git a/aircox/urls.py b/aircox/urls.py index 8857297..3c6741c 100755 --- a/aircox/urls.py +++ b/aircox/urls.py @@ -117,4 +117,6 @@ urls = [ views.errors.NoStationErrorView.as_view(), name="errors-no-station", ), + path("gestion/", views.profile, name="profile"), + path("accounts/profile/", views.profile, name="profile"), ] diff --git a/aircox/views/__init__.py b/aircox/views/__init__.py index 48e1191..2d30211 100644 --- a/aircox/views/__init__.py +++ b/aircox/views/__init__.py @@ -11,6 +11,7 @@ from .page import ( PageDetailView, PageListView, ) +from .profile import profile from .program import ( ProgramDetailView, ProgramListView, @@ -36,6 +37,7 @@ __all__ = ( "BasePageListView", "PageDetailView", "PageListView", + "profile", "ProgramDetailView", "ProgramListView", "ProgramPageDetailView", diff --git a/aircox/views/profile.py b/aircox/views/profile.py new file mode 100644 index 0000000..4e002fa --- /dev/null +++ b/aircox/views/profile.py @@ -0,0 +1,15 @@ +from django.contrib.auth.decorators import login_required +from django.template.response import TemplateResponse + +from aircox.models import Program + + +@login_required +def profile(request): + programs = [] + ugroups = request.user.groups.all() + for p in Program.objects.all(): + if p.editors in ugroups: + programs.append(p) + context = {"user": request.user, "programs": programs} + return TemplateResponse(request, "accounts/profile.html", context) -- 2.30.2 From 1d9dc4628a4fe2a9437db02cf4009cfbd98ef36c Mon Sep 17 00:00:00 2001 From: Christophe Siraut Date: Tue, 21 Nov 2023 15:28:59 +0100 Subject: [PATCH 9/9] views/program: allow changing program cover --- aircox/templates/aircox/program_form.html | 79 ++--------------------- aircox/tests/test_program.py | 23 +++++++ aircox/views/page.py | 5 +- aircox/views/program.py | 24 ++++++- 4 files changed, 55 insertions(+), 76 deletions(-) diff --git a/aircox/templates/aircox/program_form.html b/aircox/templates/aircox/program_form.html index b14fc3f..b9417b1 100644 --- a/aircox/templates/aircox/program_form.html +++ b/aircox/templates/aircox/program_form.html @@ -1,19 +1,6 @@ {% 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 %} {% csrf_token %} - {{ form.as_p }} - - -{{ block.super }} - -{% block comments %} -{% if comments or comment_form %} -
-

{% translate "Comments" %}

- - {% for comment in comments %} -
-
-

- {{ comment.nickname }} - -
- {{ comment.content }} -

-
-
- {% endfor %} - - {% if comment_form %} -
-
{% translate "Post a comment" %}
- {% csrf_token %} + + {{ form.as_table }} {% render_honeypot_field "website" %} - - {% for field in comment_form %} -
-
- -
-
-
-

{{ field }}

- {% if field.errors %} -

{{ field.errors }}

- {% endif %} - {% if field.help_text %} -

{{ field.help_text|safe }}

- {% endif %} -
-
-
- {% endfor %} -
- - -
- - {% endif %} - -{% endif %} - -{% endblock %} +
+
+ + {% endblock %} diff --git a/aircox/tests/test_program.py b/aircox/tests/test_program.py index 89a575e..4fc94e9 100644 --- a/aircox/tests/test_program.py +++ b/aircox/tests/test_program.py @@ -1,5 +1,14 @@ import pytest from django.urls import reverse +from django.core.files.uploadedfile import SimpleUploadedFile + +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() @@ -15,3 +24,17 @@ def test_edit_program(user, client, program): 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 + + +@pytest.mark.django_db() +def test_add_cover(user, client, program): + assert program.cover is None + user.groups.add(program.editors) + client.force_login(user) + cover = SimpleUploadedFile("cover1.png", png_content, content_type="image/png") + r = client.post( + reverse("program-edit", kwargs={"pk": program.pk}), {"content": "foobar", "new_cover": cover}, follow=True + ) + assert r.status_code == 200 + p = Program.objects.get(pk=program.pk) + assert "cover1.png" in p.cover.url diff --git a/aircox/views/page.py b/aircox/views/page.py index 90ead86..6f43d40 100644 --- a/aircox/views/page.py +++ b/aircox/views/page.py @@ -142,4 +142,7 @@ class PageDetailView(BasePageDetailView): class PageUpdateView(BaseView, UpdateView): - pass + context_object_name = "page" + + def get_page(self): + return self.object diff --git a/aircox/views/program.py b/aircox/views/program.py index 2b2c9fe..56e27cf 100644 --- a/aircox/views/program.py +++ b/aircox/views/program.py @@ -1,6 +1,8 @@ +from django.contrib.auth.mixins import UserPassesTestMixin +from django.forms import ModelForm, ImageField from django.urls import reverse -from django.contrib.auth.mixins import UserPassesTestMixin +from filer.models.imagemodels import Image from ..models import Page, Program, StaticPage from .mixins import ParentMixin @@ -32,9 +34,24 @@ class ProgramDetailView(BaseProgramMixin, PageDetailView): return super().get_sidebar_queryset().filter(parent=self.program) +class ProgramForm(ModelForm): + 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): model = Program - fields = ["content"] + form_class = ProgramForm def get_sidebar_queryset(self): return super().get_sidebar_queryset().filter(parent=self.program) @@ -43,6 +60,9 @@ class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView): program = self.get_object() return self.request.user.has_perm("aircox.%s" % program.change_permission_codename) + def get_success_url(self): + return reverse("program-detail", kwargs={"slug": self.get_object().slug}) + class ProgramListView(PageListView): model = Program -- 2.30.2