diff --git a/com/api.py b/com/api.py new file mode 100644 index 00000000..e46daea9 --- /dev/null +++ b/com/api.py @@ -0,0 +1,32 @@ +from pathlib import Path + +from django.conf import settings +from django.http import Http404 +from ninja_extra import ControllerBase, api_controller, route + +from com.calendar import IcsCalendar +from core.views.files import send_raw_file + + +@api_controller("/calendar") +class CalendarController(ControllerBase): + CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" + + @route.get("/external.ics", url_name="calendar_external") + def calendar_external(self): + """Return the ICS file of the AE Google Calendar + + Because of Google's cors rules, we can't just do a request to google ics + from the frontend. Google is blocking CORS request in it's responses headers. + 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 why we have this backend based solution. + """ + if (calendar := IcsCalendar.get_external()) is not None: + return send_raw_file(calendar) + raise Http404 + + @route.get("/internal.ics", url_name="calendar_internal") + def calendar_internal(self): + return send_raw_file(IcsCalendar.get_internal()) diff --git a/com/apps.py b/com/apps.py new file mode 100644 index 00000000..0502c588 --- /dev/null +++ b/com/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class ComConfig(AppConfig): + name = "com" + verbose_name = "News and communication" + + def ready(self): + import com.signals # noqa F401 diff --git a/com/calendar.py b/com/calendar.py new file mode 100644 index 00000000..9003d6de --- /dev/null +++ b/com/calendar.py @@ -0,0 +1,76 @@ +from datetime import datetime, timedelta +from pathlib import Path +from typing import final + +import urllib3 +from dateutil.relativedelta import relativedelta +from django.conf import settings +from django.urls import reverse +from django.utils import timezone +from ical.calendar import Calendar +from ical.calendar_stream import IcsCalendarStream +from ical.event import Event + +from com.models import NewsDate + + +@final +class IcsCalendar: + _CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" + _EXTERNAL_CALENDAR = _CACHE_FOLDER / "external.ics" + _INTERNAL_CALENDAR = _CACHE_FOLDER / "internal.ics" + + @classmethod + def get_external(cls, expiration: timedelta = timedelta(hours=1)) -> Path | None: + if ( + cls._EXTERNAL_CALENDAR.exists() + and timezone.make_aware( + datetime.fromtimestamp(cls._EXTERNAL_CALENDAR.stat().st_mtime) + ) + + expiration + > timezone.now() + ): + return cls._EXTERNAL_CALENDAR + return cls.make_external() + + @classmethod + def make_external(cls) -> Path | None: + calendar = urllib3.request( + "GET", + "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics", + ) + if calendar.status != 200: + return None + + cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) + with open(cls._EXTERNAL_CALENDAR, "wb") as f: + _ = f.write(calendar.data) + return cls._EXTERNAL_CALENDAR + + @classmethod + def get_internal(cls) -> Path: + if not cls._INTERNAL_CALENDAR.exists(): + return cls.make_internal() + return cls._INTERNAL_CALENDAR + + @classmethod + def make_internal(cls) -> Path: + # Updated through a post_save signal on News in com.signals + calendar = Calendar() + for news_date in NewsDate.objects.filter( + news__is_moderated=True, + end_date__gte=timezone.now() - (relativedelta(months=6)), + ).prefetch_related("news"): + event = Event( + summary=news_date.news.title, + start=news_date.start_date, + end=news_date.end_date, + url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}), + ) + calendar.events.append(event) + + # Create a file so we can offload the download to the reverse proxy if available + cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) + with open(cls._INTERNAL_CALENDAR, "wb") as f: + _ = f.write(IcsCalendarStream.calendar_to_ics(calendar).encode("utf-8")) + return cls._INTERNAL_CALENDAR diff --git a/com/models.py b/com/models.py index f3076174..633c7671 100644 --- a/com/models.py +++ b/com/models.py @@ -17,11 +17,12 @@ # details. # # You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# this program; if not, write to the Free Software Foundation, Inc., 59 Temple # Place - Suite 330, Boston, MA 02111-1307, USA. # # + from django.conf import settings from django.core.exceptions import ValidationError from django.core.mail import EmailMultiAlternatives diff --git a/com/signals.py b/com/signals.py new file mode 100644 index 00000000..ea004ad8 --- /dev/null +++ b/com/signals.py @@ -0,0 +1,10 @@ +from django.db.models.base import post_save +from django.dispatch import receiver + +from com.calendar import IcsCalendar +from com.models import News + + +@receiver(post_save, sender=News, dispatch_uid="update_internal_ics") +def update_internal_ics(*args, **kwargs): + _ = IcsCalendar.make_internal() diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts new file mode 100644 index 00000000..e3baddc6 --- /dev/null +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -0,0 +1,197 @@ +import { makeUrl } from "#core:utils/api"; +import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; +import { Calendar, type EventClickArg } from "@fullcalendar/core"; +import type { EventImpl } from "@fullcalendar/core/internal"; +import enLocale from "@fullcalendar/core/locales/en-gb"; +import frLocale from "@fullcalendar/core/locales/fr"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import iCalendarPlugin from "@fullcalendar/icalendar"; +import listPlugin from "@fullcalendar/list"; +import { calendarCalendarExternal, calendarCalendarInternal } from "#openapi"; + +@registerComponent("ics-calendar") +export class IcsCalendar extends inheritHtmlElement("div") { + static observedAttributes = ["locale"]; + private calendar: Calendar; + private locale = "en"; + + attributeChangedCallback(name: string, _oldValue?: string, newValue?: string) { + if (name !== "locale") { + return; + } + + this.locale = newValue; + } + + isMobile() { + return window.innerWidth < 765; + } + + currentView() { + // Get view type based on viewport + return this.isMobile() ? "listMonth" : "dayGridMonth"; + } + + currentToolbar() { + if (this.isMobile()) { + return { + left: "prev,next", + center: "title", + right: "", + }; + } + return { + left: "prev,next today", + center: "title", + right: "dayGridMonth,dayGridWeek,dayGridDay", + }; + } + + formatDate(date: Date) { + return new Intl.DateTimeFormat(this.locale, { + dateStyle: "medium", + timeStyle: "short", + }).format(date); + } + + createEventDetailPopup(event: EventClickArg) { + // Delete previous popup + const oldPopup = document.getElementById("event-details"); + if (oldPopup !== null) { + oldPopup.remove(); + } + + const makePopupInfo = (info: HTMLElement, iconClass: string) => { + const row = document.createElement("div"); + const icon = document.createElement("i"); + + row.setAttribute("class", "event-details-row"); + + icon.setAttribute("class", `event-detail-row-icon fa-xl ${iconClass}`); + + row.appendChild(icon); + row.appendChild(info); + + return row; + }; + + const makePopupTitle = (event: EventImpl) => { + const row = document.createElement("div"); + const title = document.createElement("h4"); + const time = document.createElement("span"); + + title.setAttribute("class", "event-details-row-content"); + title.textContent = event.title; + + time.setAttribute("class", "event-details-row-content"); + time.textContent = `${this.formatDate(event.start)} - ${this.formatDate(event.end)}`; + + row.appendChild(title); + row.appendChild(time); + return makePopupInfo( + row, + "fa-solid fa-calendar-days fa-xl event-detail-row-icon", + ); + }; + + const makePopupLocation = (event: EventImpl) => { + if (event.extendedProps.location === null) { + return null; + } + const info = document.createElement("div"); + info.innerText = event.extendedProps.location; + + return makePopupInfo(info, "fa-solid fa-location-dot"); + }; + + const makePopupUrl = (event: EventImpl) => { + if (event.url === "") { + return null; + } + const url = document.createElement("a"); + url.href = event.url; + url.textContent = gettext("More info"); + + return makePopupInfo(url, "fa-solid fa-link"); + }; + + // Create new popup + const popup = document.createElement("div"); + const popupContainer = document.createElement("div"); + + popup.setAttribute("id", "event-details"); + popupContainer.setAttribute("class", "event-details-container"); + + popupContainer.appendChild(makePopupTitle(event.event)); + + const location = makePopupLocation(event.event); + if (location !== null) { + popupContainer.appendChild(location); + } + + const url = makePopupUrl(event.event); + if (url !== null) { + popupContainer.appendChild(url); + } + + popup.appendChild(popupContainer); + + // We can't just add the element relative to the one we want to appear under + // Otherwise, it either gets clipped by the boundaries of the calendar or resize cells + // Here, we create a popup outside the calendar that follows the clicked element + this.node.appendChild(popup); + const follow = (node: HTMLElement) => { + const rect = node.getBoundingClientRect(); + popup.setAttribute( + "style", + `top: calc(${rect.top + window.scrollY}px + ${rect.height}px); left: ${rect.left + window.scrollX}px;`, + ); + }; + follow(event.el); + window.addEventListener("resize", () => { + follow(event.el); + }); + } + + async connectedCallback() { + super.connectedCallback(); + this.calendar = new Calendar(this.node, { + plugins: [dayGridPlugin, iCalendarPlugin, listPlugin], + locales: [frLocale, enLocale], + height: "auto", + locale: this.locale, + initialView: this.currentView(), + headerToolbar: this.currentToolbar(), + eventSources: [ + { + url: await makeUrl(calendarCalendarInternal), + format: "ics", + }, + { + url: await makeUrl(calendarCalendarExternal), + format: "ics", + }, + ], + windowResize: () => { + this.calendar.changeView(this.currentView()); + this.calendar.setOption("headerToolbar", this.currentToolbar()); + }, + eventClick: (event) => { + // Avoid our popup to be deleted because we clicked outside of it + event.jsEvent.stopPropagation(); + // Don't auto-follow events URLs + event.jsEvent.preventDefault(); + this.createEventDetailPopup(event); + }, + }); + this.calendar.render(); + + window.addEventListener("click", (event: MouseEvent) => { + // Auto close popups when clicking outside of it + const popup = document.getElementById("event-details"); + if (popup !== null && !popup.contains(event.target as Node)) { + popup.remove(); + } + }); + } +} diff --git a/com/static/com/components/ics-calendar.scss b/com/static/com/components/ics-calendar.scss new file mode 100644 index 00000000..21aa55d7 --- /dev/null +++ b/com/static/com/components/ics-calendar.scss @@ -0,0 +1,101 @@ +@import "core/static/core/colors"; + + +:root { + --fc-button-border-color: #fff; + --fc-button-hover-border-color: #fff; + --fc-button-active-border-color: #fff; + --fc-button-text-color: #fff; + --fc-button-bg-color: #1a78b3; + --fc-button-active-bg-color: #15608F; + --fc-button-hover-bg-color: #15608F; + --fc-today-bg-color: rgba(26, 120, 179, 0.1); + --fc-border-color: #DDDDDD; + --event-details-background-color: white; + --event-details-padding: 20px; + --event-details-border: 1px solid #EEEEEE; + --event-details-border-radius: 4px; + --event-details-box-shadow: 0px 6px 20px 4px rgb(0 0 0 / 16%); + --event-details-max-width: 600px; +} + +ics-calendar { + border: none; + box-shadow: none; + + #event-details { + z-index: 10; + max-width: 1151px; + position: absolute; + + .event-details-container { + display: flex; + color: black; + flex-direction: column; + min-width: 200px; + max-width: var(--event-details-max-width); + padding: var(--event-details-padding); + border: var(--event-details-border); + border-radius: var(--event-details-border-radius); + background-color: var(--event-details-background-color); + box-shadow: var(--event-details-box-shadow); + gap: 20px; + } + + .event-detail-row-icon { + margin-left: 10px; + margin-right: 20px; + align-content: center; + align-self: center; + } + + .event-details-row { + display: flex; + align-items: start; + } + + .event-details-row-content { + display: flex; + align-items: start; + flex-direction: row; + background-color: var(--event-details-background-color); + margin-top: 0px; + margin-bottom: 4px; + } + } + + a.fc-col-header-cell-cushion, + a.fc-col-header-cell-cushion:hover { + color: black; + } + + a.fc-daygrid-day-number, + a.fc-daygrid-day-number:hover { + color: rgb(34, 34, 34); + } + + td { + overflow-x: visible; // Show events on multiple days + } + + //Reset from style.scss + table { + box-shadow: none; + border-radius: 0px; + -moz-border-radius: 0px; + margin: 0px; + } + + // Reset from style.scss + thead { + background-color: white; + color: black; + } + + // Reset from style.scss + tbody>tr { + &:nth-child(even):not(.highlight) { + background: white; + } + } +} \ No newline at end of file diff --git a/com/static/com/css/news-detail.scss b/com/static/com/css/news-detail.scss new file mode 100644 index 00000000..0a07e62d --- /dev/null +++ b/com/static/com/css/news-detail.scss @@ -0,0 +1,66 @@ +@import "core/static/core/colors"; + +#news_details { + display: inline-block; + margin-top: 20px; + padding: 0.4em; + width: 80%; + background: $white-color; + + h4 { + margin-top: 1em; + text-transform: uppercase; + } + + .club_logo { + display: inline-block; + text-align: center; + width: 19%; + float: left; + min-width: 15em; + margin: 0; + + img { + max-height: 15em; + max-width: 12em; + display: block; + margin: 0 auto; + margin-bottom: 10px; + } + } + + .share_button { + border: none; + color: white; + padding: 0.5em 1em; + text-align: center; + text-decoration: none; + font-size: 1.2em; + border-radius: 2px; + float: right; + display: block; + margin-left: 0.3em; + + &:hover { + color: lightgrey; + } + } + + .facebook { + background: $faceblue; + } + + .twitter { + background: $twitblue; + } + + .news_meta { + margin-top: 10em; + font-size: small; + } +} + +.helptext { + margin-top: 10px; + display: block; +} \ No newline at end of file diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss new file mode 100644 index 00000000..bcbf8273 --- /dev/null +++ b/com/static/com/css/news-list.scss @@ -0,0 +1,297 @@ +@import "core/static/core/colors"; +@import "core/static/core/devices"; + +#news { + display: flex; + + @media (max-width: 800px) { + flex-direction: column; + } + + #news_admin { + margin-bottom: 1em; + } + + #right_column { + flex: 20%; + margin: 3.2px; + + display: inline-block; + vertical-align: top; + } + + #left_column { + flex: 79%; + margin: 0.2em; + } + + h3 { + background: $second-color; + box-shadow: $shadow-color 1px 1px 1px; + padding: 0.4em; + margin: 0 0 0.5em 0; + text-transform: uppercase; + font-size: 17px; + + &:not(:first-of-type) { + margin: 2em 0 1em 0; + } + } + + @media screen and (max-width: $small-devices) { + + #left_column, + #right_column { + flex: 100%; + } + } + + /* LINKS/BIRTHDAYS */ + #links, + #birthdays { + display: block; + width: 100%; + background: white; + font-size: 70%; + margin-bottom: 1em; + + h3 { + margin-bottom: 0; + } + + #links_content { + overflow: auto; + box-shadow: $shadow-color 1px 1px 1px; + height: 20em; + + h4 { + margin-left: 5px; + } + + ul { + list-style: none; + margin-left: 0; + + li { + margin: 10px; + + .fa-facebook { + color: $faceblue; + } + + .fa-discord { + color: $discordblurple; + } + + .fa-square-instagram::before { + background: $instagradient; + background-clip: text; + -webkit-text-fill-color: transparent; + } + + i { + width: 25px; + text-align: center; + } + } + } + + } + + #birthdays_content { + ul.birthdays_year { + margin: 0; + list-style-type: none; + font-weight: bold; + + >li { + padding: 0.5em; + + &:nth-child(even) { + background: $secondary-neutral-light-color; + } + } + + ul { + margin: 0; + margin-left: 1em; + list-style-type: square; + list-style-position: inside; + font-weight: normal; + } + } + } + } + + /* END AGENDA/BIRTHDAYS */ + + /* EVENTS TODAY AND NEXT FEW DAYS */ + .news_events_group { + box-shadow: $shadow-color 1px 1px 1px; + margin-left: 1em; + margin-bottom: 0.5em; + + .news_events_group_date { + display: table-cell; + padding: 0.6em; + vertical-align: middle; + background: $primary-neutral-dark-color; + color: $white-color; + text-transform: uppercase; + text-align: center; + font-weight: bold; + font-family: monospace; + font-size: 1.4em; + border-radius: 7px 0 0 7px; + + div { + margin: 0 auto; + + .day { + font-size: 1.5em; + } + } + } + + .news_events_group_items { + display: table-cell; + width: 100%; + + .news_event:nth-of-type(odd) { + background: white; + } + + .news_event:nth-of-type(even) { + background: $primary-neutral-light-color; + } + + .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; + + img { + max-height: 6em; + max-width: 8em; + display: block; + margin: 0 auto; + } + } + + .news_date { + font-size: 100%; + } + + .news_content { + clear: left; + + .button_bar { + text-align: right; + + .fb { + color: $faceblue; + } + + .twitter { + color: $twitblue; + } + } + } + } + } + } + + /* 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; + } + + .news_date { + color: grey; + } +} \ No newline at end of file diff --git a/com/static/com/css/posters.scss b/com/static/com/css/posters.scss new file mode 100644 index 00000000..26cf2b91 --- /dev/null +++ b/com/static/com/css/posters.scss @@ -0,0 +1,230 @@ +#poster_list, +#screen_list, +#poster_edit, +#screen_edit { + position: relative; + + #title { + position: relative; + padding: 10px; + margin: 10px; + border-bottom: 2px solid black; + + h3 { + display: flex; + justify-content: center; + align-items: center; + } + + #links { + position: absolute; + display: flex; + bottom: 5px; + + &.left { + left: 0; + } + + &.right { + right: 0; + } + + .link { + padding: 5px; + padding-left: 20px; + padding-right: 20px; + margin-left: 5px; + border-radius: 20px; + background-color: hsl(40, 100%, 50%); + color: black; + + &:hover { + color: black; + background-color: hsl(40, 58%, 50%); + } + + &.delete { + background-color: hsl(0, 100%, 40%); + } + } + } + } + + #posters, + #screens { + position: relative; + display: flex; + flex-wrap: wrap; + + #no-posters, + #no-screens { + display: flex; + justify-content: center; + align-items: center; + } + + .poster, + .screen { + min-width: 10%; + max-width: 20%; + display: flex; + flex-direction: column; + margin: 10px; + border: 2px solid darkgrey; + border-radius: 4px; + padding: 10px; + background-color: lightgrey; + + * { + display: flex; + justify-content: center; + align-items: center; + } + + .name { + padding-bottom: 5px; + margin-bottom: 5px; + border-bottom: 1px solid whitesmoke; + } + + .image { + flex-grow: 1; + position: relative; + padding-bottom: 5px; + margin-bottom: 5px; + border-bottom: 1px solid whitesmoke; + + img { + max-height: 20vw; + max-width: 100%; + } + + &:hover { + &::before { + position: absolute; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + top: 0; + left: 0; + z-index: 10; + content: "Click to expand"; + color: white; + background-color: rgba(black, 0.5); + } + } + } + + .dates { + padding-bottom: 5px; + margin-bottom: 5px; + border-bottom: 1px solid whitesmoke; + + * { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + margin-left: 5px; + margin-right: 5px; + } + + .begin, + .end { + width: 48%; + } + + .begin { + border-right: 1px solid whitesmoke; + padding-right: 2%; + } + } + + .edit, + .moderate, + .slideshow { + padding: 5px; + border-radius: 20px; + background-color: hsl(40, 100%, 50%); + color: black; + + &:hover { + color: black; + background-color: hsl(40, 58%, 50%); + } + + &:nth-child(2n) { + margin-top: 5px; + margin-bottom: 5px; + } + } + + .tooltip { + visibility: hidden; + width: 120px; + background-color: hsl(210, 20%, 98%); + color: hsl(0, 0%, 0%); + text-align: center; + padding: 5px 0; + border-radius: 6px; + position: absolute; + z-index: 10; + + ul { + margin-left: 0; + display: inline-block; + + li { + display: list-item; + list-style-type: none; + } + } + } + + &.not_moderated { + border: 1px solid red; + } + + &:hover .tooltip { + visibility: visible; + } + } + } + + #view { + position: fixed; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + z-index: 10; + visibility: hidden; + background-color: rgba(10, 10, 10, 0.9); + overflow: hidden; + + &.active { + visibility: visible; + } + + #placeholder { + width: 80vw; + height: 80vh; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + + img { + max-width: 100%; + max-height: 100%; + } + } + } +} \ No newline at end of file diff --git a/com/templates/com/news_detail.jinja b/com/templates/com/news_detail.jinja index cbfa596c..238515ed 100644 --- a/com/templates/com/news_detail.jinja +++ b/com/templates/com/news_detail.jinja @@ -11,6 +11,11 @@ {{ gen_news_metatags(news) }} {% endblock %} + +{% block additional_css %} + +{% endblock %} + {% block content %}

{% trans %}Back to news{% endtrans %}

diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index ac9a7892..8f20ce19 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -5,6 +5,15 @@ {% trans %}News{% endtrans %} {% endblock %} +{% block additional_css %} + + +{% endblock %} + +{% block additional_js %} + +{% endblock %} + {% block content %} {% if user.is_com_admin %}
@@ -83,60 +92,55 @@
{% endif %} -{% set coming_soon = object_list.filter(dates__start_date__gte=timezone.now()+timedelta(days=5), -type="EVENT").order_by('dates__start_date') %} -{% if coming_soon %} -

{% trans %}Coming soon... don't miss!{% endtrans %}

- {% for news in coming_soon %} -
- {{ news.title }} - {{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }} - {{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} - - {{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }} - {{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }} -
- {% endfor %} -{% endif %}

{% trans %}All coming events{% endtrans %}

- + + + -
-
-
{% trans %}Agenda{% endtrans %}
-
- {% for d in NewsDate.objects.filter(end_date__gte=timezone.now(), - news__is_moderated=True, news__type__in=["WEEKLY", - "EVENT"]).order_by('start_date', 'end_date') %} -
-
- {{ d.start_date|localtime|date('D d M Y') }} -
-
- {{ d.start_date|localtime|time(DATETIME_FORMAT) }} - - {{ d.end_date|localtime|time(DATETIME_FORMAT) }} -
- -
{{ d.news.summary|markdown }}
-
- {% endfor %} +
+
-
{% trans %}Birthdays{% endtrans %}
+

{% trans %}Birthdays{% endtrans %}

- {%- if user.is_subscribed -%} + {%- if user.was_subscribed -%}
    {%- for year, users in birthdays -%}
  • @@ -150,12 +154,14 @@ type="EVENT").order_by('dates__start_date') %} {%- endfor -%}
{%- else -%} -

{% trans %}You need an up to date subscription to access this content{% endtrans %}

+

{% trans %}You need to subscribe to access this content{% endtrans %}

{%- endif -%}
+
+
{% endblock %} diff --git a/com/templates/com/poster_list.jinja b/com/templates/com/poster_list.jinja index 8c4f5cd1..c9af62c0 100644 --- a/com/templates/com/poster_list.jinja +++ b/com/templates/com/poster_list.jinja @@ -10,6 +10,10 @@ {% trans %}Poster{% endtrans %} {% endblock %} +{% block additional_css %} + +{% endblock %} + {% block content %}
diff --git a/com/templates/com/poster_moderate.jinja b/com/templates/com/poster_moderate.jinja index 36e3dae7..6370becf 100644 --- a/com/templates/com/poster_moderate.jinja +++ b/com/templates/com/poster_moderate.jinja @@ -5,6 +5,10 @@ {% endblock %} +{% block additional_css %} + +{% endblock %} + {% block content %}
diff --git a/com/templates/com/screen_slideshow.jinja b/com/templates/com/screen_slideshow.jinja index 448f8dfc..0374257e 100644 --- a/com/templates/com/screen_slideshow.jinja +++ b/com/templates/com/screen_slideshow.jinja @@ -3,7 +3,7 @@ {% trans %}Slideshow{% endtrans %} - + diff --git a/com/tests/test_api.py b/com/tests/test_api.py new file mode 100644 index 00000000..f131052e --- /dev/null +++ b/com/tests/test_api.py @@ -0,0 +1,122 @@ +from dataclasses import dataclass +from datetime import datetime, timedelta +from pathlib import Path +from typing import Callable +from unittest.mock import MagicMock, patch + +import pytest +from django.conf import settings +from django.http import HttpResponse +from django.test.client import Client +from django.urls import reverse +from django.utils import timezone + +from com.calendar import IcsCalendar + + +@dataclass +class MockResponse: + status: int + value: str + + @property + def data(self): + return self.value.encode("utf8") + + +def accel_redirect_to_file(response: HttpResponse) -> Path | None: + redirect = Path(response.headers.get("X-Accel-Redirect", "")) + if not redirect.is_relative_to(Path("/") / settings.MEDIA_ROOT.stem): + return None + return settings.MEDIA_ROOT / redirect.relative_to( + Path("/") / settings.MEDIA_ROOT.stem + ) + + +@pytest.mark.django_db +class TestExternalCalendar: + @pytest.fixture + def mock_request(self): + mock = MagicMock() + with patch("urllib3.request", mock): + yield mock + + @pytest.fixture + def mock_current_time(self): + mock = MagicMock() + original = timezone.now + with patch("django.utils.timezone.now", mock): + yield mock, original + + @pytest.fixture(autouse=True) + def clear_cache(self): + IcsCalendar._EXTERNAL_CALENDAR.unlink(missing_ok=True) + + @pytest.mark.parametrize("error_code", [403, 404, 500]) + def test_fetch_error( + self, client: Client, mock_request: MagicMock, error_code: int + ): + mock_request.return_value = MockResponse(error_code, "not allowed") + assert client.get(reverse("api:calendar_external")).status_code == 404 + + def test_fetch_success(self, client: Client, mock_request: MagicMock): + external_response = MockResponse(200, "Definitely an ICS") + mock_request.return_value = external_response + response = client.get(reverse("api:calendar_external")) + assert response.status_code == 200 + out_file = accel_redirect_to_file(response) + assert out_file is not None + assert out_file.exists() + with open(out_file, "r") as f: + assert f.read() == external_response.value + + def test_fetch_caching( + self, + client: Client, + mock_request: MagicMock, + mock_current_time: tuple[MagicMock, Callable[[], datetime]], + ): + fake_current_time, original_timezone = mock_current_time + start_time = original_timezone() + + fake_current_time.return_value = start_time + external_response = MockResponse(200, "Definitely an ICS") + mock_request.return_value = external_response + + with open( + accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r" + ) as f: + assert f.read() == external_response.value + + mock_request.return_value = MockResponse(200, "This should be ignored") + with open( + accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r" + ) as f: + assert f.read() == external_response.value + + mock_request.assert_called_once() + + fake_current_time.return_value = start_time + timedelta(hours=1, seconds=1) + external_response = MockResponse(200, "This won't be ignored") + mock_request.return_value = external_response + + with open( + accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r" + ) as f: + assert f.read() == external_response.value + + assert mock_request.call_count == 2 + + +@pytest.mark.django_db +class TestInternalCalendar: + @pytest.fixture(autouse=True) + def clear_cache(self): + IcsCalendar._INTERNAL_CALENDAR.unlink(missing_ok=True) + + def test_fetch_success(self, client: Client): + response = client.get(reverse("api:calendar_internal")) + assert response.status_code == 200 + out_file = accel_redirect_to_file(response) + assert out_file is not None + assert out_file.exists() diff --git a/com/tests.py b/com/tests/test_views.py similarity index 93% rename from com/tests.py rename to com/tests/test_views.py index 399eb0e8..3f98bfdc 100644 --- a/com/tests.py +++ b/com/tests/test_views.py @@ -97,9 +97,7 @@ class TestCom(TestCase): response = self.client.get(reverse("core:index")) self.assertContains( response, - text=html.escape( - _("You need an up to date subscription to access this content") - ), + text=html.escape(_("You need to subscribe to access this content")), ) def test_birthday_subscibed_user(self): @@ -107,9 +105,16 @@ class TestCom(TestCase): self.assertNotContains( response, - text=html.escape( - _("You need an up to date subscription to access this content") - ), + text=html.escape(_("You need to subscribe to access this content")), + ) + + def test_birthday_old_subscibed_user(self): + self.client.force_login(User.objects.get(username="old_subscriber")) + response = self.client.get(reverse("core:index")) + + self.assertNotContains( + response, + text=html.escape(_("You need to subscribe to access this content")), ) diff --git a/com/views.py b/com/views.py index 1b7ab8bc..f9993b3c 100644 --- a/com/views.py +++ b/com/views.py @@ -685,8 +685,12 @@ class PosterEditBaseView(UpdateView): def get_initial(self): return { - "date_begin": self.object.date_begin.strftime("%Y-%m-%d %H:%M:%S"), - "date_end": self.object.date_end.strftime("%Y-%m-%d %H:%M:%S"), + "date_begin": self.object.date_begin.strftime("%Y-%m-%d %H:%M:%S") + if self.object.date_begin + else None, + "date_end": self.object.date_end.strftime("%Y-%m-%d %H:%M:%S") + if self.object.date_end + else None, } def dispatch(self, request, *args, **kwargs): diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 9cf9c59b..3ed1025d 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -46,6 +46,7 @@ from accounting.models import ( SimplifiedAccountingType, ) from club.models import Club, Membership +from com.calendar import IcsCalendar from com.models import News, NewsDate, Sith, Weekmail from core.models import Group, Page, PageRev, SithFile, User from core.utils import resize_image @@ -738,7 +739,7 @@ Welcome to the wiki page! NewsDate( news=n, start_date=friday + timedelta(hours=24 * 7 + 1), - end_date=self.now + timedelta(hours=24 * 7 + 9), + end_date=friday + timedelta(hours=24 * 7 + 9), ) ) # Weekly @@ -764,8 +765,9 @@ Welcome to the wiki page! ] ) NewsDate.objects.bulk_create(news_dates) + IcsCalendar.make_internal() # Force refresh of the calendar after a bulk_create - # Create som data for pedagogy + # Create some data for pedagogy UV( code="PA00", diff --git a/core/static/core/colors.scss b/core/static/core/colors.scss index 35dc6a69..e10eb905 100644 --- a/core/static/core/colors.scss +++ b/core/static/core/colors.scss @@ -24,6 +24,8 @@ $black-color: hsl(0, 0%, 17%); $faceblue: hsl(221, 44%, 41%); $twitblue: hsl(206, 82%, 63%); +$discordblurple: #7289da; +$instagradient: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%); $shadow-color: rgb(223, 223, 223); diff --git a/core/static/core/devices.scss b/core/static/core/devices.scss new file mode 100644 index 00000000..25839f24 --- /dev/null +++ b/core/static/core/devices.scss @@ -0,0 +1,5 @@ +/*--------------------------MEDIA QUERY HELPERS------------------------*/ + +$small-devices: 576px; +$medium-devices: 768px; +$large-devices: 992px; \ No newline at end of file diff --git a/core/static/core/style.scss b/core/static/core/style.scss index a9205e23..2f3af9f7 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -1,10 +1,6 @@ @import "colors"; @import "forms"; - -/*--------------------------MEDIA QUERY HELPERS------------------------*/ -$small-devices: 576px; -$medium-devices: 768px; -$large-devices: 992px; +@import "devices"; /*--------------------------------GENERAL------------------------------*/ @@ -453,302 +449,6 @@ body { } } - /*---------------------------------NEWS--------------------------------*/ - #news { - display: flex; - - @media (max-width: 800px) { - flex-direction: column; - } - - .news_column { - display: inline-block; - margin: 0; - vertical-align: top; - } - - #news_admin { - margin-bottom: 1em; - } - - #right_column { - flex: 20%; - float: right; - margin: 0.2em; - } - - #left_column { - flex: 79%; - margin: 0.2em; - - h3 { - background: $second-color; - box-shadow: $shadow-color 1px 1px 1px; - padding: 0.4em; - margin: 0 0 0.5em 0; - text-transform: uppercase; - font-size: 1.1em; - - &:not(:first-of-type) { - margin: 2em 0 1em 0; - } - } - } - - @media screen and (max-width: $small-devices) { - - #left_column, - #right_column { - flex: 100%; - } - } - - /* AGENDA/BIRTHDAYS */ - #agenda, - #birthdays { - display: block; - width: 100%; - background: white; - font-size: 70%; - margin-bottom: 1em; - - #agenda_title, - #birthdays_title { - margin: 0; - border-radius: 5px 5px 0 0; - box-shadow: $shadow-color 1px 1px 1px; - padding: 0.5em; - font-weight: bold; - font-size: 150%; - text-align: center; - text-transform: uppercase; - background: $second-color; - } - - #agenda_content { - overflow: auto; - box-shadow: $shadow-color 1px 1px 1px; - height: 20em; - } - - #agenda_content, - #birthdays_content { - .agenda_item { - padding: 0.5em; - margin-bottom: 0.5em; - - &:nth-of-type(even) { - background: $secondary-neutral-light-color; - } - - .agenda_time { - font-size: 90%; - color: grey; - } - - .agenda_item_content { - p { - margin-top: 0.2em; - } - } - } - - ul.birthdays_year { - margin: 0; - list-style-type: none; - font-weight: bold; - - >li { - padding: 0.5em; - - &:nth-child(even) { - background: $secondary-neutral-light-color; - } - } - - ul { - margin: 0; - margin-left: 1em; - list-style-type: square; - list-style-position: inside; - font-weight: normal; - } - } - } - } - - /* END AGENDA/BIRTHDAYS */ - - /* EVENTS TODAY AND NEXT FEW DAYS */ - .news_events_group { - box-shadow: $shadow-color 1px 1px 1px; - margin-left: 1em; - margin-bottom: 0.5em; - - .news_events_group_date { - display: table-cell; - padding: 0.6em; - vertical-align: middle; - background: $primary-neutral-dark-color; - color: $white-color; - text-transform: uppercase; - text-align: center; - font-weight: bold; - font-family: monospace; - font-size: 1.4em; - border-radius: 7px 0 0 7px; - - div { - margin: 0 auto; - - .day { - font-size: 1.5em; - } - } - } - - .news_events_group_items { - display: table-cell; - width: 100%; - - .news_event:nth-of-type(odd) { - background: white; - } - - .news_event:nth-of-type(even) { - background: $primary-neutral-light-color; - } - - .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; - - img { - max-height: 6em; - max-width: 8em; - display: block; - margin: 0 auto; - } - } - - .news_date { - font-size: 100%; - } - - .news_content { - clear: left; - - .button_bar { - text-align: right; - - .fb { - color: $faceblue; - } - - .twitter { - color: $twitblue; - } - } - } - } - } - } - - /* 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; - } - - .news_date { - color: grey; - } - } } @media screen and (max-width: $small-devices) { @@ -757,304 +457,6 @@ body { } } -#news_details { - display: inline-block; - margin-top: 20px; - padding: 0.4em; - width: 80%; - background: $white-color; - - h4 { - margin-top: 1em; - text-transform: uppercase; - } - - .club_logo { - display: inline-block; - text-align: center; - width: 19%; - float: left; - min-width: 15em; - margin: 0; - - img { - max-height: 15em; - max-width: 12em; - display: block; - margin: 0 auto; - margin-bottom: 10px; - } - } - - .share_button { - border: none; - color: white; - padding: 0.5em 1em; - text-align: center; - text-decoration: none; - font-size: 1.2em; - border-radius: 2px; - float: right; - display: block; - margin-left: 0.3em; - - &:hover { - color: lightgrey; - } - } - - .facebook { - background: $faceblue; - } - - .twitter { - background: $twitblue; - } - - .news_meta { - margin-top: 10em; - font-size: small; - } -} - -.helptext { - margin-top: 10px; - display: block; -} - -/*---------------------------POSTERS----------------------------*/ - -#poster_list, -#screen_list, -#poster_edit, -#screen_edit { - position: relative; - - #title { - position: relative; - padding: 10px; - margin: 10px; - border-bottom: 2px solid black; - - h3 { - display: flex; - justify-content: center; - align-items: center; - } - - #links { - position: absolute; - display: flex; - bottom: 5px; - - &.left { - left: 0; - } - - &.right { - right: 0; - } - - .link { - padding: 5px; - padding-left: 20px; - padding-right: 20px; - margin-left: 5px; - border-radius: 20px; - background-color: hsl(40, 100%, 50%); - color: black; - - &:hover { - color: black; - background-color: hsl(40, 58%, 50%); - } - - &.delete { - background-color: hsl(0, 100%, 40%); - } - } - } - } - - #posters, - #screens { - position: relative; - display: flex; - flex-wrap: wrap; - - #no-posters, - #no-screens { - display: flex; - justify-content: center; - align-items: center; - } - - .poster, - .screen { - min-width: 10%; - max-width: 20%; - display: flex; - flex-direction: column; - margin: 10px; - border: 2px solid darkgrey; - border-radius: 4px; - padding: 10px; - background-color: lightgrey; - - * { - display: flex; - justify-content: center; - align-items: center; - } - - .name { - padding-bottom: 5px; - margin-bottom: 5px; - border-bottom: 1px solid whitesmoke; - } - - .image { - flex-grow: 1; - position: relative; - padding-bottom: 5px; - margin-bottom: 5px; - border-bottom: 1px solid whitesmoke; - - img { - max-height: 20vw; - max-width: 100%; - } - - &:hover { - &::before { - position: absolute; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - flex-wrap: wrap; - top: 0; - left: 0; - z-index: 10; - content: "Click to expand"; - color: white; - background-color: rgba(black, 0.5); - } - } - } - - .dates { - padding-bottom: 5px; - margin-bottom: 5px; - border-bottom: 1px solid whitesmoke; - - * { - display: flex; - justify-content: center; - align-items: center; - flex-wrap: wrap; - margin-left: 5px; - margin-right: 5px; - } - - .begin, - .end { - width: 48%; - } - - .begin { - border-right: 1px solid whitesmoke; - padding-right: 2%; - } - } - - .edit, - .moderate, - .slideshow { - padding: 5px; - border-radius: 20px; - background-color: hsl(40, 100%, 50%); - color: black; - - &:hover { - color: black; - background-color: hsl(40, 58%, 50%); - } - - &:nth-child(2n) { - margin-top: 5px; - margin-bottom: 5px; - } - } - - .tooltip { - visibility: hidden; - width: 120px; - background-color: hsl(210, 20%, 98%); - color: hsl(0, 0%, 0%); - text-align: center; - padding: 5px 0; - border-radius: 6px; - position: absolute; - z-index: 10; - - ul { - margin-left: 0; - display: inline-block; - - li { - display: list-item; - list-style-type: none; - } - } - } - - &.not_moderated { - border: 1px solid red; - } - - &:hover .tooltip { - visibility: visible; - } - } - } - - #view { - position: fixed; - width: 100vw; - height: 100vh; - display: flex; - justify-content: center; - align-items: center; - top: 0; - left: 0; - z-index: 10; - visibility: hidden; - background-color: rgba(10, 10, 10, 0.9); - overflow: hidden; - - &.active { - visibility: visible; - } - - #placeholder { - width: 80vw; - height: 80vh; - display: flex; - justify-content: center; - align-items: center; - top: 0; - left: 0; - - img { - max-width: 100%; - max-height: 100%; - } - } - } -} - /*---------------------------ACCOUNTING----------------------------*/ #accounting { .journal-table { diff --git a/core/templates/core/poster_list.jinja b/core/templates/core/poster_list.jinja deleted file mode 100644 index fe65658c..00000000 --- a/core/templates/core/poster_list.jinja +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "core/base.jinja" %} - -{% block script %} - {{ super() }} - -{% endblock %} - - -{% block title %} - {% trans %}Poster{% endtrans %} -{% endblock %} - -{% block content %} -
- -
-

{% trans %}Posters{% endtrans %}

- -
- -
- - {% if poster_list.count() == 0 %} -
{% trans %}No posters{% endtrans %}
- {% else %} - - {% for poster in poster_list %} -
-
{{ poster.name }}
-
-
-
{{ poster.date_begin | date("d/M/Y H:m") }}
-
{{ poster.date_end | date("d/M/Y H:m") }}
-
- {% trans %}Edit{% endtrans %} -
- {% endfor %} - - {% endif %} - -
- -
- -
-{% endblock %} - - - diff --git a/core/views/files.py b/core/views/files.py index f8539080..d5ffabb6 100644 --- a/core/views/files.py +++ b/core/views/files.py @@ -13,6 +13,7 @@ # # import mimetypes +from pathlib import Path from urllib.parse import quote, urljoin # This file contains all the views that concern the page model @@ -48,6 +49,41 @@ from core.views.widgets.select import ( from counter.utils import is_logged_in_counter +def send_raw_file(path: Path) -> HttpResponse: + """Send a file located in the MEDIA_ROOT + + This handles all the logic of using production reverse proxy or debug server. + + THIS DOESN'T CHECK ANY PERMISSIONS ! + """ + if not path.is_relative_to(settings.MEDIA_ROOT): + raise Http404 + + if not path.is_file() or not path.exists(): + raise Http404 + + response = HttpResponse( + headers={"Content-Disposition": f'inline; filename="{quote(path.name)}"'} + ) + if not settings.DEBUG: + # When receiving a response with the Accel-Redirect header, + # the reverse proxy will automatically handle the file sending. + # This is really hard to test (thus isn't tested) + # so please do not mess with this. + response["Content-Type"] = "" # automatically set by nginx + response["X-Accel-Redirect"] = quote( + urljoin(settings.MEDIA_URL, str(path.relative_to(settings.MEDIA_ROOT))) + ) + return response + + with open(path, "rb") as filename: + response.content = FileWrapper(filename) + response["Content-Type"] = mimetypes.guess_type(path)[0] + response["Last-Modified"] = http_date(path.stat().st_mtime) + response["Content-Length"] = path.stat().st_size + return response + + def send_file( request: HttpRequest, file_id: int, @@ -66,28 +102,7 @@ def send_file( raise PermissionDenied name = getattr(f, file_attr).name - response = HttpResponse( - headers={"Content-Disposition": f'inline; filename="{quote(name)}"'} - ) - if not settings.DEBUG: - # When receiving a response with the Accel-Redirect header, - # the reverse proxy will automatically handle the file sending. - # This is really hard to test (thus isn't tested) - # so please do not mess with this. - response["Content-Type"] = "" # automatically set by nginx - response["X-Accel-Redirect"] = quote(urljoin(settings.MEDIA_URL, name)) - return response - - filepath = settings.MEDIA_ROOT / name - # check if file exists on disk - if not filepath.exists(): - raise Http404 - with open(filepath, "rb") as filename: - response.content = FileWrapper(filename) - response["Content-Type"] = mimetypes.guess_type(filepath)[0] - response["Last-Modified"] = http_date(f.date.timestamp()) - response["Content-Length"] = filepath.stat().st_size - return response + return send_raw_file(settings.MEDIA_ROOT / name) class MultipleFileInput(forms.ClearableFileInput): diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index df9689e5..3ae57bdf 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-04 21:59+0100\n" +"POT-Creation-Date: 2025-01-04 23:05+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -356,9 +356,8 @@ msgstr "Nouveau compte club" #: com/templates/com/news_admin_list.jinja com/templates/com/poster_list.jinja #: com/templates/com/screen_list.jinja com/templates/com/weekmail.jinja #: core/templates/core/file.jinja core/templates/core/group_list.jinja -#: core/templates/core/page.jinja core/templates/core/poster_list.jinja -#: core/templates/core/user_tools.jinja core/views/user.py -#: counter/templates/counter/cash_summary_list.jinja +#: core/templates/core/page.jinja core/templates/core/user_tools.jinja +#: core/views/user.py counter/templates/counter/cash_summary_list.jinja #: counter/templates/counter/counter_list.jinja #: election/templates/election/election_detail.jinja #: forum/templates/forum/macros.jinja @@ -1140,7 +1139,7 @@ msgid "New Trombi" msgstr "Nouveau Trombi" #: club/templates/club/club_tools.jinja com/templates/com/poster_list.jinja -#: core/templates/core/poster_list.jinja core/templates/core/user_tools.jinja +#: core/templates/core/user_tools.jinja msgid "Posters" msgstr "Affiches" @@ -1558,17 +1557,46 @@ msgstr "Événements aujourd'hui et dans les prochains jours" msgid "Nothing to come..." msgstr "Rien à venir..." -#: com/templates/com/news_list.jinja -msgid "Coming soon... don't miss!" -msgstr "Prochainement... à ne pas rater!" - #: com/templates/com/news_list.jinja msgid "All coming events" msgstr "Tous les événements à venir" #: com/templates/com/news_list.jinja -msgid "Agenda" -msgstr "Agenda" +msgid "Links" +msgstr "Liens" + +#: com/templates/com/news_list.jinja +msgid "Our services" +msgstr "Nos services" + +#: com/templates/com/news_list.jinja pedagogy/templates/pedagogy/guide.jinja +msgid "UV Guide" +msgstr "Guide des UVs" + +#: com/templates/com/news_list.jinja core/templates/core/base/navbar.jinja +msgid "Matmatronch" +msgstr "Matmatronch" + +#: com/templates/com/news_list.jinja core/templates/core/base/navbar.jinja +#: core/templates/core/user_tools.jinja +msgid "Elections" +msgstr "Élections" + +#: com/templates/com/news_list.jinja +msgid "Social media" +msgstr "Réseaux sociaux" + +#: com/templates/com/news_list.jinja +msgid "Discord" +msgstr "Discord" + +#: 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" @@ -1580,11 +1608,10 @@ msgid "%(age)s year old" msgstr "%(age)s ans" #: com/templates/com/news_list.jinja com/tests.py -msgid "You need an up to date subscription to access this content" -msgstr "Votre cotisation doit être à jour pour accéder à cette section" +msgid "You need to subscribe to access this content" +msgstr "Vous devez cotiser pour accéder à ce contenu" #: com/templates/com/poster_edit.jinja com/templates/com/poster_list.jinja -#: core/templates/core/poster_list.jinja msgid "Poster" msgstr "Affiche" @@ -1598,15 +1625,15 @@ msgid "Posters - edit" msgstr "Affiche - modifier" #: com/templates/com/poster_list.jinja com/templates/com/screen_list.jinja -#: core/templates/core/poster_list.jinja sas/templates/sas/main.jinja +#: sas/templates/sas/main.jinja msgid "Create" msgstr "Créer" -#: com/templates/com/poster_list.jinja core/templates/core/poster_list.jinja +#: com/templates/com/poster_list.jinja msgid "Moderation" msgstr "Modération" -#: com/templates/com/poster_list.jinja core/templates/core/poster_list.jinja +#: com/templates/com/poster_list.jinja msgid "No posters" msgstr "Aucune affiche" @@ -2233,10 +2260,6 @@ msgstr "Les clubs de L'AE" msgid "Others UTBM's Associations" msgstr "Les autres associations de l'UTBM" -#: core/templates/core/base/navbar.jinja core/templates/core/user_tools.jinja -msgid "Elections" -msgstr "Élections" - #: core/templates/core/base/navbar.jinja msgid "Big event" msgstr "Grandes Activités" @@ -2264,10 +2287,6 @@ msgstr "Eboutic" msgid "Services" msgstr "Services" -#: core/templates/core/base/navbar.jinja -msgid "Matmatronch" -msgstr "Matmatronch" - #: core/templates/core/base/navbar.jinja launderette/models.py #: launderette/templates/launderette/launderette_book.jinja #: launderette/templates/launderette/launderette_book_choose.jinja @@ -4859,10 +4878,6 @@ msgstr "signalant" msgid "reason" msgstr "raison" -#: pedagogy/templates/pedagogy/guide.jinja -msgid "UV Guide" -msgstr "Guide des UVs" - #: pedagogy/templates/pedagogy/guide.jinja #, python-format msgid "%(display_name)s" diff --git a/locale/fr/LC_MESSAGES/djangojs.po b/locale/fr/LC_MESSAGES/djangojs.po index 4c7c5dec..a8b7d40d 100644 --- a/locale/fr/LC_MESSAGES/djangojs.po +++ b/locale/fr/LC_MESSAGES/djangojs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-04 22:00+0100\n" +"POT-Creation-Date: 2025-01-04 23:07+0100\n" "PO-Revision-Date: 2024-09-17 11:54+0200\n" "Last-Translator: Sli \n" "Language-Team: AE info \n" @@ -17,6 +17,10 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +#: com/static/bundled/com/components/ics-calendar-index.ts +msgid "More info" +msgstr "Plus d'informations" + #: core/static/bundled/core/components/ajax-select-base.ts msgid "Remove" msgstr "Retirer" diff --git a/package-lock.json b/package-lock.json index 9b49ac0e..bfa05f40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,10 @@ "dependencies": { "@alpinejs/sort": "^3.14.7", "@fortawesome/fontawesome-free": "^6.6.0", + "@fullcalendar/core": "^6.1.15", + "@fullcalendar/daygrid": "^6.1.15", + "@fullcalendar/icalendar": "^6.1.15", + "@fullcalendar/list": "^6.1.15", "@hey-api/client-fetch": "^0.4.0", "@sentry/browser": "^8.34.0", "@zip.js/zip.js": "^2.7.52", @@ -2384,6 +2388,39 @@ "node": ">=6" } }, + "node_modules/@fullcalendar/core": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz", + "integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==", + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz", + "integrity": "sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.15" + } + }, + "node_modules/@fullcalendar/icalendar": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/icalendar/-/icalendar-6.1.15.tgz", + "integrity": "sha512-iroDc02fjxWCEYE9Lg8x+4HCJTrt04ZgDddwm0LLaWUbtx24rEcnzJP34NUx0KOTLsBjel6U/33lXvU9qDCrhg==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.15", + "ical.js": "^1.4.0" + } + }, + "node_modules/@fullcalendar/list": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.15.tgz", + "integrity": "sha512-U1bce04tYDwkFnuVImJSy2XalYIIQr6YusOWRPM/5ivHcJh67Gm8CIMSWpi3KdRSNKFkqBxLPkfZGBMaOcJYug==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.15" + } + }, "node_modules/@hey-api/client-fetch": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.4.0.tgz", @@ -4162,6 +4199,12 @@ "node": ">=16.17.0" } }, + "node_modules/ical.js": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/ical.js/-/ical.js-1.5.0.tgz", + "integrity": "sha512-7ZxMkogUkkaCx810yp0ZGKvq1ZpRgJeornPttpoxe6nYZ3NLesZe1wWMXDdwTkj/b5NtXT+Y16Aakph/ao98ZQ==", + "peer": true + }, "node_modules/import-from-esm": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.3.4.tgz", @@ -4924,6 +4967,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/package.json b/package.json index 9721eea4..379fc782 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "#openapi": "./staticfiles/generated/openapi/index.ts", "#core:*": "./core/static/bundled/*", "#pedagogy:*": "./pedagogy/static/bundled/*", - "#counter:*": "./counter/static/bundled/*" + "#counter:*": "./counter/static/bundled/*", + "#com:*": "./com/static/bundled/*" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -36,6 +37,10 @@ "dependencies": { "@alpinejs/sort": "^3.14.7", "@fortawesome/fontawesome-free": "^6.6.0", + "@fullcalendar/core": "^6.1.15", + "@fullcalendar/daygrid": "^6.1.15", + "@fullcalendar/icalendar": "^6.1.15", + "@fullcalendar/list": "^6.1.15", "@hey-api/client-fetch": "^0.4.0", "@sentry/browser": "^8.34.0", "@zip.js/zip.js": "^2.7.52", diff --git a/poetry.lock b/poetry.lock index 311df18a..695b4503 100644 --- a/poetry.lock +++ b/poetry.lock @@ -931,6 +931,23 @@ files = [ {file = "hiredis-3.1.0.tar.gz", hash = "sha256:51d40ac3611091020d7dea6b05ed62cb152bff595fa4f931e7b6479d777acf7c"}, ] +[[package]] +name = "ical" +version = "8.3.0" +description = "Python iCalendar implementation (rfc 2445)" +optional = false +python-versions = ">=3.10" +files = [ + {file = "ical-8.3.0-py3-none-any.whl", hash = "sha256:606f2f561bd8b75cb726710dddbb20f3f84dfa1d6323550947dba97359423850"}, + {file = "ical-8.3.0.tar.gz", hash = "sha256:e277cc518cbb0132e6827c318c8ec3b379b125ebf0a2a44337f08795d5530937"}, +] + +[package.dependencies] +pydantic = ">=1.9.1" +pyparsing = ">=3.0.9" +python-dateutil = ">=2.8.2" +tzdata = ">=2023.3" + [[package]] name = "identify" version = "2.6.3" @@ -1883,6 +1900,20 @@ pyyaml = "*" [package.extras] extra = ["pygments (>=2.12)"] +[[package]] +name = "pyparsing" +version = "3.2.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, + {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "pytest" version = "8.3.4" @@ -2724,4 +2755,4 @@ filelock = ">=3.4" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "5836c1a8ad42645d7d045194c8c371754b19957ebdcd2aaa902a2fb3dc97cc53" +content-hash = "7f348f74a05c27e29aaaf25a5584bba9b416f42c3f370db234dd69e5e10dc8df" diff --git a/pyproject.toml b/pyproject.toml index be892cdf..3d761bca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ Sphinx = "^5" # Needed for building xapian tomli = "^2.2.1" django-honeypot = "^1.2.1" pydantic-extra-types = "^2.10.1" +ical = "^8.3.0" [tool.poetry.group.prod.dependencies] # deps used in prod, but unnecessary for development diff --git a/sith/settings.py b/sith/settings.py index 5fdc3786..a88734d3 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -163,6 +163,7 @@ TEMPLATES = [ "ProductType": "counter.models.ProductType", "timezone": "django.utils.timezone", "get_sith": "com.views.sith", + "get_language": "django.utils.translation.get_language", }, "bytecode_cache": { "name": "default", diff --git a/tsconfig.json b/tsconfig.json index 7b3be5fc..aaee9330 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,8 @@ "#openapi": ["./staticfiles/generated/openapi/index.ts"], "#core:*": ["./core/static/bundled/*"], "#pedagogy:*": ["./pedagogy/static/bundled/*"], - "#counter:*": ["./counter/static/bundled/*"] + "#counter:*": ["./counter/static/bundled/*"], + "#com:*": ["./com/static/bundled/*"] } } }