mirror of
https://github.com/ae-utbm/sith.git
synced 2025-02-27 01:47:14 +00:00
Add moderation through calendar widget
This commit is contained in:
parent
e936f0d285
commit
a1bf86dabf
14
com/api.py
14
com/api.py
@ -2,10 +2,11 @@ from pathlib import Path
|
||||
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
|
||||
@ -38,6 +39,17 @@ class CalendarController(ControllerBase):
|
||||
def calendar_internal(self):
|
||||
return send_raw_file(IcsCalendar.get_internal())
|
||||
|
||||
@route.get(
|
||||
"/unmoderated.ics",
|
||||
permissions=[IsAuthenticated],
|
||||
url_name="calendar_unmoderated",
|
||||
)
|
||||
def calendar_unmoderated(self):
|
||||
return HttpResponse(
|
||||
IcsCalendar.get_unmoderated(self.context.request.user),
|
||||
content_type="text/calendar",
|
||||
)
|
||||
|
||||
|
||||
@api_controller("/news")
|
||||
class NewsController(ControllerBase):
|
||||
|
@ -5,6 +5,7 @@ from typing import final
|
||||
import requests
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.conf import settings
|
||||
from django.db.models import QuerySet
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from ical.calendar import Calendar
|
||||
@ -12,6 +13,7 @@ from ical.calendar_stream import IcsCalendarStream
|
||||
from ical.event import Event
|
||||
|
||||
from com.models import NewsDate
|
||||
from core.models import User
|
||||
|
||||
|
||||
@final
|
||||
@ -55,11 +57,32 @@ class IcsCalendar:
|
||||
@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_moderated=True,
|
||||
end_date__gte=timezone.now() - (relativedelta(months=6)),
|
||||
)
|
||||
)
|
||||
)
|
||||
return cls._INTERNAL_CALENDAR
|
||||
|
||||
@classmethod
|
||||
def get_unmoderated(cls, user: User) -> bytes:
|
||||
return cls.ics_from_queryset(
|
||||
NewsDate.objects.viewable_by(user).filter(
|
||||
news__is_moderated=False,
|
||||
end_date__gte=timezone.now() - (relativedelta(months=6)),
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def ics_from_queryset(cls, queryset: QuerySet) -> 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.prefetch_related("news"):
|
||||
event = Event(
|
||||
summary=news_date.news.title,
|
||||
start=news_date.start_date,
|
||||
@ -68,8 +91,4 @@ class IcsCalendar:
|
||||
)
|
||||
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")
|
||||
|
@ -7,20 +7,32 @@ import frLocale from "@fullcalendar/core/locales/fr";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import iCalendarPlugin from "@fullcalendar/icalendar";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import { calendarCalendarExternal, calendarCalendarInternal } from "#openapi";
|
||||
import {
|
||||
calendarCalendarExternal,
|
||||
calendarCalendarInternal,
|
||||
calendarCalendarUnmoderated,
|
||||
newsDeleteNews,
|
||||
newsModerateNews,
|
||||
} from "#openapi";
|
||||
|
||||
@registerComponent("ics-calendar")
|
||||
export class IcsCalendar extends inheritHtmlElement("div") {
|
||||
static observedAttributes = ["locale"];
|
||||
static observedAttributes = ["locale", "can_moderate", "can_delete"];
|
||||
private calendar: Calendar;
|
||||
private locale = "en";
|
||||
private canModerate = false;
|
||||
private canDelete = false;
|
||||
|
||||
attributeChangedCallback(name: string, _oldValue?: string, newValue?: string) {
|
||||
if (name !== "locale") {
|
||||
return;
|
||||
if (name === "locale") {
|
||||
this.locale = newValue;
|
||||
}
|
||||
if (name === "can_moderate") {
|
||||
this.canModerate = newValue.toLowerCase() === "true";
|
||||
}
|
||||
if (name === "can_delete") {
|
||||
this.canDelete = newValue.toLowerCase() === "true";
|
||||
}
|
||||
|
||||
this.locale = newValue;
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
@ -54,6 +66,68 @@ export class IcsCalendar extends inheritHtmlElement("div") {
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
getNewsId(event: EventImpl) {
|
||||
// return Number.parseInt(event.url.split("/").pop());
|
||||
return Number.parseInt(
|
||||
event.url
|
||||
.toString()
|
||||
.split("/")
|
||||
.filter((s) => s) // Remove blank characters
|
||||
.pop(),
|
||||
);
|
||||
}
|
||||
|
||||
async refreshEvents() {
|
||||
this.click(); // Remove focus from popup
|
||||
this.calendar.removeAllEventSources();
|
||||
for (const source of await this.getEventSources()) {
|
||||
this.calendar.addEventSource(source);
|
||||
}
|
||||
this.calendar.refetchEvents();
|
||||
}
|
||||
|
||||
async moderate(id: number) {
|
||||
await newsModerateNews({
|
||||
path: {
|
||||
// biome-ignore lint/style/useNamingConvention: python API
|
||||
news_id: id,
|
||||
},
|
||||
});
|
||||
await this.refreshEvents();
|
||||
}
|
||||
|
||||
async delete(id: number) {
|
||||
await newsDeleteNews({
|
||||
path: {
|
||||
// biome-ignore lint/style/useNamingConvention: python API
|
||||
news_id: id,
|
||||
},
|
||||
});
|
||||
await this.refreshEvents();
|
||||
}
|
||||
|
||||
async getEventSources() {
|
||||
const cacheInvalidate = `?invalidate=${Date.now()}`;
|
||||
return [
|
||||
{
|
||||
url: `${await makeUrl(calendarCalendarInternal)}${cacheInvalidate}`,
|
||||
format: "ics",
|
||||
className: "internal",
|
||||
},
|
||||
{
|
||||
url: `${await makeUrl(calendarCalendarExternal)}${cacheInvalidate}`,
|
||||
format: "ics",
|
||||
className: "external",
|
||||
},
|
||||
{
|
||||
url: `${await makeUrl(calendarCalendarUnmoderated)}${cacheInvalidate}`,
|
||||
format: "ics",
|
||||
color: "red",
|
||||
className: "unmoderated",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
createEventDetailPopup(event: EventClickArg) {
|
||||
// Delete previous popup
|
||||
const oldPopup = document.getElementById("event-details");
|
||||
@ -112,6 +186,40 @@ export class IcsCalendar extends inheritHtmlElement("div") {
|
||||
return makePopupInfo(url, "fa-solid fa-link");
|
||||
};
|
||||
|
||||
const makePopupTools = (event: EventImpl) => {
|
||||
if (event.source.internalEventSource.ui.classNames.indexOf("external") >= 0) {
|
||||
return null;
|
||||
}
|
||||
if (!(this.canDelete || this.canModerate)) {
|
||||
return null;
|
||||
}
|
||||
const newsId = this.getNewsId(event);
|
||||
const div = document.createElement("div");
|
||||
if (
|
||||
this.canModerate &&
|
||||
event.source.internalEventSource.ui.classNames.indexOf("unmoderated") >= 0
|
||||
) {
|
||||
const button = document.createElement("button");
|
||||
button.innerHTML = `<i class="fa fa-check"></i>${gettext("Moderate")}`;
|
||||
button.setAttribute("class", "btn btn-green");
|
||||
button.onclick = () => {
|
||||
this.moderate(newsId);
|
||||
};
|
||||
div.appendChild(button);
|
||||
}
|
||||
if (this.canDelete) {
|
||||
const button = document.createElement("button");
|
||||
button.innerHTML = `<i class="fa fa-trash-can"></i>${gettext("Delete")}`;
|
||||
button.setAttribute("class", "btn btn-red");
|
||||
button.onclick = () => {
|
||||
this.delete(newsId);
|
||||
};
|
||||
div.appendChild(button);
|
||||
}
|
||||
|
||||
return makePopupInfo(div, "fa-solid fa-toolbox");
|
||||
};
|
||||
|
||||
// Create new popup
|
||||
const popup = document.createElement("div");
|
||||
const popupContainer = document.createElement("div");
|
||||
@ -131,6 +239,11 @@ export class IcsCalendar extends inheritHtmlElement("div") {
|
||||
popupContainer.appendChild(url);
|
||||
}
|
||||
|
||||
const tools = makePopupTools(event.event);
|
||||
if (tools !== null) {
|
||||
popupContainer.appendChild(tools);
|
||||
}
|
||||
|
||||
popup.appendChild(popupContainer);
|
||||
|
||||
// We can't just add the element relative to the one we want to appear under
|
||||
@ -152,7 +265,6 @@ 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],
|
||||
@ -160,16 +272,7 @@ export class IcsCalendar extends inheritHtmlElement("div") {
|
||||
locale: this.locale,
|
||||
initialView: this.currentView(),
|
||||
headerToolbar: this.currentToolbar(),
|
||||
eventSources: [
|
||||
{
|
||||
url: `${await makeUrl(calendarCalendarInternal)}${cacheInvalidate}`,
|
||||
format: "ics",
|
||||
},
|
||||
{
|
||||
url: `${await makeUrl(calendarCalendarExternal)}${cacheInvalidate}`,
|
||||
format: "ics",
|
||||
},
|
||||
],
|
||||
eventSources: await this.getEventSources(),
|
||||
windowResize: () => {
|
||||
this.calendar.changeView(this.currentView());
|
||||
this.calendar.setOption("headerToolbar", this.currentToolbar());
|
||||
|
@ -179,7 +179,11 @@
|
||||
{% 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>
|
||||
<ics-calendar
|
||||
locale="{{ get_language() }}"
|
||||
can_moderate="{{ user.has_perm("com.moderate_news") }}"
|
||||
can_delete="{{ user.has_perm("com.delete_news") }}"
|
||||
></ics-calendar>
|
||||
</div>
|
||||
|
||||
<div id="right_column">
|
||||
|
Loading…
x
Reference in New Issue
Block a user