Commit acfbdd1a authored by Skia's avatar Skia

Merge branch 'feature/forum-search' into 'master'

Forum search

See merge request !181
parents a96aeba1 835782fd
Pipeline #1654 passed with stage
in 10 minutes and 32 seconds
......@@ -366,7 +366,11 @@ class User(AbstractBaseUser):
return False
if group_id == settings.SITH_GROUP_ROOT_ID and self.is_superuser:
return True
return self.groups.filter(name=group_name).exists()
return group_name in self.cached_groups_names
@cached_property
def cached_groups_names(self):
return [g.name for g in self.groups.all()]
@cached_property
def is_root(self):
......
......@@ -2,6 +2,7 @@
#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
......@@ -27,6 +28,7 @@ from django.db import models
from haystack import indexes, signals
from core.models import User
from forum.models import ForumMessage, ForumMessageMeta
class UserIndex(indexes.SearchIndex, indexes.Indexable):
......@@ -44,13 +46,68 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable):
return "last_update"
class UserOnlySignalProcessor(signals.BaseSignalProcessor):
class IndexSignalProcessor(signals.BaseSignalProcessor):
def setup(self):
# Listen only to the ``User`` model.
models.signals.post_save.connect(self.handle_save, sender=User)
models.signals.post_delete.connect(self.handle_delete, sender=User)
# Listen only to the ``ForumMessage`` model.
models.signals.post_save.connect(self.handle_save, sender=ForumMessageMeta)
models.signals.post_delete.connect(self.handle_delete, sender=ForumMessage)
# Listen to the ``ForumMessageMeta`` model pretending it's a ``ForumMessage``.
models.signals.post_save.connect(
self.handle_forum_message_meta_save, sender=ForumMessageMeta
)
models.signals.post_delete.connect(
self.handle_forum_message_meta_delete, sender=ForumMessageMeta
)
def teardown(self):
# Disconnect only for the ``User`` model.
models.signals.post_save.disconnect(self.handle_save, sender=User)
models.signals.post_delete.disconnect(self.handle_delete, sender=User)
# Disconnect only to the ``ForumMessage`` model.
models.signals.post_save.disconnect(self.handle_save, sender=ForumMessage)
models.signals.post_delete.disconnect(self.handle_delete, sender=ForumMessage)
# Disconnect to the ``ForumMessageMeta`` model pretending it's a ``ForumMessage``.
models.signals.post_save.disconnect(
self.handle_forum_message_meta_save, sender=ForumMessageMeta
)
models.signals.post_delete.disconnect(
self.handle_forum_message_meta_delete, sender=ForumMessageMeta
)
def handle_forum_message_meta_save(self, sender, instance, **kwargs):
super(IndexSignalProcessor, self).handle_save(
ForumMessage, instance.message, **kwargs
)
def handle_forum_message_meta_delete(self, sender, instance, **kwargs):
super(IndexSignalProcessor, self).handle_delete(
ForumMessage, instance.message, **kwargs
)
class BigCharFieldIndex(indexes.CharField):
"""
Workaround to avoid xapian.InvalidArgument: Term too long (> 245)
See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion
"""
def prepare(self, term):
return bytes(super(BigCharFieldIndex, self).prepare(term), "utf-8")[
:245
].decode("utf-8", errors="ignore")
class ForumMessageIndex(indexes.SearchIndex, indexes.Indexable):
text = BigCharFieldIndex(document=True, use_template=True)
auto = indexes.EdgeNgramField(use_template=True)
date = indexes.DateTimeField(model_attr="date")
def get_model(self):
return ForumMessage
......@@ -1403,6 +1403,18 @@ textarea {
}
}
.search_bar {
margin: 10px 0px;
display: flex;
height: 20p;
align-items: center;
}
.search_check {
margin-left: 10px;
}
.search_bouton {
margin-left: 10px;
}
.category {
margin-top: 5px;
background: $secondary-color;
......
{{ object.topic }}
{{ object.title }}
{{ object.message }}
{{ object.author }}
{{ object.topic }}
{{ object.title }}
{{ object.message }}
{{ object.author }}
......@@ -2,6 +2,7 @@
#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
......@@ -176,6 +177,7 @@ class CanViewMixin(View):
"""
def dispatch(self, request, *arg, **kwargs):
try:
self.object = self.get_object()
if can_view(self.object, request.user):
......@@ -184,8 +186,10 @@ class CanViewMixin(View):
except:
pass
# If we get here, it's a ListView
l_id = [o.id for o in self.get_queryset() if can_view(o, request.user)]
if not l_id and self.get_queryset().count() != 0:
queryset = self.get_queryset()
l_id = [o.id for o in queryset if can_view(o, request.user)]
if not l_id and queryset.count() != 0:
raise PermissionDenied
self._get_queryset = self.get_queryset
......
......@@ -71,6 +71,8 @@ def notification(request, notif_id):
def search_user(query, as_json=False):
if query == "":
return []
res = SearchQuerySet().models(User).autocomplete(auto=query)[:20]
return [r.object for r in res]
......
......@@ -331,9 +331,9 @@ class ForumMessage(models.Model):
return user.can_edit(self.topic.forum)
def can_be_viewed_by(self, user):
return (
not self._deleted
) # No need to check the real rights since it's already done by the Topic view
# No need to check the real rights since it's already done by the Topic view
# and it impacts performances too much
return not self._deleted
def can_be_moderated_by(self, user):
return self.topic.forum.is_owned_by(user) or user.id == self.author.id
......
{% extends "core/base.jinja" %}
{% from 'forum/macros.jinja' import display_forum, display_topic %}
{% from 'forum/macros.jinja' import display_forum, display_breadcrumb, display_topic, display_search_bar %}
{% block title %}
{{ forum }}
{% endblock %}
{% block content %}
<div>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
{% for f in forum.get_parent_list()|reverse %}
> <a href="{{ f.get_absolute_url() }}">{{ f }}</a>
{% endfor %}
> <a href="{{ forum.get_absolute_url() }}">{{ forum }}</a>
</div>
{{ display_breadcrumb(forum) }}
<div id="forum">
<h3>{{ forum.name }}</h3>
<p>
......@@ -22,6 +16,7 @@
{% if not forum.is_category %}
<a class="ib button" href="{{ url('forum:new_topic', forum_id=forum.id) }}">{% trans %}New topic{% endtrans %}</a>
{% endif %}
{{ display_search_bar(request) }}
</p>
{% if forum.children.exists() %}
<div>
......
......@@ -85,9 +85,20 @@
</div>
{% endmacro %}
{% macro display_breadcrumb(forum, topic="") %}
<p>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
{% for f in forum.get_parent_list()|reverse %}
> <a href="{{ f.get_absolute_url() }}">{{ f }}</a>
{% endfor %}
> <a href="{{ forum.get_absolute_url() }}">{{ forum }}</a>
{% if topic != "" %} > <a href="{{ topic.get_absolute_url() }}">{{ topic }}</a>{%- endif -%}
</p>
{% endmacro %}
{% macro display_message(m, user, unread=False) %}
{% if user.can_view(m) %}
<div id="msg_{{ m.id }}" class="message {% if unread %}unread{% endif %}">
<div id="msg_{{ m.id }}" class="message {% if unread %}unread{% endif %}">
<div class="msg_author {% if m.deleted %}deleted{% endif %}">
{% if m.author.avatar_pict %}
<img src="{{ m.author.avatar_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}" id="picture" />
......@@ -143,7 +154,7 @@
</div>
</div>
{% else %}
<div id="msg_{{ m.id }}" class="message">
<div id="msg_{{ m.id }}" class="message">
<div class="msg_author deleted">
</div>
<div class="msg_content deleted">
......@@ -155,3 +166,12 @@
{{ m.mark_as_read(user) or "" }}
{% endmacro %}
{% macro display_search_bar(request) %}
<form class="search_bar" action="{{ url('forum:search') }}" method="GET">
<input type="text" placeholder="{% trans %}Search{% endtrans %}" name="query" value="{{ request.GET.query|default('') }}"/>
<input type="checkbox" class="search_check" name="order" value="date" {% if request.GET.order|default("") == "date" or (request.GET.order|default("") == "" and request.GET.query|default("") == "") -%}
checked
{%- endif -%}> {% trans %}Order by date{% endtrans %}<br>
<input type="submit" class="search_bouton" value="{% trans %}Search{% endtrans %}"/>
</form>
{% endmacro %}
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link %}
{% from 'forum/macros.jinja' import display_forum %}
{% from 'forum/macros.jinja' import display_forum, display_search_bar %}
{% block title %}
{% trans %}Forum{% endtrans %}
......@@ -15,6 +15,7 @@
<p>
<a class="ib button" href="{{ url('forum:last_unread') }}">{% trans %}View last unread messages{% endtrans %}</a>
<a class="ib button" href="{{ url('forum:favorite_topics') }}">{% trans %}Favorite topics{% endtrans %}</a>
{{ display_search_bar(request) }}
</p>
{% if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
<p>
......@@ -34,6 +35,7 @@
</div>
</div>
</div>
{% for f in forum_list %}
<div>
{{ display_forum(f, user, True) }}
......
{% extends "core/base.jinja" %}
{% from 'forum/macros.jinja' import display_message %}
{% from 'forum/macros.jinja' import display_message, display_search_bar %}
{% block title %}
{% if topic %}
......@@ -11,6 +11,7 @@
{% block content %}
{% if topic %}
{{ display_search_bar(request) }}
<p>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
{% for f in topic.forum.get_parent_list() %}
......
{% extends "core/base.jinja" %}
{% from 'forum/macros.jinja' import display_message, display_breadcrumb, display_search_bar %}
{% block content %}
<div id="forum">
{{ display_search_bar(request) }}
{% if object_list|length != 0 %}
<br>
<div class="search-results">
{% for m in object_list %}
{{ display_breadcrumb(m.topic.forum, m.topic) }}
{{ display_message(m, user) }}
{% endfor %}
</div>
{% else %}
{% trans %}No result found{% endtrans %}
{% endif %}
</div>
{% endblock %}
\ No newline at end of file
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link %}
{% from 'forum/macros.jinja' import display_message %}
{% from 'forum/macros.jinja' import display_message, display_breadcrumb, display_search_bar %}
{% block title %}
{{ topic }}
......@@ -26,15 +26,8 @@
{% endblock %}
{% block content %}
<p>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
{% for f in topic.forum.get_parent_list()|reverse %}
> <a href="{{ f.get_absolute_url() }}">{{ f }}</a>
{% endfor %}
> <a href="{{ topic.forum.get_absolute_url() }}">{{ topic.forum }}</a>
> <a href="{{ topic.get_absolute_url() }}">{{ topic }}</a>
</p>
<h3>{{ topic.title }}</h3>
{{ display_breadcrumb(topic.forum, topic) }}
<h3>{{ topic.title }}</h3>
<div id="forum">
<p>{{ topic.description }}</p>
<p>
......@@ -46,6 +39,7 @@
{% endif %}
</p>
{{ display_search_bar(request) }}
<p style="text-align: right; background: #d8e7f3;">
{% for p in msgs.paginator.page_range %}
<span class="ib" style="background: {% if p == msgs.number %}white{% endif %}; margin: 0;"><a href="?page={{ p }}">{{ p }}</a></span>
......
......@@ -26,8 +26,10 @@ from django.conf.urls import url
from forum.views import *
urlpatterns = [
url(r"^$", ForumMainView.as_view(), name="main"),
url(r"^search/$", ForumSearchView.as_view(), name="search"),
url(r"^new_forum$", ForumCreateView.as_view(), name="new_forum"),
url(r"^mark_all_as_read$", ForumMarkAllAsRead.as_view(), name="mark_all_as_read"),
url(r"^last_unread$", ForumLastUnread.as_view(), name="last_unread"),
......
......@@ -2,6 +2,7 @@
#
# Copyright 2016,2017,2018
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
......@@ -36,9 +37,56 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from ajax_select import make_ajax_field
from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin
from core.views import (
CanViewMixin,
CanEditMixin,
CanEditPropMixin,
CanCreateMixin,
can_view,
)
from core.views.forms import MarkdownInput
from forum.models import Forum, ForumMessage, ForumTopic, ForumMessageMeta
from haystack.query import RelatedSearchQuerySet
class ForumSearchView(ListView):
template_name = "forum/search.jinja"
def get_queryset(self):
query = self.request.GET.get("query", "")
order_by = self.request.GET.get("order", "")
if query == "":
return []
queryset = RelatedSearchQuerySet().models(ForumMessage).autocomplete(auto=query)
if order_by == "date":
queryset = queryset.order_by("-date")
queryset = queryset.load_all()
queryset = queryset.load_all_queryset(
ForumMessage,
ForumMessage.objects.all()
.prefetch_related("topic__forum__edit_groups")
.prefetch_related("topic__forum__view_groups")
.prefetch_related("topic__forum__owner_club"),
)
# Filter unauthorized responses
resp = []
count = 0
max_count = 30
for r in queryset:
if count >= max_count:
return resp
if can_view(r.object, self.request.user) and can_view(
r.object.topic, self.request.user
):
resp.append(r.object)
count += 1
return resp
class ForumMainView(ListView):
......
......@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-11-08 00:26+0100\n"
"POT-Creation-Date: 2018-12-11 20:07+0100\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Skia <skia@libskia.so>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
......@@ -219,7 +219,7 @@ msgstr "Compte"
msgid "Company"
msgstr "Entreprise"
#: accounting/models.py:317 sith/settings.py:377
#: accounting/models.py:317 sith/settings.py:378
#: stock/templates/stock/shopping_list_items.jinja:37
msgid "Other"
msgstr "Autre"
......@@ -776,7 +776,7 @@ msgstr "Opération liée : "
#: core/templates/core/user_preferences.jinja:12
#: core/templates/core/user_preferences.jinja:19
#: counter/templates/counter/cash_register_summary.jinja:22
#: forum/templates/forum/reply.jinja:33
#: forum/templates/forum/reply.jinja:34
#: subscription/templates/subscription/subscription.jinja:25
#: trombi/templates/trombi/comment.jinja:26
#: trombi/templates/trombi/edit_profile.jinja:13
......@@ -1492,9 +1492,9 @@ msgstr "Type"
#: com/templates/com/news_admin_list.jinja:249
#: com/templates/com/news_admin_list.jinja:286
#: com/templates/com/weekmail.jinja:19 com/templates/com/weekmail.jinja:48
#: core/templates/core/base.jinja:341 forum/templates/forum/forum.jinja:29
#: forum/templates/forum/forum.jinja:48 forum/templates/forum/main.jinja:26
#: forum/views.py:192
#: core/templates/core/base.jinja:341 forum/templates/forum/forum.jinja:30
#: forum/templates/forum/forum.jinja:49 forum/templates/forum/main.jinja:27
#: forum/views.py:213
msgid "Title"
msgstr "Titre"
......@@ -1518,7 +1518,7 @@ msgstr "Résumé"
#: com/templates/com/news_admin_list.jinja:252
#: com/templates/com/news_admin_list.jinja:289
#: com/templates/com/weekmail.jinja:17 com/templates/com/weekmail.jinja:46
#: forum/templates/forum/forum.jinja:52
#: forum/templates/forum/forum.jinja:53
msgid "Author"
msgstr "Auteur"
......@@ -1628,7 +1628,7 @@ msgstr ""
#: com/templates/com/news_edit.jinja:56 com/templates/com/weekmail.jinja:10
#: core/templates/core/macros_pages.jinja:49
#: forum/templates/forum/reply.jinja:32
#: forum/templates/forum/reply.jinja:33
msgid "Preview"
msgstr "Prévisualiser"
......@@ -2113,7 +2113,7 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
#: core/templates/core/user_edit.jinja:17
#: election/templates/election/election_detail.jinja:340
#: forum/templates/forum/macros.jinja:93 forum/templates/forum/macros.jinja:95
#: forum/templates/forum/reply.jinja:38 forum/templates/forum/reply.jinja:40
#: forum/templates/forum/reply.jinja:39 forum/templates/forum/reply.jinja:41
#: trombi/templates/trombi/user_tools.jinja:41
msgid "Profile"
msgstr "Profil"
......@@ -2310,6 +2310,8 @@ msgid "Register"
msgstr "S'enregister"
#: core/templates/core/base.jinja:75 core/templates/core/base.jinja:76
#: forum/templates/forum/macros.jinja:160
#: forum/templates/forum/macros.jinja:162
#: matmat/templates/matmat/search_form.jinja:37
#: matmat/templates/matmat/search_form.jinja:47
#: matmat/templates/matmat/search_form.jinja:58
......@@ -2370,8 +2372,8 @@ msgstr "GA"
#: forum/templates/forum/forum.jinja:10
#: forum/templates/forum/last_unread.jinja:14
#: forum/templates/forum/main.jinja:6 forum/templates/forum/main.jinja:11
#: forum/templates/forum/main.jinja:14 forum/templates/forum/reply.jinja:15
#: forum/templates/forum/topic.jinja:30
#: forum/templates/forum/main.jinja:14 forum/templates/forum/reply.jinja:16
#: forum/templates/forum/topic.jinja:31
msgid "Forum"
msgstr "Forum"
......@@ -2385,7 +2387,7 @@ msgstr "Photos"
#: eboutic/templates/eboutic/eboutic_main.jinja:24
#: eboutic/templates/eboutic/eboutic_makecommand.jinja:8
#: eboutic/templates/eboutic/eboutic_payment_result.jinja:4
#: sith/settings.py:376 sith/settings.py:384
#: sith/settings.py:377 sith/settings.py:385
msgid "Eboutic"
msgstr "Eboutic"
......@@ -3589,8 +3591,8 @@ msgstr "quantité"
msgid "Sith account"
msgstr "Compte utilisateur"
#: counter/models.py:448 sith/settings.py:369 sith/settings.py:374
#: sith/settings.py:392
#: counter/models.py:448 sith/settings.py:370 sith/settings.py:375
#: sith/settings.py:393
msgid "Credit card"
msgstr "Carte bancaire"
......@@ -4352,25 +4354,25 @@ msgstr "dernière date de lecture"
msgid "Favorite topics"
msgstr "Topics favoris"
#: forum/templates/forum/forum.jinja:20 forum/templates/forum/main.jinja:21
#: forum/templates/forum/forum.jinja:20 forum/templates/forum/main.jinja:22
msgid "New forum"
msgstr "Nouveau forum"
#: forum/templates/forum/forum.jinja:23 forum/templates/forum/reply.jinja:8
#: forum/templates/forum/reply.jinja:27
#: forum/templates/forum/reply.jinja:28
msgid "New topic"
msgstr "Nouveau sujet"
#: forum/templates/forum/forum.jinja:33 forum/templates/forum/main.jinja:30
#: forum/templates/forum/forum.jinja:34 forum/templates/forum/main.jinja:31
msgid "Topics"
msgstr "Sujets"
#: forum/templates/forum/forum.jinja:36 forum/templates/forum/forum.jinja:58
#: forum/templates/forum/main.jinja:33
#: forum/templates/forum/forum.jinja:37 forum/templates/forum/forum.jinja:59
#: forum/templates/forum/main.jinja:34
msgid "Last message"
msgstr "Dernier message"
#: forum/templates/forum/forum.jinja:55
#: forum/templates/forum/forum.jinja:56
msgid "Messages"
msgstr "Messages"
......@@ -4400,28 +4402,36 @@ msgstr " le "
msgid "Deleted or unreadable message."
msgstr "Message supprimé ou non-visible."
#: forum/templates/forum/macros.jinja:161
msgid "Order by date"
msgstr "Trier par date"
#: forum/templates/forum/main.jinja:16
msgid "View last unread messages"
msgstr "Voir les derniers messages non lus"
#: forum/templates/forum/reply.jinja:6 forum/templates/forum/reply.jinja:24
#: forum/templates/forum/topic.jinja:41 forum/templates/forum/topic.jinja:66
#: forum/templates/forum/reply.jinja:6 forum/templates/forum/reply.jinja:25
#: forum/templates/forum/topic.jinja:42 forum/templates/forum/topic.jinja:67
msgid "Reply"
msgstr "Répondre"
#: forum/templates/forum/topic.jinja:43
#: forum/templates/forum/search.jinja:16
msgid "No result found"
msgstr "Pas de résultats"
#: forum/templates/forum/topic.jinja:44
msgid "Unmark as favorite"
msgstr "Enlever des favoris"
#: forum/templates/forum/topic.jinja:45
#: forum/templates/forum/topic.jinja:46
msgid "Mark as favorite"
msgstr "Ajouter aux favoris"
#: forum/views.py:138
#: forum/views.py:159
msgid "Apply rights and club owner recursively"
msgstr "Appliquer les droits et le club propriétaire récursivement"
#: forum/views.py:356
#: forum/views.py:377
#, python-format
msgid "%(author)s said"
msgstr "Citation de %(author)s"
......@@ -4475,12 +4485,12 @@ msgid "Washing and drying"
msgstr "Lavage et séchage"
#: launderette/templates/launderette/launderette_book.jinja:27
#: sith/settings.py:520
#: sith/settings.py:521
msgid "Washing"
msgstr "Lavage"
#: launderette/templates/launderette/launderette_book.jinja:31
#: sith/settings.py:520
#: sith/settings.py:521
msgid "Drying"
msgstr "Séchage"
......@@ -4660,251 +4670,251 @@ msgstr "Erreur de création de l'album %(album)s : %(msg)s"
msgid "Add user"
msgstr "Ajouter une personne"
#: sith/settings.py:215
#: sith/settings.py:216
msgid "English"
msgstr "Anglais"
#: sith/settings.py:215
#: sith/settings.py:216
msgid "French"
msgstr "Français"
#: sith/settings.py:350
#: sith/settings.py:351
msgid "TC"
msgstr "TC"
#: sith/settings.py:351
#: sith/settings.py:352
msgid "IMSI"
msgstr "IMSI"
#: sith/settings.py:352
#: sith/settings.py:353
msgid "IMAP"
msgstr "IMAP"
#: sith/settings.py:353
#: sith/settings.py:354
msgid "INFO"
msgstr "INFO"
#: sith/settings.py:354
#: sith/settings.py:355
msgid "GI"
msgstr "GI"
#: sith/settings.py:355
#: sith/settings.py:356
msgid "E"
msgstr "E"
#: sith/settings.py:356
#: sith/settings.py:357
msgid "EE"
msgstr "EE"
#: sith/settings.py:357
#: sith/settings.py:358
msgid "GESC"
msgstr "GESC"
#: sith/settings.py:358
#: sith/settings.py:359
msgid "GMC"
msgstr "GMC"
#: sith/settings.py:359
#: sith/settings.py:360
msgid "MC"
msgstr "MC"
#: sith/settings.py:360
#: sith/settings.py:361
msgid "EDIM"
msgstr "EDIM"
#: sith/settings.py:361
#: sith/settings.py:362
msgid "Humanities"
msgstr "Humanités"
#: sith/settings.py:362
#: sith/settings.py:363
msgid "N/A"
msgstr "N/A"
#: sith/settings.py:366 sith/settings.py:373 sith/settings.py:390
#: sith/settings.py:367 sith/settings.py:374 sith/settings.py:391
msgid "Check"
msgstr "Chèque"
#: sith/settings.py:367 sith/settings.py:375 sith/settings.py:391
#: sith/settings.py:368 sith/settings.py:376 sith/settings.py:392
msgid "Cash"
msgstr "Espèces"
#: sith/settings.py:368
#: sith/settings.py:369
msgid "Transfert"
msgstr "Virement"
#: sith/settings.py:381
#: sith/settings.py:382
msgid "Belfort"
msgstr "Belfort"
#: sith/settings.py:382
#: sith/settings.py:383
msgid "Sevenans"
msgstr "Sevenans"
#: sith/settings.py:383
#: sith/settings.py:384
msgid "Montbéliard"
msgstr "Montbéliard"
#: sith/settings.py:434
#: sith/settings.py:435
msgid "One semester"
msgstr "Un semestre, 15 €"
#: sith/settings.py:435
#: sith/settings.py:436
msgid "Two semesters"
msgstr "Deux semestres, 28 €"
#: sith/settings.py:437
#: sith/settings.py:438