Compare commits

..

3 Commits

Author SHA1 Message Date
Sli
917a2b50cc Fix naming, fix tooltip and cosmetic changes 2025-10-31 21:51:12 +01:00
imperosol
118a08372f simplify poster moderation 2025-10-31 17:16:52 +01:00
Sli
b8429a510f posters: fix broken moderation view 2025-10-31 12:15:44 +01:00
9 changed files with 147 additions and 196 deletions

View File

@@ -59,7 +59,7 @@ from com.views import (
PosterEditBaseView, PosterEditBaseView,
PosterListBaseView, PosterListBaseView,
) )
from core.auth.mixins import CanEditMixin from core.auth.mixins import CanEditMixin, PermissionOrClubBoardRequiredMixin
from core.models import PageRev from core.models import PageRev
from core.views import DetailFormView, PageEditViewBase, UseFragmentsMixin from core.views import DetailFormView, PageEditViewBase, UseFragmentsMixin
from core.views.mixins import FragmentMixin, FragmentRenderer, TabedViewMixin from core.views.mixins import FragmentMixin, FragmentRenderer, TabedViewMixin
@@ -758,11 +758,13 @@ class MailingAutoGenerationView(View):
return redirect("club:mailing", club_id=club.id) return redirect("club:mailing", club_id=club.id)
class PosterListView(ClubTabsMixin, PosterListBaseView): class PosterListView(
PermissionOrClubBoardRequiredMixin, ClubTabsMixin, PosterListBaseView
):
"""List communication posters.""" """List communication posters."""
current_tab = "posters" current_tab = "posters"
extra_context = {"app": "club"} permission_required = "com.view_poster"
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter(club=self.club.id) return super().get_queryset().filter(club=self.club.id)
@@ -770,6 +772,17 @@ class PosterListView(ClubTabsMixin, PosterListBaseView):
def get_object(self): def get_object(self):
return self.club return self.club
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"create_url": reverse_lazy(
"club:poster_create", kwargs={"club_id": self.club.id}
),
"get_edit_url": lambda poster: reverse(
"club:poster_edit",
kwargs={"club_id": self.club.id, "poster_id": poster.id},
),
}
class PosterCreateView(ClubTabsMixin, PosterCreateBaseView): class PosterCreateView(ClubTabsMixin, PosterCreateBaseView):
"""Create communication poster.""" """Create communication poster."""

View File

@@ -402,9 +402,7 @@ class Poster(models.Model):
groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID] groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID]
): ):
Notification.objects.create( Notification.objects.create(
user=user, user=user, url=reverse("com:poster_list"), type="POSTER_MODERATION"
url=reverse("com:poster_moderate_list"),
type="POSTER_MODERATION",
) )
return super().save(*args, **kwargs) return super().save(*args, **kwargs)

View File

@@ -20,33 +20,7 @@
position: absolute; position: absolute;
display: flex; display: flex;
bottom: 5px; bottom: 5px;
left: 0;
&.left {
left: 0;
}
&.right {
right: 0;
}
.link {
padding: 5px;
padding-left: 20px;
padding-right: 20px;
margin-left: 5px;
border-radius: 20px;
background-color: hsl(40, 100%, 50%);
color: black;
&:hover {
color: black;
background-color: hsl(40, 58%, 50%);
}
&.delete {
background-color: hsl(0, 100%, 40%);
}
}
} }
} }
@@ -143,43 +117,15 @@
} }
} }
.edit, .actions {
.moderate, display: flex;
.slideshow { flex-direction: column;
padding: 5px; align-items: stretch;
border-radius: 20px; form {
background-color: hsl(40, 100%, 50%); margin: unset;
color: black; padding: unset;
button {
&:hover { width: 100%;
color: black;
background-color: hsl(40, 58%, 50%);
}
&:nth-child(2n) {
margin-top: 5px;
margin-bottom: 5px;
}
}
.tooltip {
visibility: hidden;
width: 120px;
background-color: hsl(210, 20%, 98%);
color: hsl(0, 0%, 0%);
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 10;
ul {
margin-left: 0;
display: inline-block;
li {
display: list-item;
list-style-type: none;
} }
} }
} }

View File

@@ -13,53 +13,53 @@
<div id="title"> <div id="title">
<h3>{% trans %}Posters{% endtrans %}</h3> <h3>{% trans %}Posters{% endtrans %}</h3>
<div id="links" class="right"> <div id="links">
{% if app == "com" %} <a id="create" class="btn btn-blue" href="{{ create_url }}">
<a id="create" class="link" href="{{ url(app + ":poster_create") }}">{% trans %}Create{% endtrans %}</a> <i class="fa fa-plus"></i>
<a id="moderation" class="link" href="{{ url("com:poster_moderate_list") }}">{% trans %}Moderation{% endtrans %}</a> {% trans %}Create{% endtrans %}
{% elif app == "club" %} </a>
<a id="create" class="link" href="{{ url(app + ":poster_create", club.id) }}">{% trans %}Create{% endtrans %}</a>
{% endif %}
</div> </div>
</div> </div>
<div id="posters"> <div id="posters">
{% for poster in poster_list %}
{% if poster_list.count() == 0 %} <div class="poster{% if not poster.is_moderated %} not_moderated{% endif %}">
<div id="no-posters">{% trans %}No posters{% endtrans %}</div> <div class="name">{{ poster.name }}</div>
{% else %} <div
class="image"
{% for poster in poster_list %} hover="{% trans %}Click to expand{% endtrans %}"
<div class="poster{% if not poster.is_moderated %} not_moderated{% endif %}"> @click="active = $el.firstElementChild"
<div class="name">{{ poster.name }}</div> tooltip="{%- for screen in poster.screens.all() -%}
<div {{ screen }}
class="image" {% endfor %}"
hover="{% trans %}Click to expand{% endtrans %}" >
@click="active = $el.firstElementChild" <img src="{{ poster.file.url }}" alt="{{ poster.name }}">
>
<img src="{{ poster.file.url }}"></img>
</div>
<div class="dates">
<div class="begin">{{ poster.date_begin | localtime | date("d/M/Y H:m") }}</div>
<div class="end">{{ poster.date_end | localtime | date("d/M/Y H:m") }}</div>
</div>
{% if app == "com" %}
<a class="edit" href="{{ url(app + ":poster_edit", poster.id) }}">{% trans %}Edit{% endtrans %}</a>
{% elif app == "club" %}
<a class="edit" href="{{ url(app + ":poster_edit", club.id, poster.id) }}">{% trans %}Edit{% endtrans %}</a>
{% endif %}
<div class="tooltip">
<ul>
{% for screen in poster.screens.all() %}
<li>{{ screen }}</li>
{% endfor %}
</ul>
</div>
</div> </div>
{% endfor %} <div class="dates">
<div class="begin">{{ poster.date_begin | localtime | date("d/M/Y H:m") }}</div>
{% endif %} <div class="end">{{ poster.date_end | localtime | date("d/M/Y H:m") }}</div>
</div>
<div class="actions">
{% if poster.is_editable %}
<a class="btn btn-blue" href="{{ get_edit_url(poster) }}">
<i class="fa fa-pen-to-square"></i>
{% trans %}Edit{% endtrans %}
</a>
{% endif %}
{% if not poster.is_moderated and user.has_perm("com.moderate_poster") %}
<form action="{{ url("com:poster_moderate", object_id=poster.id) }}" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-green">
<i class="fa fa-check"></i>
{% trans %}Moderate{% endtrans %}
</button>
</form>
{% endif %}
</div>
</div>
{% else %}
<div id="no-posters">{% trans %}No posters{% endtrans %}</div>
{% endfor %}
</div> </div>
<div <div
@@ -68,7 +68,9 @@
@click="active = null" @click="active = null"
:class="{active: active !== null}" :class="{active: active !== null}"
> >
<div id="placeholder"><img :src="active?.src"></div> <div id="placeholder">
<img :src="active?.src" :alt="active?.name">
</div>
</div> </div>
</div> </div>

View File

@@ -1,43 +0,0 @@
{% extends "core/base.jinja" %}
{% block script %}
{{ super() }}
<script src="{{ static('com/js/poster_list.js') }}"></script>
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static('com/css/posters.scss') }}">
{% endblock %}
{% block content %}
<div id="poster_list">
<div id="title">
<div id="links" class="left">
<a id="list" class="link" href="{{ url("com:poster_list") }}">{% trans %}List{% endtrans %}</a>
</div>
<h3>{% trans %}Posters - moderation{% endtrans %}</h3>
</div>
<div id="posters">
{% if object_list.count == 0 %}
<div id="no-posters">{% trans %}No objects{% endtrans %}</div>
{% else %}
{% for poster in object_list %}
<div class="poster{% if not poster.is_moderated %} not_moderated{% endif %}">
<div class="name"> {{ poster.name }} </div>
<div class="image"> <img src="{{ poster.file.url }}"></img> </div>
<a class="moderate" href="{{ url("com:poster_moderate", object_id=poster.id) }}">Moderate</a>
</div>
{% endfor %}
{% endif %}
</div>
<div id="view"><div id="placeholder"></div></div>
</div>
{% endblock %}

View File

@@ -17,7 +17,9 @@ from unittest.mock import patch
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Permission
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import Client, TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import html from django.utils import html
@@ -27,9 +29,10 @@ from model_bakery import baker
from pytest_django.asserts import assertNumQueries, assertRedirects from pytest_django.asserts import assertNumQueries, assertRedirects
from club.models import Club, Membership from club.models import Club, Membership
from com.models import News, NewsDate, Sith, Weekmail, WeekmailArticle from com.models import News, NewsDate, Poster, Sith, Weekmail, WeekmailArticle
from core.baker_recipes import subscriber_user from core.baker_recipes import subscriber_user
from core.models import AnonymousUser, Group, User from core.models import AnonymousUser, Group, User
from core.utils import RED_PIXEL_PNG
@pytest.fixture() @pytest.fixture()
@@ -314,7 +317,6 @@ def test_feed(client: Client):
[ [
reverse("com:poster_list"), reverse("com:poster_list"),
reverse("com:poster_create"), reverse("com:poster_create"),
reverse("com:poster_moderate_list"),
], ],
) )
def test_poster_management_views_crash_test(client: Client, url: str): def test_poster_management_views_crash_test(client: Client, url: str):
@@ -325,3 +327,37 @@ def test_poster_management_views_crash_test(client: Client, url: str):
client.force_login(user) client.force_login(user)
res = client.get(url) res = client.get(url)
assert res.status_code == 200 assert res.status_code == 200
@pytest.mark.django_db
@pytest.mark.parametrize(
"referer",
[
None,
reverse("com:poster_list"),
reverse("club:poster_list", kwargs={"club_id": settings.SITH_MAIN_CLUB_ID}),
],
)
def test_moderate_poster(client: Client, referer: str | None):
poster = baker.make(
Poster,
is_moderated=False,
file=SimpleUploadedFile("test.png", content=RED_PIXEL_PNG),
club_id=settings.SITH_MAIN_CLUB_ID,
)
user = baker.make(
User,
user_permissions=Permission.objects.filter(
codename__in=["view_poster", "moderate_poster"]
),
)
client.force_login(user)
headers = {"REFERER": f"https://{settings.SITH_URL}{referer}"} if referer else {}
response = client.post(
reverse("com:poster_moderate", kwargs={"object_id": poster.id}), headers=headers
)
result_url = referer or reverse("com:poster_list")
assertRedirects(response, result_url)
poster.refresh_from_db()
assert poster.is_moderated
assert poster.moderator == user

View File

@@ -33,7 +33,6 @@ from com.views import (
PosterDeleteView, PosterDeleteView,
PosterEditView, PosterEditView,
PosterListView, PosterListView,
PosterModerateListView,
PosterModerateView, PosterModerateView,
ScreenCreateView, ScreenCreateView,
ScreenDeleteView, ScreenDeleteView,
@@ -102,11 +101,6 @@ urlpatterns = [
PosterDeleteView.as_view(), PosterDeleteView.as_view(),
name="poster_delete", name="poster_delete",
), ),
path(
"poster/moderate/",
PosterModerateListView.as_view(),
name="poster_moderate_list",
),
path( path(
"poster/<int:object_id>/moderate/", "poster/<int:object_id>/moderate/",
PosterModerateView.as_view(), PosterModerateView.as_view(),

View File

@@ -25,6 +25,7 @@ import itertools
from datetime import date, timedelta from datetime import date, timedelta
from smtplib import SMTPRecipientsRefused from smtplib import SMTPRecipientsRefused
from typing import Any from typing import Any
from urllib.parse import urlparse
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.conf import settings from django.conf import settings
@@ -34,7 +35,7 @@ from django.contrib.auth.mixins import (
) )
from django.contrib.syndication.views import Feed from django.contrib.syndication.views import Feed
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import Max from django.db.models import Exists, Max, OuterRef, Value
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
@@ -45,7 +46,7 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, TemplateView, View from django.views.generic import DetailView, ListView, TemplateView, View
from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.edit import CreateView, DeleteView, UpdateView
from club.models import Club, Mailing from club.models import Club, Mailing, Membership
from com.forms import NewsDateForm, NewsForm, PosterForm from com.forms import NewsDateForm, NewsForm, PosterForm
from com.ics_calendar import IcsCalendar from com.ics_calendar import IcsCalendar
from com.models import News, NewsDate, Poster, Screen, Sith, Weekmail, WeekmailArticle from com.models import News, NewsDate, Poster, Screen, Sith, Weekmail, WeekmailArticle
@@ -561,16 +562,26 @@ class MailingModerateView(View):
raise PermissionDenied raise PermissionDenied
class PosterListBaseView(PermissionOrClubBoardRequiredMixin, ListView): class PosterListBaseView(ListView):
"""List communication posters.""" """List communication posters."""
model = Poster model = Poster
template_name = "com/poster_list.jinja" template_name = "com/poster_list.jinja"
permission_required = "com.view_poster" permission_required = "com.view_poster"
ordering = ["-date_begin"]
def get_context_data(self, **kwargs): def get_queryset(self):
return super().get_context_data(**kwargs) | {"club": self.club} qs = Poster.objects.prefetch_related("screens")
if self.request.user.has_perm("com.edit_poster"):
qs = qs.annotate(is_editable=Value(value=True))
else:
qs = qs.annotate(
is_editable=Exists(
Membership.objects.ongoing()
.board()
.filter(user=self.request.user, club=OuterRef("club_id"))
)
)
return qs.order_by("-date_begin")
class PosterCreateBaseView(PermissionOrClubBoardRequiredMixin, CreateView): class PosterCreateBaseView(PermissionOrClubBoardRequiredMixin, CreateView):
@@ -633,21 +644,17 @@ class PosterDeleteBaseView(
permission_required = "com.delete_poster" permission_required = "com.delete_poster"
class PosterListView(ComTabsMixin, PosterListBaseView): class PosterListView(PermissionRequiredMixin, ComTabsMixin, PosterListBaseView):
"""List communication posters.""" """List communication posters."""
current_tab = "posters" current_tab = "posters"
extra_context = {
def get_queryset(self): "create_url": reverse_lazy("com:poster_create"),
qs = super().get_queryset() "get_edit_url": lambda poster: reverse(
if self.request.user.has_perm("com.view_poster"): "com:poster_edit", kwargs={"poster_id": poster.id}
return qs ),
return qs.filter(club=self.club.id) }
permission_required = "com.view_poster"
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["app"] = "com"
return kwargs
class PosterCreateView(ComTabsMixin, PosterCreateBaseView): class PosterCreateView(ComTabsMixin, PosterCreateBaseView):
@@ -672,17 +679,6 @@ class PosterDeleteView(PosterDeleteBaseView):
success_url = reverse_lazy("com:poster_list") success_url = reverse_lazy("com:poster_list")
class PosterModerateListView(PermissionRequiredMixin, ComTabsMixin, ListView):
"""Moderate list communication poster."""
current_tab = "posters"
model = Poster
template_name = "com/poster_moderate.jinja"
queryset = Poster.objects.filter(is_moderated=False).all()
permission_required = "com.moderate_poster"
extra_context = {"app": "com"}
class PosterModerateView(PermissionRequiredMixin, ComTabsMixin, View): class PosterModerateView(PermissionRequiredMixin, ComTabsMixin, View):
"""Moderate communication poster.""" """Moderate communication poster."""
@@ -690,12 +686,21 @@ class PosterModerateView(PermissionRequiredMixin, ComTabsMixin, View):
permission_required = "com.moderate_poster" permission_required = "com.moderate_poster"
extra_context = {"app": "com"} extra_context = {"app": "com"}
def get(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
obj = get_object_or_404(Poster, pk=kwargs["object_id"]) obj = get_object_or_404(Poster, pk=kwargs["object_id"])
obj.is_moderated = True obj.is_moderated = True
obj.moderator = request.user obj.moderator = request.user
obj.save() obj.save()
return redirect("com:poster_moderate_list") # The moderation request may be originated from a club context (/club/poster)
# or a global context (/com/poster),
# so the redirection URL will be the URL of the page that called this view,
# as long as the latter belongs to the sith.
referer = self.request.META.get("HTTP_REFERER")
if referer:
parsed = urlparse(referer)
if parsed.netloc == settings.SITH_URL:
return redirect(parsed.path)
return redirect(reverse("com:poster_list"))
class ScreenListView(PermissionRequiredMixin, ComTabsMixin, ListView): class ScreenListView(PermissionRequiredMixin, ComTabsMixin, ListView):

View File

@@ -44,7 +44,7 @@ dependencies = [
"django-honeypot>=1.3.0,<2", "django-honeypot>=1.3.0,<2",
"pydantic-extra-types<3.0.0,>=2.10.3", "pydantic-extra-types<3.0.0,>=2.10.3",
"ical>=11,<12", "ical>=11,<12",
"redis[hiredis]>=5.3.0,<8", "redis[hiredis]<7,>=5.3.0",
"environs[django]<15.0.0,>=14.1.1", "environs[django]<15.0.0,>=14.1.1",
"requests>=2.32.3", "requests>=2.32.3",
"honcho>=2.0.0", "honcho>=2.0.0",