Package: django-awesome-tools
Django Awesome ToolsThis package provides useful and powerful functions and classes to be used inDjangoprojects, specially when working withDjango Rest Framework. Below are some further explation about how to use this package and what each module inside it does.Table of ContentsInstallationhelpersget_object_or_errorget_list_or_errorset_and_destroybulk_get_or_createmixinsSerializerByMethodMixinSerializerByActionMixinSerializerByDetailActionsMixinSerializerBySafeActionsMixinFilterQuerysetMixinAttachUserOnCreateMixinAttachUserOnUpdateMixinAttachUserToReqDataMixinmanagersCustomUserManagercachebuild_cache_mixinsSetCacheOnListMixinEraseCacheOnCreateMixinEraseCacheOnUpdateMixinEraseCacheOnDestroyMixinEraseCacheOnDetailMixinEraseCacheOnDetailMixinManageCacheMixinFullManageCacheMixinByAuthToken VariationsByUser Variationsdocsbuild_list_docsbuild_create_docsbuild_retrieve_docsbuild_update_docsbuild_destroy_docsbuild_list_create_docsbuild_retrieve_update_destroy_docsbuild_docsbuild_docs_by_groupbuild_full_docsaction_patternsadminCustomUserAdminInstallationFirst, run:pipinstalldjango-awesome-toolsThat's it!helpersThis module provides three useful functions. Two of them are a more powerful and versatille version ofget_object_or_404andget_list_or_404, and the other is a handy shortcut.get_object_or_errorAlmost the same asdjango.shortcuts.get_object_or_404, but can raise any
custom error class you want, allowing you to return more precise error messages.
Another advantage of using this helper function, is that it prevents your application
from crashing. For instance, in case you want to get an object by it's primary key, and
it is of typeuuid, but another data type is provided in the url, it will not crash,
unlike the standardget_object_or_404. It expects the following arguments:klass-> The model that will be used for the queryexception-> An error class inheriting fromrest_framework.exceptions.APIException.
If noexceptionis provided, then the standarddjango.http.Http404class is used.**kwargs-> Keyword arguments representing all fields that should be used for the
search, as many as you please.For instance, in case you want to get aRoomof aCinema:# exceptions.pyfromrest_framework.exceptionsimportAPIException,statusclassCinemaNotFoundError(APIException):status_code=status.HTTP_404_NOT_FOUNDdefault_detail="Cinema not found"classRoomNotFoundError(APIException):status_code=status.HTTP_404_NOT_FOUNDdefault_detail="Room not found in this cinema"# request endpoint"/cinemas/<cinema_id>/rooms/<room_id>/"# views.pyfromawesome_tools.helpersimportget_object_or_errorcinema=get_object_or_error(Cinema,CinemaNotFoundError,pk=self.kwargs['cinema_id'])room=get_object_or_error(Room,RoomNotFoundError,pk=self.kwargs['room_id'],cinema=cinema)Note that in case a room id is valid, but the cinema id is not, an appropriated message will be
returned. In case you would useget_object_or_404, you would get just a"Not found.". Having
more than one lookup field,get_object_or_errormakes much clearer what is the problem.I highly encorage you to have a quick look at the source code, it's quite a simple concept.get_list_or_errorAlmost the same asdjango.shortcuts.get_list_or_404, but can raise any
custom error class you want, allowing you to return more precise error messages.
Another advantage of using this helper function, is that it prevents your application
from crashing. For instance, in case you want to get a list, filtering it by some foreign
key field, which is of typeuuid, but another data type is provided in the url, it will
not crash, unlike the standardget_list_or_404. Also, this function gives you the possiblity
of not raising an exception when no values are found, so you could just return an empty list.
It expects the following arguments:klass-> The model that will be used for the queryexception-> An error class inheriting fromrest_framework.exceptions.APIException.
If noexceptionis provided, then the standarddjango.http.Http404class is used.accept_empty-> A boolean argument, which defaults toFalse. When provided, determines
if an empty result is acceptable or if it should raiseexception.**kwargs-> Keyword arguments representing all fields that should be used for the
search, as many as you please.For instance, in case you want to list allMovieSessions of aRoomin aCinema:# exceptions.pyfromrest_framework.exceptionsimportAPIException,statusclassNoMovieSessionsError(APIException):status_code=status.HTTP_404_NOT_FOUNDdefault_detail="This room has no scheduled movie sessions"# request endpoint"/cinemas/<cinema_id>/rooms/<room_id>/movie-sessions/"# views.pyfromawesome_tools.helpersimportget_object_or_error,get_list_or_errorcinema=get_object_or_error(Cinema,CinemaNotFoundError,pk=self.kwargs['cinema_id'])room=get_object_or_error(Room,RoomNotFoundError,pk=self.kwargs['room_id'],cinema=cinema)movie_sessions=get_list_or_error(MovieSession,NoMovieSessionsError,room=room)I highly encorage you to have a quick look at the source code, it's quite a simple concept.set_and_destroyThis function basically sets a new list of values in a foreign key field and erases any
previous values that were related toklass. For it to work,you must setnull=Truein your model, otherwise, the values will not be subsitituted, they will only be added.
It accepts the following parameters:klass-> The model on the side1of a1:Nrelationship, the owner of the relation,
in which the new values will be setattr-> A string version of the attribute corresponding to therelated_namevalue
in the foreign key fieldvalue-> A list (or any other iterable), containing new created instances ofrelated_klassrelated_klass-> The model on the sideNof a1:Nrelationship, the one having the foreign
key field**kwargs-> Keyword arguments used in a filter to determine which objects should be destroyed.
It could be really anything, but usually you will want it to be something likeklass=None, so
that all objects that are no part of the relationship anymore can be descarded.For instance, aMoviemay have manyVideos related to it, like teasers and trailers. In case
you want to update aMovie, reseting itsVideos:# models.pyclassMovie(models.Model):...classVideo(models.Model):id=models.UUIDField(primary_key=True,editable=False,default=uuid4)title=models.CharField(max_length=127)url=models.URLField()movie=models.ForeignKey(Movie,on_delete=models.CASCADE,related_name="videos",null=True)# serializers.pyfromawesome_tools.helpersimportset_and_destroyclassMovieSerializer(serializers.ModelSerializer):...defupdate(self,instance:Movie,validated_data:dict):...videos_data=validated_data.pop("videos",None)ifvideos_data:videos=[Video.get_or_create(**video,movie=instance)[0]forvideoinvideos_data]set_and_destroy(klass=instance,attr="videos",value=videos,related_klass=Video,movie=None,)In the example above, we are first getting or creating video instances, in order to reuse the ones
passed in the body of the request that may already be in our db. Each video can only be related to
one movie, since it doesn't make sense that two movies have the same trailer or teaser. So when
assigning this new list of videos to a movie, theset_and_destroyfunction safely deletes all
videos having theirmovieforeign key equal toNone.I highly encorage you to have a quick look at the source code, it's quite a simple concept.bulk_get_or_createDespite the name of this function, it does not translate into a single database hit,
unfortunatelly. But it is still better than a loop executingModel.objects.get_or_createin every iteration.That's because this functioncombines filters and the bulk_create method.
Django querysets are lazy, but in this function they are evaluated on every iteration.
However, in the endonly oneINSERTquery is performed.Important!Django'sModel.objects.bulk_createmethod returns a list of newly created instanceswithout idswhen working withSQLite. Please, make sure to usePostgreSQLto avoid problems.It expects the following parameters:klass-> The model whose values will be retrieved or createdvalues-> A list of dictionaries having key value pairs demanded byklassonly_create-> A boolean value. Defaults toFalse. In case you don't care about getting
existing values, and just wants to create them, then you can set this arguments toTrue. It
will result in just one database hit.kwargs-> Key value pairs with extra fields you want to use for filtering/creating instances
ofklass. It can be useful for foreign key fieldsUsage example:# serializers.pyfromawesome_tools.helpersimportbulk_get_or_create,set_and_destroyclassMovieSerializer(serializers.ModelSerializer):# ...defcreate(self,validated_data:dict)->Movie:# ...videos_data=validated_data.pop("videos")# ...bulk_get_or_create(Video,videos_data,movie=movie)# ...defupdate(self,instance:Movie,validated_data:dict)->Movie:# ...videos=validated_data.pop("videos",None)# ...ifvideos:set_and_destroy(klass=instance,attr="videos",value=bulk_get_or_create(Video,videos,movie=instance),related_klass=Video,movie=None,)# ...Note that in theupdatemethod, we are combiningset_and_destroywithbulk_get_or_create.
That's totally a thing.I highly encourage you to have a look at the source code, so that you can better understand what's
happening under the hood. It's not complicated.mixinsThis module provides useful mixins to be used in Django Rest Frameworkgeneric viewsandviewsets.SerializerByMethodMixinThis mixin overrides theget_serializer_classmethod of generic views. It's
purpose is to dinamically define which serializer to use, depending on the request
method. For this to be possible, a new class property should be set, it is:method_serializers-> It should be a dictionary having it's keys with the names
of http methods and values as the serializer classes corresponding to each method.
If the request method does not match any of the dict keys, it will return the value
ofself.serializer_class.Below is an example:# views.pyfromawesome_tools.mixinsimportSerializerByMethodMixinclassMyBeautifulGenericView(SerializerByMethodMixin,ListCreateAPIView):queryset=MyWonderfulModel.objects.all()serializer_class=MyDefaultSerializermethod_serializers={"GET":MySerialzerToUseInGetRequests,}SerializerByActionMixinThis mixin overrides theget_serializer_classmethod of viewsets. It's
purpose is to dinamically define which serializer to use, depending on the viewset
action. For this to be possible, a new class property should be set, it is:action_serializers-> It should be a dictionary having it's keys with the names
of viewset actions and values as the serializer classes corresponding to each action.
If the viewset action does not match any of the dict keys, it will return the value
ofself.serializer_class.Below is an example:# views.pyfromawesome_tools.mixinsimportSerializerByActionMixinclassMyBeautifulViewSet(SerializerByActionMixin,ModelViewSet):queryset=MyWonderfulModel.objects.all()serializer_class=MyDefaultSerializeraction_serializers={"create":MySerializerToUseInCreateActions,"update":MySerialzerToUseInUpdateActions,"partial_update":MySerialzerToUseInPartialUpdateActions,}SerializerByDetailActionsMixinThis mixin overrides theget_serializer_classmethod of viewsets. It's
purpose is to dinamically define which serializer to use, depending on the viewset
action. If it is a detail action, that is, one ofretrieve,update,partial_updateanddestroy, thenself.detail_serializer_classwill be returned. Else, the defaultself.serializer_classis used. For this to be possible, a new class property should
be set, it is:detail_serializer_class-> It's value should be a serializer class. This property defines
which serializer to use in detail actions.Below is an example:# views.pyfromawesome_tools.mixinsimportSerializerByDetailActionsMixinclassMyBeautifulViewSet(SerializerByDetailActionsMixin,ModelViewSet):queryset=MyWonderfulModel.objects.all()serializer_class=MyDefaultSerializerdetail_serializer_class=MyDetailSerializerSerializerBySafeActionsMixinThis mixin overrides theget_serializer_classmethod of viewsets. It's
purpose is to dinamically define which serializer to use, depending on the viewset
action. If it is asafe action, thenself.safe_serializer_classwill be returned.
Else, the defaultself.serializer_classis returned. A safe action is an action
listed in thesafe_actionsclass property. For this to be possible, a new class
property should be set, it is:safe_serializer_class-> Its value should be a serializer class. This property defines
which serializer to use in safe actions.You can totally customize what is a "safe action". For that, you could change the value
ofself.safe_actions.safe_actions-> It should be alist[str], which each item representing a viewset action,
considered safe for that viewset. The default value is["list", "retrieve"]Below is an example:# views.pyfromawesome_tools.mixinsimportSerializerBySafeActionsMixinclassMyBeautifulViewSet(SerializerBySafeActionsMixin,ModelViewSet):queryset=MyWonderfulModel.objects.all()serializer_class=MyDefaultSerializersafe_serializer_class=MySafeSerializerFilterQuerysetMixinThis mixin overrides theget_querysetmethod of class based views. It's main goal is
to make it easier and simpler to filter and/or narrow down results. You may use it to
attach results to the logged in user, to filter the queryset by route params (orkwargs)
and by query params.These are the class properties that this mixin accepts:filter_user_key-> Astrrepresenting which keyword argument should be used for filtering by
user. The default isNone, meaning that the queryset will not be filtered by the logged in user, that
is,self.request.user. If in your queryset there is aFKpointing to your project's auth user model, then this property should
have the same name as thisFKfield.filter_kwargs-> Adict[str, str], where thekeyrepresents the name of thefieldto be searched,
and thevalueis theurl param.filter_query_params-> Adict[str, str], where thekeyis the name of thefieldto be searched,
and thevaluerepresents thequery paramreceived in the request.filter_exception_klass-> Should be anexceptioninheriting fromrest_framework.exceptions.APIException. The
default value isdjango.http.Http404. In case no value is returned or another kind of error occurs, this
exception will be raised.filter_accept_empty-> Abool, which defaults toTrue. IfFalse, then theexception_klasswill be raised
in case the results are empty. Otherwise, an empty value will be returned normaly.Below is an example of how this might be useful:# request endpoint"/categories/<category_id>/transactions/"# views.pyfromawesome_tools.mixinsimportFilterQuerysetMixinclassTransactionView(FilterQuerysetMixin,ListCreateAPIView):serializer_class=TransactionSerializerpermission_classes=[IsAuthenticated]filter_user_key="user"filter_kwargs={"category":"category_id"}filter_query_params={"month_id":"month_id","month__number":"month_number","month__name__icontains":"month_name","month__year":"year","description__icontains":"description","value":"value","value__gte":"value_gte","value__lte":"value_lte","is_income":"is_income","is_recurrent":"is_recurrent","installments":"installments",}filter_order_by=["-created_at"]In the example above, we are defining a view for monetary transactions. We don't want
users to see other user's transactions, so we attach all transactions to the logged in
user. By using thefilter_user_keyclass property, we tell the mixin that when filtering the
queryset, it should useuser=self.request.user.Also, all transactions have categories. And we want them always to be listed by category.
So in the url, we receive the<category_id>param. So that's why we declarefilter_kwargsin that way.As for thefilter_query_paramsproperty, please note how interesting it is. In thekeysof
the dictionary, we pass in the keys that will be used for filtering the queryset, just as if
we were filtering the queryset manually. And thevaluescorrespond to the query param that we
expect to receive in the request. None of these query params are mandatory.We are not declaringfilter_accept_empty, which means that we will not raisefilter_exception_klassin any case (because the default value isTrue). So that's why we don't need to definefilter_exception_klasstoo.Furthermore, we are ordering the queryset results by their creation date (in descending order) with thefilter_order_byproperty.You may have noticed that thequerysetclass property haven't been defined. That's not a
problem, because this mixin guesses what is the apropriated model by accessingself.serializer_class.Meta.model.
So as long as you define you model in that way, everything is OK.AttachUserOnCreateMixinThis mixin overrides theperform_createmethod of generic views, and simply passes to the serializersavemethod an additional keyword argument. This attaches the current user to thevalidated_dataargument on the serializer'screatemethod. You can pass the following class property:attach_user_key-> Astr, which defaults toNone. It represents which is the name of the field
that points to the user on your model. If ommited, it will try to get the value ofself.filter_user_key.So in case you are already using this module'sFilterQuerysetMixin, and is using this property, then there
is no need to repeat yourself here. But in case neitherself.attach_user_keyorself.filter_user_keyare
found, then"user"is used by default.Here is a quick example:fromawesome_tools.mixinsimportAttachUserOnUpdateMixinfromrest_frameworkimportgenericsfromrest_frameworkimportpermissionsfrom.serializersimportCinemaSerializerclassCinemaView(AttachUserOnUpdateMixin,generics.ListCreateAPIView):serializer_class=CinemaSerializerpermission_classes=[permissions.IsAuthenticated]attach_user_key="owner"This simple trick makes it possible to attach an user to aCinemainstance very easily. In this case, we are
definingattach_user_keyas"owner", because on theCinemamodel, the foreign key field that relates to
the user model, has this name.AttachUserOnUpdateMixinExactly the same asAttachUserOnCreateMixin, but overrides theperform_updatemethod.AttachUserToReqDataMixinA combination ofAttachUserOnCreateMixinandAttachUserOnUpdateMixin,
overriding bothperform_createandperform_updatemethods of generic views.managersThis module provides a custom user manager as a shortcut for whoever wants to customize
django's authentication system to use a different field instead of username for login.
It can be really anything, like email, phone, cpf, etc.CustomUserManagerA custom user manager that inherits fromdjango.contrib.auth.models.BaseUserManager.
Its purpouse in life is mainly to provide an easy and simple way to implement a login
and register system that expects another field instead ofusername.But what if you desired to customize your users in a way that other info is also required
for user creation? No problem, this class is highly customizable.Instead of having to override thecreateandcreate_superusermethods ofBaseUserManager,
you can inherit fromCustomUserManagerand then simply set some class properties at your will.
They work as follows:auth_field_name-> Defaults to"email". Defines what is the name of the field that
should be used for login (besides password, of course). Note that this fieldmustexist in your user model,having a unique constraint.user_is_staff-> Defaults toFalse. Defines the starting staff status of newly
created usersuser_start_active-> Defaults toTrue. Defines if a user account should start in
active state. In cases where users have to confirm their account in some way before getting
access, you may wish to set this property toFalsesuper_is_staff-> Defaults toTrue. Defines the starting staff status of newly
created superuserssuper_start_active-> Defaults toTrue. Defines if a superuser account should start in
active state. Usually you'll want this value to beTrue, but you're totally free to change
it, depending on your needs.required_fields-> Defaults to[]. It should be alist[str]. This property defines
which fields are required to be provided upon user creation, besidesself.auth_field_nameandpassword. The fieldsis_staff,is_superuserandis_activeshould also not be present in
this list. It is worth noting thatall fields defined here, must also be defined in your user model.
Otherwise, aValidationErroris raised.Below is an example of how you may customize the behaviour of this class:# managers.pyfromawesome_tools.managersimportCustomUserManagerclassMyOwnUserManager(CustomUserManager):user_start_active=Falserequired_fields=["first_name","last_name"]In order to implement a login with email feature, for instance, you have to make some minor
changes to your user model. Below are some settings that may come in handy for you to define
in your model:# models.pyfrom.managersimportMyOwnUserManagerfromdjango.dbimportmodelsfromdjango.contrib.auth.modelsimportAbstractUserclassMyUser(AbstractUser):email=models.EmailField(unique=True)username=Noneobjects=MyOwnUserManager()USERNAME_FIELD=objects.auth_field_nameREQUIRED_FIELDS=objects.required_fieldsTheemailproperty is defined as unique, since it's gonna be used for login (as per theUSERNAME_FIELDproperty). Theobjectsproperty may be either the standardawesome_tools.managers.CustomUserManageror your own manager that inherits from it. In the example above, we are using our own user manager,
with some minor customizations.REQUIRED_FIELDSrefer to the fields you are prompted when creating a
superuser (it must not include the value defined forUSERNAME_FIELDor"password"). Defining it toobjects.required_fieldsprevents you from making mistakes and being redundant. Note that in the example
above we are droping theusernamecolumn, but that's not necessary if you still want to have a username
in your user model.cacheThis subpackage provides a set of useful mixins that may be used for cache management. It also provides a function
for building your own custom mixins.build_cache_mixinsThis function returns a tuple of mixins used for cache management. It receives the following arguments:cache_ttl-> The ttl(time to live) for the cache is by default whatever you
set in theCACHE_TTLvariable at your project'ssettings.py, but you can totally override this here.
The value ofcache_ttlmust be anint. It represents the time that the cache will persist,in seconds.
In caseCACHE_TTLis not present insettings.py, then it defaults to 10 minutes.vary_on_headers-> This argument is atuplethat refers to which headers should be used when generating
the cache key and cache group.vary_on_user-> A boolean value that determines if the cache key and cache group should be isolated for each user.It is important to note that, besides the value ofvary_on_headersandvary_on_user, cache keys are generated
based on the request path and query params, and cache groups are generated based on the request path.Here is a simple example of how you could use it:fromawesome_tools.cacheimportbuild_cache_mixins(SetCacheOnListByMyCoolHeaderMixin,EraseCacheOnCreateByMyCoolHeaderMixin,EraseCacheOnUpdateByMyCoolHeaderMixin,EraseCacheOnDestroyByMyCoolHeaderMixin,EraseCacheOnDetailByMyCoolHeaderMixin,ManageCacheByMyCoolHeaderMixin,FullManageCacheByMyCoolHeaderMixin,)=build_cache_mixins(vary_on_headers=("my-cool-header",))In the example above, if the same request is made again on a cached view, but with a different value on"my-cool-header"header, then the view will not use the cached value, rather, it will cache the results also based on this header.Actually, this illustrates exactly how the cache management mixins on this package are generated.SetCacheOnListMixinCaches the results of thelistmethod of generic views and viewsets. After setting the cache,
if the same request is fired again, then it will return the cached value, instead of doing the
whole thing again.Here is a simple example of how you could use this mixin.fromawesome_tools.cacheimportSetCacheOnListMixinfromrest_framework.genericsimportListAPIViewclassMyAwesomeListView(SetCacheOnListMixin,ListAPIView):# my awesome view stuff...EraseCacheOnCreateMixinUpon calling thecreatemethod of generic views and viewsets, erase the cache. But what cache?
The cache related to the group this mixin belongs to. It is by default determined by the url path,
but may vary based on user or any headers on the request, if these arguments are passed to thebuild_cache_mixinsfunction.Here is an example:fromawesome_tools.cacheimportSetCacheOnListMixin,EraseCacheOnCreateMixinfromrest_framework.genericsimportListAPIView,CreateAPIViewclassMyAwesomeListView(SetCacheOnListMixin,ListAPIView):# my awesome view stuff...classMyAwesomeCreateView(EraseCacheOnCreateMixin,CreateAPIView):# my awesome view stuff...In the example above, when the create view is called, then it will erase any cache keys that were set
by the list view, if there are any, butonly within the scope of the cache group.EraseCacheOnUpdateMixinExactly the same asEraseCacheOnCreateMixin, but withupdateandpartial_updatemethods of generic views and viewsets.EraseCacheOnDestroyMixinExactly the same asEraseCacheOnCreateMixinandEraseCacheOnUpdateMixin,
but with thedestroymethod of generic views and viewsets.EraseCacheOnDetailMixinThis is just a combination of bothEraseCacheOnUpdateMixinandEraseCacheOnDestroyMixin.Here is an example:fromawesome_tools.cacheimportSetCacheOnListMixin,EraseCacheOnDetailMixinfromrest_framework.genericsimportListAPIView,RetrieveUpdateDestroyAPIViewclassMyAwesomeListView(SetCacheOnListMixin,ListAPIView):# my awesome view stuff...classMyAwesomeDetailView(EraseCacheOnDetailMixin,RetrieveUpdateDestroyAPIView):# my awesome view stuff...In the example above, when the detail view is called, then it will erase any cache keys that were set
by the list view, if there are any, butonly within the scope of the cache group.ManageCacheMixinUpon calling thelistmethod of the view, set the cache, but when calling thecreatemethod, then erase the cache.
This mixin is essentially just a combination of bothSetCacheOnListMixinandEraseCacheOnCreateMixin.Here is an example:fromawesome_tools.cacheimportManageCacheMixinfromrest_framework.genericsimportListCreateAPIViewclassMyAwesomeView(ManageCacheMixin,ListCreateAPIView):# my awesome view stuff...In the example above, when thelistmethod of the view is called, then it will set the cache, but when thecreatemethod is called, then it will erase any cache keys that were set on thelistmethod, if there are any, butonly within the scope of the cache group. The cache group is by default determined by the url path, but may vary
based on user or any headers on the request, if these arguments are passed to thebuild_cache_mixinsfunction.FullManageCacheMixinUpon calling thelistmethod of the view, set the cache, but when callingcreate,update,partial_updateanddestroymethods, then erase the cache. This mixin is essentially just a combination of bothManageCacheMixinandEraseCacheOnDetailMixin.Here is an example:fromawesome_tools.cacheimportFullManageCacheMixinfromrest_framework.viewsetsimportModelViewSetclassMyAwesomeViewSet(FullManageCacheMixin,ModelViewSet):# my awesome viewset stuff...In the example above, when thelistmethod of the viewset is called, then it will set the cache, but when any ofcreate,update,partial_updateanddestroymethods is called, then it will erase any cache keys that were
set on thelistmethod, if there are any, butonly within the scope of the cache group. The cache group is
by default determined by the url path, but may vary based on user or any headers on the request, if these arguments
are passed to thebuild_cache_mixinsfunction.ByAuthToken VariationsSetCacheOnListByAuthTokenMixinExactly the same asSetCacheOnListMixin, but grouping the cache by the"Authorization"header.
This means that the cache will be isolated by the value of the auth token, even within the scope of the same user.The following mixins are all variations of the mixins described until now, just like the one above. They all group the
cache by"Authorization"header. They are as follows:EraseCacheOnCreateByAuthTokenMixinEraseCacheOnUpdateByAuthTokenMixinEraseCacheOnDestroyByAuthTokenMixinEraseCacheOnDetailByAuthTokenMixinManageCacheByAuthTokenMixinFullManageCacheByAuthTokenMixinByUser VariationsSetCacheOnListByUserMixinExactly the same asSetCacheOnListMixin, but grouping the cache byrequest.user. This means that
the cache will be isolated by user, even if the authorization token may expire.The following mixins are all variations of the mixins described until now, just like the one above. They all group the
cache byrequest.user. They are as follows:EraseCacheOnCreateByUserMixinEraseCacheOnUpdateByUserMixinEraseCacheOnDestroyByUserMixinEraseCacheOnDetailByUserMixinManageCacheByUserMixinFullManageCacheByUserMixindocsThis module provides a set of useful and simple to use functions for building documentation mixins.
But wait, what is a "documentation mixin"? Thedrf-spectacularpackage has a wonderful feature of
automatically generating a swagger documentation, based on your project views, with minimum configurations
onsettings.pyandurls.py. But maybe you want to give more details on how each view and endpoint works,
such as a brief summary, a detailed description, or maybe specify which query parameters it accepts. For that,drf-spectacularlibrary provides some ways for us to apply such customizations.Among them, is thedrf_spectacular.utilsmodule. It has a decorator calledextend_schema, which can be used
to decorate view methods that correspond to http methods, likeget,postand so forth. It accepts many arguments.The functions defined in this module use this decorator to build mixins that can be easily aplied to any class based view.
It is recommended to put these mixins on the far left of your views inheritance list, as show in the examples below.build_list_docsCreate a mixin class that applies thedrf_spectacular.utils.extend_schemadecorator to thegetmethod of views. It receives the following arguments:summary-> Astr, which is a brief description of what that endpoint does.description-> Also astr, which can be used to provide further details on the behavior of the
endpoint.parameters-> Alistfilled with values of typeOpenApiParameter.*args, **kwargs-> Any other parameters that@extend_schemaexpects.Here's an example:fromawesome_tools.docsimportbuild_list_docsfromdrf_spectacular.utilsimportOpenApiParametersummary="A wonderful and brief description of the list action on an endpoint"description=("A more detailed description of the list action and some tips on how to use it. ""Here I could add anything more that I may wish to appear on swagger.")parameters=[OpenApiParameter("param1",int,description="A brief description of my int query param"),OpenApiParameter("param2",bool,description="A brief description of my bool query param"),]MyListViewDocsMixin=build_list_docs(summary,description,parameters)# views.pyclassMyListView(MyListViewDocsMixin,ListAPIView):# view stuff...build_create_docsSame as above, but with theposthttp method, for create viewsbuild_retrieve_docsSame as above, but with thegethttp method, for retrieve viewsbuild_update_docsSame as above, but with theputandpatchhttp methods, for update viewsbuild_destroy_docsSame as above, but with thedestroyhttp method, for destroy viewsbuild_list_create_docsJust a combination ofbuild_list_docsandbuild_create_docs. Returns a single mixin, that combine
bothListDocsMixinandCreateDocsMixin, which are the return values of these two functions, respectively.
It may come in handy forListCreateviews. It receives the following arguments:summaries-> Adict, containing the summaries of each endpoint, where the keys are any of["list", "create"],
and the values are the summaries.descriptions-> Adict, containing the desciptions of each endpoint, where the keys are any of["list", "create"],
and the values are the desciptions.parameters-> Alistfilled with values of typeOpenApiParameter, to be used on thelistendpoint.*args, **kwargs-> Any other parameters that@extend_schemaexpects (note that this will be applied to both methods).Here's an example:fromawesome_tools.docsimportbuild_list_create_docsfromdrf_spectacular.utilsimportOpenApiParametersummaries={"list":"A wonderful and brief description of the list action on an endpoint","create":"A wonderful and brief description of the create action on an endpoint",}descriptions={"create":("A more detailed description of the create action and some tips on how to use it. ""Here I could add anything more that I may wish to appear on swagger.")}parameters=[OpenApiParameter("param1",int,description="A brief description of my int query param"),OpenApiParameter("param2",bool,description="A brief description of my bool query param"),]MyViewDocsMixin=build_list_create_docs(summaries,descriptions,parameters)# views.pyclassMyView(MyViewDocsMixin,ListCreateAPIView):# view stuff...build_retrieve_update_destroy_docsJust a combination ofbuild_retrieve_docsandbuild_update_docsandbuild_destroy_docs. Returns a single
mixin, that combineRetrieveDocsMixin,UpdateDocsMixinandDestroyDocsMixin, which are the return values
of these three functions, respectively. It may come in handy forRetrieveUpdateDestroyviews. It receives the
following arguments:summaries-> Adict, containing the summaries of each endpoint, where the keys are any of["retrieve", "update", "destroy"],
and the values are the summaries.descriptions-> Adict, containing the desciptions of each endpoint, where the keys are any of["retrieve", "update", "destroy"],
and the values are the desciptions.*args, **kwargs-> Any other parameters that@extend_schemaexpects (note that this will be applied to all methods).Here's an example:fromawesome_tools.docsimportbuild_retrieve_update_destroy_docssummaries={"retrieve":"A wonderful and brief description of the retrieve action on an endpoint","update":"A wonderful and brief description of the update action on an endpoint","destroy":"A wonderful and brief description of the delete action on an endpoint",}descriptions={"destroy":("A long description of the destroy action and a warning about its consequences""Here I could add anything more that I may wish to appear on swagger.")}MyDetailViewDocsMixin=build_retrieve_update_destroy_docs(summaries,descriptions)# views.pyclassMyDetailView(MyDetailViewDocsMixin,RetrieveUpdateDestroyAPIView):# view stuff...build_docsReturn a tuple of five mixins, each of which are built usingbuild_list_docs,build_create_docsbuild_retrieve_docs,build_update_docsandbuild_destroy_docs, respectively. It receives the following arguments:summaries-> Adict, containing the summaries of each endpoint, where the keys are any of["list", "create", "retrieve", "update", "destroy"], and the values are the summaries.descriptions-> Adict, containing the desciptions of each endpoint, where the keys are any of["list", "create", "retrieve", "update", "destroy"], and the values are the desciptions.parameters-> Alistfilled with values of typeOpenApiParameter, to be used on thelistendpoint.*args, **kwargs-> Any other parameters that@extend_schemaexpects (note that this will be applied to all methods).Here's an example:fromawesome_tools.docsimportbuild_docsfromdrf_spectacular.utilsimportOpenApiParametersummaries={"list":"A wonderful and brief description of the list action on an endpoint","create":"A wonderful and brief description of the create action on an endpoint","retrieve":"A wonderful and brief description of the retrieve action on an endpoint","update":"A wonderful and brief description of the update action on an endpoint","destroy":"A wonderful and brief description of the delete action on an endpoint",}descriptions={"destroy":("A long description of the destroy action and a warning about its consequences""Here I could add anything more that I may wish to appear on swagger.")}parameters=[OpenApiParameter("param1",int,description="A brief description of my int query param"),OpenApiParameter("param2",bool,description="A brief description of my bool query param"),](MyListViewDocsMixin,MyCreateViewDocsMixin,MyRetrieveViewDocsMixinMyUpdateViewDocsMixin,MyDestroyViewDocsMixin,)=build_docs(summaries,descriptions,parameters)# views.pyclassMyListView(MyListViewDocsMixin,ListAPIView):# view stuff...classMyCreateView(MyCreateViewDocsMixin,CreateAPIView):# view stuff...# and so forth...build_docs_by_groupReturn a tuple with two mixins, which of which are built usingbuild_list_create_docsandbuild_retrieve_update_destroy_docs, respectively. This may come in handy in those cases where
you have two classes, one is aListCreateViewand the other is aRetrieveUpdateDestroyView.
It receives the following arguments:summaries-> Adict, containing the summaries of each endpoint, where the keys are any of["list", "create", "retrieve", "update", "destroy"], and the values are the summaries.descriptions-> Adict, containing the desciptions of each endpoint, where the keys are any of["list", "create", "retrieve", "update", "destroy"], and the values are the desciptions.parameters-> Alistfilled with values of typeOpenApiParameter, to be used on thelistendpoint.*args, **kwargs-> Any other parameters that@extend_schemaexpects (note that this will be applied to all methods).Here's an example:fromawesome_tools.docsimportbuild_docs_by_groupfromdrf_spectacular.utilsimportOpenApiParametersummaries={"list":"A wonderful and brief description of the list action on an endpoint","create":"A wonderful and brief description of the create action on an endpoint","retrieve":"A wonderful and brief description of the retrieve action on an endpoint","update":"A wonderful and brief description of the update action on an endpoint","destroy":"A wonderful and brief description of the delete action on an endpoint",}descriptions={"destroy":("A long description of the destroy action and a warning about its consequences""Here I could add anything more that I may wish to appear on swagger.")}parameters=[OpenApiParameter("param1",int,description="A brief description of my int query param"),OpenApiParameter("param2",bool,description="A brief description of my bool query param"),](MyViewDocsMixin,MyDetailViewDocsMixin,)=build_docs_by_group(summaries,descriptions,parameters)# views.pyclassMyView(MyViewDocsMixin,ListCreateAPIView):# view stuff...build_full_docsReturn a single mixin, that is a combination of bothListCreateDocsandRetrieveUpdateDestroyDocs,
whichbuild_docs_by_groupreturns. It receives the following arguments:summaries-> Adict, containing the summaries of each endpoint, where the keys are any of["list", "create", "retrieve", "update", "destroy"], and the values are the summaries.descriptions-> Adict, containing the desciptions of each endpoint, where the keys are any of["list", "create", "retrieve", "update", "destroy"], and the values are the desciptions.parameters-> Alistfilled with values of typeOpenApiParameter, to be used on thelistendpoint.*args, **kwargs-> Any other parameters that@extend_schemaexpects (note that this will be applied to all methods).Here's an example:fromawesome_tools.docsimportbuild_full_docsfromdrf_spectacular.utilsimportOpenApiParametersummaries={"list":"A wonderful and brief description of the list action on an endpoint","create":"A wonderful and brief description of the create action on an endpoint","retrieve":"A wonderful and brief description of the retrieve action on an endpoint","update":"A wonderful and brief description of the update action on an endpoint","destroy":"A wonderful and brief description of the delete action on an endpoint",}descriptions={"destroy":("A long description of the destroy action and a warning about its consequences""Here I could add anything more that I may wish to appear on swagger.")}parameters=[OpenApiParameter("param1",int,description="A brief description of my int query param"),OpenApiParameter("param2",bool,description="A brief description of my bool query param"),]MyViewFullDocsMixin=build_full_docs(summaries,descriptions,parameters)action_patternsViewsets have the advantage of abstracting away the work of defining routes explicitly,
but routers have some limits. They can only go to a certain depth in producing urls.For instance, let's imagine a simple application, where you have Bands and Albums.
In case you wish to list all Albums of a Band, you could make a request to an enpoint
like/bands/<band_id>/albums/. That's totally possible with routers. But what if you
want a detail route for an Album of a Band? A route like/bands/<band_id>/albums/<album_id>/would make sense, right? But routers aren't able to go to such an extent. And you could
totally imagine bigger urls in real, bigger applications.So defining our routes manually gives us a lot more control. Everything comes with a tradeoff
though. When manually defining routes for generic views, you can easily assign each view class
to their routes, using theas_viewmethod. But viewsets are different. One viewset class can
be assigned to more than one route. So for that to work, you've gotta do something likethis.In order to simplify things, and abstract away some boiler plate code, this module provides the
standard viewset actions mapped to their corresponding http method. Of course, you may have additional
actions, customized according to your own needs. In this case, you can config them on your own. But
the standard ones are all set here.Usage example:# urls.pyfromdjango.urlsimportpathfromawesome_tools.action_patternsimportSTANDARD_DETAIL_PATTERN,STANDARD_PATTERNfrom.importviewscinema_view=views.CinemaViewSet.as_view(STANDARD_PATTERN)cinema_detail_view=views.CinemaViewSet.as_view(STANDARD_DETAIL_PATTERN)urlpatterns=[path("",cinema_view),path("<cinema_id>/",cinema_detail_view),]But routers are still so cool and so simple to use. So a very good alternative isdrf-nested-routers.
It really makes it easier to deal with all of this. Thedrf-nested-routerslibrary is designed to
solve exactly this problem, and even more.adminThis module provides aCustomUserAdminclass. It inherits fromdjango.contrib.auth.admin.UserAdmin.
Have you ever created a custom user model, added it to admin and then realized that your users passwords
were being created unhashed? Then you searched the internet and found out that django provides aUserAdminclass that does the job. But what if you customized your authentication system, and you're using another
field instead ofusername? In this case, it throws an error, saying that there is nousernamefield.In order to make things easier, this module provides a class that abstracts away all the boring
configurations you would need to do.CustomUserAdminThis class inherits fromdjango.contrib.auth.admin.UserAdmin. It's purpose in life is to abstract
away some boring configurations you may need, when you're using a custom user model. The advantage is
to have the same features that Django standardUserAdminclass provides, but in a custom user model,
having a field other thanusernameused for authentication.This class automaticaly figures out what is your user model, as long as it is pointed to byAUTH_USER_MODELsetting insettings.py. Also, it takes the care of first checking for the fields you set in your user
model before referencing them. But thepassword field is mandatory.Below is an usage example:# admin.pyfromawesome_tools.adminimportCustomUserAdminfrom.modelsimportUseradmin.site.register(User,CustomUserAdmin)In case you want to customize some kind of behaviour, you totally can, either by overwriting the properties
entirely (by inheriting this class), or by using one of the class methods defined in this class. For instance,
if you added some columns that are not default of auth user model, but still want them to appear in the admin,
you could do something like this:# admin.pyfromawesome_tools.adminimportCustomUserAdminfrom.modelsimportUserfields=("cpf","phone")# add fields to the user creation formCustomUserAdmin.add_creation_fields(fields)# append fields to list_displayCustomUserAdmin.add_list_display(fields)# add fields to personal info screenCustomUserAdmin.add_personal_info(fields)admin.site.register(User,CustomUserAdmin)Not so bad.