Monday, April 15, 2024

Lecture detail view with video list

To get started with videos, I need a new model VideoContent that has the name of the video, the course to which the video belongs and the video file. A video is associated with a course and then can be added to a lecture. This association between course and video is loose, it is merely to have some association while uploading them.

def video_file_path(instance, filename):
    '''
    Generate path that has course slug.
    Called when a video file is uploaded to Video model instance.

    Attributes
    ----------------
    instance : Video model instance
    filename : str

    Raises
    ----------------
    400 error
        If uploading for a course that does not exist

    Returns
    ----------------
    String with file path containing course slug
    '''

    course = instance.course
    if course is None:
        raise CustomAPIError(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail='Associated course not found'
        )
    dir_name = ''.join(filter(str.isalnum, course.slug))
    return f'{dir_name}/{filename}'


class VideoContent(models.Model):
    '''
    Video model for lectures

    Attributes
    ---------------
    course : Reference to a course model instance
    video_file : File
    created_at: Datetime
        Autogenerated when model is created
    updated_at: Datetime
        Autoupdated when model is updated
    '''
    course = models.ForeignKey(
        'courses.Course',
        related_name='videos',
        null=True,
        on_delete=models.SET_NULL
    )
    name = models.CharField(
        max_length=300,
        unique=True,
        default='Video name'
    )
    video_file = models.FileField(upload_to=video_file_path, max_length=300)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    @property
    def video_file_path(self):
        return f'{settings.BASE_URL}{self.video_file.url}'

    objects = VideoContentManager()

    def __str__(self):
        return self.video_file.url

The VideoContent model is quite simple - there is a name field which is unique, but that is just to be able to search for videos while associating them with lectures. There is mainly the video_file field which contains the uploaded file contents. I defined a property video_file_path that returns the URL of the uploaded file - this will be used when returning the video details so that the frontend can fetch the video and play it. To upload the video file, I created an uploader method that uploads the file into a directory according to the course slug.

To upload this video through an API, Django Rest Framework recommends using the FormParser and the MultiPartParser. Also, the file name needs to be a part of the API url. Therefore, the definition of the URL for uploading videos will be:

/api/courses/course-slug/lectures/id/add-video/filename

To be able to use Postman to upload files, the body of the request needs to be form-data with name as a parameter and the second parameter will be of type File. How this will be done with React will need to be checked out later.

The view class for video upload will be:

class VideoContentView(APIView, UserAuthentication):
    '''
    View for uploading videos

    Attributes
    -----------------
    parser_classes : list
        FormParser and MultiPartParser used for file uploads

    Methods
    -----------------
    post(request, filename, *args, **kwargs):
        Creates VideoContent model instance and handles file uploads
    '''

    parser_classes = [FormParser, MultiPartParser, ]
    user_model = User

    def post(self, request, filename, *args, **kwargs):
        '''
        Creates VideoContent model instance and handles file uploads

        Parameters
        ---------------
        request : Request object
        filename : str
            Name of the video file

        Raises
        ---------------
        400 error:
            If course slug is not present in url
            If video name is not provided in request body
            If video name is not unique
        403 error:
            If no credentials are provided in header
            If non-admin credentials are provided in header
            If non-instructor credentials are provided in header
        404 error:
            If lecture cannot be found

        Returns
        ---------------
        201 with VideoContentSerializer data
        '''
        user = self.authenticate(request)
        file_obj = request.data['File']

        course_slug = self.kwargs.get('slug', None)
        course_obj = Course.objects.get_course_by_slug(
            course_slug,
            admin_only=True
        )
        if not course_obj.check_user_is_instructor(user):
            raise CustomAPIError(
                status_code=status.HTTP_403_FORBIDDEN,
                detail='Only an instructor can add videos'
            )

        video_name = request.data.get('name')
        if video_name is None:
            raise CustomAPIError(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail='Video name is required'
            )

        VideoContent.objects.is_video_name_unique(video_name)

        video_obj = VideoContent.objects.create(
            name=video_name,
            course=course_obj,
            video_file=file_obj
        )

        lecture_id = self.kwargs.get('id')
        Lecture.objects.add_video_to_lecture(lecture_id, video_obj)

        serializer = VideoContentSerializer(video_obj)

        return Response(
            data=serializer.data,
            status=status.HTTP_201_CREATED
        )

After the video is uploaded, the video is added to the lecture with the manager method add_video_to_lecture which will connect the video model instance to the videos field in the lecture instance with the ID specified in the API URL. The API returns the serializer which is:

class VideoContentSerializer(serializers.ModelSerializer):
    '''
    Serializer for model VideoContent
    '''
    video_file_path = serializers.ReadOnlyField()

    class Meta:
        model = VideoContent
        fields = ['name', 'video_file_path']

Now that the video has been uploaded, the videos have to show up in the lecture detail view. The lecture list view will not show the video list as the lecture list is an open end-point while the detail view is only for registered users or admin.

Therefore, defining a separate LectureDetailSerializer:

class LectureDetailSerializer(serializers.ModelSerializer):
    '''
    Serializer for detail view of Lecture including related videos
    '''

    videos = VideoContentSerializer(many=True, read_only=True)

    class Meta:
        model = Lecture
        fields = ['id', 'title', 'description', 'seq_no', 'videos']

The LectureDetailSerializer has an embedded serializer with video details. This now needs to be connected to the lecture view class. the only change that needs to be made in the LectureView class is to add a get_serializer_class that returns which serializer class to use according to the request type:

def get_serializer_class(self):
    '''
    Return the serializer_class according to the view.

    Returns
    ------------------
    LectureDetailSerializer for detail views and
    LectureSerializer for all other views
    '''
    if self.request.method == 'GET' and self.kwargs.get('id') is not None:
        return LectureDetailSerializer
    return LectureSerializer

Only when the request type is a GET and there is a lecture ID present in the URL parameters, should the detail serializer be used - in all other cases, the basic serializer should be used.

With this, now there is separate behavior for list and detail views as intended. And, a basic structure of the backend now exists - user registration, user login, course creation, course update, course fetch, lecture creation, lecture update, lecture fetch, video upload. Now to start coding the frontend and add features to the backend as needed.

No comments:

Post a Comment