Skip to content

Commit

Permalink
Merge pull request #1027 from ae-utbm/calendar-moderation
Browse files Browse the repository at this point in the history
Moderation of news through calendar and rename moderation to publish
  • Loading branch information
klmp200 authored Feb 25, 2025
2 parents be87af5 + a653f98 commit 1f1cd2c
Show file tree
Hide file tree
Showing 23 changed files with 440 additions and 169 deletions.
34 changes: 29 additions & 5 deletions com/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
from typing import Literal

from django.conf import settings
from django.http import Http404
from django.http import Http404, HttpResponse
from ninja import Query
from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.permissions import IsAuthenticated
from ninja_extra.schemas import PaginatedResponseSchema

from com.calendar import IcsCalendar
Expand Down Expand Up @@ -38,18 +39,41 @@ def calendar_external(self):
def calendar_internal(self):
return send_raw_file(IcsCalendar.get_internal())

@route.get(
"/unpublished.ics",
permissions=[IsAuthenticated],
url_name="calendar_unpublished",
)
def calendar_unpublished(self):
return HttpResponse(
IcsCalendar.get_unpublished(self.context.request.user),
content_type="text/calendar",
)


@api_controller("/news")
class NewsController(ControllerBase):
@route.patch(
"/{int:news_id}/moderate",
"/{int:news_id}/publish",
permissions=[HasPerm("com.moderate_news")],
url_name="moderate_news",
)
def moderate_news(self, news_id: int):
def publish_news(self, news_id: int):
news = self.get_object_or_exception(News, id=news_id)
if not news.is_published:
news.is_published = True
news.moderator = self.context.request.user
news.save()

@route.patch(
"/{int:news_id}/unpublish",
permissions=[HasPerm("com.moderate_news")],
url_name="unpublish_news",
)
def unpublish_news(self, news_id: int):
news = self.get_object_or_exception(News, id=news_id)
if not news.is_moderated:
news.is_moderated = True
if news.is_published:
news.is_published = False
news.moderator = self.context.request.user
news.save()

Expand Down
39 changes: 29 additions & 10 deletions com/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import requests
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.db.models import F, QuerySet
from django.urls import reverse
from django.utils import timezone
from ical.calendar import Calendar
from ical.calendar_stream import IcsCalendarStream
from ical.event import Event

from com.models import NewsDate
from core.models import User


@final
Expand Down Expand Up @@ -55,21 +57,38 @@ def get_internal(cls) -> Path:
@classmethod
def make_internal(cls) -> Path:
# Updated through a post_save signal on News in com.signals
# Create a file so we can offload the download to the reverse proxy if available
cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True)
with open(cls._INTERNAL_CALENDAR, "wb") as f:
_ = f.write(
cls.ics_from_queryset(
NewsDate.objects.filter(
news__is_published=True,
end_date__gte=timezone.now() - (relativedelta(months=6)),
)
)
)
return cls._INTERNAL_CALENDAR

@classmethod
def get_unpublished(cls, user: User) -> bytes:
return cls.ics_from_queryset(
NewsDate.objects.viewable_by(user).filter(
news__is_published=False,
end_date__gte=timezone.now() - (relativedelta(months=6)),
),
)

@classmethod
def ics_from_queryset(cls, queryset: QuerySet[NewsDate]) -> bytes:
calendar = Calendar()
for news_date in NewsDate.objects.filter(
news__is_moderated=True,
end_date__gte=timezone.now() - (relativedelta(months=6)),
).prefetch_related("news"):
for news_date in queryset.annotate(news_title=F("news__title")):
event = Event(
summary=news_date.news.title,
summary=news_date.news_title,
start=news_date.start_date,
end=news_date.end_date,
url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}),
)
calendar.events.append(event)

# Create a file so we can offload the download to the reverse proxy if available
cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True)
with open(cls._INTERNAL_CALENDAR, "wb") as f:
_ = f.write(IcsCalendarStream.calendar_to_ics(calendar).encode("utf-8"))
return cls._INTERNAL_CALENDAR
return IcsCalendarStream.calendar_to_ics(calendar).encode("utf-8")
10 changes: 5 additions & 5 deletions com/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ class Meta:
"content": MarkdownInput,
}

auto_moderate = forms.BooleanField(
label=_("Automoderation"),
auto_publish = forms.BooleanField(
label=_("Auto publication"),
widget=CheckboxInput(attrs={"class": "switch"}),
required=False,
)
Expand Down Expand Up @@ -182,12 +182,12 @@ def full_clean(self):
def save(self, commit: bool = True): # noqa FBT001
self.instance.author = self.author
if (self.author.is_com_admin or self.author.is_root) and (
self.cleaned_data.get("auto_moderate") is True
self.cleaned_data.get("auto_publish") is True
):
self.instance.is_moderated = True
self.instance.is_published = True
self.instance.moderator = self.author
else:
self.instance.is_moderated = False
self.instance.is_published = False
created_news = super().save(commit=commit)
self.date_form.save(commit=commit, news=created_news)
return created_news
16 changes: 16 additions & 0 deletions com/migrations/0009_remove_news_is_moderated_news_is_published.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [("com", "0008_alter_news_options_alter_newsdate_options_and_more")]

operations = [
migrations.RenameField(
model_name="news", old_name="is_moderated", new_name="is_published"
),
migrations.AlterField(
model_name="news",
name="is_published",
field=models.BooleanField(default=False, verbose_name="is published"),
),
]
16 changes: 8 additions & 8 deletions com/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def is_owned_by(self, user):

class NewsQuerySet(models.QuerySet):
def moderated(self) -> Self:
return self.filter(is_moderated=True)
return self.filter(is_published=True)

def viewable_by(self, user: User) -> Self:
"""Filter news that the given user can view.
Expand All @@ -68,7 +68,7 @@ def viewable_by(self, user: User) -> Self:
"""
if user.has_perm("com.view_unmoderated_news"):
return self
q_filter = Q(is_moderated=True)
q_filter = Q(is_published=True)
if user.is_authenticated:
q_filter |= Q(author_id=user.id)
return self.filter(q_filter)
Expand Down Expand Up @@ -104,7 +104,7 @@ class News(models.Model):
verbose_name=_("author"),
on_delete=models.PROTECT,
)
is_moderated = models.BooleanField(_("is moderated"), default=False)
is_published = models.BooleanField(_("is published"), default=False)
moderator = models.ForeignKey(
User,
related_name="moderated_news",
Expand All @@ -127,7 +127,7 @@ def __str__(self):

def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.is_moderated:
if self.is_published:
return
for user in User.objects.filter(
groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID]
Expand All @@ -154,15 +154,15 @@ def can_be_edited_by(self, user: User):

def can_be_viewed_by(self, user: User):
return (
self.is_moderated
self.is_published
or user.has_perm("com.view_unmoderated_news")
or (user.is_authenticated and self.author_id == user.id)
)


def news_notification_callback(notif):
count = News.objects.filter(
dates__start_date__gt=timezone.now(), is_moderated=False
dates__start_date__gt=timezone.now(), is_published=False
).count()
if count:
notif.viewed = False
Expand All @@ -182,7 +182,7 @@ def viewable_by(self, user: User) -> Self:
"""
if user.has_perm("com.view_unmoderated_news"):
return self
q_filter = Q(news__is_moderated=True)
q_filter = Q(news__is_published=True)
if user.is_authenticated:
q_filter |= Q(news__author_id=user.id)
return self.filter(q_filter)
Expand Down Expand Up @@ -337,7 +337,7 @@ def __str__(self):

def active_posters(self):
now = timezone.now()
return self.posters.filter(is_moderated=True, date_begin__lte=now).filter(
return self.posters.filter(d=True, date_begin__lte=now).filter(
Q(date_end__isnull=True) | Q(date_end__gte=now)
)

Expand Down
4 changes: 2 additions & 2 deletions com/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ class NewsDateFilterSchema(FilterSchema):
after: datetime | None = Field(None, q="start_date__gt")
club_id: int | None = Field(None, q="news__club_id")
news_id: int | None = None
is_moderated: bool | None = Field(None, q="news__is_moderated")
is_published: bool | None = Field(None, q="news__is_published")
title: str | None = Field(None, q="news__title__icontains")


class NewsSchema(ModelSchema):
class Meta:
model = News
fields = ["id", "title", "summary", "is_moderated"]
fields = ["id", "title", "summary", "is_published"]

club: ClubProfileSchema
url: str
Expand Down
Loading

0 comments on commit 1f1cd2c

Please sign in to comment.