mirror of
https://github.com/ae-utbm/sith.git
synced 2025-02-27 01:47:14 +00:00
Merge pull request #1027 from ae-utbm/calendar-moderation
Moderation of news through calendar and rename moderation to publish
This commit is contained in:
commit
1f1cd2ce0f
34
com/api.py
34
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,18 +39,41 @@ class CalendarController(ControllerBase):
|
||||
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_moderated:
|
||||
news.is_moderated = True
|
||||
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 news.is_published:
|
||||
news.is_published = False
|
||||
news.moderator = self.context.request.user
|
||||
news.save()
|
||||
|
||||
|
@ -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 F, 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,21 +57,38 @@ 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_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
com/forms.py
10
com/forms.py
@ -147,8 +147,8 @@ class NewsForm(forms.ModelForm):
|
||||
"content": MarkdownInput,
|
||||
}
|
||||
|
||||
auto_moderate = forms.BooleanField(
|
||||
label=_("Automoderation"),
|
||||
auto_publish = forms.BooleanField(
|
||||
label=_("Auto publication"),
|
||||
widget=CheckboxInput(attrs={"class": "switch"}),
|
||||
required=False,
|
||||
)
|
||||
@ -182,12 +182,12 @@ class NewsForm(forms.ModelForm):
|
||||
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
|
||||
|
@ -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"),
|
||||
),
|
||||
]
|
@ -56,7 +56,7 @@ class Sith(models.Model):
|
||||
|
||||
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.
|
||||
@ -68,7 +68,7 @@ class NewsQuerySet(models.QuerySet):
|
||||
"""
|
||||
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)
|
||||
@ -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",
|
||||
@ -127,7 +127,7 @@ class News(models.Model):
|
||||
|
||||
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]
|
||||
@ -154,7 +154,7 @@ class News(models.Model):
|
||||
|
||||
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)
|
||||
)
|
||||
@ -162,7 +162,7 @@ class News(models.Model):
|
||||
|
||||
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
|
||||
@ -182,7 +182,7 @@ class NewsDateQuerySet(models.QuerySet):
|
||||
"""
|
||||
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)
|
||||
@ -337,7 +337,7 @@ class Screen(models.Model):
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
@ -7,20 +7,33 @@ 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,
|
||||
calendarCalendarUnpublished,
|
||||
newsDeleteNews,
|
||||
newsPublishNews,
|
||||
newsUnpublishNews,
|
||||
} 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 +67,104 @@ export class IcsCalendar extends inheritHtmlElement("div") {
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
getNewsId(event: EventImpl) {
|
||||
return Number.parseInt(
|
||||
event.url
|
||||
.toString()
|
||||
.split("/")
|
||||
.filter((s) => s) // Remove blank characters
|
||||
.pop(),
|
||||
);
|
||||
}
|
||||
|
||||
async refreshEvents() {
|
||||
this.click(); // Remove focus from popup
|
||||
// We can't just refresh events because some ics files are in
|
||||
// local browser cache (especially internal.ics)
|
||||
// To invalidate the cache, we need to remove the source and add it again
|
||||
this.calendar.removeAllEventSources();
|
||||
for (const source of await this.getEventSources()) {
|
||||
this.calendar.addEventSource(source);
|
||||
}
|
||||
this.calendar.refetchEvents();
|
||||
}
|
||||
|
||||
async publishNews(id: number) {
|
||||
await newsPublishNews({
|
||||
path: {
|
||||
// biome-ignore lint/style/useNamingConvention: python API
|
||||
news_id: id,
|
||||
},
|
||||
});
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("calendar-publish", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
id: id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
await this.refreshEvents();
|
||||
}
|
||||
|
||||
async unpublishNews(id: number) {
|
||||
await newsUnpublishNews({
|
||||
path: {
|
||||
// biome-ignore lint/style/useNamingConvention: python API
|
||||
news_id: id,
|
||||
},
|
||||
});
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("calendar-unpublish", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
id: id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
await this.refreshEvents();
|
||||
}
|
||||
|
||||
async deleteNews(id: number) {
|
||||
await newsDeleteNews({
|
||||
path: {
|
||||
// biome-ignore lint/style/useNamingConvention: python API
|
||||
news_id: id,
|
||||
},
|
||||
});
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("calendar-delete", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
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(calendarCalendarUnpublished)}${cacheInvalidate}`,
|
||||
format: "ics",
|
||||
color: "red",
|
||||
className: "unpublished",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
createEventDetailPopup(event: EventClickArg) {
|
||||
// Delete previous popup
|
||||
const oldPopup = document.getElementById("event-details");
|
||||
@ -112,6 +223,47 @@ export class IcsCalendar extends inheritHtmlElement("div") {
|
||||
return makePopupInfo(url, "fa-solid fa-link");
|
||||
};
|
||||
|
||||
const makePopupTools = (event: EventImpl) => {
|
||||
if (event.source.internalEventSource.ui.classNames.includes("external")) {
|
||||
return null;
|
||||
}
|
||||
if (!(this.canDelete || this.canModerate)) {
|
||||
return null;
|
||||
}
|
||||
const newsId = this.getNewsId(event);
|
||||
const div = document.createElement("div");
|
||||
if (this.canModerate) {
|
||||
if (event.source.internalEventSource.ui.classNames.includes("unpublished")) {
|
||||
const button = document.createElement("button");
|
||||
button.innerHTML = `<i class="fa fa-check"></i>${gettext("Publish")}`;
|
||||
button.setAttribute("class", "btn btn-green");
|
||||
button.onclick = () => {
|
||||
this.publishNews(newsId);
|
||||
};
|
||||
div.appendChild(button);
|
||||
} else {
|
||||
const button = document.createElement("button");
|
||||
button.innerHTML = `<i class="fa fa-times"></i>${gettext("Unpublish")}`;
|
||||
button.setAttribute("class", "btn btn-orange");
|
||||
button.onclick = () => {
|
||||
this.unpublishNews(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.deleteNews(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 +283,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 +309,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 +316,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());
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { exportToHtml } from "#core:utils/globals";
|
||||
import { newsDeleteNews, newsFetchNewsDates, newsModerateNews } from "#openapi";
|
||||
import { newsDeleteNews, newsFetchNewsDates, newsPublishNews } from "#openapi";
|
||||
|
||||
// This will be used in jinja templates,
|
||||
// so we cannot use real enums as those are purely an abstraction of Typescript
|
||||
@ -7,9 +7,11 @@ const AlertState = {
|
||||
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
|
||||
PENDING: 1,
|
||||
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
|
||||
MODERATED: 2,
|
||||
PUBLISHED: 2,
|
||||
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
|
||||
DELETED: 3,
|
||||
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
|
||||
DISPLAYED: 4, // When published at page generation
|
||||
};
|
||||
exportToHtml("AlertState", AlertState);
|
||||
|
||||
@ -19,11 +21,11 @@ document.addEventListener("alpine:init", () => {
|
||||
newsId: newsId as number,
|
||||
loading: false,
|
||||
|
||||
async moderateNews() {
|
||||
async publishNews() {
|
||||
this.loading = true;
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake case
|
||||
await newsModerateNews({ path: { news_id: this.newsId } });
|
||||
this.state = AlertState.MODERATED;
|
||||
await newsPublishNews({ path: { news_id: this.newsId } });
|
||||
this.state = AlertState.PUBLISHED;
|
||||
this.$dispatch("news-moderated", { newsId: this.newsId, state: this.state });
|
||||
this.loading = false;
|
||||
},
|
||||
@ -54,7 +56,7 @@ document.addEventListener("alpine:init", () => {
|
||||
* Query the server to know the number of news dates that would be moderated
|
||||
* if this one is moderated.
|
||||
*/
|
||||
async nbToModerate(): Promise<number> {
|
||||
async nbToPublish(): Promise<number> {
|
||||
// What we want here is the count attribute of the response.
|
||||
// We don't care about the actual results,
|
||||
// so we ask for the minimum page size possible.
|
||||
@ -69,8 +71,8 @@ document.addEventListener("alpine:init", () => {
|
||||
return interpolate(
|
||||
gettext(
|
||||
"This event will take place every week for %s weeks. " +
|
||||
"If you moderate or delete this event, " +
|
||||
"it will also be moderated (or deleted) for the following weeks.",
|
||||
"If you publish or delete this event, " +
|
||||
"it will also be published (or deleted) for the following weeks.",
|
||||
),
|
||||
[nbEvents],
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% macro news_moderation_alert(news, user, alpineState = None) %}
|
||||
{# An alert to display on top of non moderated news,
|
||||
with actions to either moderate or delete them.
|
||||
{# An alert to display on top of unpublished news,
|
||||
with actions to either publish or delete them.
|
||||
|
||||
The current state of the alert is accessible through
|
||||
the given `alpineState` variable.
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
This comes in three flavours :
|
||||
- You can pass the `News` object itself to the macro.
|
||||
In this case, if `request.user` can moderate news,
|
||||
In this case, if `request.user` can publish news,
|
||||
it will perform an additional db query to know if it is a recurring event.
|
||||
- You can also give only the news id.
|
||||
In this case, a server request will be issued to know
|
||||
@ -57,23 +57,23 @@
|
||||
{# the news-moderated is received when a moderation alert is deleted or moderated #}
|
||||
@news-moderated.window="dispatchModeration($event)"
|
||||
{% if alpineState %}
|
||||
x-modelable="{{ alpineState }}"
|
||||
x-model="state"
|
||||
x-model="{{ alpineState }}"
|
||||
x-modelable="state"
|
||||
{% endif %}
|
||||
>
|
||||
<template x-if="state === AlertState.PENDING">
|
||||
<div class="alert alert-yellow">
|
||||
<div class="alert-main">
|
||||
<strong>{% trans %}Waiting moderation{% endtrans %}</strong>
|
||||
<strong>{% trans %}Waiting publication{% endtrans %}</strong>
|
||||
<p>
|
||||
{% trans trimmed %}
|
||||
This news isn't moderated and is visible
|
||||
This news isn't published and is visible
|
||||
only by its author and the communication admins.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans trimmed %}
|
||||
It will stay hidden for other users until it has been moderated.
|
||||
It will stay hidden for other users until it has been published.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
{% if user.has_perm("com.moderate_news") %}
|
||||
@ -84,7 +84,7 @@
|
||||
<div
|
||||
{% if news is integer or news is string %}
|
||||
x-data="{ nbEvents: 0 }"
|
||||
x-init="nbEvents = await nbToModerate()"
|
||||
x-init="nbEvents = await nbToPublish()"
|
||||
{% else %}
|
||||
x-data="{ nbEvents: {{ news.dates.count() }} }"
|
||||
{% endif %}
|
||||
@ -101,8 +101,8 @@
|
||||
</div>
|
||||
{% if user.has_perm("com.moderate_news") %}
|
||||
<span class="alert-aside" :aria-busy="loading">
|
||||
<button class="btn btn-green" @click="moderateNews()" :disabled="loading">
|
||||
<i class="fa fa-check"></i> {% trans %}Moderate{% endtrans %}
|
||||
<button class="btn btn-green" @click="publishNews()" :disabled="loading">
|
||||
<i class="fa fa-check"></i> {% trans %}Publish{% endtrans %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if user.has_perm("com.delete_news") %}
|
||||
@ -113,9 +113,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="state === AlertState.MODERATED">
|
||||
<template x-if="state === AlertState.PUBLISHED">
|
||||
<div class="alert alert-green">
|
||||
{% trans %}News moderated{% endtrans %}
|
||||
{% trans %}News published{% endtrans %}
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="state === AlertState.DELETED">
|
||||
|
@ -27,7 +27,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for news in weeklies.filter(is_moderated=True) %}
|
||||
{% for news in weeklies.filter(is_published=True) %}
|
||||
<tr>
|
||||
<td>{{ news.title }}</td>
|
||||
<td>{{ news.summary|markdown }}</td>
|
||||
@ -47,7 +47,7 @@
|
||||
</td>
|
||||
<td><a href="{{ url('com:news_detail', news_id=news.id) }}">{% trans %}View{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_edit', news_id=news.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_moderate', news_id=news.id) }}?remove">{% trans %}Remove{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_moderate', news_id=news.id) }}?remove">{% trans %}Unpublish{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_delete', news_id=news.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
@ -67,7 +67,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for news in weeklies.filter(is_moderated=False) %}
|
||||
{% for news in weeklies.filter(is_published=False) %}
|
||||
<tr>
|
||||
<td>{{ news.title }}</td>
|
||||
<td>{{ news.summary|markdown }}</td>
|
||||
@ -86,7 +86,7 @@
|
||||
</td>
|
||||
<td><a href="{{ url('com:news_detail', news_id=news.id) }}">{% trans %}View{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_edit', news_id=news.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_moderate', news_id=news.id) }}">{% trans %}Moderate{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_moderate', news_id=news.id) }}">{% trans %}Publish{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_delete', news_id=news.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
@ -111,7 +111,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for news in events.filter(is_moderated=True) %}
|
||||
{% for news in events.filter(is_published=True) %}
|
||||
<tr>
|
||||
<td>{{ news.title }}</td>
|
||||
<td>{{ news.summary|markdown }}</td>
|
||||
@ -124,7 +124,7 @@
|
||||
{{ news.dates.all()[0].end_date|localtime|time(DATETIME_FORMAT) }}</td>
|
||||
<td><a href="{{ url('com:news_detail', news_id=news.id) }}">{% trans %}View{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_edit', news_id=news.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_moderate', news_id=news.id) }}?remove">{% trans %}Remove{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_moderate', news_id=news.id) }}?remove">{% trans %}Unpublish{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_delete', news_id=news.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
@ -145,7 +145,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for news in events.filter(is_moderated=False) %}
|
||||
{% for news in events.filter(is_published=False) %}
|
||||
<tr>
|
||||
<td>{{ news.title }}</td>
|
||||
<td>{{ news.summary|markdown }}</td>
|
||||
@ -157,7 +157,7 @@
|
||||
{{ news.dates.all()[0].end_date|localtime|time(DATETIME_FORMAT) }}</td>
|
||||
<td><a href="{{ url('com:news_detail', news_id=news.id) }}">{% trans %}View{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_edit', news_id=news.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_moderate', news_id=news.id) }}">{% trans %}Moderate{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_moderate', news_id=news.id) }}">{% trans %}Publish{% endtrans %}</a>
|
||||
<a href="{{ url('com:news_delete', news_id=news.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -25,7 +25,7 @@
|
||||
<p><a href="{{ url('com:news_list') }}">{% trans %}Back to news{% endtrans %}</a></p>
|
||||
<div x-data="{newsState: AlertState.PENDING}">
|
||||
|
||||
{% if not news.is_moderated %}
|
||||
{% if not news.is_published %}
|
||||
{{ news_moderation_alert(news, user, "newsState") }}
|
||||
{% endif %}
|
||||
<article id="news_details" x-show="newsState !== AlertState.DELETED">
|
||||
@ -35,10 +35,10 @@
|
||||
</div>
|
||||
<h4>{{ news.title }}</h4>
|
||||
<p class="date">
|
||||
<span>{{ date.start_date|localtime|date(DATETIME_FORMAT) }}
|
||||
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
||||
<span>{{ date.end_date|localtime|date(DATETIME_FORMAT) }}
|
||||
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||
<time datetime="{{ date.start_date.isoformat(timespec="seconds") }}">{{ date.start_date|localtime|date(DATETIME_FORMAT) }}
|
||||
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}</time> -
|
||||
<time datetime="{{ date.end_date.isoformat(timespec="seconds") }}">{{ date.end_date|localtime|date(DATETIME_FORMAT) }}
|
||||
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}</time>
|
||||
</p>
|
||||
<div class="news_content">
|
||||
<div><em>{{ news.summary|markdown }}</em></div>
|
||||
@ -51,7 +51,7 @@
|
||||
{% if news.moderator %}
|
||||
<p>{% trans %}Moderator: {% endtrans %}{{ user_profile_link(news.moderator) }}</p>
|
||||
{% elif user.is_com_admin %}
|
||||
<p> <a href="{{ url('com:news_moderate', news_id=news.id) }}">{% trans %}Moderate{% endtrans %}</a></p>
|
||||
<p> <a href="{{ url('com:news_moderate', news_id=news.id) }}">{% trans %}Publish{% endtrans %}</a></p>
|
||||
{% endif %}
|
||||
{% if user.can_edit(news) %}
|
||||
<p> <a href="{{ url('com:news_edit', news_id=news.id) }}">{% trans %}Edit (will be moderated again){% endtrans %}</a></p>
|
||||
|
@ -80,9 +80,9 @@
|
||||
</fieldset>
|
||||
{% if user.is_root or user.is_com_admin %}
|
||||
<fieldset>
|
||||
{{ form.auto_moderate.errors }}
|
||||
{{ form.auto_moderate }}
|
||||
{{ form.auto_moderate.label_tag() }}
|
||||
{{ form.auto_publish.errors }}
|
||||
{{ form.auto_publish }}
|
||||
{{ form.auto_publish.label_tag() }}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
<p><input type="submit" value="{% trans %}Save{% endtrans %}" class="btn btn-blue"/></p>
|
||||
|
@ -44,7 +44,7 @@
|
||||
<em>{% trans %}Nothing to come...{% endtrans %}</em>
|
||||
</div>
|
||||
{% else %}
|
||||
{% for day, dates_group in news_dates %}
|
||||
{% for day, dates_group in news_dates.items() %}
|
||||
<div class="news_events_group">
|
||||
<div class="news_events_group_date">
|
||||
<div>
|
||||
@ -57,19 +57,17 @@
|
||||
{% for date in dates_group %}
|
||||
<article
|
||||
class="news_event"
|
||||
{%- if not date.news.is_moderated -%}
|
||||
{%- if not date.news.is_published -%}
|
||||
x-data="{newsState: AlertState.PENDING}"
|
||||
{% else %}
|
||||
x-data="{newsState: AlertState.DISPLAYED}"
|
||||
{%- endif -%}
|
||||
>
|
||||
{% if not date.news.is_moderated %}
|
||||
{# if a non moderated news is in the object list,
|
||||
the logged user is either an admin or the news author #}
|
||||
{{ news_moderation_alert(date.news, user, "newsState") }}
|
||||
{% endif %}
|
||||
{# if a non published news is in the object list,
|
||||
the logged user is either an admin or the news author #}
|
||||
{{ news_moderation_alert(date.news, user, "newsState") }}
|
||||
<div
|
||||
{% if not date.news.is_moderated -%}
|
||||
x-show="newsState !== AlertState.DELETED"
|
||||
{%- endif -%}
|
||||
x-show="newsState !== AlertState.DELETED"
|
||||
>
|
||||
<header class="row gap">
|
||||
{% if date.news.club.logo %}
|
||||
@ -86,9 +84,11 @@
|
||||
<a href="{{ date.news.club.get_absolute_url() }}">{{ date.news.club }}</a>
|
||||
<div class="news_date">
|
||||
<time datetime="{{ date.start_date.isoformat(timespec="seconds") }}">
|
||||
{{ date.start_date|localtime|date(DATETIME_FORMAT) }}
|
||||
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}
|
||||
</time> -
|
||||
<time datetime="{{ date.end_date.isoformat(timespec="seconds") }}">
|
||||
{{ date.end_date|localtime|date(DATETIME_FORMAT) }}
|
||||
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}
|
||||
</time>
|
||||
</div>
|
||||
@ -120,9 +120,9 @@
|
||||
<template x-for="newsDate in newsList" :key="newsDate.id">
|
||||
<article
|
||||
class="news_event"
|
||||
x-data="{ newsState: newsDate.news.is_moderated ? AlertState.MODERATED : AlertState.PENDING }"
|
||||
x-data="{ newsState: newsDate.news.is_published ? AlertState.PUBLISHED : AlertState.PENDING }"
|
||||
>
|
||||
<template x-if="!newsDate.news.is_moderated">
|
||||
<template x-if="!newsDate.news.is_published">
|
||||
{{ news_moderation_alert("newsDate.news.id", user, "newsState") }}
|
||||
</template>
|
||||
<div x-show="newsState !== AlertState.DELETED">
|
||||
@ -179,7 +179,23 @@
|
||||
{% 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
|
||||
x-data
|
||||
x-ref="calendar"
|
||||
@news-moderated.window="
|
||||
if ($event.target !== $refs.calendar){
|
||||
// Avoid triggering a refresh with a dispatch
|
||||
// from the calendar itself
|
||||
$refs.calendar.refreshEvents($event);
|
||||
}
|
||||
"
|
||||
@calendar-delete="$dispatch('news-moderated', {newsId: $event.detail.id, state: AlertState.DELETED})"
|
||||
@calendar-unpublish="$dispatch('news-moderated', {newsId: $event.detail.id, state: AlertState.PENDING})"
|
||||
@calendar-publish="$dispatch('news-moderated', {newsId: $event.detail.id, state: AlertState.PUBLISHED})"
|
||||
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">
|
||||
|
@ -129,14 +129,14 @@ class TestInternalCalendar:
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestModerateNews:
|
||||
@pytest.mark.parametrize("news_is_moderated", [True, False])
|
||||
def test_moderation_ok(self, client: Client, news_is_moderated: bool): # noqa FBT
|
||||
@pytest.mark.parametrize("news_is_published", [True, False])
|
||||
def test_moderation_ok(self, client: Client, news_is_published: bool): # noqa FBT
|
||||
user = baker.make(
|
||||
User, user_permissions=[Permission.objects.get(codename="moderate_news")]
|
||||
)
|
||||
# The API call should work even if the news is initially moderated.
|
||||
# In the latter case, the result should be a noop, rather than an error.
|
||||
news = baker.make(News, is_moderated=news_is_moderated)
|
||||
news = baker.make(News, is_published=news_is_published)
|
||||
initial_moderator = news.moderator
|
||||
client.force_login(user)
|
||||
response = client.patch(
|
||||
@ -147,22 +147,22 @@ class TestModerateNews:
|
||||
# If it was already moderated, it should be a no-op, but not an error
|
||||
assert response.status_code == 200
|
||||
news.refresh_from_db()
|
||||
assert news.is_moderated
|
||||
if not news_is_moderated:
|
||||
assert news.is_published
|
||||
if not news_is_published:
|
||||
assert news.moderator == user
|
||||
else:
|
||||
assert news.moderator == initial_moderator
|
||||
|
||||
def test_moderation_forbidden(self, client: Client):
|
||||
user = baker.make(User)
|
||||
news = baker.make(News, is_moderated=False)
|
||||
news = baker.make(News, is_published=False)
|
||||
client.force_login(user)
|
||||
response = client.patch(
|
||||
reverse("api:moderate_news", kwargs={"news_id": news.id})
|
||||
)
|
||||
assert response.status_code == 403
|
||||
news.refresh_from_db()
|
||||
assert not news.is_moderated
|
||||
assert not news.is_published
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ -203,7 +203,7 @@ class TestFetchNewsDates(TestCase):
|
||||
value=now() + timedelta(hours=2), increment_by=timedelta(days=1)
|
||||
),
|
||||
news=iter(
|
||||
baker.make(News, is_moderated=True, _quantity=5, _bulk_create=True)
|
||||
baker.make(News, is_published=True, _quantity=5, _bulk_create=True)
|
||||
),
|
||||
)
|
||||
cls.dates.append(
|
||||
@ -211,7 +211,7 @@ class TestFetchNewsDates(TestCase):
|
||||
NewsDate,
|
||||
start_date=now() + timedelta(days=2, hours=1),
|
||||
end_date=now() + timedelta(days=2, hours=5),
|
||||
news=baker.make(News, is_moderated=True),
|
||||
news=baker.make(News, is_published=True),
|
||||
)
|
||||
)
|
||||
cls.dates.sort(key=lambda d: d.start_date)
|
||||
|
@ -18,7 +18,7 @@ class TestNewsViewableBy(TestCase):
|
||||
cls.news = baker.make(
|
||||
News,
|
||||
author=itertools.cycle(cls.users),
|
||||
is_moderated=iter([True, True, True, False, False, False]),
|
||||
is_published=iter([True, True, True, False, False, False]),
|
||||
_quantity=6,
|
||||
_bulk_create=True,
|
||||
)
|
||||
|
@ -168,7 +168,7 @@ class TestNews(TestCase):
|
||||
assert not self.new.can_be_viewed_by(self.sli)
|
||||
assert not self.new.can_be_viewed_by(self.anonymous)
|
||||
|
||||
self.new.is_moderated = True
|
||||
self.new.is_published = True
|
||||
self.new.save()
|
||||
assert self.new.can_be_viewed_by(self.com_admin)
|
||||
assert self.new.can_be_viewed_by(self.sli)
|
||||
@ -258,7 +258,7 @@ class TestNewsCreation(TestCase):
|
||||
created = News.objects.order_by("id").last()
|
||||
assertRedirects(response, created.get_absolute_url())
|
||||
assert created.title == "Test news"
|
||||
assert not created.is_moderated
|
||||
assert not created.is_published
|
||||
dates = list(created.dates.values("start_date", "end_date"))
|
||||
assert dates == [{"start_date": self.start, "end_date": self.end}]
|
||||
|
||||
@ -281,7 +281,7 @@ class TestNewsCreation(TestCase):
|
||||
]
|
||||
|
||||
def test_edit_news(self):
|
||||
news = baker.make(News, author=self.user, is_moderated=True)
|
||||
news = baker.make(News, author=self.user, is_published=True)
|
||||
baker.make(
|
||||
NewsDate,
|
||||
news=news,
|
||||
@ -296,7 +296,7 @@ class TestNewsCreation(TestCase):
|
||||
created = News.objects.order_by("id").last()
|
||||
assertRedirects(response, created.get_absolute_url())
|
||||
assert created.title == "Test news"
|
||||
assert not created.is_moderated
|
||||
assert not created.is_published
|
||||
dates = list(created.dates.values("start_date", "end_date"))
|
||||
assert dates == [{"start_date": self.start, "end_date": self.end}]
|
||||
|
||||
|
36
com/views.py
36
com/views.py
@ -217,9 +217,9 @@ class NewsModerateView(PermissionRequiredMixin, DetailView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if "remove" in request.GET:
|
||||
self.object.is_moderated = False
|
||||
self.object.is_published = False
|
||||
else:
|
||||
self.object.is_moderated = True
|
||||
self.object.is_published = True
|
||||
self.object.moderator = request.user
|
||||
self.object.save()
|
||||
if "next" in self.request.GET:
|
||||
@ -253,7 +253,7 @@ class NewsListView(TemplateView):
|
||||
key=lambda u: u.date_of_birth.year,
|
||||
)
|
||||
|
||||
def get_last_day(self) -> date:
|
||||
def get_last_day(self) -> date | None:
|
||||
"""Get the last day when news will be displayed
|
||||
|
||||
The returned day is the third one where something happen.
|
||||
@ -261,31 +261,37 @@ class NewsListView(TemplateView):
|
||||
D on 20/03, E on 21/03 and F on 22/03 ;
|
||||
then the result is 20/03.
|
||||
"""
|
||||
return list(
|
||||
dates = list(
|
||||
NewsDate.objects.filter(end_date__gt=now())
|
||||
.order_by("start_date")
|
||||
.values_list("start_date__date", flat=True)
|
||||
.distinct()[:4]
|
||||
)[-1]
|
||||
)
|
||||
return dates[-1] if len(dates) > 0 else None
|
||||
|
||||
def get_news_dates(self, until: date):
|
||||
def get_news_dates(self, until: date) -> dict[date, list[date]]:
|
||||
"""Return the event dates to display.
|
||||
|
||||
The selected events are the ones that happens between
|
||||
right now and the given day (included).
|
||||
"""
|
||||
return itertools.groupby(
|
||||
NewsDate.objects.viewable_by(self.request.user)
|
||||
.filter(end_date__gt=now(), start_date__date__lte=until)
|
||||
.order_by("start_date")
|
||||
.select_related("news", "news__club"),
|
||||
key=lambda d: d.start_date.date(),
|
||||
)
|
||||
return {
|
||||
date: list(dates)
|
||||
for date, dates in itertools.groupby(
|
||||
NewsDate.objects.viewable_by(self.request.user)
|
||||
.filter(end_date__gt=now(), start_date__date__lte=until)
|
||||
.order_by("start_date")
|
||||
.select_related("news", "news__club"),
|
||||
key=lambda d: d.start_date.date(),
|
||||
)
|
||||
}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
last_day = self.get_last_day()
|
||||
return super().get_context_data(**kwargs) | {
|
||||
"news_dates": self.get_news_dates(until=last_day),
|
||||
"news_dates": self.get_news_dates(until=last_day)
|
||||
if last_day is not None
|
||||
else {},
|
||||
"birthdays": self.get_birthdays(),
|
||||
"last_day": last_day,
|
||||
}
|
||||
@ -309,7 +315,7 @@ class NewsFeed(Feed):
|
||||
def items(self):
|
||||
return (
|
||||
NewsDate.objects.filter(
|
||||
news__is_moderated=True,
|
||||
news__is_published=True,
|
||||
end_date__gte=timezone.now() - (relativedelta(months=6)),
|
||||
)
|
||||
.select_related("news", "news__author")
|
||||
|
@ -690,7 +690,7 @@ Welcome to the wiki page!
|
||||
content="Glou glou glou glou glou glou glou",
|
||||
club=bar_club,
|
||||
author=subscriber,
|
||||
is_moderated=True,
|
||||
is_published=True,
|
||||
moderator=skia,
|
||||
)
|
||||
news_dates.append(
|
||||
@ -704,12 +704,11 @@ Welcome to the wiki page!
|
||||
title="Repas barman",
|
||||
summary="Enjoy la fin du semestre!",
|
||||
content=(
|
||||
"Viens donc t'enjailler avec les autres barmans aux "
|
||||
"frais du BdF! \\o/"
|
||||
"Viens donc t'enjailler avec les autres barmans aux frais du BdF! \\o/"
|
||||
),
|
||||
club=bar_club,
|
||||
author=subscriber,
|
||||
is_moderated=True,
|
||||
is_published=True,
|
||||
moderator=skia,
|
||||
)
|
||||
news_dates.append(
|
||||
@ -725,7 +724,7 @@ Welcome to the wiki page!
|
||||
content="Fô viendre mangey d'la bonne fondue!",
|
||||
club=bar_club,
|
||||
author=subscriber,
|
||||
is_moderated=True,
|
||||
is_published=True,
|
||||
moderator=skia,
|
||||
)
|
||||
news_dates.append(
|
||||
@ -741,7 +740,7 @@ Welcome to the wiki page!
|
||||
content="Viens faire la fête avec tout plein de gens!",
|
||||
club=bar_club,
|
||||
author=subscriber,
|
||||
is_moderated=True,
|
||||
is_published=True,
|
||||
moderator=skia,
|
||||
)
|
||||
news_dates.append(
|
||||
@ -759,7 +758,7 @@ Welcome to the wiki page!
|
||||
"t'amuser le Vendredi soir!",
|
||||
club=troll,
|
||||
author=subscriber,
|
||||
is_moderated=True,
|
||||
is_published=True,
|
||||
moderator=skia,
|
||||
)
|
||||
news_dates.extend(
|
||||
|
16
core/migrations/0044_alter_userban_options.py
Normal file
16
core/migrations/0044_alter_userban_options.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Generated by Django 4.2.17 on 2025-02-25 14:45
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0043_bangroup_alter_group_description_alter_user_groups_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="userban",
|
||||
options={"verbose_name": "user ban", "verbose_name_plural": "user bans"},
|
||||
),
|
||||
]
|
@ -272,6 +272,20 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-orange {
|
||||
background-color: #fcbf81;
|
||||
color: black;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background-color: darken(#fcbf81, 15%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: lighten(#fcbf81, 15%);
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.btn-no-text) {
|
||||
i {
|
||||
margin-right: 4px;
|
||||
|
@ -6,7 +6,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-25 11:04+0100\n"
|
||||
"POT-Creation-Date: 2025-02-25 16:38+0100\n"
|
||||
"PO-Revision-Date: 2016-07-18\n"
|
||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||
@ -812,7 +812,7 @@ msgstr "Nouvelle mailing liste"
|
||||
msgid "Subscribe"
|
||||
msgstr "S'abonner"
|
||||
|
||||
#: club/forms.py com/templates/com/news_admin_list.jinja
|
||||
#: club/forms.py
|
||||
msgid "Remove"
|
||||
msgstr "Retirer"
|
||||
|
||||
@ -1296,8 +1296,8 @@ msgstr ""
|
||||
"Combien de fois l'événement doit-il se répéter (en incluant la première fois)"
|
||||
|
||||
#: com/forms.py
|
||||
msgid "Automoderation"
|
||||
msgstr "Automodération"
|
||||
msgid "Auto publication"
|
||||
msgstr "Publication automatique"
|
||||
|
||||
#: com/models.py
|
||||
msgid "alert message"
|
||||
@ -1344,6 +1344,10 @@ msgstr "Le club qui organise l'évènement."
|
||||
msgid "author"
|
||||
msgstr "auteur"
|
||||
|
||||
#: com/models.py
|
||||
msgid "is published"
|
||||
msgstr "est publié"
|
||||
|
||||
#: com/models.py
|
||||
msgid "news"
|
||||
msgstr "nouvelle"
|
||||
@ -1409,34 +1413,31 @@ msgid "Begin date should be before end date"
|
||||
msgstr "La date de début doit être avant celle de fin"
|
||||
|
||||
#: com/templates/com/macros.jinja
|
||||
msgid "Waiting moderation"
|
||||
msgstr "En attente de modération"
|
||||
msgid "Waiting publication"
|
||||
msgstr "En attente de publication"
|
||||
|
||||
#: com/templates/com/macros.jinja
|
||||
msgid ""
|
||||
"This news isn't moderated and is visible only by its author and the "
|
||||
"This news isn't published and is visible only by its author and the "
|
||||
"communication admins."
|
||||
msgstr ""
|
||||
"Cette nouvelle n'est pas modérée et n'est visible que par son auteur et les "
|
||||
"Cette nouvelle n'est pas publiée et n'est visible que par son auteur et les "
|
||||
"admins communication."
|
||||
|
||||
#: com/templates/com/macros.jinja
|
||||
msgid "It will stay hidden for other users until it has been moderated."
|
||||
msgid "It will stay hidden for other users until it has been published."
|
||||
msgstr ""
|
||||
"Elle sera cachée pour les autres utilisateurs tant qu'elle ne sera pas "
|
||||
"modérée."
|
||||
"publiée."
|
||||
|
||||
#: com/templates/com/macros.jinja com/templates/com/mailing_admin.jinja
|
||||
#: com/templates/com/news_admin_list.jinja com/templates/com/news_detail.jinja
|
||||
#: core/templates/core/file_detail.jinja
|
||||
#: core/templates/core/file_moderation.jinja sas/templates/sas/moderation.jinja
|
||||
#: sas/templates/sas/picture.jinja
|
||||
msgid "Moderate"
|
||||
msgstr "Modérer"
|
||||
#: com/templates/com/macros.jinja com/templates/com/news_admin_list.jinja
|
||||
#: com/templates/com/news_detail.jinja
|
||||
msgid "Publish"
|
||||
msgstr "Publier"
|
||||
|
||||
#: com/templates/com/macros.jinja
|
||||
msgid "News moderated"
|
||||
msgstr "Nouvelle modérée"
|
||||
msgid "News published"
|
||||
msgstr "Nouvelle publiée"
|
||||
|
||||
#: com/templates/com/macros.jinja
|
||||
msgid "News deleted"
|
||||
@ -1447,6 +1448,12 @@ msgstr "Nouvelle supprimée"
|
||||
msgid "Mailing lists administration"
|
||||
msgstr "Administration des mailing listes"
|
||||
|
||||
#: com/templates/com/mailing_admin.jinja core/templates/core/file_detail.jinja
|
||||
#: core/templates/core/file_moderation.jinja sas/templates/sas/moderation.jinja
|
||||
#: sas/templates/sas/picture.jinja
|
||||
msgid "Moderate"
|
||||
msgstr "Modérer"
|
||||
|
||||
#: com/templates/com/mailing_admin.jinja
|
||||
#, python-format
|
||||
msgid "Moderated by %(user)s"
|
||||
@ -1514,6 +1521,10 @@ msgstr "Modérateur"
|
||||
msgid "Dates"
|
||||
msgstr "Dates"
|
||||
|
||||
#: com/templates/com/news_admin_list.jinja
|
||||
msgid "Unpublish"
|
||||
msgstr "Dépublier"
|
||||
|
||||
#: com/templates/com/news_admin_list.jinja
|
||||
msgid "Weeklies to moderate"
|
||||
msgstr "Nouvelles hebdomadaires à modérer"
|
||||
@ -6031,13 +6042,3 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée."
|
||||
#, python-format
|
||||
msgid "Maximum characters: %(max_length)s"
|
||||
msgstr "Nombre de caractères max: %(max_length)s"
|
||||
|
||||
#, python-format
|
||||
#~ msgid ""
|
||||
#~ "This event will take place every week for %%s weeks. If you moderate or "
|
||||
#~ "delete this event, it will also be moderated (or deleted) for the "
|
||||
#~ "following weeks."
|
||||
#~ msgstr ""
|
||||
#~ "Cet événement se déroulera chaque semaine pendant %%s semaines. Si vous "
|
||||
#~ "modérez ou supprimez cet événement, il sera également modéré (ou "
|
||||
#~ "supprimé) pour les semaines suivantes."
|
||||
|
@ -7,7 +7,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-25 11:05+0100\n"
|
||||
"POT-Creation-Date: 2025-02-25 16:10+0100\n"
|
||||
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
|
||||
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
|
||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||
@ -21,15 +21,25 @@ msgstr ""
|
||||
msgid "More info"
|
||||
msgstr "Plus d'informations"
|
||||
|
||||
#: com/static/bundled/com/components/ics-calendar-index.ts
|
||||
msgid "Publish"
|
||||
msgstr "Publier"
|
||||
|
||||
#: com/static/bundled/com/components/ics-calendar-index.ts
|
||||
msgid "Unpublish"
|
||||
msgstr "Dépublier"
|
||||
|
||||
#: com/static/bundled/com/components/ics-calendar-index.ts
|
||||
msgid "Delete"
|
||||
msgstr "Supprimer"
|
||||
|
||||
#: com/static/bundled/com/components/moderation-alert-index.ts
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"This event will take place every week for %s weeks. If you moderate or "
|
||||
"delete this event, it will also be moderated (or deleted) for the following "
|
||||
"weeks."
|
||||
"This event will take place every week for %s weeks. If you publish or delete "
|
||||
"this event, it will also be published (or deleted) for the following weeks."
|
||||
msgstr ""
|
||||
"Cet événement se déroulera chaque semaine pendant %s semaines. Si vous "
|
||||
"modérez ou supprimez cet événement, il sera également modéré (ou supprimé) "
|
||||
"publiez ou supprimez cet événement, il sera également publié (ou supprimé) "
|
||||
"pour les semaines suivantes."
|
||||
|
||||
#: core/static/bundled/core/components/ajax-select-base.ts
|
||||
|
@ -12,6 +12,7 @@
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["jquery", "alpinejs"],
|
||||
"lib": ["es7"],
|
||||
"paths": {
|
||||
"#openapi": ["./staticfiles/generated/openapi/index.ts"],
|
||||
"#core:*": ["./core/static/bundled/*"],
|
||||
|
Loading…
x
Reference in New Issue
Block a user