mirror of
https://github.com/ae-utbm/sith.git
synced 2025-02-21 23:17:14 +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 com.calendar import IcsCalendar
|
||||
from com.models import News
|
||||
from core.auth.api_permissions import HasPerm
|
||||
from core.views.files import send_raw_file
|
||||
|
||||
|
||||
@ -17,7 +19,7 @@ class CalendarController(ControllerBase):
|
||||
"""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.
|
||||
|
||||
@ -30,3 +32,27 @@ class CalendarController(ControllerBase):
|
||||
@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()
|
||||
|
@ -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.
|
||||
|
||||
@ -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")
|
||||
|
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
.header_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: .2rem;
|
||||
|
||||
.news_date {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.news_content {
|
||||
clear: left;
|
||||
|
||||
.button_bar {
|
||||
text-align: right;
|
||||
|
||||
.fb {
|
||||
color: $faceblue;
|
||||
}
|
||||
|
||||
.twitter {
|
||||
color: $twitblue;
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
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" %}
|
||||
{% 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 %}
|
||||
{% trans %}News{% endtrans %} -
|
||||
@ -16,39 +17,49 @@
|
||||
<link rel="stylesheet" href="{{ static('com/css/news-detail.scss') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_js %}
|
||||
<script type="module" src={{ static("bundled/com/components/moderation-alert-index.ts") }}></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p><a href="{{ url('com:news_list') }}">{% trans %}Back to news{% endtrans %}</a></p>
|
||||
<section id="news_details">
|
||||
<div class="club_logo">
|
||||
<img src="{{ link_news_logo(news)}}" alt="{{ news.club }}" />
|
||||
<a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a>
|
||||
</div>
|
||||
<h4>{{ news.title }}</h4>
|
||||
<p class="date">
|
||||
<span>{{ date.start_date|localtime|date(DATETIME_FORMAT) }}
|
||||
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
||||
<span>{{ date.end_date|localtime|date(DATETIME_FORMAT) }}
|
||||
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||
</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 x-data="{newsState: AlertState.PENDING}">
|
||||
|
||||
{% if not news.is_moderated %}
|
||||
{{ news_moderation_alert(news, user, "newsState") }}
|
||||
{% endif %}
|
||||
<article id="news_details" x-show="newsState !== AlertState.DELETED">
|
||||
<div class="club_logo">
|
||||
<img src="{{ link_news_logo(news)}}" alt="{{ news.club }}" />
|
||||
<a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<h4>{{ news.title }}</h4>
|
||||
<p class="date">
|
||||
<span>{{ date.start_date|localtime|date(DATETIME_FORMAT) }}
|
||||
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
||||
<span>{{ date.end_date|localtime|date(DATETIME_FORMAT) }}
|
||||
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||
</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 %}
|
||||
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
{% from 'core/macros.jinja' import tweet_quick, fb_quick %}
|
||||
{% from "com/macros.jinja" import news_moderation_alert %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}News{% endtrans %}
|
||||
@ -15,13 +15,12 @@
|
||||
|
||||
{% block additional_js %}
|
||||
<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 %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="news">
|
||||
<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>
|
||||
{% trans %}Events today and the next few days{% endtrans %}
|
||||
<a target="#" href="{{ url("com:news_feed") }}"><i class="fa fa-rss feed"></i></a>
|
||||
@ -33,51 +32,74 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% if events_dates %}
|
||||
{% for d in events_dates %}
|
||||
<div class="news_events_group">
|
||||
<div class="news_events_group_date">
|
||||
<div>
|
||||
<div>{{ d|localtime|date('D') }}</div>
|
||||
<div class="day">{{ d|localtime|date('d') }}</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 %}
|
||||
{% endif %}
|
||||
{% for day, dates_group in news_dates %}
|
||||
<div class="news_events_group">
|
||||
<div class="news_events_group_date">
|
||||
<div>
|
||||
<div>{{ day|date('D') }}</div>
|
||||
<div class="day">{{ day|date('d') }}</div>
|
||||
<div>{{ day|date('b') }}</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 %}
|
||||
<div class="news_empty">
|
||||
<em>{% trans %}Nothing to come...{% endtrans %}</em>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<h3>
|
||||
{% trans %}All coming events{% endtrans %}
|
||||
@ -110,18 +132,26 @@
|
||||
<ul>
|
||||
<li>
|
||||
<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 %}
|
||||
- <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 %}
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
@ -130,7 +160,7 @@
|
||||
<div id="birthdays">
|
||||
<h3>{% trans %}Birthdays{% endtrans %}</h3>
|
||||
<div id="birthdays_content">
|
||||
{%- if user.was_subscribed -%}
|
||||
{%- if user.has_perm("core.view_user") -%}
|
||||
<ul class="birthdays_year">
|
||||
{%- for year, users in birthdays -%}
|
||||
<li>
|
||||
@ -143,8 +173,13 @@
|
||||
</li>
|
||||
{%- endfor -%}
|
||||
</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>
|
||||
{%- 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 -%}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,12 +6,16 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.http import HttpResponse
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from model_bakery import baker
|
||||
|
||||
from com.calendar import IcsCalendar
|
||||
from com.models import News
|
||||
from core.models import User
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -117,3 +121,66 @@ class TestInternalCalendar:
|
||||
out_file = accel_redirect_to_file(response)
|
||||
assert out_file is not None
|
||||
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.urls import reverse, reverse_lazy
|
||||
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.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 club.models import Club, Mailing
|
||||
@ -236,28 +236,37 @@ class NewsAdminListView(PermissionRequiredMixin, ListView):
|
||||
permission_required = ["com.moderate_news", "com.delete_news"]
|
||||
|
||||
|
||||
class NewsListView(ListView):
|
||||
model = News
|
||||
class NewsListView(TemplateView):
|
||||
template_name = "com/news_list.jinja"
|
||||
queryset = News.objects.filter(is_moderated=True)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().viewable_by(self.request.user)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["NewsDate"] = NewsDate
|
||||
kwargs["timedelta"] = timedelta
|
||||
kwargs["birthdays"] = itertools.groupby(
|
||||
def get_birthdays(self):
|
||||
if not self.request.user.has_perm("core.view_user"):
|
||||
return []
|
||||
return itertools.groupby(
|
||||
User.objects.filter(
|
||||
date_of_birth__month=localdate().month,
|
||||
date_of_birth__day=localdate().day,
|
||||
is_subscriber_viewable=True,
|
||||
)
|
||||
.filter(role__in=["STUDENT", "FORMER STUDENT"])
|
||||
.order_by("-date_of_birth"),
|
||||
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):
|
||||
|
@ -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 {
|
||||
background-color: #fc8181;
|
||||
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>
|
||||
{%- 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) %}
|
||||
<div class="user_mini_profile">
|
||||
<div class="user_mini_profile_infos">
|
||||
|
@ -310,7 +310,7 @@ msgstr "Compte en banque : "
|
||||
#: accounting/templates/accounting/club_account_details.jinja
|
||||
#: accounting/templates/accounting/label_list.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/screen_edit.jinja com/templates/com/weekmail.jinja
|
||||
#: core/templates/core/file_detail.jinja
|
||||
@ -1263,7 +1263,7 @@ msgstr "Format : 16:9 | Résolution : 1920x1080"
|
||||
msgid "Start date"
|
||||
msgstr "Date de début"
|
||||
|
||||
#: com/forms.py
|
||||
#: com/forms.py com/templates/com/macros.jinja
|
||||
msgid "Weekly event"
|
||||
msgstr "Événement Hebdomadaire"
|
||||
|
||||
@ -1404,12 +1404,35 @@ msgstr "temps d'affichage"
|
||||
msgid "Begin date should be before end date"
|
||||
msgstr "La date de début doit être avant celle de fin"
|
||||
|
||||
#: 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/macros.jinja
|
||||
msgid "Waiting moderation"
|
||||
msgstr "En attente de modération"
|
||||
|
||||
#: 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
|
||||
#: core/templates/core/file_detail.jinja
|
||||
#: core/templates/core/file_moderation.jinja sas/templates/sas/moderation.jinja
|
||||
@ -1417,6 +1440,19 @@ msgstr "Administration des mailing listes"
|
||||
msgid "Moderate"
|
||||
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
|
||||
#, python-format
|
||||
msgid "Moderated by %(user)s"
|
||||
@ -1574,14 +1610,6 @@ msgstr "Discord AE"
|
||||
msgid "Dev Team"
|
||||
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
|
||||
msgid "Birthdays"
|
||||
msgstr "Anniversaires"
|
||||
@ -1595,6 +1623,10 @@ msgstr "%(age)s ans"
|
||||
msgid "You need to subscribe to access this content"
|
||||
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
|
||||
msgid "Poster"
|
||||
msgstr "Affiche"
|
||||
|
Loading…
x
Reference in New Issue
Block a user