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..47586f8b
--- /dev/null
+++ b/apps/member/templates/member/profile_trust.html
@@ -0,0 +1,31 @@
+{% 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 %}
+
+{% 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..4775f1ed 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,38 @@ 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..4ae6d51e 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 bccb7629..182bcd4f 100644
--- a/apps/note/models/notes.py
+++ b/apps/note/models/notes.py
@@ -229,16 +229,21 @@ class Trust(models.Model):
Note,
on_delete=models.CASCADE,
related_name='trusting',
- verbose_name=('trusting')
+ verbose_name=_('trusting')
)
trusted = models.ForeignKey(
Note,
on_delete=models.CASCADE,
related_name='trusted',
- verbose_name=('trusted')
+ verbose_name=_('trusted')
)
+ class Meta:
+ verbose_name = _("frienship")
+ verbose_name_plural = _("friendships")
+ unique_together = ("trusting", "trusted")
+
class Alias(models.Model):
"""
diff --git a/apps/note/tables.py b/apps/note/tables.py
index 2cfbcc76..cbe77772 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,27 @@ 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 = {