Skip to content

Commit

Permalink
feat: add import taxonomy endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido committed Nov 1, 2023
1 parent 51e2724 commit c1d5944
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 1 deletion.
20 changes: 20 additions & 0 deletions openedx_tagging/core/tagging/import_export/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,32 @@

from django.utils.translation import gettext as _

from .. import api as taxonomy_api
from ..models import TagImportTask, TagImportTaskState, Taxonomy
from .exceptions import TagImportError
from .import_plan import TagImportPlan, TagImportTask
from .parsers import ParserFormat, get_parser


def create_taxonomy_and_import_tags(
taxonomy_name: str,
taxonomy_description: str,
file: BytesIO,
parser_format: ParserFormat
) -> bool:
"""
Create a taxonomy and import the tags from `file`
"""
taxonomy = taxonomy_api.create_taxonomy(taxonomy_name, taxonomy_description)

import_success = import_tags(taxonomy, file, parser_format)

if not import_success:
taxonomy.delete()

return import_success


def import_tags(
taxonomy: Taxonomy,
file: BytesIO,
Expand Down
31 changes: 31 additions & 0 deletions openedx_tagging/core/tagging/rest_api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from openedx_tagging.core.tagging.data import TagData
from openedx_tagging.core.tagging.models import ObjectTag, Tag, Taxonomy
from openedx_tagging.core.tagging.import_export.parsers import ParserFormat


class TaxonomyListQueryParamsSerializer(serializers.Serializer): # pylint: disable=abstract-method
Expand Down Expand Up @@ -175,3 +176,33 @@ class TaxonomyTagDeleteBodySerializer(serializers.Serializer): # pylint: disabl
child=serializers.CharField(), required=True
)
with_subtags = serializers.BooleanField(required=False)


class TaxonomyImportBodySerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer of the body for the Taxonomy Import action
"""
taxonomy_name = serializers.CharField(required=True)
taxonomy_description = serializers.CharField(default="")
file = serializers.FileField(required=True)

def get_parser_format(self, obj):
"""
Returns the ParserFormat based on the file extension
"""
filename = obj["file"].name
ext = filename.split('.')[-1]
if ext.lower() == 'csv':
return ParserFormat.CSV
elif ext.lower() == 'json':
return ParserFormat.JSON
else:
raise serializers.ValidationError(f'File type not supported: ${ext.lower()}')

def to_internal_value(self, data):
"""
Adds the parser_format to the validated data
"""
validated_data = super().to_internal_value(data)
validated_data['parser_format'] = self.get_parser_format(data)
return validated_data
5 changes: 5 additions & 0 deletions openedx_tagging/core/tagging/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@
views_import.TemplateView.as_view(),
name="taxonomy-import-template",
),
path(
"import/",
views_import.ImportView.as_view(),
name="taxonomy-import",
),
]
51 changes: 50 additions & 1 deletion openedx_tagging/core/tagging/rest_api/v1/views_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@

import os

from django.http import FileResponse, Http404
from django.http import FileResponse, Http404, HttpResponse
from rest_framework.request import Request
from rest_framework.views import APIView

from ...import_export import api
from .serializers import (
TaxonomyImportBodySerializer,
)


class TemplateView(APIView):
"""
Expand Down Expand Up @@ -49,3 +54,47 @@ def get(self, request: Request, file_ext: str, *args, **kwargs) -> FileResponse:
response = FileResponse(fh, content_type=content_type)
response['Content-Disposition'] = content_disposition
return response


class ImportView(APIView):
"""
View to import taxonomies
**Example Requests**
POST /tagging/rest_api/v1/import/
{
"taxonomy_name": "Taxonomy Name",
"taxonomy_description": "This is a description",
"file": <file>,
}
**Query Returns**
* 200 - Success
* 400 - Bad request
* 405 - Method not allowed
"""
http_method_names = ['post']

def post(self, request: Request, *args, **kwargs) -> HttpResponse:
"""
Imports the taxonomy from the uploaded file.
"""
body = TaxonomyImportBodySerializer(data=request.data)
body.is_valid(raise_exception=True)

taxonomy_name = body.validated_data["taxonomy_name"]
taxonomy_description = body.validated_data["taxonomy_description"]
file = body.validated_data["file"].file
parser_format = body.validated_data["parser_format"]

import_success = api.create_taxonomy_and_import_tags(
taxonomy_name=taxonomy_name,
taxonomy_description=taxonomy_description,
file=file,
parser_format=parser_format,
)

if import_success:
return HttpResponse(status=200)
else:
return HttpResponse(status=400)
44 changes: 44 additions & 0 deletions tests/openedx_tagging/core/tagging/test_views_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
"""
from __future__ import annotations

import json

import ddt # type: ignore[import]
from django.core.files.uploadedfile import SimpleUploadedFile
from rest_framework import status
from rest_framework.test import APITestCase

from openedx_tagging.core.tagging.models import Tag, Taxonomy

TAXONOMY_TEMPLATE_URL = "/tagging/rest_api/v1/import/{filename}"
TAXONOMY_IMPORT_URL = "/tagging/rest_api/v1/import/"


@ddt.ddt
Expand Down Expand Up @@ -37,3 +43,41 @@ def test_download_method_not_allowed(self):
url = TAXONOMY_TEMPLATE_URL.format(filename="template.txt")
response = self.client.post(url)
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED


class TestImportView(APITestCase):
"""
Tests the import taxonomy view.
"""

def test_import(self):
url = TAXONOMY_IMPORT_URL
new_tags = [
{"id": "tag_1", "value": "Tag 1"},
{"id": "tag_2", "value": "Tag 2"},
{"id": "tag_3", "value": "Tag 3"},
{"id": "tag_4", "value": "Tag 4"},
]
json_data = {"tags": new_tags}
file = SimpleUploadedFile("taxonomy.json", json.dumps(json_data).encode(), content_type="application/json")

response = self.client.post(
url,
{
"taxonomy_name": "Imported Taxonomy name",
"taxonomy_description": "Imported Taxonomy description",
"file": file
},
format="multipart"
)
assert response.status_code == status.HTTP_200_OK

# Check if the taxonomy was created
taxonomy = Taxonomy.objects.get(name="Imported Taxonomy name")
assert taxonomy.description == "Imported Taxonomy description"

# Check if the tags were created
tags = list(Tag.objects.filter(taxonomy=taxonomy))
assert len(tags) == len(new_tags)
for i, tag in enumerate(tags):
assert tag.value == new_tags[i]["value"]

0 comments on commit c1d5944

Please sign in to comment.