mirror of
https://github.com/ae-utbm/sith.git
synced 2025-02-27 09:57:09 +00:00
Merge pull request #1024 from ae-utbm/news-list
Allow displaying more news
This commit is contained in:
commit
e936f0d285
@ -7,3 +7,17 @@ class ClubSchema(ModelSchema):
|
||||
class Meta:
|
||||
model = Club
|
||||
fields = ["id", "name"]
|
||||
|
||||
|
||||
class ClubProfileSchema(ModelSchema):
|
||||
"""The infos needed to display a simple club profile."""
|
||||
|
||||
class Meta:
|
||||
model = Club
|
||||
fields = ["id", "name", "logo"]
|
||||
|
||||
url: str
|
||||
|
||||
@staticmethod
|
||||
def resolve_url(obj: Club) -> str:
|
||||
return obj.get_absolute_url()
|
||||
|
30
com/api.py
30
com/api.py
@ -1,11 +1,16 @@
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from ninja_extra import ControllerBase, api_controller, route
|
||||
from ninja import Query
|
||||
from ninja_extra import ControllerBase, api_controller, paginate, route
|
||||
from ninja_extra.pagination import PageNumberPaginationExtra
|
||||
from ninja_extra.schemas import PaginatedResponseSchema
|
||||
|
||||
from com.calendar import IcsCalendar
|
||||
from com.models import News
|
||||
from com.models import News, NewsDate
|
||||
from com.schemas import NewsDateFilterSchema, NewsDateSchema
|
||||
from core.auth.api_permissions import HasPerm
|
||||
from core.views.files import send_raw_file
|
||||
|
||||
@ -37,7 +42,7 @@ class CalendarController(ControllerBase):
|
||||
@api_controller("/news")
|
||||
class NewsController(ControllerBase):
|
||||
@route.patch(
|
||||
"/{news_id}/moderate",
|
||||
"/{int:news_id}/moderate",
|
||||
permissions=[HasPerm("com.moderate_news")],
|
||||
url_name="moderate_news",
|
||||
)
|
||||
@ -49,10 +54,27 @@ class NewsController(ControllerBase):
|
||||
news.save()
|
||||
|
||||
@route.delete(
|
||||
"/{news_id}",
|
||||
"/{int: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()
|
||||
|
||||
@route.get(
|
||||
"/date",
|
||||
url_name="fetch_news_dates",
|
||||
response=PaginatedResponseSchema[NewsDateSchema],
|
||||
)
|
||||
@paginate(PageNumberPaginationExtra, page_size=50)
|
||||
def fetch_news_dates(
|
||||
self,
|
||||
filters: Query[NewsDateFilterSchema],
|
||||
text_format: Literal["md", "html"] = "md",
|
||||
):
|
||||
return filters.filter(
|
||||
NewsDate.objects.viewable_by(self.context.request.user)
|
||||
.order_by("start_date")
|
||||
.select_related("news", "news__club")
|
||||
)
|
||||
|
58
com/schemas.py
Normal file
58
com/schemas.py
Normal file
@ -0,0 +1,58 @@
|
||||
from datetime import datetime
|
||||
|
||||
from ninja import FilterSchema, ModelSchema
|
||||
from ninja_extra import service_resolver
|
||||
from ninja_extra.controllers import RouteContext
|
||||
from pydantic import Field
|
||||
|
||||
from club.schemas import ClubProfileSchema
|
||||
from com.models import News, NewsDate
|
||||
from core.markdown import markdown
|
||||
|
||||
|
||||
class NewsDateFilterSchema(FilterSchema):
|
||||
before: datetime | None = Field(None, q="end_date__lt")
|
||||
after: datetime | None = Field(None, q="start_date__gt")
|
||||
club_id: int | None = Field(None, q="news__club_id")
|
||||
news_id: int | None = None
|
||||
is_moderated: bool | None = Field(None, q="news__is_moderated")
|
||||
title: str | None = Field(None, q="news__title__icontains")
|
||||
|
||||
|
||||
class NewsSchema(ModelSchema):
|
||||
class Meta:
|
||||
model = News
|
||||
fields = ["id", "title", "summary", "is_moderated"]
|
||||
|
||||
club: ClubProfileSchema
|
||||
url: str
|
||||
|
||||
@staticmethod
|
||||
def resolve_summary(obj: News) -> str:
|
||||
# if this is returned from a route that allows the
|
||||
# user to choose the text format (md or html)
|
||||
# and the user chose "html", convert the markdown to html
|
||||
context: RouteContext = service_resolver(RouteContext)
|
||||
if context.kwargs.get("text_format", "") == "html":
|
||||
return markdown(obj.summary)
|
||||
return obj.summary
|
||||
|
||||
@staticmethod
|
||||
def resolve_url(obj: News) -> str:
|
||||
return obj.get_absolute_url()
|
||||
|
||||
|
||||
class NewsDateSchema(ModelSchema):
|
||||
"""Basic infos about an event occurrence.
|
||||
|
||||
Warning:
|
||||
This uses [NewsSchema][], which itself
|
||||
uses [ClubProfileSchema][club.schemas.ClubProfileSchema].
|
||||
Don't forget the appropriated `select_related`.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = NewsDate
|
||||
fields = ["id", "start_date", "end_date"]
|
||||
|
||||
news: NewsSchema
|
@ -1,5 +1,5 @@
|
||||
import { exportToHtml } from "#core:utils/globals";
|
||||
import { newsDeleteNews, newsModerateNews } from "#openapi";
|
||||
import { newsDeleteNews, newsFetchNewsDates, newsModerateNews } from "#openapi";
|
||||
|
||||
// This will be used in jinja templates,
|
||||
// so we cannot use real enums as those are purely an abstraction of Typescript
|
||||
@ -24,6 +24,7 @@ document.addEventListener("alpine:init", () => {
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake case
|
||||
await newsModerateNews({ path: { news_id: this.newsId } });
|
||||
this.state = AlertState.MODERATED;
|
||||
this.$dispatch("news-moderated", { newsId: this.newsId, state: this.state });
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
@ -32,7 +33,47 @@ document.addEventListener("alpine:init", () => {
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake case
|
||||
await newsDeleteNews({ path: { news_id: this.newsId } });
|
||||
this.state = AlertState.DELETED;
|
||||
this.$dispatch("news-moderated", { newsId: this.newsId, state: this.state });
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Event receiver for when news dates are moderated.
|
||||
*
|
||||
* If the moderated date is linked to the same news
|
||||
* as the one this moderation alert is attached to,
|
||||
* then set the alert state to the same as the moderated one.
|
||||
*/
|
||||
dispatchModeration(event: CustomEvent) {
|
||||
if (event.detail.newsId === this.newsId) {
|
||||
this.state = event.detail.state;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Query the server to know the number of news dates that would be moderated
|
||||
* if this one is moderated.
|
||||
*/
|
||||
async nbToModerate(): Promise<number> {
|
||||
// What we want here is the count attribute of the response.
|
||||
// We don't care about the actual results,
|
||||
// so we ask for the minimum page size possible.
|
||||
const response = await newsFetchNewsDates({
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake-case
|
||||
query: { news_id: this.newsId, page: 1, page_size: 1 },
|
||||
});
|
||||
return response.data.count;
|
||||
},
|
||||
|
||||
weeklyEventWarningMessage(nbEvents: number): string {
|
||||
return interpolate(
|
||||
gettext(
|
||||
"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.",
|
||||
),
|
||||
[nbEvents],
|
||||
);
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
@ -0,0 +1,67 @@
|
||||
import { type NewsDateSchema, newsFetchNewsDates } from "#openapi";
|
||||
|
||||
interface ParsedNewsDateSchema extends Omit<NewsDateSchema, "start_date" | "end_date"> {
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake_case
|
||||
start_date: Date;
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake_case
|
||||
end_date: Date;
|
||||
}
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("upcomingNewsLoader", (startDate: Date) => ({
|
||||
startDate: startDate,
|
||||
currentPage: 1,
|
||||
pageSize: 6,
|
||||
hasNext: true,
|
||||
loading: false,
|
||||
newsDates: [] as NewsDateSchema[],
|
||||
|
||||
async loadMore() {
|
||||
this.loading = true;
|
||||
const response = await newsFetchNewsDates({
|
||||
query: {
|
||||
after: this.startDate.toISOString(),
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake_case
|
||||
text_format: "html",
|
||||
page: this.currentPage,
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake_case
|
||||
page_size: this.pageSize,
|
||||
},
|
||||
});
|
||||
if (response.response.status === 404) {
|
||||
this.hasNext = false;
|
||||
} else if (response.data.next === null) {
|
||||
this.newsDates.push(...response.data.results);
|
||||
this.hasNext = false;
|
||||
} else {
|
||||
this.newsDates.push(...response.data.results);
|
||||
this.currentPage += 1;
|
||||
}
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
groupedDates(): Record<string, NewsDateSchema[]> {
|
||||
return this.newsDates
|
||||
.map(
|
||||
(date: NewsDateSchema): ParsedNewsDateSchema => ({
|
||||
...date,
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake_case
|
||||
start_date: new Date(date.start_date),
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake_case
|
||||
end_date: new Date(date.end_date),
|
||||
}),
|
||||
)
|
||||
.reduce(
|
||||
(acc: Record<string, ParsedNewsDateSchema[]>, date: ParsedNewsDateSchema) => {
|
||||
const key = date.start_date.toDateString();
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(date);
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
},
|
||||
}));
|
||||
});
|
@ -51,6 +51,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* UPCOMING EVENTS */
|
||||
|
||||
#upcoming-events {
|
||||
max-height: 600px;
|
||||
overflow-y: scroll;
|
||||
|
||||
#load-more-news-button {
|
||||
text-align: center;
|
||||
button {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* LINKS/BIRTHDAYS */
|
||||
#links,
|
||||
#birthdays {
|
||||
|
@ -6,15 +6,41 @@
|
||||
the given `alpineState` variable.
|
||||
This state is a `AlertState`, as defined in `moderation-alert-index.ts`
|
||||
|
||||
Example :
|
||||
This comes in three flavours :
|
||||
- You can pass the `News` object itself to the macro.
|
||||
In this case, if `request.user` can moderate news,
|
||||
it will perform an additional db query to know if it is a recurring event.
|
||||
- You can also give only the news id.
|
||||
In this case, a server request will be issued to know
|
||||
if it is a recurring event.
|
||||
- Finally, you can pass the name of an alpine variable, which value is the id.
|
||||
In this case, a server request will be issued to know
|
||||
if it is a recurring event.
|
||||
|
||||
Example with full `News` object :
|
||||
```jinja
|
||||
<div x-data="{state: AlertState.PENDING}">
|
||||
{{ news_moderation_alert(news, user, "state") }}
|
||||
</div>
|
||||
```
|
||||
With an id :
|
||||
```jinja
|
||||
<div x-data="{state: AlertState.PENDING}">
|
||||
{{ news_moderation_alert(news.id, user, "state") }}
|
||||
</div>
|
||||
```
|
||||
An with an alpine variable
|
||||
```jinja
|
||||
<div x-data="{state: AlertState.PENDING, newsId: {{ news.id }}">
|
||||
{{ news_moderation_alert("newsId", user, "state") }}
|
||||
</div>
|
||||
```
|
||||
|
||||
|
||||
Args:
|
||||
news: The `News` object to which this alert is related
|
||||
news: (News | int | string)
|
||||
Either the `News` object to which this alert is related,
|
||||
or its id, or the name of an Alpine which value is its id
|
||||
user: The request.user
|
||||
alpineState: An alpine variable name
|
||||
|
||||
@ -23,7 +49,13 @@
|
||||
in your template.
|
||||
#}
|
||||
<div
|
||||
{% if news is integer or news is string %}
|
||||
x-data="moderationAlert({{ news }})"
|
||||
{% else %}
|
||||
x-data="moderationAlert({{ news.id }})"
|
||||
{% endif %}
|
||||
{# the news-moderated is received when a moderation alert is deleted or moderated #}
|
||||
@news-moderated.window="dispatchModeration($event)"
|
||||
{% if alpineState %}
|
||||
x-modelable="{{ alpineState }}"
|
||||
x-model="state"
|
||||
@ -49,18 +81,22 @@
|
||||
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 %}
|
||||
<div
|
||||
{% if news is integer or news is string %}
|
||||
x-data="{ nbEvents: 0 }"
|
||||
x-init="nbEvents = await nbToModerate()"
|
||||
{% else %}
|
||||
x-data="{ nbEvents: {{ news.dates.count() }} }"
|
||||
{% endif %}
|
||||
>
|
||||
<template x-if="nbEvents > 1">
|
||||
<div>
|
||||
<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 %}
|
||||
<p x-text="weeklyEventWarningMessage(nbEvents)"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if user.has_perm("com.moderate_news") %}
|
||||
|
@ -16,6 +16,7 @@
|
||||
{% 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>
|
||||
<script type="module" src={{ static("bundled/com/components/upcoming-news-loader-index.ts") }}></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -37,6 +38,12 @@
|
||||
</a>
|
||||
<br>
|
||||
{% endif %}
|
||||
<section id="upcoming-events">
|
||||
{% if not news_dates %}
|
||||
<div class="news_empty">
|
||||
<em>{% trans %}Nothing to come...{% endtrans %}</em>
|
||||
</div>
|
||||
{% else %}
|
||||
{% for day, dates_group in news_dates %}
|
||||
<div class="news_events_group">
|
||||
<div class="news_events_group_date">
|
||||
@ -95,11 +102,78 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="news_empty">
|
||||
<em>{% trans %}Nothing to come...{% endtrans %}</em>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div x-data="upcomingNewsLoader(new Date('{{ last_day + timedelta(days=1) }}'))">
|
||||
<template x-for="newsList in Object.values(groupedDates())">
|
||||
<div class="news_events_group">
|
||||
<div class="news_events_group_date">
|
||||
<div x-data="{day: newsList[0].start_date}">
|
||||
<div x-text="day.toLocaleString('{{ get_language() }}', { weekday: 'short' }).substring(0, 3)"></div>
|
||||
<div
|
||||
class="day"
|
||||
x-text="day.toLocaleString('{{ get_language() }}', { day: 'numeric' })"
|
||||
></div>
|
||||
<div x-text="day.toLocaleString('{{ get_language() }}', { month: 'short' }).substring(0, 3)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="news_events_group_items">
|
||||
<template x-for="newsDate in newsList" :key="newsDate.id">
|
||||
<article
|
||||
class="news_event"
|
||||
x-data="{ newsState: newsDate.news.is_moderated ? AlertState.MODERATED : AlertState.PENDING }"
|
||||
>
|
||||
<template x-if="!newsDate.news.is_moderated">
|
||||
{{ news_moderation_alert("newsDate.news.id", user, "newsState") }}
|
||||
</template>
|
||||
<div x-show="newsState !== AlertState.DELETED">
|
||||
<header class="row gap">
|
||||
<img
|
||||
:src="newsDate.news.club.logo || '{{ static("com/img/news.png") }}'"
|
||||
:alt="newsDate.news.club.name"
|
||||
/>
|
||||
<div class="header_content">
|
||||
<h4>
|
||||
<a :href="newsDate.news.url" x-text="newsDate.news.title"></a>
|
||||
</h4>
|
||||
<a :href="newsDate.news.club.url" x-text="newsDate.news.club.name"></a>
|
||||
<div class="news_date">
|
||||
<time
|
||||
:datetime="newsDate.start_date.toISOString()"
|
||||
x-text="`${newsDate.start_date.getHours()}:${newsDate.start_date.getMinutes()}`"
|
||||
></time> -
|
||||
<time
|
||||
:datetime="newsDate.end_date.toISOString()"
|
||||
x-text="`${newsDate.end_date.getHours()}:${newsDate.end_date.getMinutes()}`"
|
||||
></time>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{# The API returns a summary in html.
|
||||
It's generated from our markdown subset, so it should be safe #}
|
||||
<div class="news_content markdown" x-html="newsDate.news.summary"></div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div id="load-more-news-button" :aria-busy="loading">
|
||||
<button class="btn btn-grey" x-show="!loading && hasNext" @click="loadMore()">
|
||||
{% trans %}See more{% endtrans %} <i class="fa fa-arrow-down"></i>
|
||||
</button>
|
||||
<p x-show="!loading && !hasNext">
|
||||
<em>
|
||||
{% trans trimmed %}
|
||||
It was too short.
|
||||
You already reached the end of the upcoming events list.
|
||||
{% endtrans %}
|
||||
</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<h3>
|
||||
{% trans %}All coming events{% endtrans %}
|
||||
|
@ -3,18 +3,22 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from unittest.mock import MagicMock, patch
|
||||
from urllib.parse import quote
|
||||
|
||||
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.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from model_bakery import baker
|
||||
from django.utils.timezone import now
|
||||
from model_bakery import baker, seq
|
||||
from pytest_django.asserts import assertNumQueries
|
||||
|
||||
from com.calendar import IcsCalendar
|
||||
from com.models import News
|
||||
from com.models import News, NewsDate
|
||||
from core.markdown import markdown
|
||||
from core.models import User
|
||||
|
||||
|
||||
@ -184,3 +188,63 @@ class TestDeleteNews:
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert News.objects.filter(id=news.id).exists()
|
||||
|
||||
|
||||
class TestFetchNewsDates(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
News.objects.all().delete()
|
||||
cls.dates = baker.make(
|
||||
NewsDate,
|
||||
_quantity=5,
|
||||
_bulk_create=True,
|
||||
start_date=seq(value=now(), increment_by=timedelta(days=1)),
|
||||
end_date=seq(
|
||||
value=now() + timedelta(hours=2), increment_by=timedelta(days=1)
|
||||
),
|
||||
news=iter(
|
||||
baker.make(News, is_moderated=True, _quantity=5, _bulk_create=True)
|
||||
),
|
||||
)
|
||||
cls.dates.append(
|
||||
baker.make(
|
||||
NewsDate,
|
||||
start_date=now() + timedelta(days=2, hours=1),
|
||||
end_date=now() + timedelta(days=2, hours=5),
|
||||
news=baker.make(News, is_moderated=True),
|
||||
)
|
||||
)
|
||||
cls.dates.sort(key=lambda d: d.start_date)
|
||||
|
||||
def test_num_queries(self):
|
||||
with assertNumQueries(2):
|
||||
self.client.get(reverse("api:fetch_news_dates"))
|
||||
|
||||
def test_html_format(self):
|
||||
"""Test that when the summary is asked in html, the summary is in html."""
|
||||
summary_1 = "# First event\nThere is something happening.\n"
|
||||
self.dates[0].news.summary = summary_1
|
||||
self.dates[0].news.save()
|
||||
summary_2 = (
|
||||
"# Second event\n"
|
||||
"There is something happening **for real**.\n"
|
||||
"Everything is [here](https://youtu.be/dQw4w9WgXcQ)\n"
|
||||
)
|
||||
self.dates[1].news.summary = summary_2
|
||||
self.dates[1].news.save()
|
||||
response = self.client.get(
|
||||
reverse("api:fetch_news_dates") + "?page_size=2&text_format=html"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
dates = response.json()["results"]
|
||||
assert dates[0]["news"]["summary"] == markdown(summary_1)
|
||||
assert dates[1]["news"]["summary"] == markdown(summary_2)
|
||||
|
||||
def test_fetch(self):
|
||||
after = quote((now() + timedelta(days=1)).isoformat())
|
||||
response = self.client.get(
|
||||
reverse("api:fetch_news_dates") + f"?page_size=3&after={after}"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
dates = response.json()["results"]
|
||||
assert [d["id"] for d in dates] == [d.id for d in self.dates[1:4]]
|
||||
|
30
com/views.py
30
com/views.py
@ -22,7 +22,7 @@
|
||||
#
|
||||
#
|
||||
import itertools
|
||||
from datetime import timedelta
|
||||
from datetime import date, timedelta
|
||||
from smtplib import SMTPRecipientsRefused
|
||||
from typing import Any
|
||||
|
||||
@ -253,19 +253,41 @@ class NewsListView(TemplateView):
|
||||
key=lambda u: u.date_of_birth.year,
|
||||
)
|
||||
|
||||
def get_news_dates(self):
|
||||
def get_last_day(self) -> date:
|
||||
"""Get the last day when news will be displayed
|
||||
|
||||
The returned day is the third one where something happen.
|
||||
For example, if there are 6 events : A on 15/03, B and C on 17/03,
|
||||
D on 20/03, E on 21/03 and F on 22/03 ;
|
||||
then the result is 20/03.
|
||||
"""
|
||||
return list(
|
||||
NewsDate.objects.filter(end_date__gt=now())
|
||||
.order_by("start_date")
|
||||
.values_list("start_date__date", flat=True)
|
||||
.distinct()[:4]
|
||||
)[-1]
|
||||
|
||||
def get_news_dates(self, until: date):
|
||||
"""Return the event dates to display.
|
||||
|
||||
The selected events are the ones that happens between
|
||||
right now and the given day (included).
|
||||
"""
|
||||
return itertools.groupby(
|
||||
NewsDate.objects.viewable_by(self.request.user)
|
||||
.filter(end_date__gt=now(), start_date__lt=now() + timedelta(days=6))
|
||||
.filter(end_date__gt=now(), start_date__date__lte=until)
|
||||
.order_by("start_date")
|
||||
.select_related("news", "news__club"),
|
||||
key=lambda d: d.start_date.date(),
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
last_day = self.get_last_day()
|
||||
return super().get_context_data(**kwargs) | {
|
||||
"news_dates": self.get_news_dates(),
|
||||
"news_dates": self.get_news_dates(until=last_day),
|
||||
"birthdays": self.get_birthdays(),
|
||||
"last_day": last_day,
|
||||
}
|
||||
|
||||
|
||||
|
@ -169,7 +169,7 @@ class Command(BaseCommand):
|
||||
Weekmail().save()
|
||||
|
||||
# Here we add a lot of test datas, that are not necessary for the Sith, but that provide a basic development environment
|
||||
self.now = timezone.now().replace(hour=12)
|
||||
self.now = timezone.now().replace(hour=12, second=0)
|
||||
|
||||
skia = User.objects.create_user(
|
||||
username="skia",
|
||||
@ -681,7 +681,7 @@ Welcome to the wiki page!
|
||||
friday = self.now
|
||||
while friday.weekday() != 4:
|
||||
friday += timedelta(hours=6)
|
||||
friday.replace(hour=20, minute=0, second=0)
|
||||
friday.replace(hour=20, minute=0)
|
||||
# Event
|
||||
news_dates = []
|
||||
n = News.objects.create(
|
||||
|
@ -6,7 +6,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-18 15:03+0100\n"
|
||||
"POT-Creation-Date: 2025-02-25 11:04+0100\n"
|
||||
"PO-Revision-Date: 2016-07-18\n"
|
||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||
@ -1426,17 +1426,6 @@ 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
|
||||
@ -1578,6 +1567,17 @@ msgstr "Administrer les news"
|
||||
msgid "Nothing to come..."
|
||||
msgstr "Rien à venir..."
|
||||
|
||||
#: com/templates/com/news_list.jinja
|
||||
msgid "See more"
|
||||
msgstr "Voir plus"
|
||||
|
||||
#: com/templates/com/news_list.jinja
|
||||
msgid ""
|
||||
"It was too short. You already reached the end of the upcoming events list."
|
||||
msgstr ""
|
||||
"C'était trop court. Vous êtes déjà arrivés à la fin de la liste des "
|
||||
"événements à venir."
|
||||
|
||||
#: com/templates/com/news_list.jinja
|
||||
msgid "All coming events"
|
||||
msgstr "Tous les événements à venir"
|
||||
@ -6031,3 +6031,13 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée."
|
||||
#, python-format
|
||||
msgid "Maximum characters: %(max_length)s"
|
||||
msgstr "Nombre de caractères max: %(max_length)s"
|
||||
|
||||
#, python-format
|
||||
#~ msgid ""
|
||||
#~ "This event will take place every week for %%s weeks. If you moderate or "
|
||||
#~ "delete this event, it will also be moderated (or deleted) for the "
|
||||
#~ "following weeks."
|
||||
#~ msgstr ""
|
||||
#~ "Cet événement se déroulera chaque semaine pendant %%s semaines. Si vous "
|
||||
#~ "modérez ou supprimez cet événement, il sera également modéré (ou "
|
||||
#~ "supprimé) pour les semaines suivantes."
|
||||
|
@ -7,7 +7,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-01-08 12:23+0100\n"
|
||||
"POT-Creation-Date: 2025-02-25 11:05+0100\n"
|
||||
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
|
||||
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
|
||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||
@ -21,6 +21,17 @@ msgstr ""
|
||||
msgid "More info"
|
||||
msgstr "Plus d'informations"
|
||||
|
||||
#: com/static/bundled/com/components/moderation-alert-index.ts
|
||||
#, javascript-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."
|
||||
|
||||
#: core/static/bundled/core/components/ajax-select-base.ts
|
||||
msgid "Remove"
|
||||
msgstr "Retirer"
|
||||
@ -125,10 +136,6 @@ msgstr "Montrer plus"
|
||||
msgid "family_tree.%(extension)s"
|
||||
msgstr "arbre_genealogique.%(extension)s"
|
||||
|
||||
#: core/static/bundled/user/pictures-index.js
|
||||
msgid "pictures.%(extension)s"
|
||||
msgstr "photos.%(extension)s"
|
||||
|
||||
#: core/static/user/js/user_edit.js
|
||||
#, javascript-format
|
||||
msgid "captured.%s"
|
||||
@ -187,6 +194,10 @@ msgstr "La réorganisation des types de produit a échoué avec le code : %d"
|
||||
msgid "Incorrect value"
|
||||
msgstr "Valeur incorrecte"
|
||||
|
||||
#: sas/static/bundled/sas/pictures-download-index.ts
|
||||
msgid "pictures.%(extension)s"
|
||||
msgstr "photos.%(extension)s"
|
||||
|
||||
#: sas/static/bundled/sas/viewer-index.ts
|
||||
msgid "Couldn't moderate picture"
|
||||
msgstr "Il n'a pas été possible de modérer l'image"
|
||||
|
@ -171,6 +171,7 @@ TEMPLATES = [
|
||||
"timezone": "django.utils.timezone",
|
||||
"get_sith": "com.views.sith",
|
||||
"get_language": "django.utils.translation.get_language",
|
||||
"timedelta": "datetime.timedelta",
|
||||
},
|
||||
"bytecode_cache": {
|
||||
"name": "default",
|
||||
|
Loading…
x
Reference in New Issue
Block a user