diff --git a/server/books/schema/nodes/content.py b/server/books/schema/nodes/content.py index 01cfec2a..fe1c1217 100644 --- a/server/books/schema/nodes/content.py +++ b/server/books/schema/nodes/content.py @@ -10,7 +10,7 @@ from books.utils import are_solutions_enabled_for from core.logger import get_logger from core.mixins import HiddenAndVisibleForMixin from notes.models import ContentBlockBookmark -from notes.schema import ContentBlockBookmarkNode +from notes.schema import ContentBlockBookmarkNode, HighlightNode from rooms.models import ModuleRoomSlug logger = get_logger(__name__) @@ -20,7 +20,7 @@ class TextBlockNode(graphene.ObjectType): text = graphene.String() def resolve_text(root, info, **kwargs): - return root['value']['text'] + return root["value"]["text"] class ContentNode(graphene.Union): @@ -30,30 +30,43 @@ class ContentNode(graphene.Union): @classmethod def resolve_type(cls, instance, info): logger.info(instance) - if instance['type'] == 'text_block': + if instance["type"] == "text_block": return TextBlockNode def is_solution_and_hidden_for_user(type, user, module): - return type == 'solution' and not (are_solutions_enabled_for(user, module) or user.is_teacher()) + return type == "solution" and not ( + are_solutions_enabled_for(user, module) or user.is_teacher() + ) class ContentBlockNode(DjangoObjectType, HiddenAndVisibleForMixin): mine = graphene.Boolean() bookmarks = graphene.List(ContentBlockBookmarkNode) - original_creator = graphene.Field('users.schema.PublicUserNode') + original_creator = graphene.Field("users.schema.PublicUserNode") instrument_category = graphene.Field(InstrumentCategoryNode) path = graphene.String() + highlights = graphene.List(HighlightNode) class Meta: model = ContentBlock only_fields = [ - 'slug', 'title', 'type', 'contents', 'hidden_for', 'visible_for', 'user_created' + "slug", + "title", + "type", + "contents", + "hidden_for", + "visible_for", + "user_created", ] filter_fields = [ - 'slug', 'title', + "slug", + "title", ] - interfaces = (relay.Node, ContentBlockInterface,) + interfaces = ( + relay.Node, + ContentBlockInterface, + ) convert_choices_to_enum = False def resolve_mine(parent, info, **kwargs): @@ -64,18 +77,22 @@ class ContentBlockNode(DjangoObjectType, HiddenAndVisibleForMixin): updated_raw_data = [] for content in self.contents.raw_data: # only show solutions to teachers and students for whom their teachers have them enabled - if is_solution_and_hidden_for_user(content['type'], info.context.user, self.module): - logger.debug('Solution is hidden for this user') + if is_solution_and_hidden_for_user( + content["type"], info.context.user, self.module + ): + logger.debug("Solution is hidden for this user") continue - if content['type'] == 'content_list_item': + if content["type"] == "content_list_item": _values = [] - for index, list_block in enumerate(content['value']): - if is_solution_and_hidden_for_user(list_block['type'], info.context.user, self.module): - logger.debug('Solution is hidden for this user') + for index, list_block in enumerate(content["value"]): + if is_solution_and_hidden_for_user( + list_block["type"], info.context.user, self.module + ): + logger.debug("Solution is hidden for this user") continue _values.append(process_module_room_slug_block(list_block)) - content['value'] = _values + content["value"] = _values content = process_module_room_slug_block(content) updated_raw_data.append(content) @@ -85,16 +102,18 @@ class ContentBlockNode(DjangoObjectType, HiddenAndVisibleForMixin): def resolve_bookmarks(self, info, **kwargs): return ContentBlockBookmark.objects.filter( - user=info.context.user, - content_block=self + user=info.context.user, content_block=self ) @staticmethod def resolve_instrument_category(root: ContentBlock, info, **kwargs): if root.type == ContentBlock.INSTRUMENT: for content in root.contents.raw_data: - if content['type'] == 'instrument' or content['type'] == 'basic_knowledge': - _id = content['value']['basic_knowledge'] + if ( + content["type"] == "instrument" + or content["type"] == "basic_knowledge" + ): + _id = content["value"]["basic_knowledge"] instrument = BasicKnowledge.objects.get(id=_id) category = instrument.new_type.category return category @@ -103,17 +122,22 @@ class ContentBlockNode(DjangoObjectType, HiddenAndVisibleForMixin): @staticmethod def resolve_path(root: ContentBlock, info, **kwargs): module = root.get_parent().get_parent() - return f'module/{module.slug}#{root.graphql_id}' + return f"module/{module.slug}#{root.graphql_id}" + + @staticmethod + def resolve_highlights(root: ContentBlock, info, **kwargs): + return root.highlights.filter(user=info.context.user) def process_module_room_slug_block(content): - if content['type'] == 'module_room_slug': + if content["type"] == "module_room_slug": try: module_room_slug = ModuleRoomSlug.objects.get( - title=content['value']['title']) - content['value'] = { - 'title': content['value']['title'], - 'slug': module_room_slug.slug + title=content["value"]["title"] + ) + content["value"] = { + "title": content["value"]["title"], + "slug": module_room_slug.slug, } except ModuleRoomSlug.DoesNotExist: pass diff --git a/server/notes/migrations/0005_highlight.py b/server/notes/migrations/0005_highlight.py new file mode 100644 index 00000000..f91e18d8 --- /dev/null +++ b/server/notes/migrations/0005_highlight.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.8 on 2024-01-09 15:07 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("books", "0045_alter_snapshot_objective_groups"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("notes", "0004_auto_20210322_1514"), + ] + + operations = [ + migrations.CreateModel( + name="Highlight", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content_index", models.IntegerField()), + ("content_uuid", models.UUIDField()), + ("paragraph_index", models.IntegerField()), + ("start_position", models.IntegerField()), + ("selection_length", models.IntegerField()), + ("text", models.TextField()), + ("color", models.CharField(max_length=50)), + ( + "content_block", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="highlights", + to="books.contentblock", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/server/notes/models.py b/server/notes/models.py index 9fc60ce6..cd6dc613 100644 --- a/server/notes/models.py +++ b/server/notes/models.py @@ -19,26 +19,50 @@ class Bookmark(models.Model): class ContentBlockBookmark(Bookmark): uuid = models.UUIDField(unique=False) - content_block = models.ForeignKey('books.ContentBlock', on_delete=models.CASCADE) + content_block = models.ForeignKey("books.ContentBlock", on_delete=models.CASCADE) class Meta: constraints = [ - models.UniqueConstraint(fields=['uuid', 'content_block', 'user'], name='unique_content_bookmark_per_user') + models.UniqueConstraint( + fields=["uuid", "content_block", "user"], + name="unique_content_bookmark_per_user", + ) ] class ModuleBookmark(Bookmark): - module = models.ForeignKey('books.Module', on_delete=models.CASCADE) + module = models.ForeignKey("books.Module", on_delete=models.CASCADE) class ChapterBookmark(Bookmark): - chapter = models.ForeignKey('books.Chapter', on_delete=models.CASCADE) + chapter = models.ForeignKey("books.Chapter", on_delete=models.CASCADE) + class InstrumentBookmark(Bookmark): uuid = models.UUIDField(unique=False) - instrument = models.ForeignKey('basicknowledge.BasicKnowledge', on_delete=models.CASCADE) + instrument = models.ForeignKey( + "basicknowledge.BasicKnowledge", on_delete=models.CASCADE + ) class Meta: constraints = [ - models.UniqueConstraint(fields=['uuid', 'instrument', 'user'], name='unique_instrument_bookmark_per_user') + models.UniqueConstraint( + fields=["uuid", "instrument", "user"], + name="unique_instrument_bookmark_per_user", + ) ] + + +class Highlight(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + content_block = models.ForeignKey( + "books.ContentBlock", on_delete=models.CASCADE, related_name="highlights" + ) + # see highlight.ts for comments + content_index = models.IntegerField() + content_uuid = models.UUIDField() + paragraph_index = models.IntegerField() + start_position = models.IntegerField() + selection_length = models.IntegerField() + text = models.TextField() + color = models.CharField(max_length=50) diff --git a/server/notes/schema.py b/server/notes/schema.py index 1c18d25a..eeba20bf 100644 --- a/server/notes/schema.py +++ b/server/notes/schema.py @@ -2,7 +2,14 @@ import graphene from graphene import relay from graphene_django import DjangoObjectType -from notes.models import Note, ContentBlockBookmark, ModuleBookmark, ChapterBookmark, InstrumentBookmark +from notes.models import ( + Highlight, + Note, + ContentBlockBookmark, + ModuleBookmark, + ChapterBookmark, + InstrumentBookmark, +) class NoteNode(DjangoObjectType): @@ -36,7 +43,6 @@ class ModuleBookmarkNode(DjangoObjectType): fields = "__all__" - class ChapterBookmarkNode(DjangoObjectType): note = graphene.Field(NoteNode) @@ -56,3 +62,11 @@ class InstrumentBookmarkNode(DjangoObjectType): fields = "__all__" filter_fields = [] interfaces = (relay.Node,) + + +class HighlightNode(DjangoObjectType): + class Meta: + model = Highlight + fields = "__all__" + filter_fields = [] + interfaces = (relay.Node,) diff --git a/server/schema.graphql b/server/schema.graphql index 3f9887f5..d894170a 100644 --- a/server/schema.graphql +++ b/server/schema.graphql @@ -511,6 +511,7 @@ type ContentBlockNode implements Node & ContentBlockInterface { originalCreator: PublicUserNode instrumentCategory: InstrumentCategoryNode path: String + highlights: [HighlightNode] } type ContentBlockBookmarkNode implements Node { @@ -616,6 +617,20 @@ type InstrumentCategoryNode implements Node { types: [InstrumentTypeNode] } +type HighlightNode implements Node { + """The ID of the object""" + id: ID! + user: PrivateUserNode! + contentBlock: ContentBlockNode! + contentIndex: Int! + contentUuid: UUID! + paragraphIndex: Int! + startPosition: Int! + selectionLength: Int! + text: String! + color: String! +} + type SnapshotObjectiveGroupNode implements Node { """The ID of the object""" id: ID!