Skip to content

Instantly share code, notes, and snippets.

@hharzer
Forked from D2theR/README.md
Created February 22, 2022 12:55
Show Gist options
  • Save hharzer/2fc69dd5fb00145fc01863085b721bd2 to your computer and use it in GitHub Desktop.
Save hharzer/2fc69dd5fb00145fc01863085b721bd2 to your computer and use it in GitHub Desktop.
Auto-generates Serializers & ModelViewSets in a Django API using just models

Why?

I got sick of writing the same Serializer & ModelViewSet classes over and over so I found and wrote some code to do it for me, and somehow it works! Please note that there are a lot of caveats to running an API like this and while this may work, I know there's A LOT of room for improvement, feel free to fork and help!

USAGE

Import the router module to your main sites urls.py file as the injection point like so... Make sure to remove any other imports from other viewsets that you don't need that may conflict!

from .app_api_generator import router

urlpatterns = [
    path('admin/', admin.site.urls),
    path('your-api-url/v1/', include(router.urls)), # <-- HERE
    ...
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

This will then generate a nice base API directory that looks something like this... Using the base class viewsets.ModelViewSet within the app_api_generator.py gives you access to GET, PUT, PATCH, DELETE, OPTIONS, etc.

{
    "menu/bookmark": "http://localhost:8888/pdi-api/v1/menu/bookmark/",
    "dashboard/dashboardpreferences": "http://localhost:8888/pdi-api/v1/dashboard/dashboardpreferences/",
    "admin/logentry": "http://localhost:8888/pdi-api/v1/admin/logentry/",
    "auth/permission": "http://localhost:8888/pdi-api/v1/auth/permission/",
    "auth/group": "http://localhost:8888/pdi-api/v1/auth/group/",
    "auth/user": "http://localhost:8888/pdi-api/v1/auth/user/",
    "contenttypes/contenttype": "http://localhost:8888/pdi-api/v1/contenttypes/contenttype/",
    "sessions/session": "http://localhost:8888/pdi-api/v1/sessions/session/",
    "contacts/companyprofile": "http://localhost:8888/pdi-api/v1/contacts/companyprofile/",
    "contacts/location": "http://localhost:8888/pdi-api/v1/contacts/location/",
    "contacts/contacts": "http://localhost:8888/pdi-api/v1/contacts/contacts/",
    "your_app/model_name": ""http://localhost:8888/pdi-api/v1/your_app/model_name/",
}

RELATIONSHIPS

I ran into an issue when using the depth attribute on the ModelSerializers Meta class. It makes the database un-godly slow when you have lots of relationships becuase it doesn't properly preform the joins it needs using select_related and prefetch_related on the querysets and it didn't seem fair leave coders in the dark when it came to solving this problem.

This is where model Managers come into play... This forces joins of the defined columns in the database and keeps to the mantra of thick models, thin views that so many Django users sometimes miss. Using the above manager class, import it and use it like below.

from mysite.global_model_manager import DefaultSelectOrPrefetchManager

class Child(models.Model):
  objects = DefaultSelectOrPrefetchManager(select_related=('parents',),prefetch_related=('family_members'))
  
  other fields, functions etc... 

Thanks & Contributions

This would NOT have been possible without:

from rest_framework import serializers, viewsets, routers
from django import apps
import sys
module = sys.modules[__name__]
# "module/sys.modules" is a list of all the system files that are loading into memory at run time.
# There are for loops below that bolt the auto-generated ViewSets and Serializers to
# Django at runtime using the setattr() method.
"""
This function generates serializers for every model returned in apps.apps.get_models()
Adjusting the `depth` variable on Meta class can drastically speed up the API.
It's recommended to use a customer Manager on each of your models to override
`select_related` and `prefetch_related` and define which fields need joined there.
"""
def make_api_serializers(api_models, base_serializer_class=serializers.ModelSerializer):
api_serializers = []
for ModelClass in api_models:
#Create the serializer class
class_name = f'{ModelClass.__name__}Serializer'
class ModelSerializer(base_serializer_class):
class Meta:
model = ModelClass
fields = '__all__'
depth = 2
ModelSerializer.__name__ = class_name
api_serializers.append({f"{ModelClass._meta.app_label}.{ModelClass.__name__}" :ModelSerializer})
return api_serializers
api_serializers = make_api_serializers(apps.apps.get_models())
for ser in api_serializers:
name = tuple(ser.keys())
setattr(module, name[0].lower(), ser[name[0]])
"""
This function generates ModelViewSets for every model returned in apps.apps.get_models()
and zips in all the api_serializers models generated in previous for-loop.
"""
def make_api_viewsets(api_models, api_serializers):
api_viewsets = []
for ModelClass, SerializerClass in zip(api_models, api_serializers):
table_name = ModelClass._meta.db_table
app_name = ModelClass._meta.app_label
viewset_name = f'{ModelClass.__name__}ViewSet'
viewset_bases = (viewsets.ModelViewSet,)
viewset_attrs = {
'db_table': table_name,
'queryset': ModelClass.objects.all(),
'serializer_class': SerializerClass[f'{app_name}.{ModelClass.__name__}'],
'app_name': app_name
}
ModelViewSet = type(
viewset_name,
viewset_bases,
viewset_attrs,
)
api_viewsets.append({f"{app_name}.{ModelClass.__name__}" :ModelViewSet})
return api_viewsets
api_viewsets = make_api_viewsets(apps.apps.get_models(), api_serializers)
for vs in api_viewsets:
name = tuple(vs.keys())
setattr(module, name[0].lower(), vs[name[0]])
# Creates a list of tuples that is used to then register generated ModelViewSets in DRF.
# The router is imported into the main urls.py file and uses include('router.urls') within
# the urlpatters list that Django loads at runtime. See the README for more info.
rest_api_urls = []
for viewset in api_viewsets:
app_name = list(viewset.keys())[0].lower().split('.')
k = list(viewset.keys())[0]
rest_api_urls.append((fr'{app_name[0]}/{app_name[1]}', viewset[k], f'{app_name[0]}/{app_name[1]}'))
router = routers.DefaultRouter()
for route in rest_api_urls:
router.register(route[0], route[1], basename=route[2])
from django.db import models
class DefaultSelectOrPrefetchManager(models.Manager):
"""
This is for forcing foreign key fields on each model and helps if there are lots
of relationships and you have the `depth` variable set on your Serializer classes.
See the README for more info on usage.
See the StackOverflow question: https://stackoverflow.com/questions/59358079/is-it-possible-to-automatically-create-viewsets-and-serializers-for-each-model
"""
def __init__(self, *args, **kwargs):
self._select_related = kwargs.pop('select_related', None)
self._prefetch_related = kwargs.pop('prefetch_related', None)
super(DefaultSelectOrPrefetchManager, self).__init__(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
qs = super(DefaultSelectOrPrefetchManager, self).get_queryset(*args, **kwargs)
if self._select_related:
qs = qs.select_related(*self._select_related)
if self._prefetch_related:
qs = qs.prefetch_related(*self._prefetch_related)
return qs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment