From 4e1ab68a5203931325e7ba2d851c76842f3f6f7e Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Wed, 24 Mar 2021 23:43:36 +0100 Subject: [PATCH] Add team model, team node and a mutation for creating a team --- server/core/factories.py | 12 +++- .../commands/export_schema_graphql.py | 4 +- server/users/factories.py | 11 ++- .../migrations/0026_auto_20210324_2126.py | 33 +++++++++ server/users/models.py | 53 ++++++++++----- server/users/mutations.py | 27 +++++++- server/users/schema.py | 22 ++++-- server/users/tests/test_teams.py | 68 +++++++++++++++++++ 8 files changed, 203 insertions(+), 27 deletions(-) create mode 100644 server/users/migrations/0026_auto_20210324_2126.py create mode 100644 server/users/tests/test_teams.py diff --git a/server/core/factories.py b/server/core/factories.py index 72665bf5..f88e4bf2 100644 --- a/server/core/factories.py +++ b/server/core/factories.py @@ -10,6 +10,8 @@ from faker import Faker from wagtail.documents.models import get_document_model from wagtail.images import get_image_model +from users.models import Role, UserRole + fake = Faker('de_CH') @@ -48,7 +50,7 @@ class DummyImageFactory(factory.DjangoModelFactory): class UserFactory(factory.django.DjangoModelFactory): class Meta: model = get_user_model() - django_get_or_create = ('username', ) + django_get_or_create = ('username',) first_name = factory.LazyAttribute(lambda x: fake.first_name()) last_name = factory.LazyAttribute(lambda x: fake.last_name()) @@ -58,3 +60,11 @@ class UserFactory(factory.django.DjangoModelFactory): def post(self, create, extracted, **kwargs): self.set_password('test') self.save() + + +class TeacherFactory(UserFactory): + @factory.post_generation + def post(self, create, extracted, **kwargs): + Role.objects.create_default_roles() + teacher_role = Role.objects.get_default_teacher_role() + UserRole.objects.create(user=self, role=teacher_role) diff --git a/server/core/management/commands/export_schema_graphql.py b/server/core/management/commands/export_schema_graphql.py index 916bc051..25850f58 100644 --- a/server/core/management/commands/export_schema_graphql.py +++ b/server/core/management/commands/export_schema_graphql.py @@ -16,6 +16,6 @@ class Command(BaseCommand): with open(schema_path, 'w') as o: o.write(str(schema)) - with open(public_schema_path, 'w') as o: - o.write(str(schema_public)) + # with open(public_schema_path, 'w') as o: + # o.write(str(schema_public)) diff --git a/server/users/factories.py b/server/users/factories.py index 56e990d6..4f6f6362 100644 --- a/server/users/factories.py +++ b/server/users/factories.py @@ -1,7 +1,8 @@ import random import factory -from users.models import SchoolClass, SchoolClassMember, License + +from users.models import SchoolClass, SchoolClassMember, License, Team class_types = ['DA', 'KV', 'INF', 'EE'] class_suffix = ['A', 'B', 'C', 'D', 'E'] @@ -31,6 +32,14 @@ class SchoolClassFactory(factory.django.DjangoModelFactory): SchoolClassMember.objects.create(user=user, school_class=self, active=True) +class TeamFactory(factory.django.DjangoModelFactory): + class Meta: + model = Team + + name = factory.Faker('name') + is_deleted = False + + class LicenseFactory(factory.django.DjangoModelFactory): class Meta: model = License diff --git a/server/users/migrations/0026_auto_20210324_2126.py b/server/users/migrations/0026_auto_20210324_2126.py new file mode 100644 index 00000000..ba827ca1 --- /dev/null +++ b/server/users/migrations/0026_auto_20210324_2126.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.19 on 2021-03-24 21:26 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0025_auto_20210126_1343'), + ] + + operations = [ + migrations.CreateModel( + name='Team', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('is_deleted', models.BooleanField(default=False)), + ('code', models.CharField(blank=True, default=None, max_length=10, null=True, unique=True, verbose_name='Code zum Beitreten')), + ('creator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='user', + name='team', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='users.Team'), + ), + ] diff --git a/server/users/models.py b/server/users/models.py index 1e405e7d..47ce9bd9 100644 --- a/server/users/models.py +++ b/server/users/models.py @@ -1,7 +1,7 @@ -import re -from datetime import datetime -import string import random +import re +import string +from datetime import datetime from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractUser, Permission @@ -25,6 +25,7 @@ class User(AbstractUser): hep_group_id = models.PositiveIntegerField(null=True, blank=False) license_expiry_date = models.DateField(blank=False, null=True, default=None) onboarding_visited = models.BooleanField(default=False) + team = models.ForeignKey('users.Team', on_delete=models.SET_NULL, blank=True, null=True, related_name='members') # for wagtail autocomplete autocomplete_search_field = 'username' @@ -110,14 +111,43 @@ class User(AbstractUser): return self.get_full_name() class Meta: - ordering = ['pk',] + ordering = ['pk', ] + + +class GroupWithCode(models.Model): + class Meta: + abstract = True -class SchoolClass(models.Model): name = models.CharField(max_length=100, blank=False, null=False, unique=True) is_deleted = models.BooleanField(blank=False, null=False, default=False) + code = models.CharField('Code zum Beitreten', blank=True, null=True, max_length=10, unique=True, default=None) + + def generate_code(self): + letters = string.ascii_lowercase + digits = string.digits + code = ''.join(random.choice(letters) for i in range(4)) + ''.join(random.choice(digits) for i in range(2)) + try: + self.__class__.objects.get(code=code) + self.generate_code() + except self.__class__.DoesNotExist: + self.code = code.upper() + self.save() + + +class Team(GroupWithCode): + creator = models.ForeignKey(get_user_model(), null=True, on_delete=models.SET_NULL, blank=True, related_name='+') + + class Meta: + verbose_name = 'Team' + verbose_name_plural = 'Teams' + + def __str__(self): + return self.name + + +class SchoolClass(GroupWithCode): users = models.ManyToManyField(get_user_model(), related_name='school_classes', blank=True, through='users.SchoolClassMember') - code = models.CharField('Code zum Beitreten', blank=True, null=True, max_length=10, unique=True, default=None) class Meta: verbose_name = 'Schulklasse' @@ -162,17 +192,6 @@ class SchoolClass(models.Model): def get_teacher(self): return self.users.filter(user_roles__role__key='teacher').first() - def generate_code(self): - letters = string.ascii_lowercase - digits = string.digits - code = ''.join(random.choice(letters) for i in range(4)) + ''.join(random.choice(digits) for i in range(2)) - try: - SchoolClass.objects.get(code=code) - self.generate_code() - except SchoolClass.DoesNotExist: - self.code = code.upper() - self.save() - def save(self, *args, **kwargs): if self.code == '': # '' can't be unique, so we null it self.code = None diff --git a/server/users/mutations.py b/server/users/mutations.py index eacb6b71..cda3f4f2 100644 --- a/server/users/mutations.py +++ b/server/users/mutations.py @@ -7,8 +7,8 @@ from graphql_relay import from_global_id from api.utils import get_object from users.inputs import PasswordUpdateInput -from users.models import SchoolClass, UserSetting, User, SchoolClassMember -from users.schema import SchoolClassNode +from users.models import SchoolClass, SchoolClassMember, Team +from users.schema import SchoolClassNode, TeamNode from users.serializers import PasswordSerialzer, AvatarUrlSerializer @@ -209,6 +209,28 @@ class CreateSchoolClass(relay.ClientIDMutation): return cls(success=True, school_class=school_class) +class CreateTeam(relay.ClientIDMutation): + class Input: + name = graphene.String() + + success = graphene.Boolean() + team = graphene.Field(TeamNode) + + @classmethod + def mutate_and_get_payload(cls, root, info, **kwargs): + name = kwargs.get('name') + user = info.context.user + + if 'users.can_manage_school_class_content' not in user.get_role_permissions(): + raise PermissionError() + + team = Team.objects.create(name=name, creator=user) + team.generate_code() + user.team = team + user.save() + return cls(success=True, team=team) + + class UpdateOnboardingProgress(graphene.Mutation): success = graphene.Boolean() @@ -231,3 +253,4 @@ class ProfileMutations: update_school_class = UpdateSchoolClass.Field() create_school_class = CreateSchoolClass.Field() update_onboarding_progress = UpdateOnboardingProgress.Field() + create_team = CreateTeam.Field() diff --git a/server/users/schema.py b/server/users/schema.py index 45fe3a12..5149d97d 100644 --- a/server/users/schema.py +++ b/server/users/schema.py @@ -2,18 +2,18 @@ from datetime import datetime import graphene from django.db.models import Q +from django.utils.dateformat import format from django_filters import FilterSet, OrderingFilter from graphene import relay, ObjectType from graphene_django import DjangoObjectType from graphene_django.filter import DjangoFilterConnectionField -from django.utils.dateformat import format from graphql_relay import to_global_id from basicknowledge.models import BasicKnowledge from basicknowledge.queries import InstrumentNode -from books.models import Module, RecentModule +from books.models import Module from books.schema.queries import ModuleNode -from users.models import User, SchoolClass, SchoolClassMember +from users.models import User, SchoolClass, SchoolClassMember, Team class SchoolClassNode(DjangoObjectType): @@ -40,6 +40,19 @@ class SchoolClassNode(DjangoObjectType): return self.code +class TeamNode(DjangoObjectType): + class Meta: + model = Team + filter_fields = ['name'] + interfaces = (relay.Node,) + + pk = graphene.Int() + members = graphene.List('users.schema.UserNode') + + def resolve_pk(self, *args, **kwargs): + return self.id + + class RecentModuleFilter(FilterSet): class Meta: model = Module @@ -60,13 +73,14 @@ class UserNode(DjangoObjectType): is_teacher = graphene.Boolean() old_classes = DjangoFilterConnectionField(SchoolClassNode) recent_modules = DjangoFilterConnectionField(ModuleNode, filterset_class=RecentModuleFilter) + team = graphene.Field(TeamNode) class Meta: model = User filter_fields = ['username', 'email'] only_fields = ['username', 'email', 'first_name', 'last_name', 'school_classes', 'last_module', 'last_topic', 'avatar_url', - 'selected_class', 'expiry_date', 'onboarding_visited'] + 'selected_class', 'expiry_date', 'onboarding_visited', 'team'] interfaces = (relay.Node,) def resolve_pk(self, info, **kwargs): diff --git a/server/users/tests/test_teams.py b/server/users/tests/test_teams.py new file mode 100644 index 00000000..63bde988 --- /dev/null +++ b/server/users/tests/test_teams.py @@ -0,0 +1,68 @@ +from django.test import TestCase +from graphene import Context +from graphene.test import Client + +from api.schema import schema +from core.factories import UserFactory, TeacherFactory +from users.factories import TeamFactory +from users.models import Role +from users.services import create_teacher + +ME_QUERY = """ + query MeQuery { + me { + team { + name + code + } + } + } + """ + +CREATE_MUTATION = """ + mutation CreateTeamMutation($input: CreateTeamInput!) { + createTeam(input: $input) { + success + team { + name + code + } + } + } +""" + + +class TeamTest(TestCase): + def setUp(self): + self.client = Client(schema=schema) + self.team_name = 'Fiterativ' + self.code = 'AAAA' + self.team = TeamFactory(name=self.team_name, code=self.code) + self.user = TeacherFactory(username='ueli', team=self.team) + self.context = Context(user=self.user) + + def test_team_query(self): + result = self.client.execute(ME_QUERY, context=self.context) + self.assertIsNone(result.get('errors')) + team = result.get('data').get('me').get('team') + self.assertEqual(team.get('name'), self.team_name) + self.assertEqual(team.get('code'), self.code) + + def test_join_team_mutation(self): + raise NotImplementedError() + + def test_create_team_mutation(self): + team_name = "Dunder Mifflin" + variables = { + "input": { + "name": team_name + } + } + result = self.client.execute(CREATE_MUTATION, context=self.context, variables=variables) + self.assertIsNone(result.get('errors')) + create_team = result.get('data').get('createTeam') + team = create_team.get('team') + success = create_team.get('success') + self.assertTrue(success) + self.assertEqual(team.get('name'), team_name) + self.assertIsNotNone(team.get('code'))