rework news form

This commit is contained in:
imperosol 2025-01-10 00:45:25 +01:00
parent 600657b1a8
commit c3fc8538cc
15 changed files with 646 additions and 523 deletions

View File

@ -13,17 +13,25 @@
#
#
from django.contrib import admin
from django.contrib.admin import TabularInline
from haystack.admin import SearchModelAdmin
from com.models import News, Poster, Screen, Sith, Weekmail
from com.models import News, NewsDate, Poster, Screen, Sith, Weekmail
class NewsDateInline(TabularInline):
model = NewsDate
extra = 0
@admin.register(News)
class NewsAdmin(SearchModelAdmin):
list_display = ("title", "type", "club", "author")
list_display = ("title", "club", "author")
search_fields = ("title", "summary", "content")
autocomplete_fields = ("author", "moderator")
inlines = [NewsDateInline]
@admin.register(Poster)
class PosterAdmin(SearchModelAdmin):

View File

@ -1,12 +1,17 @@
from datetime import timedelta
from datetime import date
from dateutil.relativedelta import relativedelta
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Exists, OuterRef
from django.forms import CheckboxInput
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from club.models import Club
from club.widgets.select import AutoCompleteSelectClub
from com.models import News, NewsDate, Poster
from core.models import User
from core.utils import get_end_of_semester
from core.views.forms import SelectDateTime
from core.views.widgets.markdown import MarkdownInput
@ -46,66 +51,143 @@ class PosterForm(forms.ModelForm):
self.fields.pop("display_time")
class NewsDateForm(forms.ModelForm):
"""Form to select the dates of an event."""
required_css_class = "required"
class Meta:
model = NewsDate
fields = ["start_date", "end_date"]
widgets = {"start_date": SelectDateTime, "end_date": SelectDateTime}
is_weekly = forms.BooleanField(
label=_("Weekly event"),
help_text=_("Weekly events will occur each week for a specified timespan."),
widget=CheckboxInput(attrs={"class": "switch"}),
initial=False,
required=False,
)
occurrence_choices = [
*[(str(i), _("%d times") % i) for i in range(2, 7)],
("SEMESTER_END", _("Until the end of the semester")),
]
occurrences = forms.ChoiceField(
label=_("Occurrences"),
help_text=_("How much times should the event occur (including the first one)"),
choices=occurrence_choices,
initial="SEMESTER_END",
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.label_suffix = ""
@classmethod
def get_occurrences(cls, number: int) -> tuple[str, str] | None:
"""Find the occurrence choice corresponding to numeric number of occurrences."""
if number < 2:
# If only 0 or 1 date, there cannot be weekly events
return None
# occurrences have all a numeric value, except "SEMESTER_END"
str_num = str(number)
occurrences = next((c for c in cls.occurrence_choices if c[0] == str_num), None)
if occurrences:
return occurrences
return next((c for c in cls.occurrence_choices if c[0] == "SEMESTER_END"), None)
def save(self, commit: bool = True, *, news: News): # noqa FBT001
# the base save method contains some checks we want to run
# before doing our own logic
super().save(commit=False)
# delete existing dates before creating new ones
news.dates.all().delete()
if not self.cleaned_data.get("is_weekly"):
self.instance.news = news
return super().save(commit=commit)
dates: list[NewsDate] = [self.instance]
occurrences = self.cleaned_data.get("occurrences")
start = self.instance.start_date
end = self.instance.end_date
if occurrences[0].isdigit():
nb_occurrences = int(occurrences[0])
else: # to the end of the semester
start_date = date(start.year, start.month, start.day)
nb_occurrences = (get_end_of_semester(start_date) - start_date).days // 7
dates.extend(
[
NewsDate(
start_date=start + relativedelta(weeks=i),
end_date=end + relativedelta(weeks=i),
)
for i in range(1, nb_occurrences)
]
)
for d in dates:
d.news = news
if not commit:
return dates
return NewsDate.objects.bulk_create(dates)
class NewsForm(forms.ModelForm):
"""Form to create or edit news."""
error_css_class = "error"
required_css_class = "required"
class Meta:
model = News
fields = ["type", "title", "club", "summary", "content", "author"]
fields = ["title", "club", "summary", "content"]
widgets = {
"author": forms.HiddenInput,
"summary": MarkdownInput,
"content": MarkdownInput,
}
start_date = forms.DateTimeField(
label=_("Start date"), widget=SelectDateTime, required=False
auto_moderate = forms.BooleanField(
label=_("Automoderation"),
widget=CheckboxInput(attrs={"class": "switch"}),
required=False,
)
end_date = forms.DateTimeField(
label=_("End date"), widget=SelectDateTime, required=False
)
until = forms.DateTimeField(label=_("Until"), widget=SelectDateTime, required=False)
automoderation = forms.BooleanField(label=_("Automoderation"), required=False)
def __init__(self, *args, author: User, date_form: NewsDateForm, **kwargs):
super().__init__(*args, **kwargs)
self.author = author
self.date_form = date_form
self.label_suffix = ""
# if the author is an admin, he/she can choose any club,
# otherwise, only clubs for which he/she is a board member can be selected
if author.is_root or author.is_com_admin:
self.fields["club"] = forms.ModelChoiceField(
queryset=Club.objects.all(), widget=AutoCompleteSelectClub
)
else:
active_memberships = author.memberships.board().ongoing()
self.fields["club"] = forms.ModelChoiceField(
queryset=Club.objects.filter(
Exists(active_memberships.filter(club=OuterRef("pk")))
)
)
def clean(self):
self.cleaned_data = super().clean()
if self.cleaned_data["type"] != "NOTICE":
if not self.cleaned_data["start_date"]:
self.add_error(
"start_date", ValidationError(_("This field is required."))
)
if not self.cleaned_data["end_date"]:
self.add_error(
"end_date", ValidationError(_("This field is required."))
)
if (
not self.has_error("start_date")
and not self.has_error("end_date")
and self.cleaned_data["start_date"] > self.cleaned_data["end_date"]
):
self.add_error(
"end_date",
ValidationError(_("An event cannot end before its beginning.")),
)
if self.cleaned_data["type"] == "WEEKLY" and not self.cleaned_data["until"]:
self.add_error("until", ValidationError(_("This field is required.")))
return self.cleaned_data
def is_valid(self):
return super().is_valid() and self.date_form.is_valid()
def save(self, *args, **kwargs):
ret = super().save()
self.instance.dates.all().delete()
if self.instance.type == "EVENT" or self.instance.type == "CALL":
NewsDate(
start_date=self.cleaned_data["start_date"],
end_date=self.cleaned_data["end_date"],
news=self.instance,
).save()
elif self.instance.type == "WEEKLY":
start_date = self.cleaned_data["start_date"]
end_date = self.cleaned_data["end_date"]
while start_date <= self.cleaned_data["until"]:
NewsDate(
start_date=start_date, end_date=end_date, news=self.instance
).save()
start_date += timedelta(days=7)
end_date += timedelta(days=7)
return ret
def full_clean(self):
super().full_clean()
self.date_form.full_clean()
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.instance.is_moderated = True
self.instance.moderator = self.author
else:
self.instance.is_moderated = False
created_news = super().save(commit=commit)
self.date_form.save(commit=commit, news=created_news)
return created_news

View File

@ -58,6 +58,11 @@ class NewsQuerySet(models.QuerySet):
def moderated(self):
return self.filter(is_moderated=True)
def viewable_by(self, user: User):
if user.has_perm("com.view_unmoderated_news"):
return self
return self.moderated()
class News(models.Model):
"""News about club events."""
@ -132,11 +137,13 @@ class News(models.Model):
return False
return user.is_com_admin or user == self.author
def can_be_edited_by(self, user):
return user.is_com_admin
def can_be_edited_by(self, user: User):
return user.is_authenticated and (
self.author_id == user.id or user.has_perm("com.change_news")
)
def can_be_viewed_by(self, user):
return self.is_moderated or user.is_com_admin
return self.is_moderated or user.has_perm("com.view_unmoderated_news")
def news_notification_callback(notif):

View File

@ -25,10 +25,10 @@
</div>
<h4>{{ news.title }}</h4>
<p class="date">
<span>{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
<span>{{ date.start_date|localtime|date(DATETIME_FORMAT) }}
{{ date.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ date.end_date|localtime|date(DATETIME_FORMAT) }}
{{ date.end_date|localtime|time(DATETIME_FORMAT) }}</span>
</p>
<div class="news_content">
<div><em>{{ news.summary|markdown }}</em></div>

View File

@ -10,21 +10,6 @@
{% endblock %}
{% block content %}
{% if 'preview' in request.POST.keys() %}
<section class="news_event">
<h4>{{ form.instance.title }}</h4>
<p class="date">
<span>{{ form.instance.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
{{ form.instance.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ form.instance.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
{{ form.instance.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</p>
<p><a href="#">{{ form.instance.club or "Club" }}</a></p>
<div>{{ form.instance.summary|markdown }}</div>
<div>{{ form.instance.content|markdown }}</div>
<p>{% trans %}Author: {% endtrans %} {{ user_profile_link(form.instance.author) }}</p>
</section>
{% endif %}
{% if object %}
<h2>{% trans %}Edit news{% endtrans %}</h2>
{% else %}
@ -33,103 +18,73 @@
<form action="" method="post">
{% csrf_token %}
{{ form.non_field_errors() }}
{{ form.author }}
<p>
{{ form.type.errors }}
<label for="{{ form.type.name }}" class="required">{{ form.type.label }}</label>
<ul>
<li>{% trans %}Notice: Information, election result - no date{% endtrans %}</li>
<li>{% trans %}Event: punctual event, associated with one date{% endtrans %}</li>
<li>
{% trans trimmed%}
Weekly: recurrent event, associated with many dates
(specify the first one, and a deadline)
{% endtrans %}
</li>
<li>
{% trans trimmed %}
Call: long time event, associated with a long date (like election appliance)
{% endtrans %}
</li>
</ul>
{{ form.type }}
</p>
<p class="date">
{{ form.start_date.errors }}
<label for="{{ form.start_date.name }}">{{ form.start_date.label }}</label>
{{ form.start_date }}
</p>
<p class="date">
{{ form.end_date.errors }}
<label for="{{ form.end_date.name }}">{{ form.end_date.label }}</label>
{{ form.end_date }}
</p>
<p class="until">
{{ form.until.errors }}
<label for="{{ form.until.name }}">{{ form.until.label }}</label>
{{ form.until }}
</p>
<p>
<fieldset>
{{ form.title.errors }}
<label for="{{ form.title.name }}" class="required">{{ form.title.label }}</label>
{{ form.title.label_tag() }}
{{ form.title }}
</p>
<p>
</fieldset>
<fieldset>
{{ form.club.errors }}
<label for="{{ form.club.name }}" class="required">{{ form.club.label }}</label>
{{ form.club.label_tag() }}
<span class="helptext">{{ form.club.help_text }}</span>
{{ form.club }}
</p>
<p>
</fieldset>
{{ form.date_form.non_field_errors() }}
<div
class="row gap-2x"
x-data="{startDate: '{{ form.date_form.start_date.value() }}'}"
>
{# startDate is used to dynamically ensure end_date >= start_date,
whatever the value of start_date #}
<fieldset>
{{ form.date_form.start_date.errors }}
{{ form.date_form.start_date.label_tag() }}
<span class="helptext">{{ form.date_form.start_date.help_text }}</span>
{{ form.date_form.start_date|add_attr("x-model=startDate") }}
</fieldset>
<fieldset>
{{ form.date_form.end_date.errors }}
{{ form.date_form.end_date.label_tag() }}
<span class="helptext">{{ form.date_form.end_date.help_text }}</span>
{{ form.date_form.end_date|add_attr(":min=startDate") }}
</fieldset>
</div>
{# lower to convert True and False to true and false #}
<div x-data="{isWeekly: {{ form.date_form.is_weekly.value()|lower }}}">
<fieldset>
<div class="row gap">
{{ form.date_form.is_weekly|add_attr("x-model=isWeekly") }}
<div>
{{ form.date_form.is_weekly.label_tag() }}
<span class="helptext">{{ form.date_form.is_weekly.help_text }}</span>
</div>
</div>
</fieldset>
<fieldset x-show="isWeekly" x-transition x-cloak>
{{ form.date_form.occurrences.label_tag() }}
<span class="helptext">{{ form.date_form.occurrences.help_text }}</span>
{{ form.date_form.occurrences }}
</fieldset>
</div>
<fieldset>
{{ form.summary.errors }}
<label for="{{ form.summary.name }}" class="required">{{ form.summary.label }}</label>
{{ form.summary.label_tag() }}
<span class="helptext">{{ form.summary.help_text }}</span>
{{ form.summary }}
</p>
<p>
</fieldset>
<fieldset>
{{ form.content.errors }}
<label for="{{ form.content.name }}">{{ form.content.label }}</label>
{{ form.content.label_tag() }}
<span class="helptext">{{ form.content.help_text }}</span>
{{ form.content }}
</p>
{% if user.is_com_admin %}
<p>
{{ form.automoderation.errors }}
<label for="{{ form.automoderation.name }}">{{ form.automoderation.label }}</label>
{{ form.automoderation }}
</p>
</fieldset>
{% if user.is_root or user.is_com_admin %}
<fieldset>
{{ form.auto_moderate.errors }}
{{ form.auto_moderate }}
{{ form.auto_moderate.label_tag() }}
</fieldset>
{% endif %}
<p><input type="submit" name="preview" value="{% trans %}Preview{% endtrans %}"/></p>
<p><input type="submit" value="{% trans %}Save{% endtrans %}"/></p>
<p><input type="submit" value="{% trans %}Save{% endtrans %}" class="btn btn-blue"/></p>
</form>
{% endblock %}
{% block script %}
{{ super() }}
<script>
$(function () {
let type = $('input[name=type]');
let dates = $('.date');
let until = $('.until');
function update_targets() {
const type_checked = $('input[name=type]:checked');
if (["CALL", "EVENT"].includes(type_checked.val())) {
dates.show();
until.hide();
} else if (type_checked.val() === "WEEKLY") {
dates.show();
until.show();
} else {
dates.hide();
until.hide();
}
}
update_targets();
type.change(update_targets);
});
</script>
{% endblock %}

View File

@ -15,37 +15,21 @@
{% endblock %}
{% block content %}
{% if user.is_com_admin %}
<div id="news_admin">
<a class="button" href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
</div>
<br>
{% endif %}
<div id="news">
<div id="left_column" class="news_column">
{% for news in object_list.filter(type="NOTICE") %}
<section class="news_notice">
<h4><a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div class="news_content">{{ news.summary|markdown }}</div>
</section>
{% endfor %}
{% for news in object_list.filter(dates__start_date__lte=timezone.now(), dates__end_date__gte=timezone.now(), type="CALL") %}
<section class="news_call">
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div class="news_date">
<span>{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div class="news_content">{{ news.summary|markdown }}</div>
</section>
{% endfor %}
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5), news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %}
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5), news__is_moderated=True).datetimes('start_date', 'day') %}
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
{% if user.is_authenticated and (user.is_com_admin or user.memberships.board().ongoing().exists()) %}
<a class="btn btn-blue margin-bottom" href="{{ url("com:news_new") }}">
<i class="fa fa-plus"></i>
{% trans %}Create news{% endtrans %}
</a>
{% endif %}
{% if user.is_com_admin %}
<a class="btn btn-blue" href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
<br>
{% endif %}
{% if events_dates %}
{% for d in events_dates %}
<div class="news_events_group">
@ -57,113 +41,104 @@
</div>
</div>
<div class="news_events_group_items">
{% for news in object_list.filter(dates__start_date__gte=d,
dates__start_date__lte=d+timedelta(days=1),
type="EVENT").exclude(dates__end_date__lt=timezone.now())
.order_by('dates__start_date') %}
<section class="news_event">
<div class="club_logo">
{% if news.club.logo %}
<img src="{{ news.club.logo.url }}" alt="{{ news.club }}" />
{% else %}
<img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" />
{% endif %}
</div>
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div><a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a></div>
<div class="news_date">
<span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div class="news_content">{{ news.summary|markdown }}
<div class="button_bar">
{{ fb_quick(news) }}
{{ tweet_quick(news) }}
{% for news in object_list.filter(dates__start_date__gte=d,dates__start_date__lte=d+timedelta(days=1)).exclude(dates__end_date__lt=timezone.now()).order_by('dates__start_date') %}
<section class="news_event">
<div class="club_logo">
{% if news.club.logo %}
<img src="{{ news.club.logo.url }}" alt="{{ news.club }}" />
{% else %}
<img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" />
{% endif %}
</div>
</div>
</section>
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div><a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a></div>
<div class="news_date">
<span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div class="news_content">{{ news.summary|markdown }}
<div class="button_bar">
{{ fb_quick(news) }}
{{ tweet_quick(news) }}
</div>
</div>
</section>
{% endfor %}
</div>
</div>
{% endfor %}
{% else %}
<div class="news_empty">
<em>{% trans %}Nothing to come...{% endtrans %}</em>
</div>
</div>
{% endfor %}
{% else %}
<div class="news_empty">
<em>{% trans %}Nothing to come...{% endtrans %}</em>
</div>
{% endif %}
{% endif %}
<h3>{% trans %}All coming events{% endtrans %}</h3>
<ics-calendar locale="{{ get_language() }}"></ics-calendar>
</div>
<div id="right_column">
<div id="links">
<h3>{% trans %}Links{% endtrans %}</h3>
<div id="links_content">
<h4>{% trans %}Our services{% endtrans %}</h4>
<ul>
<li>
<i class="fa-solid fa-graduation-cap fa-xl"></i>
<a href="{{ url("pedagogy:guide") }}">{% trans %}UV Guide{% endtrans %}</a>
</li>
<li>
<i class="fa-solid fa-magnifying-glass fa-xl"></i>
<a href="{{ url("matmat:search_clear") }}">{% trans %}Matmatronch{% endtrans %}</a>
</li>
<li>
<i class="fa-solid fa-check-to-slot fa-xl"></i>
<a href="{{ url("election:list") }}">{% trans %}Elections{% endtrans %}</a>
</li>
</ul>
<br>
<h4>{% trans %}Social media{% endtrans %}</h4>
<ul>
<li>
<i class="fa-brands fa-discord fa-xl"></i>
<a rel="nofollow" target="#" href="https://discord.gg/QvTm3XJrHR">{% trans %}Discord AE{% endtrans %}</a>
{% if user.was_subscribed %}
- <a rel="nofollow" target="#" href="https://discord.gg/u6EuMfyGaJ">{% trans %}Dev Team{% endtrans %}</a>
{% endif %}
</li>
<li>
<i class="fa-brands fa-facebook fa-xl"></i>
<a rel="nofollow" target="#" href="https://www.facebook.com/@AEUTBM/">{% trans %}Facebook{% endtrans %}</a>
</li>
<li>
<i class="fa-brands fa-square-instagram fa-xl"></i>
<a rel="nofollow" target="#" href="https://www.instagram.com/ae_utbm">{% trans %}Instagram{% endtrans %}</a>
</li>
</ul>
<h3>{% trans %}All coming events{% endtrans %}</h3>
<ics-calendar locale="{{ get_language() }}"></ics-calendar>
</div>
</div>
<div id="birthdays">
<h3>{% trans %}Birthdays{% endtrans %}</h3>
<div id="birthdays_content">
{%- if user.was_subscribed -%}
<ul class="birthdays_year">
{%- for year, users in birthdays -%}
<div id="right_column">
<div id="links">
<h3>{% trans %}Links{% endtrans %}</h3>
<div id="links_content">
<h4>{% trans %}Our services{% endtrans %}</h4>
<ul>
<li>
{% trans age=timezone.now().year - year %}{{ age }} year old{% endtrans %}
<ul>
{%- for u in users -%}
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
{%- endfor -%}
</ul>
<i class="fa-solid fa-graduation-cap fa-xl"></i>
<a href="{{ url("pedagogy:guide") }}">{% trans %}UV Guide{% endtrans %}</a>
</li>
{%- endfor -%}
</ul>
{%- else -%}
<p>{% trans %}You need to subscribe to access this content{% endtrans %}</p>
{%- endif -%}
<li>
<i class="fa-solid fa-magnifying-glass fa-xl"></i>
<a href="{{ url("matmat:search_clear") }}">{% trans %}Matmatronch{% endtrans %}</a>
</li>
<li>
<i class="fa-solid fa-check-to-slot fa-xl"></i>
<a href="{{ url("election:list") }}">{% trans %}Elections{% endtrans %}</a>
</li>
</ul>
<br>
<h4>{% trans %}Social media{% endtrans %}</h4>
<ul>
<li>
<i class="fa-brands fa-discord fa-xl"></i>
<a rel="nofollow" target="#" href="https://discord.gg/QvTm3XJrHR">{% trans %}Discord AE{% endtrans %}</a>
{% if user.was_subscribed %}
- <a rel="nofollow" target="#" href="https://discord.gg/u6EuMfyGaJ">{% trans %}Dev Team{% endtrans %}</a>
{% endif %}
</li>
<li>
<i class="fa-brands fa-facebook fa-xl"></i>
<a rel="nofollow" target="#" href="https://www.facebook.com/@AEUTBM/">{% trans %}Facebook{% endtrans %}</a>
</li>
<li>
<i class="fa-brands fa-square-instagram fa-xl"></i>
<a rel="nofollow" target="#" href="https://www.instagram.com/ae_utbm">{% trans %}Instagram{% endtrans %}</a>
</li>
</ul>
</div>
</div>
<div id="birthdays">
<h3>{% trans %}Birthdays{% endtrans %}</h3>
<div id="birthdays_content">
{%- if user.was_subscribed -%}
<ul class="birthdays_year">
{%- for year, users in birthdays -%}
<li>
{% trans age=timezone.now().year - year %}{{ age }} year old{% endtrans %}
<ul>
{%- for u in users -%}
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
{%- endfor -%}
</ul>
</li>
{%- endfor -%}
</ul>
{%- else -%}
<p>{% trans %}You need to subscribe to access this content{% endtrans %}</p>
{%- endif -%}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -12,6 +12,9 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from datetime import timedelta
from unittest.mock import patch
import pytest
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
@ -20,9 +23,12 @@ from django.urls import reverse
from django.utils import html
from django.utils.timezone import localtime, now
from django.utils.translation import gettext as _
from model_bakery import baker
from pytest_django.asserts import assertRedirects
from club.models import Club, Membership
from com.models import News, Poster, Sith, Weekmail, WeekmailArticle
from com.models import News, NewsDate, Poster, Sith, Weekmail, WeekmailArticle
from core.baker_recipes import subscriber_user
from core.models import AnonymousUser, Group, User
@ -137,15 +143,8 @@ class TestNews(TestCase):
@classmethod
def setUpTestData(cls):
cls.com_admin = User.objects.get(username="comunity")
new = News.objects.create(
title="dummy new",
summary="This is a dummy new",
content="Look at that beautiful dummy new",
author=User.objects.get(username="subscriber"),
club=Club.objects.first(),
)
cls.new = new
cls.author = new.author
cls.new = baker.make(News)
cls.author = cls.new.author
cls.sli = User.objects.get(username="sli")
cls.anonymous = AnonymousUser()
@ -176,11 +175,11 @@ class TestNews(TestCase):
assert self.new.can_be_viewed_by(self.author)
def test_news_editor(self):
"""Test that only com admins can edit news."""
"""Test that only com admins and the original author can edit news."""
assert self.new.can_be_edited_by(self.com_admin)
assert self.new.can_be_edited_by(self.author)
assert not self.new.can_be_edited_by(self.sli)
assert not self.new.can_be_edited_by(self.anonymous)
assert not self.new.can_be_edited_by(self.author)
class TestWeekmailArticle(TestCase):
@ -230,3 +229,93 @@ class TestPoster(TestCase):
assert not self.poster.is_owned_by(self.susbcriber)
assert self.poster.is_owned_by(self.sli)
class TestNewsCreation(TestCase):
@classmethod
def setUpTestData(cls):
cls.club = baker.make(Club)
cls.user = subscriber_user.make()
baker.make(Membership, user=cls.user, club=cls.club, role=5)
def setUp(self):
self.client.force_login(self.user)
self.start = now() + timedelta(days=1)
self.end = self.start + timedelta(hours=5)
self.valid_payload = {
"title": "Test news",
"summary": "This is a test news",
"content": "This is a test news",
"club": self.club.pk,
"is_weekly": False,
"start_date": self.start,
"end_date": self.end,
}
def test_create_news(self):
response = self.client.post(reverse("com:news_new"), self.valid_payload)
created = News.objects.order_by("id").last()
assertRedirects(response, created.get_absolute_url())
assert created.title == "Test news"
assert not created.is_moderated
dates = list(created.dates.values("start_date", "end_date"))
assert dates == [{"start_date": self.start, "end_date": self.end}]
def test_create_news_multiple_dates(self):
self.valid_payload["is_weekly"] = True
self.valid_payload["occurrences"] = 2
response = self.client.post(reverse("com:news_new"), self.valid_payload)
created = News.objects.order_by("id").last()
assertRedirects(response, created.get_absolute_url())
dates = list(
created.dates.values("start_date", "end_date").order_by("start_date")
)
assert dates == [
{"start_date": self.start, "end_date": self.end},
{
"start_date": self.start + timedelta(days=7),
"end_date": self.end + timedelta(days=7),
},
]
def test_edit_news(self):
news = baker.make(News, author=self.user, is_moderated=True)
baker.make(
NewsDate,
news=news,
start_date=self.start + timedelta(hours=1),
end_date=self.end + timedelta(hours=1),
_quantity=2,
)
response = self.client.post(
reverse("com:news_edit", kwargs={"news_id": news.id}), self.valid_payload
)
created = News.objects.order_by("id").last()
assertRedirects(response, created.get_absolute_url())
assert created.title == "Test news"
assert not created.is_moderated
dates = list(created.dates.values("start_date", "end_date"))
assert dates == [{"start_date": self.start, "end_date": self.end}]
def test_ics_updated(self):
"""Test that the internal ICS is updated when news are created"""
# we will just test that the ICS is modified.
# Checking that the ICS is *well* modified is up to the ICS tests
with patch("com.calendar.IcsCalendar.make_internal") as mocked:
self.client.post(reverse("com:news_new"), self.valid_payload)
mocked.assert_called()
# The ICS file should also change after an update
self.valid_payload["is_weekly"] = True
self.valid_payload["occurrences"] = 2
last_news = News.objects.order_by("id").last()
with patch("com.calendar.IcsCalendar.make_internal") as mocked:
self.client.post(
reverse("com:news_edit", kwargs={"news_id": last_news.id}),
self.valid_payload,
)
mocked.assert_called()

View File

@ -25,9 +25,9 @@ from com.views import (
NewsCreateView,
NewsDeleteView,
NewsDetailView,
NewsEditView,
NewsListView,
NewsModerateView,
NewsUpdateView,
PosterCreateView,
PosterDeleteView,
PosterEditView,
@ -75,11 +75,11 @@ urlpatterns = [
path("news/", NewsListView.as_view(), name="news_list"),
path("news/admin/", NewsAdminListView.as_view(), name="news_admin_list"),
path("news/create/", NewsCreateView.as_view(), name="news_new"),
path("news/<int:news_id>/edit/", NewsUpdateView.as_view(), name="news_edit"),
path("news/<int:news_id>/delete/", NewsDeleteView.as_view(), name="news_delete"),
path(
"news/<int:news_id>/moderate/", NewsModerateView.as_view(), name="news_moderate"
),
path("news/<int:news_id>/edit/", NewsEditView.as_view(), name="news_edit"),
path("news/<int:news_id>/", NewsDetailView.as_view(), name="news_detail"),
path("mailings/", MailingListAdminView.as_view(), name="mailing_admin"),
path(

View File

@ -24,10 +24,15 @@
import itertools
from datetime import timedelta
from smtplib import SMTPRecipientsRefused
from typing import Any
from django.conf import settings
from django.contrib.auth.mixins import (
AccessMixin,
PermissionRequiredMixin,
)
from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import Exists, Max, OuterRef
from django.db.models import Max
from django.forms.models import modelform_factory
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
@ -36,16 +41,14 @@ from django.utils import timezone
from django.utils.timezone import localdate
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, View
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from club.models import Club, Mailing
from com.forms import NewsForm, PosterForm
from com.calendar import IcsCalendar
from com.forms import NewsDateForm, NewsForm, PosterForm
from com.models import News, NewsDate, Poster, Screen, Sith, Weekmail, WeekmailArticle
from core.models import Notification, User
from core.models import User
from core.views import (
CanCreateMixin,
CanEditMixin,
CanEditPropMixin,
CanViewMixin,
QuickNotifMixin,
@ -63,52 +66,42 @@ class ComTabsMixin(TabedViewMixin):
return _("Communication administration")
def get_list_of_tabs(self):
tab_list = []
tab_list.append(
{"url": reverse("com:weekmail"), "slug": "weekmail", "name": _("Weekmail")}
)
tab_list.append(
return [
{"url": reverse("com:weekmail"), "slug": "weekmail", "name": _("Weekmail")},
{
"url": reverse("com:weekmail_destinations"),
"slug": "weekmail_destinations",
"name": _("Weekmail destinations"),
}
)
tab_list.append(
{"url": reverse("com:info_edit"), "slug": "info", "name": _("Info message")}
)
tab_list.append(
},
{
"url": reverse("com:info_edit"),
"slug": "info",
"name": _("Info message"),
},
{
"url": reverse("com:alert_edit"),
"slug": "alert",
"name": _("Alert message"),
}
)
tab_list.append(
},
{
"url": reverse("com:mailing_admin"),
"slug": "mailings",
"name": _("Mailing lists administration"),
}
)
tab_list.append(
},
{
"url": reverse("com:poster_list"),
"slug": "posters",
"name": _("Posters list"),
}
)
tab_list.append(
},
{
"url": reverse("com:screen_list"),
"slug": "screens",
"name": _("Screens list"),
}
)
return tab_list
},
]
class IsComAdminMixin(View):
class IsComAdminMixin(AccessMixin):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_com_admin:
raise PermissionDenied
@ -145,99 +138,89 @@ class WeekmailDestinationEditView(ComEditView):
success_url = reverse_lazy("com:weekmail_destinations")
class NewsEditView(CanEditMixin, UpdateView):
# News
class NewsCreateView(PermissionRequiredMixin, CreateView):
"""View to either create or update News."""
model = News
form_class = NewsForm
template_name = "com/news_edit.jinja"
permission_required = "com.add_news"
def get_date_form_kwargs(self) -> dict[str, Any]:
"""Get initial data for NewsDateForm"""
if self.request.method == "POST":
return {"data": self.request.POST}
return {}
def get_form_kwargs(self):
return super().get_form_kwargs() | {
"author": self.request.user,
"date_form": NewsDateForm(**self.get_date_form_kwargs()),
}
def get_initial(self):
init = super().get_initial()
# if the id of a club is provided, select it by default
if club_id := self.request.GET.get("club"):
init["club"] = Club.objects.filter(id=club_id).first()
return init
class NewsUpdateView(UpdateView):
model = News
form_class = NewsForm
template_name = "com/news_edit.jinja"
pk_url_kwarg = "news_id"
def get_initial(self):
news_date: NewsDate = self.object.dates.order_by("id").first()
if news_date is None:
return {"start_date": None, "end_date": None}
return {"start_date": news_date.start_date, "end_date": news_date.end_date}
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid() and "preview" not in request.POST:
return self.form_valid(form)
else:
return self.form_invalid(form)
def dispatch(self, request, *args, **kwargs):
if (
not request.user.has_perm("com.edit_news")
and self.get_object().author != request.user
):
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
self.object = form.save()
if form.cleaned_data["automoderation"] and self.request.user.is_com_admin:
self.object.moderator = self.request.user
self.object.is_moderated = True
self.object.save()
else:
self.object.is_moderated = False
self.object.save()
unread_notif_subquery = Notification.objects.filter(
user=OuterRef("pk"), viewed=False
)
for user in User.objects.with_perm("com.moderate_news").filter(
~Exists(unread_notif_subquery)
):
Notification.objects.create(
user=user,
url=self.object.get_absolute_url(),
type="NEWS_MODERATION",
)
IcsCalendar.make_internal()
return super().form_valid(form)
def get_date_form_kwargs(self) -> dict[str, Any]:
"""Get initial data for NewsDateForm"""
response = {}
if self.request.method == "POST":
response["data"] = self.request.POST
dates = list(self.object.dates.order_by("id"))
if len(dates) == 0:
return {}
response["instance"] = dates[0]
occurrences = NewsDateForm.get_occurrences(len(dates))
if occurrences is not None:
response["initial"] = {"is_weekly": True, "occurrences": occurrences}
return response
class NewsCreateView(CanCreateMixin, CreateView):
model = News
form_class = NewsForm
template_name = "com/news_edit.jinja"
def get_initial(self):
init = {"author": self.request.user}
if "club" not in self.request.GET:
return init
init["club"] = Club.objects.filter(id=self.request.GET["club"]).first()
return init
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid() and "preview" not in request.POST:
return self.form_valid(form)
else:
self.object = form.instance
return self.form_invalid(form)
def form_valid(self, form):
self.object = form.save()
if form.cleaned_data["automoderation"] and self.request.user.is_com_admin:
self.object.moderator = self.request.user
self.object.is_moderated = True
self.object.save()
else:
unread_notif_subquery = Notification.objects.filter(
user=OuterRef("pk"), viewed=False
)
for user in User.objects.with_perm("com.moderate_news").filter(
~Exists(unread_notif_subquery)
):
Notification.objects.create(
user=user,
url=reverse("com:news_admin_list"),
type="NEWS_MODERATION",
)
return super().form_valid(form)
def get_form_kwargs(self):
return super().get_form_kwargs() | {
"author": self.request.user,
"date_form": NewsDateForm(**self.get_date_form_kwargs()),
}
class NewsDeleteView(CanEditMixin, DeleteView):
class NewsDeleteView(PermissionRequiredMixin, DeleteView):
model = News
pk_url_kwarg = "news_id"
template_name = "core/delete_confirm.jinja"
success_url = reverse_lazy("com:news_admin_list")
permission_required = "com.delete_news"
class NewsModerateView(CanEditMixin, SingleObjectMixin):
class NewsModerateView(PermissionRequiredMixin, DetailView):
model = News
pk_url_kwarg = "news_id"
permission_required = "com.moderate_news"
def get(self, request, *args, **kwargs):
self.object = self.get_object()
@ -252,17 +235,23 @@ class NewsModerateView(CanEditMixin, SingleObjectMixin):
return redirect("com:news_admin_list")
class NewsAdminListView(CanEditMixin, ListView):
class NewsAdminListView(PermissionRequiredMixin, ListView):
model = News
template_name = "com/news_admin_list.jinja"
queryset = News.objects.all()
queryset = News.objects.select_related(
"club", "author", "moderator"
).prefetch_related("dates")
permission_required = ["com.moderate_news", "com.delete_news"]
class NewsListView(CanViewMixin, ListView):
class NewsListView(ListView):
model = News
template_name = "com/news_list.jinja"
queryset = News.objects.filter(is_moderated=True)
def get_queryset(self):
return super().get_queryset().viewable_by(self.request.user)
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["NewsDate"] = NewsDate
@ -283,6 +272,10 @@ class NewsDetailView(CanViewMixin, DetailView):
model = News
template_name = "com/news_detail.jinja"
pk_url_kwarg = "news_id"
queryset = News.objects.select_related("club", "author", "moderator")
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {"date": self.object.dates.first()}
# Weekmail

View File

@ -894,6 +894,7 @@ Welcome to the wiki page!
public_group = Group.objects.create(name="Public")
subscribers = Group.objects.create(name="Subscribers")
subscribers.permissions.add(*list(perms.filter(codename__in=["add_news"])))
old_subscribers = Group.objects.create(name="Old subscribers")
old_subscribers.permissions.add(
*list(

View File

@ -665,7 +665,9 @@ form {
}
&:checked {
background: var(--nf-input-focus-border-color) none initial;
background: none;
background-position: 0 0;
background-color: var(--nf-input-focus-border-color);
&::after {
transform: translateY(-50%) translateX(

View File

@ -436,8 +436,8 @@ body {
$row-gap: 0.5rem;
&.gap {
column-gap: var($col-gap);
row-gap: var($row-gap);
column-gap: $col-gap;
row-gap: $row-gap;
}
@for $i from 2 through 5 {

View File

@ -26,6 +26,7 @@ import datetime
import phonenumbers
from django import template
from django.forms import BoundField
from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe
from django.utils.translation import ngettext
@ -80,3 +81,43 @@ def format_timedelta(value: datetime.timedelta) -> str:
return ngettext(
"%(nb_days)d day, %(remainder)s", "%(nb_days)d days, %(remainder)s", days
) % {"nb_days": days, "remainder": str(remainder)}
@register.filter(name="add_attr")
def add_attr(field: BoundField, attr: str):
"""Add attributes to a form field directly in the template.
Attributes are `key=value` pairs, separated by commas.
Example:
```jinja
<form x-data="{alpineField: null}">
{{ form.field|add_attr("x-model=alpineField") }}
</form>
```
will render :
```html
<form x-data="{alpineField: null}">
<input type="..." x-model="alpineField">
</form>
```
Notes:
Doing this gives the same result as setting the attribute
directly in the python code.
However, sometimes there are attributes that are tightly
coupled to the frontend logic (like Alpine variables)
and that shouldn't be declared outside of it.
"""
attrs = {}
definition = attr.split(",")
for d in definition:
if "=" not in d:
attrs["class"] = d
else:
key, val = d.split("=")
attrs[key] = val
return field.as_widget(attrs=attrs)

View File

@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-08 12:23+0100\n"
"POT-Creation-Date: 2025-01-10 00:25+0100\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@ -841,7 +841,7 @@ msgstr "vous devez spécifier au moins un utilisateur ou une adresse email"
msgid "Begin date"
msgstr "Date de début"
#: club/forms.py com/views.py counter/forms.py election/views.py
#: club/forms.py com/forms.py counter/forms.py election/views.py
#: subscription/forms.py
msgid "End date"
msgstr "Date de fin"
@ -1259,6 +1259,43 @@ msgstr "Liste d'affiches"
msgid "Props"
msgstr "Propriétés"
#: com/forms.py
msgid "Format: 16:9 | Resolution: 1920x1080"
msgstr "Format : 16:9 | Résolution : 1920x1080"
#: com/forms.py election/views.py subscription/forms.py
msgid "Start date"
msgstr "Date de début"
#: com/forms.py
msgid "Weekly event"
msgstr "Événement Hebdomadaire"
#: com/forms.py
msgid "Weekly events will occur each week for a specified timespan."
msgstr "Les événements hebdomadaires se répéteront chaque semaine pendant une durée déterminée"
#: com/forms.py
#, python-format
msgid "%d times"
msgstr "%d fois"
#: com/forms.py
msgid "Until the end of the semester"
msgstr "Jusqu'à la fin du semestre"
#: com/forms.py
msgid "Occurrences"
msgstr "Occurences"
#: com/forms.py
msgid "How much times should the event occur (including the first one)"
msgstr "Combien de fois l'événement doit-il se répéter (en incluant la première fois)"
#: com/forms.py
msgid "Automoderation"
msgstr "Automodération"
#: com/models.py
msgid "alert message"
msgstr "message d'alerte"
@ -1271,22 +1308,6 @@ msgstr "message d'info"
msgid "weekmail destinations"
msgstr "destinataires du weekmail"
#: com/models.py
msgid "Notice"
msgstr "Information"
#: com/models.py
msgid "Event"
msgstr "Événement"
#: com/models.py
msgid "Weekly"
msgstr "Hebdomadaire"
#: com/models.py
msgid "Call"
msgstr "Appel"
#: com/models.py core/templates/core/macros.jinja election/models.py
#: forum/models.py pedagogy/models.py
msgid "title"
@ -1312,10 +1333,6 @@ msgstr "contenu"
msgid "A more detailed and exhaustive description of the event."
msgstr "Une description plus détaillée et exhaustive de l'évènement."
#: com/models.py core/models.py launderette/models.py
msgid "type"
msgstr "type"
#: com/models.py
msgid "The club which organizes the event."
msgstr "Le club qui organise l'évènement."
@ -1324,6 +1341,10 @@ msgstr "Le club qui organise l'évènement."
msgid "author"
msgstr "auteur"
#: com/models.py
msgid "news"
msgstr "nouvelle"
#: com/models.py
msgid "news_date"
msgstr "date de la nouvelle"
@ -1336,6 +1357,14 @@ msgstr "date de début"
msgid "end_date"
msgstr "date de fin"
#: com/models.py
msgid "news date"
msgstr "date de la nouvelle"
#: com/models.py
msgid "news dates"
msgstr "dates de la nouvelle"
#: com/models.py
msgid "intro"
msgstr "intro"
@ -1372,6 +1401,10 @@ msgstr "fichier"
msgid "display time"
msgstr "temps d'affichage"
#: com/models.py
msgid "Can moderate poster"
msgstr "Peut modérer les posters"
#: com/models.py
msgid "Begin date should be before end date"
msgstr "La date de début doit être avant celle de fin"
@ -1420,23 +1453,17 @@ msgid "News"
msgstr "Nouvelles"
#: com/templates/com/news_admin_list.jinja com/templates/com/news_edit.jinja
#: core/templates/core/user_tools.jinja
#: com/templates/com/news_list.jinja core/templates/core/user_tools.jinja
msgid "Create news"
msgstr "Créer nouvelle"
msgstr "Créer une nouvelle"
#: com/templates/com/news_admin_list.jinja
msgid "Notices"
msgstr "Information"
msgid "Weeklies"
msgstr "Événements hebdomadaires"
#: com/templates/com/news_admin_list.jinja
msgid "Displayed notices"
msgstr "Informations affichées"
#: com/templates/com/news_admin_list.jinja
#: launderette/templates/launderette/launderette_admin.jinja
#: launderette/views.py
msgid "Type"
msgstr "Type"
msgid "Displayed weeklies"
msgstr "Événements hebdomadaires affichées"
#: com/templates/com/news_admin_list.jinja com/templates/com/weekmail.jinja
#: forum/templates/forum/forum.jinja forum/templates/forum/main.jinja
@ -1457,18 +1484,6 @@ msgstr "Auteur"
msgid "Moderator"
msgstr "Modérateur"
#: com/templates/com/news_admin_list.jinja
msgid "Notices to moderate"
msgstr "Informations à modérer"
#: com/templates/com/news_admin_list.jinja
msgid "Weeklies"
msgstr "Nouvelles hebdomadaires"
#: com/templates/com/news_admin_list.jinja
msgid "Displayed weeklies"
msgstr "Nouvelles hebdomadaires affichées"
#: com/templates/com/news_admin_list.jinja
#: trombi/templates/trombi/edit_profile.jinja
msgid "Dates"
@ -1478,18 +1493,6 @@ msgstr "Dates"
msgid "Weeklies to moderate"
msgstr "Nouvelles hebdomadaires à modérer"
#: com/templates/com/news_admin_list.jinja
msgid "Calls"
msgstr "Appels"
#: com/templates/com/news_admin_list.jinja
msgid "Displayed calls"
msgstr "Appels affichés"
#: com/templates/com/news_admin_list.jinja
msgid "Calls to moderate"
msgstr "Appels à modérer"
#: com/templates/com/news_admin_list.jinja
#: core/templates/core/base/navbar.jinja
msgid "Events"
@ -1507,7 +1510,7 @@ msgstr "Événements à modérer"
msgid "Back to news"
msgstr "Retour aux nouvelles"
#: com/templates/com/news_detail.jinja com/templates/com/news_edit.jinja
#: com/templates/com/news_detail.jinja
msgid "Author: "
msgstr "Auteur : "
@ -1523,41 +1526,14 @@ msgstr "Éditer (sera soumise de nouveau à la modération)"
msgid "Edit news"
msgstr "Éditer la nouvelle"
#: com/templates/com/news_edit.jinja
msgid "Notice: Information, election result - no date"
msgstr "Information, résultat d'élection - sans date"
#: com/templates/com/news_edit.jinja
msgid "Event: punctual event, associated with one date"
msgstr "Événement : événement ponctuel associé à une date"
#: com/templates/com/news_edit.jinja
msgid ""
"Weekly: recurrent event, associated with many dates (specify the first one, "
"and a deadline)"
msgstr ""
"Hebdomadaire : événement récurrent, associé à plusieurs dates (spécifier la "
"première, ainsi que la date de fin)"
#: com/templates/com/news_edit.jinja
msgid ""
"Call: long time event, associated with a long date (like election appliance)"
msgstr ""
"Appel : événement de longue durée, associé à une longue date (comme des "
"candidatures à une élection)"
#: com/templates/com/news_edit.jinja com/templates/com/weekmail.jinja
msgid "Preview"
msgstr "Prévisualiser"
#: com/templates/com/news_list.jinja
msgid "Events today and the next few days"
msgstr "Événements aujourd'hui et dans les prochains jours"
#: com/templates/com/news_list.jinja
msgid "Administrate news"
msgstr "Administrer les news"
#: com/templates/com/news_list.jinja
msgid "Events today and the next few days"
msgstr "Événements aujourd'hui et dans les prochains jours"
#: com/templates/com/news_list.jinja
msgid "Nothing to come..."
msgstr "Rien à venir..."
@ -1679,6 +1655,10 @@ msgstr "Diaporama"
msgid "Weekmail"
msgstr "Weekmail"
#: com/templates/com/weekmail.jinja
msgid "Preview"
msgstr "Prévisualiser"
#: com/templates/com/weekmail.jinja com/templates/com/weekmail_preview.jinja
msgid "Send"
msgstr "Envoyer"
@ -1768,14 +1748,6 @@ msgstr "Astuce"
msgid "Final word"
msgstr "Le mot de la fin"
#: com/views.py
msgid "Format: 16:9 | Resolution: 1920x1080"
msgstr "Format : 16:9 | Résolution : 1920x1080"
#: com/views.py election/views.py subscription/forms.py
msgid "Start date"
msgstr "Date de début"
#: com/views.py
msgid "Communication administration"
msgstr "Administration de la communication"
@ -1796,22 +1768,6 @@ msgstr "Message d'alerte"
msgid "Screens list"
msgstr "Liste d'écrans"
#: com/views.py rootplace/templates/rootplace/userban.jinja
msgid "Until"
msgstr "Jusqu'à"
#: com/views.py
msgid "Automoderation"
msgstr "Automodération"
#: com/views.py
msgid "This field is required."
msgstr "Ce champ est obligatoire."
#: com/views.py
msgid "An event cannot end before its beginning."
msgstr "Un évènement ne peut pas se finir avant d'avoir commencé."
#: com/views.py
msgid "Delete and save to regenerate"
msgstr "Supprimer et sauver pour régénérer"
@ -2215,6 +2171,10 @@ msgstr "url"
msgid "param"
msgstr "param"
#: core/models.py launderette/models.py
msgid "type"
msgstr "type"
#: core/models.py
msgid "viewed"
msgstr "vue"
@ -4734,6 +4694,11 @@ msgstr "Machines"
msgid "New machine"
msgstr "Nouvelle machine"
#: launderette/templates/launderette/launderette_admin.jinja
#: launderette/views.py
msgid "Type"
msgstr "Type"
#: launderette/templates/launderette/launderette_book.jinja
msgid "Choose"
msgstr "Choisir"
@ -5134,6 +5099,10 @@ msgstr "Fusion"
msgid "Ban a user"
msgstr "Bannir un utilisateur"
#: rootplace/templates/rootplace/userban.jinja
msgid "Until"
msgstr "Jusqu'à"
#: rootplace/templates/rootplace/userban.jinja
msgid "not specified"
msgstr "non spécifié"

View File

@ -152,6 +152,7 @@ TEMPLATES = [
"phonenumber": "core.templatetags.renderer.phonenumber",
"truncate_time": "core.templatetags.renderer.truncate_time",
"format_timedelta": "core.templatetags.renderer.format_timedelta",
"add_attr": "core.templatetags.renderer.add_attr",
},
"globals": {
"can_edit_prop": "core.views.can_edit_prop",