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.