Dev Journal #10 - More than One Way to Skin the Cat



I am slowly realizing how massive Django library really is. For every task, I can come up with multiple ways to implement it. I am mostly experiencing this while working with the view classes. Today I was working on creating the APIs for all the exercise related models I created back in post #8. But before I explain the views, I should first briefly go over the serializers.

Exercise Model Serializers

There are just few pointers I wanted to mention about serializers since I reviewed most of it already when creating the UserProfileSerializer. First is the get_FIELD_NAME_display function. Because most of my models have a ChoiceField, I wanted to display the pretty text instead of the id by using this function.

class LeverageProgressionTypeSerializer(serializers.ModelSerializer):
    name = serializers.CharField(source='get_leverage_progression_type_display', read_only=True)

    class Meta:
        model = models.LeverageProgressionType
        fields = [
            'id',
            'name'
        ]

Next, I wanted to return nested information instead of an id for foreign key fields. Nested relationships can be expressed by using the serializers as fields. By default, nested serializers are read-only.

class DefaultSettingSerializer(serializers.ModelSerializer):
    angle_type = AngleTypeSerializer(read_only=True)
    apparatus_type = ApparatusTypeSerializer(read_only=True)
    arm_motion_type = ArmMotionTypeSerializer(read_only=True)
    grip_direction_type = GripDirectionTypeSerializer(read_only=True)
    grip_width_type = GripWidthTypeSerializer(read_only=True)
    leverage_progression_type = LeverageProgressionTypeSerializer(read_only=True)
    muscle_contraction_type = MuscleContractionTypeSerializer(read_only=True)
    plyometric_type = PlyometricTypeSerializer(read_only=True)
    unilateral_type = UnilateralTypeSerializer(read_only=True)
    weight_progression_type = WeightProgressionTypeSerializer(read_only=True)

    class Meta:
        model = models.DefaultSetting
        fields = '__all__'

Exercise Views

Back to views!

Initially, my approach was to simply use the Generic views. I used the generics.ListAPIView to provide the list of exercises and the generics.RetrieveAPIView to provide a detail view of the queried exercise.

class ExerciseAPIView(generics.ListAPIView):
    serializer_class = ExerciseSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Exercise.objects.all()

    def get_serializer_context(self, *args, **kwargs):
        return {'request': self.request}


class ExerciseDetailAPIView(generics.RetrieveAPIView):
    serializer_class = ExerciseSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Exercise.objects.all()

    def get_serializer_context(self, *args, **kwargs):
        return {'request': self.request}
app_name = 'api-exercises'

urlpatterns = [
    path('', ExerciseAPIView.as_view(), name='exercise-list'),
    path('<int:pk>/', ExerciseDetailAPIView.as_view(), name='exercise-detail'),
]

However, I noticed that the two views had the exact same definition. I tried to find a way to refactor them so that I wouldn’t have to repeat the same code. So my next solution was to use the ViewSets. ViewSet classes inherit from APIView, and they allow you to combine logic for related views into a single class.

class ExerciseViewSet(viewsets.ViewSet):
    permission_classes = [IsAuthenticated]

    @staticmethod
    def list(request):
        queryset = Exercise.objects.all()
        serializer = ExerciseSerializer(queryset, many=True)
        return Response(serializer.data)

    @staticmethod
    def retrieve(request, pk=None):
        queryset = Exercise.objects.all()
        exercise = get_object_or_404(queryset, pk=pk)
        serializer = ExerciseSerializer(exercise)
        return Response(serializer.data)

In addition, ViewSet uses routers which will configure the URL patterns accordingly instead of writing up the URL configurations manually.

app_name = 'api-exercises'
router = routers.DefaultRouter()
router.register(r'', ExerciseViewSet, base_name=app_name)
urlpatterns = router.urls

When I shared my code to the Django discord channel for some feedback, I received a suggestion to try using the ReadOnlyModelViewSet. I couldn’t believe how much simpler my class became.

class ExerciseViewSet(ReadOnlyModelViewSet):
    queryset = Exercise.objects.all()
    serializer_class = ExerciseSerializer
    permission_classes = [IsAuthenticated]
app_name = 'api-exercises'

router = routers.DefaultRouter()
router.register(r'', ExerciseViewSet)
urlpatterns = router.urls

As a beginner, it’s difficult to grasp which built-in classes are best appropriate. It also doesn’t help that I didn’t even know such classes existed in the first place! The best way, I find, is to first implement something that works on your own, but don’t just settle on your first solution. Search up on alternative ways to implement the same thing, read documentation around the tools you are using, and share your code with others for code reviews. There are many ways to skin a cat.

There are many ways to skin a cat (or peel an orange)

Continuing on with this method, I appended more views for each of the settings models. Since all of my lists are fairly short (the longest has 36 items), I removed the pagination class that capped a default limit of 10 items per page.

class ExerciseViewSet(ReadOnlyModelViewSet):
    queryset = models.Exercise.objects.all()
    pagination_class = None
    serializer_class = serializers.ExerciseSerializer
    permission_classes = [IsAuthenticated]


class ExerciseTypeViewSet(ReadOnlyModelViewSet):
    queryset = models.ExerciseType.objects.all()
    pagination_class = None
    serializer_class = serializers.ExerciseTypeSerializer
    permission_classes = [IsAuthenticated]


...


class WeightProgressionTypeViewSet(ReadOnlyModelViewSet):
    queryset = models.WeightProgressionType.objects.all()
    pagination_class = None
    serializer_class = serializers.WeightProgressionTypeSerializer
    permission_classes = [IsAuthenticated]
app_name = 'api-exercises'

router = routers.DefaultRouter()
router.register(r'exercises', views.ExerciseViewSet)
router.register(r'types', views.ExerciseTypeViewSet)
...
router.register(r'weight-progression-types', views.WeightProgressionTypeViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

Final API

The completed API root looks like the following:

{
    "exercises": "/api/exercises/exercises/",
    "motion-types": "/api/exercises/motion-types/",
    "skill-levels": "/api/exercises/skill-levels/",
    "angle-types": "/api/exercises/angle-types/",
    "apparatus-types": "/api/exercises/apparatus-types/",
    "arm-motion-types": "/api/exercises/arm-motion-types/",
    "grip-direction-types": "/api/exercises/grip-direction-types/",
    "grip-width-types": "/api/exercises/grip-width-types/",
    "leverage-progression-types": "/api/exercises/leverage-progression-types/",
    "muscle-contraction-types": "/api/exercises/muscle-contraction-types/",
    "plyometric-types": "/api/exercises/plyometric-types/",
    "unilateral-types": "/api/exercises/unilateral-types/",
    "weight-progression-types": "/api/exercises/weight-progression-types/",
    "angle-options": "/api/exercises/angle-options/",
    "apparatus-options": "/api/exercises/apparatus-options/",
    "arm-motion-options": "/api/exercises/arm-motion-options/",
    "grip-direction-options": "/api/exercises/grip-direction-options/",
    "grip-width-options": "/api/exercises/grip-width-options/",
    "leverage-progression-options": "/api/exercises/leverage-progression-options/",
    "muscle-contraction-options": "/api/exercises/muscle-contraction-options/",
    "plyometric-options": "/api/exercises/plyometric-options/",
    "unilateral-options": "/api/exercises/unilateral-options/",
    "weight-progression-options": "/api/exercises/weight-progression-options/",
    "default-settings": "/api/exercises/default-settings/"
}

The naming convention could use some work 😅

I will display an example of each type of API below. I will skip over the individual instance (retrieve API) as you can easily derive it from the list object.

Motion Type List

[
    {
        "id": 1,
        "name": "Vertical Pull"
    },

    ...

    {
        "id": 6,
        "name": "Horizontal Push"
    }
]

Skill Level List

[
    {
        "id": 1,
        "name": "Beginner"
    },

    ...

    {
        "id": 4,
        "name": "Elite"
    }
]

Grip Direction Type as example of *Type List

[
    {
        "id": 1,
        "name": "Neutral Grip"
    },
    {
        "id": 2,
        "name": "Pronated Grip"
    },
    {
        "id": 3,
        "name": "Supinated Grip"
    }
]

Grip Direction Option as example of *Option List

[
    {
        "id": 1,
        "exercise": {
            "id": 1,
            "name": "Pull Up"
        },
        "grip_direction_type": {
            "id": 1,
            "name": "Neutral Grip"
        }
    },

    ...

    {
        "id": 14,
        "exercise": {
            "id": 11,
            "name": "Planche"
        },
        "grip_direction_type": {
            "id": 3,
            "name": "Supinated Grip"
        }
    }
]

Default Setting List

[
    {
        "id": 1,
        "angle_type": null,
        "apparatus_type": {
            "id": 1,
            "name": "Bar"
        },
        "arm_motion_type": {
            "id": 1,
            "name": "Bent Arm"
        },
        "grip_direction_type": {
            "id": 2,
            "name": "Pronated Grip"
        },
        "grip_width_type": {
            "id": 2,
            "name": "Standard Width"
        },
        "leverage_progression_type": null,
        "muscle_contraction_type": null,
        "plyometric_type": {
            "id": 1,
            "name": "Strict"
        },
        "unilateral_type": null,
        "weight_progression_type": {
            "id": 2,
            "name": "Body Weight"
        }
    },

    ...

    {
        "id": 11,
        "angle_type": null,
        "apparatus_type": {
            "id": 4,
            "name": "Parallettes"
        },
        "arm_motion_type": {
            "id": 2,
            "name": "Straight Arm"
        },
        "grip_direction_type": {
            "id": 1,
            "name": "Neutral Grip"
        },
        "grip_width_type": {
            "id": 2,
            "name": "Standard Width"
        },
        "leverage_progression_type": {
            "id": 5,
            "name": "Full"
        },
        "muscle_contraction_type": {
            "id": 3,
            "name": "Isometric"
        },
        "plyometric_type": null,
        "unilateral_type": null,
        "weight_progression_type": {
            "id": 2,
            "name": "Body Weight"
        }
    }
]

Exercise List

[
    {
        "id": 1,
        "name": "Pull Up",
        "motion_type": {
            "id": 1,
            "name": "Vertical Pull"
        },
        "skill_level": {
            "id": 2,
            "name": "Intermediate"
        },
        "exercise_name": "PULL_UP",
        "default_setting": 1
    },

    ...

    {
        "id": 11,
        "name": "Planche",
        "motion_type": {
            "id": 6,
            "name": "Horizontal Push"
        },
        "skill_level": {
            "id": 4,
            "name": "Elite"
        },
        "exercise_name": "PLANCHE",
        "default_setting": 11
    }
]
Previous Post Next Post