Skip to content

Commit

Permalink
Merge pull request #1020 from ae-utbm/taiste
Browse files Browse the repository at this point in the history
RSS feed, subscription creation permisssion, pedagogy permissions and bugfixes
  • Loading branch information
imperosol authored Feb 15, 2025
2 parents 170f9dd + 3df3326 commit fa02f4b
Show file tree
Hide file tree
Showing 38 changed files with 858 additions and 464 deletions.
2 changes: 1 addition & 1 deletion .github/actions/setup_project/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ runs:
using: composite
steps:
- name: Install apt packages
uses: awalsh128/cache-apt-pkgs-action@latest
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
with:
packages: gettext
version: 1.0 # increment to reset cache
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
uv run coverage report
uv run coverage html
- name: Archive code coverage results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: coverage-report
name: coverage-report-${{ matrix.pytest-mark }}
path: coverage_report
10 changes: 4 additions & 6 deletions club/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["request_user"] = self.request.user
kwargs["club"] = self.get_object()
kwargs["club"] = self.object
kwargs["club_members"] = self.members
return kwargs

Expand All @@ -273,9 +273,9 @@ def form_valid(self, form):
users = data.pop("users", [])
users_old = data.pop("users_old", [])
for user in users:
Membership(club=self.get_object(), user=user, **data).save()
Membership(club=self.object, user=user, **data).save()
for user in users_old:
membership = self.get_object().get_membership_for(user)
membership = self.object.get_membership_for(user)
membership.end_date = timezone.now()
membership.save()
return resp
Expand All @@ -285,9 +285,7 @@ def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

def get_success_url(self, **kwargs):
return reverse_lazy(
"club:club_members", kwargs={"club_id": self.get_object().id}
)
return reverse_lazy("club:club_members", kwargs={"club_id": self.object.id})


class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
Expand Down
5 changes: 3 additions & 2 deletions com/static/bundled/com/components/ics-calendar-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export class IcsCalendar extends inheritHtmlElement("div") {

async connectedCallback() {
super.connectedCallback();
const cacheInvalidate = `?invalidate=${Date.now()}`;
this.calendar = new Calendar(this.node, {
plugins: [dayGridPlugin, iCalendarPlugin, listPlugin],
locales: [frLocale, enLocale],
Expand All @@ -161,11 +162,11 @@ export class IcsCalendar extends inheritHtmlElement("div") {
headerToolbar: this.currentToolbar(),
eventSources: [
{
url: await makeUrl(calendarCalendarInternal),
url: `${await makeUrl(calendarCalendarInternal)}${cacheInvalidate}`,
format: "ics",
},
{
url: await makeUrl(calendarCalendarExternal),
url: `${await makeUrl(calendarCalendarExternal)}${cacheInvalidate}`,
format: "ics",
},
],
Expand Down
8 changes: 4 additions & 4 deletions com/static/com/components/ics-calendar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -75,24 +75,24 @@ ics-calendar {
}

td {
overflow-x: visible; // Show events on multiple days
overflow: visible; // Show events on multiple days
}

//Reset from style.scss
//Reset from style.scss
table {
box-shadow: none;
border-radius: 0px;
-moz-border-radius: 0px;
margin: 0px;
}

// Reset from style.scss
// Reset from style.scss
thead {
background-color: white;
color: black;
}

// Reset from style.scss
// Reset from style.scss
tbody>tr {
&:nth-child(even):not(.highlight) {
background: white;
Expand Down
5 changes: 5 additions & 0 deletions com/static/com/css/news-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
&:not(:first-of-type) {
margin: 2em 0 1em 0;
}

.feed {
float: right;
color: #f26522;
}
}

@media screen and (max-width: $small-devices) {
Expand Down
13 changes: 11 additions & 2 deletions com/templates/com/news_list.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
{% block additional_css %}
<link rel="stylesheet" href="{{ static('com/css/news-list.scss') }}">
<link rel="stylesheet" href="{{ static('com/components/ics-calendar.scss') }}">

{# Atom feed discovery, not really css but also goes there #}
<link rel="alternate" type="application/rss+xml" title="{% trans %}News feed{% endtrans %}" href="{{ url("com:news_feed") }}">
{% endblock %}

{% block additional_js %}
Expand All @@ -19,7 +22,10 @@
<div id="news">
<div id="left_column" class="news_column">
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5), news__is_moderated=True).datetimes('start_date', 'day') %}
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
<h3>
{% trans %}Events today and the next few days{% endtrans %}
<a target="#" href="{{ url("com:news_feed") }}"><i class="fa fa-rss feed"></i></a>
</h3>
{% if user.is_authenticated and (user.is_com_admin or user.memberships.board().ongoing().exists()) %}
<a class="btn btn-blue margin-bottom" href="{{ url("com:news_new") }}">
<i class="fa fa-plus"></i>
Expand Down Expand Up @@ -73,7 +79,10 @@
</div>
{% endif %}

<h3>{% trans %}All coming events{% endtrans %}</h3>
<h3>
{% trans %}All coming events{% endtrans %}
<a target="#" href="{{ url("com:news_feed") }}"><i class="fa fa-rss feed"></i></a>
</h3>
<ics-calendar locale="{{ get_language() }}"></ics-calendar>
</div>

Expand Down
15 changes: 14 additions & 1 deletion com/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@

import pytest
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.urls import reverse
from django.utils import html
from django.utils.timezone import localtime, now
from django.utils.translation import gettext as _
from model_bakery import baker
from pytest_django.asserts import assertRedirects
from pytest_django.asserts import assertNumQueries, assertRedirects

from club.models import Club, Membership
from com.models import News, NewsDate, Poster, Sith, Weekmail, WeekmailArticle
Expand Down Expand Up @@ -319,3 +320,15 @@ def test_ics_updated(self):
self.valid_payload,
)
mocked.assert_called()


@pytest.mark.django_db
def test_feed(client):
"""Smoke test that checks that the atom feed is working"""
Site.objects.clear_cache()
with assertNumQueries(2):
# get sith domain with Site api: 1 request
# get all news and related info: 1 request
resp = client.get(reverse("com:news_feed"))
assert resp.status_code == 200
assert resp.headers["Content-Type"] == "application/rss+xml; charset=utf-8"
2 changes: 2 additions & 0 deletions com/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
NewsCreateView,
NewsDeleteView,
NewsDetailView,
NewsFeed,
NewsListView,
NewsModerateView,
NewsUpdateView,
Expand Down Expand Up @@ -73,6 +74,7 @@
name="weekmail_article_edit",
),
path("news/", NewsListView.as_view(), name="news_list"),
path("news/feed/", NewsFeed(), name="news_feed"),
path("news/admin/", NewsAdminListView.as_view(), name="news_admin_list"),
path("news/create/", NewsCreateView.as_view(), name="news_new"),
path("news/<int:news_id>/edit/", NewsUpdateView.as_view(), name="news_edit"),
Expand Down
30 changes: 30 additions & 0 deletions com/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
from smtplib import SMTPRecipientsRefused
from typing import Any

from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin
from django.contrib.syndication.views import Feed
from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import Max
from django.forms.models import modelform_factory
Expand Down Expand Up @@ -268,6 +270,34 @@ def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {"date": self.object.dates.first()}


class NewsFeed(Feed):
title = _("News")
link = reverse_lazy("com:news_list")
description = _("All incoming events")

def items(self):
return (
NewsDate.objects.filter(
news__is_moderated=True,
end_date__gte=timezone.now() - (relativedelta(months=6)),
)
.select_related("news", "news__author")
.order_by("-start_date")
)

def item_title(self, item: NewsDate):
return item.news.title

def item_description(self, item: NewsDate):
return item.news.summary

def item_link(self, item: NewsDate):
return item.news.get_absolute_url()

def item_author_name(self, item: NewsDate):
return item.news.author.get_display_name()


# Weekmail


Expand Down
43 changes: 43 additions & 0 deletions core/auth/api_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ def bar_delete(self, bar_id: int):
```
"""

import operator
from functools import reduce
from typing import Any

from django.contrib.auth.models import Permission
from django.http import HttpRequest
from ninja_extra import ControllerBase
from ninja_extra.permissions import BasePermission
Expand All @@ -56,6 +59,46 @@ def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bo
return request.user.is_in_group(pk=self._group_pk)


class HasPerm(BasePermission):
"""Check that the user has the required perm.
If multiple perms are given, a comparer function can also be passed,
in order to change the way perms are checked.
Example:
```python
# this route will require both permissions
@route.put("/foo", permissions=[HasPerm(["foo.change_foo", "foo.add_foo"])]
def foo(self): ...
# This route will require at least one of the perm,
# but it's not mandatory to have all of them
@route.put(
"/bar",
permissions=[HasPerm(["foo.change_bar", "foo.add_bar"], op=operator.or_)],
)
def bar(self): ...
"""

def __init__(
self, perms: str | Permission | list[str | Permission], op=operator.and_
):
"""
Args:
perms: a permission or a list of permissions the user must have
op: An operator to combine multiple permissions (in most cases,
it will be either `operator.and_` or `operator.or_`)
"""
super().__init__()
if not isinstance(perms, (list, tuple, set)):
perms = [perms]
self._operator = op
self._perms = perms

def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
return reduce(self._operator, (request.user.has_perm(p) for p in self._perms))


class IsRoot(BasePermission):
"""Check that the user is root."""

Expand Down
23 changes: 18 additions & 5 deletions core/management/commands/populate.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,12 @@ def handle(self, *args, **options):
raise Exception("Never call this command in prod. Never.")

Sith.objects.create(weekmail_destinations="[email protected] [email protected]")
Site.objects.create(domain=settings.SITH_URL, name=settings.SITH_NAME)

site = Site.objects.get_current()
site.domain = settings.SITH_URL
site.name = settings.SITH_NAME
site.save()

groups = self._create_groups()
self._create_ban_groups()

Expand Down Expand Up @@ -120,6 +125,11 @@ def handle(self, *args, **options):
unix_name=settings.SITH_MAIN_CLUB["unix_name"],
address=settings.SITH_MAIN_CLUB["address"],
)
main_club.board_group.permissions.add(
*Permission.objects.filter(
codename__in=["view_subscription", "add_subscription"]
)
)
bar_club = Club.objects.create(
id=2,
name=settings.SITH_BAR_MANAGER["name"],
Expand Down Expand Up @@ -895,13 +905,16 @@ def _create_groups(self) -> PopulatedGroups:

subscribers = Group.objects.create(name="Subscribers")
subscribers.permissions.add(
*list(perms.filter(codename__in=["add_news", "add_uvcommentreport"]))
*list(perms.filter(codename__in=["add_news", "add_uvcomment"]))
)
old_subscribers = Group.objects.create(name="Old subscribers")
old_subscribers.permissions.add(
*list(
perms.filter(
codename__in=[
"view_uv",
"view_uvcomment",
"add_uvcommentreport",
"view_user",
"view_picture",
"view_album",
Expand Down Expand Up @@ -973,9 +986,9 @@ def _create_groups(self) -> PopulatedGroups:
)
pedagogy_admin.permissions.add(
*list(
perms.filter(content_type__app_label="pedagogy").values_list(
"pk", flat=True
)
perms.filter(content_type__app_label="pedagogy")
.exclude(codename__in=["change_uvcomment"])
.values_list("pk", flat=True)
)
)
self.reset_index("core", "auth")
Expand Down
Loading

0 comments on commit fa02f4b

Please sign in to comment.