Skip to content

Commit c616ed0

Browse files
authored
Add content management queries and mutations (#91)
* Add content management queries and mutations * Fix pre-commit fail issue * Fix merge issue on schema file * Update schema
1 parent f467c0c commit c616ed0

16 files changed

+541
-7
lines changed

.pre-commit-config.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ repos:
1717
rev: 24.4.2
1818
hooks:
1919
- id: black
20+
exclude: ^.*migrations.*
21+
2022
# args: ["--check"]
2123

2224
- repo: https://github.com/PyCQA/isort
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import argparse
2+
3+
from django.core.management.base import BaseCommand
4+
from strawberry.printer import print_schema
5+
6+
from main.graphql.schema import schema
7+
8+
9+
class Command(BaseCommand):
10+
help = "Create schema.graphql file"
11+
12+
def add_arguments(self, parser):
13+
parser.add_argument(
14+
"--out",
15+
type=argparse.FileType("w"),
16+
default="schema.graphql",
17+
)
18+
19+
def handle(self, *args, **options):
20+
file = options["out"]
21+
file.write(print_schema(schema))
22+
file.close()
23+
self.stdout.write(self.style.SUCCESS(f"{file.name} file generated"))

content/enums.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import strawberry
2+
3+
from content.models import Content
4+
from utils.strawberry.enums import get_enum_name_from_django_field
5+
6+
DocumentStatusTypeEnum = strawberry.enum(Content.DocumentStatus, name="DocumentStatusTypeEnum")
7+
8+
DocumentTypeEnum = strawberry.enum(Content.DocumentType, name="DocumentTypeEnum")
9+
10+
11+
enum_map = {
12+
get_enum_name_from_django_field(field): enum
13+
for field, enum in (
14+
(Content.document_status, DocumentStatusTypeEnum),
15+
(Content.document_type, DocumentTypeEnum),
16+
)
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.1.6 on 2025-02-23 08:35
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('content', '0002_alter_content_document_type'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='content',
15+
name='document_url',
16+
field=models.URLField(blank=True, null=True),
17+
),
18+
migrations.AlterField(
19+
model_name='content',
20+
name='document_type',
21+
field=models.IntegerField(choices=[(1, 'WEB_URl'), (2, 'PDF'), (3, 'Text')], default=3),
22+
),
23+
]

content/models.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def __str__(self):
1717

1818
class Content(UserResource):
1919
class DocumentType(models.IntegerChoices):
20-
WORD = 1, _("Word")
20+
WEB_URl = 1, _("WEB_URl")
2121
PDF = 2, _("PDF")
2222
TEXT = 3, _("Text")
2323

@@ -31,6 +31,7 @@ class DocumentStatus(models.IntegerChoices):
3131
title = models.CharField(max_length=100)
3232
document_type = models.IntegerField(choices=DocumentType.choices, default=DocumentType.TEXT)
3333
document_file = models.FileField(upload_to="documents")
34+
document_url = models.URLField(null=True, blank=True)
3435
extracted_file = models.FileField(upload_to="documents-extracts", null=True, blank=True)
3536
content_id = models.UUIDField(default=uuid.uuid4, editable=False)
3637
document_status = models.PositiveSmallIntegerField(choices=DocumentStatus.choices, default=DocumentStatus.PENDING)

content/mutations.py

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import typing
2+
3+
import strawberry
4+
from asgiref.sync import sync_to_async
5+
from strawberry.file_uploads import Upload
6+
7+
from content.models import Content
8+
from content.serializers import (
9+
ArchiveContentSerializer,
10+
ContentSerializer,
11+
TagSerializer,
12+
UpdateContentSerializer,
13+
)
14+
from content.types import ContentType, TagType
15+
from main.graphql.context import Info
16+
from utils.strawberry.mutations import (
17+
ModelMutation,
18+
MutationEmptyResponseType,
19+
MutationResponseType,
20+
mutation_is_not_valid,
21+
process_input_data,
22+
)
23+
24+
25+
@strawberry.input
26+
class FolderInput:
27+
files: typing.List[Upload]
28+
29+
30+
CreateContentMutation = ModelMutation("Content", ContentSerializer)
31+
UpdateMutation = ModelMutation("UpdateContent", UpdateContentSerializer)
32+
DeleteContent = ModelMutation("archive", ArchiveContentSerializer)
33+
CreateTagMutation = ModelMutation("CreateTag", TagSerializer)
34+
35+
36+
@strawberry.type
37+
class PrivateMutation:
38+
@strawberry.mutation
39+
async def create_content(
40+
self,
41+
data: CreateContentMutation.InputType, # type: ignore[reportInvalidTypeForm]
42+
info: Info,
43+
) -> MutationResponseType[ContentType]:
44+
return await CreateContentMutation.handle_create_mutation(data, info, None)
45+
46+
@strawberry.mutation
47+
def read_file(self, file: Upload) -> str:
48+
return file.read().decode("utf-8")
49+
50+
@strawberry.mutation
51+
async def update_content_title(
52+
self,
53+
id: strawberry.ID,
54+
data: UpdateMutation.PartialInputType, # type: ignore[reportInvalidTypeForm]
55+
info: Info,
56+
) -> MutationResponseType[ContentType]:
57+
try:
58+
instance = await Content.objects.aget(id=id)
59+
except Content.DoesNotExist:
60+
return MutationResponseType(ok=False, errors=["Content not found"])
61+
serializer = UpdateContentSerializer(
62+
instance, data=process_input_data(data), context={"request": info.context.request}, partial=True
63+
)
64+
if errors := mutation_is_not_valid(serializer):
65+
return MutationResponseType(ok=False, errors=errors)
66+
await sync_to_async(serializer.save)()
67+
return MutationResponseType()
68+
69+
@strawberry.mutation
70+
async def archive_content(
71+
self, id: strawberry.ID, info: Info, data: DeleteContent.PartialInputType # type: ignore[reportInvalidTypeForm]
72+
) -> MutationEmptyResponseType:
73+
try:
74+
instance = await Content.objects.aget(id=id)
75+
except Content.DoesNotExist:
76+
return MutationEmptyResponseType(ok=False, errors=["Content not found"])
77+
78+
serializer = ArchiveContentSerializer(
79+
instance, data=process_input_data(data), context={"request": info.context.request}, partial=True
80+
)
81+
82+
if errors := mutation_is_not_valid(serializer):
83+
return MutationEmptyResponseType(ok=False, errors=errors)
84+
85+
await sync_to_async(serializer.save)()
86+
87+
return MutationEmptyResponseType(ok=True)
88+
89+
@strawberry.mutation
90+
async def create_tag(
91+
self,
92+
data: CreateTagMutation.InputType, # type: ignore[reportInvalidTypeForm]
93+
info: Info,
94+
) -> MutationResponseType[TagType]:
95+
return await CreateTagMutation.handle_create_mutation(data, info, None)

content/queries.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import strawberry
2+
import strawberry_django
3+
4+
from content.types import ContentType, TagType
5+
from main.graphql.context import Info
6+
from utils.strawberry.paginations import CountList, pagination_field
7+
8+
9+
@strawberry.type
10+
class PrivateQuery:
11+
content: CountList[ContentType] = pagination_field(
12+
pagination=True,
13+
)
14+
15+
@strawberry_django.field(description="Return all content")
16+
async def all_content(self, info: Info) -> list[ContentType]:
17+
return [content async for content in ContentType.get_queryset(None, None, info)]
18+
19+
tag: CountList[ContentType] = pagination_field(
20+
pagination=True,
21+
)
22+
23+
@strawberry_django.field()
24+
async def all_tags(self, info: Info) -> list[TagType]:
25+
return [tag async for tag in TagType.get_queryset(None, None, info)]

content/serializers.py

+64
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,73 @@
11
from rest_framework import serializers
22

33
from chat.models import UserChatSession
4+
from content.models import Content, Tag
5+
from utils.file_check import validate_document_size, validate_file_type
46

57

68
class UserQuerySerializer(serializers.Serializer):
79
query = serializers.CharField(required=True, allow_null=False, allow_blank=False)
810
user_id = serializers.UUIDField(required=True)
911
platform = serializers.IntegerField(default=UserChatSession.Platform.STREAMLIT)
12+
13+
14+
class TagSerializer(serializers.ModelSerializer):
15+
class Meta:
16+
model = Tag
17+
fields = ["name", "description"]
18+
19+
20+
class ContentSerializer(serializers.ModelSerializer):
21+
tag = serializers.PrimaryKeyRelatedField(queryset=Tag.objects.all(), many=True, required=False)
22+
document_file = serializers.FileField(required=False)
23+
24+
class Meta:
25+
model = Content
26+
fields = ["title", "document_file", "tag", "document_type", "document_url"]
27+
read_only_fields = ["created_by", "modified_by"]
28+
29+
def validate_document_file(self, file):
30+
validate_document_size(file)
31+
validate_file_type(file)
32+
return file
33+
34+
def create(self, validated_data):
35+
tags = validated_data.pop("tag", [])
36+
validated_data["created_by"] = self.context["request"].user
37+
validated_data["modified_by"] = self.context["request"].user
38+
content = super().create(validated_data)
39+
content.tag.set(tags)
40+
return content
41+
42+
43+
class UpdateContentSerializer(serializers.ModelSerializer):
44+
class Meta:
45+
model = Content
46+
fields = ["title"]
47+
read_only_fields = ["modified_by"]
48+
49+
def update(self, instance, validated_data):
50+
validated_data["modified_by"] = self.context["request"].user
51+
52+
return super().update(instance, validated_data)
53+
54+
55+
class ArchiveContentSerializer(serializers.ModelSerializer):
56+
class Meta:
57+
model = Content
58+
fields = [
59+
"is_deleted",
60+
]
61+
read_only_fields = ["deleted_at", "deleted_by"]
62+
63+
def validate(self, data):
64+
instance = self.instance
65+
if instance.is_deleted:
66+
raise serializers.ValidationError("Content is already deleted.")
67+
return data
68+
69+
def update(self, instance, validated_data):
70+
instance.is_deleted = True
71+
instance.deleted_by = self.context["request"].user
72+
instance.save(update_fields=("is_deleted", "deleted_by"))
73+
return instance

content/types.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import strawberry
2+
import strawberry_django
3+
from django.db import models
4+
5+
from content.models import Content, Tag
6+
from main.graphql.context import Info
7+
from utils.common import get_queryset_for_model
8+
from utils.strawberry.enums import enum_field
9+
10+
11+
@strawberry_django.type(Tag)
12+
class TagType:
13+
id: strawberry.ID
14+
name: strawberry.auto
15+
description: strawberry.auto
16+
17+
@staticmethod
18+
def get_queryset(_, queryset: models.QuerySet | None, info: Info) -> models.QuerySet:
19+
return get_queryset_for_model(Tag, queryset)
20+
21+
22+
@strawberry_django.type(Tag)
23+
class TagNameType:
24+
name: strawberry.auto
25+
26+
27+
@strawberry_django.type(Content)
28+
class ContentType:
29+
id: strawberry.ID
30+
title: strawberry.auto
31+
extracted_file: strawberry.auto
32+
tag: list[TagNameType]
33+
document_status = enum_field(Content.document_status)
34+
document_type = enum_field(Content.document_type)
35+
36+
@staticmethod
37+
def get_queryset(_, queryset: models.QuerySet | None, info: Info):
38+
return get_queryset_for_model(Content, queryset)

main/graphql/enums.py

+2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import strawberry
44

5+
from content.enums import enum_map as content_enum_map
56
from user.enums import enum_map as user_enum_map
67

78
ENUM_TO_STRAWBERRY_ENUM_MAP: dict[str, type] = {
89
**user_enum_map,
10+
**content_enum_map,
911
}
1012

1113

main/graphql/schema.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import strawberry
22
from strawberry.django.views import AsyncGraphQLView
33

4+
from content import mutations as content_mutations
5+
from content import queries as content_queries
46
from user import mutations as user_mutations
57
from user import queries as user_queries
68

@@ -22,13 +24,15 @@ async def get_context(self, *args, **kwargs) -> GraphQLContext:
2224
@strawberry.type
2325
class PublicQuery(
2426
user_queries.PublicQuery,
27+
content_queries.PrivateQuery,
2528
):
2629
id: strawberry.ID = strawberry.ID("public")
2730

2831

2932
@strawberry.type
3033
class PrivateQuery(
3134
user_queries.PrivateQuery,
35+
content_queries.PrivateQuery,
3236
):
3337
id: strawberry.ID = strawberry.ID("private")
3438

@@ -41,7 +45,7 @@ class PublicMutation(
4145

4246

4347
@strawberry.type
44-
class PrivateMutation(user_mutations.PrivateMutation):
48+
class PrivateMutation(user_mutations.PrivateMutation, content_mutations.PrivateMutation):
4549
id: strawberry.ID = strawberry.ID("private")
4650

4751

main/urls.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@
2525
from main.graphql.schema import CustomAsyncGraphQLView
2626
from main.graphql.schema import schema as graphql_schema
2727

28-
urlpatterns = [path("admin/", admin.site.urls), path("chat_message", UserQuery.as_view())]
28+
urlpatterns = [
29+
path("admin/", admin.site.urls),
30+
path("chat_message", UserQuery.as_view()),
31+
path("graphql/", csrf_exempt(CustomAsyncGraphQLView.as_view(schema=graphql_schema, multipart_uploads_enabled=True))),
32+
]
2933
if settings.DEBUG:
30-
urlpatterns.append(path("graphiql/", csrf_exempt(CustomAsyncGraphQLView.as_view(schema=graphql_schema))))
34+
urlpatterns.append(
35+
path("graphiql/", csrf_exempt(CustomAsyncGraphQLView.as_view(schema=graphql_schema, multipart_uploads_enabled=True)))
36+
)
3137

3238
# Static and media file URLs
3339
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

0 commit comments

Comments
 (0)