22Serializers for the content libraries REST API
33"""
44# pylint: disable=abstract-method
5+ import json
6+ import logging
7+
58from django .core .validators import validate_unicode_slug
69from opaque_keys import InvalidKeyError , OpaqueKey
710from opaque_keys .edx .locator import LibraryContainerLocator , LibraryUsageLocatorV2
8- from openedx_learning .api .authoring_models import Collection
11+ from openedx_learning .api .authoring_models import Collection , LearningPackage
912from rest_framework import serializers
1013from rest_framework .exceptions import ValidationError
14+ from user_tasks .models import UserTaskStatus
1115
16+ from openedx .core .djangoapps .content_libraries .tasks import LibraryRestoreTask
1217from openedx .core .djangoapps .content_libraries .api .containers import ContainerType
1318from openedx .core .djangoapps .content_libraries .constants import ALL_RIGHTS_RESERVED , LICENSE_OPTIONS
1419from openedx .core .djangoapps .content_libraries .models import (
2227
2328DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
2429
30+ log = logging .getLogger (__name__ )
31+
2532
2633class ContentLibraryMetadataSerializer (serializers .Serializer ):
2734 """
@@ -37,6 +44,7 @@ class ContentLibraryMetadataSerializer(serializers.Serializer):
3744 slug = serializers .CharField (source = "key.slug" , validators = (validate_unicode_slug , ))
3845 title = serializers .CharField ()
3946 description = serializers .CharField (allow_blank = True )
47+ learning_package = serializers .PrimaryKeyRelatedField (queryset = LearningPackage .objects .all (), required = False )
4048 num_blocks = serializers .IntegerField (read_only = True )
4149 last_published = serializers .DateTimeField (format = DATETIME_FORMAT , read_only = True )
4250 published_by = serializers .CharField (read_only = True )
@@ -426,3 +434,111 @@ class LibraryBackupTaskStatusSerializer(serializers.Serializer):
426434 """
427435 state = serializers .CharField ()
428436 url = serializers .FileField (source = 'file' , allow_null = True , use_url = True )
437+
438+
439+ class LibraryRestoreFileSerializer (serializers .Serializer ):
440+ """
441+ Serializer for restoring a library from a backup file.
442+ """
443+ # input only fields
444+ file = serializers .FileField (write_only = True , help_text = "A ZIP file containing a library backup." )
445+
446+ # output only fields
447+ task_id = serializers .UUIDField (read_only = True )
448+
449+ def validate_file (self , value ):
450+ """
451+ Validate that the uploaded file is a ZIP file.
452+ """
453+ if value .content_type != 'application/zip' :
454+ raise serializers .ValidationError ("Only ZIP files are allowed." )
455+ return value
456+
457+
458+ class LibraryRestoreTaskRequestSerializer (serializers .Serializer ):
459+ """
460+ Serializer for requesting the status of a library restore task.
461+ """
462+ task_id = serializers .UUIDField (write_only = True , help_text = "The ID of the restore task to check." )
463+
464+
465+ class RestoreSuccessDataSerializer (serializers .Serializer ):
466+ """
467+ Serializer for the data returned upon successful restoration of a library.
468+ """
469+ learning_package_id = serializers .IntegerField (source = "lp_restored_data.id" )
470+ title = serializers .CharField (source = "lp_restored_data.title" )
471+ org = serializers .CharField (source = "lp_restored_data.archive_org_key" )
472+ slug = serializers .CharField (source = "lp_restored_data.archive_slug" )
473+
474+ # The `key` is a unique temporary key assigned to the learning package during the restore process,
475+ # whereas the `archive_key` is the original key of the learning package from the backup.
476+ # The temporary learning package key is replaced with a standard key once it is added to a content library.
477+ key = serializers .CharField (source = "lp_restored_data.key" )
478+ archive_key = serializers .CharField (source = "lp_restored_data.archive_lp_key" )
479+
480+ containers = serializers .IntegerField (source = "lp_restored_data.num_containers" )
481+ components = serializers .IntegerField (source = "lp_restored_data.num_components" )
482+ collections = serializers .IntegerField (source = "lp_restored_data.num_collections" )
483+ sections = serializers .IntegerField (source = "lp_restored_data.num_sections" )
484+ subsections = serializers .IntegerField (source = "lp_restored_data.num_subsections" )
485+ units = serializers .IntegerField (source = "lp_restored_data.num_units" )
486+
487+ created_on_server = serializers .CharField (source = "backup_metadata.original_server" , required = False )
488+ created_at = serializers .DateTimeField (source = "backup_metadata.created_at" , format = DATETIME_FORMAT )
489+ created_by = serializers .SerializerMethodField ()
490+
491+ def get_created_by (self , obj ):
492+ """
493+ Get the user information of the archive creator, if available.
494+
495+ The information is stored in the backup metadata of the archive and references
496+ a user that may not exist in the system where the restore is being performed.
497+ """
498+ username = obj ["backup_metadata" ].get ("created_by" )
499+ email = obj ["backup_metadata" ].get ("created_by_email" )
500+ return {"username" : username , "email" : email }
501+
502+
503+ class LibraryRestoreTaskResultSerializer (serializers .Serializer ):
504+ """
505+ Serializer for the result of a library restore task.
506+ """
507+ state = serializers .CharField ()
508+ result = RestoreSuccessDataSerializer (required = False , allow_null = True , default = None )
509+ error = serializers .CharField (required = False , allow_blank = True , default = None )
510+ error_log = serializers .FileField (source = 'error_log_url' , allow_null = True , use_url = True , default = None )
511+
512+ @classmethod
513+ def from_task_status (cls , task_status , request ):
514+ """Build serializer input from task status object."""
515+
516+ # If the task did not complete, just return the state.
517+ if task_status .state not in {UserTaskStatus .SUCCEEDED , UserTaskStatus .FAILED }:
518+ return cls ({
519+ "state" : task_status .state ,
520+ })
521+
522+ artifact_name = LibraryRestoreTask .ARTIFACT_NAMES .get (task_status .state , '' )
523+ artifact = task_status .artifacts .filter (name = artifact_name ).first ()
524+
525+ # If the task failed, include the log artifact if it exists
526+ if task_status .state == UserTaskStatus .FAILED :
527+ return cls ({
528+ "state" : UserTaskStatus .FAILED ,
529+ "error" : "Library restore failed. See error log for details." ,
530+ "error_log_url" : artifact .file if artifact else None ,
531+ }, context = {'request' : request })
532+
533+ if task_status .state == UserTaskStatus .SUCCEEDED :
534+ input_data = {
535+ "state" : UserTaskStatus .SUCCEEDED ,
536+ }
537+ try :
538+ result = json .loads (artifact .text ) if artifact else {}
539+ input_data ["result" ] = result
540+ except json .JSONDecodeError :
541+ log .error ("Failed to decode JSON from artifact (%s): %s" , artifact .id , artifact .text )
542+ input_data ["error" ] = f'Could not decode artifact JSON. Artifact Text: { artifact .text } '
543+
544+ return cls (input_data )
0 commit comments