From 6646b328b7b98b385bb495927f9bb9ed262b951d Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Thu, 3 Jun 2021 17:52:29 +0200 Subject: [PATCH] Add original creator to custom content blocks --- .tool-versions | 2 +- .../migrations/0030_auto_20210603_1305.py | 26 ++++++++++ .../migrations/0031_auto_20210603_1306.py | 20 ++++++++ server/books/models/contentblock.py | 4 +- server/books/models/snapshot.py | 3 +- server/books/schema/nodes/content.py | 1 + server/books/tests/queries.py | 5 ++ server/books/tests/test_snapshots.py | 7 ++- server/rooms/schema.py | 3 +- server/users/schema.py | 49 +++++++++++-------- .../users/tests/test_leave_reenter_class.py | 8 +-- 11 files changed, 98 insertions(+), 30 deletions(-) create mode 100644 server/books/migrations/0030_auto_20210603_1305.py create mode 100644 server/books/migrations/0031_auto_20210603_1306.py diff --git a/.tool-versions b/.tool-versions index 593085d3..8adf7ed6 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ nodejs 12.22.1 -python 3.8.5 +python 3.8.10 diff --git a/server/books/migrations/0030_auto_20210603_1305.py b/server/books/migrations/0030_auto_20210603_1305.py new file mode 100644 index 00000000..e441b8a3 --- /dev/null +++ b/server/books/migrations/0030_auto_20210603_1305.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.23 on 2021-06-03 13:05 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('books', '0029_auto_20210511_1301'), + ] + + operations = [ + migrations.AddField( + model_name='contentblock', + name='original_creator', + field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='snapshot', + name='objective_groups', + field=models.ManyToManyField(related_name='_snapshot_objective_groups_+', through='books.ObjectiveGroupSnapshot', to='objectives.ObjectiveGroup'), + ), + ] diff --git a/server/books/migrations/0031_auto_20210603_1306.py b/server/books/migrations/0031_auto_20210603_1306.py new file mode 100644 index 00000000..96d727ab --- /dev/null +++ b/server/books/migrations/0031_auto_20210603_1306.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.23 on 2021-06-03 13:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('books', '0030_auto_20210603_1305'), + ] + + operations = [ + migrations.AlterField( + model_name='contentblock', + name='original_creator', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/server/books/models/contentblock.py b/server/books/models/contentblock.py index da4b0db8..4fff35c9 100644 --- a/server/books/models/contentblock.py +++ b/server/books/models/contentblock.py @@ -42,6 +42,7 @@ class ContentBlock(StrictHierarchyPage): # blocks with owner are hidden by default, need to be shown for each class visible_for = models.ManyToManyField(SchoolClass, related_name='visible_content_blocks') user_created = models.BooleanField(default=False) + original_creator = models.ForeignKey(User, null=True, blank=True, default=None, on_delete=models.SET_NULL) bookmarks = models.ManyToManyField(User, through=ContentBlockBookmark, related_name='bookmarked_content_blocks') @@ -133,7 +134,8 @@ class ContentBlockSnapshot(ContentBlock): contents=self.contents, type=self.type, title=self.title, - owner=owner + owner=owner, + original_creator=self.original_creator ) self.add_sibling(instance=cb, pos='right') # some wagtail magic diff --git a/server/books/models/snapshot.py b/server/books/models/snapshot.py index e8cd4a52..17ffc545 100644 --- a/server/books/models/snapshot.py +++ b/server/books/models/snapshot.py @@ -59,7 +59,8 @@ class SnapshotManager(models.Manager): snapshot=snapshot, contents=content_block.contents, type=content_block.type, - title=content_block.title + title=content_block.title, + original_creator=content_block.owner ) content_block.add_sibling(instance=new_content_block, pos='right') revision = new_content_block.save_revision() diff --git a/server/books/schema/nodes/content.py b/server/books/schema/nodes/content.py index 33013b32..9433a6f4 100644 --- a/server/books/schema/nodes/content.py +++ b/server/books/schema/nodes/content.py @@ -39,6 +39,7 @@ def is_solution_and_hidden_for_user(type, user, module): class ContentBlockNode(DjangoObjectType, HiddenAndVisibleForMixin): mine = graphene.Boolean() bookmarks = graphene.List(ContentBlockBookmarkNode) + original_creator = graphene.Field('users.schema.PublicUserNode') class Meta: model = ContentBlock diff --git a/server/books/tests/queries.py b/server/books/tests/queries.py index f5bdb338..f5532264 100644 --- a/server/books/tests/queries.py +++ b/server/books/tests/queries.py @@ -20,6 +20,11 @@ query ModulesQuery($slug: String, $id: ID) { contentBlocks { id title + originalCreator { + id + fullName + avatarUrl + } visibleFor { name } diff --git a/server/books/tests/test_snapshots.py b/server/books/tests/test_snapshots.py index 72330ec8..3d25dcad 100644 --- a/server/books/tests/test_snapshots.py +++ b/server/books/tests/test_snapshots.py @@ -121,6 +121,8 @@ class CreateSnapshotTestCase(SkillboxTestCase): school_class_name in [school_class['name'] for school_class in custom_objective.get('visibleFor')]) + return module + def _compare_content_blocks(self, content_blocks): self.assertEqual(len(content_blocks), 4) first, second, third, fourth = content_blocks @@ -203,7 +205,10 @@ class CreateSnapshotTestCase(SkillboxTestCase): } }) self.assertIsNone(result.get('errors')) - self._test_module_visibility(client, school_class_name) + module = self._test_module_visibility(client, school_class_name) + original_creator = module['chapters'][0]['contentBlocks'][2].get('originalCreator') + self.assertIsNotNone(original_creator) + self.assertEqual(original_creator.get('id'), to_global_id('PublicUserNode', self.teacher.pk)) def test_display_snapshot_module(self): self.snapshot = Snapshot.objects.create_snapshot(module=self.module, school_class=self.skillbox_class, diff --git a/server/rooms/schema.py b/server/rooms/schema.py index 4ea816ad..561cc4d6 100644 --- a/server/rooms/schema.py +++ b/server/rooms/schema.py @@ -8,14 +8,13 @@ from graphene_django.filter import DjangoFilterConnectionField from api.utils import get_object, get_by_id_or_slug from rooms.models import Room, RoomEntry, ModuleRoomSlug from users.models import SchoolClass -from users.schema import UserNode logger = logging.getLogger(__name__) class RoomEntryNode(DjangoObjectType): pk = graphene.Int() - author = UserNode() + author = graphene.Field('users.schema.PublicUserNode') class Meta: model = RoomEntry diff --git a/server/users/schema.py b/server/users/schema.py index e820e3a3..b415b553 100644 --- a/server/users/schema.py +++ b/server/users/schema.py @@ -47,7 +47,7 @@ class TeamNode(DjangoObjectType): interfaces = (relay.Node,) pk = graphene.Int() - members = graphene.List('users.schema.UserNode') + members = graphene.List('users.schema.PublicUserNode') def resolve_pk(self, *args, **kwargs): return self.id @@ -68,7 +68,28 @@ class RecentModuleFilter(FilterSet): ) -class UserNode(DjangoObjectType): +class PublicUserNode(DjangoObjectType): + is_me = graphene.Boolean() + full_name = graphene.String(required=True) + + class Meta: + model = User + only_fields = ['full_name', 'first_name', 'last_name', 'avatar_url'] + interfaces = (relay.Node,) + + @staticmethod + def resolve_is_me(parent: User, info, **kwargs): + return info.context.user.pk == parent.pk + +class PrivateUserNode(DjangoObjectType): + 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', 'team'] + interfaces = (relay.Node,) + pk = graphene.Int() permissions = graphene.List(graphene.String) selected_class = graphene.Field(SchoolClassNode) @@ -77,15 +98,6 @@ class UserNode(DjangoObjectType): old_classes = DjangoFilterConnectionField(SchoolClassNode) recent_modules = DjangoFilterConnectionField(ModuleNode, filterset_class=RecentModuleFilter) team = graphene.Field(TeamNode) - is_me = graphene.Boolean() - - 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', 'team'] - interfaces = (relay.Node,) def resolve_pk(self, info, **kwargs): return self.id @@ -121,16 +133,13 @@ class UserNode(DjangoObjectType): def resolve_team(self, info, **kwargs): return self.team - def resolve_is_me(self, info, **kwargs): - return info.context.user.pk == self.pk - class ClassMemberNode(ObjectType): """ We need to build this ourselves, because we want the active property on the node, because providing it on the Connection or Edge for a UserNodeConnection is difficult. """ - user = graphene.Field(UserNode) + user = graphene.Field('users.schema.PublicUserNode') active = graphene.Boolean() first_name = graphene.String() last_name = graphene.String() @@ -138,7 +147,7 @@ class ClassMemberNode(ObjectType): id = graphene.ID() def resolve_id(self, *args): - return to_global_id('UserNode', self.user.pk) + return to_global_id('PublicUserNode', self.user.pk) def resolve_active(self, *args): return self.active @@ -157,8 +166,8 @@ class ClassMemberNode(ObjectType): class UsersQuery(object): - me = graphene.Field(UserNode) - all_users = DjangoFilterConnectionField(UserNode) + me = graphene.Field(PrivateUserNode) + all_users = DjangoFilterConnectionField(PrivateUserNode) my_activity = DjangoFilterConnectionField(ModuleNode) my_instrument_activity = DjangoFilterConnectionField(InstrumentNode) @@ -179,8 +188,8 @@ class UsersQuery(object): class AllUsersQuery(object): - me = graphene.Field(UserNode) - all_users = DjangoFilterConnectionField(UserNode) + me = graphene.Field(PrivateUserNode) + all_users = DjangoFilterConnectionField(PrivateUserNode) def resolve_all_users(self, info, **kwargs): if not info.context.user.is_superuser: diff --git a/server/users/tests/test_leave_reenter_class.py b/server/users/tests/test_leave_reenter_class.py index 1f63cc05..3867920e 100644 --- a/server/users/tests/test_leave_reenter_class.py +++ b/server/users/tests/test_leave_reenter_class.py @@ -39,11 +39,11 @@ class JoinSchoolClassTest(TestCase): ] create_users(user_data) teacher = User.objects.get(username='emily.sands') - self.teacher_id = to_global_id('UserNode', teacher.pk) + self.teacher_id = to_global_id('PublicUserNode', teacher.pk) student = User.objects.get(username='adam.groff') - self.student_id = to_global_id('UserNode', student.pk) + self.student_id = to_global_id('PublicUserNode', student.pk) other_student = User.objects.get(username='eric.effiong') - self.other_student_id = to_global_id('UserNode', other_student.pk) + self.other_student_id = to_global_id('PublicUserNode', other_student.pk) school_class = SchoolClass.objects.get(name=self.school_class_name) self.school_class_id = to_global_id('SchoolClassNode', school_class.pk) @@ -115,7 +115,7 @@ class JoinSchoolClassTest(TestCase): result = self.client.execute(self.mutation, variables={ 'input': { 'schoolClass': to_global_id('SchoolClassNode', school_class.id), - 'member': to_global_id('UserNode', student.id), + 'member': to_global_id('PublicUserNode', student.id), 'active': False } }, context=self.teacher_context)