I am using Django with Restframework, and I want to update a certain object. I have the following model:
class PromotionalCode(models.Model):
promotional_code = models.CharField(max_length=10, default=random_with_letters(), unique=True)
myuser = models.ForeignKey('MyUser', related_name='promotional_code_user', blank=True, null=True)
promotion_type = models.PositiveIntegerField(default=1)
time_transaction = models.DateTimeField(default=datetime.now())
used = models.BooleanField(default=False)
The following viewset:
class UpdateOnlyDetailViewSet(mixins.UpdateModelMixin,
viewsets.GenericViewSet):
pass
The following view:
class PromotionalCodeViewSet(UpdateOnlyDetailViewSet):
queryset = PromotionalCode.objects.all()
serializer_class = PromotionalCodeSerializer
permission_classes = (IsOwnerOrReadOnly,)
And it has the following url:
router = DefaultRouter()
router.register(r'promotionalcode', views.PromotionalCodeViewSet)
I can only update the Promotionalcode object when I access to
/promotionalcode/code_id
and I want to update a certain code when I access to
/promotionalcode/
without specifying the id. What I have to do for do it?
I have found 3 solutions to my problem. I have tested 2 of them:
First solution: I have created another model called PromotionalCodesHistory(models.Model) with the same attributes as PromotionalCode and create a serializer with the method create, creating a PromotionalCodesHistory instance and call update method to update PromotionalCode. Then, when I access to the url /promotional/ I can update Promotional code.
Second solution: Modify DefaultRouter() to access to update on /promotional/ url, by the following code:
from rest_framework.routers import DefaultRouter
class CustomUpdateRouter(DefaultRouter):
routes = [
# List route.
Route(
url=r'^{prefix}{trailing_slash}$',
mapping={
'get': 'list',
'post': 'create',
'put': 'update',
'patch': 'partial_update',
},
name='{basename}-list',
initkwargs={'suffix': 'List'}
),
# Dynamically generated list routes.
# Generated using #list_route decorator
# on methods of the viewset.
DynamicListRoute(
url=r'^{prefix}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
# Detail route.
Route(
url=r'^{prefix}/{lookup}{trailing_slash}$',
mapping={
'get': 'retrieve',
'delete': 'destroy'
},
name='{basename}-detail',
initkwargs={'suffix': 'Instance'}
),
# Dynamically generated detail routes.
# Generated using #detail_route decorator on methods of the viewset.
DynamicDetailRoute(
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
]
Then, on view class, reimplement the get_object method to specify your own filters to get the object, you can access to get_serializer_context method to get context data, the filter is specified using a dictionary of unicode, as the following:
{u'key': unicode(variable1), u'key2': unicode(variable2)}
Third solution: Last solution which I have not tested, is to change the primary key to promotional_code field, to access promotional code, using /promotionalcode/promotional_code
Related
I would like to specify a custom lookup field on the action (different from the viewset default "pk"), i.e.
#action(
methods=["GET"],
detail=True,
url_name="something",
url_path="something",
lookup_field="uuid", # this does not work unfortunately
)
def get_something(self, request, uuid=None):
pass
But the router does not generate the correct urls:
router = DefaultRouter()
router.register(r"test", TestViewSet)
router.urls
yields url:
'^test/(?P<pk>[^/.]+)/something/$'
instead of
'^test/(?P<uuid>[^/.]+)/something/$'
I do not want to change the lookup field for the whole viewset though and have been unsuccessful in finding a way to do this for the action itself after debugging through the router url generation. I did notice that model viewsets have this method:
get_extra_action_url_map(self)
but am unsure how to get it to be called to generate custom urls or if it is even relevant. Any help would be great thanks!
According to their docs you could use a regex lookup field. Their example uses a CBV instead of a request based view.
class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
lookup_field = 'uuid'
lookup_value_regex = '[0-9a-f]{32}'
This could work:
#action(
methods=["GET"],
detail=True,
url_name="something",
url_path="something",
lookup_field = 'uuid'
lookup_value_regex = '[0-9a-f]{32}'
)
def get_something(self, request, uuid=None):
pass
I think it will create much confusion for your API consumers if you have 2 different resource identification on the same resource.
You can name that action query_by_uuid or just allow them to use list_view to filter by uuid if you only want to represent the object tho. (so consumers can use /test/?uuid= to retrieve data)
But if you really want to do it, you can simply override get_object method to filter for your custom action tho:
def get_object(self):
if self.action == 'do_something':
return get_object_or_404(self.get_queryset(), uuid=self.kwargs['pk'])
return super().get_object()
Here is a bit hacky solution for generate uuid in router with detail=False.
#action(detail=False, url_path=r'(?P<uuid>[^/.]+)/do_something')
def do_something(self, request, uuid=None):
pass
What i currently have setted up with django rest is :
MODEL NAME : Animes
/api/ <- root
/api/animes/ <- lists all animes available in my db
/api/animes/id/ <- returns the anime instance that has id=id
MODEL NAME : Episodes
/api/ <- root
/api/episodes/ <- lists all episodes of all animes available in my db
/api/episodes/id/ <- returns the episode instance that has id=id
So basically im trying to achieve is :
if i request api/episodes/{anime_name}/
i get that specific anime's Episodes listed .
how can i do that ?
EpisodesSerializer
class EpisodesSerializer(serializers.ModelSerializer):
class Meta:
model = Episodes
fields = '__all__'
Router
router = routers.DefaultRouter()
router.register('animes', AnimesViewSet, 'animes')
router.register('episodes', EpisodesViewSet, 'episodes')
urlpatterns = router.urls
EpisodesViewSet
class EpisodesViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.AllowAny]
MainModel = Episodes
queryset = Episodes.objects.all()
serializer_class = EpisodesSerializer
EDIT
As the OP mentioned in the comments, the newer versions of DRF use #action instead as #detail_route and #list_route are deprecated.
To use a different field for lookup, you can implement the logic to get the object yourself but you have to make sure that the field you're using for lookup is unique, else you can have many objects returned.
So assuming the anime name is unique and you want to use it for lookup, you can do this:
#action(detail=True, methods=['get'])
def episodes(self, *args, **kwargs):
anime = Anime.objects.get(name=self.kwarg[self.lookup_field])
episodes = Episode.objects.filter(anime=anime)
...
You can also check how get_object() is implemented to make it more robust.
I made a generic view mixin for myself that allows lookup with multiple unique fields aprt from the main pk lookup field:
class AlternateLookupFieldsMixin(object):
"""
Looks up objects for detail endpoints using alternate
lookup fields assigned in `alternate_lookup_fields` apart
from the default lookup_field. Only unique fields should be used
else Http404 is raised if multiple objects are found
"""
alternate_lookup_fields = []
def get_object(self):
try:
return super().get_object()
except Http404:
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
queryset = self.filter_queryset(self.get_queryset())
obj = None
for lookup_field in self.alternate_lookup_fields:
filter_kwargs = {lookup_field: self.kwargs[lookup_url_kwarg]}
try:
obj = get_object_or_404(queryset, **filter_kwargs)
except Http404:
pass
if obj:
self.check_object_permissions(self.request, obj)
return obj
raise Http404
All you have to do is add it to your view's base classes and add the fields for lookup(name in your case) in the alternate_lookup_fields attribute. Of course, only use unique fields.
As for filtering, you can check how simple filtering is done here.
However, I will recommend using a more generic filter backend like django-filter
ORIGINAL ANSWER
First of all, the url will look more initutive like this:
api/anime/<anime_id>/episodes/
This is because you should usually start from a more generic resource to more specific ones.
To achieve this, in your AnimeViewSet(not EpisodesViewSet), you can have a detail route for the episodes like this:
from rest_framework.decorators import detail_route
#detail_route(methods=['get'])
def episodes(self, *args, **kwargs):
anime = self.get_object()
episodes = Episode.objects.filter(anime=anime)
page = self.paginate_queryset(anime)
if page is not None:
serialier = EpisodesSerializer(page, context=self.get_serializer_context(), many=True)
return self.get_paginated_response(serializer.data)
serializer = EpisodesSerializer(episodes, context=self.get_serializer_context()) many=True)
return Response(serializer.data)
You could also just use a filter on the EpisodesViewSet to fetch episodes belonging to a particular anime this way:
api/episodes?anime=<anime_id>
Hard facts:
I am using Django 2.0 with python 3.6, if it makes any difference.
What I am trying to achieve is a link to a list of objects that belong to a summary.
I have a ManyToOne relationship in my models.py.
class Summary(models.model):
type=models.CharField
class Object(models.Model):
summary= models.ForeignKey(Summary, on_delete=models.CASCADE)
in urls.py
object_list= views.ObjectListViewSet.as_view({
'get': 'list'
})
urlpatterns = format_suffix_patterns([
url(r'^summary/(?P<pk>[^/.]+)/objects/$', object_list, name='summary-objects')
])
and now the idea was to give a user the possibility to click the an url in the browsable API and getting all objects.
So, I tried to write a MethodField in serializers.py. I am not able to get any reasonable URL here, the only solution would be to hardcode it.
class SummarySerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="app:summary-detail")
objects= serializers.SerializerMethodField('get_obj_url')
def get_obj_url(self, obj):
pass
class Meta:
model = Summary
Is this possible?
Is it necessary to write a MethodField?
If yes, how do I get the url I need?
Actually, reverse, as suggested in the comments, does the trick.
The solution is:
def get_obj_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(reverse('api-root')) + 'summary/{id}/objects'.format(
id=obj.id)
EDIT:Typo
I have two related models (Events + Locations) with a serialzer shown below:
class Locations
title = models.CharField(max_length=250)
address = model.CharField(max_length=250)
class Events
title = models.CharField(max_length=250)
locations = models.ForeignKey(Locations, related_name='events'
class EventsSerializer(serializers.ModelSerializer):
class Meta:
model = Events
depth = 1
I set the depth to 1 in the serializer so I can get the information from the Locations model instead of a single id. When doing this however, I cant post to events with the location info. I can only perform a post with the title attribute. If I remove the depth option in the serializer, I can perform the post with both the title and location id.
I tried to create a second serializer (EventsSerialzerB) without the depth field with the intention of using the first one as a read-only response, however when I created a second serializer, viewset, and added it to the router, it would automatically override the original viewset.
Is it possible for me to create a serializer that outputs the related model fields, and allows you to post directly to the single model?
// EDIT - Here's what I'm trying to post
$scope.doClick = function (event) {
var test_data = {
title: 'Event Test',
content: 'Some test content here',
location: 2,
date: '2014-12-16T11:00:00Z'
}
// $resource.save() doesn't work?
$http.post('/api/events/', test_data).
success(function(data, status, headers, config) {
console.log('sucess', status);
}).
error(function(data, status, headers, config) {
console.log('error', status);
});
}
So when the serializers are flat, I can post all of these fields. The location field is the id of a location from the related Locations table. When they are nested, I can't include the location field in the test data.
By setting the depth option on the serializer, you are telling it to make any relation nested instead of flat. For the most part, nested serializers should be considered read-only by default, as they are buggy in Django REST Framework 2.4 and there are better ways to handle them in 3.0.
It sounds like you want a nested representation when reading, but a flat representation when writing. While this isn't recommended, as it means GET requests don't match PUT requests, it is possible to do this in a way to makes everyone happy.
In Django REST Framework 3.0, you can try the following to get what you want:
class LocationsSerializer(serializers.ModelSerializer):
class Meta:
model = Locations
fields = ('title', 'address', )
class EventsSerializer(serializers.ModelSerializer):
locations = LocationsSerializer(read_only=True)
class Meta:
model = Events
fields = ('locations', )
class EventViewSet(viewsets.ModelViewSet):
queryet = Event.objects.all()
serializer_class = EventsSerializer
def perform_create(self, serializer):
serializer.save(locations=self.request.data['locations'])
def perform_update(self, serializer):
serializer.save(locations=self.request.data['locations'])
A new LocationsSerializer was created, which will handle the read-only nested representation of the Locations object. By overriding perform_create and perform_update, we can pass in the location id that was passed in with the request body, so the location can still be updated.
Also, you should avoid having model names being plurals. It's confusing when Events.locations is a single location, even though Locations.events is a list of events for the location. Event.location and Location.events reads a bit more clearly, the Django admin will display them reasonably, and your fellow developers will be able to easily understand how the relations are set up.
I was wondering how I would be able to create an object in a database based the URL a user is going to.
Say for example they would go to /schedule/addbid/1/ and this would create an object in the table containing the owner of the bid, the schedule they bidded on and if the bid has been completed. This here is what I have for my model so far for bids.
class Bids(models.Model):
id = models.AutoField("ID", primary_key=True, editable=False,)
owner = models.ForeignKey(User)
biddedschedule = models.ForeignKey(Schedule)
complete = models.BooleanField("Completed?", default=False)
The biddedschedule would be based on the number in the URL as the 1 in this case would be the first schedule in the schedule table
Any ideas on how to do this?
You should get the id parameter using urls.py:
#urls.py
from appname.views import some_view
urlpatterns = patterns('',
url(r'^schedule/addbid/(?P<id>\d+)$', some_view),
...
)
Take a look at the documentation about capturing parameters in the urlconf.
And then, in views.py you should construct a Bids Object using the id passed in the URL, the currently logged in user (request.user), and the biddschedule from your DB. For example:
#views.py
def some_view(request, id):
if request.user.is_authenticated():
# get the biddschedule from your DB
# ...
bids = models.Bids(id=id, owner=request.user, biddedschedule=biddedschedule)
bids.save()
return HttpResponse("OK")
return Http404()
Catch the number via the urlconf. Get the current user via request.user. Create a model instance by calling its constructor, and save it to the database via its save() method.
`#view.py under a post or get method`
new_bird, created = Bids.objects.get_or_create(
owner = user_obj,
biddedschedule = biddedschedule_obj,
complete = bool
)
new_bird.save()