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-11-21
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
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
data = {}
and add fields piecemealfrom 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")
]
return Response(serializer.data)
for data. It will have 3 methods: create/update/retrieveclass 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')
validate_{field_name}
methodsclass 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()
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()
)
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)
@api_view(['GET"])
above to annotate method@authentification_classes(BasicAuthentication)
access controlrequest
and format
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
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
uuids_on_device = serializers.ListField(write_only=True,
required=True,
child=serializers.UUIDField())
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)
Visit the docs and execute something (e.g. a POST
create
give back custom fieldsclass 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}
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