Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

News list improvements #1009

Open
wants to merge 7 commits into
base: taiste
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion com/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from ninja_extra import ControllerBase, api_controller, route

from com.calendar import IcsCalendar
from com.models import News
from core.auth.api_permissions import HasPerm
from core.views.files import send_raw_file


Expand All @@ -17,7 +19,7 @@ def calendar_external(self):
"""Return the ICS file of the AE Google Calendar

Because of Google's cors rules, we can't just do a request to google ics
from the frontend. Google is blocking CORS request in it's responses headers.
from the frontend. Google is blocking CORS request in its responses headers.
The only way to do it from the frontend is to use Google Calendar API with an API key
This is not especially desirable as your API key is going to be provided to the frontend.

Expand All @@ -30,3 +32,27 @@ def calendar_external(self):
@route.get("/internal.ics", url_name="calendar_internal")
def calendar_internal(self):
return send_raw_file(IcsCalendar.get_internal())


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

@route.delete(
"/{news_id}",
permissions=[HasPerm("com.delete_news")],
url_name="delete_news",
)
def delete_news(self, news_id: int):
news = self.get_object_or_exception(News, id=news_id)
news.delete()
18 changes: 18 additions & 0 deletions com/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,22 @@ def news_notification_callback(notif):
notif.viewed = True


class NewsDateQuerySet(models.QuerySet):
def viewable_by(self, user: User) -> Self:
"""Filter the event dates that the given user can view.

- If the can view non moderated news, he can view all news dates
- else, he can view the dates of news that are either
authored by him or moderated.
"""
if user.has_perm("com.view_unmoderated_news"):
return self
q_filter = Q(news__is_moderated=True)
if user.is_authenticated:
q_filter |= Q(news__author_id=user.id)
return self.filter(q_filter)


class NewsDate(models.Model):
"""A date associated with news.

Expand All @@ -187,6 +203,8 @@ class NewsDate(models.Model):
start_date = models.DateTimeField(_("start_date"))
end_date = models.DateTimeField(_("end_date"))

objects = NewsDateQuerySet.as_manager()

class Meta:
verbose_name = _("news date")
verbose_name_plural = _("news dates")
Expand Down
38 changes: 38 additions & 0 deletions com/static/bundled/com/components/moderation-alert-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { exportToHtml } from "#core:utils/globals";
import { newsDeleteNews, newsModerateNews } from "#openapi";

// This will be used in jinja templates,
// so we cannot use real enums as those are purely an abstraction of Typescript
const AlertState = {
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
PENDING: 1,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
MODERATED: 2,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
DELETED: 3,
};
exportToHtml("AlertState", AlertState);

document.addEventListener("alpine:init", () => {
Alpine.data("moderationAlert", (newsId: number) => ({
state: AlertState.PENDING,
newsId: newsId as number,
loading: false,

async moderateNews() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ce serait bien de pouvoir forcer le reload du calendrier quand on fait ça pour avoir une expérience plus smooth.
Tu peux mettre un x-ref dessus et utiliser refetchEvents https://fullcalendar.io/docs/Calendar-refetchEvents

this.loading = true;
// biome-ignore lint/style/useNamingConvention: api is snake case
await newsModerateNews({ path: { news_id: this.newsId } });
this.state = AlertState.MODERATED;
this.loading = false;
},

async deleteNews() {
this.loading = true;
// biome-ignore lint/style/useNamingConvention: api is snake case
await newsDeleteNews({ path: { news_id: this.newsId } });
this.state = AlertState.DELETED;
this.loading = false;
},
}));
});
124 changes: 15 additions & 109 deletions com/static/com/css/news-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -171,54 +171,24 @@
}

.news_event {
display: block;
padding: 0.4em;

&:not(:last-child) {
border-bottom: 1px solid grey;
}

div {
margin: 0.2em;
}

h4 {
margin-top: 1em;
text-transform: uppercase;
}

.club_logo {
float: left;
min-width: 7em;
max-width: 9em;
margin: 0;
margin-right: 1em;
margin-top: 0.8em;
display: flex;
flex-direction: column;
gap: .5em;
padding: 1em;

header {
img {
max-height: 6em;
max-width: 8em;
display: block;
margin: 0 auto;
height: 75px;
}
}

.news_date {
font-size: 100%;
}

.news_content {
clear: left;

.button_bar {
text-align: right;

.fb {
color: $faceblue;
}

.twitter {
color: $twitblue;
.header_content {
display: flex;
flex-direction: column;
justify-content: center;
gap: .2rem;

h4 {
margin-top: 0;
text-transform: uppercase;
}
}
}
Expand All @@ -228,70 +198,6 @@

/* END EVENTS TODAY AND NEXT FEW DAYS */

/* COMING SOON */
.news_coming_soon {
display: list-item;
list-style-type: square;
list-style-position: inside;
margin-left: 1em;
padding-left: 0;

a {
font-weight: bold;
text-transform: uppercase;
}

.news_date {
font-size: 0.9em;
}
}

/* END COMING SOON */

/* NOTICES */
.news_notice {
margin: 0 0 1em 1em;
padding: 0.4em;
padding-left: 1em;
background: $secondary-neutral-light-color;
box-shadow: $shadow-color 0 0 2px;
border-radius: 18px 5px 18px 5px;

h4 {
margin: 0;
}

.news_content {
margin-left: 1em;
}
}

/* END NOTICES */

/* CALLS */
.news_call {
margin: 0 0 1em 1em;
padding: 0.4em;
padding-left: 1em;
background: $secondary-neutral-light-color;
border: 1px solid grey;
box-shadow: $shadow-color 1px 1px 1px;

h4 {
margin: 0;
}

.news_date {
font-size: 0.9em;
}

.news_content {
margin-left: 1em;
}
}

/* END CALLS */

.news_empty {
margin-left: 1em;
}
Expand Down
91 changes: 91 additions & 0 deletions com/templates/com/macros.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{% macro news_moderation_alert(news, user, alpineState = None) %}
{# An alert to display on top of non moderated news,
with actions to either moderate or delete them.

The current state of the alert is accessible through
the given `alpineState` variable.
This state is a `AlertState`, as defined in `moderation-alert-index.ts`

Example :
```jinja
<div x-data="{state: AlertState.PENDING}">
{{ news_moderation_alert(news, user, "state") }}
</div>
```

Args:
news: The `News` object to which this alert is related
user: The request.user
alpineState: An alpine variable name

Warning:
If you use this macro, you must also include `moderation-alert-index.ts`
in your template.
#}
<div
x-data="moderationAlert({{ news.id }})"
{% if alpineState %}
x-modelable="{{ alpineState }}"
x-model="state"
{% endif %}
>
<template x-if="state === AlertState.PENDING">
<div class="alert alert-yellow">
<div class="alert-main">
<strong>{% trans %}Waiting moderation{% endtrans %}</strong>
<p>
{% trans trimmed %}
This news isn't moderated and is visible
only by its author and the communication admins.
{% endtrans %}
</p>
<p>
{% trans trimmed %}
It will stay hidden for other users until it has been moderated.
{% endtrans %}
</p>
{% if user.has_perm("com.moderate_news") %}
{# This is an additional query for each non-moderated news,
but it will be executed only for admin users, and only one time
(if they do their job and moderated news as soon as they see them),
so it's still reasonable #}
{% set nb_event=news.dates.count() %}
{% if nb_event > 1 %}
<br>
<strong>{% trans %}Weekly event{% endtrans %}</strong>
<p>
{% trans trimmed nb=nb_event %}
This event will take place every week for {{ nb }} weeks.
If you moderate or delete this event,
it will also be moderated (or deleted) for the following weeks.
{% endtrans %}
</p>
{% endif %}
{% endif %}
</div>
{% if user.has_perm("com.moderate_news") %}
<span class="alert-aside" :aria-busy="loading">
<button class="btn btn-green" @click="moderateNews()" :disabled="loading">
<i class="fa fa-check"></i> {% trans %}Moderate{% endtrans %}
</button>
{% endif %}
{% if user.has_perm("com.delete_news") %}
<button class="btn btn-red" @click="deleteNews()" :disabled="loading">
<i class="fa fa-trash-can"></i> {% trans %}Delete{% endtrans %}
</button>
</span>
{% endif %}
</div>
</template>
<template x-if="state === AlertState.MODERATED">
<div class="alert alert-green">
{% trans %}News moderated{% endtrans %}
</div>
</template>
<template x-if="state === AlertState.DELETED">
<div class="alert alert-red">
{% trans %}News deleted{% endtrans %}
</div>
</template>
</div>
{% endmacro %}
Loading
Loading