mirror of
https://github.com/ae-utbm/sith.git
synced 2025-02-27 09:57:09 +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 typing import Literal
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import Http404
|
from django.http import Http404, HttpResponse
|
||||||
from ninja import Query
|
from ninja import Query
|
||||||
from ninja_extra import ControllerBase, api_controller, paginate, route
|
from ninja_extra import ControllerBase, api_controller, paginate, route
|
||||||
from ninja_extra.pagination import PageNumberPaginationExtra
|
from ninja_extra.pagination import PageNumberPaginationExtra
|
||||||
|
from ninja_extra.permissions import IsAuthenticated
|
||||||
from ninja_extra.schemas import PaginatedResponseSchema
|
from ninja_extra.schemas import PaginatedResponseSchema
|
||||||
|
|
||||||
from com.calendar import IcsCalendar
|
from com.calendar import IcsCalendar
|
||||||
@ -38,18 +39,41 @@ class CalendarController(ControllerBase):
|
|||||||
def calendar_internal(self):
|
def calendar_internal(self):
|
||||||
return send_raw_file(IcsCalendar.get_internal())
|
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")
|
@api_controller("/news")
|
||||||
class NewsController(ControllerBase):
|
class NewsController(ControllerBase):
|
||||||
@route.patch(
|
@route.patch(
|
||||||
"/{int:news_id}/moderate",
|
"/{int:news_id}/publish",
|
||||||
permissions=[HasPerm("com.moderate_news")],
|
permissions=[HasPerm("com.moderate_news")],
|
||||||
url_name="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)
|
news = self.get_object_or_exception(News, id=news_id)
|
||||||
if not news.is_moderated:
|
if not news.is_published:
|
||||||
news.is_moderated = True
|
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.moderator = self.context.request.user
|
||||||
news.save()
|
news.save()
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ from typing import final
|
|||||||
import requests
|
import requests
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db.models import F, QuerySet
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from ical.calendar import Calendar
|
from ical.calendar import Calendar
|
||||||
@ -12,6 +13,7 @@ from ical.calendar_stream import IcsCalendarStream
|
|||||||
from ical.event import Event
|
from ical.event import Event
|
||||||
|
|
||||||
from com.models import NewsDate
|
from com.models import NewsDate
|
||||||
|
from core.models import User
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@ -55,21 +57,38 @@ class IcsCalendar:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def make_internal(cls) -> Path:
|
def make_internal(cls) -> Path:
|
||||||
# Updated through a post_save signal on News in com.signals
|
# 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()
|
calendar = Calendar()
|
||||||
for news_date in NewsDate.objects.filter(
|
for news_date in queryset.annotate(news_title=F("news__title")):
|
||||||
news__is_moderated=True,
|
|
||||||
end_date__gte=timezone.now() - (relativedelta(months=6)),
|
|
||||||
).prefetch_related("news"):
|
|
||||||
event = Event(
|
event = Event(
|
||||||
summary=news_date.news.title,
|
summary=news_date.news_title,
|
||||||
start=news_date.start_date,
|
start=news_date.start_date,
|
||||||
end=news_date.end_date,
|
end=news_date.end_date,
|
||||||
url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}),
|
url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}),
|
||||||
)
|
)
|
||||||
calendar.events.append(event)
|
calendar.events.append(event)
|
||||||
|
|
||||||
# Create a file so we can offload the download to the reverse proxy if available
|
return IcsCalendarStream.calendar_to_ics(calendar).encode("utf-8")
|
||||||
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
|
|
||||||
|
10
com/forms.py
10
com/forms.py
@ -147,8 +147,8 @@ class NewsForm(forms.ModelForm):
|
|||||||
"content": MarkdownInput,
|
"content": MarkdownInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
auto_moderate = forms.BooleanField(
|
auto_publish = forms.BooleanField(
|
||||||
label=_("Automoderation"),
|
label=_("Auto publication"),
|
||||||
widget=CheckboxInput(attrs={"class": "switch"}),
|
widget=CheckboxInput(attrs={"class": "switch"}),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
@ -182,12 +182,12 @@ class NewsForm(forms.ModelForm):
|
|||||||
def save(self, commit: bool = True): # noqa FBT001
|
def save(self, commit: bool = True): # noqa FBT001
|
||||||
self.instance.author = self.author
|
self.instance.author = self.author
|
||||||
if (self.author.is_com_admin or self.author.is_root) and (
|
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
|
self.instance.moderator = self.author
|
||||||
else:
|
else:
|
||||||
self.instance.is_moderated = False
|
self.instance.is_published = False
|
||||||
created_news = super().save(commit=commit)
|
created_news = super().save(commit=commit)
|
||||||
self.date_form.save(commit=commit, news=created_news)
|
self.date_form.save(commit=commit, news=created_news)
|
||||||
return 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):
|
class NewsQuerySet(models.QuerySet):
|
||||||
def moderated(self) -> Self:
|
def moderated(self) -> Self:
|
||||||
return self.filter(is_moderated=True)
|
return self.filter(is_published=True)
|
||||||
|
|
||||||
def viewable_by(self, user: User) -> Self:
|
def viewable_by(self, user: User) -> Self:
|
||||||
"""Filter news that the given user can view.
|
"""Filter news that the given user can view.
|
||||||
@ -68,7 +68,7 @@ class NewsQuerySet(models.QuerySet):
|
|||||||
"""
|
"""
|
||||||
if user.has_perm("com.view_unmoderated_news"):
|
if user.has_perm("com.view_unmoderated_news"):
|
||||||
return self
|
return self
|
||||||
q_filter = Q(is_moderated=True)
|
q_filter = Q(is_published=True)
|
||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
q_filter |= Q(author_id=user.id)
|
q_filter |= Q(author_id=user.id)
|
||||||
return self.filter(q_filter)
|
return self.filter(q_filter)
|
||||||
@ -104,7 +104,7 @@ class News(models.Model):
|
|||||||
verbose_name=_("author"),
|
verbose_name=_("author"),
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
is_moderated = models.BooleanField(_("is moderated"), default=False)
|
is_published = models.BooleanField(_("is published"), default=False)
|
||||||
moderator = models.ForeignKey(
|
moderator = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
related_name="moderated_news",
|
related_name="moderated_news",
|
||||||
@ -127,7 +127,7 @@ class News(models.Model):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if self.is_moderated:
|
if self.is_published:
|
||||||
return
|
return
|
||||||
for user in User.objects.filter(
|
for user in User.objects.filter(
|
||||||
groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID]
|
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):
|
def can_be_viewed_by(self, user: User):
|
||||||
return (
|
return (
|
||||||
self.is_moderated
|
self.is_published
|
||||||
or user.has_perm("com.view_unmoderated_news")
|
or user.has_perm("com.view_unmoderated_news")
|
||||||
or (user.is_authenticated and self.author_id == user.id)
|
or (user.is_authenticated and self.author_id == user.id)
|
||||||
)
|
)
|
||||||
@ -162,7 +162,7 @@ class News(models.Model):
|
|||||||
|
|
||||||
def news_notification_callback(notif):
|
def news_notification_callback(notif):
|
||||||
count = News.objects.filter(
|
count = News.objects.filter(
|
||||||
dates__start_date__gt=timezone.now(), is_moderated=False
|
dates__start_date__gt=timezone.now(), is_published=False
|
||||||
).count()
|
).count()
|
||||||
if count:
|
if count:
|
||||||
notif.viewed = False
|
notif.viewed = False
|
||||||
@ -182,7 +182,7 @@ class NewsDateQuerySet(models.QuerySet):
|
|||||||
"""
|
"""
|
||||||
if user.has_perm("com.view_unmoderated_news"):
|
if user.has_perm("com.view_unmoderated_news"):
|
||||||
return self
|
return self
|
||||||
q_filter = Q(news__is_moderated=True)
|
q_filter = Q(news__is_published=True)
|
||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
q_filter |= Q(news__author_id=user.id)
|
q_filter |= Q(news__author_id=user.id)
|
||||||
return self.filter(q_filter)
|
return self.filter(q_filter)
|
||||||
@ -337,7 +337,7 @@ class Screen(models.Model):
|
|||||||
|
|
||||||
def active_posters(self):
|
def active_posters(self):
|
||||||
now = timezone.now()
|
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)
|
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")
|
after: datetime | None = Field(None, q="start_date__gt")
|
||||||
club_id: int | None = Field(None, q="news__club_id")
|
club_id: int | None = Field(None, q="news__club_id")
|
||||||
news_id: int | None = None
|
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")
|
title: str | None = Field(None, q="news__title__icontains")
|
||||||
|
|
||||||
|
|
||||||
class NewsSchema(ModelSchema):
|
class NewsSchema(ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = News
|
model = News
|
||||||
fields = ["id", "title", "summary", "is_moderated"]
|
fields = ["id", "title", "summary", "is_published"]
|
||||||
|
|
||||||
club: ClubProfileSchema
|
club: ClubProfileSchema
|
||||||
url: str
|
url: str
|
||||||
|
@ -7,20 +7,33 @@ import frLocale from "@fullcalendar/core/locales/fr";
|
|||||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||||
import iCalendarPlugin from "@fullcalendar/icalendar";
|
import iCalendarPlugin from "@fullcalendar/icalendar";
|
||||||
import listPlugin from "@fullcalendar/list";
|
import listPlugin from "@fullcalendar/list";
|
||||||
import { calendarCalendarExternal, calendarCalendarInternal } from "#openapi";
|
import {
|
||||||
|
calendarCalendarExternal,
|
||||||
|
calendarCalendarInternal,
|
||||||
|
calendarCalendarUnpublished,
|
||||||
|
newsDeleteNews,
|
||||||
|
newsPublishNews,
|
||||||
|
newsUnpublishNews,
|
||||||
|
} from "#openapi";
|
||||||
|
|
||||||
@registerComponent("ics-calendar")
|
@registerComponent("ics-calendar")
|
||||||
export class IcsCalendar extends inheritHtmlElement("div") {
|
export class IcsCalendar extends inheritHtmlElement("div") {
|
||||||
static observedAttributes = ["locale"];
|
static observedAttributes = ["locale", "can_moderate", "can_delete"];
|
||||||
private calendar: Calendar;
|
private calendar: Calendar;
|
||||||
private locale = "en";
|
private locale = "en";
|
||||||
|
private canModerate = false;
|
||||||
|
private canDelete = false;
|
||||||
|
|
||||||
attributeChangedCallback(name: string, _oldValue?: string, newValue?: string) {
|
attributeChangedCallback(name: string, _oldValue?: string, newValue?: string) {
|
||||||
if (name !== "locale") {
|
if (name === "locale") {
|
||||||
return;
|
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() {
|
isMobile() {
|
||||||
@ -54,6 +67,104 @@ export class IcsCalendar extends inheritHtmlElement("div") {
|
|||||||
}).format(date);
|
}).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) {
|
createEventDetailPopup(event: EventClickArg) {
|
||||||
// Delete previous popup
|
// Delete previous popup
|
||||||
const oldPopup = document.getElementById("event-details");
|
const oldPopup = document.getElementById("event-details");
|
||||||
@ -112,6 +223,47 @@ export class IcsCalendar extends inheritHtmlElement("div") {
|
|||||||
return makePopupInfo(url, "fa-solid fa-link");
|
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
|
// Create new popup
|
||||||
const popup = document.createElement("div");
|
const popup = document.createElement("div");
|
||||||
const popupContainer = document.createElement("div");
|
const popupContainer = document.createElement("div");
|
||||||
@ -131,6 +283,11 @@ export class IcsCalendar extends inheritHtmlElement("div") {
|
|||||||
popupContainer.appendChild(url);
|
popupContainer.appendChild(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tools = makePopupTools(event.event);
|
||||||
|
if (tools !== null) {
|
||||||
|
popupContainer.appendChild(tools);
|
||||||
|
}
|
||||||
|
|
||||||
popup.appendChild(popupContainer);
|
popup.appendChild(popupContainer);
|
||||||
|
|
||||||
// We can't just add the element relative to the one we want to appear under
|
// 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() {
|
async connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
const cacheInvalidate = `?invalidate=${Date.now()}`;
|
|
||||||
this.calendar = new Calendar(this.node, {
|
this.calendar = new Calendar(this.node, {
|
||||||
plugins: [dayGridPlugin, iCalendarPlugin, listPlugin],
|
plugins: [dayGridPlugin, iCalendarPlugin, listPlugin],
|
||||||
locales: [frLocale, enLocale],
|
locales: [frLocale, enLocale],
|
||||||
@ -160,16 +316,7 @@ export class IcsCalendar extends inheritHtmlElement("div") {
|
|||||||
locale: this.locale,
|
locale: this.locale,
|
||||||
initialView: this.currentView(),
|
initialView: this.currentView(),
|
||||||
headerToolbar: this.currentToolbar(),
|
headerToolbar: this.currentToolbar(),
|
||||||
eventSources: [
|
eventSources: await this.getEventSources(),
|
||||||
{
|
|
||||||
url: `${await makeUrl(calendarCalendarInternal)}${cacheInvalidate}`,
|
|
||||||
format: "ics",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: `${await makeUrl(calendarCalendarExternal)}${cacheInvalidate}`,
|
|
||||||
format: "ics",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
windowResize: () => {
|
windowResize: () => {
|
||||||
this.calendar.changeView(this.currentView());
|
this.calendar.changeView(this.currentView());
|
||||||
this.calendar.setOption("headerToolbar", this.currentToolbar());
|
this.calendar.setOption("headerToolbar", this.currentToolbar());
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { exportToHtml } from "#core:utils/globals";
|
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,
|
// This will be used in jinja templates,
|
||||||
// so we cannot use real enums as those are purely an abstraction of Typescript
|
// 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
|
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
|
||||||
PENDING: 1,
|
PENDING: 1,
|
||||||
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
|
// 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
|
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
|
||||||
DELETED: 3,
|
DELETED: 3,
|
||||||
|
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
|
||||||
|
DISPLAYED: 4, // When published at page generation
|
||||||
};
|
};
|
||||||
exportToHtml("AlertState", AlertState);
|
exportToHtml("AlertState", AlertState);
|
||||||
|
|
||||||
@ -19,11 +21,11 @@ document.addEventListener("alpine:init", () => {
|
|||||||
newsId: newsId as number,
|
newsId: newsId as number,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
async moderateNews() {
|
async publishNews() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
// biome-ignore lint/style/useNamingConvention: api is snake case
|
// biome-ignore lint/style/useNamingConvention: api is snake case
|
||||||
await newsModerateNews({ path: { news_id: this.newsId } });
|
await newsPublishNews({ path: { news_id: this.newsId } });
|
||||||
this.state = AlertState.MODERATED;
|
this.state = AlertState.PUBLISHED;
|
||||||
this.$dispatch("news-moderated", { newsId: this.newsId, state: this.state });
|
this.$dispatch("news-moderated", { newsId: this.newsId, state: this.state });
|
||||||
this.loading = false;
|
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
|
* Query the server to know the number of news dates that would be moderated
|
||||||
* if this one is 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.
|
// What we want here is the count attribute of the response.
|
||||||
// We don't care about the actual results,
|
// We don't care about the actual results,
|
||||||
// so we ask for the minimum page size possible.
|
// so we ask for the minimum page size possible.
|
||||||
@ -69,8 +71,8 @@ document.addEventListener("alpine:init", () => {
|
|||||||
return interpolate(
|
return interpolate(
|
||||||
gettext(
|
gettext(
|
||||||
"This event will take place every week for %s weeks. " +
|
"This event will take place every week for %s weeks. " +
|
||||||
"If you moderate or delete this event, " +
|
"If you publish or delete this event, " +
|
||||||
"it will also be moderated (or deleted) for the following weeks.",
|
"it will also be published (or deleted) for the following weeks.",
|
||||||
),
|
),
|
||||||
[nbEvents],
|
[nbEvents],
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% macro news_moderation_alert(news, user, alpineState = None) %}
|
{% macro news_moderation_alert(news, user, alpineState = None) %}
|
||||||
{# An alert to display on top of non moderated news,
|
{# An alert to display on top of unpublished news,
|
||||||
with actions to either moderate or delete them.
|
with actions to either publish or delete them.
|
||||||
|
|
||||||
The current state of the alert is accessible through
|
The current state of the alert is accessible through
|
||||||
the given `alpineState` variable.
|
the given `alpineState` variable.
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
This comes in three flavours :
|
This comes in three flavours :
|
||||||
- You can pass the `News` object itself to the macro.
|
- 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.
|
it will perform an additional db query to know if it is a recurring event.
|
||||||
- You can also give only the news id.
|
- You can also give only the news id.
|
||||||
In this case, a server request will be issued to know
|
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 #}
|
{# the news-moderated is received when a moderation alert is deleted or moderated #}
|
||||||
@news-moderated.window="dispatchModeration($event)"
|
@news-moderated.window="dispatchModeration($event)"
|
||||||
{% if alpineState %}
|
{% if alpineState %}
|
||||||
x-modelable="{{ alpineState }}"
|
x-model="{{ alpineState }}"
|
||||||
x-model="state"
|
x-modelable="state"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
>
|
>
|
||||||
<template x-if="state === AlertState.PENDING">
|
<template x-if="state === AlertState.PENDING">
|
||||||
<div class="alert alert-yellow">
|
<div class="alert alert-yellow">
|
||||||
<div class="alert-main">
|
<div class="alert-main">
|
||||||
<strong>{% trans %}Waiting moderation{% endtrans %}</strong>
|
<strong>{% trans %}Waiting publication{% endtrans %}</strong>
|
||||||
<p>
|
<p>
|
||||||
{% trans trimmed %}
|
{% 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.
|
only by its author and the communication admins.
|
||||||
{% endtrans %}
|
{% endtrans %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{% trans trimmed %}
|
{% 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 %}
|
{% endtrans %}
|
||||||
</p>
|
</p>
|
||||||
{% if user.has_perm("com.moderate_news") %}
|
{% if user.has_perm("com.moderate_news") %}
|
||||||
@ -84,7 +84,7 @@
|
|||||||
<div
|
<div
|
||||||
{% if news is integer or news is string %}
|
{% if news is integer or news is string %}
|
||||||
x-data="{ nbEvents: 0 }"
|
x-data="{ nbEvents: 0 }"
|
||||||
x-init="nbEvents = await nbToModerate()"
|
x-init="nbEvents = await nbToPublish()"
|
||||||
{% else %}
|
{% else %}
|
||||||
x-data="{ nbEvents: {{ news.dates.count() }} }"
|
x-data="{ nbEvents: {{ news.dates.count() }} }"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -101,8 +101,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if user.has_perm("com.moderate_news") %}
|
{% if user.has_perm("com.moderate_news") %}
|
||||||
<span class="alert-aside" :aria-busy="loading">
|
<span class="alert-aside" :aria-busy="loading">
|
||||||
<button class="btn btn-green" @click="moderateNews()" :disabled="loading">
|
<button class="btn btn-green" @click="publishNews()" :disabled="loading">
|
||||||
<i class="fa fa-check"></i> {% trans %}Moderate{% endtrans %}
|
<i class="fa fa-check"></i> {% trans %}Publish{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.has_perm("com.delete_news") %}
|
{% if user.has_perm("com.delete_news") %}
|
||||||
@ -113,9 +113,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="state === AlertState.MODERATED">
|
<template x-if="state === AlertState.PUBLISHED">
|
||||||
<div class="alert alert-green">
|
<div class="alert alert-green">
|
||||||
{% trans %}News moderated{% endtrans %}
|
{% trans %}News published{% endtrans %}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="state === AlertState.DELETED">
|
<template x-if="state === AlertState.DELETED">
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for news in weeklies.filter(is_moderated=True) %}
|
{% for news in weeklies.filter(is_published=True) %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ news.title }}</td>
|
<td>{{ news.title }}</td>
|
||||||
<td>{{ news.summary|markdown }}</td>
|
<td>{{ news.summary|markdown }}</td>
|
||||||
@ -47,7 +47,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td><a href="{{ url('com:news_detail', news_id=news.id) }}">{% trans %}View{% endtrans %}</a>
|
<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_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>
|
<a href="{{ url('com:news_delete', news_id=news.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -67,7 +67,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for news in weeklies.filter(is_moderated=False) %}
|
{% for news in weeklies.filter(is_published=False) %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ news.title }}</td>
|
<td>{{ news.title }}</td>
|
||||||
<td>{{ news.summary|markdown }}</td>
|
<td>{{ news.summary|markdown }}</td>
|
||||||
@ -86,7 +86,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td><a href="{{ url('com:news_detail', news_id=news.id) }}">{% trans %}View{% endtrans %}</a>
|
<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_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>
|
<a href="{{ url('com:news_delete', news_id=news.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -111,7 +111,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for news in events.filter(is_moderated=True) %}
|
{% for news in events.filter(is_published=True) %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ news.title }}</td>
|
<td>{{ news.title }}</td>
|
||||||
<td>{{ news.summary|markdown }}</td>
|
<td>{{ news.summary|markdown }}</td>
|
||||||
@ -124,7 +124,7 @@
|
|||||||
{{ news.dates.all()[0].end_date|localtime|time(DATETIME_FORMAT) }}</td>
|
{{ 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>
|
<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_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>
|
<a href="{{ url('com:news_delete', news_id=news.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -145,7 +145,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for news in events.filter(is_moderated=False) %}
|
{% for news in events.filter(is_published=False) %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ news.title }}</td>
|
<td>{{ news.title }}</td>
|
||||||
<td>{{ news.summary|markdown }}</td>
|
<td>{{ news.summary|markdown }}</td>
|
||||||
@ -157,7 +157,7 @@
|
|||||||
{{ news.dates.all()[0].end_date|localtime|time(DATETIME_FORMAT) }}</td>
|
{{ 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>
|
<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_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>
|
<a href="{{ url('com:news_delete', news_id=news.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
<p><a href="{{ url('com:news_list') }}">{% trans %}Back to news{% endtrans %}</a></p>
|
<p><a href="{{ url('com:news_list') }}">{% trans %}Back to news{% endtrans %}</a></p>
|
||||||
<div x-data="{newsState: AlertState.PENDING}">
|
<div x-data="{newsState: AlertState.PENDING}">
|
||||||
|
|
||||||
{% if not news.is_moderated %}
|
{% if not news.is_published %}
|
||||||
{{ news_moderation_alert(news, user, "newsState") }}
|
{{ news_moderation_alert(news, user, "newsState") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<article id="news_details" x-show="newsState !== AlertState.DELETED">
|
<article id="news_details" x-show="newsState !== AlertState.DELETED">
|
||||||
@ -35,10 +35,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<h4>{{ news.title }}</h4>
|
<h4>{{ news.title }}</h4>
|
||||||
<p class="date">
|
<p class="date">
|
||||||
<span>{{ date.start_date|localtime|date(DATETIME_FORMAT) }}
|
<time datetime="{{ date.start_date.isoformat(timespec="seconds") }}">{{ date.start_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}</time> -
|
||||||
<span>{{ date.end_date|localtime|date(DATETIME_FORMAT) }}
|
<time datetime="{{ date.end_date.isoformat(timespec="seconds") }}">{{ date.end_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}</time>
|
||||||
</p>
|
</p>
|
||||||
<div class="news_content">
|
<div class="news_content">
|
||||||
<div><em>{{ news.summary|markdown }}</em></div>
|
<div><em>{{ news.summary|markdown }}</em></div>
|
||||||
@ -51,7 +51,7 @@
|
|||||||
{% if news.moderator %}
|
{% if news.moderator %}
|
||||||
<p>{% trans %}Moderator: {% endtrans %}{{ user_profile_link(news.moderator) }}</p>
|
<p>{% trans %}Moderator: {% endtrans %}{{ user_profile_link(news.moderator) }}</p>
|
||||||
{% elif user.is_com_admin %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% if user.can_edit(news) %}
|
{% 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>
|
<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>
|
</fieldset>
|
||||||
{% if user.is_root or user.is_com_admin %}
|
{% if user.is_root or user.is_com_admin %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{{ form.auto_moderate.errors }}
|
{{ form.auto_publish.errors }}
|
||||||
{{ form.auto_moderate }}
|
{{ form.auto_publish }}
|
||||||
{{ form.auto_moderate.label_tag() }}
|
{{ form.auto_publish.label_tag() }}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p><input type="submit" value="{% trans %}Save{% endtrans %}" class="btn btn-blue"/></p>
|
<p><input type="submit" value="{% trans %}Save{% endtrans %}" class="btn btn-blue"/></p>
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
<em>{% trans %}Nothing to come...{% endtrans %}</em>
|
<em>{% trans %}Nothing to come...{% endtrans %}</em>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% 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">
|
||||||
<div class="news_events_group_date">
|
<div class="news_events_group_date">
|
||||||
<div>
|
<div>
|
||||||
@ -57,19 +57,17 @@
|
|||||||
{% for date in dates_group %}
|
{% for date in dates_group %}
|
||||||
<article
|
<article
|
||||||
class="news_event"
|
class="news_event"
|
||||||
{%- if not date.news.is_moderated -%}
|
{%- if not date.news.is_published -%}
|
||||||
x-data="{newsState: AlertState.PENDING}"
|
x-data="{newsState: AlertState.PENDING}"
|
||||||
|
{% else %}
|
||||||
|
x-data="{newsState: AlertState.DISPLAYED}"
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
>
|
>
|
||||||
{% if not date.news.is_moderated %}
|
{# if a non published news is in the object list,
|
||||||
{# if a non moderated news is in the object list,
|
the logged user is either an admin or the news author #}
|
||||||
the logged user is either an admin or the news author #}
|
{{ news_moderation_alert(date.news, user, "newsState") }}
|
||||||
{{ news_moderation_alert(date.news, user, "newsState") }}
|
|
||||||
{% endif %}
|
|
||||||
<div
|
<div
|
||||||
{% if not date.news.is_moderated -%}
|
x-show="newsState !== AlertState.DELETED"
|
||||||
x-show="newsState !== AlertState.DELETED"
|
|
||||||
{%- endif -%}
|
|
||||||
>
|
>
|
||||||
<header class="row gap">
|
<header class="row gap">
|
||||||
{% if date.news.club.logo %}
|
{% if date.news.club.logo %}
|
||||||
@ -86,9 +84,11 @@
|
|||||||
<a href="{{ date.news.club.get_absolute_url() }}">{{ date.news.club }}</a>
|
<a href="{{ date.news.club.get_absolute_url() }}">{{ date.news.club }}</a>
|
||||||
<div class="news_date">
|
<div class="news_date">
|
||||||
<time datetime="{{ date.start_date.isoformat(timespec="seconds") }}">
|
<time datetime="{{ date.start_date.isoformat(timespec="seconds") }}">
|
||||||
|
{{ date.start_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}
|
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}
|
||||||
</time> -
|
</time> -
|
||||||
<time datetime="{{ date.end_date.isoformat(timespec="seconds") }}">
|
<time datetime="{{ date.end_date.isoformat(timespec="seconds") }}">
|
||||||
|
{{ date.end_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}
|
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
@ -120,9 +120,9 @@
|
|||||||
<template x-for="newsDate in newsList" :key="newsDate.id">
|
<template x-for="newsDate in newsList" :key="newsDate.id">
|
||||||
<article
|
<article
|
||||||
class="news_event"
|
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") }}
|
{{ news_moderation_alert("newsDate.news.id", user, "newsState") }}
|
||||||
</template>
|
</template>
|
||||||
<div x-show="newsState !== AlertState.DELETED">
|
<div x-show="newsState !== AlertState.DELETED">
|
||||||
@ -179,7 +179,23 @@
|
|||||||
{% trans %}All coming events{% endtrans %}
|
{% trans %}All coming events{% endtrans %}
|
||||||
<a target="#" href="{{ url("com:news_feed") }}"><i class="fa fa-rss feed"></i></a>
|
<a target="#" href="{{ url("com:news_feed") }}"><i class="fa fa-rss feed"></i></a>
|
||||||
</h3>
|
</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>
|
||||||
|
|
||||||
<div id="right_column">
|
<div id="right_column">
|
||||||
|
@ -129,14 +129,14 @@ class TestInternalCalendar:
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestModerateNews:
|
class TestModerateNews:
|
||||||
@pytest.mark.parametrize("news_is_moderated", [True, False])
|
@pytest.mark.parametrize("news_is_published", [True, False])
|
||||||
def test_moderation_ok(self, client: Client, news_is_moderated: bool): # noqa FBT
|
def test_moderation_ok(self, client: Client, news_is_published: bool): # noqa FBT
|
||||||
user = baker.make(
|
user = baker.make(
|
||||||
User, user_permissions=[Permission.objects.get(codename="moderate_news")]
|
User, user_permissions=[Permission.objects.get(codename="moderate_news")]
|
||||||
)
|
)
|
||||||
# The API call should work even if the news is initially moderated.
|
# 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.
|
# 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
|
initial_moderator = news.moderator
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
@ -147,22 +147,22 @@ class TestModerateNews:
|
|||||||
# If it was already moderated, it should be a no-op, but not an error
|
# If it was already moderated, it should be a no-op, but not an error
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
news.refresh_from_db()
|
news.refresh_from_db()
|
||||||
assert news.is_moderated
|
assert news.is_published
|
||||||
if not news_is_moderated:
|
if not news_is_published:
|
||||||
assert news.moderator == user
|
assert news.moderator == user
|
||||||
else:
|
else:
|
||||||
assert news.moderator == initial_moderator
|
assert news.moderator == initial_moderator
|
||||||
|
|
||||||
def test_moderation_forbidden(self, client: Client):
|
def test_moderation_forbidden(self, client: Client):
|
||||||
user = baker.make(User)
|
user = baker.make(User)
|
||||||
news = baker.make(News, is_moderated=False)
|
news = baker.make(News, is_published=False)
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
reverse("api:moderate_news", kwargs={"news_id": news.id})
|
reverse("api:moderate_news", kwargs={"news_id": news.id})
|
||||||
)
|
)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
news.refresh_from_db()
|
news.refresh_from_db()
|
||||||
assert not news.is_moderated
|
assert not news.is_published
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@ -203,7 +203,7 @@ class TestFetchNewsDates(TestCase):
|
|||||||
value=now() + timedelta(hours=2), increment_by=timedelta(days=1)
|
value=now() + timedelta(hours=2), increment_by=timedelta(days=1)
|
||||||
),
|
),
|
||||||
news=iter(
|
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(
|
cls.dates.append(
|
||||||
@ -211,7 +211,7 @@ class TestFetchNewsDates(TestCase):
|
|||||||
NewsDate,
|
NewsDate,
|
||||||
start_date=now() + timedelta(days=2, hours=1),
|
start_date=now() + timedelta(days=2, hours=1),
|
||||||
end_date=now() + timedelta(days=2, hours=5),
|
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)
|
cls.dates.sort(key=lambda d: d.start_date)
|
||||||
|
@ -18,7 +18,7 @@ class TestNewsViewableBy(TestCase):
|
|||||||
cls.news = baker.make(
|
cls.news = baker.make(
|
||||||
News,
|
News,
|
||||||
author=itertools.cycle(cls.users),
|
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,
|
_quantity=6,
|
||||||
_bulk_create=True,
|
_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.sli)
|
||||||
assert not self.new.can_be_viewed_by(self.anonymous)
|
assert not self.new.can_be_viewed_by(self.anonymous)
|
||||||
|
|
||||||
self.new.is_moderated = True
|
self.new.is_published = True
|
||||||
self.new.save()
|
self.new.save()
|
||||||
assert self.new.can_be_viewed_by(self.com_admin)
|
assert self.new.can_be_viewed_by(self.com_admin)
|
||||||
assert self.new.can_be_viewed_by(self.sli)
|
assert self.new.can_be_viewed_by(self.sli)
|
||||||
@ -258,7 +258,7 @@ class TestNewsCreation(TestCase):
|
|||||||
created = News.objects.order_by("id").last()
|
created = News.objects.order_by("id").last()
|
||||||
assertRedirects(response, created.get_absolute_url())
|
assertRedirects(response, created.get_absolute_url())
|
||||||
assert created.title == "Test news"
|
assert created.title == "Test news"
|
||||||
assert not created.is_moderated
|
assert not created.is_published
|
||||||
dates = list(created.dates.values("start_date", "end_date"))
|
dates = list(created.dates.values("start_date", "end_date"))
|
||||||
assert dates == [{"start_date": self.start, "end_date": self.end}]
|
assert dates == [{"start_date": self.start, "end_date": self.end}]
|
||||||
|
|
||||||
@ -281,7 +281,7 @@ class TestNewsCreation(TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def test_edit_news(self):
|
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(
|
baker.make(
|
||||||
NewsDate,
|
NewsDate,
|
||||||
news=news,
|
news=news,
|
||||||
@ -296,7 +296,7 @@ class TestNewsCreation(TestCase):
|
|||||||
created = News.objects.order_by("id").last()
|
created = News.objects.order_by("id").last()
|
||||||
assertRedirects(response, created.get_absolute_url())
|
assertRedirects(response, created.get_absolute_url())
|
||||||
assert created.title == "Test news"
|
assert created.title == "Test news"
|
||||||
assert not created.is_moderated
|
assert not created.is_published
|
||||||
dates = list(created.dates.values("start_date", "end_date"))
|
dates = list(created.dates.values("start_date", "end_date"))
|
||||||
assert dates == [{"start_date": self.start, "end_date": self.end}]
|
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):
|
def get(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if "remove" in request.GET:
|
if "remove" in request.GET:
|
||||||
self.object.is_moderated = False
|
self.object.is_published = False
|
||||||
else:
|
else:
|
||||||
self.object.is_moderated = True
|
self.object.is_published = True
|
||||||
self.object.moderator = request.user
|
self.object.moderator = request.user
|
||||||
self.object.save()
|
self.object.save()
|
||||||
if "next" in self.request.GET:
|
if "next" in self.request.GET:
|
||||||
@ -253,7 +253,7 @@ class NewsListView(TemplateView):
|
|||||||
key=lambda u: u.date_of_birth.year,
|
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
|
"""Get the last day when news will be displayed
|
||||||
|
|
||||||
The returned day is the third one where something happen.
|
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 ;
|
D on 20/03, E on 21/03 and F on 22/03 ;
|
||||||
then the result is 20/03.
|
then the result is 20/03.
|
||||||
"""
|
"""
|
||||||
return list(
|
dates = list(
|
||||||
NewsDate.objects.filter(end_date__gt=now())
|
NewsDate.objects.filter(end_date__gt=now())
|
||||||
.order_by("start_date")
|
.order_by("start_date")
|
||||||
.values_list("start_date__date", flat=True)
|
.values_list("start_date__date", flat=True)
|
||||||
.distinct()[:4]
|
.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.
|
"""Return the event dates to display.
|
||||||
|
|
||||||
The selected events are the ones that happens between
|
The selected events are the ones that happens between
|
||||||
right now and the given day (included).
|
right now and the given day (included).
|
||||||
"""
|
"""
|
||||||
return itertools.groupby(
|
return {
|
||||||
NewsDate.objects.viewable_by(self.request.user)
|
date: list(dates)
|
||||||
.filter(end_date__gt=now(), start_date__date__lte=until)
|
for date, dates in itertools.groupby(
|
||||||
.order_by("start_date")
|
NewsDate.objects.viewable_by(self.request.user)
|
||||||
.select_related("news", "news__club"),
|
.filter(end_date__gt=now(), start_date__date__lte=until)
|
||||||
key=lambda d: d.start_date.date(),
|
.order_by("start_date")
|
||||||
)
|
.select_related("news", "news__club"),
|
||||||
|
key=lambda d: d.start_date.date(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
last_day = self.get_last_day()
|
last_day = self.get_last_day()
|
||||||
return super().get_context_data(**kwargs) | {
|
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(),
|
"birthdays": self.get_birthdays(),
|
||||||
"last_day": last_day,
|
"last_day": last_day,
|
||||||
}
|
}
|
||||||
@ -309,7 +315,7 @@ class NewsFeed(Feed):
|
|||||||
def items(self):
|
def items(self):
|
||||||
return (
|
return (
|
||||||
NewsDate.objects.filter(
|
NewsDate.objects.filter(
|
||||||
news__is_moderated=True,
|
news__is_published=True,
|
||||||
end_date__gte=timezone.now() - (relativedelta(months=6)),
|
end_date__gte=timezone.now() - (relativedelta(months=6)),
|
||||||
)
|
)
|
||||||
.select_related("news", "news__author")
|
.select_related("news", "news__author")
|
||||||
|
@ -690,7 +690,7 @@ Welcome to the wiki page!
|
|||||||
content="Glou glou glou glou glou glou glou",
|
content="Glou glou glou glou glou glou glou",
|
||||||
club=bar_club,
|
club=bar_club,
|
||||||
author=subscriber,
|
author=subscriber,
|
||||||
is_moderated=True,
|
is_published=True,
|
||||||
moderator=skia,
|
moderator=skia,
|
||||||
)
|
)
|
||||||
news_dates.append(
|
news_dates.append(
|
||||||
@ -704,12 +704,11 @@ Welcome to the wiki page!
|
|||||||
title="Repas barman",
|
title="Repas barman",
|
||||||
summary="Enjoy la fin du semestre!",
|
summary="Enjoy la fin du semestre!",
|
||||||
content=(
|
content=(
|
||||||
"Viens donc t'enjailler avec les autres barmans aux "
|
"Viens donc t'enjailler avec les autres barmans aux frais du BdF! \\o/"
|
||||||
"frais du BdF! \\o/"
|
|
||||||
),
|
),
|
||||||
club=bar_club,
|
club=bar_club,
|
||||||
author=subscriber,
|
author=subscriber,
|
||||||
is_moderated=True,
|
is_published=True,
|
||||||
moderator=skia,
|
moderator=skia,
|
||||||
)
|
)
|
||||||
news_dates.append(
|
news_dates.append(
|
||||||
@ -725,7 +724,7 @@ Welcome to the wiki page!
|
|||||||
content="Fô viendre mangey d'la bonne fondue!",
|
content="Fô viendre mangey d'la bonne fondue!",
|
||||||
club=bar_club,
|
club=bar_club,
|
||||||
author=subscriber,
|
author=subscriber,
|
||||||
is_moderated=True,
|
is_published=True,
|
||||||
moderator=skia,
|
moderator=skia,
|
||||||
)
|
)
|
||||||
news_dates.append(
|
news_dates.append(
|
||||||
@ -741,7 +740,7 @@ Welcome to the wiki page!
|
|||||||
content="Viens faire la fête avec tout plein de gens!",
|
content="Viens faire la fête avec tout plein de gens!",
|
||||||
club=bar_club,
|
club=bar_club,
|
||||||
author=subscriber,
|
author=subscriber,
|
||||||
is_moderated=True,
|
is_published=True,
|
||||||
moderator=skia,
|
moderator=skia,
|
||||||
)
|
)
|
||||||
news_dates.append(
|
news_dates.append(
|
||||||
@ -759,7 +758,7 @@ Welcome to the wiki page!
|
|||||||
"t'amuser le Vendredi soir!",
|
"t'amuser le Vendredi soir!",
|
||||||
club=troll,
|
club=troll,
|
||||||
author=subscriber,
|
author=subscriber,
|
||||||
is_moderated=True,
|
is_published=True,
|
||||||
moderator=skia,
|
moderator=skia,
|
||||||
)
|
)
|
||||||
news_dates.extend(
|
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) {
|
&:not(.btn-no-text) {
|
||||||
i {
|
i {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"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"
|
"PO-Revision-Date: 2016-07-18\n"
|
||||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@ -812,7 +812,7 @@ msgstr "Nouvelle mailing liste"
|
|||||||
msgid "Subscribe"
|
msgid "Subscribe"
|
||||||
msgstr "S'abonner"
|
msgstr "S'abonner"
|
||||||
|
|
||||||
#: club/forms.py com/templates/com/news_admin_list.jinja
|
#: club/forms.py
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr "Retirer"
|
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)"
|
"Combien de fois l'événement doit-il se répéter (en incluant la première fois)"
|
||||||
|
|
||||||
#: com/forms.py
|
#: com/forms.py
|
||||||
msgid "Automoderation"
|
msgid "Auto publication"
|
||||||
msgstr "Automodération"
|
msgstr "Publication automatique"
|
||||||
|
|
||||||
#: com/models.py
|
#: com/models.py
|
||||||
msgid "alert message"
|
msgid "alert message"
|
||||||
@ -1344,6 +1344,10 @@ msgstr "Le club qui organise l'évènement."
|
|||||||
msgid "author"
|
msgid "author"
|
||||||
msgstr "auteur"
|
msgstr "auteur"
|
||||||
|
|
||||||
|
#: com/models.py
|
||||||
|
msgid "is published"
|
||||||
|
msgstr "est publié"
|
||||||
|
|
||||||
#: com/models.py
|
#: com/models.py
|
||||||
msgid "news"
|
msgid "news"
|
||||||
msgstr "nouvelle"
|
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"
|
msgstr "La date de début doit être avant celle de fin"
|
||||||
|
|
||||||
#: com/templates/com/macros.jinja
|
#: com/templates/com/macros.jinja
|
||||||
msgid "Waiting moderation"
|
msgid "Waiting publication"
|
||||||
msgstr "En attente de modération"
|
msgstr "En attente de publication"
|
||||||
|
|
||||||
#: com/templates/com/macros.jinja
|
#: com/templates/com/macros.jinja
|
||||||
msgid ""
|
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."
|
"communication admins."
|
||||||
msgstr ""
|
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."
|
"admins communication."
|
||||||
|
|
||||||
#: com/templates/com/macros.jinja
|
#: 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 ""
|
msgstr ""
|
||||||
"Elle sera cachée pour les autres utilisateurs tant qu'elle ne sera pas "
|
"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/macros.jinja com/templates/com/news_admin_list.jinja
|
||||||
#: com/templates/com/news_admin_list.jinja com/templates/com/news_detail.jinja
|
#: com/templates/com/news_detail.jinja
|
||||||
#: core/templates/core/file_detail.jinja
|
msgid "Publish"
|
||||||
#: core/templates/core/file_moderation.jinja sas/templates/sas/moderation.jinja
|
msgstr "Publier"
|
||||||
#: sas/templates/sas/picture.jinja
|
|
||||||
msgid "Moderate"
|
|
||||||
msgstr "Modérer"
|
|
||||||
|
|
||||||
#: com/templates/com/macros.jinja
|
#: com/templates/com/macros.jinja
|
||||||
msgid "News moderated"
|
msgid "News published"
|
||||||
msgstr "Nouvelle modérée"
|
msgstr "Nouvelle publiée"
|
||||||
|
|
||||||
#: com/templates/com/macros.jinja
|
#: com/templates/com/macros.jinja
|
||||||
msgid "News deleted"
|
msgid "News deleted"
|
||||||
@ -1447,6 +1448,12 @@ msgstr "Nouvelle supprimée"
|
|||||||
msgid "Mailing lists administration"
|
msgid "Mailing lists administration"
|
||||||
msgstr "Administration des mailing listes"
|
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
|
#: com/templates/com/mailing_admin.jinja
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Moderated by %(user)s"
|
msgid "Moderated by %(user)s"
|
||||||
@ -1514,6 +1521,10 @@ msgstr "Modérateur"
|
|||||||
msgid "Dates"
|
msgid "Dates"
|
||||||
msgstr "Dates"
|
msgstr "Dates"
|
||||||
|
|
||||||
|
#: com/templates/com/news_admin_list.jinja
|
||||||
|
msgid "Unpublish"
|
||||||
|
msgstr "Dépublier"
|
||||||
|
|
||||||
#: com/templates/com/news_admin_list.jinja
|
#: com/templates/com/news_admin_list.jinja
|
||||||
msgid "Weeklies to moderate"
|
msgid "Weeklies to moderate"
|
||||||
msgstr "Nouvelles hebdomadaires à modérer"
|
msgstr "Nouvelles hebdomadaires à modérer"
|
||||||
@ -6031,13 +6042,3 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée."
|
|||||||
#, python-format
|
#, python-format
|
||||||
msgid "Maximum characters: %(max_length)s"
|
msgid "Maximum characters: %(max_length)s"
|
||||||
msgstr "Nombre de caractères max: %(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 ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"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"
|
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
|
||||||
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
|
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@ -21,15 +21,25 @@ msgstr ""
|
|||||||
msgid "More info"
|
msgid "More info"
|
||||||
msgstr "Plus d'informations"
|
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
|
#: com/static/bundled/com/components/moderation-alert-index.ts
|
||||||
#, javascript-format
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"This event will take place every week for %s weeks. If you moderate or "
|
"This event will take place every week for %s weeks. If you publish or delete "
|
||||||
"delete this event, it will also be moderated (or deleted) for the following "
|
"this event, it will also be published (or deleted) for the following weeks."
|
||||||
"weeks."
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Cet événement se déroulera chaque semaine pendant %s semaines. Si vous "
|
"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."
|
"pour les semaines suivantes."
|
||||||
|
|
||||||
#: core/static/bundled/core/components/ajax-select-base.ts
|
#: core/static/bundled/core/components/ajax-select-base.ts
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"types": ["jquery", "alpinejs"],
|
"types": ["jquery", "alpinejs"],
|
||||||
|
"lib": ["es7"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"#openapi": ["./staticfiles/generated/openapi/index.ts"],
|
"#openapi": ["./staticfiles/generated/openapi/index.ts"],
|
||||||
"#core:*": ["./core/static/bundled/*"],
|
"#core:*": ["./core/static/bundled/*"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user