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 %} +
+

+ {% trans "Note friendships" %} +

+
+ {% if can_create %} +
+ {% csrf_token %} + + {%include "autocomplete_model.html" %} +
+ +
+
+ {% 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 %} -
    -
+