From 083a8b03a88fe4702d09d0d0a8705b4ecd26dbe9 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Wed, 7 Aug 2019 14:42:20 +0200 Subject: [PATCH] Add admin slug model --- server/api/schema.py | 4 +- server/books/blocks.py | 8 ++ .../migrations/0013_auto_20190807_1220.py | 24 +++++ server/books/models/contentblock.py | 5 +- server/rooms/admin.py | 8 +- server/rooms/factories.py | 11 ++- .../migrations/0007_auto_20190807_1218.py | 26 +++++ server/rooms/models.py | 7 ++ server/rooms/schema.py | 36 ++++++- server/rooms/tests/test_admin_room_query.py | 96 +++++++++++++++++++ .../rooms/tests/test_room_query_permission.py | 21 ++++ server/rooms/wagtail_hooks.py | 39 ++++++++ 12 files changed, 277 insertions(+), 8 deletions(-) create mode 100644 server/books/migrations/0013_auto_20190807_1220.py create mode 100644 server/rooms/migrations/0007_auto_20190807_1218.py create mode 100644 server/rooms/tests/test_admin_room_query.py create mode 100644 server/rooms/wagtail_hooks.py diff --git a/server/api/schema.py b/server/api/schema.py index 9c39b4db..b4800f09 100644 --- a/server/api/schema.py +++ b/server/api/schema.py @@ -18,12 +18,12 @@ from portfolio.schema import PortfolioQuery from surveys.schema import SurveysQuery from surveys.mutations import SurveysMutations from rooms.mutations import RoomMutations -from rooms.schema import RoomsQuery +from rooms.schema import RoomsQuery, AdminRoomsQuery from users.schema import UsersQuery from users.mutations import ProfileMutations -class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, StudentSubmissionQuery, +class Query(UsersQuery, AdminRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, StudentSubmissionQuery, BasicKnowledgeQuery, PortfolioQuery, MyActivityQuery, SurveysQuery, graphene.ObjectType): node = relay.Node.Field() diff --git a/server/books/blocks.py b/server/books/blocks.py index 585157fc..38c0a094 100644 --- a/server/books/blocks.py +++ b/server/books/blocks.py @@ -98,6 +98,14 @@ class InstrumentTextBlock(blocks.StructBlock): text = blocks.RichTextBlock(features=INSTRUMENTS_RICH_TEXT_FEATURES) + +class AdminRoomSlugBlock(blocks.StructBlock): + class Meta: + icon = 'link' + + slug = blocks.TextBlock() + title = blocks.TextBlock() + # 'text_block' 'task' 'basic_knowledge' 'student_entry' 'image_block' # # url = blocks.URLBlock() diff --git a/server/books/migrations/0013_auto_20190807_1220.py b/server/books/migrations/0013_auto_20190807_1220.py new file mode 100644 index 00000000..e3bf9b9f --- /dev/null +++ b/server/books/migrations/0013_auto_20190807_1220.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.6 on 2019-08-07 12:20 + +import assignments.models +from django.db import migrations +import surveys.models +import wagtail.core.blocks +import wagtail.core.fields +import wagtail.images.blocks +import wagtail.snippets.blocks + + +class Migration(migrations.Migration): + + dependencies = [ + ('books', '0012_auto_20190722_0932'), + ] + + operations = [ + migrations.AlterField( + model_name='contentblock', + name='contents', + field=wagtail.core.fields.StreamField([('text_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))])), ('basic_knowledge', wagtail.core.blocks.StructBlock([('description', wagtail.core.blocks.RichTextBlock(required=False)), ('basic_knowledge', wagtail.core.blocks.PageChooserBlock(required=True, target_model=['basicknowledge.BasicKnowledge']))])), ('assignment', wagtail.core.blocks.StructBlock([('assignment_id', wagtail.snippets.blocks.SnippetChooserBlock(assignments.models.Assignment))])), ('survey', wagtail.core.blocks.StructBlock([('survey_id', wagtail.snippets.blocks.SnippetChooserBlock(surveys.models.Survey))])), ('image_block', wagtail.images.blocks.ImageChooserBlock()), ('image_url_block', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('link_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('solution', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))], icon='tick')), ('video_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('document_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('infogram_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock()), ('title', wagtail.core.blocks.TextBlock())])), ('genially_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('subtitle', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock())])), ('admin_room_slug', wagtail.core.blocks.StructBlock([('slug', wagtail.core.blocks.TextBlock()), ('title', wagtail.core.blocks.TextBlock())])), ('content_list_item', wagtail.core.blocks.StreamBlock([('text_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))])), ('basic_knowledge', wagtail.core.blocks.StructBlock([('description', wagtail.core.blocks.RichTextBlock(required=False)), ('basic_knowledge', wagtail.core.blocks.PageChooserBlock(required=True, target_model=['basicknowledge.BasicKnowledge']))])), ('assignment', wagtail.core.blocks.StructBlock([('assignment_id', wagtail.snippets.blocks.SnippetChooserBlock(assignments.models.Assignment))])), ('survey', wagtail.core.blocks.StructBlock([('survey_id', wagtail.snippets.blocks.SnippetChooserBlock(surveys.models.Survey))])), ('image_block', wagtail.images.blocks.ImageChooserBlock()), ('image_url_block', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('link_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('solution', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))], icon='tick')), ('video_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('document_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('infogram_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock()), ('title', wagtail.core.blocks.TextBlock())])), ('genially_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('subtitle', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock())])), ('admin_room_slug', wagtail.core.blocks.StructBlock([('slug', wagtail.core.blocks.TextBlock()), ('title', wagtail.core.blocks.TextBlock())]))]))], blank=True, null=True), + ), + ] diff --git a/server/books/models/contentblock.py b/server/books/models/contentblock.py index 95a3ef80..a4bab50f 100644 --- a/server/books/models/contentblock.py +++ b/server/books/models/contentblock.py @@ -7,7 +7,7 @@ from wagtail.core.fields import StreamField from wagtail.images.blocks import ImageChooserBlock from books.blocks import TextBlock, BasicKnowledgeBlock, LinkBlock, VideoBlock, DocumentBlock, \ - ImageUrlBlock, AssignmentBlock, InfogramBlock, GeniallyBlock, SubtitleBlock, SurveyBlock + ImageUrlBlock, AssignmentBlock, InfogramBlock, GeniallyBlock, SubtitleBlock, SurveyBlock, AdminRoomSlugBlock from core.wagtail_utils import StrictHierarchyPage from users.models import SchoolClass @@ -48,7 +48,8 @@ class ContentBlock(StrictHierarchyPage): ('document_block', DocumentBlock()), ('infogram_block', InfogramBlock()), ('genially_block', GeniallyBlock()), - ('subtitle', SubtitleBlock()) + ('subtitle', SubtitleBlock()), + ('admin_room_slug', AdminRoomSlugBlock()) ] content_list_item = StreamBlock(content_blocks) diff --git a/server/rooms/admin.py b/server/rooms/admin.py index 094630e9..c1c4a702 100644 --- a/server/rooms/admin.py +++ b/server/rooms/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from rooms.models import Room, RoomEntry +from rooms.models import Room, RoomEntry, AdminGeneratedRoomSlug @admin.register(Room) @@ -13,3 +13,9 @@ class RoomAdmin(admin.ModelAdmin): class RoomEntryAdmin(admin.ModelAdmin): list_display = ('id', 'slug', 'title', 'room', 'author') list_filter = ('room', 'author') + + +@admin.register(AdminGeneratedRoomSlug) +class AdminGeneratedRoomSlugAdmin(admin.ModelAdmin): + list_display = ('id', 'slug', 'title') + list_filter = ('slug', 'title') diff --git a/server/rooms/factories.py b/server/rooms/factories.py index e7c2acea..d34e1532 100644 --- a/server/rooms/factories.py +++ b/server/rooms/factories.py @@ -8,7 +8,7 @@ from wagtail.core.rich_text import RichText from books.factories import TextBlockFactory, ImageUrlBlockFactory, LinkBlockFactory from core.factories import fake, fake_paragraph -from rooms.models import Room, RoomEntry +from rooms.models import Room, RoomEntry, AdminGeneratedRoomSlug from users.models import SchoolClass @@ -77,3 +77,12 @@ class RoomEntryFactory(factory.django.DjangoModelFactory): def create(cls, **kwargs): cls.stream_field_magic(kwargs, 'contents') return cls._generate(CREATE_STRATEGY, kwargs) + + +class AdminGeneratedRoomSlugFactory(factory.django.DjangoModelFactory): + class Meta: + model = AdminGeneratedRoomSlug + + slug = factory.Sequence(lambda n: u'slug-{:d}'.format(n)) + title = factory.Sequence(lambda n: u'Title {:d}'.format(n)) + diff --git a/server/rooms/migrations/0007_auto_20190807_1218.py b/server/rooms/migrations/0007_auto_20190807_1218.py new file mode 100644 index 00000000..353dcef5 --- /dev/null +++ b/server/rooms/migrations/0007_auto_20190807_1218.py @@ -0,0 +1,26 @@ +# Generated by Django 2.0.6 on 2019-08-07 12:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rooms', '0006_auto_20190722_0932'), + ] + + operations = [ + migrations.CreateModel( + name='AdminGeneratedRoomSlug', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.CharField(max_length=255)), + ('title', models.CharField(max_length=255)), + ], + ), + migrations.AddField( + model_name='room', + name='user_created', + field=models.BooleanField(default=True), + ), + ] diff --git a/server/rooms/models.py b/server/rooms/models.py index a22f9dd5..47120c14 100644 --- a/server/rooms/models.py +++ b/server/rooms/models.py @@ -15,6 +15,7 @@ class Room(TitleSlugDescriptionModel): school_class = models.ForeignKey(SchoolClass, blank=False, null=False, on_delete=models.CASCADE, related_name='rooms') appearance = models.CharField(blank=True, null=False, max_length=255) + user_created = models.BooleanField(blank=False, null=False, default=True) def __str__(self): return 'Room {}-{}-{}'.format(self.id, self.title, self.school_class) @@ -41,3 +42,9 @@ class RoomEntry(TitleSlugDescriptionModel): def can_user_see_entry(self, user): return user.is_superuser or self.room.school_class.is_user_in_schoolclass(user) + + +class AdminGeneratedRoomSlug(models.Model): + slug = models.CharField(blank=False, null=False, max_length=255) + title = models.CharField(blank=False, null=False, max_length=255) + diff --git a/server/rooms/schema.py b/server/rooms/schema.py index 556c98b4..3db48198 100644 --- a/server/rooms/schema.py +++ b/server/rooms/schema.py @@ -6,7 +6,8 @@ from graphene_django import DjangoObjectType from graphene_django.filter import DjangoFilterConnectionField from api.utils import get_object, get_by_id_or_slug -from rooms.models import Room, RoomEntry +from rooms.models import Room, RoomEntry, AdminGeneratedRoomSlug +from users.models import SchoolClass from users.schema import UserNode logger = logging.getLogger(__name__) @@ -53,7 +54,7 @@ class RoomsQuery(object): user = info.context.user if user.is_superuser: return Room.objects.all() - return Room.objects.filter(school_class__in=user.school_classes.all()) + return Room.objects.filter(school_class__in=user.school_classes.all()).exclude(user_created=False) def resolve_room(self, info, **kwargs): room = get_by_id_or_slug(Room, **kwargs) @@ -82,3 +83,34 @@ class RoomsQuery(object): return RoomEntry.objects.none() else: return RoomEntry.objects.all() + + +class AdminRoomsQuery(object): + + admin_room = graphene.Field(RoomNode, slug=graphene.String(), class_id=graphene.ID()) + + def resolve_admin_room(self, info, **kwargs): + + slug = kwargs.get('slug') + schoolclass = get_object(SchoolClass, kwargs.get('class_id')) + + try: + slug = AdminGeneratedRoomSlug.objects.get(slug=slug) + except AdminGeneratedRoomSlug.DoesNotExist: + return None + + if schoolclass is None or not schoolclass.is_user_in_schoolclass(info.context.user): + return None + + room, created = Room.objects.get_or_create(school_class=schoolclass, user_created=False, slug=slug, + title=slug.title) + if not room.user_created and room.school_class.is_user_in_schoolclass(info.context.user): + return room + else: + return None + + # def resolve_all_room_entries(self, info, **kwargs): + # if not info.context.user.is_superuser: + # return RoomEntry.objects.none() + # else: + # return RoomEntry.objects.all() diff --git a/server/rooms/tests/test_admin_room_query.py b/server/rooms/tests/test_admin_room_query.py new file mode 100644 index 00000000..9c4ce36c --- /dev/null +++ b/server/rooms/tests/test_admin_room_query.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-08-07 +# @author: chrigu +from django.test import TestCase, RequestFactory +from graphene.test import Client +from graphql_relay import to_global_id + +from api.schema import schema +from core.factories import UserFactory +from rooms.factories import RoomFactory, AdminGeneratedRoomSlugFactory +from users.factories import SchoolClassFactory + + +class AdminRoomQueryPermission(TestCase): + + def setUp(self): + self.user = UserFactory(username='aschi') + self.another_user = UserFactory(username='pesche') + self.sc1 = SchoolClassFactory(users=[self.user]) + sc2 = SchoolClassFactory(users=[self.another_user]) + self.room1 = RoomFactory(school_class=self.sc1) + self.room2 = RoomFactory(school_class=sc2) + + self.admin_slug = AdminGeneratedRoomSlugFactory(slug='test-slug', title='title') + + self.sc1_id = to_global_id('SchoolClass', self.sc1.pk) + self.sc2_id = to_global_id('SchoolClass', sc2.pk) + + request = RequestFactory().get('/') + request.user = self.user + self.client = Client(schema=schema, context_value=request) + + self.query = ''' + query AdminRoomQuery($slug: String, $classId: ID!) { + adminRoom(slug: $slug, classId: $classId) { + title + } + } + ''' + + def test_should_return_none_if_slug_does_not_exist(self): + + result = self.client.execute(self.query, variables={ + 'slug': 'no-slug', + 'classId': 'norealId' + }) + self.assertIsNone(result.get('errors')) + self.assertIsNone(result.get('data').get('adminRoom')) + + def test_should_return_none_if_class_id_does_not_exist(self): + + result = self.client.execute(self.query, variables={ + 'slug': 'no-slug', + 'classId': 'norealId' + }) + self.assertIsNone(result.get('errors')) + self.assertIsNone(result.get('data').get('adminRoom')) + + def test_user_should_not_be_able_to_create_room_for_other_class(self): + + result = self.client.execute(self.query, variables={ + 'slug': self.admin_slug.slug, + 'classId': self.sc2_id + }) + + self.assertIsNone(result.get('errors')) + self.assertIsNone(result.get('data').get('adminRoom')) + + def test_should_create_room_if_none_exists(self): + + result = self.client.execute(self.query, variables={ + 'slug': self.admin_slug.slug, + 'classId': self.sc1_id + }) + + self.assertIsNone(result.get('errors')) + self.assertEqual(result.get('data').get('adminRoom').get('title'), self.admin_slug.title) + + def test_should_return_room_if_one_exists(self): + + existing_room = RoomFactory(school_class=self.sc1, user_created=False) + admin_slug = AdminGeneratedRoomSlugFactory(slug=existing_room.slug, title=existing_room.title) + + result = self.client.execute(self.query, variables={ + 'slug': admin_slug.slug, + 'classId': self.sc1_id + }) + + self.assertIsNone(result.get('errors')) + self.assertEqual(result.get('data').get('adminRoom').get('title'), existing_room.title) diff --git a/server/rooms/tests/test_room_query_permission.py b/server/rooms/tests/test_room_query_permission.py index 6a2cc583..a4b35b6e 100644 --- a/server/rooms/tests/test_room_query_permission.py +++ b/server/rooms/tests/test_room_query_permission.py @@ -61,6 +61,27 @@ class RoomQueryPermission(TestCase): self.assertIsNone(result.get('errors')) self.assertEqual(result.get('data').get('room'), None) + def test_student_should_only_user_created_rooms(self): + + admin_room = RoomFactory(school_class=self.room1.school_class, user_created=False) + + query = ''' + query { + rooms { + edges { + node { + title + } + } + } + } + ''' + + result = self.client.execute(query) + self.assertIsNone(result.get('errors')) + self.assertEqual(len(result.get('data').get('rooms').get('edges')), 1) + self.assertNotEqual(result.get('data').get('rooms').get('edges')[0].get('node').get('title'), admin_room.title) + class RoomEntryQueryPermissions(TestCase): diff --git a/server/rooms/wagtail_hooks.py b/server/rooms/wagtail_hooks.py new file mode 100644 index 00000000..26bbfee5 --- /dev/null +++ b/server/rooms/wagtail_hooks.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-08-05 +# @author: chrigu +from wagtail.core import hooks + +from rooms.models import AdminGeneratedRoomSlug + + +@hooks.register('after_edit_page') +@hooks.register('after_create_page') +def do_after_page_edit(request, page): + blocks = get_room_blocks(page) + for block in blocks: + AdminGeneratedRoomSlug.objects.get_or_create(slug=block[1]['slug']) + + +def get_room_blocks(page): + top_level_admin_slug_blocks = get_block_from_stream_data(page.contents.stream_data, 'admin_room_slug') + content_list_admin_slug_blocks = get_admin_slugs_from_content_list(page.contents.stream_data) + return top_level_admin_slug_blocks + content_list_admin_slug_blocks + + +def get_block_from_stream_data(stream_data, block_name): + return [block for block in stream_data if block[0] in [block_name]] + + +def get_admin_slugs_from_content_list(stream_data): + admin_slug_blocks = [] + content_list_items = get_block_from_stream_data(stream_data, 'content_list_item') + for content_list_item in content_list_items: + admin_slug_blocks = admin_slug_blocks + get_block_from_stream_data(content_list_item[1].stream_data, + 'admin_room_slug') + return admin_slug_blocks