Rename news moderate to publish

This commit is contained in:
Antoine Bartuccio 2025-02-25 18:08:16 +01:00
parent 2e71275f5b
commit 4890fcf0e1
21 changed files with 186 additions and 153 deletions

View File

@ -40,13 +40,13 @@ class CalendarController(ControllerBase):
return send_raw_file(IcsCalendar.get_internal()) return send_raw_file(IcsCalendar.get_internal())
@route.get( @route.get(
"/unmoderated.ics", "/unpublished.ics",
permissions=[IsAuthenticated], permissions=[IsAuthenticated],
url_name="calendar_unmoderated", url_name="calendar_unpublished",
) )
def calendar_unmoderated(self): def calendar_unpublished(self):
return HttpResponse( return HttpResponse(
IcsCalendar.get_unmoderated(self.context.request.user), IcsCalendar.get_unpublished(self.context.request.user),
content_type="text/calendar", content_type="text/calendar",
) )
@ -54,26 +54,26 @@ class CalendarController(ControllerBase):
@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.moderator = self.context.request.user
news.save() news.save()
@route.patch( @route.patch(
"/{int:news_id}/remove", "/{int:news_id}/unpublish",
permissions=[HasPerm("com.moderate_news")], permissions=[HasPerm("com.moderate_news")],
url_name="remove_news", url_name="unpublish_news",
) )
def remove_news(self, news_id: int): def unpublish_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 news.is_moderated: if news.is_published:
news.is_moderated = False news.is_published = False
news.moderator = self.context.request.user news.moderator = self.context.request.user
news.save() news.save()

View File

@ -63,7 +63,7 @@ class IcsCalendar:
_ = f.write( _ = f.write(
cls.ics_from_queryset( cls.ics_from_queryset(
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)),
) )
) )
@ -71,10 +71,10 @@ class IcsCalendar:
return cls._INTERNAL_CALENDAR return cls._INTERNAL_CALENDAR
@classmethod @classmethod
def get_unmoderated(cls, user: User) -> bytes: def get_unpublished(cls, user: User) -> bytes:
return cls.ics_from_queryset( return cls.ics_from_queryset(
NewsDate.objects.viewable_by(user).filter( NewsDate.objects.viewable_by(user).filter(
news__is_moderated=False, news__is_published=False,
end_date__gte=timezone.now() - (relativedelta(months=6)), end_date__gte=timezone.now() - (relativedelta(months=6)),
), ),
) )

View File

@ -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

View File

@ -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"),
),
]

View File

@ -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)
) )

View File

@ -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

View File

@ -10,10 +10,10 @@ import listPlugin from "@fullcalendar/list";
import { import {
calendarCalendarExternal, calendarCalendarExternal,
calendarCalendarInternal, calendarCalendarInternal,
calendarCalendarUnmoderated, calendarCalendarUnpublished,
newsDeleteNews, newsDeleteNews,
newsModerateNews, newsPublishNews,
newsRemoveNews, newsUnpublishNews,
} from "#openapi"; } from "#openapi";
@registerComponent("ics-calendar") @registerComponent("ics-calendar")
@ -89,15 +89,15 @@ export class IcsCalendar extends inheritHtmlElement("div") {
this.calendar.refetchEvents(); this.calendar.refetchEvents();
} }
async moderateNews(id: number) { async publishNews(id: number) {
await newsModerateNews({ await newsPublishNews({
path: { path: {
// biome-ignore lint/style/useNamingConvention: python API // biome-ignore lint/style/useNamingConvention: python API
news_id: id, news_id: id,
}, },
}); });
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("calendar-moderate", { new CustomEvent("calendar-publish", {
bubbles: true, bubbles: true,
detail: { detail: {
id: id, id: id,
@ -107,15 +107,15 @@ export class IcsCalendar extends inheritHtmlElement("div") {
await this.refreshEvents(); await this.refreshEvents();
} }
async removeNews(id: number) { async unpublishNews(id: number) {
await newsRemoveNews({ await newsUnpublishNews({
path: { path: {
// biome-ignore lint/style/useNamingConvention: python API // biome-ignore lint/style/useNamingConvention: python API
news_id: id, news_id: id,
}, },
}); });
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("calendar-remove", { new CustomEvent("calendar-unpublish", {
bubbles: true, bubbles: true,
detail: { detail: {
id: id, id: id,
@ -157,10 +157,10 @@ export class IcsCalendar extends inheritHtmlElement("div") {
className: "external", className: "external",
}, },
{ {
url: `${await makeUrl(calendarCalendarUnmoderated)}${cacheInvalidate}`, url: `${await makeUrl(calendarCalendarUnpublished)}${cacheInvalidate}`,
format: "ics", format: "ics",
color: "red", color: "red",
className: "unmoderated", className: "unpublished",
}, },
]; ];
} }
@ -233,20 +233,20 @@ export class IcsCalendar extends inheritHtmlElement("div") {
const newsId = this.getNewsId(event); const newsId = this.getNewsId(event);
const div = document.createElement("div"); const div = document.createElement("div");
if (this.canModerate) { if (this.canModerate) {
if (event.source.internalEventSource.ui.classNames.includes("unmoderated")) { if (event.source.internalEventSource.ui.classNames.includes("unpublished")) {
const button = document.createElement("button"); const button = document.createElement("button");
button.innerHTML = `<i class="fa fa-check"></i>${gettext("Moderate")}`; button.innerHTML = `<i class="fa fa-check"></i>${gettext("Publish")}`;
button.setAttribute("class", "btn btn-green"); button.setAttribute("class", "btn btn-green");
button.onclick = () => { button.onclick = () => {
this.moderateNews(newsId); this.publishNews(newsId);
}; };
div.appendChild(button); div.appendChild(button);
} else { } else {
const button = document.createElement("button"); const button = document.createElement("button");
button.innerHTML = `<i class="fa fa-times"></i>${gettext("Remove")}`; button.innerHTML = `<i class="fa fa-times"></i>${gettext("Unpublish")}`;
button.setAttribute("class", "btn btn-orange"); button.setAttribute("class", "btn btn-orange");
button.onclick = () => { button.onclick = () => {
this.removeNews(newsId); this.unpublishNews(newsId);
}; };
div.appendChild(button); div.appendChild(button);
} }

View File

@ -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,7 +7,7 @@ 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,
}; };
@ -19,11 +19,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 +54,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 +69,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],
); );

View File

@ -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
@ -64,16 +64,16 @@
<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">

View File

@ -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>

View File

@ -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">
@ -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>

View File

@ -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>

View File

@ -57,17 +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}"
{%- endif -%} {%- endif -%}
> >
{% if not date.news.is_moderated %} {% if not date.news.is_published %}
{# if a non moderated news is in the object list, {# if a non published 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 %} {% endif %}
<div <div
{% if not date.news.is_moderated -%} {% if not date.news.is_published -%}
x-show="newsState !== AlertState.DELETED" x-show="newsState !== AlertState.DELETED"
{%- endif -%} {%- endif -%}
> >
@ -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.PULISHED : 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">
@ -183,15 +183,15 @@
x-data x-data
x-ref="calendar" x-ref="calendar"
@news-moderated.window=" @news-moderated.window="
if ($event.target !== $refs.calendar){
// Avoid triggering a refresh with a dispatch // Avoid triggering a refresh with a dispatch
// from the calendar itself // from the calendar itself
if ($event.target !== $refs.calendar){
$refs.calendar.refreshEvents($event); $refs.calendar.refreshEvents($event);
} }
" "
@calendar-remove="$dispatch('news-moderated', {newsId: $event.detail.id, state: AlertState.PENDING})" @calendar-remove="$dispatch('news-moderated', {newsId: $event.detail.id, state: AlertState.DELETED})"
@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-moderate="$dispatch('news-moderated', {newsId: $event.detail.id, state: AlertState.MODERATED})" @calendar-publish="$dispatch('news-moderated', {newsId: $event.detail.id, state: AlertState.PUBLISHED})"
locale="{{ get_language() }}" locale="{{ get_language() }}"
can_moderate="{{ user.has_perm("com.moderate_news") }}" can_moderate="{{ user.has_perm("com.moderate_news") }}"
can_delete="{{ user.has_perm("com.delete_news") }}" can_delete="{{ user.has_perm("com.delete_news") }}"

View File

@ -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)

View File

@ -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,
) )

View File

@ -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}]

View File

@ -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:
@ -309,7 +309,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")

View File

@ -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(

View 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"},
),
]

View File

@ -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."

View File

@ -7,7 +7,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-25 14:38+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"
@ -22,29 +22,30 @@ msgid "More info"
msgstr "Plus d'informations" msgstr "Plus d'informations"
#: com/static/bundled/com/components/ics-calendar-index.ts #: com/static/bundled/com/components/ics-calendar-index.ts
msgid "Moderate" msgid "Publish"
msgstr "Modérer" msgstr "Publier"
#: com/static/bundled/com/components/ics-calendar-index.ts #: com/static/bundled/com/components/ics-calendar-index.ts
#: core/static/bundled/core/components/ajax-select-base.ts msgid "Unpublish"
msgid "Remove" msgstr "Dépublier"
msgstr "Retirer"
#: com/static/bundled/com/components/ics-calendar-index.ts #: com/static/bundled/com/components/ics-calendar-index.ts
msgid "Delete" msgid "Delete"
msgstr "Supprimer" 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
msgid "Remove"
msgstr "Retirer"
#: core/static/bundled/core/components/ajax-select-base.ts #: core/static/bundled/core/components/ajax-select-base.ts
msgid "You need to type %(number)s more characters" msgid "You need to type %(number)s more characters"
msgstr "Vous devez taper %(number)s caractères de plus" msgstr "Vous devez taper %(number)s caractères de plus"