Skip to content

Instantly share code, notes, and snippets.

@D2theR
Last active February 28, 2024 17:34
Show Gist options
  • Star 36 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save D2theR/0b439164e94a9577d4b502496c7672cf to your computer and use it in GitHub Desktop.
Save D2theR/0b439164e94a9577d4b502496c7672cf 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
@sahilklanto
Copy link

This is some really interesting stuff. Thanks for sharing your work and research

@danihodovic
Copy link

Is it possible to exclude certain apps and models?

@D2theR
Copy link
Author

D2theR commented Feb 7, 2022

The apps.apps.get_models() gives you a list of all the models in the database so running and iteration over that to exclude certain models should work.

@TuringNPcomplete
Copy link

Interesting work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment