Django Rest Framework: Get Data by Field - django

i want to learn django.
My first learning project is a django + rest framework api.
i want to get a destination by its airport code. not by pk / id
currently when i call /api/destination/1 i get the destination with id 1
i want something like /api/destination/PMI or /api/destination/mallorca and as response i only want to get the destination with code PMI or with name mallorca.
is this possible?
my files:
modely.py
class Destination(models.Model):
name = models.CharField(max_length=50)
code = models.CharField(max_length=3)
country = models.CharField(max_length=50)
image = models.FileField()
serializers.py
class DestinationSerializer(serializers.ModelSerializer):
class Meta:
model = Destination
fields = ("id", "name", "code", "country", "image")
urls.py
router = DefaultRouter()
router.register(r'destination', DestinationViewSet)
views.py
class DestinationViewSet(viewsets.ModelViewSet):
serializer_class = DestinationSerializer
queryset = Destination.objects.all()

I would recommend picking one or the other to have as the identifier. For this example, I'm going to use the airport code.
In urls.py, you'll want to switch from a router to a urlpattern - remember to register this in your project.urls file!
from django.urls import path
urlpatterns = [path('destination/<code>/', DestinationViewSet.as_view())]
In your view, you'll want to switch to just a normal view and call the get() method.
from destinations.api.serializers import DestinationSerializer
from destinations.models import Destination
from rest_framework import views
from rest_framework.response import Response
class DestinationView(views.APIView):
def get(self, request, code):
destination = Destination.objects.filter(code=code)
if destination:
serializer = DestinationSerializer(destination, many=True)
return Response(status=200, data=serializer.data)
return Response(status=400, data={'Destination Not Found'})
Everything else should work the way it is!

Use an action decorator to create a custom get method
#action(detail=False, methods=['GET'], url_path='destination/(?P<pmi>\w{0,500})')
def custom_ge(self, request, pmi):
#Function implementation in here

Related

Combine two django models with shared ids into a single viewset

I have two django models in two independent apps, who use the same user ids from an external authentication service:
In app1/models.py:
class App1User(models.Model):
user_id = models.UUIDField(unique=True)
app1_field = models.BooleanField()
In app2/models.py:
class App2User(models.Model):
user_id = models.UUIDField(unique=True)
app2_field = models.BooleanField()
I would like to have a combined viewset that can make it seem like these two are a single model with a list response as follows:
[
{
'user_id': ...,
'app1_field': ...,
'app2_field': ...
},
...
]
If I create or update with this viewset, it should save the data to each of the two models.
To create a combined viewset for App1User and App2User models, you can
follow these steps:
Create a serializer for the combined model. In your main app, create a
serializers.py file and define the following serializer:
from rest_framework import serializers
from app1.models import App1User
from app2.models import App2User
class App1UserSerializer(serializers.ModelSerializer):
class Meta:
model = App1User
fields = ('user_id', 'app1_field')
class App2UserSerializer(serializers.ModelSerializer):
class Meta:
model = App2User
fields = ('user_id', 'app2_field')
class CombinedUserSerializer(serializers.Serializer):
user_id = serializers.UUIDField()
app1_field = serializers.BooleanField()
app2_field = serializers.BooleanField()
def create(self, validated_data):
app1_data = {'user_id': validated_data['user_id'], 'app1_field': validated_data['app1_field']}
app2_data = {'user_id': validated_data['user_id'], 'app2_field': validated_data['app2_field']}
app1_serializer = App1UserSerializer(data=app1_data)
app2_serializer = App2UserSerializer(data=app2_data)
if app1_serializer.is_valid() and app2_serializer.is_valid():
app1_serializer.save()
app2_serializer.save()
return validated_data
def update(self, instance, validated_data):
app1_instance = App1User.objects.get(user_id=validated_data['user_id'])
app2_instance = App2User.objects.get(user_id=validated_data['user_id'])
app1_serializer = App1UserSerializer(app1_instance, data={'app1_field': validated_data['app1_field']})
app2_serializer = App2UserSerializer(app2_instance, data={'app2_field': validated_data['app2_field']})
if app1_serializer.is_valid() and app2_serializer.is_valid():
app1_serializer.save()
app2_serializer.save()
return validated_data
This serializer defines the structure of the combined model and uses App1UserSerializer and App2UserSerializer to handle the fields of each model.
Create a viewset for the combined model. In your main app, create a viewsets.py file and define the following viewset:
from rest_framework import viewsets
from app1.models import App1User
from app2.models import App2User
from .serializers import CombinedUserSerializer
class CombinedUserViewSet(viewsets.ModelViewSet):
serializer_class = CombinedUserSerializer
def get_queryset(self):
app1_users = App1User.objects.all()
app2_users = App2User.objects.all()
combined_users = []
for app1_user in app1_users:
app2_user = app2_users.filter(user_id=app1_user.user_id).first()
if app2_user:
combined_user = {
'user_id': app1_user.user_id,
'app1_field': app1_user.app1_field,
'app2_field': app2_user.app2_field
}
combined_users.append(combined_user)
return combined_users
This viewset uses CombinedUserSerializer to handle the requests and retrieves the data from both App1User and App2User models. The get_queryset() method retrieves all the App1User and App2User instances, matches them based on their user_id, and creates a list of combined users.
Register the viewset in your main app's urls.py file:
# main_app/urls.py
from django.urls import include, path
from rest_framework import routers
from .views import CombinedUserViewSet
router = routers.DefaultRouter()
router.register(r'combined-users', CombinedUserViewSet)
urlpatterns = [
path('', include(router.urls)),
]
This code sets up a default router and registers the CombinedUserViewSet with the endpoint /combined-users/, which can be used to access the combined model data.
Above solution is the idea how it can be done, you can modify according to your needs. If this idea helps, plz mark this as accept solution.

DRF: Simple Switch for a choice field in model via router

I want to build an APIView that can turn on power in a store so to say ...
Can I do it using a router?
model:
class Store(models.Model):
C = [(0,0), (1,1), (2,2), (3,3)]
name = models.IntegerField("name", max_length=60)
power_state = models.PositiveIntegerField("current state", default=0, choices=C)
user = models.ForeignKey(User, on_delete=models.CASCADE)
view:
class OnOff(APIView):
def patch(self, request):
store = Store.objects.get(pk = request.user.id)
return Response("switched")
I am new to DRF and I do not know if I need a serializer here. The interface I see looks like this:
while I was hoping for a simple dropdown between 0 and ... 3 in this case. Also how would the router have to be registered? Right now I put a path in the urls.py:
path('test/', views.OnOff.as_view(), name = "on-off"),
which means it will not be listed under 127.0.0.1:8000/api/ which would be nice.
I tried using (but 404):
router = routers.DefaultRouter()
...
router.register(r'onoff', views.OnOff, basename = "onoff")
urlpatterns = [
path("", views.StoreView.as_view(), name = 'index'),
url('^api/', include(router.urls)),
... ]
The DRF router generates REST style urls and this API does not appear to be REST-ful in the standard sense. ie. list all the objects and detail single objects. docs.
To add the /api/ using the path method:
path('api/test/', views.OnOff.as_view(), name = "on-off"),
if you want to just quickly get you API working via the built in interface, add a post method.
class OnOff(APIView):
def post(self, request):
store = Store.objects.get(pk = request.user.id)
... sudo code to save the on-off value sent from the interface ...
store.power_state = request.data.get('on-off', 0)
store.save()
return Response("switched")
To use the DRF router, a Viewset is required.
The router.url propertyu is list of urls that can be included in the main urls.py
urls.py
from rest_framework.routers import DefaultRouter
import .views
app_name = 'on-off'
router = DefaultRouter()
router.register(r'onoff', views.OnOffViewset)
urlpatterns = router.urls
views.py
class OnOffViewset(viewsets.ViewSet):
def update(self, request, pk=None):
# in this case the pk is a user.id
store = Store.objects.get(pk = request.user.id)
... sudo code to save the on-off value sent from the interface ...
store.power_state = request.data.get('on-off', 0)
store.save()
return Response({'status': store.power_state})
Projects main urls.py
path('api/', include('onoff.urls', namespace='on-off')),

Got a `TypeError` when calling `Article.objects.create()`

I am working on Django React Project using the Django REST FRAMEWORK,I am trying to post some data tied to my model.
The list view and the detail view of the project works pretty fine,The only problem is when I try to make a POST request.
Whenever I Try post the data in the CreateAPIView I get an error :
Got a `TypeError` when calling `Article.objects.create()`. This may be because
you have a writable field on the serializer class that is not a valid argument to
`Article.objects.create()`. You may need to make the field read-only, or override
the ArticleSerializer.create() method to handle this correctly.
I have searched through various past problems but non of them seem to fix my problem.
Here is my serializers file:
from rest_framework import serializers
from articles.models import Article
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ('id','title','content','star_count','like_count','comment_count','avatar')
Here is my views file
from rest_framework.generics import ListAPIView,RetrieveAPIView,CreateAPIView,UpdateAPIView,DestroyAPIView
from .serializers import ArticleSerializer
from articles.models import Article
from rest_framework import viewsets
class ArticleViewSets(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
models file
content = models.TextField()
comment_count = models.IntegerField(default=0,null=True,blank=True)
like_count = models.IntegerField(default=0,null=True,blank=True)
star_count = models.IntegerField(default=0,null=True,blank=True)
avatar = models.ImageField(null=True,blank=True)
def __str__(self):
return self.title
def save(self):
if not self.slug:
self.slug = slugify(self.title)
super(Article,self).save()
Here is the error generated when I try to make a POST request based on the django rest framework createAPIVew
Got a `TypeError` when calling `Article.objects.create()`. This may be because you have a writable field on the serializer class that is not a valid argument to `Article.objects.create()`. You may need to make the field read-only, or override the ArticleSerializer.create() method to handle this correctly.

How to accept form data and return it along with the results of some processing in Django RESTFramework?

I am trying to understand Django RESTFramework. I am already familiar with Django. I want to create an endpoint that accepts some text data and processes it and returns it to the user along with the results of the processing (in text). I have completed a couple of tutorials on the topic but I still don't understand how it works. Here is an example from a working tutorial project. How can I edit it to achieve my goal? It all looks automagical.
# views.py
from rest_framework import generics
from .models import Snippet
from .serializers import SnippetSerializer
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
​
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
# Here I would like to accept form data and process it before returning it along with the
# results of the processing.
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
Okay, I think you are a newbie in Django rest and try to understand its flow so I can explain it with an example of a subscription plan.
First, create a model in models.py file
from django.db import models
class SubscriptionPlan(models.Model):
plan_name = models.CharField(max_length=255)
monthly_price = models.IntegerField()
yearly_price = models.IntegerField()
Then create views in a view.py file like
from rest_framework.views import APIView
class SubscriptionCreateAPIView(APIView):
serializer_class = SubscriptionSerializer
def post(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(
{'message': 'Subscription plan created successfully.',
'data': serializer.data},
status=status.HTTP_201_CREATED
)
and then define a serializer for validation and fields in which we can verify which fields will be included in the request and response object.
serializers.py
from rest_framework import serializers
from .models import SubscriptionPlan
class SubscriptionSerializer(serializers.ModelSerializer):
plan_name = serializers.CharField(max_length=255)
monthly_price = serializers.IntegerField(required=True)
yearly_price = serializers.IntegerField(required=True)
class Meta:
model = SubscriptionPlan
fields = (
'plan_name', 'monthly_price', 'yearly_price',
)
def create(self, validated_data):
return SubscriptionPlan.objects.create(**validated_data)
Now add urls in src/subsciption_module/urls.py
from django.urls import path
from .views import SubscriptionCreateAPIView
app_name = 'subscription_plan'
urlpatterns = [
path('subscription_plan/', SubscriptionCreateAPIView.as_view()),
]
At the end include module url in root urls.py file where your main urls will be located. It will be the same directory which contains settings.py and wsgi.py files.
src/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('src.subscription_plan.urls', namespace='subscription_plan')),
]
That's it. This is how flow works in django rest and you can process data and display data in this way. For more details you can refer django rest docs.
But this is not in any way different from what you do with plain Django. Your SnippetDetail view is just a class-based view, and like any class-based view if you want to do anything specific you override the relevant method. In your case, you probably want to override update() to do your custom logic when receiving a PUT request to update data.

How Do I Show Django Admin Change List View of foreign key children?

I'm working on an app with a Model hierarchy of Campaign > Category > Account. Ideally, I'd like users to be able to click on a link in the campaign admin list view and go to a URL like "/admin/myapp/campaign/2/accounts/" which will show a Django admin view with all the handy ChangeList amenities but which is filtered to show just the accounts in categories in the specified campaign (ie. Account.object.filter(category__campaign__id = 2)). (Note, categories themselves I'm happy to just be "filters" on this accounts list view).
I can't seem to find any reference to a way to mimic this item-click-goes-to-list-of-foriegn-key-children approach that is common in many other frameworks.
Is it possible? Is there a "better" approach in the django paradigm?
thanks for any help!
This was an interesting question so I whipped up a sample app to figure it out.
# models.py
from django.db import models
class Campaign(models.Model):
name = models.CharField(max_length=20)
def __unicode__(self):
return unicode(self.name)
class Category(models.Model):
campaign = models.ForeignKey(Campaign)
name = models.CharField(max_length=20)
def __unicode__(self):
return unicode(self.name)
class Account(models.Model):
category = models.ForeignKey(Category)
name = models.CharField(max_length=20)
def __unicode__(self):
return unicode(self.name)
# admin.py
from django.contrib import admin
from models import Campaign, Category, Account
class CampaignAdmin(admin.ModelAdmin):
list_display = ('name', 'related_accounts', )
def related_accounts(self, obj):
from django.core import urlresolvers
url = urlresolvers.reverse("admin:<yourapp>_account_changelist")
lookup = u"category__campaign__exact"
text = u"View Accounts"
return u"<a href='%s?%s=%d'>%s</a>" % (url, lookup, obj.pk, text)
related_accounts.allow_tags = True
admin.site.register(Campaign, CampaignAdmin)
admin.site.register(Category)
class AccountAdmin(admin.ModelAdmin):
list_display = ('category', 'name')
list_filter = ('category__campaign',)
admin.site.register(Account, AccountAdmin)
You'll need to replace with the name of your app where the Account ModelAdmin lives.
Note: the list_filter on the AccountAdmin is required since Django 1.2.4, Django 1.1.3 and Django 1.3 beta 1, which introduced protection from arbitrary filtering via URL parameter in the admin.
If i understand you correctly, you want to add a custom field (a callable in your ModelAdmin's list_display) to your CampaignAdmin change_list view.
Your custom field would be a link that takes the category.id of each category in your change_list and generates a link to the desired, filtered admin view, which seems to be the account-change_list in your case:
admin/yourproject/account/?category__id__exact=<category.id>
Assuming category is a field on your Campaign-Model you could add the follwoing method to your CampaignAdmin:
def account_link(self, obj):
return 'Accounts' % (obj.category.id)
account_link.allow_tags = True
And then you add it to the admin's list_display option:
list_display = ('account_link', ...)
It depends a bit on your data model though.
If you want to create a permanent, filtered change_list view that suits your needs, you may take a look at this article: http://lincolnloop.com/blog/2011/jan/11/custom-filters-django-admin/
The other solutions don't pay attention to the filters you already have applied. They are part of the query string and I wanted to retain them as well.
First you need to get a reference to the request, you can do that by wrapping changelist_view or queryset as I did:
class AccountAdmin(ModelAdmin):
model = Account
list_display = ('pk', 'campaign_changelist')
# ...
def queryset(self, request):
self._get_params = request.GET
return super(AccountAdmin, self).queryset(request)
def campaign_changelist(self, obj):
url = reverse('admin:yourapp_account_changelist')
querystring = self._get_params.copy()
querystring['campaign__id__exact'] = obj.campaign.pk
return u'{2}'.format(
url, querystring.urlencode(), obj.campaign)
campaign_changelist.allow_tags = True
And something like that will give you a filter inside the changelist rows. Really helpful. :-)
These are good solutions. I wasn't aware of the auto-filter by url paradigm. Here's another I've discovered which allows you use a custom url scheme:
from consensio.models import Account
from django.contrib import admin
from django.conf.urls.defaults import patterns, include, url
class AccountAdmin(admin.ModelAdmin):
campaign_id = 0;
def campaign_account_list(self, request, campaign_id, extra_context=None):
'''
First create your changelist_view wrapper which grabs the extra
pattern matches
'''
self.campaign_id = int(campaign_id)
return self.changelist_view(request, extra_context)
def get_urls(self):
'''
Add your url patterns to get the foreign key
'''
urls = super(AccountAdmin, self).get_urls()
my_urls = patterns('',
(r'^bycampaign/(?P<campaign_id>\d+)/$', self.admin_site.admin_view(self.campaign_account_list))
)
return my_urls + urls
def queryset(self, request):
'''
Filter the query set based on the additional param if set
'''
qs = super(AccountAdmin, self).queryset(request)
if (self.campaign_id > 0):
qs = qs.filter(category__campaign__id = self.campaign_id)
return qs
And plus you'd need to incorporate the URL link into CampaignAdmin's list view...