Dev Journal #10 - More than One Way to Skin the Cat
djangoI 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
}
]