mirror of
https://github.com/ae-utbm/sith.git
synced 2024-12-22 15:51:19 +00:00
Merge pull request #932 from ae-utbm/fix-subscriptions
Rework the subscription page
This commit is contained in:
commit
95f8e7517c
@ -529,13 +529,15 @@ class User(AbstractBaseUser):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def can_create_subscription(self):
|
def can_create_subscription(self) -> bool:
|
||||||
from club.models import Club
|
from club.models import Membership
|
||||||
|
|
||||||
for club in Club.objects.filter(id__in=settings.SITH_CAN_CREATE_SUBSCRIPTIONS):
|
return (
|
||||||
if club in self.clubs_with_rights:
|
Membership.objects.board()
|
||||||
return True
|
.ongoing()
|
||||||
return False
|
.filter(club_id__in=settings.SITH_CAN_CREATE_SUBSCRIPTIONS)
|
||||||
|
.exists()
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def is_launderette_manager(self):
|
def is_launderette_manager(self):
|
||||||
|
89
core/static/core/forms.scss
Normal file
89
core/static/core/forms.scss
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
@import "colors";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Style related to forms
|
||||||
|
*/
|
||||||
|
|
||||||
|
a.button,
|
||||||
|
button,
|
||||||
|
input[type="button"],
|
||||||
|
input[type="submit"],
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="file"] {
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: $background-button-color;
|
||||||
|
padding: 0.4em;
|
||||||
|
margin: 0.1em;
|
||||||
|
font-size: 1.2em;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: hsl(0, 0%, 83%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button,
|
||||||
|
input[type="button"],
|
||||||
|
input[type="submit"],
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="file"] {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button:not(:disabled),
|
||||||
|
button:not(:disabled),
|
||||||
|
input[type="button"]:not(:disabled),
|
||||||
|
input[type="submit"]:not(:disabled),
|
||||||
|
input[type="reset"]:not(:disabled),
|
||||||
|
input[type="checkbox"]:not(:disabled),
|
||||||
|
input[type="file"]:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea[type="text"],
|
||||||
|
[type="number"] {
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: $background-button-color;
|
||||||
|
padding: 0.4em;
|
||||||
|
margin: 0.1em;
|
||||||
|
font-size: 1.2em;
|
||||||
|
border-radius: 5px;
|
||||||
|
max-width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: $background-button-color;
|
||||||
|
padding: 7px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.2em;
|
||||||
|
background-color: $background-button-color;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not(.button) {
|
||||||
|
text-decoration: none;
|
||||||
|
color: $primary-dark-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-light-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
@import "colors";
|
@import "colors";
|
||||||
|
@import "forms";
|
||||||
|
|
||||||
/*--------------------------MEDIA QUERY HELPERS------------------------*/
|
/*--------------------------MEDIA QUERY HELPERS------------------------*/
|
||||||
$small-devices: 576px;
|
$small-devices: 576px;
|
||||||
@ -13,91 +14,6 @@ body {
|
|||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
a.button,
|
|
||||||
button,
|
|
||||||
input[type="button"],
|
|
||||||
input[type="submit"],
|
|
||||||
input[type="reset"],
|
|
||||||
input[type="file"] {
|
|
||||||
border: none;
|
|
||||||
text-decoration: none;
|
|
||||||
background-color: $background-button-color;
|
|
||||||
padding: 0.4em;
|
|
||||||
margin: 0.1em;
|
|
||||||
font-size: 1.2em;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: black;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: hsl(0, 0%, 83%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a.button,
|
|
||||||
input[type="button"],
|
|
||||||
input[type="submit"],
|
|
||||||
input[type="reset"],
|
|
||||||
input[type="file"] {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.button:not(:disabled),
|
|
||||||
button:not(:disabled),
|
|
||||||
input[type="button"]:not(:disabled),
|
|
||||||
input[type="submit"]:not(:disabled),
|
|
||||||
input[type="reset"]:not(:disabled),
|
|
||||||
input[type="checkbox"]:not(:disabled),
|
|
||||||
input[type="file"]:not(:disabled) {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
textarea[type="text"],
|
|
||||||
[type="number"] {
|
|
||||||
border: none;
|
|
||||||
text-decoration: none;
|
|
||||||
background-color: $background-button-color;
|
|
||||||
padding: 0.4em;
|
|
||||||
margin: 0.1em;
|
|
||||||
font-size: 1.2em;
|
|
||||||
border-radius: 5px;
|
|
||||||
max-width: 95%;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
border: none;
|
|
||||||
text-decoration: none;
|
|
||||||
background-color: $background-button-color;
|
|
||||||
padding: 7px;
|
|
||||||
font-size: 1.2em;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
border: none;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 1.2em;
|
|
||||||
background-color: $background-button-color;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:not(.button) {
|
|
||||||
text-decoration: none;
|
|
||||||
color: $primary-dark-color;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $primary-light-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
color: $primary-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[aria-busy] {
|
[aria-busy] {
|
||||||
--loading-size: 50px;
|
--loading-size: 50px;
|
||||||
--loading-stroke: 5px;
|
--loading-stroke: 5px;
|
||||||
@ -262,8 +178,10 @@ a:not(.button) {
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 9px 13px;
|
padding: 9px 13px;
|
||||||
|
margin: 3px;
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
&.btn-blue {
|
&.btn-blue {
|
||||||
@ -367,6 +285,49 @@ a:not(.button) {
|
|||||||
.alert-aside {
|
.alert-aside {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
.tab-headers {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
background-color: $primary-neutral-light-color;
|
||||||
|
padding: 3px 12px 12px;
|
||||||
|
column-gap: 20px;
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
|
||||||
|
.tab-header {
|
||||||
|
border: none;
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
font-size: 120%;
|
||||||
|
background-color: unset;
|
||||||
|
position: relative;
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 4px solid darken($primary-neutral-light-color, 10%);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
&:hover:after {
|
||||||
|
border-bottom-color: darken($primary-neutral-light-color, 20%);
|
||||||
|
}
|
||||||
|
&.active:after {
|
||||||
|
border-bottom-color: $primary-dark-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1246,26 +1207,26 @@ u,
|
|||||||
/*-----------------------------USER PROFILE----------------------------*/
|
/*-----------------------------USER PROFILE----------------------------*/
|
||||||
|
|
||||||
.user_mini_profile {
|
.user_mini_profile {
|
||||||
height: 100%;
|
--gap-size: 1em;
|
||||||
width: 100%;
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--gap-size);
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user_mini_profile_infos {
|
.user_mini_profile_infos {
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
height: 20%;
|
max-height: 20%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
|
||||||
div {
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user_mini_profile_infos_text {
|
.user_mini_profile_infos_text {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
@ -1276,10 +1237,10 @@ u,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user_mini_profile_picture {
|
.user_mini_profile_picture {
|
||||||
height: 80%;
|
max-height: calc(80% - var(--gap-size));
|
||||||
display: flex;
|
max-width: 100%;
|
||||||
justify-content: center;
|
display: block;
|
||||||
align-items: center;
|
margin: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if user.promo and user.promo_has_logo() %}
|
{% if user.promo and user.promo_has_logo() %}
|
||||||
<div class="user_mini_profile_promo">
|
<div class="user_mini_profile_promo">
|
||||||
<img src="{{ static('core/img/promo_%02d.png' % user.promo) }}" title="Promo {{ user.promo }}" alt="Promo {{ user.promo }}" class="promo_pict" />
|
<img
|
||||||
|
src="{{ static('core/img/promo_%02d.png' % user.promo) }}"
|
||||||
|
title="Promo {{ user.promo }}"
|
||||||
|
alt="Promo {{ user.promo }}"
|
||||||
|
class="promo_pict"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -74,8 +79,11 @@
|
|||||||
{% if user.profile_pict %}
|
{% if user.profile_pict %}
|
||||||
<img src="{{ user.profile_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}" />
|
<img src="{{ user.profile_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}" />
|
||||||
{% else %}
|
{% else %}
|
||||||
<img src="{{ static('core/img/unknown.jpg') }}" alt="{% trans %}Profile{% endtrans %}"
|
<img
|
||||||
title="{% trans %}Profile{% endtrans %}" />
|
src="{{ static('core/img/unknown.jpg') }}"
|
||||||
|
alt="{% trans %}Profile{% endtrans %}"
|
||||||
|
title="{% trans %}Profile{% endtrans %}"
|
||||||
|
/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -170,12 +178,12 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro paginate_htmx(current_page, paginator) %}
|
{% macro paginate_htmx(current_page, paginator) %}
|
||||||
{# Add pagination buttons for pages without Alpine but supporting framgents.
|
{# Add pagination buttons for pages without Alpine but supporting fragments.
|
||||||
|
|
||||||
This must be coupled with a view that handles pagination
|
This must be coupled with a view that handles pagination
|
||||||
with the Django Paginator object and supports framgents.
|
with the Django Paginator object and supports fragments.
|
||||||
|
|
||||||
The relpaced fragment will be #content so make sure you are calling this macro inside your content block.
|
The replaced fragment will be #content so make sure you are calling this macro inside your content block.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
current_page (django.core.paginator.Page): the current page object
|
current_page (django.core.paginator.Page): the current page object
|
||||||
@ -247,9 +255,9 @@
|
|||||||
{% macro select_all_checkbox(form_id) %}
|
{% macro select_all_checkbox(form_id) %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
function checkbox_{{form_id}}(value) {
|
function checkbox_{{form_id}}(value) {
|
||||||
list = document.getElementById("{{ form_id }}").getElementsByTagName("input");
|
const inputs = document.getElementById("{{ form_id }}").getElementsByTagName("input");
|
||||||
for (let element of list){
|
for (let element of inputs){
|
||||||
if (element.type == "checkbox"){
|
if (element.type === "checkbox"){
|
||||||
element.checked = value;
|
element.checked = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -258,3 +266,65 @@
|
|||||||
<button type="button" onclick="checkbox_{{form_id}}(true);">{% trans %}Select All{% endtrans %}</button>
|
<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>
|
<button type="button" onclick="checkbox_{{form_id}}(false);">{% trans %}Unselect All{% endtrans %}</button>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro tabs(tab_list, attrs = "") %}
|
||||||
|
{# Tab component
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
tab_list: list[tuple[str, str]] The list of tabs to display.
|
||||||
|
Each element of the list is a tuple which first element
|
||||||
|
is the title of the tab and the second element its content
|
||||||
|
attrs: str Additional attributes to put on the enclosing div
|
||||||
|
|
||||||
|
Example:
|
||||||
|
A basic usage would be as follow :
|
||||||
|
|
||||||
|
{{ tabs([("title 1", "content 1"), ("title 2", "content 2")]) }}
|
||||||
|
|
||||||
|
If you want to display more complex logic, you can define macros
|
||||||
|
and use those macros in parameters :
|
||||||
|
|
||||||
|
{{ tabs([("title", my_macro())]) }}
|
||||||
|
|
||||||
|
It's also possible to get and set the currently selected tab using Alpine.
|
||||||
|
Here, the title of the currently selected tab will be displayed.
|
||||||
|
Moreover, on page load, the tab will be opened on "tab 2".
|
||||||
|
|
||||||
|
<div x-data="{current_tab: 'tab 2'}">
|
||||||
|
<p x-text="current_tab"></p>
|
||||||
|
{{ tabs([("tab 1", "Hello"), ("tab 2", "World")], "x-model=current_tab") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
If you want to have translated tab titles, you can enclose the macro call
|
||||||
|
in a with block :
|
||||||
|
|
||||||
|
{% with title=_("title"), content=_("Content") %}
|
||||||
|
{{ tabs([(tab1, content)]) }}
|
||||||
|
{% endwith %}
|
||||||
|
#}
|
||||||
|
<div
|
||||||
|
class="tabs shadow"
|
||||||
|
x-data="{selected: '{{ tab_list[0][0] }}'}"
|
||||||
|
x-modelable="selected"
|
||||||
|
{{ attrs }}
|
||||||
|
>
|
||||||
|
<div class="tab-headers">
|
||||||
|
{% for title, _ in tab_list %}
|
||||||
|
<button
|
||||||
|
class="tab-header clickable"
|
||||||
|
:class="{active: selected === '{{ title }}'}"
|
||||||
|
@click="selected = '{{ title }}'"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
{% for title, content in tab_list %}
|
||||||
|
<section x-show="selected === '{{ title }}'">
|
||||||
|
{{ content }}
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-11-19 00:47+0100\n"
|
"POT-Creation-Date: 2024-11-29 18:04+0100\n"
|
||||||
"PO-Revision-Date: 2016-07-18\n"
|
"PO-Revision-Date: 2016-07-18\n"
|
||||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@ -40,7 +40,7 @@ msgstr "code postal"
|
|||||||
msgid "country"
|
msgid "country"
|
||||||
msgstr "pays"
|
msgstr "pays"
|
||||||
|
|
||||||
#: accounting/models.py:67 core/models.py:390
|
#: accounting/models.py:67 core/models.py:391
|
||||||
msgid "phone"
|
msgid "phone"
|
||||||
msgstr "téléphone"
|
msgstr "téléphone"
|
||||||
|
|
||||||
@ -126,8 +126,8 @@ msgstr "numéro"
|
|||||||
msgid "journal"
|
msgid "journal"
|
||||||
msgstr "classeur"
|
msgstr "classeur"
|
||||||
|
|
||||||
#: accounting/models.py:256 core/models.py:945 core/models.py:1456
|
#: accounting/models.py:256 core/models.py:956 core/models.py:1467
|
||||||
#: core/models.py:1501 core/models.py:1530 core/models.py:1554
|
#: core/models.py:1512 core/models.py:1541 core/models.py:1565
|
||||||
#: counter/models.py:689 counter/models.py:793 counter/models.py:997
|
#: counter/models.py:689 counter/models.py:793 counter/models.py:997
|
||||||
#: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:312
|
#: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:312
|
||||||
#: forum/models.py:413
|
#: forum/models.py:413
|
||||||
@ -165,7 +165,7 @@ msgid "accounting type"
|
|||||||
msgstr "type comptable"
|
msgstr "type comptable"
|
||||||
|
|
||||||
#: accounting/models.py:294 accounting/models.py:429 accounting/models.py:460
|
#: accounting/models.py:294 accounting/models.py:429 accounting/models.py:460
|
||||||
#: accounting/models.py:492 core/models.py:1529 core/models.py:1555
|
#: accounting/models.py:492 core/models.py:1540 core/models.py:1566
|
||||||
#: counter/models.py:759
|
#: counter/models.py:759
|
||||||
msgid "label"
|
msgid "label"
|
||||||
msgstr "étiquette"
|
msgstr "étiquette"
|
||||||
@ -218,7 +218,7 @@ msgstr "Compte"
|
|||||||
msgid "Company"
|
msgid "Company"
|
||||||
msgstr "Entreprise"
|
msgstr "Entreprise"
|
||||||
|
|
||||||
#: accounting/models.py:307 core/models.py:337 sith/settings.py:421
|
#: accounting/models.py:307 core/models.py:338 sith/settings.py:421
|
||||||
msgid "Other"
|
msgid "Other"
|
||||||
msgstr "Autre"
|
msgstr "Autre"
|
||||||
|
|
||||||
@ -362,8 +362,8 @@ msgstr "Compte en banque : "
|
|||||||
#: core/templates/core/file_detail.jinja:62
|
#: core/templates/core/file_detail.jinja:62
|
||||||
#: core/templates/core/file_moderation.jinja:48
|
#: core/templates/core/file_moderation.jinja:48
|
||||||
#: core/templates/core/group_detail.jinja:26
|
#: core/templates/core/group_detail.jinja:26
|
||||||
#: core/templates/core/group_list.jinja:25 core/templates/core/macros.jinja:96
|
#: core/templates/core/group_list.jinja:25 core/templates/core/macros.jinja:104
|
||||||
#: core/templates/core/macros.jinja:115 core/templates/core/page_prop.jinja:14
|
#: core/templates/core/macros.jinja:123 core/templates/core/page_prop.jinja:14
|
||||||
#: core/templates/core/user_account_detail.jinja:41
|
#: core/templates/core/user_account_detail.jinja:41
|
||||||
#: core/templates/core/user_account_detail.jinja:77
|
#: core/templates/core/user_account_detail.jinja:77
|
||||||
#: core/templates/core/user_clubs.jinja:34
|
#: core/templates/core/user_clubs.jinja:34
|
||||||
@ -372,7 +372,7 @@ msgstr "Compte en banque : "
|
|||||||
#: core/templates/core/user_preferences.jinja:48
|
#: core/templates/core/user_preferences.jinja:48
|
||||||
#: counter/templates/counter/last_ops.jinja:35
|
#: counter/templates/counter/last_ops.jinja:35
|
||||||
#: counter/templates/counter/last_ops.jinja:65
|
#: counter/templates/counter/last_ops.jinja:65
|
||||||
#: election/templates/election/election_detail.jinja:187
|
#: election/templates/election/election_detail.jinja:191
|
||||||
#: forum/templates/forum/macros.jinja:21
|
#: forum/templates/forum/macros.jinja:21
|
||||||
#: launderette/templates/launderette/launderette_admin.jinja:16
|
#: launderette/templates/launderette/launderette_admin.jinja:16
|
||||||
#: launderette/views.py:210 pedagogy/templates/pedagogy/guide.jinja:99
|
#: launderette/views.py:210 pedagogy/templates/pedagogy/guide.jinja:99
|
||||||
@ -424,7 +424,7 @@ msgstr "Nouveau compte club"
|
|||||||
#: counter/templates/counter/counter_list.jinja:17
|
#: counter/templates/counter/counter_list.jinja:17
|
||||||
#: counter/templates/counter/counter_list.jinja:33
|
#: counter/templates/counter/counter_list.jinja:33
|
||||||
#: counter/templates/counter/counter_list.jinja:49
|
#: counter/templates/counter/counter_list.jinja:49
|
||||||
#: election/templates/election/election_detail.jinja:184
|
#: election/templates/election/election_detail.jinja:188
|
||||||
#: forum/templates/forum/macros.jinja:20 forum/templates/forum/macros.jinja:62
|
#: forum/templates/forum/macros.jinja:20 forum/templates/forum/macros.jinja:62
|
||||||
#: launderette/templates/launderette/launderette_list.jinja:16
|
#: launderette/templates/launderette/launderette_list.jinja:16
|
||||||
#: pedagogy/templates/pedagogy/guide.jinja:98
|
#: pedagogy/templates/pedagogy/guide.jinja:98
|
||||||
@ -774,7 +774,7 @@ msgstr "Opération liée : "
|
|||||||
#: core/templates/core/user_preferences.jinja:65
|
#: core/templates/core/user_preferences.jinja:65
|
||||||
#: counter/templates/counter/cash_register_summary.jinja:28
|
#: counter/templates/counter/cash_register_summary.jinja:28
|
||||||
#: forum/templates/forum/reply.jinja:39
|
#: forum/templates/forum/reply.jinja:39
|
||||||
#: subscription/templates/subscription/subscription.jinja:25
|
#: subscription/templates/subscription/fragments/creation_form.jinja:9
|
||||||
#: trombi/templates/trombi/comment.jinja:26
|
#: trombi/templates/trombi/comment.jinja:26
|
||||||
#: trombi/templates/trombi/edit_profile.jinja:13
|
#: trombi/templates/trombi/edit_profile.jinja:13
|
||||||
#: trombi/templates/trombi/user_tools.jinja:13
|
#: trombi/templates/trombi/user_tools.jinja:13
|
||||||
@ -956,7 +956,7 @@ msgid "Begin date"
|
|||||||
msgstr "Date de début"
|
msgstr "Date de début"
|
||||||
|
|
||||||
#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:206
|
#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:206
|
||||||
#: election/views.py:170 subscription/views.py:38
|
#: election/views.py:170 subscription/forms.py:21
|
||||||
msgid "End date"
|
msgid "End date"
|
||||||
msgstr "Date de fin"
|
msgstr "Date de fin"
|
||||||
|
|
||||||
@ -1025,11 +1025,11 @@ msgstr "actif"
|
|||||||
msgid "short description"
|
msgid "short description"
|
||||||
msgstr "description courte"
|
msgstr "description courte"
|
||||||
|
|
||||||
#: club/models.py:81 core/models.py:392
|
#: club/models.py:81 core/models.py:393
|
||||||
msgid "address"
|
msgid "address"
|
||||||
msgstr "Adresse"
|
msgstr "Adresse"
|
||||||
|
|
||||||
#: club/models.py:98 core/models.py:303
|
#: club/models.py:98 core/models.py:304
|
||||||
msgid "home"
|
msgid "home"
|
||||||
msgstr "home"
|
msgstr "home"
|
||||||
|
|
||||||
@ -1048,12 +1048,12 @@ msgstr "Un club avec ce nom UNIX existe déjà."
|
|||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "nom d'utilisateur"
|
msgstr "nom d'utilisateur"
|
||||||
|
|
||||||
#: club/models.py:354 core/models.py:356 election/models.py:178
|
#: club/models.py:354 core/models.py:357 election/models.py:178
|
||||||
#: election/models.py:212 trombi/models.py:210
|
#: election/models.py:212 trombi/models.py:210
|
||||||
msgid "role"
|
msgid "role"
|
||||||
msgstr "rôle"
|
msgstr "rôle"
|
||||||
|
|
||||||
#: club/models.py:359 core/models.py:89 counter/models.py:298
|
#: club/models.py:359 core/models.py:90 counter/models.py:298
|
||||||
#: counter/models.py:329 election/models.py:13 election/models.py:115
|
#: counter/models.py:329 election/models.py:13 election/models.py:115
|
||||||
#: election/models.py:188 forum/models.py:61 forum/models.py:245
|
#: election/models.py:188 forum/models.py:61 forum/models.py:245
|
||||||
msgid "description"
|
msgid "description"
|
||||||
@ -1068,7 +1068,7 @@ msgid "Enter a valid address. Only the root of the address is needed."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Entrez une adresse valide. Seule la racine de l'adresse est nécessaire."
|
"Entrez une adresse valide. Seule la racine de l'adresse est nécessaire."
|
||||||
|
|
||||||
#: club/models.py:427 com/models.py:82 com/models.py:309 core/models.py:946
|
#: club/models.py:427 com/models.py:82 com/models.py:309 core/models.py:957
|
||||||
msgid "is moderated"
|
msgid "is moderated"
|
||||||
msgstr "est modéré"
|
msgstr "est modéré"
|
||||||
|
|
||||||
@ -1334,6 +1334,7 @@ msgid "No mailing list existing for this club"
|
|||||||
msgstr "Aucune mailing liste n'existe pour ce club"
|
msgstr "Aucune mailing liste n'existe pour ce club"
|
||||||
|
|
||||||
#: club/templates/club/mailing.jinja:72
|
#: club/templates/club/mailing.jinja:72
|
||||||
|
#: subscription/templates/subscription/subscription.jinja:39
|
||||||
msgid "New member"
|
msgid "New member"
|
||||||
msgstr "Nouveau membre"
|
msgstr "Nouveau membre"
|
||||||
|
|
||||||
@ -1439,7 +1440,7 @@ msgstr "résumé"
|
|||||||
msgid "content"
|
msgid "content"
|
||||||
msgstr "contenu"
|
msgstr "contenu"
|
||||||
|
|
||||||
#: com/models.py:71 core/models.py:1499 launderette/models.py:88
|
#: com/models.py:71 core/models.py:1510 launderette/models.py:88
|
||||||
#: launderette/models.py:124 launderette/models.py:167
|
#: launderette/models.py:124 launderette/models.py:167
|
||||||
msgid "type"
|
msgid "type"
|
||||||
msgstr "type"
|
msgstr "type"
|
||||||
@ -1489,7 +1490,7 @@ msgstr "weekmail"
|
|||||||
msgid "rank"
|
msgid "rank"
|
||||||
msgstr "rang"
|
msgstr "rang"
|
||||||
|
|
||||||
#: com/models.py:295 core/models.py:911 core/models.py:961
|
#: com/models.py:295 core/models.py:922 core/models.py:972
|
||||||
msgid "file"
|
msgid "file"
|
||||||
msgstr "fichier"
|
msgstr "fichier"
|
||||||
|
|
||||||
@ -1917,7 +1918,7 @@ msgid "Format: 16:9 | Resolution: 1920x1080"
|
|||||||
msgstr "Format : 16:9 | Résolution : 1920x1080"
|
msgstr "Format : 16:9 | Résolution : 1920x1080"
|
||||||
|
|
||||||
#: com/views.py:78 com/views.py:199 election/views.py:167
|
#: com/views.py:78 com/views.py:199 election/views.py:167
|
||||||
#: subscription/views.py:35
|
#: subscription/forms.py:18
|
||||||
msgid "Start date"
|
msgid "Start date"
|
||||||
msgstr "Date de début"
|
msgstr "Date de début"
|
||||||
|
|
||||||
@ -1972,30 +1973,30 @@ msgstr ""
|
|||||||
"Vous devez êtres un membre du bureau du club sélectionné pour poster dans le "
|
"Vous devez êtres un membre du bureau du club sélectionné pour poster dans le "
|
||||||
"Weekmail."
|
"Weekmail."
|
||||||
|
|
||||||
#: core/models.py:84
|
#: core/models.py:85
|
||||||
msgid "meta group status"
|
msgid "meta group status"
|
||||||
msgstr "status du meta-groupe"
|
msgstr "status du meta-groupe"
|
||||||
|
|
||||||
#: core/models.py:86
|
#: core/models.py:87
|
||||||
msgid "Whether a group is a meta group or not"
|
msgid "Whether a group is a meta group or not"
|
||||||
msgstr "Si un groupe est un meta-groupe ou pas"
|
msgstr "Si un groupe est un meta-groupe ou pas"
|
||||||
|
|
||||||
#: core/models.py:172
|
#: core/models.py:173
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(value)s is not a valid promo (between 0 and %(end)s)"
|
msgid "%(value)s is not a valid promo (between 0 and %(end)s)"
|
||||||
msgstr "%(value)s n'est pas une promo valide (doit être entre 0 et %(end)s)"
|
msgstr "%(value)s n'est pas une promo valide (doit être entre 0 et %(end)s)"
|
||||||
|
|
||||||
#: core/models.py:256
|
#: core/models.py:257
|
||||||
msgid "username"
|
msgid "username"
|
||||||
msgstr "nom d'utilisateur"
|
msgstr "nom d'utilisateur"
|
||||||
|
|
||||||
#: core/models.py:260
|
#: core/models.py:261
|
||||||
msgid "Required. 254 characters or fewer. Letters, digits and ./+/-/_ only."
|
msgid "Required. 254 characters or fewer. Letters, digits and ./+/-/_ only."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Requis. Pas plus de 254 caractères. Uniquement des lettres, numéros, et ./"
|
"Requis. Pas plus de 254 caractères. Uniquement des lettres, numéros, et ./"
|
||||||
"+/-/_"
|
"+/-/_"
|
||||||
|
|
||||||
#: core/models.py:266
|
#: core/models.py:267
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enter a valid username. This value may contain only letters, numbers and ./"
|
"Enter a valid username. This value may contain only letters, numbers and ./"
|
||||||
"+/-/_ characters."
|
"+/-/_ characters."
|
||||||
@ -2003,43 +2004,43 @@ msgstr ""
|
|||||||
"Entrez un nom d'utilisateur correct. Uniquement des lettres, numéros, et ./"
|
"Entrez un nom d'utilisateur correct. Uniquement des lettres, numéros, et ./"
|
||||||
"+/-/_"
|
"+/-/_"
|
||||||
|
|
||||||
#: core/models.py:272
|
#: core/models.py:273
|
||||||
msgid "A user with that username already exists."
|
msgid "A user with that username already exists."
|
||||||
msgstr "Un utilisateur de ce nom existe déjà"
|
msgstr "Un utilisateur de ce nom existe déjà"
|
||||||
|
|
||||||
#: core/models.py:274
|
#: core/models.py:275
|
||||||
msgid "first name"
|
msgid "first name"
|
||||||
msgstr "Prénom"
|
msgstr "Prénom"
|
||||||
|
|
||||||
#: core/models.py:275
|
#: core/models.py:276
|
||||||
msgid "last name"
|
msgid "last name"
|
||||||
msgstr "Nom"
|
msgstr "Nom"
|
||||||
|
|
||||||
#: core/models.py:276
|
#: core/models.py:277
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "adresse email"
|
msgstr "adresse email"
|
||||||
|
|
||||||
#: core/models.py:277
|
#: core/models.py:278
|
||||||
msgid "date of birth"
|
msgid "date of birth"
|
||||||
msgstr "date de naissance"
|
msgstr "date de naissance"
|
||||||
|
|
||||||
#: core/models.py:278
|
#: core/models.py:279
|
||||||
msgid "nick name"
|
msgid "nick name"
|
||||||
msgstr "surnom"
|
msgstr "surnom"
|
||||||
|
|
||||||
#: core/models.py:280
|
#: core/models.py:281
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "status \"staff\""
|
msgstr "status \"staff\""
|
||||||
|
|
||||||
#: core/models.py:282
|
#: core/models.py:283
|
||||||
msgid "Designates whether the user can log into this admin site."
|
msgid "Designates whether the user can log into this admin site."
|
||||||
msgstr "Est-ce que l'utilisateur peut se logger à la partie admin du site."
|
msgstr "Est-ce que l'utilisateur peut se logger à la partie admin du site."
|
||||||
|
|
||||||
#: core/models.py:285
|
#: core/models.py:286
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "actif"
|
msgstr "actif"
|
||||||
|
|
||||||
#: core/models.py:288
|
#: core/models.py:289
|
||||||
msgid ""
|
msgid ""
|
||||||
"Designates whether this user should be treated as active. Unselect this "
|
"Designates whether this user should be treated as active. Unselect this "
|
||||||
"instead of deleting accounts."
|
"instead of deleting accounts."
|
||||||
@ -2047,164 +2048,164 @@ msgstr ""
|
|||||||
"Est-ce que l'utilisateur doit être traité comme actif. Désélectionnez au "
|
"Est-ce que l'utilisateur doit être traité comme actif. Désélectionnez au "
|
||||||
"lieu de supprimer les comptes."
|
"lieu de supprimer les comptes."
|
||||||
|
|
||||||
#: core/models.py:292
|
#: core/models.py:293
|
||||||
msgid "date joined"
|
msgid "date joined"
|
||||||
msgstr "date d'inscription"
|
msgstr "date d'inscription"
|
||||||
|
|
||||||
#: core/models.py:293
|
#: core/models.py:294
|
||||||
msgid "last update"
|
msgid "last update"
|
||||||
msgstr "dernière mise à jour"
|
msgstr "dernière mise à jour"
|
||||||
|
|
||||||
#: core/models.py:295
|
#: core/models.py:296
|
||||||
msgid "superuser"
|
msgid "superuser"
|
||||||
msgstr "super-utilisateur"
|
msgstr "super-utilisateur"
|
||||||
|
|
||||||
#: core/models.py:297
|
#: core/models.py:298
|
||||||
msgid "Designates whether this user is a superuser. "
|
msgid "Designates whether this user is a superuser. "
|
||||||
msgstr "Est-ce que l'utilisateur est super-utilisateur."
|
msgstr "Est-ce que l'utilisateur est super-utilisateur."
|
||||||
|
|
||||||
#: core/models.py:311
|
#: core/models.py:312
|
||||||
msgid "profile"
|
msgid "profile"
|
||||||
msgstr "profil"
|
msgstr "profil"
|
||||||
|
|
||||||
#: core/models.py:319
|
#: core/models.py:320
|
||||||
msgid "avatar"
|
msgid "avatar"
|
||||||
msgstr "avatar"
|
msgstr "avatar"
|
||||||
|
|
||||||
#: core/models.py:327
|
#: core/models.py:328
|
||||||
msgid "scrub"
|
msgid "scrub"
|
||||||
msgstr "blouse"
|
msgstr "blouse"
|
||||||
|
|
||||||
#: core/models.py:333
|
#: core/models.py:334
|
||||||
msgid "sex"
|
msgid "sex"
|
||||||
msgstr "Genre"
|
msgstr "Genre"
|
||||||
|
|
||||||
#: core/models.py:337
|
#: core/models.py:338
|
||||||
msgid "Man"
|
msgid "Man"
|
||||||
msgstr "Homme"
|
msgstr "Homme"
|
||||||
|
|
||||||
#: core/models.py:337
|
#: core/models.py:338
|
||||||
msgid "Woman"
|
msgid "Woman"
|
||||||
msgstr "Femme"
|
msgstr "Femme"
|
||||||
|
|
||||||
#: core/models.py:339
|
#: core/models.py:340
|
||||||
msgid "pronouns"
|
msgid "pronouns"
|
||||||
msgstr "pronoms"
|
msgstr "pronoms"
|
||||||
|
|
||||||
#: core/models.py:341
|
#: core/models.py:342
|
||||||
msgid "tshirt size"
|
msgid "tshirt size"
|
||||||
msgstr "taille de t-shirt"
|
msgstr "taille de t-shirt"
|
||||||
|
|
||||||
#: core/models.py:344
|
#: core/models.py:345
|
||||||
msgid "-"
|
msgid "-"
|
||||||
msgstr "-"
|
msgstr "-"
|
||||||
|
|
||||||
#: core/models.py:345
|
#: core/models.py:346
|
||||||
msgid "XS"
|
msgid "XS"
|
||||||
msgstr "XS"
|
msgstr "XS"
|
||||||
|
|
||||||
#: core/models.py:346
|
#: core/models.py:347
|
||||||
msgid "S"
|
msgid "S"
|
||||||
msgstr "S"
|
msgstr "S"
|
||||||
|
|
||||||
#: core/models.py:347
|
#: core/models.py:348
|
||||||
msgid "M"
|
msgid "M"
|
||||||
msgstr "M"
|
msgstr "M"
|
||||||
|
|
||||||
#: core/models.py:348
|
#: core/models.py:349
|
||||||
msgid "L"
|
msgid "L"
|
||||||
msgstr "L"
|
msgstr "L"
|
||||||
|
|
||||||
#: core/models.py:349
|
#: core/models.py:350
|
||||||
msgid "XL"
|
msgid "XL"
|
||||||
msgstr "XL"
|
msgstr "XL"
|
||||||
|
|
||||||
#: core/models.py:350
|
#: core/models.py:351
|
||||||
msgid "XXL"
|
msgid "XXL"
|
||||||
msgstr "XXL"
|
msgstr "XXL"
|
||||||
|
|
||||||
#: core/models.py:351
|
#: core/models.py:352
|
||||||
msgid "XXXL"
|
msgid "XXXL"
|
||||||
msgstr "XXXL"
|
msgstr "XXXL"
|
||||||
|
|
||||||
#: core/models.py:359
|
#: core/models.py:360
|
||||||
msgid "Student"
|
msgid "Student"
|
||||||
msgstr "Étudiant"
|
msgstr "Étudiant"
|
||||||
|
|
||||||
#: core/models.py:360
|
#: core/models.py:361
|
||||||
msgid "Administrative agent"
|
msgid "Administrative agent"
|
||||||
msgstr "Personnel administratif"
|
msgstr "Personnel administratif"
|
||||||
|
|
||||||
#: core/models.py:361
|
#: core/models.py:362
|
||||||
msgid "Teacher"
|
msgid "Teacher"
|
||||||
msgstr "Enseignant"
|
msgstr "Enseignant"
|
||||||
|
|
||||||
#: core/models.py:362
|
#: core/models.py:363
|
||||||
msgid "Agent"
|
msgid "Agent"
|
||||||
msgstr "Personnel"
|
msgstr "Personnel"
|
||||||
|
|
||||||
#: core/models.py:363
|
#: core/models.py:364
|
||||||
msgid "Doctor"
|
msgid "Doctor"
|
||||||
msgstr "Doctorant"
|
msgstr "Doctorant"
|
||||||
|
|
||||||
#: core/models.py:364
|
#: core/models.py:365
|
||||||
msgid "Former student"
|
msgid "Former student"
|
||||||
msgstr "Ancien étudiant"
|
msgstr "Ancien étudiant"
|
||||||
|
|
||||||
#: core/models.py:365
|
#: core/models.py:366
|
||||||
msgid "Service"
|
msgid "Service"
|
||||||
msgstr "Service"
|
msgstr "Service"
|
||||||
|
|
||||||
#: core/models.py:371
|
#: core/models.py:372
|
||||||
msgid "department"
|
msgid "department"
|
||||||
msgstr "département"
|
msgstr "département"
|
||||||
|
|
||||||
#: core/models.py:378
|
#: core/models.py:379
|
||||||
msgid "dpt option"
|
msgid "dpt option"
|
||||||
msgstr "Filière"
|
msgstr "Filière"
|
||||||
|
|
||||||
#: core/models.py:380 pedagogy/models.py:70 pedagogy/models.py:294
|
#: core/models.py:381 pedagogy/models.py:70 pedagogy/models.py:294
|
||||||
msgid "semester"
|
msgid "semester"
|
||||||
msgstr "semestre"
|
msgstr "semestre"
|
||||||
|
|
||||||
#: core/models.py:381
|
#: core/models.py:382
|
||||||
msgid "quote"
|
msgid "quote"
|
||||||
msgstr "citation"
|
msgstr "citation"
|
||||||
|
|
||||||
#: core/models.py:382
|
#: core/models.py:383
|
||||||
msgid "school"
|
msgid "school"
|
||||||
msgstr "école"
|
msgstr "école"
|
||||||
|
|
||||||
#: core/models.py:384
|
#: core/models.py:385
|
||||||
msgid "promo"
|
msgid "promo"
|
||||||
msgstr "promo"
|
msgstr "promo"
|
||||||
|
|
||||||
#: core/models.py:387
|
#: core/models.py:388
|
||||||
msgid "forum signature"
|
msgid "forum signature"
|
||||||
msgstr "signature du forum"
|
msgstr "signature du forum"
|
||||||
|
|
||||||
#: core/models.py:389
|
#: core/models.py:390
|
||||||
msgid "second email address"
|
msgid "second email address"
|
||||||
msgstr "adresse email secondaire"
|
msgstr "adresse email secondaire"
|
||||||
|
|
||||||
#: core/models.py:391
|
#: core/models.py:392
|
||||||
msgid "parent phone"
|
msgid "parent phone"
|
||||||
msgstr "téléphone des parents"
|
msgstr "téléphone des parents"
|
||||||
|
|
||||||
#: core/models.py:394
|
#: core/models.py:395
|
||||||
msgid "parent address"
|
msgid "parent address"
|
||||||
msgstr "adresse des parents"
|
msgstr "adresse des parents"
|
||||||
|
|
||||||
#: core/models.py:397
|
#: core/models.py:398
|
||||||
msgid "is subscriber viewable"
|
msgid "is subscriber viewable"
|
||||||
msgstr "profil visible par les cotisants"
|
msgstr "profil visible par les cotisants"
|
||||||
|
|
||||||
#: core/models.py:591
|
#: core/models.py:594
|
||||||
msgid "A user with that username already exists"
|
msgid "A user with that username already exists"
|
||||||
msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
|
msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
|
||||||
|
|
||||||
#: core/models.py:750 core/templates/core/macros.jinja:75
|
#: core/models.py:761 core/templates/core/macros.jinja:80
|
||||||
#: core/templates/core/macros.jinja:77 core/templates/core/macros.jinja:78
|
#: core/templates/core/macros.jinja:84 core/templates/core/macros.jinja:85
|
||||||
#: core/templates/core/user_detail.jinja:100
|
#: core/templates/core/user_detail.jinja:100
|
||||||
#: core/templates/core/user_detail.jinja:101
|
#: core/templates/core/user_detail.jinja:101
|
||||||
#: core/templates/core/user_detail.jinja:103
|
#: core/templates/core/user_detail.jinja:103
|
||||||
@ -2214,8 +2215,8 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
|
|||||||
#: core/templates/core/user_detail.jinja:112
|
#: core/templates/core/user_detail.jinja:112
|
||||||
#: core/templates/core/user_detail.jinja:113
|
#: core/templates/core/user_detail.jinja:113
|
||||||
#: core/templates/core/user_edit.jinja:21
|
#: core/templates/core/user_edit.jinja:21
|
||||||
#: election/templates/election/election_detail.jinja:132
|
#: election/templates/election/election_detail.jinja:136
|
||||||
#: election/templates/election/election_detail.jinja:134
|
#: election/templates/election/election_detail.jinja:138
|
||||||
#: forum/templates/forum/macros.jinja:105
|
#: forum/templates/forum/macros.jinja:105
|
||||||
#: forum/templates/forum/macros.jinja:107
|
#: forum/templates/forum/macros.jinja:107
|
||||||
#: forum/templates/forum/macros.jinja:109
|
#: forum/templates/forum/macros.jinja:109
|
||||||
@ -2223,101 +2224,101 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
|
|||||||
msgid "Profile"
|
msgid "Profile"
|
||||||
msgstr "Profil"
|
msgstr "Profil"
|
||||||
|
|
||||||
#: core/models.py:861
|
#: core/models.py:872
|
||||||
msgid "Visitor"
|
msgid "Visitor"
|
||||||
msgstr "Visiteur"
|
msgstr "Visiteur"
|
||||||
|
|
||||||
#: core/models.py:868
|
#: core/models.py:879
|
||||||
msgid "receive the Weekmail"
|
msgid "receive the Weekmail"
|
||||||
msgstr "recevoir le Weekmail"
|
msgstr "recevoir le Weekmail"
|
||||||
|
|
||||||
#: core/models.py:869
|
#: core/models.py:880
|
||||||
msgid "show your stats to others"
|
msgid "show your stats to others"
|
||||||
msgstr "montrez vos statistiques aux autres"
|
msgstr "montrez vos statistiques aux autres"
|
||||||
|
|
||||||
#: core/models.py:871
|
#: core/models.py:882
|
||||||
msgid "get a notification for every click"
|
msgid "get a notification for every click"
|
||||||
msgstr "avoir une notification pour chaque click"
|
msgstr "avoir une notification pour chaque click"
|
||||||
|
|
||||||
#: core/models.py:874
|
#: core/models.py:885
|
||||||
msgid "get a notification for every refilling"
|
msgid "get a notification for every refilling"
|
||||||
msgstr "avoir une notification pour chaque rechargement"
|
msgstr "avoir une notification pour chaque rechargement"
|
||||||
|
|
||||||
#: core/models.py:900 sas/forms.py:81
|
#: core/models.py:911 sas/forms.py:81
|
||||||
msgid "file name"
|
msgid "file name"
|
||||||
msgstr "nom du fichier"
|
msgstr "nom du fichier"
|
||||||
|
|
||||||
#: core/models.py:904 core/models.py:1257
|
#: core/models.py:915 core/models.py:1268
|
||||||
msgid "parent"
|
msgid "parent"
|
||||||
msgstr "parent"
|
msgstr "parent"
|
||||||
|
|
||||||
#: core/models.py:918
|
#: core/models.py:929
|
||||||
msgid "compressed file"
|
msgid "compressed file"
|
||||||
msgstr "version allégée"
|
msgstr "version allégée"
|
||||||
|
|
||||||
#: core/models.py:925
|
#: core/models.py:936
|
||||||
msgid "thumbnail"
|
msgid "thumbnail"
|
||||||
msgstr "miniature"
|
msgstr "miniature"
|
||||||
|
|
||||||
#: core/models.py:933 core/models.py:950
|
#: core/models.py:944 core/models.py:961
|
||||||
msgid "owner"
|
msgid "owner"
|
||||||
msgstr "propriétaire"
|
msgstr "propriétaire"
|
||||||
|
|
||||||
#: core/models.py:937 core/models.py:1274
|
#: core/models.py:948 core/models.py:1285
|
||||||
msgid "edit group"
|
msgid "edit group"
|
||||||
msgstr "groupe d'édition"
|
msgstr "groupe d'édition"
|
||||||
|
|
||||||
#: core/models.py:940 core/models.py:1277
|
#: core/models.py:951 core/models.py:1288
|
||||||
msgid "view group"
|
msgid "view group"
|
||||||
msgstr "groupe de vue"
|
msgstr "groupe de vue"
|
||||||
|
|
||||||
#: core/models.py:942
|
#: core/models.py:953
|
||||||
msgid "is folder"
|
msgid "is folder"
|
||||||
msgstr "est un dossier"
|
msgstr "est un dossier"
|
||||||
|
|
||||||
#: core/models.py:943
|
#: core/models.py:954
|
||||||
msgid "mime type"
|
msgid "mime type"
|
||||||
msgstr "type mime"
|
msgstr "type mime"
|
||||||
|
|
||||||
#: core/models.py:944
|
#: core/models.py:955
|
||||||
msgid "size"
|
msgid "size"
|
||||||
msgstr "taille"
|
msgstr "taille"
|
||||||
|
|
||||||
#: core/models.py:955
|
#: core/models.py:966
|
||||||
msgid "asked for removal"
|
msgid "asked for removal"
|
||||||
msgstr "retrait demandé"
|
msgstr "retrait demandé"
|
||||||
|
|
||||||
#: core/models.py:957
|
#: core/models.py:968
|
||||||
msgid "is in the SAS"
|
msgid "is in the SAS"
|
||||||
msgstr "est dans le SAS"
|
msgstr "est dans le SAS"
|
||||||
|
|
||||||
#: core/models.py:1026
|
#: core/models.py:1037
|
||||||
msgid "Character '/' not authorized in name"
|
msgid "Character '/' not authorized in name"
|
||||||
msgstr "Le caractère '/' n'est pas autorisé dans les noms de fichier"
|
msgstr "Le caractère '/' n'est pas autorisé dans les noms de fichier"
|
||||||
|
|
||||||
#: core/models.py:1028 core/models.py:1032
|
#: core/models.py:1039 core/models.py:1043
|
||||||
msgid "Loop in folder tree"
|
msgid "Loop in folder tree"
|
||||||
msgstr "Boucle dans l'arborescence des dossiers"
|
msgstr "Boucle dans l'arborescence des dossiers"
|
||||||
|
|
||||||
#: core/models.py:1035
|
#: core/models.py:1046
|
||||||
msgid "You can not make a file be a children of a non folder file"
|
msgid "You can not make a file be a children of a non folder file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Vous ne pouvez pas mettre un fichier enfant de quelque chose qui n'est pas "
|
"Vous ne pouvez pas mettre un fichier enfant de quelque chose qui n'est pas "
|
||||||
"un dossier"
|
"un dossier"
|
||||||
|
|
||||||
#: core/models.py:1046
|
#: core/models.py:1057
|
||||||
msgid "Duplicate file"
|
msgid "Duplicate file"
|
||||||
msgstr "Un fichier de ce nom existe déjà"
|
msgstr "Un fichier de ce nom existe déjà"
|
||||||
|
|
||||||
#: core/models.py:1063
|
#: core/models.py:1074
|
||||||
msgid "You must provide a file"
|
msgid "You must provide a file"
|
||||||
msgstr "Vous devez fournir un fichier"
|
msgstr "Vous devez fournir un fichier"
|
||||||
|
|
||||||
#: core/models.py:1240
|
#: core/models.py:1251
|
||||||
msgid "page unix name"
|
msgid "page unix name"
|
||||||
msgstr "nom unix de la page"
|
msgstr "nom unix de la page"
|
||||||
|
|
||||||
#: core/models.py:1246
|
#: core/models.py:1257
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enter a valid page name. This value may contain only unaccented letters, "
|
"Enter a valid page name. This value may contain only unaccented letters, "
|
||||||
"numbers and ./+/-/_ characters."
|
"numbers and ./+/-/_ characters."
|
||||||
@ -2325,55 +2326,55 @@ msgstr ""
|
|||||||
"Entrez un nom de page correct. Uniquement des lettres non accentuées, "
|
"Entrez un nom de page correct. Uniquement des lettres non accentuées, "
|
||||||
"numéros, et ./+/-/_"
|
"numéros, et ./+/-/_"
|
||||||
|
|
||||||
#: core/models.py:1264
|
#: core/models.py:1275
|
||||||
msgid "page name"
|
msgid "page name"
|
||||||
msgstr "nom de la page"
|
msgstr "nom de la page"
|
||||||
|
|
||||||
#: core/models.py:1269
|
#: core/models.py:1280
|
||||||
msgid "owner group"
|
msgid "owner group"
|
||||||
msgstr "groupe propriétaire"
|
msgstr "groupe propriétaire"
|
||||||
|
|
||||||
#: core/models.py:1282
|
#: core/models.py:1293
|
||||||
msgid "lock user"
|
msgid "lock user"
|
||||||
msgstr "utilisateur bloquant"
|
msgstr "utilisateur bloquant"
|
||||||
|
|
||||||
#: core/models.py:1289
|
#: core/models.py:1300
|
||||||
msgid "lock_timeout"
|
msgid "lock_timeout"
|
||||||
msgstr "décompte du déblocage"
|
msgstr "décompte du déblocage"
|
||||||
|
|
||||||
#: core/models.py:1339
|
#: core/models.py:1350
|
||||||
msgid "Duplicate page"
|
msgid "Duplicate page"
|
||||||
msgstr "Une page de ce nom existe déjà"
|
msgstr "Une page de ce nom existe déjà"
|
||||||
|
|
||||||
#: core/models.py:1342
|
#: core/models.py:1353
|
||||||
msgid "Loop in page tree"
|
msgid "Loop in page tree"
|
||||||
msgstr "Boucle dans l'arborescence des pages"
|
msgstr "Boucle dans l'arborescence des pages"
|
||||||
|
|
||||||
#: core/models.py:1453
|
#: core/models.py:1464
|
||||||
msgid "revision"
|
msgid "revision"
|
||||||
msgstr "révision"
|
msgstr "révision"
|
||||||
|
|
||||||
#: core/models.py:1454
|
#: core/models.py:1465
|
||||||
msgid "page title"
|
msgid "page title"
|
||||||
msgstr "titre de la page"
|
msgstr "titre de la page"
|
||||||
|
|
||||||
#: core/models.py:1455
|
#: core/models.py:1466
|
||||||
msgid "page content"
|
msgid "page content"
|
||||||
msgstr "contenu de la page"
|
msgstr "contenu de la page"
|
||||||
|
|
||||||
#: core/models.py:1496
|
#: core/models.py:1507
|
||||||
msgid "url"
|
msgid "url"
|
||||||
msgstr "url"
|
msgstr "url"
|
||||||
|
|
||||||
#: core/models.py:1497
|
#: core/models.py:1508
|
||||||
msgid "param"
|
msgid "param"
|
||||||
msgstr "param"
|
msgstr "param"
|
||||||
|
|
||||||
#: core/models.py:1502
|
#: core/models.py:1513
|
||||||
msgid "viewed"
|
msgid "viewed"
|
||||||
msgstr "vue"
|
msgstr "vue"
|
||||||
|
|
||||||
#: core/models.py:1560
|
#: core/models.py:1571
|
||||||
msgid "operation type"
|
msgid "operation type"
|
||||||
msgstr "type d'opération"
|
msgstr "type d'opération"
|
||||||
|
|
||||||
@ -2393,27 +2394,27 @@ msgstr "500, Erreur Serveur"
|
|||||||
msgid "Welcome!"
|
msgid "Welcome!"
|
||||||
msgstr "Bienvenue !"
|
msgstr "Bienvenue !"
|
||||||
|
|
||||||
#: core/templates/core/base.jinja:104 core/templates/core/base/navbar.jinja:43
|
#: core/templates/core/base.jinja:105 core/templates/core/base/navbar.jinja:43
|
||||||
msgid "Contacts"
|
msgid "Contacts"
|
||||||
msgstr "Contacts"
|
msgstr "Contacts"
|
||||||
|
|
||||||
#: core/templates/core/base.jinja:105
|
#: core/templates/core/base.jinja:106
|
||||||
msgid "Legal notices"
|
msgid "Legal notices"
|
||||||
msgstr "Mentions légales"
|
msgstr "Mentions légales"
|
||||||
|
|
||||||
#: core/templates/core/base.jinja:106
|
#: core/templates/core/base.jinja:107
|
||||||
msgid "Intellectual property"
|
msgid "Intellectual property"
|
||||||
msgstr "Propriété intellectuelle"
|
msgstr "Propriété intellectuelle"
|
||||||
|
|
||||||
#: core/templates/core/base.jinja:107
|
#: core/templates/core/base.jinja:108
|
||||||
msgid "Help & Documentation"
|
msgid "Help & Documentation"
|
||||||
msgstr "Aide & Documentation"
|
msgstr "Aide & Documentation"
|
||||||
|
|
||||||
#: core/templates/core/base.jinja:108
|
#: core/templates/core/base.jinja:109
|
||||||
msgid "R&D"
|
msgid "R&D"
|
||||||
msgstr "R&D"
|
msgstr "R&D"
|
||||||
|
|
||||||
#: core/templates/core/base.jinja:111
|
#: core/templates/core/base.jinja:112
|
||||||
msgid "Site created by the IT Department of the AE"
|
msgid "Site created by the IT Department of the AE"
|
||||||
msgstr "Site réalisé par le Pôle Informatique de l'AE"
|
msgstr "Site réalisé par le Pôle Informatique de l'AE"
|
||||||
|
|
||||||
@ -2752,29 +2753,29 @@ msgstr "Partager sur Facebook"
|
|||||||
msgid "Tweet"
|
msgid "Tweet"
|
||||||
msgstr "Tweeter"
|
msgstr "Tweeter"
|
||||||
|
|
||||||
#: core/templates/core/macros.jinja:85
|
#: core/templates/core/macros.jinja:93
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Subscribed until %(subscription_end)s"
|
msgid "Subscribed until %(subscription_end)s"
|
||||||
msgstr "Cotisant jusqu'au %(subscription_end)s"
|
msgstr "Cotisant jusqu'au %(subscription_end)s"
|
||||||
|
|
||||||
#: core/templates/core/macros.jinja:86
|
#: core/templates/core/macros.jinja:94
|
||||||
msgid "Account number: "
|
msgid "Account number: "
|
||||||
msgstr "Numéro de compte : "
|
msgstr "Numéro de compte : "
|
||||||
|
|
||||||
#: core/templates/core/macros.jinja:91 launderette/models.py:188
|
#: core/templates/core/macros.jinja:99 launderette/models.py:188
|
||||||
msgid "Slot"
|
msgid "Slot"
|
||||||
msgstr "Créneau"
|
msgstr "Créneau"
|
||||||
|
|
||||||
#: core/templates/core/macros.jinja:104
|
#: core/templates/core/macros.jinja:112
|
||||||
#: launderette/templates/launderette/launderette_admin.jinja:20
|
#: launderette/templates/launderette/launderette_admin.jinja:20
|
||||||
msgid "Tokens"
|
msgid "Tokens"
|
||||||
msgstr "Jetons"
|
msgstr "Jetons"
|
||||||
|
|
||||||
#: core/templates/core/macros.jinja:258
|
#: core/templates/core/macros.jinja:266
|
||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr "Tout sélectionner"
|
msgstr "Tout sélectionner"
|
||||||
|
|
||||||
#: core/templates/core/macros.jinja:259
|
#: core/templates/core/macros.jinja:267
|
||||||
msgid "Unselect All"
|
msgid "Unselect All"
|
||||||
msgstr "Tout désélectionner"
|
msgstr "Tout désélectionner"
|
||||||
|
|
||||||
@ -3135,8 +3136,8 @@ msgid "Not subscribed"
|
|||||||
msgstr "Non cotisant"
|
msgstr "Non cotisant"
|
||||||
|
|
||||||
#: core/templates/core/user_detail.jinja:162
|
#: core/templates/core/user_detail.jinja:162
|
||||||
#: subscription/templates/subscription/subscription.jinja:4
|
#: subscription/templates/subscription/subscription.jinja:6
|
||||||
#: subscription/templates/subscription/subscription.jinja:8
|
#: subscription/templates/subscription/subscription.jinja:37
|
||||||
msgid "New subscription"
|
msgid "New subscription"
|
||||||
msgstr "Nouvelle cotisation"
|
msgstr "Nouvelle cotisation"
|
||||||
|
|
||||||
@ -4512,7 +4513,7 @@ msgstr "candidature"
|
|||||||
|
|
||||||
#: election/templates/election/candidate_form.jinja:4
|
#: election/templates/election/candidate_form.jinja:4
|
||||||
#: election/templates/election/candidate_form.jinja:13
|
#: election/templates/election/candidate_form.jinja:13
|
||||||
#: election/templates/election/election_detail.jinja:175
|
#: election/templates/election/election_detail.jinja:179
|
||||||
msgid "Candidate"
|
msgid "Candidate"
|
||||||
msgstr "Candidater"
|
msgstr "Candidater"
|
||||||
|
|
||||||
@ -4520,20 +4521,20 @@ msgstr "Candidater"
|
|||||||
msgid "Candidature are closed for this election"
|
msgid "Candidature are closed for this election"
|
||||||
msgstr "Les candidatures sont fermées pour cette élection"
|
msgstr "Les candidatures sont fermées pour cette élection"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:19
|
#: election/templates/election/election_detail.jinja:23
|
||||||
msgid "Polls close "
|
msgid "Polls close "
|
||||||
msgstr "Votes fermés"
|
msgstr "Votes fermés"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:21
|
#: election/templates/election/election_detail.jinja:25
|
||||||
msgid "Polls closed "
|
msgid "Polls closed "
|
||||||
msgstr "Votes fermés"
|
msgstr "Votes fermés"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:23
|
#: election/templates/election/election_detail.jinja:27
|
||||||
msgid "Polls will open "
|
msgid "Polls will open "
|
||||||
msgstr "Les votes ouvriront "
|
msgstr "Les votes ouvriront "
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:25
|
|
||||||
#: election/templates/election/election_detail.jinja:29
|
#: election/templates/election/election_detail.jinja:29
|
||||||
|
#: election/templates/election/election_detail.jinja:33
|
||||||
#: election/templates/election/election_list.jinja:32
|
#: election/templates/election/election_list.jinja:32
|
||||||
#: election/templates/election/election_list.jinja:35
|
#: election/templates/election/election_list.jinja:35
|
||||||
#: election/templates/election/election_list.jinja:40
|
#: election/templates/election/election_list.jinja:40
|
||||||
@ -4542,58 +4543,58 @@ msgstr "Les votes ouvriront "
|
|||||||
msgid " at "
|
msgid " at "
|
||||||
msgstr " à "
|
msgstr " à "
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:26
|
#: election/templates/election/election_detail.jinja:30
|
||||||
msgid "and will close "
|
msgid "and will close "
|
||||||
msgstr "et fermeront"
|
msgstr "et fermeront"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:34
|
#: election/templates/election/election_detail.jinja:38
|
||||||
msgid "You already have submitted your vote."
|
msgid "You already have submitted your vote."
|
||||||
msgstr "Vous avez déjà soumis votre vote."
|
msgstr "Vous avez déjà soumis votre vote."
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:36
|
#: election/templates/election/election_detail.jinja:40
|
||||||
msgid "You have voted in this election."
|
msgid "You have voted in this election."
|
||||||
msgstr "Vous avez déjà voté pour cette élection."
|
msgstr "Vous avez déjà voté pour cette élection."
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:49 election/views.py:98
|
#: election/templates/election/election_detail.jinja:53 election/views.py:98
|
||||||
msgid "Blank vote"
|
msgid "Blank vote"
|
||||||
msgstr "Vote blanc"
|
msgstr "Vote blanc"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:71
|
#: election/templates/election/election_detail.jinja:75
|
||||||
msgid "You may choose up to"
|
msgid "You may choose up to"
|
||||||
msgstr "Vous pouvez choisir jusqu'à"
|
msgstr "Vous pouvez choisir jusqu'à"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:71
|
#: election/templates/election/election_detail.jinja:75
|
||||||
msgid "people."
|
msgid "people."
|
||||||
msgstr "personne(s)"
|
msgstr "personne(s)"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:108
|
#: election/templates/election/election_detail.jinja:112
|
||||||
msgid "Choose blank vote"
|
msgid "Choose blank vote"
|
||||||
msgstr "Choisir de voter blanc"
|
msgstr "Choisir de voter blanc"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:116
|
#: election/templates/election/election_detail.jinja:120
|
||||||
#: election/templates/election/election_detail.jinja:159
|
#: election/templates/election/election_detail.jinja:163
|
||||||
msgid "votes"
|
msgid "votes"
|
||||||
msgstr "votes"
|
msgstr "votes"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:178
|
#: election/templates/election/election_detail.jinja:182
|
||||||
msgid "Add a new list"
|
msgid "Add a new list"
|
||||||
msgstr "Ajouter une nouvelle liste"
|
msgstr "Ajouter une nouvelle liste"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:182
|
#: election/templates/election/election_detail.jinja:186
|
||||||
msgid "Add a new role"
|
msgid "Add a new role"
|
||||||
msgstr "Ajouter un nouveau rôle"
|
msgstr "Ajouter un nouveau rôle"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:192
|
#: election/templates/election/election_detail.jinja:196
|
||||||
msgid "Submit the vote !"
|
msgid "Submit the vote !"
|
||||||
msgstr "Envoyer le vote !"
|
msgstr "Envoyer le vote !"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:201
|
#: election/templates/election/election_detail.jinja:205
|
||||||
#: election/templates/election/election_detail.jinja:206
|
#: election/templates/election/election_detail.jinja:210
|
||||||
msgid "Show more"
|
msgid "Show more"
|
||||||
msgstr "Montrer plus"
|
msgstr "Montrer plus"
|
||||||
|
|
||||||
#: election/templates/election/election_detail.jinja:202
|
#: election/templates/election/election_detail.jinja:206
|
||||||
#: election/templates/election/election_detail.jinja:207
|
#: election/templates/election/election_detail.jinja:211
|
||||||
msgid "Show less"
|
msgid "Show less"
|
||||||
msgstr "Montrer moins"
|
msgstr "Montrer moins"
|
||||||
|
|
||||||
@ -5790,6 +5791,10 @@ msgstr "Weekmail envoyé avec succès"
|
|||||||
msgid "AE tee-shirt"
|
msgid "AE tee-shirt"
|
||||||
msgstr "Tee-shirt AE"
|
msgstr "Tee-shirt AE"
|
||||||
|
|
||||||
|
#: subscription/forms.py:93
|
||||||
|
msgid "A user with that email address already exists"
|
||||||
|
msgstr "Un utilisateur avec cette adresse email existe déjà"
|
||||||
|
|
||||||
#: subscription/models.py:34
|
#: subscription/models.py:34
|
||||||
msgid "Bad subscription type"
|
msgid "Bad subscription type"
|
||||||
msgstr "Mauvais type de cotisation"
|
msgstr "Mauvais type de cotisation"
|
||||||
@ -5814,10 +5819,36 @@ msgstr "fin de la cotisation"
|
|||||||
msgid "location"
|
msgid "location"
|
||||||
msgstr "lieu"
|
msgstr "lieu"
|
||||||
|
|
||||||
#: subscription/models.py:106
|
#: subscription/models.py:107
|
||||||
msgid "You can not subscribe many time for the same period"
|
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"
|
msgstr "Vous ne pouvez pas cotiser plusieurs fois pour la même période"
|
||||||
|
|
||||||
|
#: subscription/templates/subscription/fragments/creation_success.jinja:4
|
||||||
|
#, python-format
|
||||||
|
msgid "Subscription created for %(user)s"
|
||||||
|
msgstr "Cotisation créée pour %(user)s"
|
||||||
|
|
||||||
|
#: subscription/templates/subscription/fragments/creation_success.jinja:8
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"%(user)s received its new %(type)s subscription. It will be active until "
|
||||||
|
"%(end)s included."
|
||||||
|
msgstr ""
|
||||||
|
"%(user)s a reçu sa nouvelle cotisaton %(type)s. Elle sert active jusqu'au "
|
||||||
|
"%(end)s inclu."
|
||||||
|
|
||||||
|
#: subscription/templates/subscription/fragments/creation_success.jinja:16
|
||||||
|
msgid "Go to user profile"
|
||||||
|
msgstr "Voir le profil de l'utilisateur"
|
||||||
|
|
||||||
|
#: subscription/templates/subscription/fragments/creation_success.jinja:24
|
||||||
|
msgid "Create another subscription"
|
||||||
|
msgstr "Créer une nouvelle cotisation"
|
||||||
|
|
||||||
|
#: subscription/templates/subscription/subscription.jinja
|
||||||
|
msgid "Existing member"
|
||||||
|
msgstr "Membre existant"
|
||||||
|
|
||||||
#: subscription/templates/subscription/stats.jinja:27
|
#: subscription/templates/subscription/stats.jinja:27
|
||||||
msgid "Total subscriptions"
|
msgid "Total subscriptions"
|
||||||
msgstr "Cotisations totales"
|
msgstr "Cotisations totales"
|
||||||
@ -5826,20 +5857,6 @@ msgstr "Cotisations totales"
|
|||||||
msgid "Subscriptions by type"
|
msgid "Subscriptions by type"
|
||||||
msgstr "Cotisations par type"
|
msgstr "Cotisations par type"
|
||||||
|
|
||||||
#: subscription/templates/subscription/subscription.jinja:23
|
|
||||||
msgid "Eboutic is reserved to specific users. In doubt, don't use it."
|
|
||||||
msgstr ""
|
|
||||||
"Eboutic est réservé à des cas particuliers. Dans le doute, ne l'utilisez pas."
|
|
||||||
|
|
||||||
#: subscription/views.py:78
|
|
||||||
msgid "A user with that email address already exists"
|
|
||||||
msgstr "Un utilisateur avec cette adresse email existe déjà"
|
|
||||||
|
|
||||||
#: subscription/views.py:102
|
|
||||||
msgid "You must either choose an existing user or create a new one properly"
|
|
||||||
msgstr ""
|
|
||||||
"Vous devez soit choisir un utilisateur existant, soit en créer un proprement"
|
|
||||||
|
|
||||||
#: trombi/models.py:55
|
#: trombi/models.py:55
|
||||||
msgid "subscription deadline"
|
msgid "subscription deadline"
|
||||||
msgstr "fin des inscriptions"
|
msgstr "fin des inscriptions"
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
from pydantic import TypeAdapter
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
from core.views.widgets.select import (
|
from core.views.widgets.select import AutoCompleteSelect, AutoCompleteSelectMultiple
|
||||||
AutoCompleteSelect,
|
|
||||||
AutoCompleteSelectMultiple,
|
|
||||||
)
|
|
||||||
from sas.models import Album
|
from sas.models import Album
|
||||||
from sas.schemas import AlbumSchema
|
from sas.schemas import AlbumSchema
|
||||||
|
|
||||||
|
124
subscription/forms.py
Normal file
124
subscription/forms.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import secrets
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from core.models import User
|
||||||
|
from core.views.forms import SelectDate, SelectDateTime
|
||||||
|
from core.views.widgets.select import AutoCompleteSelectUser
|
||||||
|
from subscription.models import Subscription
|
||||||
|
|
||||||
|
|
||||||
|
class SelectionDateForm(forms.Form):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["start_date"] = forms.DateTimeField(
|
||||||
|
label=_("Start date"), widget=SelectDateTime, required=True
|
||||||
|
)
|
||||||
|
self.fields["end_date"] = forms.DateTimeField(
|
||||||
|
label=_("End date"), widget=SelectDateTime, required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
initial = kwargs.pop("initial", {})
|
||||||
|
if "subscription_type" not in initial:
|
||||||
|
initial["subscription_type"] = "deux-semestres"
|
||||||
|
if "payment_method" not in initial:
|
||||||
|
initial["payment_method"] = "CARD"
|
||||||
|
super().__init__(*args, initial=initial, **kwargs)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.errors:
|
||||||
|
# let django deal with the error messages
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
duration, user = self.instance.semester_duration, self.instance.member
|
||||||
|
self.instance.subscription_start = self.instance.compute_start(
|
||||||
|
duration=duration, user=user
|
||||||
|
)
|
||||||
|
self.instance.subscription_end = self.instance.compute_end(
|
||||||
|
duration=duration, start=self.instance.subscription_start, user=user
|
||||||
|
)
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionNewUserForm(SubscriptionForm):
|
||||||
|
"""Form to create subscriptions with the user they belong to.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```py
|
||||||
|
assert not User.objects.filter(email=request.POST.get("email")).exists()
|
||||||
|
form = SubscriptionNewUserForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
|
||||||
|
# now the user exists and is subscribed
|
||||||
|
user = User.objects.get(email=request.POST.get("email"))
|
||||||
|
assert user.is_subscribed
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_name = "subscription/forms/create_new_user.html"
|
||||||
|
|
||||||
|
__user_fields = forms.fields_for_model(
|
||||||
|
User,
|
||||||
|
["first_name", "last_name", "email", "date_of_birth"],
|
||||||
|
widgets={"date_of_birth": SelectDate},
|
||||||
|
)
|
||||||
|
first_name = __user_fields["first_name"]
|
||||||
|
last_name = __user_fields["last_name"]
|
||||||
|
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",
|
||||||
|
"email",
|
||||||
|
"date_of_birth",
|
||||||
|
"subscription_type",
|
||||||
|
"payment_method",
|
||||||
|
"location",
|
||||||
|
]
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
email = self.cleaned_data["email"]
|
||||||
|
if User.objects.filter(email=email).exists():
|
||||||
|
raise ValidationError(_("A user with that email address already exists"))
|
||||||
|
return email
|
||||||
|
|
||||||
|
def clean(self) -> dict[str, Any]:
|
||||||
|
member = User(
|
||||||
|
first_name=self.cleaned_data.get("first_name"),
|
||||||
|
last_name=self.cleaned_data.get("last_name"),
|
||||||
|
email=self.cleaned_data.get("email"),
|
||||||
|
date_of_birth=self.cleaned_data.get("date_of_birth"),
|
||||||
|
)
|
||||||
|
member.generate_username()
|
||||||
|
member.set_password(secrets.token_urlsafe(nbytes=10))
|
||||||
|
self.instance.member = member
|
||||||
|
return super().clean()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.errors:
|
||||||
|
# let django deal with the error messages
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
self.instance.member.save()
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionExistingUserForm(SubscriptionForm):
|
||||||
|
"""Form to add a subscription to an existing user."""
|
||||||
|
|
||||||
|
template_name = "subscription/forms/create_existing_user.html"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Subscription
|
||||||
|
fields = ["member", "subscription_type", "payment_method", "location"]
|
||||||
|
widgets = {"member": AutoCompleteSelectUser}
|
@ -93,22 +93,23 @@ class Subscription(models.Model):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
today = localdate()
|
today = localdate()
|
||||||
active_subscriptions = Subscription.objects.exclude(pk=self.pk).filter(
|
threshold = timedelta(weeks=settings.SITH_SUBSCRIPTION_END)
|
||||||
subscription_start__gte=today, subscription_end__lte=today
|
# a user may subscribe if :
|
||||||
|
# - he/she is not currently subscribed
|
||||||
|
# - its current subscription ends in less than a few weeks
|
||||||
|
overlapping_subscriptions = Subscription.objects.exclude(pk=self.pk).filter(
|
||||||
|
member=self.member,
|
||||||
|
subscription_start__lte=today,
|
||||||
|
subscription_end__gte=today + threshold,
|
||||||
)
|
)
|
||||||
for s in active_subscriptions:
|
if overlapping_subscriptions.exists():
|
||||||
if (
|
raise ValidationError(
|
||||||
s.is_valid_now()
|
_("You can not subscribe many time for the same period")
|
||||||
and s.subscription_end - timedelta(weeks=settings.SITH_SUBSCRIPTION_END)
|
)
|
||||||
> date.today()
|
|
||||||
):
|
|
||||||
raise ValidationError(
|
|
||||||
_("You can not subscribe many time for the same period")
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compute_start(
|
def compute_start(
|
||||||
d: date | None = None, duration: int = 1, user: User | None = None
|
d: date | None = None, duration: int | float = 1, user: User | None = None
|
||||||
) -> date:
|
) -> date:
|
||||||
"""Computes the start date of the subscription.
|
"""Computes the start date of the subscription.
|
||||||
|
|
||||||
@ -132,7 +133,7 @@ class Subscription(models.Model):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compute_end(
|
def compute_end(
|
||||||
duration: int, start: date | None = None, user: User | None = None
|
duration: int | float, start: date | None = None, user: User | None = None
|
||||||
) -> date:
|
) -> date:
|
||||||
"""Compute the end date of the subscription.
|
"""Compute the end date of the subscription.
|
||||||
|
|
||||||
@ -163,3 +164,19 @@ class Subscription(models.Model):
|
|||||||
|
|
||||||
def is_valid_now(self):
|
def is_valid_now(self):
|
||||||
return self.subscription_start <= date.today() <= self.subscription_end
|
return self.subscription_start <= date.today() <= self.subscription_end
|
||||||
|
|
||||||
|
@property
|
||||||
|
def semester_duration(self) -> float:
|
||||||
|
"""Duration of this subscription, in number of semester.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
The `Subscription` object doesn't have to actually exist
|
||||||
|
in the database to access this property
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```py
|
||||||
|
subscription = Subscription(subscription_type="deux-semestres")
|
||||||
|
assert subscription.semester_duration == 2.0
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
return settings.SITH_SUBSCRIPTIONS[self.subscription_type]["duration"]
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
Alpine.data("existing_user_subscription_form", () => ({
|
||||||
|
loading: false,
|
||||||
|
profileFragment: "" as string,
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const userSelect = document.getElementById("id_member") as HTMLSelectElement;
|
||||||
|
userSelect.addEventListener("change", async () => {
|
||||||
|
await this.loadProfile(Number.parseInt(userSelect.value));
|
||||||
|
});
|
||||||
|
await this.loadProfile(Number.parseInt(userSelect.value));
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadProfile(userId: number) {
|
||||||
|
if (!Number.isInteger(userId)) {
|
||||||
|
this.profileFragment = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
const response = await fetch(`/user/${userId}/mini/`);
|
||||||
|
this.profileFragment = await response.text();
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
28
subscription/static/subscription/css/subscription.scss
Normal file
28
subscription/static/subscription/css/subscription.scss
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#subscription-form form {
|
||||||
|
.form-content.existing-user {
|
||||||
|
max-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the form fields take exactly the space they need,
|
||||||
|
* then display the user profile right in the middle of the remaining space. */
|
||||||
|
fieldset {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subscription-form-user-mini-profile {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user_mini_profile {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
{% 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>
|
@ -0,0 +1 @@
|
|||||||
|
{{ form.as_p }}
|
@ -0,0 +1,10 @@
|
|||||||
|
<form
|
||||||
|
hx-post="{{ post_url }}"
|
||||||
|
hx-target="this"
|
||||||
|
hx-disabled-elt="find input[type='submit']"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
|
<input type="submit" value="{% trans %}Save{% endtrans %}">
|
||||||
|
</form>
|
@ -0,0 +1,30 @@
|
|||||||
|
<div class="alert alert-green">
|
||||||
|
<div class="alert-main">
|
||||||
|
<h3 class="alert-title">
|
||||||
|
{% trans user=subscription.member %}Subscription created for {{ user }}{% endtrans %}
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
{% trans trimmed
|
||||||
|
user=subscription.member.get_short_name(),
|
||||||
|
type=subscription.subscription_type,
|
||||||
|
end=subscription.subscription_end
|
||||||
|
%}
|
||||||
|
{{ user }} received its new {{ type }} subscription.
|
||||||
|
It will be active until {{ end }} included.
|
||||||
|
{% endtrans %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="alert-aside">
|
||||||
|
<a class="btn btn-blue" href="{{ subscription.member.get_absolute_url() }}">
|
||||||
|
{% trans %}Go to user profile{% endtrans %}
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-grey" href="{{ url("subscription:subscription") }}">
|
||||||
|
{# We don't know if this fragment is displayed after creating a subscription
|
||||||
|
for a previously existing user or for a newly created one.
|
||||||
|
Thus, we don't know which form should be used to create another subscription
|
||||||
|
in this place.
|
||||||
|
Therefore, we reload the entire page. It just works. #}
|
||||||
|
{% trans %}Create another subscription{% endtrans %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,62 +1,45 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
|
{% from "core/macros.jinja" import tabs %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}New subscription{% endtrans %}
|
{% trans %}New subscription{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{# The following statics are bundled with our autocomplete select.
|
||||||
|
However, if one tries to swap a form by another, then the urls in script-once
|
||||||
|
and link-once disappear.
|
||||||
|
So we give them here.
|
||||||
|
If the aforementioned bug is resolved, you can remove this. #}
|
||||||
|
{% block additional_js %}
|
||||||
|
<script type="module" src="{{ static("bundled/core/components/ajax-select-index.ts") }}"></script>
|
||||||
|
<script
|
||||||
|
type="module"
|
||||||
|
src="{{ static("bundled/subscription/creation-form-existing-user-index.ts") }}"
|
||||||
|
></script>
|
||||||
|
{% endblock %}
|
||||||
|
{% block additional_css %}
|
||||||
|
<link rel="stylesheet" href="{{ static("bundled/core/components/ajax-select-index.css") }}">
|
||||||
|
<link rel="stylesheet" href="{{ static("core/components/ajax-select.scss") }}">
|
||||||
|
<link rel="stylesheet" href="{{ static("subscription/css/subscription.scss") }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% macro form_fragment(form_object, post_url) %}
|
||||||
|
{# Include the form fragment inside a with block,
|
||||||
|
in order to inject the right form in the right place #}
|
||||||
|
{% with form=form_object, post_url=post_url %}
|
||||||
|
{% include "subscription/fragments/creation_form.jinja" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans %}New subscription{% endtrans %}</h3>
|
<h3>{% trans %}New subscription{% endtrans %}</h3>
|
||||||
<div id="user_info"></div>
|
<div id="subscription-form">
|
||||||
<form action="" method="post" id="subscription_form">
|
{% with title1=_("Existing member"), title2=_("New member") %}
|
||||||
{% csrf_token %}
|
{{ tabs([
|
||||||
{{ form.non_field_errors() }}
|
(title1, form_fragment(existing_user_form, existing_user_post_url)),
|
||||||
<p>{{ form.member.errors }}<label for="{{ form.member.name }}">{{ form.member.label }}</label> {{ form.member }}</p>
|
(title2, form_fragment(new_user_form, new_user_post_url)),
|
||||||
<div id="new_member">
|
]) }}
|
||||||
<p>{{ form.first_name.errors }}<label for="{{ form.first_name.name }}">{{ form.first_name.label }}</label> {{ form.first_name }}</p>
|
{% endwith %}
|
||||||
<p>{{ form.last_name.errors }}<label for="{{ form.last_name.name }}">{{ form.last_name.label }}</label> {{ form.last_name }}</p>
|
</div>
|
||||||
<p>{{ form.email.errors }}<label for="{{ form.email.name }}">{{ form.email.label }}</label> {{ form.email }}</p>
|
|
||||||
<p>{{ form.date_of_birth.errors }}<label for="{{ form.date_of_birth.name }}">{{ form.date_of_birth.label}}</label> {{ form.date_of_birth }}</p>
|
|
||||||
</div>
|
|
||||||
<p>{{ form.subscription_type.errors }}<label for="{{ form.subscription_type.name }}">{{ form.subscription_type.label }}</label> {{ form.subscription_type }}</p>
|
|
||||||
<p>{{ form.payment_method.errors }}<label for="{{ form.payment_method.name }}">{{ form.payment_method.label }}</label> {{
|
|
||||||
form.payment_method }}</p>
|
|
||||||
<p>{% trans %}Eboutic is reserved to specific users. In doubt, don't use it.{% endtrans %}</p>
|
|
||||||
<p>{{ form.location.errors }}<label for="{{ form.location.name }}">{{ form.location.label }}</label> {{ form.location }}</p>
|
|
||||||
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block script %}
|
|
||||||
{{ super() }}
|
|
||||||
<script type="text/javascript" charset="utf-8">
|
|
||||||
$( function() {
|
|
||||||
select = $("#id_member");
|
|
||||||
member_block = $("#subscription_form #new_member");
|
|
||||||
user_info = $("#user_info");
|
|
||||||
function display_new_member() {
|
|
||||||
if (select.val()) {
|
|
||||||
member_block.hide();
|
|
||||||
member_block.children().each(function() {
|
|
||||||
$(this).children().each(function() {
|
|
||||||
$(this).removeAttr('required');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
user_info.load("/user/"+select.val()+"/mini");
|
|
||||||
user_info.show();
|
|
||||||
} else {
|
|
||||||
member_block.show();
|
|
||||||
member_block.children().each(function() {
|
|
||||||
$(this).children().each(function() {
|
|
||||||
$(this).prop('required', true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
user_info.empty();
|
|
||||||
user_info.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select.on("change", display_new_member);
|
|
||||||
display_new_member();
|
|
||||||
} );
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
0
subscription/tests/__init__.py
Normal file
0
subscription/tests/__init__.py
Normal file
@ -12,6 +12,8 @@
|
|||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
"""Tests focused on the computing of subscription end, start and duration"""
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
import freezegun
|
import freezegun
|
151
subscription/tests/test_new_susbcription.py
Normal file
151
subscription/tests/test_new_susbcription.py
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
"""Tests focused on testing subscription creation"""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from django.test import Client
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.timezone import localdate
|
||||||
|
from model_bakery import baker
|
||||||
|
from pytest_django.asserts import assertRedirects
|
||||||
|
from pytest_django.fixtures import SettingsWrapper
|
||||||
|
|
||||||
|
from core.baker_recipes import board_user, old_subscriber_user, subscriber_user
|
||||||
|
from core.models import User
|
||||||
|
from subscription.forms import SubscriptionExistingUserForm, SubscriptionNewUserForm
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"user_factory",
|
||||||
|
[old_subscriber_user.make, lambda: baker.make(User)],
|
||||||
|
)
|
||||||
|
def test_form_existing_user_valid(
|
||||||
|
user_factory: Callable[[], User], settings: SettingsWrapper
|
||||||
|
):
|
||||||
|
"""Test `SubscriptionExistingUserForm`"""
|
||||||
|
user = user_factory()
|
||||||
|
data = {
|
||||||
|
"member": user,
|
||||||
|
"subscription_type": "deux-semestres",
|
||||||
|
"location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0],
|
||||||
|
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
|
||||||
|
}
|
||||||
|
form = SubscriptionExistingUserForm(data)
|
||||||
|
|
||||||
|
assert form.is_valid()
|
||||||
|
form.save()
|
||||||
|
user.refresh_from_db()
|
||||||
|
assert user.is_subscribed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_form_existing_user_invalid(settings: SettingsWrapper):
|
||||||
|
"""Test `SubscriptionExistingUserForm`, with users that shouldn't subscribe."""
|
||||||
|
user = subscriber_user.make()
|
||||||
|
# make sure the current subscription will end in a long time
|
||||||
|
last_sub = user.subscriptions.order_by("subscription_end").last()
|
||||||
|
last_sub.subscription_end = localdate() + timedelta(weeks=50)
|
||||||
|
last_sub.save()
|
||||||
|
data = {
|
||||||
|
"member": user,
|
||||||
|
"subscription_type": "deux-semestres",
|
||||||
|
"location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0],
|
||||||
|
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
|
||||||
|
}
|
||||||
|
form = SubscriptionExistingUserForm(data)
|
||||||
|
|
||||||
|
assert not form.is_valid()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
form.save()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_form_new_user(settings: SettingsWrapper):
|
||||||
|
data = {
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe",
|
||||||
|
"email": "jdoe@utbm.fr",
|
||||||
|
"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],
|
||||||
|
}
|
||||||
|
form = SubscriptionNewUserForm(data)
|
||||||
|
assert form.is_valid()
|
||||||
|
form.save()
|
||||||
|
user = User.objects.get(email="jdoe@utbm.fr")
|
||||||
|
assert user.username == "jdoe"
|
||||||
|
assert user.is_subscribed
|
||||||
|
|
||||||
|
# if trying to instantiate a new form with the same email,
|
||||||
|
# it should fail
|
||||||
|
form = SubscriptionNewUserForm(data)
|
||||||
|
assert not form.is_valid()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
form.save()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"user_factory", [lambda: baker.make(User, is_superuser=True), board_user.make]
|
||||||
|
)
|
||||||
|
def test_load_page(client: Client, user_factory: Callable[[], User]):
|
||||||
|
"""Just check the page doesn't crash."""
|
||||||
|
client.force_login(user_factory())
|
||||||
|
res = client.get(reverse("subscription:subscription"))
|
||||||
|
assert res.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_submit_form_existing_user(client: Client, settings: SettingsWrapper):
|
||||||
|
client.force_login(board_user.make())
|
||||||
|
user = old_subscriber_user.make()
|
||||||
|
response = client.post(
|
||||||
|
reverse("subscription:fragment-existing-user"),
|
||||||
|
{
|
||||||
|
"member": user.id,
|
||||||
|
"subscription_type": "deux-semestres",
|
||||||
|
"location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0],
|
||||||
|
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
user.refresh_from_db()
|
||||||
|
assert user.is_subscribed
|
||||||
|
current_subscription = user.subscriptions.order_by("-subscription_start").first()
|
||||||
|
assertRedirects(
|
||||||
|
response,
|
||||||
|
reverse(
|
||||||
|
"subscription:creation-success",
|
||||||
|
kwargs={"subscription_id": current_subscription.id},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_submit_form_new_user(client: Client, settings: SettingsWrapper):
|
||||||
|
client.force_login(board_user.make())
|
||||||
|
response = client.post(
|
||||||
|
reverse("subscription:fragment-new-user"),
|
||||||
|
{
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe",
|
||||||
|
"email": "jdoe@utbm.fr",
|
||||||
|
"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],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
user = User.objects.get(email="jdoe@utbm.fr")
|
||||||
|
assert user.is_subscribed
|
||||||
|
current_subscription = user.subscriptions.order_by("-subscription_start").first()
|
||||||
|
assertRedirects(
|
||||||
|
response,
|
||||||
|
reverse(
|
||||||
|
"subscription:creation-success",
|
||||||
|
kwargs={"subscription_id": current_subscription.id},
|
||||||
|
),
|
||||||
|
)
|
@ -15,10 +15,31 @@
|
|||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from subscription.views import NewSubscription, SubscriptionsStatsView
|
from subscription.views import (
|
||||||
|
CreateSubscriptionExistingUserFragment,
|
||||||
|
CreateSubscriptionNewUserFragment,
|
||||||
|
NewSubscription,
|
||||||
|
SubscriptionCreatedFragment,
|
||||||
|
SubscriptionsStatsView,
|
||||||
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Subscription views
|
# Subscription views
|
||||||
path("", NewSubscription.as_view(), name="subscription"),
|
path("", NewSubscription.as_view(), name="subscription"),
|
||||||
|
path(
|
||||||
|
"fragment/existing-user/",
|
||||||
|
CreateSubscriptionExistingUserFragment.as_view(),
|
||||||
|
name="fragment-existing-user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"fragment/new-user/",
|
||||||
|
CreateSubscriptionNewUserFragment.as_view(),
|
||||||
|
name="fragment-new-user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"fragment/<int:subscription_id>/creation-success",
|
||||||
|
SubscriptionCreatedFragment.as_view(),
|
||||||
|
name="creation-success",
|
||||||
|
),
|
||||||
path("stats/", SubscriptionsStatsView.as_view(), name="stats"),
|
path("stats/", SubscriptionsStatsView.as_view(), name="stats"),
|
||||||
]
|
]
|
||||||
|
@ -13,166 +13,96 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
import secrets
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import PermissionDenied, ValidationError
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
from django.urls import reverse_lazy
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.views.generic.edit import CreateView, FormView
|
from django.utils.timezone import localdate
|
||||||
|
from django.views.generic import CreateView, DetailView, TemplateView
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
|
|
||||||
from core.models import User
|
from subscription.forms import (
|
||||||
from core.views.forms import SelectDate, SelectDateTime
|
SelectionDateForm,
|
||||||
from core.views.widgets.select import AutoCompleteSelectUser
|
SubscriptionExistingUserForm,
|
||||||
|
SubscriptionNewUserForm,
|
||||||
|
)
|
||||||
from subscription.models import Subscription
|
from subscription.models import Subscription
|
||||||
|
|
||||||
|
|
||||||
class SelectionDateForm(forms.Form):
|
class CanCreateSubscriptionMixin(UserPassesTestMixin):
|
||||||
def __init__(self, *args, **kwargs):
|
def test_func(self):
|
||||||
super().__init__(*args, **kwargs)
|
return self.request.user.can_create_subscription
|
||||||
self.fields["start_date"] = forms.DateTimeField(
|
|
||||||
label=_("Start date"), widget=SelectDateTime, required=True
|
|
||||||
)
|
|
||||||
self.fields["end_date"] = forms.DateTimeField(
|
|
||||||
label=_("End date"), widget=SelectDateTime, required=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionForm(forms.ModelForm):
|
class NewSubscription(CanCreateSubscriptionMixin, TemplateView):
|
||||||
class Meta:
|
|
||||||
model = Subscription
|
|
||||||
fields = ["member", "subscription_type", "payment_method", "location"]
|
|
||||||
widgets = {"member": AutoCompleteSelectUser}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["member"].required = False
|
|
||||||
self.fields |= forms.fields_for_model(
|
|
||||||
User,
|
|
||||||
fields=["first_name", "last_name", "email", "date_of_birth"],
|
|
||||||
widgets={"date_of_birth": SelectDate},
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean_member(self):
|
|
||||||
subscriber = self.cleaned_data.get("member")
|
|
||||||
if subscriber:
|
|
||||||
subscriber = User.objects.filter(id=subscriber.id).first()
|
|
||||||
return subscriber
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
cleaned_data = super().clean()
|
|
||||||
if (
|
|
||||||
cleaned_data.get("member") is None
|
|
||||||
and "last_name" not in self.errors.as_data()
|
|
||||||
and "first_name" not in self.errors.as_data()
|
|
||||||
and "email" not in self.errors.as_data()
|
|
||||||
and "date_of_birth" not in self.errors.as_data()
|
|
||||||
):
|
|
||||||
self.errors.pop("member", None)
|
|
||||||
if self.errors:
|
|
||||||
return cleaned_data
|
|
||||||
if User.objects.filter(email=cleaned_data.get("email")).first() is not None:
|
|
||||||
self.add_error(
|
|
||||||
"email",
|
|
||||||
ValidationError(_("A user with that email address already exists")),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
u = User(
|
|
||||||
last_name=self.cleaned_data.get("last_name"),
|
|
||||||
first_name=self.cleaned_data.get("first_name"),
|
|
||||||
email=self.cleaned_data.get("email"),
|
|
||||||
date_of_birth=self.cleaned_data.get("date_of_birth"),
|
|
||||||
)
|
|
||||||
u.generate_username()
|
|
||||||
u.set_password(secrets.token_urlsafe(nbytes=10))
|
|
||||||
u.save()
|
|
||||||
cleaned_data["member"] = u
|
|
||||||
elif cleaned_data.get("member") is not None:
|
|
||||||
self.errors.pop("last_name", None)
|
|
||||||
self.errors.pop("first_name", None)
|
|
||||||
self.errors.pop("email", None)
|
|
||||||
self.errors.pop("date_of_birth", None)
|
|
||||||
if cleaned_data.get("member") is None:
|
|
||||||
# This should be handled here,
|
|
||||||
# but it is done in the Subscription model's clean method
|
|
||||||
# TODO investigate why!
|
|
||||||
raise ValidationError(
|
|
||||||
_(
|
|
||||||
"You must either choose an existing "
|
|
||||||
"user or create a new one properly"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return cleaned_data
|
|
||||||
|
|
||||||
|
|
||||||
class NewSubscription(CreateView):
|
|
||||||
template_name = "subscription/subscription.jinja"
|
template_name = "subscription/subscription.jinja"
|
||||||
form_class = SubscriptionForm
|
|
||||||
|
|
||||||
def dispatch(self, request, *arg, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
if request.user.can_create_subscription:
|
return super().get_context_data(**kwargs) | {
|
||||||
return super().dispatch(request, *arg, **kwargs)
|
"existing_user_form": SubscriptionExistingUserForm(
|
||||||
raise PermissionDenied
|
initial={"member": self.request.GET.get("member")}
|
||||||
|
),
|
||||||
|
"new_user_form": SubscriptionNewUserForm(),
|
||||||
|
"existing_user_post_url": reverse("subscription:fragment-existing-user"),
|
||||||
|
"new_user_post_url": reverse("subscription:fragment-new-user"),
|
||||||
|
}
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
if "member" in self.request.GET:
|
|
||||||
return {
|
|
||||||
"member": self.request.GET["member"],
|
|
||||||
"subscription_type": "deux-semestres",
|
|
||||||
}
|
|
||||||
return {"subscription_type": "deux-semestres"}
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
class CreateSubscriptionFragment(CanCreateSubscriptionMixin, CreateView):
|
||||||
form.instance.subscription_start = Subscription.compute_start(
|
template_name = "subscription/fragments/creation_form.jinja"
|
||||||
duration=settings.SITH_SUBSCRIPTIONS[form.instance.subscription_type][
|
|
||||||
"duration"
|
def get_success_url(self):
|
||||||
],
|
return reverse(
|
||||||
user=form.instance.member,
|
"subscription:creation-success", kwargs={"subscription_id": self.object.id}
|
||||||
)
|
)
|
||||||
form.instance.subscription_end = Subscription.compute_end(
|
|
||||||
duration=settings.SITH_SUBSCRIPTIONS[form.instance.subscription_type][
|
|
||||||
"duration"
|
class CreateSubscriptionExistingUserFragment(CreateSubscriptionFragment):
|
||||||
],
|
"""Create a subscription for a user who already exists."""
|
||||||
start=form.instance.subscription_start,
|
|
||||||
user=form.instance.member,
|
form_class = SubscriptionExistingUserForm
|
||||||
)
|
extra_context = {"post_url": reverse_lazy("subscription:fragment-existing-user")}
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
class CreateSubscriptionNewUserFragment(CreateSubscriptionFragment):
|
||||||
|
"""Create a subscription for a user who already exists."""
|
||||||
|
|
||||||
|
form_class = SubscriptionNewUserForm
|
||||||
|
extra_context = {"post_url": reverse_lazy("subscription:fragment-new-user")}
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionCreatedFragment(CanCreateSubscriptionMixin, DetailView):
|
||||||
|
template_name = "subscription/fragments/creation_success.jinja"
|
||||||
|
model = Subscription
|
||||||
|
pk_url_kwarg = "subscription_id"
|
||||||
|
context_object_name = "subscription"
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionsStatsView(FormView):
|
class SubscriptionsStatsView(FormView):
|
||||||
template_name = "subscription/stats.jinja"
|
template_name = "subscription/stats.jinja"
|
||||||
form_class = SelectionDateForm
|
form_class = SelectionDateForm
|
||||||
|
success_url = reverse_lazy("subscriptions:stats")
|
||||||
|
|
||||||
def dispatch(self, request, *arg, **kwargs):
|
def dispatch(self, request, *arg, **kwargs):
|
||||||
import datetime
|
self.start_date = localdate()
|
||||||
|
|
||||||
self.start_date = datetime.datetime.today()
|
|
||||||
self.end_date = self.start_date
|
self.end_date = self.start_date
|
||||||
res = super().dispatch(request, *arg, **kwargs)
|
|
||||||
if request.user.is_root or request.user.is_board_member:
|
if request.user.is_root or request.user.is_board_member:
|
||||||
return res
|
return super().dispatch(request, *arg, **kwargs)
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.form = self.get_form()
|
self.form = self.get_form()
|
||||||
self.start_date = self.form["start_date"]
|
self.start_date = self.form["start_date"]
|
||||||
self.end_date = self.form["end_date"]
|
self.end_date = self.form["end_date"]
|
||||||
res = super().post(request, *args, **kwargs)
|
return super().post(request, *args, **kwargs)
|
||||||
if request.user.is_root or request.user.is_board_member:
|
|
||||||
return res
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
init = {
|
return {
|
||||||
"start_date": self.start_date.strftime("%Y-%m-%d %H:%M:%S"),
|
"start_date": self.start_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"end_date": self.end_date.strftime("%Y-%m-%d %H:%M:%S"),
|
"end_date": self.end_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
}
|
}
|
||||||
return init
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
from subscription.models import Subscription
|
|
||||||
|
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["subscriptions_total"] = Subscription.objects.filter(
|
kwargs["subscriptions_total"] = Subscription.objects.filter(
|
||||||
subscription_end__gte=self.end_date, subscription_start__lte=self.start_date
|
subscription_end__gte=self.end_date, subscription_start__lte=self.start_date
|
||||||
@ -181,6 +111,3 @@ class SubscriptionsStatsView(FormView):
|
|||||||
kwargs["payment_types"] = settings.SITH_COUNTER_PAYMENT_METHOD
|
kwargs["payment_types"] = settings.SITH_COUNTER_PAYMENT_METHOD
|
||||||
kwargs["locations"] = settings.SITH_SUBSCRIPTION_LOCATIONS
|
kwargs["locations"] = settings.SITH_SUBSCRIPTION_LOCATIONS
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
|
||||||
return reverse_lazy("subscriptions:stats")
|
|
||||||
|
@ -85,6 +85,7 @@ export default defineConfig((config: UserConfig) => {
|
|||||||
inject({
|
inject({
|
||||||
// biome-ignore lint/style/useNamingConvention: that's how it's called
|
// biome-ignore lint/style/useNamingConvention: that's how it's called
|
||||||
Alpine: "alpinejs",
|
Alpine: "alpinejs",
|
||||||
|
htmx: "htmx.org",
|
||||||
}),
|
}),
|
||||||
viteStaticCopy({
|
viteStaticCopy({
|
||||||
targets: [
|
targets: [
|
||||||
|
Loading…
Reference in New Issue
Block a user