mirror of
https://github.com/ae-utbm/sith.git
synced 2025-02-22 07:27:12 +00:00
commit
88b3f7c322
28
com/api.py
28
com/api.py
@ -5,6 +5,8 @@ from django.http import Http404
|
|||||||
from ninja_extra import ControllerBase, api_controller, route
|
from ninja_extra import ControllerBase, api_controller, route
|
||||||
|
|
||||||
from com.calendar import IcsCalendar
|
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
|
from core.views.files import send_raw_file
|
||||||
|
|
||||||
|
|
||||||
@ -17,7 +19,7 @@ class CalendarController(ControllerBase):
|
|||||||
"""Return the ICS file of the AE Google Calendar
|
"""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
|
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
|
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.
|
This is not especially desirable as your API key is going to be provided to the frontend.
|
||||||
|
|
||||||
@ -30,3 +32,27 @@ class CalendarController(ControllerBase):
|
|||||||
@route.get("/internal.ics", url_name="calendar_internal")
|
@route.get("/internal.ics", url_name="calendar_internal")
|
||||||
def calendar_internal(self):
|
def calendar_internal(self):
|
||||||
return send_raw_file(IcsCalendar.get_internal())
|
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()
|
||||||
|
@ -172,6 +172,22 @@ def news_notification_callback(notif):
|
|||||||
notif.viewed = True
|
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):
|
class NewsDate(models.Model):
|
||||||
"""A date associated with news.
|
"""A date associated with news.
|
||||||
|
|
||||||
@ -187,6 +203,8 @@ class NewsDate(models.Model):
|
|||||||
start_date = models.DateTimeField(_("start_date"))
|
start_date = models.DateTimeField(_("start_date"))
|
||||||
end_date = models.DateTimeField(_("end_date"))
|
end_date = models.DateTimeField(_("end_date"))
|
||||||
|
|
||||||
|
objects = NewsDateQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("news date")
|
verbose_name = _("news date")
|
||||||
verbose_name_plural = _("news dates")
|
verbose_name_plural = _("news dates")
|
||||||
|
38
com/static/bundled/com/components/moderation-alert-index.ts
Normal file
38
com/static/bundled/com/components/moderation-alert-index.ts
Normal file
@ -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() {
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
@ -171,54 +171,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.news_event {
|
.news_event {
|
||||||
display: block;
|
display: flex;
|
||||||
padding: 0.4em;
|
flex-direction: column;
|
||||||
|
gap: .5em;
|
||||||
&:not(:last-child) {
|
padding: 1em;
|
||||||
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;
|
|
||||||
|
|
||||||
|
header {
|
||||||
img {
|
img {
|
||||||
max-height: 6em;
|
height: 75px;
|
||||||
max-width: 8em;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
}
|
.header_content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: .2rem;
|
||||||
|
|
||||||
.news_date {
|
h4 {
|
||||||
font-size: 100%;
|
margin-top: 0;
|
||||||
}
|
text-transform: uppercase;
|
||||||
|
|
||||||
.news_content {
|
|
||||||
clear: left;
|
|
||||||
|
|
||||||
.button_bar {
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
.fb {
|
|
||||||
color: $faceblue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.twitter {
|
|
||||||
color: $twitblue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,70 +198,6 @@
|
|||||||
|
|
||||||
/* END EVENTS TODAY AND NEXT FEW DAYS */
|
/* 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 {
|
.news_empty {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
}
|
}
|
||||||
|
91
com/templates/com/macros.jinja
Normal file
91
com/templates/com/macros.jinja
Normal file
@ -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 %}
|
@ -1,5 +1,6 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
{% from 'core/macros.jinja' import user_profile_link, facebook_share, tweet, link_news_logo, gen_news_metatags %}
|
{% from 'core/macros.jinja' import user_profile_link, facebook_share, tweet, link_news_logo, gen_news_metatags %}
|
||||||
|
{% from "com/macros.jinja" import news_moderation_alert %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}News{% endtrans %} -
|
{% trans %}News{% endtrans %} -
|
||||||
@ -16,39 +17,49 @@
|
|||||||
<link rel="stylesheet" href="{{ static('com/css/news-detail.scss') }}">
|
<link rel="stylesheet" href="{{ static('com/css/news-detail.scss') }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block additional_js %}
|
||||||
|
<script type="module" src={{ static("bundled/com/components/moderation-alert-index.ts") }}></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<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>
|
||||||
<section id="news_details">
|
<div x-data="{newsState: AlertState.PENDING}">
|
||||||
<div class="club_logo">
|
|
||||||
<img src="{{ link_news_logo(news)}}" alt="{{ news.club }}" />
|
{% if not news.is_moderated %}
|
||||||
<a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a>
|
{{ news_moderation_alert(news, user, "newsState") }}
|
||||||
</div>
|
{% endif %}
|
||||||
<h4>{{ news.title }}</h4>
|
<article id="news_details" x-show="newsState !== AlertState.DELETED">
|
||||||
<p class="date">
|
<div class="club_logo">
|
||||||
<span>{{ date.start_date|localtime|date(DATETIME_FORMAT) }}
|
<img src="{{ link_news_logo(news)}}" alt="{{ news.club }}" />
|
||||||
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
<a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a>
|
||||||
<span>{{ date.end_date|localtime|date(DATETIME_FORMAT) }}
|
|
||||||
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
|
||||||
</p>
|
|
||||||
<div class="news_content">
|
|
||||||
<div><em>{{ news.summary|markdown }}</em></div>
|
|
||||||
<br/>
|
|
||||||
<div>{{ news.content|markdown }}</div>
|
|
||||||
{{ facebook_share(news) }}
|
|
||||||
{{ tweet(news) }}
|
|
||||||
<div class="news_meta">
|
|
||||||
<p>{% trans %}Author: {% endtrans %}{{ user_profile_link(news.author) }}</p>
|
|
||||||
{% if news.moderator %}
|
|
||||||
<p>{% trans %}Moderator: {% endtrans %}{{ user_profile_link(news.moderator) }}</p>
|
|
||||||
{% elif user.is_com_admin %}
|
|
||||||
<p> <a href="{{ url('com:news_moderate', news_id=news.id) }}">{% trans %}Moderate{% endtrans %}</a></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if user.can_edit(news) %}
|
|
||||||
<p> <a href="{{ url('com:news_edit', news_id=news.id) }}">{% trans %}Edit (will be moderated again){% endtrans %}</a></p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<h4>{{ news.title }}</h4>
|
||||||
</section>
|
<p class="date">
|
||||||
|
<span>{{ date.start_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
|
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
||||||
|
<span>{{ date.end_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
|
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||||
|
</p>
|
||||||
|
<div class="news_content">
|
||||||
|
<div><em>{{ news.summary|markdown }}</em></div>
|
||||||
|
<br/>
|
||||||
|
<div>{{ news.content|markdown }}</div>
|
||||||
|
{{ facebook_share(news) }}
|
||||||
|
{{ tweet(news) }}
|
||||||
|
<div class="news_meta">
|
||||||
|
<p>{% trans %}Author: {% endtrans %}{{ user_profile_link(news.author) }}</p>
|
||||||
|
{% if news.moderator %}
|
||||||
|
<p>{% trans %}Moderator: {% endtrans %}{{ user_profile_link(news.moderator) }}</p>
|
||||||
|
{% elif user.is_com_admin %}
|
||||||
|
<p> <a href="{{ url('com:news_moderate', news_id=news.id) }}">{% trans %}Moderate{% endtrans %}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.can_edit(news) %}
|
||||||
|
<p> <a href="{{ url('com:news_edit', news_id=news.id) }}">{% trans %}Edit (will be moderated again){% endtrans %}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
{% from 'core/macros.jinja' import tweet_quick, fb_quick %}
|
{% from "com/macros.jinja" import news_moderation_alert %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}News{% endtrans %}
|
{% trans %}News{% endtrans %}
|
||||||
@ -15,13 +15,12 @@
|
|||||||
|
|
||||||
{% block additional_js %}
|
{% block additional_js %}
|
||||||
<script type="module" src={{ static("bundled/com/components/ics-calendar-index.ts") }}></script>
|
<script type="module" src={{ static("bundled/com/components/ics-calendar-index.ts") }}></script>
|
||||||
|
<script type="module" src={{ static("bundled/com/components/moderation-alert-index.ts") }}></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div id="news">
|
<div id="news">
|
||||||
<div id="left_column" class="news_column">
|
<div id="left_column" class="news_column">
|
||||||
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5), news__is_moderated=True).datetimes('start_date', 'day') %}
|
|
||||||
<h3>
|
<h3>
|
||||||
{% trans %}Events today and the next few days{% endtrans %}
|
{% trans %}Events today and the next few days{% 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>
|
||||||
@ -33,51 +32,74 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.is_com_admin %}
|
{% if user.is_com_admin %}
|
||||||
<a class="btn btn-blue" href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
|
<a class="btn btn-blue" href="{{ url('com:news_admin_list') }}">
|
||||||
|
{% trans %}Administrate news{% endtrans %}
|
||||||
|
</a>
|
||||||
<br>
|
<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if events_dates %}
|
{% for day, dates_group in news_dates %}
|
||||||
{% for d in events_dates %}
|
<div class="news_events_group">
|
||||||
<div class="news_events_group">
|
<div class="news_events_group_date">
|
||||||
<div class="news_events_group_date">
|
<div>
|
||||||
<div>
|
<div>{{ day|date('D') }}</div>
|
||||||
<div>{{ d|localtime|date('D') }}</div>
|
<div class="day">{{ day|date('d') }}</div>
|
||||||
<div class="day">{{ d|localtime|date('d') }}</div>
|
<div>{{ day|date('b') }}</div>
|
||||||
<div>{{ d|localtime|date('b') }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="news_events_group_items">
|
|
||||||
{% for news in object_list.filter(dates__start_date__gte=d,dates__start_date__lte=d+timedelta(days=1)).exclude(dates__end_date__lt=timezone.now()).order_by('dates__start_date') %}
|
|
||||||
<section class="news_event">
|
|
||||||
<div class="club_logo">
|
|
||||||
{% if news.club.logo %}
|
|
||||||
<img src="{{ news.club.logo.url }}" alt="{{ news.club }}" />
|
|
||||||
{% else %}
|
|
||||||
<img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" />
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
|
||||||
<div><a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a></div>
|
|
||||||
<div class="news_date">
|
|
||||||
<span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
|
||||||
<span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="news_content">{{ news.summary|markdown }}
|
|
||||||
<div class="button_bar">
|
|
||||||
{{ fb_quick(news) }}
|
|
||||||
{{ tweet_quick(news) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
<div class="news_events_group_items">
|
||||||
|
{% for date in dates_group %}
|
||||||
|
<article
|
||||||
|
class="news_event"
|
||||||
|
{%- if not date.news.is_moderated -%}
|
||||||
|
x-data="{newsState: AlertState.PENDING}"
|
||||||
|
{%- endif -%}
|
||||||
|
>
|
||||||
|
{% if not date.news.is_moderated %}
|
||||||
|
{# if a non moderated news is in the object list,
|
||||||
|
the logged user is either an admin or the news author #}
|
||||||
|
{{ news_moderation_alert(date.news, user, "newsState") }}
|
||||||
|
{% endif %}
|
||||||
|
<div
|
||||||
|
{% if not date.news.is_moderated -%}
|
||||||
|
x-show="newsState !== AlertState.DELETED"
|
||||||
|
{%- endif -%}
|
||||||
|
>
|
||||||
|
<header class="row gap">
|
||||||
|
{% if date.news.club.logo %}
|
||||||
|
<img src="{{ date.news.club.logo.url }}" alt="{{ date.news.club }}"/>
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ static("com/img/news.png") }}" alt="{{ date.news.club }}"/>
|
||||||
|
{% endif %}
|
||||||
|
<div class="header_content">
|
||||||
|
<h4>
|
||||||
|
<a href="{{ url('com:news_detail', news_id=date.news_id) }}">
|
||||||
|
{{ date.news.title }}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<a href="{{ date.news.club.get_absolute_url() }}">{{ date.news.club }}</a>
|
||||||
|
<div class="news_date">
|
||||||
|
<time datetime="{{ date.start_date.isoformat(timespec="seconds") }}">
|
||||||
|
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}
|
||||||
|
</time> -
|
||||||
|
<time datetime="{{ date.end_date.isoformat(timespec="seconds") }}">
|
||||||
|
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="news_content markdown">
|
||||||
|
{{ date.news.summary|markdown }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="news_empty">
|
<div class="news_empty">
|
||||||
<em>{% trans %}Nothing to come...{% endtrans %}</em>
|
<em>{% trans %}Nothing to come...{% endtrans %}</em>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
|
|
||||||
<h3>
|
<h3>
|
||||||
{% trans %}All coming events{% endtrans %}
|
{% trans %}All coming events{% endtrans %}
|
||||||
@ -110,18 +132,26 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<i class="fa-brands fa-discord fa-xl"></i>
|
<i class="fa-brands fa-discord fa-xl"></i>
|
||||||
<a rel="nofollow" target="#" href="https://discord.gg/QvTm3XJrHR">{% trans %}Discord AE{% endtrans %}</a>
|
<a rel="nofollow" target="#" href="https://discord.gg/QvTm3XJrHR">
|
||||||
|
{% trans %}Discord AE{% endtrans %}
|
||||||
|
</a>
|
||||||
{% if user.was_subscribed %}
|
{% if user.was_subscribed %}
|
||||||
- <a rel="nofollow" target="#" href="https://discord.gg/u6EuMfyGaJ">{% trans %}Dev Team{% endtrans %}</a>
|
- <a rel="nofollow" target="#" href="https://discord.gg/u6EuMfyGaJ">
|
||||||
|
{% trans %}Dev Team{% endtrans %}
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<i class="fa-brands fa-facebook fa-xl"></i>
|
<i class="fa-brands fa-facebook fa-xl"></i>
|
||||||
<a rel="nofollow" target="#" href="https://www.facebook.com/@AEUTBM/">{% trans %}Facebook{% endtrans %}</a>
|
<a rel="nofollow" target="#" href="https://www.facebook.com/@AEUTBM/">
|
||||||
|
Facebook
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<i class="fa-brands fa-square-instagram fa-xl"></i>
|
<i class="fa-brands fa-square-instagram fa-xl"></i>
|
||||||
<a rel="nofollow" target="#" href="https://www.instagram.com/ae_utbm">{% trans %}Instagram{% endtrans %}</a>
|
<a rel="nofollow" target="#" href="https://www.instagram.com/ae_utbm">
|
||||||
|
Instagram
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -130,7 +160,7 @@
|
|||||||
<div id="birthdays">
|
<div id="birthdays">
|
||||||
<h3>{% trans %}Birthdays{% endtrans %}</h3>
|
<h3>{% trans %}Birthdays{% endtrans %}</h3>
|
||||||
<div id="birthdays_content">
|
<div id="birthdays_content">
|
||||||
{%- if user.was_subscribed -%}
|
{%- if user.has_perm("core.view_user") -%}
|
||||||
<ul class="birthdays_year">
|
<ul class="birthdays_year">
|
||||||
{%- for year, users in birthdays -%}
|
{%- for year, users in birthdays -%}
|
||||||
<li>
|
<li>
|
||||||
@ -143,8 +173,13 @@
|
|||||||
</li>
|
</li>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</ul>
|
</ul>
|
||||||
{%- else -%}
|
{%- elif not user.was_subscribed -%}
|
||||||
|
{# The user cannot view birthdays, because he never subscribed #}
|
||||||
<p>{% trans %}You need to subscribe to access this content{% endtrans %}</p>
|
<p>{% trans %}You need to subscribe to access this content{% endtrans %}</p>
|
||||||
|
{%- else -%}
|
||||||
|
{# There is another reason why user cannot view birthdays (maybe he is banned)
|
||||||
|
but we cannot know exactly what is this reason #}
|
||||||
|
<p>{% trans %}You cannot access this content{% endtrans %}</p>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,12 +6,16 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.test.client import Client
|
from django.test.client import Client
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from model_bakery import baker
|
||||||
|
|
||||||
from com.calendar import IcsCalendar
|
from com.calendar import IcsCalendar
|
||||||
|
from com.models import News
|
||||||
|
from core.models import User
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -117,3 +121,66 @@ class TestInternalCalendar:
|
|||||||
out_file = accel_redirect_to_file(response)
|
out_file = accel_redirect_to_file(response)
|
||||||
assert out_file is not None
|
assert out_file is not None
|
||||||
assert out_file.exists()
|
assert out_file.exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestModerateNews:
|
||||||
|
@pytest.mark.parametrize("news_is_moderated", [True, False])
|
||||||
|
def test_moderation_ok(self, client: Client, news_is_moderated: bool): # noqa FBT
|
||||||
|
user = baker.make(
|
||||||
|
User, user_permissions=[Permission.objects.get(codename="moderate_news")]
|
||||||
|
)
|
||||||
|
# The API call should work even if the news is initially moderated.
|
||||||
|
# In the latter case, the result should be a noop, rather than an error.
|
||||||
|
news = baker.make(News, is_moderated=news_is_moderated)
|
||||||
|
initial_moderator = news.moderator
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.patch(
|
||||||
|
reverse("api:moderate_news", kwargs={"news_id": news.id})
|
||||||
|
)
|
||||||
|
# if it wasn't moderated, it should now be moderated and the moderator should
|
||||||
|
# be the user that made the request.
|
||||||
|
# If it was already moderated, it should be a no-op, but not an error
|
||||||
|
assert response.status_code == 200
|
||||||
|
news.refresh_from_db()
|
||||||
|
assert news.is_moderated
|
||||||
|
if not news_is_moderated:
|
||||||
|
assert news.moderator == user
|
||||||
|
else:
|
||||||
|
assert news.moderator == initial_moderator
|
||||||
|
|
||||||
|
def test_moderation_forbidden(self, client: Client):
|
||||||
|
user = baker.make(User)
|
||||||
|
news = baker.make(News, is_moderated=False)
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.patch(
|
||||||
|
reverse("api:moderate_news", kwargs={"news_id": news.id})
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
news.refresh_from_db()
|
||||||
|
assert not news.is_moderated
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestDeleteNews:
|
||||||
|
def test_delete_news_ok(self, client: Client):
|
||||||
|
user = baker.make(
|
||||||
|
User, user_permissions=[Permission.objects.get(codename="delete_news")]
|
||||||
|
)
|
||||||
|
news = baker.make(News)
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.delete(
|
||||||
|
reverse("api:delete_news", kwargs={"news_id": news.id})
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert not News.objects.filter(id=news.id).exists()
|
||||||
|
|
||||||
|
def test_delete_news_forbidden(self, client: Client):
|
||||||
|
user = baker.make(User)
|
||||||
|
news = baker.make(News)
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.delete(
|
||||||
|
reverse("api:delete_news", kwargs={"news_id": news.id})
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert News.objects.filter(id=news.id).exists()
|
||||||
|
37
com/views.py
37
com/views.py
@ -37,9 +37,9 @@ from django.http import HttpResponseRedirect
|
|||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.timezone import localdate
|
from django.utils.timezone import localdate, now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DetailView, ListView, View
|
from django.views.generic import DetailView, ListView, TemplateView, View
|
||||||
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||||
|
|
||||||
from club.models import Club, Mailing
|
from club.models import Club, Mailing
|
||||||
@ -236,28 +236,37 @@ class NewsAdminListView(PermissionRequiredMixin, ListView):
|
|||||||
permission_required = ["com.moderate_news", "com.delete_news"]
|
permission_required = ["com.moderate_news", "com.delete_news"]
|
||||||
|
|
||||||
|
|
||||||
class NewsListView(ListView):
|
class NewsListView(TemplateView):
|
||||||
model = News
|
|
||||||
template_name = "com/news_list.jinja"
|
template_name = "com/news_list.jinja"
|
||||||
queryset = News.objects.filter(is_moderated=True)
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_birthdays(self):
|
||||||
return super().get_queryset().viewable_by(self.request.user)
|
if not self.request.user.has_perm("core.view_user"):
|
||||||
|
return []
|
||||||
def get_context_data(self, **kwargs):
|
return itertools.groupby(
|
||||||
kwargs = super().get_context_data(**kwargs)
|
|
||||||
kwargs["NewsDate"] = NewsDate
|
|
||||||
kwargs["timedelta"] = timedelta
|
|
||||||
kwargs["birthdays"] = itertools.groupby(
|
|
||||||
User.objects.filter(
|
User.objects.filter(
|
||||||
date_of_birth__month=localdate().month,
|
date_of_birth__month=localdate().month,
|
||||||
date_of_birth__day=localdate().day,
|
date_of_birth__day=localdate().day,
|
||||||
|
is_subscriber_viewable=True,
|
||||||
)
|
)
|
||||||
.filter(role__in=["STUDENT", "FORMER STUDENT"])
|
.filter(role__in=["STUDENT", "FORMER STUDENT"])
|
||||||
.order_by("-date_of_birth"),
|
.order_by("-date_of_birth"),
|
||||||
key=lambda u: u.date_of_birth.year,
|
key=lambda u: u.date_of_birth.year,
|
||||||
)
|
)
|
||||||
return kwargs
|
|
||||||
|
def get_news_dates(self):
|
||||||
|
return itertools.groupby(
|
||||||
|
NewsDate.objects.viewable_by(self.request.user)
|
||||||
|
.filter(end_date__gt=now(), start_date__lt=now() + timedelta(days=6))
|
||||||
|
.order_by("start_date")
|
||||||
|
.select_related("news", "news__club"),
|
||||||
|
key=lambda d: d.start_date.date(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return super().get_context_data(**kwargs) | {
|
||||||
|
"news_dates": self.get_news_dates(),
|
||||||
|
"birthdays": self.get_birthdays(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NewsDetailView(CanViewMixin, DetailView):
|
class NewsDetailView(CanViewMixin, DetailView):
|
||||||
|
@ -244,6 +244,20 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.btn-green {
|
||||||
|
$bg-color: rgba(0, 210, 83, 0.4);
|
||||||
|
background-color: $bg-color;
|
||||||
|
color: $black-color;
|
||||||
|
|
||||||
|
&:not(:disabled):hover {
|
||||||
|
background-color: darken($bg-color, 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: lighten($bg-color, 15%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.btn-red {
|
&.btn-red {
|
||||||
background-color: #fc8181;
|
background-color: #fc8181;
|
||||||
color: black;
|
color: black;
|
||||||
|
@ -39,14 +39,6 @@
|
|||||||
<a rel="nofollow" target="#" class="share_button twitter" href="https://twitter.com/intent/tweet?text={{ news.get_full_url() }}">{% trans %}Tweet{% endtrans %}</a>
|
<a rel="nofollow" target="#" class="share_button twitter" href="https://twitter.com/intent/tweet?text={{ news.get_full_url() }}">{% trans %}Tweet{% endtrans %}</a>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro fb_quick(news) -%}
|
|
||||||
<a rel="nofollow" target="#" href="https://www.facebook.com/sharer/sharer.php?u={{ news.get_full_url() }}" class="fb fa-brands fa-facebook fa-2x"></a>
|
|
||||||
{%- endmacro %}
|
|
||||||
|
|
||||||
{% macro tweet_quick(news) -%}
|
|
||||||
<a rel="nofollow" target="#" href="https://twitter.com/intent/tweet?text={{ news.get_full_url() }}" class="twitter fa-brands fa-twitter-square fa-2x"></a>
|
|
||||||
{%- endmacro %}
|
|
||||||
|
|
||||||
{% macro user_mini_profile(user) %}
|
{% macro user_mini_profile(user) %}
|
||||||
<div class="user_mini_profile">
|
<div class="user_mini_profile">
|
||||||
<div class="user_mini_profile_infos">
|
<div class="user_mini_profile_infos">
|
||||||
|
@ -310,7 +310,7 @@ msgstr "Compte en banque : "
|
|||||||
#: accounting/templates/accounting/club_account_details.jinja
|
#: accounting/templates/accounting/club_account_details.jinja
|
||||||
#: accounting/templates/accounting/label_list.jinja
|
#: accounting/templates/accounting/label_list.jinja
|
||||||
#: club/templates/club/club_sellings.jinja club/templates/club/mailing.jinja
|
#: club/templates/club/club_sellings.jinja club/templates/club/mailing.jinja
|
||||||
#: com/templates/com/mailing_admin.jinja
|
#: com/templates/com/macros.jinja com/templates/com/mailing_admin.jinja
|
||||||
#: com/templates/com/news_admin_list.jinja com/templates/com/poster_edit.jinja
|
#: com/templates/com/news_admin_list.jinja com/templates/com/poster_edit.jinja
|
||||||
#: com/templates/com/screen_edit.jinja com/templates/com/weekmail.jinja
|
#: com/templates/com/screen_edit.jinja com/templates/com/weekmail.jinja
|
||||||
#: core/templates/core/file_detail.jinja
|
#: core/templates/core/file_detail.jinja
|
||||||
@ -1263,7 +1263,7 @@ msgstr "Format : 16:9 | Résolution : 1920x1080"
|
|||||||
msgid "Start date"
|
msgid "Start date"
|
||||||
msgstr "Date de début"
|
msgstr "Date de début"
|
||||||
|
|
||||||
#: com/forms.py
|
#: com/forms.py com/templates/com/macros.jinja
|
||||||
msgid "Weekly event"
|
msgid "Weekly event"
|
||||||
msgstr "Événement Hebdomadaire"
|
msgstr "Événement Hebdomadaire"
|
||||||
|
|
||||||
@ -1404,12 +1404,35 @@ msgstr "temps d'affichage"
|
|||||||
msgid "Begin date should be before end date"
|
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/mailing_admin.jinja com/views.py
|
#: com/templates/com/macros.jinja
|
||||||
#: core/templates/core/user_tools.jinja
|
msgid "Waiting moderation"
|
||||||
msgid "Mailing lists administration"
|
msgstr "En attente de modération"
|
||||||
msgstr "Administration des mailing listes"
|
|
||||||
|
|
||||||
#: com/templates/com/mailing_admin.jinja
|
#: com/templates/com/macros.jinja
|
||||||
|
msgid ""
|
||||||
|
"This news isn't moderated and is visible only by its author and the "
|
||||||
|
"communication admins."
|
||||||
|
msgstr ""
|
||||||
|
"Cette nouvelle n'est pas modérée et n'est visible que par son auteur et les "
|
||||||
|
"admins communication."
|
||||||
|
|
||||||
|
#: com/templates/com/macros.jinja
|
||||||
|
msgid "It will stay hidden for other users until it has been moderated."
|
||||||
|
msgstr ""
|
||||||
|
"Elle sera cachée pour les autres utilisateurs tant qu'elle ne sera pas "
|
||||||
|
"modérée."
|
||||||
|
|
||||||
|
#: com/templates/com/macros.jinja
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"This event will take place every week for %(nb)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 %(nb)s semaines. Si vous "
|
||||||
|
"modérez ou supprimez cet événement, il sera également modéré (ou supprimé) "
|
||||||
|
"pour les semaines suivantes."
|
||||||
|
|
||||||
|
#: com/templates/com/macros.jinja com/templates/com/mailing_admin.jinja
|
||||||
#: com/templates/com/news_admin_list.jinja com/templates/com/news_detail.jinja
|
#: com/templates/com/news_admin_list.jinja com/templates/com/news_detail.jinja
|
||||||
#: core/templates/core/file_detail.jinja
|
#: core/templates/core/file_detail.jinja
|
||||||
#: core/templates/core/file_moderation.jinja sas/templates/sas/moderation.jinja
|
#: core/templates/core/file_moderation.jinja sas/templates/sas/moderation.jinja
|
||||||
@ -1417,6 +1440,19 @@ msgstr "Administration des mailing listes"
|
|||||||
msgid "Moderate"
|
msgid "Moderate"
|
||||||
msgstr "Modérer"
|
msgstr "Modérer"
|
||||||
|
|
||||||
|
#: com/templates/com/macros.jinja
|
||||||
|
msgid "News moderated"
|
||||||
|
msgstr "Nouvelle modérée"
|
||||||
|
|
||||||
|
#: com/templates/com/macros.jinja
|
||||||
|
msgid "News deleted"
|
||||||
|
msgstr "Nouvelle supprimée"
|
||||||
|
|
||||||
|
#: com/templates/com/mailing_admin.jinja com/views.py
|
||||||
|
#: core/templates/core/user_tools.jinja
|
||||||
|
msgid "Mailing lists administration"
|
||||||
|
msgstr "Administration des mailing listes"
|
||||||
|
|
||||||
#: 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"
|
||||||
@ -1574,14 +1610,6 @@ msgstr "Discord AE"
|
|||||||
msgid "Dev Team"
|
msgid "Dev Team"
|
||||||
msgstr "Pôle Informatique"
|
msgstr "Pôle Informatique"
|
||||||
|
|
||||||
#: com/templates/com/news_list.jinja
|
|
||||||
msgid "Facebook"
|
|
||||||
msgstr "Facebook"
|
|
||||||
|
|
||||||
#: com/templates/com/news_list.jinja
|
|
||||||
msgid "Instagram"
|
|
||||||
msgstr "Instagram"
|
|
||||||
|
|
||||||
#: com/templates/com/news_list.jinja
|
#: com/templates/com/news_list.jinja
|
||||||
msgid "Birthdays"
|
msgid "Birthdays"
|
||||||
msgstr "Anniversaires"
|
msgstr "Anniversaires"
|
||||||
@ -1595,6 +1623,10 @@ msgstr "%(age)s ans"
|
|||||||
msgid "You need to subscribe to access this content"
|
msgid "You need to subscribe to access this content"
|
||||||
msgstr "Vous devez cotiser pour accéder à ce contenu"
|
msgstr "Vous devez cotiser pour accéder à ce contenu"
|
||||||
|
|
||||||
|
#: com/templates/com/news_list.jinja
|
||||||
|
msgid "You cannot access this content"
|
||||||
|
msgstr "Vous n'avez pas accès à ce contenu"
|
||||||
|
|
||||||
#: com/templates/com/poster_edit.jinja com/templates/com/poster_list.jinja
|
#: com/templates/com/poster_edit.jinja com/templates/com/poster_list.jinja
|
||||||
msgid "Poster"
|
msgid "Poster"
|
||||||
msgstr "Affiche"
|
msgstr "Affiche"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user