Django gotchas

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the python category.

Last Updated: 2024-04-26

The fixtures have no good way of hashing user passwords, so it's easier to generate that data in the admin then dump it

python manage.py dumpdata | jq

Put fixtures in a fixtures folder.

To use directives (like static) in the templating language, you need to 1st load it

{% load static %}

To refer to the built-in auth user, use this

from django.contrib.auth import get_user_model

class Account(models.Model):
    user = models.ForeignKey(get_user_model())

Get built in auth with this

  path('auth/', include('django.contrib.auth.urls')),

The auth review overrides need to be top-level templates (not sub-apps)

You need to both makemigrations and migrate

When you create an app with django, you need to add it to installed apps.

Make sure your code comes after libraries in installed apps

REST Framework

You need to add mixins for routes to even work

# This failed the url /experiences?search_query="x" despite my router
# connecting to the entire view set:
# router.register(r'liked_experiences', LikedExperiencesViewSet, basename="liked_experiences")
class ExperiencesViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    serializer_class = ExperienceSerializer

    def get_queryset(self):
        """
        Optionally restricts the returned experience
        by filtering against a `search_query` parameter in the URL.
        """
        ...

The trick was to add a mixin "ListModelMixin"

Custom converters don't work with this style of URL

register_converter(converters.FloatConverter, 'float')
router.register(r'suggest_experiences/(?P<float:latitude>)/(?P<float:longitude>[0-9\.]+)',

Instead this works

router.register(r'suggest_experiences/(?P<latitude>[0-9\.]+)/(?P<longitude>[0-9\.]+)',
                SuggestExperiencesView,
                basename="suggest_experiences")

In fact, I listed the route twice to get separate auto-gen docs

router.register(r'suggest_experiences', SuggestExperiencesView, basename="suggest_experiences")
router.register(r'suggest_experiences/(?P<latitude>[0-9\.]+)/(?P<longitude>[0-9\.]+)',
                SuggestExperiencesView,
                basename="suggest_experiences")

How to send a simple array response

from rest_framework.response import Response

class CategoriesAndCitiesViewSet(viewsets.GenericViewSet):
    """
    This class is responsible for grabbing data populating drop-down boxes
    options, especially in the publisher flow
    """
    def list(self, request):
        categories = [
            experience["category"]
            for experience in Experience.objects.order_by("category").values('category').distinct()
        ]
        sub_categories = [
            experience["sub_category"] for experience in Experience.objects.order_by(
                "sub_category").values('sub_category').distinct()
        ]
        return Response(
            {
                # Note, however, that whereas simple arrays of categories strings work, full ORM City objects
                # do not with this setup:
                # "cities": City.objects.all(),
                "categories": categories,
                "sub_categories": sub_categories,
            }, )

great guide (seems unrelated at first, but IS related): https://aiorest-ws.readthedocs.io/en/stable/

How to use city_id vs. default city

class VenueSerializer(serializers.ModelSerializer):
    # The trick is a combo of PrimaryKeyRelatedField, source, and queryset
    city_id = serializers.PrimaryKeyRelatedField(source="city",
                                                 queryset=City.objects.active().all())
    # Optionally you can put city here for sending data
    city = serializers.SlugRelatedField(slug_field='name', read_only=True)

You can make the current user the owner of anything created (and avoid hacks) with this simple line

class VenueSerializer(serializers.ModelSerializer):
    publisher = serializers.HiddenField(default=serializers.CurrentUserDefault())


Partial operation: default values are not applied here. And you don't need to submit all the data again as with update - it goes easy on some of the validations

Fear a file into binary to work with json

import base64
  dummy_photo = generate_dummy_photo_file()
  encoded = base64.b64encode(dummy_photo.read())
  response = self.client.put(f'/api/v1/venues/{venue.id}', {"photo_file": encoded})


If you need a place to stand in a few, try either `def get_object` (for single resourcs) or `def get_queryset`.
You can access `self.request.user` `self.request.query_params` and `self.kwargs.get('pk')` here for power.

Another option - less general but more flexible - is overriding the `def update` etc. functions

Be careful of where you should call serializers vs. not call them

- e..g the following broke my code

```python
class VenuesViewSet(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.CreateModelMixin,
    serializer_class = VenueSerializer() # should not be called in this context

Unlike in Rails, there is no automatic 404 when a record is not found. You need to handle this


try:
  return Venue.objects.filter(publisher=current_user).get(pk=pk)
except Venue.DoesNotExist:
  raise Http404

View

How to do a basic view with URL and JSON response (without DRF)

from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse

from .models import Poll

def polls_list(request):
    MAX_OBJECTS = 20
    polls = Poll.objects.all()[:MAX_OBJECTS]
    # NB: Notice how we get the associated attribute `created_by__username`
    data = {"results": list(polls.values("question", "created_by__username", "pub_date"))}
    return JsonResponse(data)


# NB: Notice how `pk` is passed by route
def polls_detail(request, pk):
    # NB: Notice usage of `get_object_or_404`
    poll = get_object_or_404(Poll, pk=pk)
    data = {"results": {
        "question": poll.question,
        "created_by": poll.created_by.username,
        "pub_date": poll.pub_date
    }}
    return JsonResponse(data)


# urls
from django.urls import path
from .views import polls_list, polls_detail

urlpatterns = [
    path("polls/", polls_list, name="polls_list"),
    path("polls/<int:pk>/", polls_detail, name="polls_detail")
]

Serializer

class TagSerializer(serializers.ModelSerializer):
    # calls get_created below
    created = serializers.SerializerMethodField()

    class Meta:
        model = Tag
        # method field included here
        fields = ('label', 'created')

    def get_created(self, obj):
        return round(obj.created.timestamp())
   # model has task_type
  job_type = serializers.CharField(source='task_type')


  class Meta:
      model = Task
      fields = ('job_type',)
owner_email = serializers.CharField(source='owner.email')
class TransactionSerializer(serializers.ModelSerializer):
    bid = serializers.IntegerField()

    def validate_bid(self, bid: int) -> int:
        if bid > self.context['request'].user.available_balance:
            raise serializers.ValidationError(
                _('Bid is greater than your balance')
            )
        return bid

Keep in mind that field level validation is invoked by serializer.tointernalvalue(), which takes place before calling serializer.validate()

CurrentUserDefault

auto set a field to logged in user!

class EmailSerializer(serializers.ModelSerializer):
# because of using HiddenField, any incoming data is not taken into account, so it’s impossible to set another user as
# an owner.
    owner = serializers.HiddenField(
        default=serializers.CurrentUserDefault()
    )

Viewset

Allow you to concisely do a lot - e.g. list/create/get tags here:

class TagViewSet(mixins.ListModelMixin,
                 mixins.CreateModelMixin,
                 mixins.RetrieveModelMixin,
                 GenericViewSet):

    queryset = Tag.objects.all()
    serializer_class = TagSerializer
    permission_classes = (permissions.IsAuthenticate)

Functional views

Urls

If your API is only giving you an object in place on an array, and all your serlializers are being difficult, what

can you do?

categories = [...] # manual array
cities = CitySerializer(data=City.objects.active().all())

return Response(
  {
      "cities": cities,
      "categories": categories,
      "sub_categories": sub_categories,
  }, )

Unfortunately, the API, when empty, gave inconsistent data types for each key. I wanted arrays everywhere, and no objects!

{"cities":{},"categories":[""],"sub_categories":[""]}

The fix involved called model_to_dict on each item in a map (it did not work over a collection) ```python

from django.forms.models import modeltodict

cities = [modeltodict(city) for city in City.objects.active().all()] return Response( { "cities": cities, "categories": categories, "subcategories": subcategories, }, )


## How to save nested records in serializer

```python
class VenueSerializer(serializers.ModelSerializer):
    city = serializers.SlugRelatedField(slug_field='name', read_only=True)
    city_id = serializers.PrimaryKeyRelatedField(source="city",
                                                 queryset=City.objects.active().all())
    hours = HoursSerializer(
        many=True,
        source="current_hours",
        required=False,
    )
    photo_file = Base64ImageField(max_length=None, use_url=True, required=False)
    publisher = serializers.HiddenField(default=serializers.CurrentUserDefault())

    class Meta:
        model = Venue
        exclude = ['created_at', 'updated_at']

    def update(self, instance, validated_data):
        instance.__dict__.update(**validated_data)
        instance.save()

        # Save the nested records
        if 'current_hours' in validated_data:
            for hours_data in validated_data.pop('current_hours'):
                Hours.objects.create(venue=instance, **hours_data)
            return instance

        return instance

How to add validations to serializer end point (that do not exist in model layer)

class ExperienceSerializer(serializer.ModelSerializer):
    description = serializers.CharField(required=True)

    # but no need to mention the `name` field if it is `blank=False` in the model layer

How to deal with list of fields in serializer

    uuids_on_device = serializers.ListField(write_only=True,
                                            required=True,
                                            child=serializers.UUIDField())

How to access default serializer

class EmailVehicleViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
    queryset = models.Vehicle.objects
    serializer_class = serializers.EmailVehicleSerializer

    def create(self, request, *args, **kwargs):
        # Here is the key part: self.serializer_class to access attribute
        # from instance
        serializer = self.serializer_class(data=request.data)
        if serializer.is_valid():
            serializer.save()
            # Note that Reponse and status must be required
            return Response(serializer.data, status=status.HTTP_201_CREATED)

How to get complex POST curl request example

Visit the docs and execute something (e.g. a POST

How to make serializer create give back custom fields

class EmailVehicleSerializer(serializers.ModelSerializer):
    uuids_on_device = serializers.ListField(write_only=True,
                                            required=True,
                                            child=serializers.UUIDField())
    vehicles = VehicleSerializer(many=True, read_only=True)

    def create(self, validated_data):
        vehicles = Vehicle.objects.filter(uuid_on_device__in=validated_data["uuids_on_device"])
        # KEY BIT: you need to return an object with keys/values. Just returning the array of vehicles `vehicles` would
        # fail.
        return {"vehicles": vehicles}

How to ensure serializer hides field (e.g. password field) that was sent on POST but should not be returned in the response

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('username', 'email', 'password')
        # NB: Use the extra_kwargs bit
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = User(
            email=validated_data['email'],
            username=validated_data['username']
        )
        user.set_password(validated_data['password'])
        user.save()
        Token.objects.create(user=user)
        return user