diff --git a/apps/member/static/member/js/trust.js b/apps/member/static/member/js/trust.js
new file mode 100644
index 00000000..a16bed08
--- /dev/null
+++ b/apps/member/static/member/js/trust.js
@@ -0,0 +1,53 @@
+/**
+ * On form submit, create a new friendship
+ */
+function create_trust (e) {
+ // Do not submit HTML form
+ e.preventDefault()
+
+ // Get data and send to API
+ const formData = new FormData(e.target)
+ $.getJSON('/api/note/alias/'+formData.get('trusted') + '/',
+ function (trusted_alias) {
+ if ((trusted_alias.note == formData.get('trusting')))
+ {
+ addMsg(gettext("You can't add yourself as a friend"), "danger")
+ return
+ }
+ $.post('/api/note/trust/', {
+ csrfmiddlewaretoken: formData.get('csrfmiddlewaretoken'),
+ trusting: formData.get('trusting'),
+ trusted: trusted_alias.note
+ }).done(function () {
+ // Reload table
+ $('#trust_table').load(location.pathname + ' #trust_table')
+ addMsg(gettext('Friendship successfully added'), 'success')
+ }).fail(function (xhr, _textStatus, _error) {
+ errMsg(xhr.responseJSON)
+ })
+ }).fail(function (xhr, _textStatus, _error) {
+ errMsg(xhr.responseJSON)
+ })
+}
+
+/**
+ * On click of "delete", delete the alias
+ * @param button_id:Integer Alias id to remove
+ */
+function delete_button (button_id) {
+ $.ajax({
+ url: '/api/note/trust/' + button_id + '/',
+ method: 'DELETE',
+ headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
+ }).done(function () {
+ addMsg(gettext('Friendship successfully deleted'), 'success')
+ $('#trust_table').load(location.pathname + ' #trust_table')
+ }).fail(function (xhr, _textStatus, _error) {
+ errMsg(xhr.responseJSON)
+ })
+}
+
+$(document).ready(function () {
+ // Attach event
+ document.getElementById('form_trust').addEventListener('submit', create_trust)
+})
diff --git a/apps/member/templates/member/includes/profile_info.html b/apps/member/templates/member/includes/profile_info.html
index 378d54e2..3a927c9f 100644
--- a/apps/member/templates/member/includes/profile_info.html
+++ b/apps/member/templates/member/includes/profile_info.html
@@ -25,6 +25,14 @@
+
{% trans 'friendships'|capfirst %}
+
+
+
+ {% trans 'Manage friendships' %} ({{ user_object.note.trusting.all|length }})
+
+
+
{% if "member.view_profile"|has_perm:user_object.profile %}
{% trans 'section'|capfirst %}
{{ user_object.profile.section }}
diff --git a/apps/member/templates/member/profile_trust.html b/apps/member/templates/member/profile_trust.html
new file mode 100644
index 00000000..bd8d6b50
--- /dev/null
+++ b/apps/member/templates/member/profile_trust.html
@@ -0,0 +1,41 @@
+{% extends "member/base.html" %}
+{% comment %}
+SPDX-License-Identifier: GPL-3.0-or-later
+{% endcomment %}
+{% load static django_tables2 i18n %}
+
+{% block profile_content %}
+
+
+
+ {% if can_create %}
+
+ {% endif %}
+
+ {% render_table trusting %}
+
+
+
+ {% blocktrans trimmed %}
+ Adding someone as a friend enables them to initiate transactions coming
+ from your account (while keeping your balance positive). This is
+ designed to simplify using note kfet transfers to transfer money between
+ users. The intent is that one person can make all transfers for a group of
+ friends without needing additional rights among them.
+ {% endblocktrans %}
+
+{% endblock %}
+
+{% block extrajavascript %}
+
+
+{% endblock%}
diff --git a/apps/member/urls.py b/apps/member/urls.py
index b1c537d5..54b0f91d 100644
--- a/apps/member/urls.py
+++ b/apps/member/urls.py
@@ -23,5 +23,6 @@ urlpatterns = [
path('user//update/', views.UserUpdateView.as_view(), name="user_update_profile"),
path('user//update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
path('user//aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
+ path('user//trust', views.ProfileTrustView.as_view(), name="user_trust"),
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
]
diff --git a/apps/member/views.py b/apps/member/views.py
index 6ce8d4c5..2f6348a6 100644
--- a/apps/member/views.py
+++ b/apps/member/views.py
@@ -8,6 +8,7 @@ from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.auth.views import LoginView
+from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.db.models import Q, F
from django.shortcuts import redirect
@@ -18,9 +19,9 @@ from django.views.generic import DetailView, UpdateView, TemplateView
from django.views.generic.edit import FormMixin
from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token
-from note.models import Alias, NoteUser, NoteClub
+from note.models import Alias, NoteClub, NoteUser, Trust
from note.models.transactions import Transaction, SpecialTransaction
-from note.tables import HistoryTable, AliasTable
+from note.tables import HistoryTable, AliasTable, TrustTable
from note_kfet.middlewares import _set_current_request
from permission.backends import PermissionBackend
from permission.models import Role
@@ -243,6 +244,39 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return context
+class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
+ """
+ View and manage user trust relationships
+ """
+ model = User
+ template_name = 'member/profile_trust.html'
+ context_object_name = 'user_object'
+ extra_context = {"title": _("Note friendships")}
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ note = context['object'].note
+ context["trusting"] = TrustTable(
+ note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
+ context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
+ trusting=context["object"].note,
+ trusted=context["object"].note
+ ))
+ context["widget"] = {
+ "name": "trusted",
+ "attrs": {
+ "model_pk": ContentType.objects.get_for_model(Alias).pk,
+ "class": "autocomplete form-control",
+ "id": "trusted",
+ "resetable": True,
+ "api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
+ "name_field": "name",
+ "placeholder": ""
+ }
+ }
+ return context
+
+
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
View and manage user aliases.
diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py
index 7dda6dba..33bf75ba 100644
--- a/apps/note/api/serializers.py
+++ b/apps/note/api/serializers.py
@@ -12,7 +12,7 @@ from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
from rest_framework.utils import model_meta
-from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
+from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias, Trust
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
RecurrentTransaction, SpecialTransaction
@@ -77,6 +77,22 @@ class NoteUserSerializer(serializers.ModelSerializer):
return str(obj)
+class TrustSerializer(serializers.ModelSerializer):
+ """
+ REST API Serializer for Trusts.
+ The djangorestframework plugin will analyse the model `Trust` and parse all fields in the API.
+ """
+
+ class Meta:
+ model = Trust
+ fields = '__all__'
+
+ def validate(self, attrs):
+ instance = Trust(**attrs)
+ instance.clean()
+ return attrs
+
+
class AliasSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Aliases.
diff --git a/apps/note/api/urls.py b/apps/note/api/urls.py
index bacf3d32..d15e8241 100644
--- a/apps/note/api/urls.py
+++ b/apps/note/api/urls.py
@@ -2,7 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
- TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet
+ TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet, \
+ TrustViewSet
def register_note_urls(router, path):
@@ -11,6 +12,7 @@ def register_note_urls(router, path):
"""
router.register(path + '/note', NotePolymorphicViewSet)
router.register(path + '/alias', AliasViewSet)
+ router.register(path + '/trust', TrustViewSet)
router.register(path + '/consumer', ConsumerViewSet)
router.register(path + '/transaction/category', TemplateCategoryViewSet)
diff --git a/apps/note/api/views.py b/apps/note/api/views.py
index a228bdf6..34ffaf2d 100644
--- a/apps/note/api/views.py
+++ b/apps/note/api/views.py
@@ -14,8 +14,9 @@ from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSe
from permission.backends import PermissionBackend
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
- TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
-from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial
+ TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer, \
+ TrustSerializer
+from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial, Trust
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
@@ -56,11 +57,41 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
return queryset.order_by("id")
+class TrustViewSet(ReadProtectedModelViewSet):
+ """
+ REST Trust View set.
+ The djangorestframework plugin will get all `Trust` objects, serialize it to JSON with the given serializer,
+ then render it on /api/note/trust/
+ """
+ queryset = Trust.objects
+ serializer_class = TrustSerializer
+ filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
+ search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
+ '$trusted__alias__name', '$trusted__alias__normalized_name']
+ filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
+ ordering_fields = ['trusting', 'trusted', ]
+
+ def get_serializer_class(self):
+ serializer_class = self.serializer_class
+ if self.request.method in ['PUT', 'PATCH']:
+ # trust relationship can't change people involved
+ serializer_class.Meta.read_only_fields = ('trusting', 'trusting',)
+ return serializer_class
+
+ def destroy(self, request, *args, **kwargs):
+ instance = self.get_object()
+ try:
+ self.perform_destroy(instance)
+ except ValidationError as e:
+ return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
class AliasViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
- then render it on /api/aliases/
+ then render it on /api/note/aliases/
"""
queryset = Alias.objects
serializer_class = AliasSerializer
diff --git a/apps/note/migrations/0006_trust.py b/apps/note/migrations/0006_trust.py
new file mode 100644
index 00000000..4ed059fb
--- /dev/null
+++ b/apps/note/migrations/0006_trust.py
@@ -0,0 +1,27 @@
+# Generated by Django 2.2.24 on 2021-09-05 19:16
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('note', '0005_auto_20210313_1235'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Trust',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('trusted', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusted', to='note.Note', verbose_name='trusted')),
+ ('trusting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusting', to='note.Note', verbose_name='trusting')),
+ ],
+ options={
+ 'verbose_name': 'frienship',
+ 'verbose_name_plural': 'friendships',
+ 'unique_together': {('trusting', 'trusted')},
+ },
+ ),
+ ]
diff --git a/apps/note/models/__init__.py b/apps/note/models/__init__.py
index 07a1d6e0..ab5d4ff1 100644
--- a/apps/note/models/__init__.py
+++ b/apps/note/models/__init__.py
@@ -1,13 +1,13 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
-from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
+from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
from .transactions import MembershipTransaction, Transaction, \
TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction
__all__ = [
# Notes
- 'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
+ 'Alias', 'Trust', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
# Transactions
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
'RecurrentTransaction', 'SpecialTransaction',
diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py
index f760882b..6db9e5f8 100644
--- a/apps/note/models/notes.py
+++ b/apps/note/models/notes.py
@@ -217,6 +217,38 @@ class NoteSpecial(Note):
return self.special_type
+class Trust(models.Model):
+ """
+ A one-sided trust relationship bertween two users
+
+ If another user considers you as your friend, you can transfer money from
+ them
+ """
+
+ trusting = models.ForeignKey(
+ Note,
+ on_delete=models.CASCADE,
+ related_name='trusting',
+ verbose_name=_('trusting')
+ )
+
+ trusted = models.ForeignKey(
+ Note,
+ on_delete=models.CASCADE,
+ related_name='trusted',
+ verbose_name=_('trusted')
+ )
+
+ class Meta:
+ verbose_name = _("frienship")
+ verbose_name_plural = _("friendships")
+ unique_together = ("trusting", "trusted")
+
+ def __str__(self):
+ return _("Friendship between {trusting} and {trusted}").format(
+ trusting=str(self.trusting), trusted=str(self.trusted))
+
+
class Alias(models.Model):
"""
points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance.
diff --git a/apps/note/tables.py b/apps/note/tables.py
index 2cfbcc76..1e94a39f 100644
--- a/apps/note/tables.py
+++ b/apps/note/tables.py
@@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
-from .models.notes import Alias
+from .models.notes import Alias, Trust
from .models.transactions import Transaction, TransactionTemplate
from .templatetags.pretty_money import pretty_money
@@ -148,6 +148,31 @@ DELETE_TEMPLATE = """
"""
+class TrustTable(tables.Table):
+ class Meta:
+ attrs = {
+ 'class': 'table table condensed table-striped',
+ 'id': "trust_table"
+ }
+ model = Trust
+ fields = ("trusted",)
+ template_name = 'django_tables2/bootstrap4.html'
+
+ show_header = False
+ trusted = tables.Column(attrs={'td': {'class': 'text_center'}})
+
+ delete_col = tables.TemplateColumn(
+ template_code=DELETE_TEMPLATE,
+ extra_context={"delete_trans": _('delete')},
+ attrs={
+ 'td': {
+ 'class': lambda record: 'col-sm-1'
+ + (' d-none' if not PermissionBackend.check_perm(
+ get_current_request(), "note.delete_trust", record)
+ else '')}},
+ verbose_name=_("Delete"),)
+
+
class AliasTable(tables.Table):
class Meta:
attrs = {
diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json
index ddd5b0d2..1335b3aa 100644
--- a/apps/permission/fixtures/initial.json
+++ b/apps/permission/fixtures/initial.json
@@ -2967,6 +2967,118 @@
"description": "Supprimer une application OAuth2"
}
},
+ {
+ "model": "permission.permission",
+ "pk": 190,
+ "fields": {
+ "model": [
+ "note",
+ "trust"
+ ],
+ "query": "{\"trusting\": [\"user\", \"note\"]}",
+ "type": "delete",
+ "mask": 1,
+ "field": "",
+ "permanent": false,
+ "description": "Supprimer une amitié à sa note"
+ }
+ },
+ {
+ "model": "permission.permission",
+ "pk": 191,
+ "fields": {
+ "model": [
+ "note",
+ "trust"
+ ],
+ "query": "{\"trusting\": [\"user\", \"note\"]}",
+ "type": "add",
+ "mask": 1,
+ "field": "",
+ "permanent": false,
+ "description": "Ajouter une amitié à sa note"
+ }
+ },
+ {
+ "model": "permission.permission",
+ "pk": 192,
+ "fields": {
+ "model": [
+ "note",
+ "trust"
+ ],
+ "query": "{\"trusting__is_active\": true}",
+ "type": "add",
+ "mask": 1,
+ "field": "",
+ "permanent": false,
+ "description": "Ajouter une amitié à une note non bloquée"
+ }
+ },
+ {
+ "model": "permission.permission",
+ "pk": 193,
+ "fields": {
+ "model": [
+ "note",
+ "trust"
+ ],
+ "query": "{\"trusting__is_active\": true}",
+ "type": "delete",
+ "mask": 3,
+ "field": "",
+ "permanent": false,
+ "description": "Supprimer une amitié à une note non bloquée"
+ }
+ },
+ {
+ "model": "permission.permission",
+ "pk": 194,
+ "fields": {
+ "model": [
+ "note",
+ "trust"
+ ],
+ "query": "{}",
+ "type": "view",
+ "mask": 3,
+ "field": "",
+ "permanent": false,
+ "description": "Voir toutes les amitiés, y compris celles des non adhérents"
+ }
+ },
+ {
+ "model": "permission.permission",
+ "pk": 195,
+ "fields": {
+ "model": [
+ "note",
+ "trust"
+ ],
+ "query": "{\"trusting__noteuser__user\": [\"user\"]}",
+ "type": "view",
+ "mask": 1,
+ "field": "",
+ "permanent": true,
+ "description": "Voir ses propres amitiés, pour toujours"
+ }
+ },
+ {
+ "model": "permission.permission",
+ "pk": 196,
+ "fields": {
+ "model": [
+ "note",
+ "transaction"
+ ],
+ "query": "[\"AND\", {\"source__trusting__trusted\": [\"user\", \"note\"]}, [\"OR\", {\"source__balance__gte\": {\"F\": [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]]}}, {\"valid\": false}]]",
+ "type": "add",
+ "mask": 1,
+ "field": "",
+ "permanent": false,
+ "description": "Transférer de l'argent depuis une note amie en restant positif"
+ }
+ },
{
"model": "permission.role",
"pk": 1,
@@ -3001,7 +3113,11 @@
186,
187,
188,
- 189
+ 189,
+ 190,
+ 191,
+ 195,
+ 196
]
}
},
@@ -3042,7 +3158,9 @@
158,
159,
160,
- 179
+ 179,
+ 189,
+ 190
]
}
},
@@ -3192,7 +3310,10 @@
176,
177,
178,
- 183
+ 188,
+ 183,
+ 186,
+ 187
]
}
},
@@ -3386,7 +3507,14 @@
186,
187,
188,
- 189
+ 189,
+ 190,
+ 191,
+ 192,
+ 193,
+ 194,
+ 195,
+ 196
]
}
},
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 4ee77566..528a5976 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-07 22:55+0200\n"
+"POT-Creation-Date: 2022-04-08 18:55+0200\n"
"PO-Revision-Date: 2020-11-16 20:02+0000\n"
"Last-Translator: Yohann D'ANELLO \n"
"Language-Team: French \n"
@@ -56,7 +56,7 @@ msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité."
#: apps/member/models.py:199
#: apps/member/templates/member/includes/club_info.html:4
#: apps/member/templates/member/includes/profile_info.html:4
-#: apps/note/models/notes.py:231 apps/note/models/transactions.py:26
+#: apps/note/models/notes.py:263 apps/note/models/transactions.py:26
#: apps/note/models/transactions.py:46 apps/note/models/transactions.py:301
#: apps/permission/models.py:330
#: apps/registration/templates/registration/future_profile_detail.html:16
@@ -114,7 +114,7 @@ msgstr "Lieu où l'activité est organisée, par exemple la Kfet."
msgid "type"
msgstr "type"
-#: apps/activity/models.py:89 apps/logs/models.py:22 apps/member/models.py:305
+#: apps/activity/models.py:89 apps/logs/models.py:22 apps/member/models.py:307
#: apps/note/models/notes.py:148 apps/treasury/models.py:285
#: apps/wei/models.py:173 apps/wei/templates/wei/attribute_bus_1A.html:13
#: apps/wei/templates/wei/survey.html:15
@@ -295,7 +295,7 @@ msgstr "Invité supprimé"
#: apps/note/models/transactions.py:257
#: apps/note/templates/note/transaction_form.html:17
#: apps/note/templates/note/transaction_form.html:152
-#: note_kfet/templates/base.html:73
+#: note_kfet/templates/base.html:72
msgid "Transfer"
msgstr "Virement"
@@ -388,7 +388,7 @@ msgid "validate"
msgstr "valider"
#: apps/activity/templates/activity/includes/activity_info.html:71
-#: apps/logs/models.py:64 apps/note/tables.py:195
+#: apps/logs/models.py:64 apps/note/tables.py:220
msgid "edit"
msgstr "modifier"
@@ -400,7 +400,7 @@ msgstr "Inviter"
msgid "Create new activity"
msgstr "Créer une nouvelle activité"
-#: apps/activity/views.py:67 note_kfet/templates/base.html:91
+#: apps/activity/views.py:67 note_kfet/templates/base.html:90
msgid "Activities"
msgstr "Activités"
@@ -466,9 +466,9 @@ msgstr "nouvelles données"
msgid "create"
msgstr "créer"
-#: apps/logs/models.py:65 apps/note/tables.py:165 apps/note/tables.py:211
-#: apps/permission/models.py:127 apps/treasury/tables.py:38
-#: apps/wei/tables.py:74
+#: apps/logs/models.py:65 apps/note/tables.py:166 apps/note/tables.py:190
+#: apps/note/tables.py:237 apps/permission/models.py:127
+#: apps/treasury/tables.py:38 apps/wei/tables.py:74
msgid "delete"
msgstr "supprimer"
@@ -507,11 +507,11 @@ msgstr "cotisation pour adhérer (normalien élève)"
msgid "membership fee (unpaid students)"
msgstr "cotisation pour adhérer (normalien étudiant)"
-#: apps/member/admin.py:65 apps/member/models.py:317
+#: apps/member/admin.py:65 apps/member/models.py:319
msgid "roles"
msgstr "rôles"
-#: apps/member/admin.py:66 apps/member/models.py:331
+#: apps/member/admin.py:66 apps/member/models.py:333
msgid "fee"
msgstr "cotisation"
@@ -547,7 +547,7 @@ msgstr "Taille maximale : 2 Mo"
msgid "This image cannot be loaded."
msgstr "Cette image ne peut pas être chargée."
-#: apps/member/forms.py:141 apps/member/views.py:102
+#: apps/member/forms.py:141 apps/member/views.py:103
#: apps/registration/forms.py:33 apps/registration/views.py:262
msgid "An alias with a similar name already exists."
msgstr "Un alias avec un nom similaire existe déjà."
@@ -610,14 +610,14 @@ msgid "hash"
msgstr "haché"
#: apps/member/models.py:38
-#: apps/member/templates/member/includes/profile_info.html:35
+#: apps/member/templates/member/includes/profile_info.html:43
#: apps/registration/templates/registration/future_profile_detail.html:40
#: apps/wei/templates/wei/weimembership_form.html:44
msgid "phone number"
msgstr "numéro de téléphone"
#: apps/member/models.py:45
-#: apps/member/templates/member/includes/profile_info.html:29
+#: apps/member/templates/member/includes/profile_info.html:37
#: apps/registration/templates/registration/future_profile_detail.html:34
#: apps/wei/templates/wei/weimembership_form.html:38
msgid "section"
@@ -705,14 +705,14 @@ msgid "Year of entry to the school (None if not ENS student)"
msgstr "Année d'entrée dans l'école (None si non-étudiant·e de l'ENS)"
#: apps/member/models.py:83
-#: apps/member/templates/member/includes/profile_info.html:39
+#: apps/member/templates/member/includes/profile_info.html:47
#: apps/registration/templates/registration/future_profile_detail.html:37
#: apps/wei/templates/wei/weimembership_form.html:41
msgid "address"
msgstr "adresse"
#: apps/member/models.py:90
-#: apps/member/templates/member/includes/profile_info.html:42
+#: apps/member/templates/member/includes/profile_info.html:50
#: apps/registration/templates/registration/future_profile_detail.html:43
#: apps/wei/templates/wei/weimembership_form.html:47
msgid "paid"
@@ -784,7 +784,7 @@ msgstr "Activez votre compte Note Kfet"
#: apps/member/models.py:204
#: apps/member/templates/member/includes/club_info.html:55
-#: apps/member/templates/member/includes/profile_info.html:32
+#: apps/member/templates/member/includes/profile_info.html:40
#: apps/registration/templates/registration/future_profile_detail.html:22
#: apps/wei/templates/wei/base.html:70
#: apps/wei/templates/wei/weimembership_form.html:20
@@ -833,46 +833,46 @@ msgstr ""
"Date maximale d'une fin d'adhésion, après laquelle les adhérents doivent la "
"renouveler."
-#: apps/member/models.py:286 apps/member/models.py:311
+#: apps/member/models.py:288 apps/member/models.py:313
#: apps/note/models/notes.py:176
msgid "club"
msgstr "club"
-#: apps/member/models.py:287
+#: apps/member/models.py:289
msgid "clubs"
msgstr "clubs"
-#: apps/member/models.py:322
+#: apps/member/models.py:324
msgid "membership starts on"
msgstr "l'adhésion commence le"
-#: apps/member/models.py:326
+#: apps/member/models.py:328
msgid "membership ends on"
msgstr "l'adhésion finit le"
-#: apps/member/models.py:428
+#: apps/member/models.py:430
#, python-brace-format
msgid "The role {role} does not apply to the club {club}."
msgstr "Le rôle {role} ne s'applique pas au club {club}."
-#: apps/member/models.py:437 apps/member/views.py:651
+#: apps/member/models.py:439 apps/member/views.py:712
msgid "User is already a member of the club"
msgstr "L'utilisateur est déjà membre du club"
-#: apps/member/models.py:449 apps/member/views.py:660
+#: apps/member/models.py:451 apps/member/views.py:721
msgid "User is not a member of the parent club"
msgstr "L'utilisateur n'est pas membre du club parent"
-#: apps/member/models.py:502
+#: apps/member/models.py:504
#, python-brace-format
msgid "Membership of {user} for the club {club}"
msgstr "Adhésion de {user} pour le club {club}"
-#: apps/member/models.py:505 apps/note/models/transactions.py:389
+#: apps/member/models.py:507 apps/note/models/transactions.py:389
msgid "membership"
msgstr "adhésion"
-#: apps/member/models.py:506
+#: apps/member/models.py:508
msgid "memberships"
msgstr "adhésions"
@@ -924,7 +924,7 @@ msgid "Account #"
msgstr "Compte n°"
#: apps/member/templates/member/base.html:48
-#: apps/member/templates/member/base.html:62 apps/member/views.py:59
+#: apps/member/templates/member/base.html:62 apps/member/views.py:60
#: apps/registration/templates/registration/future_profile_detail.html:48
#: apps/wei/templates/wei/weimembership_form.html:117
msgid "Update Profile"
@@ -985,13 +985,14 @@ msgstr ""
"seront à nouveau possible."
#: apps/member/templates/member/club_alias.html:10
-#: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:253
-#: apps/member/views.py:456
+#: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:287
+#: apps/member/views.py:517
msgid "Note aliases"
msgstr "Alias de la note"
#: apps/member/templates/member/club_alias.html:20
#: apps/member/templates/member/profile_alias.html:19
+#: apps/member/templates/member/profile_trust.html:19
#: apps/treasury/tables.py:99
#: apps/treasury/templates/treasury/sogecredit_list.html:34
#: apps/treasury/templates/treasury/sogecredit_list.html:73
@@ -1044,7 +1045,7 @@ msgid "membership fee"
msgstr "cotisation pour adhérer"
#: apps/member/templates/member/includes/club_info.html:43
-#: apps/member/templates/member/includes/profile_info.html:47
+#: apps/member/templates/member/includes/profile_info.html:55
#: apps/treasury/templates/treasury/sogecredit_detail.html:24
#: apps/wei/templates/wei/base.html:60
msgid "balance"
@@ -1052,7 +1053,7 @@ msgstr "solde du compte"
#: apps/member/templates/member/includes/club_info.html:47
#: apps/member/templates/member/includes/profile_info.html:20
-#: apps/note/models/notes.py:255 apps/wei/templates/wei/base.html:66
+#: apps/note/models/notes.py:287 apps/wei/templates/wei/base.html:66
msgid "aliases"
msgstr "alias"
@@ -1076,7 +1077,16 @@ msgstr "mot de passe"
msgid "Change password"
msgstr "Changer le mot de passe"
-#: apps/member/templates/member/includes/profile_info.html:55
+#: apps/member/templates/member/includes/profile_info.html:28
+#: apps/note/models/notes.py:244
+msgid "friendships"
+msgstr "amitiés"
+
+#: apps/member/templates/member/includes/profile_info.html:32
+msgid "Manage friendships"
+msgstr "Gérer les amitiés"
+
+#: apps/member/templates/member/includes/profile_info.html:63
msgid "API token"
msgstr "Accès API"
@@ -1148,6 +1158,23 @@ msgstr "Cliquez ici pour renvoyer un lien de validation."
msgid "View my memberships"
msgstr "Voir mes adhésions"
+#: apps/member/templates/member/profile_trust.html:10 apps/member/views.py:254
+msgid "Note friendships"
+msgstr "Amitiés note"
+
+#: apps/member/templates/member/profile_trust.html:28
+msgid ""
+"Adding someone as a friend enables them to initiate transactions coming from "
+"your account (while keeping your balance positive). This is designed to "
+"simplify using note kfet transfers to transfer money between users. The "
+"intent is that one person can make all transfers for a group of friends "
+"without needing additional rights among them."
+msgstr ""
+"Ajouter quelqu'un⋅e en ami⋅e lui permet de me prélever de l'argent (tant que "
+"ma note reste positive). Ceci sert à simplifier les remboursements entre "
+"ami⋅es via note. En effet, une personne peut effectuer tous les transferts "
+"sans posséder de droits supplémentaires."
+
#: apps/member/templates/member/profile_update.html:18
msgid "Save Changes"
msgstr "Sauvegarder les changements"
@@ -1156,47 +1183,47 @@ msgstr "Sauvegarder les changements"
msgid "Registrations"
msgstr "Inscriptions"
-#: apps/member/views.py:72 apps/registration/forms.py:23
+#: apps/member/views.py:73 apps/registration/forms.py:23
msgid "This address must be valid."
msgstr "Cette adresse doit être valide."
-#: apps/member/views.py:139
+#: apps/member/views.py:140
msgid "Profile detail"
msgstr "Détails de l'utilisateur"
-#: apps/member/views.py:205
+#: apps/member/views.py:206
msgid "Search user"
msgstr "Chercher un utilisateur"
-#: apps/member/views.py:273
+#: apps/member/views.py:308
msgid "Update note picture"
msgstr "Modifier la photo de la note"
-#: apps/member/views.py:319
+#: apps/member/views.py:354
msgid "Manage auth token"
msgstr "Gérer les jetons d'authentification"
-#: apps/member/views.py:346
+#: apps/member/views.py:381
msgid "Create new club"
msgstr "Créer un nouveau club"
-#: apps/member/views.py:365
+#: apps/member/views.py:400
msgid "Search club"
msgstr "Chercher un club"
-#: apps/member/views.py:398
+#: apps/member/views.py:433
msgid "Club detail"
msgstr "Détails du club"
-#: apps/member/views.py:479
+#: apps/member/views.py:540
msgid "Update club"
msgstr "Modifier le club"
-#: apps/member/views.py:513
+#: apps/member/views.py:574
msgid "Add new member to the club"
msgstr "Ajouter un nouveau membre au club"
-#: apps/member/views.py:642 apps/wei/views.py:973
+#: apps/member/views.py:703 apps/wei/views.py:973
msgid ""
"This user don't have enough money to join this club, and can't have a "
"negative balance."
@@ -1204,19 +1231,19 @@ msgstr ""
"Cet utilisateur n'a pas assez d'argent pour rejoindre ce club et ne peut pas "
"avoir un solde négatif."
-#: apps/member/views.py:664
+#: apps/member/views.py:725
msgid "The membership must start after {:%m-%d-%Y}."
msgstr "L'adhésion doit commencer après le {:%d/%m/%Y}."
-#: apps/member/views.py:669
+#: apps/member/views.py:730
msgid "The membership must begin before {:%m-%d-%Y}."
msgstr "L'adhésion doit commencer avant le {:%d/%m/%Y}."
-#: apps/member/views.py:815
+#: apps/member/views.py:876
msgid "Manage roles of an user in the club"
msgstr "Gérer les rôles d'un utilisateur dans le club"
-#: apps/member/views.py:840
+#: apps/member/views.py:901
msgid "Members of the club"
msgstr "Membres du club"
@@ -1234,7 +1261,7 @@ msgstr "destination"
msgid "amount"
msgstr "montant"
-#: apps/note/api/serializers.py:183 apps/note/api/serializers.py:189
+#: apps/note/api/serializers.py:199 apps/note/api/serializers.py:205
#: apps/note/models/transactions.py:228
msgid ""
"The transaction can't be saved since the source note or the destination note "
@@ -1366,30 +1393,47 @@ msgstr "note spéciale"
msgid "special notes"
msgstr "notes spéciales"
-#: apps/note/models/notes.py:237
+#: apps/note/models/notes.py:232
+msgid "trusting"
+msgstr "note"
+
+#: apps/note/models/notes.py:239
+msgid "trusted"
+msgstr "ami"
+
+#: apps/note/models/notes.py:243
+msgid "frienship"
+msgstr "amitié"
+
+#: apps/note/models/notes.py:248
+#, python-brace-format
+msgid "Friendship between {trusting} and {trusted}"
+msgstr "Amitié entre {trusting} et {trusted}"
+
+#: apps/note/models/notes.py:269
msgid "Invalid alias"
msgstr "Alias invalide"
-#: apps/note/models/notes.py:254
+#: apps/note/models/notes.py:286
msgid "alias"
msgstr "alias"
-#: apps/note/models/notes.py:278
+#: apps/note/models/notes.py:310
msgid "Alias is too long."
msgstr "L'alias est trop long."
-#: apps/note/models/notes.py:281
+#: apps/note/models/notes.py:313
msgid ""
"This alias contains only complex character. Please use a more simple alias."
msgstr ""
"Cet alias ne contient que des caractères complexes. Merci d'utiliser un "
"alias plus simple."
-#: apps/note/models/notes.py:285
+#: apps/note/models/notes.py:317
msgid "An alias with a similar name already exists: {} "
msgstr "Un alias avec un nom similaire existe déjà : {} "
-#: apps/note/models/notes.py:299
+#: apps/note/models/notes.py:331
msgid "You can't delete your main alias."
msgstr "Vous ne pouvez pas supprimer votre alias principal."
@@ -1535,7 +1579,8 @@ msgstr "Cliquez pour valider"
msgid "No reason specified"
msgstr "Pas de motif spécifié"
-#: apps/note/tables.py:169 apps/note/tables.py:213 apps/treasury/tables.py:39
+#: apps/note/tables.py:173 apps/note/tables.py:194 apps/note/tables.py:239
+#: apps/treasury/tables.py:39
#: apps/treasury/templates/treasury/invoice_confirm_delete.html:30
#: apps/treasury/templates/treasury/sogecredit_detail.html:65
#: apps/wei/tables.py:75 apps/wei/tables.py:118
@@ -1546,7 +1591,7 @@ msgstr "Pas de motif spécifié"
msgid "Delete"
msgstr "Supprimer"
-#: apps/note/tables.py:197 apps/note/templates/note/conso_form.html:132
+#: apps/note/tables.py:222 apps/note/templates/note/conso_form.html:132
#: apps/wei/tables.py:49 apps/wei/tables.py:50
#: apps/wei/templates/wei/base.html:89
#: apps/wei/templates/wei/bus_detail.html:20
@@ -1556,7 +1601,7 @@ msgstr "Supprimer"
msgid "Edit"
msgstr "Éditer"
-#: apps/note/tables.py:201 apps/note/tables.py:224
+#: apps/note/tables.py:226 apps/note/tables.py:253
msgid "Hide/Show"
msgstr "Afficher/Masquer"
@@ -1717,7 +1762,7 @@ msgstr "Chercher un bouton"
msgid "Update button"
msgstr "Modifier le bouton"
-#: apps/note/views.py:151 note_kfet/templates/base.html:67
+#: apps/note/views.py:151 note_kfet/templates/base.html:66
msgid "Consumptions"
msgstr "Consommations"
@@ -1915,7 +1960,7 @@ msgstr ""
"Vous n'avez pas la permission d'ajouter une instance du modèle « {model} » "
"avec ces paramètres. Merci de les corriger et de réessayer."
-#: apps/permission/views.py:112 note_kfet/templates/base.html:109
+#: apps/permission/views.py:112 note_kfet/templates/base.html:108
msgid "Rights"
msgstr "Droits"
@@ -2122,7 +2167,7 @@ msgstr ""
msgid "Invalidate pre-registration"
msgstr "Invalider l'inscription"
-#: apps/treasury/apps.py:12 note_kfet/templates/base.html:97
+#: apps/treasury/apps.py:12 note_kfet/templates/base.html:96
msgid "Treasury"
msgstr "Trésorerie"
@@ -2530,7 +2575,7 @@ msgstr "Gérer les crédits de la Société générale"
#: apps/wei/apps.py:10 apps/wei/models.py:50 apps/wei/models.py:51
#: apps/wei/models.py:62 apps/wei/models.py:180
-#: note_kfet/templates/base.html:103
+#: note_kfet/templates/base.html:102
msgid "WEI"
msgstr "WEI"
@@ -2579,7 +2624,7 @@ msgstr "Sélectionnez les rôles qui vous intéressent."
msgid "This team doesn't belong to the given bus."
msgstr "Cette équipe n'appartient pas à ce bus."
-#: apps/wei/forms/surveys/wei2021.py:35
+#: apps/wei/forms/surveys/wei2021.py:35 apps/wei/forms/surveys/wei2022.py:35
msgid "Choose a word:"
msgstr "Choisissez un mot :"
@@ -3140,19 +3185,19 @@ msgstr "Répartir les 1A dans les bus"
msgid "Attribute bus"
msgstr "Attribuer un bus"
-#: note_kfet/settings/base.py:161
+#: note_kfet/settings/base.py:172
msgid "German"
msgstr "Allemand"
-#: note_kfet/settings/base.py:162
+#: note_kfet/settings/base.py:173
msgid "English"
msgstr "Anglais"
-#: note_kfet/settings/base.py:163
+#: note_kfet/settings/base.py:174
msgid "Spanish"
msgstr "Espagnol"
-#: note_kfet/settings/base.py:164
+#: note_kfet/settings/base.py:175
msgid "French"
msgstr "Français"
@@ -3209,7 +3254,7 @@ msgstr ""
"erreur, qui sera corrigée rapidement. Vous pouvez désormais aller boire une "
"bière."
-#: note_kfet/templates/autocomplete_model.html:14
+#: note_kfet/templates/autocomplete_model.html:15
msgid "Reset"
msgstr "Réinitialiser"
@@ -3217,34 +3262,34 @@ msgstr "Réinitialiser"
msgid "The ENS Paris-Saclay BDE note."
msgstr "La note du BDE de l'ENS Paris-Saclay."
-#: note_kfet/templates/base.html:79
+#: note_kfet/templates/base.html:78
msgid "Users"
msgstr "Utilisateurs"
-#: note_kfet/templates/base.html:85
+#: note_kfet/templates/base.html:84
msgid "Clubs"
msgstr "Clubs"
-#: note_kfet/templates/base.html:114
+#: note_kfet/templates/base.html:113
msgid "Admin"
msgstr "Admin"
-#: note_kfet/templates/base.html:128
+#: note_kfet/templates/base.html:127
msgid "My account"
msgstr "Mon compte"
-#: note_kfet/templates/base.html:131
+#: note_kfet/templates/base.html:130
msgid "Log out"
msgstr "Se déconnecter"
-#: note_kfet/templates/base.html:139
+#: note_kfet/templates/base.html:138
#: note_kfet/templates/registration/signup.html:6
#: note_kfet/templates/registration/signup.html:11
#: note_kfet/templates/registration/signup.html:28
msgid "Sign up"
msgstr "Inscription"
-#: note_kfet/templates/base.html:146
+#: note_kfet/templates/base.html:145
#: note_kfet/templates/registration/login.html:6
#: note_kfet/templates/registration/login.html:15
#: note_kfet/templates/registration/login.html:38
@@ -3252,7 +3297,7 @@ msgstr "Inscription"
msgid "Log in"
msgstr "Se connecter"
-#: note_kfet/templates/base.html:160
+#: note_kfet/templates/base.html:159
msgid ""
"You are not a BDE member anymore. Please renew your membership if you want "
"to use the note."
@@ -3260,7 +3305,7 @@ msgstr ""
"Vous n'êtes plus adhérent BDE. Merci de réadhérer si vous voulez profiter de "
"la note."
-#: note_kfet/templates/base.html:166
+#: note_kfet/templates/base.html:165
msgid ""
"Your e-mail address is not validated. Please check your mail inbox and click "
"on the validation link."
@@ -3268,7 +3313,7 @@ msgstr ""
"Votre adresse e-mail n'est pas validée. Merci de vérifier votre boîte mail "
"et de cliquer sur le lien de validation."
-#: note_kfet/templates/base.html:172
+#: note_kfet/templates/base.html:171
msgid ""
"You declared that you opened a bank account in the Société générale. The "
"bank did not validate the creation of the account to the BDE, so the "
@@ -3282,11 +3327,11 @@ msgstr ""
"vérification peut durer quelques jours. Merci de vous assurer de bien aller "
"au bout de vos démarches."
-#: note_kfet/templates/base.html:195
+#: note_kfet/templates/base.html:194
msgid "Contact us"
msgstr "Nous contacter"
-#: note_kfet/templates/base.html:197
+#: note_kfet/templates/base.html:196
msgid "Technical Support"
msgstr "Support technique"
diff --git a/note_kfet/static/js/autocomplete_model.js b/note_kfet/static/js/autocomplete_model.js
index f7aafbc6..2a2677d4 100644
--- a/note_kfet/static/js/autocomplete_model.js
+++ b/note_kfet/static/js/autocomplete_model.js
@@ -13,21 +13,29 @@ $(document).ready(function () {
$('#' + prefix + '_reset').removeClass('d-none')
$.getJSON(api_url + (api_url.includes('?') ? '&' : '?') + 'format=json&search=^' + input + api_url_suffix, function (objects) {
- let html = ''
+ let html = ''
objects.results.forEach(function (obj) {
html += li(prefix + '_' + obj.id, obj[name_field])
})
+ html += '
'
- const results_list = $('#' + prefix + '_list')
- results_list.html(html)
+ target.tooltip({
+ html: true,
+ placement: 'bottom',
+ trigger: 'manual',
+ container: target.parent(),
+ fallbackPlacement: 'clockwise'
+ })
+
+ target.attr("data-original-title", html).tooltip("show")
objects.results.forEach(function (obj) {
$('#' + prefix + '_' + obj.id).click(function () {
target.val(obj[name_field])
$('#' + prefix + '_pk').val(obj.id)
- results_list.html('')
+ target.tooltip("hide")
target.removeClass('is-invalid')
target.addClass('is-valid')
@@ -37,8 +45,8 @@ $(document).ready(function () {
if (input === obj[name_field]) { $('#' + prefix + '_pk').val(obj.id) }
})
- if (results_list.children().length === 1 && e.originalEvent.keyCode >= 32) {
- results_list.children().first().trigger('click')
+ if (objects.results.length === 1 && e.originalEvent.keyCode >= 32) {
+ $('#' + prefix + '_' + objects.results[0].id).trigger('click')
}
})
})
diff --git a/note_kfet/templates/autocomplete_model.html b/note_kfet/templates/autocomplete_model.html
index 0e5d17cd..20c8041e 100644
--- a/note_kfet/templates/autocomplete_model.html
+++ b/note_kfet/templates/autocomplete_model.html
@@ -9,9 +9,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
name="{{ widget.name }}_name" autocomplete="off"
{% for name, value in widget.attrs.items %}
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
- {% endfor %}>
+ {% endfor %}
+ aria-describedby="{{widget.attrs.id}}_tooltip">
{% if widget.resetable %}
{% trans "Reset" %}
{% endif %}
-
+