From 442a5c5e364db6397cb8bbc9a281d2b5da272cba Mon Sep 17 00:00:00 2001 From: Nicolas Margulies Date: Sun, 5 Sep 2021 21:55:22 +0200 Subject: [PATCH] First proro of trusting, with models and front, but no additional permissions --- apps/member/static/member/js/trust.js | 53 +++++++++++++++++++ .../member/includes/profile_info.html | 8 +++ .../templates/member/profile_trust.html | 31 +++++++++++ apps/member/urls.py | 1 + apps/member/views.py | 37 ++++++++++++- apps/note/api/serializers.py | 18 ++++++- apps/note/api/urls.py | 4 +- apps/note/api/views.py | 37 +++++++++++-- apps/note/migrations/0006_trust.py | 27 ++++++++++ apps/note/models/__init__.py | 4 +- apps/note/models/notes.py | 9 +++- apps/note/tables.py | 23 +++++++- 12 files changed, 240 insertions(+), 12 deletions(-) create mode 100644 apps/member/static/member/js/trust.js create mode 100644 apps/member/templates/member/profile_trust.html create mode 100644 apps/note/migrations/0006_trust.py 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 %} +
+

+ {% trans "Note friendships" %} +

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