Friday, January 26, 2024

Starting with the lecture app

A new app called lectures was created and the following very basic model just as a starting point:

class Lecture(models.Model):
    '''Lecture model'''

    course = models.ForeignKey(
        'courses.Course',
        models.SET_NULL,
        null=True
    )
    title = models.CharField(max_length=300)
    description = models.TextField(blank=True, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title

Each lecture has to belong to a course, has to have a title and a description. For now, I have not created a ForeignKey pointing to User though it might be useful to know which instructor created the course and which one updated it. But, again, since this app is mainly for small-time instructors (such as myself) to host their courses rather than become a full-scale MOOC platform, there may not be that many instructors and so just logs might be good enough to keep track of what's going on. It can be added without too much fuss.

All URLs for lectures will be with respect to courses, and this implies, the structure will be /api/courses/<course-slug>/lectures/<lecture-urls>. For this reason, I did not add a placeholder for lectures in the main urls.py file, but rather inside the urls.py file of the courses app. In the urls.py file of the lectures app, I started off with a basic create view. Using the learning from the views in courses, I created a base view that inherits UserAuthentication and will later implement a get_object which will determine if the user has the authority to view the lecture.

class LectureBaseView(GenericAPIView, UserAuthentication):
    '''Basic lecture view'''

    serializer_class = LectureSerializer
    user_model = User
    lookup_field = 'id'


class LectureView(LectureBaseView, CreateModelMixin):
    '''Basic lecture view'''

    def get(self, request, *args, **kwargs):
        return Response('TODO')

    def post(self, request, *args, **kwargs):
        course_slug = self.kwargs.get('slug', None)
        try:
            course_obj = Course.objects.get_course_by_slug(course_slug)
            self.authenticate(request)
            serializer = LectureSerializer(data=request.data)
            serializer.save(
                user=self.request.user,
                course=course_obj
            )
            return Response(serializer.data)
        except Http400Error as e:
            return Response(
                data=str(e),
                status=status.HTTP_400_BAD_REQUEST
            )
        except Http403Error as e:
            return Response(
                data=str(e),
                status=status.HTTP_403_FORBIDDEN
            )
        except Http404Error as e:
            return Response(
                data=str(e),
                status=status.HTTP_404_NOT_FOUND
            )
        except Exception:
            return Response(
                data=DEFAULT_ERROR_RESPONSE,
                status=status.HTTP_400_BAD_REQUEST
            )

Though I am inheriting the CreateModelMixin, I am not really using it as I would like to throw custom exceptions rather than ValidationErrors by rest_framework. Maybe I will remove it later. But other than that the usual stuff, make sure user is authenticated and as an instructor (which means admin). Once course has been extracted from the slug and the user from the JWT token, the lecture can be created from the LectureSerializer:

class LectureSerializer(serializers.ModelSerializer):
    '''Serializer for Lecture model'''

    def save(self, *args, **kwargs):
        if self.is_valid():
            return super().save(*args, **kwargs)
        else:
            raise Http400Error(extract_serializer_error(self.errors))

    def check_user_is_instructor(self, course, user):
        if user is None:
            raise Http403Error(
                'Must be logged in as an instructor to create lectures'
            )
        if not course.check_user_is_instructor(user):
            raise Http403Error(
                'Must be an instructor of the course to create lectures'
            )
        return True

    def create(self, validated_data):
        user = validated_data.get('user', None)
        course = validated_data.get('course', None)
        if self.check_user_is_instructor(course, user):
            del validated_data['user']
            del validated_data['course']
            return Lecture.objects.create(
                **validated_data,
                course=course
            )

    class Meta:
        model = Lecture
        fields = ['title', 'description']
        extra_kwargs = {
            'title': {
                'error_messages': {
                    'required': 'The title of a lecture is required',
                    'blank': 'The title of a lecture is required'
                }
            }
        }

The create method checks if the user is an instructor and only then creates the course or else throws an exception.

Tried it out a bit through Postman and it works. A little more tweaking required to ensure that two lectures with the same title do not exist in the same course. That will probably a lecture manager method. Tests will make the code better.

No comments:

Post a Comment