diff --git a/client/src/graphql/gql/mutations/removeMember.gql b/client/src/graphql/gql/mutations/removeMember.gql new file mode 100644 index 00000000..c43074b7 --- /dev/null +++ b/client/src/graphql/gql/mutations/removeMember.gql @@ -0,0 +1,5 @@ +mutation RemoveMember($input: RemoveMemberInput!) { + removeMember(input: $input) { + success + } +} diff --git a/server/users/admin.py b/server/users/admin.py index db39db05..23971f94 100644 --- a/server/users/admin.py +++ b/server/users/admin.py @@ -7,15 +7,24 @@ from .models import User, SchoolClass, Role, UserRole, UserSetting class SchoolClassInline(admin.TabularInline): model = SchoolClass.users.through + extra = 1 class RoleInline(admin.TabularInline): model = UserRole + extra = 1 @admin.register(SchoolClass) class SchoolClassAdmin(admin.ModelAdmin): - list_display = ('name', 'code', 'is_deleted') + list_display = ('name', 'code', 'user_list', 'is_deleted') + + inlines = [ + SchoolClassInline + ] + + def user_list(self, obj): + return ', '.join([s.username for s in obj.users.all()]) @admin.register(Role) diff --git a/server/users/factories.py b/server/users/factories.py index 3c07c478..4ac1e5dc 100644 --- a/server/users/factories.py +++ b/server/users/factories.py @@ -2,7 +2,7 @@ import random import factory -from users.models import SchoolClass +from users.models import SchoolClass, SchoolClassMember class_types = ['DA', 'KV', 'INF', 'EE'] class_suffix = ['A', 'B', 'C', 'D', 'E'] @@ -16,7 +16,8 @@ class SchoolClassFactory(factory.django.DjangoModelFactory): class Meta: model = SchoolClass - name = factory.Sequence(lambda n: '{}{}{}'.format(random.choice(class_types), '18', class_suffix[n % len(class_suffix)])) + name = factory.Sequence( + lambda n: '{}{}{}'.format(random.choice(class_types), '18', class_suffix[n % len(class_suffix)])) is_deleted = False @factory.post_generation @@ -28,4 +29,4 @@ class SchoolClassFactory(factory.django.DjangoModelFactory): if extracted: # A list of groups were passed in, use them for user in extracted: - self.users.add(user) + SchoolClassMember.objects.create(user=user, school_class=self, active=True) diff --git a/server/users/mutations.py b/server/users/mutations.py index 96490bc9..1100f9a7 100644 --- a/server/users/mutations.py +++ b/server/users/mutations.py @@ -3,10 +3,11 @@ from django.contrib.auth import update_session_auth_hash from django.core.exceptions import PermissionDenied from django.db.models import Q from graphene import relay +from graphql_relay import from_global_id from api.utils import get_object from users.inputs import PasswordUpdateInput -from users.models import SchoolClass, UserSetting +from users.models import SchoolClass, UserSetting, User, SchoolClassMember from users.schema import SchoolClassNode from users.serializers import PasswordSerialzer, AvatarUrlSerializer @@ -122,15 +123,40 @@ class JoinClass(relay.ClientIDMutation): try: school_class = SchoolClass.objects.get(Q(code__iexact=code)) - if user not in list(school_class.users.all()): school_class.users.add(user) else: - raise CodeNotFoundException('[CAJ] Schüler ist bereits in Klasse') # CAJ = Class Already Joined + raise CodeNotFoundException('[CAJ] Schüler ist bereits in Klasse') # CAJ = Class Already Joined return cls(success=True, school_class=school_class) except SchoolClass.DoesNotExist: - raise CodeNotFoundException('[CNV] Code ist nicht gültig') # CAV = Code Not Valid + raise CodeNotFoundException('[CNV] Code ist nicht gültig') # CAV = Code Not Valid + + +class RemoveMember(relay.ClientIDMutation): + class Input: + member = graphene.ID(required=True) + school_class = graphene.ID(required=True) + + success = graphene.Boolean() + + @classmethod + def mutate_and_get_payload(cls, root, info, **kwargs): + member_id = kwargs.get('member') + school_class_id = kwargs.get('school_class') + user = info.context.user + + member_pk = from_global_id(member_id)[1] + school_class = get_object(SchoolClass, school_class_id) + + if not user.is_teacher() or not school_class.users.filter(pk=user.pk).exists(): + raise PermissionError('Fehlende Berechtigung') + + school_class_member = SchoolClassMember.objects.get(user__pk=member_pk, school_class=school_class) + school_class_member.active = False + school_class_member.save() + + return cls(success=True) class ProfileMutations: @@ -138,3 +164,4 @@ class ProfileMutations: update_avatar = UpdateAvatar.Field() update_setting = UpdateSetting.Field() join_class = JoinClass.Field() + remove_member = RemoveMember.Field() diff --git a/server/users/tests/test_leave_reenter_class.py b/server/users/tests/test_leave_reenter_class.py new file mode 100644 index 00000000..2db4a35e --- /dev/null +++ b/server/users/tests/test_leave_reenter_class.py @@ -0,0 +1,108 @@ +from django.test import TestCase +from graphene import Context +from graphene.test import Client +from graphql_relay import to_global_id + +from api.utils import get_graphql_mutation +from core.factories import UserFactory +from users.factories import SchoolClassFactory +from users.models import SchoolClass, User, SchoolClassMember +from api.schema import schema +from users.services import create_users + + +class JoinSchoolClassTest(TestCase): + def setUp(self): + self.client = Client(schema=schema) + self.school_class_name = 'Moordale' + + user_data = [ + { + 'teacher': ('Emily', 'Sands',), + 'class': self.school_class_name, + 'code': 'SEXED', + 'students': [ + ('Otis', 'Milburn'), + ('Maeve', 'Wiley'), + ('Adam', 'Groff'), + ('Eric', 'Effiong'), + ('Jackson', 'Marchetti'), + ] + }, + { + 'teacher': ('Colin', 'Hendricks'), + 'class': 'Swing Band', + 'students': [ + ('Ola', 'Nyman'), + ] + } + ] + create_users(user_data) + teacher = User.objects.get(username='emily.sands') + self.teacher_id = to_global_id('UserNode', teacher.pk) + student = User.objects.get(username='adam.groff') + self.student_id = to_global_id('UserNode', student.pk) + other_student = User.objects.get(username='eric.effiong') + self.other_student_id = to_global_id('UserNode', other_student.pk) + + school_class = SchoolClass.objects.get(name=self.school_class_name) + self.school_class_id = to_global_id('SchoolClassNode', school_class.pk) + self.teacher_context = Context(user=teacher) + self.student_context = Context(user=student) + + self.mutation = get_graphql_mutation('removeMember.gql') + + def test_leave_class(self): + self.assertEqual( + SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 6) + self.assertEqual( + SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(), + 0) + result = self.client.execute(self.mutation, variables={ + 'input': { + 'schoolClass': self.school_class_id, + 'member': self.student_id + } + }, context=self.teacher_context) + self.assertIsNone(result.get('errors')) + self.assertEqual( + SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 5) + self.assertEqual( + SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(), + 1) + + def test_leave_class_student_raises_error(self): + self.assertEqual( + SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 6) + self.assertEqual( + SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(), 0) + result = self.client.execute(self.mutation, variables={ + 'input': { + 'schoolClass': self.school_class_id, + 'member': self.other_student_id + } + }, context=self.student_context) + self.assertIsNotNone(result['errors']) + self.assertEqual( + SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 6) + self.assertEqual( + SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(), 0) + + def test_leave_class_other_school_class_raises_error(self): + self.assertEqual( + SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 6) + self.assertEqual( + SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(), 0) + student = User.objects.get(username='ola.nyman') + school_class = SchoolClass.objects.get(name='Swing Band') + result = self.client.execute(self.mutation, variables={ + 'input': { + 'schoolClass': to_global_id('SchoolClassNode', school_class.id), + 'member': to_global_id('UserNode', student.id) + } + }, context=self.teacher_context) + self.assertIsNotNone(result['errors']) + self.assertEqual( + SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 6) + self.assertEqual( + SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(), 0)