Compare commits

...

31 Commits

Author SHA1 Message Date
Sli
9be4b8f58e Add macro to refresh messages from htmx swap 2025-09-27 16:59:43 +02:00
Bartuccio Antoine
289ffe1109 Merge pull request #1190 from ae-utbm/alpine-notifications
Add alpine notifications plugin
2025-09-26 18:29:04 +02:00
thomas girod
ff5bb04af1 Merge pull request #1188 from ae-utbm/autocomplete-sas
Clear tom select text when identifying users in SAS
2025-09-26 15:48:24 +02:00
Sli
ca50e5dc81 Add alpine notifications plugin 2025-09-26 14:54:26 +02:00
Bartuccio Antoine
f015bde768 Merge pull request #1186 from ae-utbm/jquery
Remove JQuery
2025-09-26 14:36:02 +02:00
Sli
bb09fd0feb Apply review comments 2025-09-26 14:33:17 +02:00
Sli
210278440a Change notification zone position 2025-09-26 13:36:36 +02:00
Sli
e041da9cf4 Remove unnecessary complex anonymous callback on poster list 2025-09-25 22:07:29 +02:00
Sli
54c1957776 Move notifications from eboutic checkout to billing info fragment 2025-09-25 16:02:56 +02:00
Sli
30356d97f3 Use SuccessMessageMixin on trombi 2025-09-25 16:02:56 +02:00
Sli
7eaf25a64f Remove QuikNotifMixin 2025-09-25 16:02:56 +02:00
Sli
c6e86841b3 Remove jquery remeanants 2025-09-25 16:02:56 +02:00
Sli
cbe9887efb Create unified notification system 2025-09-25 16:02:55 +02:00
Noa Fouich
980952807a Merge pull request #1189 from ae-utbm/deleted_barman_user_fix
Deleted barman user fix
2025-09-25 16:01:36 +02:00
Noa Fouich
0b7c516f18 adding test 2025-09-25 15:57:21 +02:00
Noa Fouich
e186052283 Fix deleted barman on user account
# Conflicts:
#	locale/fr/LC_MESSAGES/django.po
2025-09-25 15:57:16 +02:00
imperosol
ec80b72a25 clear tom select text when identifying users in SAS 2025-09-25 07:38:44 +02:00
Bartuccio Antoine
6cd3875b2b Merge pull request #1187 from ae-utbm/fix-search
Remove `s` shortcut for search bar
2025-09-24 18:09:00 +02:00
Sli
ad8b003336 Remove s shortcut for search bar 2025-09-24 16:36:55 +02:00
Bartuccio Antoine
b4f5a866e3 Merge pull request #1185 from ae-utbm/posters
Remove jquery from posters
2025-09-23 14:59:24 +02:00
Sli
d87b069769 Apply review comments 2025-09-23 10:28:05 +02:00
thomas girod
9461b2e5d9 Merge pull request #1184 from ae-utbm/page-N+1
fix: N+1 query on PageListView
2025-09-23 09:18:24 +02:00
Sli
4701c0804b Fix slideshow transition 2025-09-22 23:06:18 +02:00
imperosol
acb6c6ce9c fix: N+1 query on PageListView 2025-09-22 18:14:14 +02:00
Sli
95e6fff98b Migrate poster view to alpine 2025-09-22 14:30:23 +02:00
thomas girod
f1a5a0781c Merge pull request #1181 from ae-utbm/fix-subscription
Fix subscription
2025-09-22 13:41:15 +02:00
imperosol
854dd2d9e7 add disclaimer for subscription purchase with AE account 2025-09-22 13:28:42 +02:00
imperosol
a7c96425c8 fix: ClubSellingView N+1 queries 2025-09-22 13:28:42 +02:00
Sli
dff23fae7f Migrate slideshow to alpine 2025-09-22 13:26:28 +02:00
thomas girod
34b0dc3302 Merge pull request #1182 from ae-utbm/fix-pagerev
fix: 500 on page properties edit
2025-09-22 13:04:22 +02:00
imperosol
ce2ef78a6d fix: 500 on page properties edit 2025-09-21 16:01:17 +02:00
51 changed files with 699 additions and 611 deletions

View File

@@ -83,9 +83,10 @@ TODO : rewrite the pagination used in this template an Alpine one
</table>
<script type="text/javascript">
function formPagination(link){
$("form").attr("action", link.href);
const form = document.getElementById("form")
form.action = link.href;
link.href = "javascript:void(0)"; // block link action
$("form").submit();
form.submit();
}
</script>
{{ paginate(paginated_result, paginator, "formPagination(this)") }}

View File

@@ -344,7 +344,7 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
form = self.get_form()
if form.is_valid():
if not len([v for v in form.cleaned_data.values() if v is not None]):
qs = Selling.objects.filter(id=-1)
qs = Selling.objects.none()
if form.cleaned_data["begin_date"]:
qs = qs.filter(date__gte=form.cleaned_data["begin_date"])
if form.cleaned_data["end_date"]:
@@ -362,7 +362,9 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
if len(selected_products) > 0:
qs = qs.filter(product__in=selected_products)
kwargs["result"] = qs.all().order_by("-id")
kwargs["result"] = qs.select_related(
"counter", "counter__club", "customer", "customer__user", "seller"
).order_by("-id")
kwargs["total"] = sum([s.quantity * s.unit_price for s in kwargs["result"]])
total_quantity = qs.all().aggregate(Sum("quantity"))
if total_quantity["quantity__sum"]:

View File

@@ -0,0 +1,49 @@
const INTERVAL = 10;
interface Poster {
url: string; // URL of the poster
displayTime: number; // Number of seconds to display that poster
}
document.addEventListener("alpine:init", () => {
Alpine.data("slideshow", (posters: Poster[]) => ({
posters: posters,
progress: 0,
elapsed: 0,
current: 0,
previous: 0,
init() {
this.$watch("elapsed", () => {
const displayTime = this.posters[this.current].displayTime * 1000;
if (this.elapsed > displayTime) {
this.previous = this.current;
this.current = this.getNext();
this.elapsed = 0;
}
if (displayTime === 0) {
this.progress = 100;
} else {
this.progress = (100 * this.elapsed) / displayTime;
}
});
setInterval(() => {
this.elapsed += INTERVAL;
}, INTERVAL);
},
getNext() {
return (this.current + 1) % this.posters.length;
},
async toggleFullScreen(event: Event) {
if (document.fullscreenElement) {
await document.exitFullscreen();
return;
}
const target = event.target as HTMLElement;
await target.requestFullscreen();
},
}));
});

View File

@@ -111,7 +111,7 @@
top: 0;
left: 0;
z-index: 10;
content: "Click to expand";
content: attr(hover);
color: white;
background-color: rgba(black, 0.5);
}

View File

@@ -1,23 +0,0 @@
$(document).ready(() => {
$("#poster_list #view").click(() => {
$("#view").removeClass("active");
});
$("#poster_list .poster .image").click((e) => {
let el = $(e.target);
if (el.hasClass("image")) {
el = el.find("img");
}
$("#poster_list #view #placeholder").html(el.clone());
$("#view").addClass("active");
});
$(document).keyup((e) => {
if (e.keyCode === 27) {
// escape key maps to keycode `27`
e.preventDefault();
$("#view").removeClass("active");
}
});
});

View File

@@ -1,98 +0,0 @@
$(document).ready(() => {
const transitionTime = 1000;
let i = 0;
const max = $("#slideshow .slide").length;
function enterFullscreen() {
const element = document.getElementById("slideshow");
$(element).addClass("fullscreen");
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
}
function exitFullscreen() {
const element = document.getElementById("slideshow");
$(element).removeClass("fullscreen");
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
function initProgressBar() {
$("#slideshow #progress_bar").css("transition", "none");
$("#slideshow #progress_bar").removeClass("progress");
$("#slideshow #progress_bar").addClass("init");
}
function startProgressBar(displayTime) {
$("#slideshow #progress_bar").removeClass("init");
$("#slideshow #progress_bar").addClass("progress");
$("#slideshow #progress_bar").css("transition", `width ${displayTime}s linear`);
}
function next() {
initProgressBar();
const slide = $($("#slideshow .slide").get(i % max));
slide.removeClass("center");
slide.addClass("left");
const nextSlide = $($("#slideshow .slide").get((i + 1) % max));
nextSlide.removeClass("right");
nextSlide.addClass("center");
const displayTime = nextSlide.attr("display_time") || 2;
$("#slideshow .bullet").removeClass("active");
const bullet = $("#slideshow .bullet")[(i + 1) % max];
$(bullet).addClass("active");
i = (i + 1) % max;
setTimeout(() => {
const othersLeft = $("#slideshow .slide.left");
othersLeft.removeClass("left");
othersLeft.addClass("right");
startProgressBar(displayTime);
setTimeout(next, displayTime * 1000);
}, transitionTime);
}
const displayTime = $("#slideshow .center").attr("display_time");
initProgressBar();
setTimeout(() => {
if (max > 1) {
startProgressBar(displayTime);
setTimeout(next, displayTime * 1000);
}
}, 10);
$("#slideshow").click(() => {
if ($("#slideshow").hasClass("fullscreen")) {
exitFullscreen();
} else {
enterFullscreen();
}
});
$(document).keyup((e) => {
if (e.keyCode === 27) {
// escape key maps to keycode `27`
e.preventDefault();
exitFullscreen();
}
});
});

View File

@@ -1,4 +1,4 @@
body{
body {
position: absolute;
width: 100vw;
height: 100vh;
@@ -7,22 +7,22 @@ body{
margin: 0;
}
#slideshow{
#slideshow {
position: relative;
background-color: lightgrey;
height: 100%;
*{
* {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
&:hover{
&:hover {
&::before{
&::before {
position: absolute;
width: 100%;
@@ -34,7 +34,7 @@ body{
z-index: 10;
content: "Click to expand";
content: attr(hover);
color: white;
background-color: rgba(black, 0.5);
@@ -43,7 +43,7 @@ body{
}
&.fullscreen{
&:fullscreen {
position: fixed;
width: 100%;
height: 100%;
@@ -51,57 +51,78 @@ body{
left: 0;
background: none;
&:before{
display:none;
&:before {
display: none;
}
#slides{
#slides {
height: 100vh;
}
}
#slides{
#slides {
position: relative;
height: 100%;
overflow: hidden;
background-color: grey;
.slide{
.slide {
position: absolute;
width: 100%;
height: 100%;
display: inline-flex;
display: none;
justify-content: center;
top: 0px;
left: 0%;
background-color: grey;
transition: left 1s ease-out;
img{
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.slide.left{
left: -100%;
}
&.current {
display: inline-flex;
left: 0%;
animation: scrolling-in 1s linear;
}
.slide.center{
left: 0px;
}
&.previous {
display: inline-flex;
animation: scrolling-out 1s linear;
opacity: 0;
transition: opacity 0.1s;
transition-delay: 0.9s;
}
@keyframes scrolling-in {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(0%);
}
}
@keyframes scrolling-out {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(-100%);
}
}
.slide.right{
left: 100%;
transition: none;
}
}
#progress_bullets{
#progress_bullets {
position: absolute;
bottom: 10px;
width: 100%;
@@ -112,7 +133,7 @@ body{
margin-bottom: 10px;
.bullet{
.bullet {
height: 10px;
width: 10px;
@@ -123,27 +144,33 @@ body{
background-color: grey;
&.active{
&.active {
background-color: #c99836;
}
}
}
#progress_bar{
progress {
--color: #304c83;
position: absolute;
bottom: 0px;
height: 10px;
background-color: #304c83;
color: var(--color);
width: 100%;
margin-bottom: 0px;
border: none;
&.init{
width: 0px;
transition: none;
&::-moz-progress-bar {
background: var(--color);
}
&.progress{
width: 100%;
transition: width 10s linear;
&::-webkit-progress-value {
background: var(--color);
}
&[value] {
background-color: transparent;
}
}
}
}

View File

@@ -1,11 +1,5 @@
{% extends "core/base.jinja" %}
{% block script %}
{{ super() }}
<script src="{{ static('com/js/poster_list.js') }}"></script>
{% endblock %}
{% block title %}
{% trans %}Poster{% endtrans %}
{% endblock %}
@@ -15,7 +9,7 @@
{% endblock %}
{% block content %}
<div id="poster_list">
<div id="poster_list" x-data="{ active: null }">
<div id="title">
<h3>{% trans %}Posters{% endtrans %}</h3>
@@ -38,7 +32,13 @@
{% for poster in poster_list %}
<div class="poster{% if not poster.is_moderated %} not_moderated{% endif %}">
<div class="name">{{ poster.name }}</div>
<div class="image"><img src="{{ poster.file.url }}"></img></div>
<div
class="image"
hover="{% trans %}Click to expand{% endtrans %}"
@click="active = $el.firstElementChild"
>
<img src="{{ poster.file.url }}"></img>
</div>
<div class="dates">
<div class="begin">{{ poster.date_begin | localtime | date("d/M/Y H:m") }}</div>
<div class="end">{{ poster.date_end | localtime | date("d/M/Y H:m") }}</div>
@@ -62,7 +62,14 @@
</div>
<div id="view"><div id="placeholder"></div></div>
<div
id="view"
@keyup.escape.window="active = null"
@click="active = null"
:class="{active: active !== null}"
>
<div id="placeholder"><img :src="active?.src"></div>
</div>
</div>
{% endblock %}

View File

@@ -2,28 +2,44 @@
<html lang="fr">
<head>
<title>{% trans %}Slideshow{% endtrans %}</title>
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
<link href="{{ static('css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
<script src="{{ static('bundled/vendored/jquery.min.js') }}"></script>
<script src="{{ static('com/js/slideshow.js') }}"></script>
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script>
<script type="module" src="{{ static('bundled/com/slideshow-index.ts') }}"></script>
</head>
<body>
<div id="slideshow">
<body x-data="slideshow([
{% for poster in posters %}
{
url: '{{ poster.file.url }}',
displayTime: {{ poster.display_time }}
},
{% endfor %}
])">
<div
id="slideshow"
@click="toggleFullScreen"
hover="{% trans %}Click to expand{% endtrans %}"
@keyup.f.window="toggleFullScreen"
>
<div id="slides">
{% for poster in posters %}
<div class="slide {% if loop.first %}center{% else %}right{% endif %}" display_time="{{ poster.display_time }}">
<img src="{{ poster.file.url }}">
<template x-for="(poster, index) in posters">
<div class="slide" :class="{
current: index === current,
previous: index !== current && index === previous,
}">
<img :src="poster.url">
</div>
{% endfor %}
</template>
</div>
<div id="progress_bullets">
{% for poster in posters %}
<div class="bullet {% if loop.first %}active{% endif %}"></div>
{% endfor %}
<template x-for="(poster, index) in posters">
<div class="bullet" :class="{active: current === index}"></div>
</template>
</div>
<div id="progress_bar"></div>
<progress :value="progress" max="100" x-show="posters.length > 1 && progress > 0"></progress>
</div>
</body>

View File

@@ -31,9 +31,7 @@
<td>
<a href="{{ url('com:weekmail_article_edit', article_id=a.id) }}">{% trans %}Edit{% endtrans %}</a> |
<a href="{{ url('com:weekmail_article_delete', article_id=a.id) }}">{% trans %}Delete{% endtrans %}</a> |
<a href="?add_article={{ a.id }}">{% trans %}Add to weekmail{% endtrans %}</a> |
<a href="?up_article={{ a.id }}">{% trans %}Up{% endtrans %}</a> |
<a href="?down_article={{ a.id }}">{% trans %}Down{% endtrans %}</a>
<a href="?add_article={{ a.id }}">{% trans %}Add to weekmail{% endtrans %}</a>
</td>
</tr>
{% endfor %}

View File

@@ -28,6 +28,7 @@ from typing import Any
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import (
PermissionRequiredMixin,
)
@@ -55,7 +56,7 @@ from core.auth.mixins import (
PermissionOrClubBoardRequiredMixin,
)
from core.models import User
from core.views.mixins import QuickNotifMixin, TabedViewMixin
from core.views.mixins import TabedViewMixin
from core.views.widgets.markdown import MarkdownInput
# Sith object
@@ -333,7 +334,7 @@ class NewsFeed(Feed):
# Weekmail
class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, DetailView):
class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView):
model = Weekmail
template_name = "com/weekmail_preview.jinja"
success_url = reverse_lazy("com:weekmail")
@@ -345,12 +346,11 @@ class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, Detai
def post(self, request, *args, **kwargs):
self.object = self.get_object()
messages.success(self.request, _("Weekmail sent successfully"))
if request.POST["send"] == "validate":
try:
self.object.send()
return HttpResponseRedirect(
reverse("com:weekmail") + "?qn_weekmail_send_success"
)
return HttpResponseRedirect(reverse("com:weekmail"))
except SMTPRecipientsRefused as e:
self.bad_recipients = e.recipients
elif request.POST["send"] == "clean":
@@ -361,7 +361,6 @@ class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, Detai
for u in users:
u.preferences.receive_weekmail = False
u.preferences.save()
self.quick_notif_list += ["qn_success"]
return super().get(request, *args, **kwargs)
def get_object(self, queryset=None):
@@ -375,7 +374,7 @@ class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, Detai
return kwargs
class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView):
class WeekmailEditView(ComTabsMixin, CanEditPropMixin, UpdateView):
model = Weekmail
template_name = "com/weekmail.jinja"
form_class = modelform_factory(
@@ -415,7 +414,10 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
art.rank, prev_art.rank = prev_art.rank, art.rank
art.save()
prev_art.save()
self.quick_notif_list += ["qn_success"]
messages.success(
self.request,
_("%(title)s moved up in the Weekmail") % {"title": art.title},
)
if "down_article" in request.GET:
art = get_object_or_404(
WeekmailArticle, id=request.GET["down_article"], weekmail=self.object
@@ -427,7 +429,10 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
art.rank, next_art.rank = next_art.rank, art.rank
art.save()
next_art.save()
self.quick_notif_list += ["qn_success"]
messages.success(
self.request,
_("%(title)s moved down in the Weekmail") % {"title": art.title},
)
if "add_article" in request.GET:
art = get_object_or_404(
WeekmailArticle, id=request.GET["add_article"], weekmail=None
@@ -436,7 +441,10 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
art.rank = self.object.articles.aggregate(Max("rank"))["rank__max"] or 0
art.rank += 1
art.save()
self.quick_notif_list += ["qn_success"]
messages.success(
self.request,
_("%(title)s added to the Weekmail") % {"title": art.title},
)
if "del_article" in request.GET:
art = get_object_or_404(
WeekmailArticle, id=request.GET["del_article"], weekmail=self.object
@@ -444,7 +452,10 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
art.weekmail = None
art.rank = -1
art.save()
self.quick_notif_list += ["qn_success"]
messages.success(
self.request,
_("%(title)s removed from the Weekmail") % {"title": art.title},
)
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
@@ -454,9 +465,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
return kwargs
class WeekmailArticleEditView(
ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView
):
class WeekmailArticleEditView(ComTabsMixin, CanEditPropMixin, UpdateView):
"""Edit an article."""
model = WeekmailArticle
@@ -468,11 +477,10 @@ class WeekmailArticleEditView(
pk_url_kwarg = "article_id"
template_name = "core/edit.jinja"
success_url = reverse_lazy("com:weekmail")
quick_notif_url_arg = "qn_weekmail_article_edit"
current_tab = "weekmail"
class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
class WeekmailArticleCreateView(CreateView):
"""Post an article."""
model = WeekmailArticle
@@ -483,7 +491,6 @@ class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
)
template_name = "core/create.jinja"
success_url = reverse_lazy("core:user_tools")
quick_notif_url_arg = "qn_weekmail_new_article"
def get_initial(self):
if "club" not in self.request.GET:

View File

@@ -768,7 +768,7 @@ class Command(BaseCommand):
s = Subscription(
member=user,
subscription_type=subscription_type,
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1][0],
)
s.subscription_start = s.compute_start(start)
s.subscription_end = s.compute_end(

View File

@@ -1197,6 +1197,18 @@ class NotLocked(LockError):
pass
class PageQuerySet(models.QuerySet):
def viewable_by(self, user: User) -> Self:
if user.is_anonymous:
return self.filter(view_groups=settings.SITH_GROUP_PUBLIC_ID)
if user.has_perm("core.view_page"):
return self.all()
groups_ids = [g.id for g in user.cached_groups]
if user.is_subscribed:
groups_ids.append(settings.SITH_GROUP_SUBSCRIBERS_ID)
return self.filter(view_groups__in=groups_ids)
# This function prevents generating migration upon settings change
def get_default_owner_group():
return settings.SITH_GROUP_ROOT_ID
@@ -1266,6 +1278,8 @@ class Page(models.Model):
_("lock_timeout"), null=True, blank=True, default=None
)
objects = PageQuerySet.as_manager()
class Meta:
unique_together = ("name", "parent")
permissions = (
@@ -1275,12 +1289,9 @@ class Page(models.Model):
def __str__(self):
return self.get_full_name()
def save(self, *args, **kwargs):
def save(self, *args, force_lock: bool = False, **kwargs):
"""Performs some needed actions before and after saving a page in database."""
locked = kwargs.pop("force_lock", False)
if not locked:
locked = self.is_locked()
if not locked:
if not force_lock and not self.is_locked():
raise NotLocked("The page is not locked and thus can not be saved")
self.full_clean()
if not self.id:
@@ -1292,7 +1303,7 @@ class Page(models.Model):
# It also update all the children to maintain correct names
self._full_name = self.get_full_name()
for c in self.children.all():
c.save()
c.save(force_lock=force_lock)
super().save(*args, **kwargs)
self.unset_lock()
@@ -1408,14 +1419,14 @@ class Page(models.Model):
def need_club_redirection(self):
return self.is_club_page and self.name != settings.SITH_CLUB_ROOT_PAGE
def delete(self):
def delete(self, *args, **kwargs):
self.unset_lock_recursive()
self.set_lock_recursive(User.objects.get(id=0))
for child in self.children.all():
child.parent = self.parent
child.save()
child.unset_lock_recursive()
super().delete()
return super().delete(*args, **kwargs)
class PageRev(models.Model):
@@ -1462,9 +1473,12 @@ class PageRev(models.Model):
def get_absolute_url(self):
return reverse("core:page", kwargs={"page_name": self.page._full_name})
def can_be_edited_by(self, user):
def can_be_edited_by(self, user: User) -> bool:
return self.page.can_be_edited_by(user)
def is_owned_by(self, user: User) -> bool:
return any(g.id == self.page.owner_group_id for g in user.cached_groups)
def get_notification_types():
return settings.SITH_NOTIFICATIONS

View File

@@ -1,7 +1,9 @@
import { alpinePlugin } from "#core:utils/notifications";
import sort from "@alpinejs/sort";
import Alpine from "alpinejs";
Alpine.plugin(sort);
Alpine.magic("notifications", alpinePlugin);
window.Alpine = Alpine;
window.addEventListener("DOMContentLoaded", () => {

View File

@@ -0,0 +1,36 @@
export enum NotificationLevel {
Error = "error",
Warning = "warning",
Success = "success",
}
export function createNotification(message: string, level: NotificationLevel) {
const element = document.getElementById("quick-notifications");
if (element === null) {
return false;
}
return element.dispatchEvent(
new CustomEvent("quick-notification-add", {
detail: { text: message, tag: level },
}),
);
}
export function deleteNotifications() {
const element = document.getElementById("quick-notifications");
if (element === null) {
return false;
}
return element.dispatchEvent(new CustomEvent("quick-notification-delete"));
}
export function alpinePlugin() {
return {
error: (message: string) => createNotification(message, NotificationLevel.Error),
warning: (message: string) =>
createNotification(message, NotificationLevel.Warning),
success: (message: string) =>
createNotification(message, NotificationLevel.Success),
clear: () => deleteNotifications(),
};
}

View File

@@ -321,7 +321,6 @@ $hovered-red-text-color: #ff4d4d;
>#header_notif {
box-sizing: border-box;
display: none;
position: absolute;
margin: 0;
background-color: whitesmoke;

View File

@@ -1,38 +0,0 @@
$(() => {
$("#quick_notif li").click(function () {
$(this).hide();
});
});
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function createQuickNotif(msg) {
const el = document.createElement("li");
el.textContent = msg;
el.addEventListener("click", () => el.parentNode.removeChild(el));
document.getElementById("quick_notif").appendChild(el);
}
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function deleteQuickNotifs() {
const el = document.getElementById("quick_notif");
while (el.firstChild) {
el.removeChild(el.firstChild);
}
}
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function displayNotif() {
$("#header_notif").toggle().parent().toggleClass("white");
}
// You can't get the csrf token from the template in a widget
// We get it from a cookie as a workaround, see this link
// https://docs.djangoproject.com/en/2.0/ref/csrf/#ajax
// Sadly, getting the cookie is not possible with CSRF_COOKIE_HTTPONLY or CSRF_USE_SESSIONS is True
// So, the true workaround is to get the token from the dom
// https://docs.djangoproject.com/en/2.0/ref/csrf/#acquiring-the-token-if-csrf-use-sessions-is-true
// biome-ignore lint/style/useNamingConvention: can't find it used anywhere but I will not play with the devil
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function getCSRFToken() {
return $("[name=csrfmiddlewaretoken]").val();
}

View File

@@ -270,17 +270,6 @@ body {
}
/*--------------------------------CONTENT------------------------------*/
#quick_notif {
width: 100%;
margin: 0 auto;
list-style-type: none;
background: $second-color;
li {
padding: 10px;
}
}
#content {
padding: 1em 1%;
box-shadow: $shadow-color 0 5px 10px;

View File

@@ -32,10 +32,6 @@
<script type="module" src="{{ static('bundled/country-flags-index.ts') }}"></script>
<script type="module" src="{{ static('bundled/core/tooltips-index.ts') }}"></script>
<!-- Jquery declared here to be accessible in every django widgets -->
<script src="{{ static('bundled/vendored/jquery.min.js') }}"></script>
<script src="{{ static('core/js/script.js') }}"></script>
{% block additional_css %}{% endblock %}
{% block additional_js %}{% endblock %}
{% endblock %}
@@ -74,17 +70,15 @@
<div id="page">
<ul id="quick_notif">
{% for n in quick_notifs %}
<li>{{ n }}</li>
{% endfor %}
</ul>
<div id="content">
{%- block tabs -%}
{% include "core/base/tabs.jinja" %}
{%- endblock -%}
{% block notifications %}
{% include "core/base/notifications.jinja" %}
{% endblock %}
{%- block errors -%}
{% if error %}
{{ error }}
@@ -101,16 +95,6 @@
{% endblock %}
{% block script %}
<script>
document.addEventListener("keydown", (e) => {
// Looking at the `s` key when not typing in a form
if (e.keyCode !== 83 || ["INPUT", "TEXTAREA", "SELECT"].includes(e.target.nodeName)) {
return;
}
document.getElementById("search").focus();
e.preventDefault(); // Don't type the character in the focused search input
})
</script>
{% endblock %}
</body>
</html>

View File

@@ -74,9 +74,9 @@
{% endif %}
></a>
</div>
<div class="notification">
<a href="#" onclick="displayNotif()">
<i class="fa-regular fa-bell"></i>
<div class="notification" x-data="{display: false}" :class="{white: display}">
<a href="#" @click.prevent="display = !display">
<i :class="`fa-${display ? 'solid': 'regular'} fa-bell`" x-transition></i>
{% set notification_count = user.notifications.filter(viewed=False).count() %}
{% if notification_count > 0 %}
@@ -89,7 +89,7 @@
</span>
{% endif %}
</a>
<div id="header_notif">
<div id="header_notif" x-show="display" x-cloak x-transition @click.outside="display = false">
<ul>
{% if user.notifications.filter(viewed=False).count() > 0 %}
{% for n in user.notifications.filter(viewed=False).order_by('-date') %}

View File

@@ -0,0 +1,24 @@
<div id="quick-notifications"
x-data="{
messages: [
{% if messages %}
{% for message in messages %}
{
tag: '{{ message.tags }}',
text: '{{ message }}',
},
{% endfor %}
{% endif %}
]
}"
@quick-notification-add="(e) => messages.push(e?.detail)"
@quick-notification-delete="messages = []">
<template x-for="(message, index) in messages">
<div class="alert" :class="`alert-${message.tag}`" x-transition>
<span class="alert-main" x-text="message.text"></span>
<span class="clickable" @click="messages = messages.filter((item, i) => i !== index)">
<i class="fa fa-close"></i>
</span>
</div>
</template>
</div>

View File

@@ -15,6 +15,7 @@
{{ select_all_checkbox("add_users") }}
<hr>
{% csrf_token %}
{{ form.non_field_errors() }}
<label for="{{ form.users_removed.id_for_label }}">{{ form.users_removed.label }} :</label>
{{ form.users_removed.errors }}
{% for user in form.users_removed %}

View File

@@ -245,3 +245,26 @@
<button type="button" onclick="checkbox_{{form_id}}(true);">{% trans %}Select All{% endtrans %}</button>
<button type="button" onclick="checkbox_{{form_id}}(false);">{% trans %}Unselect All{% endtrans %}</button>
{% endmacro %}
{% macro update_notifications(messages, clear) %}
{# Update notification area from new messages sent by django backend
This is useful when performing fragment swaps to keep messages up to date
Without this, the fragment would need to take control of the notification area and
this would be an issue when having more than one fragment
Parameters:
messages: messages from django.contrib
clear : optional boolean that controls if notifications should be cleared first. True is the default
#}
{% set clear = clear|default(true) %}
{% if messages %}
<div x-init="() => {
{% if clear %}
$notifications.clear()
{% endif %}
{% for message in messages %}
$notifications.{{ message.tags }}('{{ message }}')
{% endfor %}
}"></div>
{% endif %}
{% endmacro %}

View File

@@ -30,7 +30,11 @@
- {{ purchase.date|localtime|time(DATETIME_FORMAT) }}
</td>
<td>{{ purchase.counter }}</td>
<td><a href="{{ purchase.seller.get_absolute_url() }}">{{ purchase.seller.get_display_name() }}</a></td>
{% if not purchase.seller %}
<td>{% trans %}Deleted user{% endtrans %}</td>
{% else %}
<td><a href="{{ purchase.seller.get_absolute_url() }}">{{ purchase.seller.get_display_name() }}</a></td>
{% endif %}
<td>{{ purchase.label }}</td>
<td>{{ purchase.quantity }}</td>
<td>{{ purchase.quantity * purchase.unit_price }} €</td>

58
core/tests/test_page.py Normal file
View File

@@ -0,0 +1,58 @@
import pytest
from django.conf import settings
from django.contrib.auth.models import Permission
from django.test import Client
from django.urls import reverse
from model_bakery import baker
from pytest_django.asserts import assertRedirects
from core.baker_recipes import board_user, subscriber_user
from core.models import AnonymousUser, Page, User
from sith.settings import SITH_GROUP_OLD_SUBSCRIBERS_ID, SITH_GROUP_SUBSCRIBERS_ID
@pytest.mark.django_db
def test_edit_page(client: Client):
user = board_user.make()
page = baker.prepare(Page)
page.save(force_lock=True)
page.view_groups.add(user.groups.first())
client.force_login(user)
url = reverse("core:page_edit", kwargs={"page_name": page._full_name})
res = client.get(url)
assert res.status_code == 200
res = client.post(url, data={"content": "Hello World"})
assertRedirects(res, reverse("core:page", kwargs={"page_name": page._full_name}))
revision = page.revisions.last()
assert revision.content == "Hello World"
@pytest.mark.django_db
def test_viewable_by():
# remove existing pages to prevent side effect
Page.objects.all().delete()
view_groups = [
[settings.SITH_GROUP_PUBLIC_ID],
[settings.SITH_GROUP_PUBLIC_ID, SITH_GROUP_SUBSCRIBERS_ID],
[SITH_GROUP_SUBSCRIBERS_ID],
[SITH_GROUP_SUBSCRIBERS_ID, SITH_GROUP_OLD_SUBSCRIBERS_ID],
[],
]
pages = baker.make(Page, _quantity=len(view_groups), _bulk_create=True)
for page, groups in zip(pages, view_groups, strict=True):
page.view_groups.set(groups)
viewable = Page.objects.viewable_by(AnonymousUser()).values_list("id", flat=True)
assert set(viewable) == {pages[0].id, pages[1].id}
subscriber = subscriber_user.make()
viewable = Page.objects.viewable_by(subscriber).values_list("id", flat=True)
assert set(viewable) == {p.id for p in pages[0:4]}
root_user = baker.make(
User, user_permissions=[Permission.objects.get(codename="view_page")]
)
viewable = Page.objects.viewable_by(root_user).values_list("id", flat=True)
assert set(viewable) == {p.id for p in pages}

View File

@@ -20,7 +20,8 @@ from core.baker_recipes import (
)
from core.models import Group, User
from core.views import UserTabsMixin
from counter.models import Counter, Refilling, Selling
from counter.baker_recipes import sale_recipe
from counter.models import Counter, Customer, Refilling, Selling
from eboutic.models import Invoice, InvoiceItem
@@ -129,6 +130,31 @@ def test_user_account_not_found(client: Client):
assert res.status_code == 404
@pytest.mark.django_db
def test_is_deleted_barman_shown_as_deleted(client: Client):
customer = baker.make(Customer)
date = now()
sale_recipe.make(
seller=iter([None, baker.make(User)]),
customer=customer,
date=date,
_quantity=2,
_bulk_create=True,
)
client.force_login(customer.user)
res = client.get(
reverse(
"core:user_account_detail",
kwargs={
"user_id": customer.user.id,
"year": date.year,
"month": date.month,
},
)
)
assert res.status_code == 200
class TestFilterInactive(TestCase):
@classmethod
def setUpTestData(cls):

View File

@@ -2,7 +2,6 @@ import copy
import inspect
from typing import Any, ClassVar, LiteralString, Protocol, Unpack
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse
from django.template.loader import render_to_string
@@ -41,36 +40,6 @@ class TabedViewMixin(View):
return kwargs
class QuickNotifMixin:
quick_notif_list = []
def dispatch(self, request, *arg, **kwargs):
# In some cases, the class can stay instanciated, so we need to reset the list
self.quick_notif_list = []
return super().dispatch(request, *arg, **kwargs)
def get_success_url(self):
ret = super().get_success_url()
if hasattr(self, "quick_notif_url_arg"):
if "?" in ret:
ret += "&" + self.quick_notif_url_arg
else:
ret += "?" + self.quick_notif_url_arg
return ret
def get_context_data(self, **kwargs):
"""Add quick notifications to context."""
kwargs = super().get_context_data(**kwargs)
kwargs["quick_notifs"] = []
for n in self.quick_notif_list:
kwargs["quick_notifs"].append(settings.SITH_QUICK_NOTIF[n])
for key, val in settings.SITH_QUICK_NOTIF.items():
for gk in self.request.GET:
if key == gk:
kwargs["quick_notifs"].append(val)
return kwargs
class AllowFragment:
"""Add `is_fragment` to templates. It's only True if the request is emitted by htmx"""

View File

@@ -43,23 +43,25 @@ class CanEditPagePropMixin(CanEditPropMixin):
return res
class PageListView(CanViewMixin, ListView):
class PageListView(ListView):
model = Page
template_name = "core/page_list.jinja"
queryset = (
Page.objects.annotate(
display_name=Coalesce(
Subquery(
PageRev.objects.filter(page=OuterRef("id"))
.order_by("-date")
.values("title")[:1]
),
F("name"),
def get_queryset(self):
return (
Page.objects.viewable_by(self.request.user)
.annotate(
display_name=Coalesce(
Subquery(
PageRev.objects.filter(page=OuterRef("id"))
.order_by("-date")
.values("title")[:1]
),
F("name"),
)
)
.select_related("parent")
)
.prefetch_related("view_groups")
.select_related("parent")
)
class PageView(CanViewMixin, DetailView):
@@ -184,7 +186,7 @@ class PageEditViewBase(CanEditMixin, UpdateView):
)
template_name = "core/pagerev_edit.jinja"
def get_object(self):
def get_object(self, *args, **kwargs):
self.page = Page.get_page_by_full_name(self.kwargs["page_name"])
return self._get_revision()

View File

@@ -65,7 +65,7 @@ from core.views.forms import (
UserGroupsForm,
UserProfileForm,
)
from core.views.mixins import QuickNotifMixin, TabedViewMixin, UseFragmentsMixin
from core.views.mixins import TabedViewMixin, UseFragmentsMixin
from counter.models import Counter, Refilling, Selling
from eboutic.models import Invoice
from subscription.models import Subscription
@@ -564,7 +564,7 @@ class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
current_tab = "groups"
class UserToolsView(LoginRequiredMixin, QuickNotifMixin, UserTabsMixin, TemplateView):
class UserToolsView(LoginRequiredMixin, UserTabsMixin, TemplateView):
"""Displays the logged user's tools."""
template_name = "core/user_tools.jinja"

View File

@@ -4,7 +4,6 @@
heading_level: 3
members:
- TabedViewMixin
- QuickNotifMixin
- AllowFragment
- FragmentMixin
- UseFragmentsMixin

View File

@@ -1,3 +1,5 @@
{% from 'core/macros.jinja' import update_notifications %}
<div id=billing-infos-fragment>
<div
class="collapse"
@@ -29,14 +31,6 @@
>
</form>
</div>
<br>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{{ update_notifications(messages) }}
</div>

View File

@@ -1,5 +1,9 @@
{% extends "core/base.jinja" %}
{% block notifications %}
{# Notifications are moved under the billing form #}
{% endblock %}
{% block title %}
{% trans %}Basket state{% endtrans %}
{% endblock %}
@@ -56,6 +60,7 @@
<div @htmx:after-request="fill">
{{ billing_infos_form }}
</div>
{% include "core/base/notifications.jinja" %}
<form
method="post"
action="{{ settings.SITH_EBOUTIC_ET_URL }}"

View File

@@ -22,14 +22,6 @@
{% block content %}
<h1 id="eboutic-title">{% trans %}Eboutic{% endtrans %}</h1>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<div id="eboutic" x-data="basket({{ last_purchase_time }})">
<div id="basket">
<h3>Panier</h3>

View File

@@ -4,14 +4,6 @@
<h3>{% trans %}Eboutic{% endtrans %}</h3>
<div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% if success %}
{% trans %}Payment successful{% endtrans %}
{% else %}

View File

@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-19 17:22+0200\n"
"POT-Creation-Date: 2025-09-25 15:33+0200\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"
@@ -1103,6 +1103,10 @@ msgstr "Modération"
msgid "No posters"
msgstr "Aucune affiche"
#: com/templates/com/poster_list.jinja com/templates/com/screen_slideshow.jinja
msgid "Click to expand"
msgstr "Cliquez pour agrandir"
#: com/templates/com/poster_moderate.jinja
msgid "Posters - moderation"
msgstr "Affiches - modération"
@@ -1160,14 +1164,6 @@ msgstr "Contenu"
msgid "Add to weekmail"
msgstr "Ajouter au Weekmail"
#: com/templates/com/weekmail.jinja
msgid "Up"
msgstr "Monter"
#: com/templates/com/weekmail.jinja
msgid "Down"
msgstr "Descendre"
#: com/templates/com/weekmail.jinja
msgid "Articles included the next weekmail"
msgstr "Article inclus dans le prochain Weekmail"
@@ -1176,6 +1172,14 @@ msgstr "Article inclus dans le prochain Weekmail"
msgid "Delete from weekmail"
msgstr "Supprimer du Weekmail"
#: com/templates/com/weekmail.jinja
msgid "Up"
msgstr "Monter"
#: com/templates/com/weekmail.jinja
msgid "Down"
msgstr "Descendre"
#: com/templates/com/weekmail_preview.jinja
#: core/templates/core/user_account_detail.jinja
#: pedagogy/templates/pedagogy/uv_detail.jinja
@@ -1257,6 +1261,10 @@ msgstr "Liste d'écrans"
msgid "All incoming events"
msgstr "Tous les événements à venir"
#: com/views.py
msgid "Weekmail sent successfully"
msgstr "Weekmail envoyé avec succès"
#: com/views.py
msgid "Delete and save to regenerate"
msgstr "Supprimer et sauver pour régénérer"
@@ -1265,6 +1273,26 @@ msgstr "Supprimer et sauver pour régénérer"
msgid "Weekmail of the "
msgstr "Weekmail du "
#: com/views.py
#, python-format
msgid "%(title)s moved up in the Weekmail"
msgstr "%(title)s monté dans le Weekmail"
#: com/views.py
#, python-format
msgid "%(title)s moved down in the Weekmail"
msgstr "%(title)s descendu dans le Weekmail"
#: com/views.py
#, python-format
msgid "%(title)s added to the Weekmail"
msgstr "%(title)s ajouté dans Weekmail"
#: com/views.py
#, python-format
msgid "%(title)s removed from the Weekmail"
msgstr "%(title)s retiré du Weekmail"
#: com/views.py
msgid ""
"You must be a board member of the selected club to post in the Weekmail."
@@ -2340,6 +2368,10 @@ msgstr "Etickets"
msgid "User has no account"
msgstr "L'utilisateur n'a pas de compte"
#: core/templates/core/user_account_detail.jinja
msgid "Deleted user"
msgstr "Utilisateur supprimé"
#: core/templates/core/user_account_detail.jinja
#: counter/templates/counter/last_ops.jinja
#: counter/templates/counter/refilling_list.jinja
@@ -4540,22 +4572,6 @@ msgstr "Signaler ce commentaire"
msgid "Edit UE"
msgstr "Éditer l'UE"
#: pedagogy/templates/pedagogy/uv_edit.jinja
msgid "Import from UTBM"
msgstr "Importer depuis l'UTBM"
#: pedagogy/templates/pedagogy/uv_edit.jinja
msgid "Unknown UE code"
msgstr "Code d'UE inconnu"
#: pedagogy/templates/pedagogy/uv_edit.jinja
msgid "Successful autocomplete"
msgstr "Autocomplétion réussite"
#: pedagogy/templates/pedagogy/uv_edit.jinja
msgid "An error occurred: "
msgstr "Une erreur est survenue : "
#: rootplace/forms.py
msgid "User that will be kept"
msgstr "Utilisateur qui sera conservé"
@@ -4819,8 +4835,8 @@ msgid "N/A"
msgstr "N/A"
#: sith/settings.py
msgid "Transfert"
msgstr "Virement"
msgid "AE account"
msgstr "Compte AE"
#: sith/settings.py
msgid "Belfort"
@@ -5108,26 +5124,6 @@ msgstr "Vous avez acheté %s"
msgid "You have a notification"
msgstr "Vous avez une notification"
#: sith/settings.py
msgid "Success!"
msgstr "Succès !"
#: sith/settings.py
msgid "Fail!"
msgstr "Échec !"
#: sith/settings.py
msgid "You successfully posted an article in the Weekmail"
msgstr "Article posté avec succès dans le Weekmail"
#: sith/settings.py
msgid "You successfully edited an article in the Weekmail"
msgstr "Article édité avec succès dans le Weekmail"
#: sith/settings.py
msgid "You successfully sent the Weekmail"
msgstr "Weekmail envoyé avec succès"
#: sith/settings.py
msgid "AE tee-shirt"
msgstr "Tee-shirt AE"
@@ -5168,6 +5164,14 @@ msgstr "lieu"
msgid "You can not subscribe many time for the same period"
msgstr "Vous ne pouvez pas cotiser plusieurs fois pour la même période"
#: subscription/templates/subscription/forms/create_existing_user.jinja
msgid ""
"If the subscription is done using the AE account, you must also click it on "
"the AE counter."
msgstr ""
"Si la cotisation est faite en utilisant le compte AE, vous devez également "
"la cliquer sur le comptoir AE."
#: subscription/templates/subscription/fragments/creation_success.jinja
#, python-format
msgid "Subscription created for %(user)s"
@@ -5431,10 +5435,38 @@ msgstr "Mes photos"
msgid "Admin tools"
msgstr "Admin Trombi"
#: trombi/views.py
msgid "Trombi modified"
msgstr "Trombi modifié"
#: trombi/views.py
msgid "User added to the trombi"
msgstr "Utilisateur ajouté au trombi"
#: trombi/views.py
msgid "User couldn't be added to the trombi"
msgstr "L'utilisateur n'a pas pu être ajouté au trombi"
#: trombi/views.py
msgid "User removed from the trombi"
msgstr "Utilisateur retiré du trombi"
#: trombi/views.py
msgid "Explain why you rejected the comment"
msgstr "Expliquez pourquoi vous refusez le commentaire"
#: trombi/views.py
msgid "Comment accepted"
msgstr "Commentaire accepté"
#: trombi/views.py
msgid "Comment rejected"
msgstr "Commentaire rejeté"
#: trombi/views.py
msgid "Comment removed"
msgstr "Commentaire retiré"
#: trombi/views.py
msgid "Rejected comment"
msgstr "Commentaire rejeté"
@@ -5475,6 +5507,10 @@ msgstr ""
"pouvez vous inscrire qu'à un seul Trombi, donc ne jouez pas avec cet option "
"ou vous encourerez la colère des admins!"
#: trombi/views.py
msgid "User modified"
msgstr "Utilisateur modifié"
#: trombi/views.py
msgid "Personal email (not UTBM)"
msgstr "Email personnel (pas UTBM)"
@@ -5487,6 +5523,14 @@ msgstr "Téléphone"
msgid "Native town"
msgstr "Ville d'origine"
#: trombi/views.py
msgid "User removed from trombi"
msgstr "Utilisateur retiré du trombi"
#: trombi/views.py
msgid "Comment added"
msgstr "Commentaire ajouté"
#: trombi/views.py
msgid ""
"You can not yet write comment, you must wait for the subscription deadline "
@@ -5502,4 +5546,4 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée."
#: trombi/views.py
#, python-format
msgid "Maximum characters: %(max_length)s"
msgstr "Nombre de caractères max: %(max_length)s"
msgstr "Nombre de caractères max: %(max_length)s"

25
package-lock.json generated
View File

@@ -30,7 +30,6 @@
"easymde": "^2.19.0",
"glob": "^11.0.0",
"htmx.org": "^2.0.3",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",
"lit-html": "^3.3.0",
"native-file-system-adapter": "^3.0.1",
@@ -47,7 +46,6 @@
"@types/alpinejs": "^3.13.10",
"@types/cytoscape-cxtmenu": "^3.4.4",
"@types/cytoscape-klay": "^3.1.4",
"@types/jquery": "^3.5.31",
"@types/js-cookie": "^3.0.6",
"typescript": "^5.8.3",
"vite": "^6.3.6",
@@ -2889,16 +2887,6 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/jquery": {
"version": "3.5.33",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.33.tgz",
"integrity": "sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/js-cookie": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
@@ -2919,13 +2907,6 @@
"integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==",
"license": "MIT"
},
"node_modules/@types/sizzle": {
"version": "2.3.10",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz",
"integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
@@ -4384,12 +4365,6 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/jquery": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"license": "MIT"
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",

View File

@@ -32,7 +32,6 @@
"@types/alpinejs": "^3.13.10",
"@types/cytoscape-cxtmenu": "^3.4.4",
"@types/cytoscape-klay": "^3.1.4",
"@types/jquery": "^3.5.31",
"@types/js-cookie": "^3.0.6",
"typescript": "^5.8.3",
"vite": "^6.3.6",
@@ -61,7 +60,6 @@
"easymde": "^2.19.0",
"glob": "^11.0.0",
"htmx.org": "^2.0.3",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",
"lit-html": "^3.3.0",
"native-file-system-adapter": "^3.0.1",

View File

@@ -13,16 +13,15 @@
{% block content %}
<div class="pedagogy">
<div id="uv_detail">
<p id="return_noscript"><a href="{{ url('pedagogy:guide') }}">{% trans %}Back{% endtrans %}</a></p>
<button id="return_js" onclick='(function(){
// If comes from the guide page, go back with history
if (document.referrer.replace(/\?(.+)/gm,"").endsWith(`{{ url("pedagogy:guide") }}`)){
window.history.back();
return;
}
// Simply goes to the guide page
window.location.href = `{{ url("pedagogy:guide") }}`;
})()' hidden>{% trans %}Back{% endtrans %}</button>
<button onclick='(function(){
// If comes from the guide page, go back with history
if (document.referrer.replace(/\?(.+)/gm,"").endsWith(`{{ url("pedagogy:guide") }}`)){
window.history.back();
return;
}
// Simply goes to the guide page
window.location.href = `{{ url("pedagogy:guide") }}`;
})()' hidden>{% trans %}Back{% endtrans %}</button>
<h1>{{ object.code }} - {{ object.title }}</h1>
<br>
@@ -217,9 +216,4 @@
</div>
</div>
<script type="text/javascript">
$("#return_noscript").hide();
$("#return_js").show();
</script>
{% endblock %}

View File

@@ -21,11 +21,6 @@
{{ field.errors }}
<label for="{{ field.name }}">{{ field.label }}</label>
{{ field }}
{% if field.name == 'code' %}
<button type="button" id="autofill">{% trans %}Import from UTBM{% endtrans %}</button>
{% endif %}
</p>
{% endif %}
@@ -36,48 +31,3 @@
<p><input type="submit" value="{% trans %}Update{% endtrans %}" /></p>
</form>
{% endblock %}
{% block script %}
{{ super() }}
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
const autofillBtn = document.getElementById('autofill')
const codeInput = document.querySelector('input[name="code"]')
autofillBtn.addEventListener('click', () => {
const url = `/api/uv/${codeInput.value}`;
deleteQuickNotifs()
$.ajax({
dataType: "json",
url: url,
success: function(data, _, xhr) {
if (xhr.status !== 200) {
createQuickNotif("{% trans %}Unknown UE code{% endtrans %}")
return
}
Object.entries(data)
.filter(([_, val]) => !!val) // skip entries with null or undefined value
.map(([key, val]) => { // convert keys to DOM elements
return [document.querySelector('[name="' + key + '"]'), val];
})
.filter(([elem, _]) => !!elem) // skip non-existing DOM elements
.forEach(([elem, val]) => { // write the value in the form field
if (elem.tagName === 'TEXTAREA') {
// MD editor text input
elem.parentNode.querySelector('.CodeMirror').CodeMirror.setValue(val);
} else {
elem.value = val;
}
});
createQuickNotif('{% trans %}Successful autocomplete{% endtrans %}')
},
error: function(_, _, statusMessage) {
createQuickNotif('{% trans %}An error occurred: {% endtrans %}' + statusMessage)
},
})
})
})
</script>
{% endblock %}

View File

@@ -309,6 +309,7 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
// Clear selection and cache of retrieved user so they can be filtered again
widget.clear(false);
widget.clearOptions();
widget.setTextboxValue("");
},
/**

View File

@@ -421,18 +421,11 @@ SITH_PROFILE_DEPARTMENTS = [
("NA", _("N/A")),
]
SITH_ACCOUNTING_PAYMENT_METHOD = [
("CHECK", _("Check")),
("CASH", _("Cash")),
("TRANSFERT", _("Transfert")),
("CARD", _("Credit card")),
]
SITH_SUBSCRIPTION_PAYMENT_METHOD = [
("CHECK", _("Check")),
("CARD", _("Credit card")),
("CASH", _("Cash")),
("EBOUTIC", _("Eboutic")),
("AE_ACCOUNT", _("AE account")),
("OTHER", _("Other")),
]
@@ -441,6 +434,7 @@ SITH_SUBSCRIPTION_LOCATIONS = [
("SEVENANS", _("Sevenans")),
("MONTBELIARD", _("Montbéliard")),
("EBOUTIC", _("Eboutic")),
("OTHER", _("Other")),
]
SITH_COUNTER_BARS = [(1, "MDE"), (2, "Foyer"), (35, "La Gommette")]
@@ -691,14 +685,6 @@ SITH_PERMANENT_NOTIFICATIONS = {
"SAS_MODERATION": "sas.models.sas_notification_callback",
}
SITH_QUICK_NOTIF = {
"qn_success": _("Success!"),
"qn_fail": _("Fail!"),
"qn_weekmail_new_article": _("You successfully posted an article in the Weekmail"),
"qn_weekmail_article_edit": _("You successfully edited an article in the Weekmail"),
"qn_weekmail_send_success": _("You successfully sent the Weekmail"),
}
# Mailing related settings
SITH_MAILING_DOMAIN = "utbm.fr"

View File

@@ -2,6 +2,7 @@ import secrets
from typing import Any
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
@@ -23,6 +24,13 @@ class SelectionDateForm(forms.Form):
class SubscriptionForm(forms.ModelForm):
allowed_payment_methods = ["CARD", "CASH", "AE_ACCOUNT"]
class Meta:
model = Subscription
fields = ["subscription_type", "payment_method", "location"]
widgets = {"payment_method": forms.RadioSelect}
def __init__(self, *args, initial=None, **kwargs):
initial = initial or {}
if "subscription_type" not in initial:
@@ -30,6 +38,14 @@ class SubscriptionForm(forms.ModelForm):
if "payment_method" not in initial:
initial["payment_method"] = "CARD"
super().__init__(*args, initial=initial, **kwargs)
self.fields["payment_method"].choices = [
m
for m in settings.SITH_SUBSCRIPTION_PAYMENT_METHOD
if m[0] in self.allowed_payment_methods
]
self.fields["location"].choices = [
m for m in settings.SITH_SUBSCRIPTION_LOCATIONS if m[0] != "EBOUTIC"
]
def save(self, *args, **kwargs):
if self.errors:
@@ -61,7 +77,8 @@ class SubscriptionNewUserForm(SubscriptionForm):
assert user.is_subscribed
"""
template_name = "subscription/forms/create_new_user.html"
allowed_payment_methods = ["CARD", "CASH"]
template_name = "subscription/forms/create_new_user.jinja"
__user_fields = forms.fields_for_model(
User,
@@ -73,10 +90,6 @@ class SubscriptionNewUserForm(SubscriptionForm):
email = __user_fields["email"]
date_of_birth = __user_fields["date_of_birth"]
class Meta:
model = Subscription
fields = ["subscription_type", "payment_method", "location"]
field_order = [
"first_name",
"last_name",
@@ -130,7 +143,7 @@ class SubscriptionNewUserForm(SubscriptionForm):
class SubscriptionExistingUserForm(SubscriptionForm):
"""Form to add a subscription to an existing user."""
template_name = "subscription/forms/create_existing_user.html"
template_name = "subscription/forms/create_existing_user.jinja"
required_css_class = "required"
birthdate = forms.fields_for_model(
@@ -140,10 +153,9 @@ class SubscriptionExistingUserForm(SubscriptionForm):
help_texts={"date_of_birth": _("This user didn't fill its birthdate yet.")},
)["date_of_birth"]
class Meta:
model = Subscription
fields = ["member", "subscription_type", "payment_method", "location"]
widgets = {"member": AutoCompleteSelectUser}
class Meta(SubscriptionForm.Meta):
fields = ["member", *SubscriptionForm.Meta.fields]
widgets = SubscriptionForm.Meta.widgets | {"member": AutoCompleteSelectUser}
field_order = [
"member",

View File

@@ -0,0 +1,56 @@
# Generated by Django 5.2.3 on 2025-09-08 05:38
from django.db import migrations, models
from django.db.migrations.state import StateApps
def rename_enums(apps: StateApps, schema_editor):
Subscription = apps.get_model("subscription", "Subscription")
Subscription.objects.filter(subscription_type="EBOUTIC").update(
subscription_type="AE_ACCOUNT"
)
def rename_enums_reverse(apps: StateApps, schema_editor):
Subscription = apps.get_model("subscription", "Subscription")
Subscription.objects.filter(subscription_type="AE_ACCOUNT").update(
subscription_type="EBOUTIC"
)
class Migration(migrations.Migration):
dependencies = [("subscription", "0014_auto_20201207_2323")]
operations = [
migrations.AlterField(
model_name="subscription",
name="location",
field=models.CharField(
choices=[
("BELFORT", "Belfort"),
("SEVENANS", "Sevenans"),
("MONTBELIARD", "Montbéliard"),
("EBOUTIC", "Eboutic"),
("OTHER", "Other"),
],
max_length=20,
verbose_name="location",
),
),
migrations.AlterField(
model_name="subscription",
name="payment_method",
field=models.CharField(
choices=[
("CHECK", "Check"),
("CARD", "Credit card"),
("CASH", "Cash"),
("AE_ACCOUNT", "AE account"),
("OTHER", "Other"),
],
max_length=255,
verbose_name="payment method",
),
),
migrations.RunPython(rename_enums, reverse_code=rename_enums_reverse),
]

View File

@@ -1,14 +0,0 @@
{% load static %}
{% load i18n %}
<div x-data="existing_user_subscription_form" class="form-content existing-user">
<fieldset>
{{ form.as_p }}
</fieldset>
<div
id="subscription-form-user-mini-profile"
x-html="profileFragment"
:aria-busy="loading"
></div>
</div>

View File

@@ -0,0 +1,28 @@
{% load static %}
{% load i18n %}
<div x-data="existing_user_subscription_form" class="form-content existing-user">
<fieldset>
{{ errors }}
{% for field, errors in fields %}
<p{% with classes=field.css_classes %}{% if classes %} class="{{ classes }}"{% endif %}{% endwith %}>
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}
<span class="helptext">{{ field.help_text }}</span>
{% endif %}
</p>
{% if field.name == "payment_method" %}
<i>
{% blocktranslate %}If the subscription is done using the AE account, you must also click it on the AE counter.{% endblocktranslate %}
</i>
{% endif %}
{% endfor %}
</fieldset>
<div
id="subscription-form-user-mini-profile"
x-html="profileFragment"
:aria-busy="loading"
></div>
</div>

View File

@@ -90,7 +90,7 @@ class TestSubscriptionIntegration(TestCase):
s = Subscription(
member=self.user,
subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[3],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1],
)
s.subscription_start = date(2017, 8, 29)
s.subscription_end = s.compute_end(duration=0.166, start=s.subscription_start)
@@ -101,7 +101,7 @@ class TestSubscriptionIntegration(TestCase):
s = Subscription(
member=self.user,
subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[3],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1],
)
s.subscription_start = date(2017, 8, 29)
s.subscription_end = s.compute_end(duration=0.333, start=s.subscription_start)
@@ -112,7 +112,7 @@ class TestSubscriptionIntegration(TestCase):
s = Subscription(
member=self.user,
subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[3],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1],
)
s.subscription_start = date(2017, 8, 29)
s.subscription_end = s.compute_end(
@@ -126,7 +126,7 @@ class TestSubscriptionIntegration(TestCase):
s = Subscription(
member=self.user,
subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[3],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1],
)
s.subscription_start = date(2017, 8, 29)
s.subscription_end = s.compute_end(duration=0.5, start=s.subscription_start)
@@ -137,7 +137,7 @@ class TestSubscriptionIntegration(TestCase):
s = Subscription(
member=self.user,
subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[3],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1],
)
s.subscription_start = date(2017, 8, 29)
s.subscription_end = s.compute_end(duration=0.67, start=s.subscription_start)
@@ -148,7 +148,7 @@ class TestSubscriptionIntegration(TestCase):
s = Subscription(
member=self.user,
subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[3],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1],
)
s.subscription_start = date(2018, 9, 1)
s.subscription_end = s.compute_end(duration=0.23, start=s.subscription_start)
@@ -160,7 +160,7 @@ class TestSubscriptionIntegration(TestCase):
s = Subscription(
member=user,
subscription_type="deux-semestres",
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1],
)
s.subscription_start = date(2015, 8, 29)
s.subscription_end = s.compute_end(
@@ -181,7 +181,7 @@ class TestSubscriptionIntegration(TestCase):
s = Subscription(
member=user,
subscription_type="deux-mois-essai",
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1],
)
s.subscription_start = date(2015, 8, 29)
s.subscription_end = s.compute_end(
@@ -202,7 +202,7 @@ class TestSubscriptionIntegration(TestCase):
s = Subscription(
member=user,
subscription_type="deux-mois-essai",
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1],
)
s.subscription_start = date(2015, 8, 29)
s.subscription_end = s.compute_end(

View File

@@ -38,7 +38,7 @@ def test_form_existing_user_valid(
"birthdate": user.date_of_birth,
"subscription_type": "deux-semestres",
"location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1][0],
}
form = SubscriptionExistingUserForm(data)
assert form.is_valid()
@@ -55,7 +55,7 @@ def test_form_existing_user_with_birthdate(settings: SettingsWrapper):
"member": user,
"subscription_type": "deux-semestres",
"location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1][0],
}
form = SubscriptionExistingUserForm(data)
assert not form.is_valid()
@@ -81,7 +81,7 @@ def test_form_existing_user_invalid(settings: SettingsWrapper):
"member": user,
"subscription_type": "deux-semestres",
"location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1][0],
}
form = SubscriptionExistingUserForm(data)
@@ -99,7 +99,7 @@ def test_form_new_user(settings: SettingsWrapper):
"date_of_birth": localdate() - relativedelta(years=18),
"subscription_type": "deux-semestres",
"location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1][0],
}
form = SubscriptionNewUserForm(data)
assert form.is_valid()
@@ -130,7 +130,7 @@ def test_form_set_new_user_as_student(settings: SettingsWrapper, subscription_ty
"date_of_birth": localdate() - relativedelta(years=18),
"subscription_type": subscription_type,
"location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1][0],
}
form = SubscriptionNewUserForm(data)
assert form.is_valid()
@@ -180,7 +180,7 @@ def test_submit_form_existing_user(client: Client, settings: SettingsWrapper):
"birthdate": user.date_of_birth,
"subscription_type": "deux-semestres",
"location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1][0],
},
)
user.refresh_from_db()
@@ -212,7 +212,7 @@ def test_submit_form_new_user(client: Client, settings: SettingsWrapper):
"date_of_birth": localdate() - relativedelta(years=18),
"subscription_type": "deux-semestres",
"location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[1][0],
},
)
user = User.objects.get(email="jdoe@utbm.fr")

View File

@@ -26,7 +26,9 @@ from datetime import date
from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError
from django.forms.models import modelform_factory
@@ -46,7 +48,7 @@ from core.auth.mixins import (
)
from core.models import User
from core.views.forms import SelectDate
from core.views.mixins import QuickNotifMixin, TabedViewMixin
from core.views.mixins import TabedViewMixin
from core.views.widgets.ajax_select import AutoCompleteSelectUser
from trombi.models import Trombi, TrombiClubMembership, TrombiComment, TrombiUser
@@ -134,15 +136,15 @@ class TrombiCreateView(CanCreateMixin, CreateView):
return self.form_invalid(form)
class TrombiEditView(CanEditPropMixin, TrombiTabsMixin, UpdateView):
class TrombiEditView(
CanEditPropMixin, TrombiTabsMixin, SuccessMessageMixin, UpdateView
):
model = Trombi
form_class = TrombiForm
template_name = "core/edit.jinja"
pk_url_kwarg = "trombi_id"
current_tab = "admin_tools"
def get_success_url(self):
return super().get_success_url() + "?qn_success"
success_message = _("Trombi modified")
class AddUserForm(forms.Form):
@@ -155,7 +157,7 @@ class AddUserForm(forms.Form):
)
class TrombiDetailView(CanEditMixin, QuickNotifMixin, TrombiTabsMixin, DetailView):
class TrombiDetailView(CanEditMixin, TrombiTabsMixin, DetailView):
model = Trombi
template_name = "trombi/detail.jinja"
pk_url_kwarg = "trombi_id"
@@ -167,9 +169,9 @@ class TrombiDetailView(CanEditMixin, QuickNotifMixin, TrombiTabsMixin, DetailVie
if form.is_valid():
try:
TrombiUser(user=form.cleaned_data["user"], trombi=self.object).save()
self.quick_notif_list.append("qn_success")
messages.success(self.request, _("User added to the trombi"))
except IntegrityError: # We don't care about duplicate keys
self.quick_notif_list.append("qn_fail")
messages.error(self.request, _("User couldn't be added to the trombi"))
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
@@ -185,22 +187,20 @@ class TrombiExportView(CanEditMixin, TrombiTabsMixin, DetailView):
current_tab = "admin_tools"
class TrombiDeleteUserView(CanEditPropMixin, TrombiTabsMixin, DeleteView):
class TrombiDeleteUserView(
CanEditPropMixin, TrombiTabsMixin, SuccessMessageMixin, DeleteView
):
model = TrombiUser
pk_url_kwarg = "user_id"
template_name = "core/delete_confirm.jinja"
current_tab = "admin_tools"
success_message = _("User removed from the trombi")
def get_success_url(self):
return (
reverse("trombi:detail", kwargs={"trombi_id": self.object.trombi.id})
+ "?qn_success"
)
return reverse("trombi:detail", kwargs={"trombi_id": self.object.trombi.id})
class TrombiModerateCommentsView(
CanEditPropMixin, QuickNotifMixin, TrombiTabsMixin, DetailView
):
class TrombiModerateCommentsView(CanEditPropMixin, TrombiTabsMixin, DetailView):
model = Trombi
template_name = "trombi/comment_moderation.jinja"
pk_url_kwarg = "trombi_id"
@@ -235,16 +235,18 @@ class TrombiModerateCommentView(DetailView):
if request.POST["action"] == "accept":
self.object.is_moderated = True
self.object.save()
messages.success(self.request, _("Comment accepted"))
return redirect(
reverse(
"trombi:moderate_comments",
kwargs={"trombi_id": self.object.author.trombi.id},
)
+ "?qn_success"
)
elif request.POST["action"] == "reject":
messages.success(self.request, _("Comment rejected"))
return super().get(request, *args, **kwargs)
elif request.POST["action"] == "delete" and "reason" in request.POST:
messages.success(self.request, _("Comment removed"))
self.object.author.user.email_user(
subject="[%s] %s" % (settings.SITH_NAME, _("Rejected comment")),
message=_(
@@ -265,7 +267,6 @@ class TrombiModerateCommentView(DetailView):
"trombi:moderate_comments",
kwargs={"trombi_id": self.object.author.trombi.id},
)
+ "?qn_success"
)
raise Http404
@@ -299,9 +300,7 @@ class UserTrombiForm(forms.Form):
)
class UserTrombiToolsView(
LoginRequiredMixin, QuickNotifMixin, TrombiTabsMixin, TemplateView
):
class UserTrombiToolsView(LoginRequiredMixin, TrombiTabsMixin, TemplateView):
"""Display a user's trombi tools."""
template_name = "trombi/user_tools.jinja"
@@ -318,7 +317,6 @@ class UserTrombiToolsView(
user=request.user, trombi=self.form.cleaned_data["trombi"]
)
trombi_user.save()
self.quick_notif_list += ["qn_success"]
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
@@ -335,21 +333,24 @@ class UserTrombiToolsView(
return kwargs
class UserTrombiEditPicturesView(TrombiTabsMixin, UserIsInATrombiMixin, UpdateView):
class UserTrombiEditPicturesView(
TrombiTabsMixin, UserIsInATrombiMixin, SuccessMessageMixin, UpdateView
):
model = TrombiUser
fields = ["profile_pict", "scrub_pict"]
template_name = "core/edit.jinja"
current_tab = "pictures"
success_message = _("User modified")
def get_object(self):
return self.request.user.trombi_user
def get_success_url(self):
return reverse("trombi:user_tools") + "?qn_success"
return reverse("trombi:user_tools")
class UserTrombiEditProfileView(
QuickNotifMixin, TrombiTabsMixin, UserIsInATrombiMixin, UpdateView
TrombiTabsMixin, UserIsInATrombiMixin, SuccessMessageMixin, UpdateView
):
model = User
form_class = modelform_factory(
@@ -370,16 +371,20 @@ class UserTrombiEditProfileView(
)
template_name = "trombi/edit_profile.jinja"
current_tab = "profile"
success_message = _("User modified")
def get_object(self):
return self.request.user
def get_success_url(self):
return reverse("trombi:user_tools") + "?qn_success"
return reverse("trombi:user_tools")
class UserTrombiResetClubMembershipsView(UserIsInATrombiMixin, RedirectView):
class UserTrombiResetClubMembershipsView(
UserIsInATrombiMixin, SuccessMessageMixin, RedirectView
):
permanent = False
success_message = _("User modified")
def get(self, request, *args, **kwargs):
user = self.request.user.trombi_user
@@ -387,18 +392,18 @@ class UserTrombiResetClubMembershipsView(UserIsInATrombiMixin, RedirectView):
return redirect(self.get_success_url())
def get_success_url(self):
return reverse("trombi:profile") + "?qn_success"
return reverse("trombi:profile")
class UserTrombiDeleteMembershipView(TrombiTabsMixin, CanEditMixin, DeleteView):
class UserTrombiDeleteMembershipView(
TrombiTabsMixin, CanEditMixin, SuccessMessageMixin, DeleteView
):
model = TrombiClubMembership
pk_url_kwarg = "membership_id"
template_name = "core/delete_confirm.jinja"
success_url = reverse_lazy("trombi:profile")
current_tab = "profile"
def get_success_url(self):
return super().get_success_url() + "?qn_success"
success_message = _("User removed from trombi")
# Used by admins when someone does not have every club in his list
@@ -428,15 +433,18 @@ class UserTrombiAddMembershipView(TrombiTabsMixin, CreateView):
)
class UserTrombiEditMembershipView(CanEditMixin, TrombiTabsMixin, UpdateView):
class UserTrombiEditMembershipView(
CanEditMixin, TrombiTabsMixin, SuccessMessageMixin, UpdateView
):
model = TrombiClubMembership
pk_url_kwarg = "membership_id"
fields = ["role", "start", "end"]
template_name = "core/edit.jinja"
current_tab = "profile"
success_message = _("User modified")
def get_success_url(self):
return super().get_success_url() + "?qn_success"
return super().get_success_url()
class UserTrombiProfileView(TrombiTabsMixin, DetailView):
@@ -461,12 +469,13 @@ class UserTrombiProfileView(TrombiTabsMixin, DetailView):
return super().get(request, *args, **kwargs)
class TrombiCommentFormView(LoginRequiredMixin, View):
class TrombiCommentFormView(LoginRequiredMixin, SuccessMessageMixin, View):
"""Create/edit a trombi comment."""
model = TrombiComment
fields = ["content"]
template_name = "trombi/comment.jinja"
success_message = _("Comment added")
def get_form_class(self):
self.trombi = self.request.user.trombi_user.trombi
@@ -496,7 +505,7 @@ class TrombiCommentFormView(LoginRequiredMixin, View):
)
def get_success_url(self):
return reverse("trombi:user_tools") + "?qn_success"
return reverse("trombi:user_tools")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)

View File

@@ -11,7 +11,7 @@
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"types": ["jquery", "alpinejs"],
"types": ["alpinejs"],
"paths": {
"#openapi": ["./staticfiles/generated/openapi/client/index.ts"],
"#openapi:*": ["./staticfiles/generated/openapi/client/*"],

View File

@@ -4,7 +4,6 @@ import inject from "@rollup/plugin-inject";
import { glob } from "glob";
import { type AliasOptions, type UserConfig, defineConfig } from "vite";
import type { Rollup } from "vite";
import { viteStaticCopy } from "vite-plugin-static-copy";
import tsconfig from "./tsconfig.json";
const outDir = resolve(__dirname, "./staticfiles/generated/bundled");
@@ -87,17 +86,6 @@ export default defineConfig((config: UserConfig) => {
Alpine: "alpinejs",
htmx: "htmx.org",
}),
viteStaticCopy({
targets: [
{
src: resolve(nodeModules, "jquery/dist/jquery.min.js"),
dest: vendored,
},
],
}),
],
optimizeDeps: {
include: ["jquery"],
},
} satisfies UserConfig;
});