Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Great news improvements, .env for configuration, full uv guide update command and more #1034

Merged
merged 59 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
85c8b7d
Use requests for external requests
imperosol Jan 20, 2025
6d519e3
Custom client for UTBM UV API calls
imperosol Jan 21, 2025
78f3caa
management command to update the whole uv guide
imperosol Jan 21, 2025
5fa431e
Visually differentiate closed UVs from the others
imperosol Jan 21, 2025
4320745
API to moderate and delete news
imperosol Jan 20, 2025
92b2bef
Improve news list display
imperosol Jan 20, 2025
6627ea4
News moderation buttons directly on the home page
imperosol Jan 20, 2025
8f17c3d
Set the moderator when moderating news
imperosol Jan 23, 2025
5252d45
remove alpine instructions for moderated news
imperosol Jan 25, 2025
bf388e6
remove Alpine import in moderation-alert-index.ts
imperosol Jan 25, 2025
b43b531
Add a disclaimer when moderating weekly news
imperosol Jan 25, 2025
2dc32f8
Merge pull request #1021 from ae-utbm/master
imperosol Feb 15, 2025
b31445f
Merge pull request #1010 from ae-utbm/populate-all-uvs
imperosol Feb 15, 2025
88b3f7c
Merge pull request #1009 from ae-utbm/news-list
imperosol Feb 15, 2025
41bff53
use `.env` for project configuration
imperosol Dec 24, 2024
59e90ec
add CSRF_TRUSTED_ORIGINS to settings
imperosol Jan 25, 2025
9945993
simplify `.env.example`
imperosol Feb 16, 2025
a96b374
Merge pull request #971 from ae-utbm/environ
klmp200 Feb 17, 2025
8cb53ce
download button for user pictures and albums
ken-soares Feb 17, 2025
86c68ee
fix indenting
ken-soares Feb 17, 2025
2bed89a
typescriptification de picture-index et bonne instantiation alpine-data
ken-soares Feb 17, 2025
b1db52d
clean typescript
ken-soares Feb 17, 2025
ba21738
biome reformat
ken-soares Feb 17, 2025
e46cba7
Move all user picture logic to sas
klmp200 Feb 18, 2025
93a5c3a
Separate album downloading logic from user display. Allow downloading…
klmp200 Feb 18, 2025
e8db68b
Add missing translations
klmp200 Feb 18, 2025
f7ff77b
Use real images with lazy loading in sas albums and user pictures
klmp200 Feb 18, 2025
a87016a
Apply some review comments
klmp200 Feb 20, 2025
2918048
Improve download user album button
klmp200 Feb 20, 2025
219700f
Add redirect for user picture url
klmp200 Feb 20, 2025
1978658
Allow transactions on counter when an user has recorded too many prod…
klmp200 Feb 21, 2025
f4ff247
Remove call from removed loadCounter function
klmp200 Feb 23, 2025
9c0d89d
Give the student role when creating a new user subscription
imperosol Feb 24, 2025
aa60462
Merge pull request #1028 from ae-utbm/counters
klmp200 Feb 24, 2025
8705fbe
Merge pull request #1025 from ae-utbm/dl_pictures
klmp200 Feb 24, 2025
e757fb4
replaced check with valid attribute is_check
ken-soares Feb 24, 2025
c272cad
Merge pull request #1030 from ae-utbm/subscription-student-status
imperosol Feb 25, 2025
1d17741
change upcoming news selection on main page
imperosol Feb 15, 2025
fc3b82c
Make upcoming nws scrollable on y-overflow
imperosol Feb 16, 2025
86c2ea7
API route to fetch news dates
imperosol Feb 17, 2025
0e88260
fix news dates timestamp in populate.py
imperosol Feb 19, 2025
2def57d
Close alerts related to a moderated event
imperosol Feb 19, 2025
71b3588
Add a "see more" button on news dates list
imperosol Feb 19, 2025
94d2c56
move hybrid translation to full front translation
imperosol Feb 25, 2025
01c92fe
fix warning message display on subsequently loaded news
imperosol Feb 25, 2025
6af0324
fix `Selling.__str__`
imperosol Feb 25, 2025
e936f0d
Merge pull request #1024 from ae-utbm/news-list
imperosol Feb 25, 2025
2128454
Merge pull request #1032 from ae-utbm/check_cashreg
ken-soares Feb 25, 2025
a1bf86d
Add moderation through calendar widget
klmp200 Feb 20, 2025
92d282f
Add possibility to de-moderate news through api and calendar widget
klmp200 Feb 25, 2025
f9c36c8
Apply review comments
klmp200 Feb 24, 2025
be87af5
Merge pull request #1033 from ae-utbm/fixed
imperosol Feb 25, 2025
2e71275
Connect calendar moderation with outside moderation
klmp200 Feb 25, 2025
4890fcf
Rename news moderate to publish
klmp200 Feb 25, 2025
07028c8
Harmonize news date display
klmp200 Feb 25, 2025
10701cc
Synchronize calendar moderation and news list moderation
klmp200 Feb 25, 2025
a01ea13
Fix crash when no news is available
klmp200 Feb 25, 2025
a653f98
Apply review comments
klmp200 Feb 25, 2025
1f1cd2c
Merge pull request #1027 from ae-utbm/calendar-moderation
klmp200 Feb 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
HTTPS=off
SITH_DEBUG=true

# This is not the real key used in prod
SECRET_KEY=(4sjxvhz@m5$0a$j0_pqicnc$s!vbve)z+&++m%g%bjhlz4+g2

# comment the sqlite line and uncomment the postgres one to switch the dbms
DATABASE_URL=sqlite:///db.sqlite3
#DATABASE_URL=postgres://user:[email protected]:5432/sith

CACHE_URL=redis://127.0.0.1:6379/0
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
branches: [master, taiste]
workflow_dispatch:

env:
SECRET_KEY: notTheRealOne
DATABASE_URL: sqlite:///db.sqlite3

jobs:
pre-commit:
name: Launch pre-commits checks (ruff)
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ node_modules/

# compiled documentation
site/
.env
14 changes: 14 additions & 0 deletions club/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
78 changes: 75 additions & 3 deletions com/api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
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 django.http import Http404, HttpResponse
from ninja import Query
from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.permissions import IsAuthenticated
from ninja_extra.schemas import PaginatedResponseSchema

from com.calendar import IcsCalendar
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


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

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

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

@route.get(
"/unpublished.ics",
permissions=[IsAuthenticated],
url_name="calendar_unpublished",
)
def calendar_unpublished(self):
return HttpResponse(
IcsCalendar.get_unpublished(self.context.request.user),
content_type="text/calendar",
)


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

@route.patch(
"/{int:news_id}/unpublish",
permissions=[HasPerm("com.moderate_news")],
url_name="unpublish_news",
)
def unpublish_news(self, news_id: int):
news = self.get_object_or_exception(News, id=news_id)
if news.is_published:
news.is_published = False
news.moderator = self.context.request.user
news.save()

@route.delete(
"/{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")
)
50 changes: 34 additions & 16 deletions com/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
from pathlib import Path
from typing import final

import urllib3
import requests
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.db.models import F, QuerySet
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
from core.models import User


@final
Expand All @@ -35,16 +37,15 @@ def get_external(cls, expiration: timedelta = timedelta(hours=1)) -> Path | None

@classmethod
def make_external(cls) -> Path | None:
calendar = urllib3.request(
"GET",
"https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics",
calendar = requests.get(
"https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics"
)
if calendar.status != 200:
if not calendar.ok:
return None

cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True)
with open(cls._EXTERNAL_CALENDAR, "wb") as f:
_ = f.write(calendar.data)
_ = f.write(calendar.content)
return cls._EXTERNAL_CALENDAR

@classmethod
Expand All @@ -56,21 +57,38 @@ def get_internal(cls) -> Path:
@classmethod
def make_internal(cls) -> Path:
# Updated through a post_save signal on News in com.signals
# 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(
cls.ics_from_queryset(
NewsDate.objects.filter(
news__is_published=True,
end_date__gte=timezone.now() - (relativedelta(months=6)),
)
)
)
return cls._INTERNAL_CALENDAR

@classmethod
def get_unpublished(cls, user: User) -> bytes:
return cls.ics_from_queryset(
NewsDate.objects.viewable_by(user).filter(
news__is_published=False,
end_date__gte=timezone.now() - (relativedelta(months=6)),
),
)

@classmethod
def ics_from_queryset(cls, queryset: QuerySet[NewsDate]) -> bytes:
calendar = Calendar()
for news_date in NewsDate.objects.filter(
news__is_moderated=True,
end_date__gte=timezone.now() - (relativedelta(months=6)),
).prefetch_related("news"):
for news_date in queryset.annotate(news_title=F("news__title")):
event = Event(
summary=news_date.news.title,
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
return IcsCalendarStream.calendar_to_ics(calendar).encode("utf-8")
10 changes: 5 additions & 5 deletions com/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ class Meta:
"content": MarkdownInput,
}

auto_moderate = forms.BooleanField(
label=_("Automoderation"),
auto_publish = forms.BooleanField(
label=_("Auto publication"),
widget=CheckboxInput(attrs={"class": "switch"}),
required=False,
)
Expand Down Expand Up @@ -182,12 +182,12 @@ def full_clean(self):
def save(self, commit: bool = True): # noqa FBT001
self.instance.author = self.author
if (self.author.is_com_admin or self.author.is_root) and (
self.cleaned_data.get("auto_moderate") is True
self.cleaned_data.get("auto_publish") is True
):
self.instance.is_moderated = True
self.instance.is_published = True
self.instance.moderator = self.author
else:
self.instance.is_moderated = False
self.instance.is_published = False
created_news = super().save(commit=commit)
self.date_form.save(commit=commit, news=created_news)
return created_news
16 changes: 16 additions & 0 deletions com/migrations/0009_remove_news_is_moderated_news_is_published.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [("com", "0008_alter_news_options_alter_newsdate_options_and_more")]

operations = [
migrations.RenameField(
model_name="news", old_name="is_moderated", new_name="is_published"
),
migrations.AlterField(
model_name="news",
name="is_published",
field=models.BooleanField(default=False, verbose_name="is published"),
),
]
32 changes: 25 additions & 7 deletions com/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def is_owned_by(self, user):

class NewsQuerySet(models.QuerySet):
def moderated(self) -> Self:
return self.filter(is_moderated=True)
return self.filter(is_published=True)

def viewable_by(self, user: User) -> Self:
"""Filter news that the given user can view.
Expand All @@ -68,7 +68,7 @@ def viewable_by(self, user: User) -> Self:
"""
if user.has_perm("com.view_unmoderated_news"):
return self
q_filter = Q(is_moderated=True)
q_filter = Q(is_published=True)
if user.is_authenticated:
q_filter |= Q(author_id=user.id)
return self.filter(q_filter)
Expand Down Expand Up @@ -104,7 +104,7 @@ class News(models.Model):
verbose_name=_("author"),
on_delete=models.PROTECT,
)
is_moderated = models.BooleanField(_("is moderated"), default=False)
is_published = models.BooleanField(_("is published"), default=False)
moderator = models.ForeignKey(
User,
related_name="moderated_news",
Expand All @@ -127,7 +127,7 @@ def __str__(self):

def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.is_moderated:
if self.is_published:
return
for user in User.objects.filter(
groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID]
Expand All @@ -154,15 +154,15 @@ def can_be_edited_by(self, user: User):

def can_be_viewed_by(self, user: User):
return (
self.is_moderated
self.is_published
or user.has_perm("com.view_unmoderated_news")
or (user.is_authenticated and self.author_id == user.id)
)


def news_notification_callback(notif):
count = News.objects.filter(
dates__start_date__gt=timezone.now(), is_moderated=False
dates__start_date__gt=timezone.now(), is_published=False
).count()
if count:
notif.viewed = False
Expand All @@ -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_published=True)
if user.is_authenticated:
q_filter |= Q(news__author_id=user.id)
return self.filter(q_filter)


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

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

objects = NewsDateQuerySet.as_manager()

class Meta:
verbose_name = _("news date")
verbose_name_plural = _("news dates")
Expand Down Expand Up @@ -319,7 +337,7 @@ def __str__(self):

def active_posters(self):
now = timezone.now()
return self.posters.filter(is_moderated=True, date_begin__lte=now).filter(
return self.posters.filter(d=True, date_begin__lte=now).filter(
Q(date_end__isnull=True) | Q(date_end__gte=now)
)

Expand Down
Loading
Loading