Extending django-oscarapi API ROOT to custom API class - django

I have a django oscar application and I use django-oscarapi for my custom APIs. Some things are missing from the oscarapi like category and promotions but I have been able to use django-restframework to create the category API but the challenge I am facing now is how to add it to the API-ROOT. This is my code for rendering categories
customapi serializer class
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'numchild', 'name', 'description', 'image', 'slug')
Views
class CategoryList(generics.ListAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer
class CategoryDetail(generics.RetrieveAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer
customapi/urls.py
url(r'^caty/$', CategoryList.as_view(), name='category-list'),
url(r'^caty/(?P<category_slug>[\w-]+(/[\w-]+)*)_(?P<pk>\d+)/$',
CategoryDetail.as_view(), name='category'),
Thanks in advance

You have to override the root view of oscarapi. There maybe a way to partially override the module, but I was unsuccessful at this.
module to override:
https://github.com/django-oscar/django-oscar-api/blob/master/oscarapi/views/root.py
In your project add a file yourApp/api/views/root.py and paste in the contents of the source file above.
You can then add end points by adding a tuple to the PUBLIC_APIS or ADMIN_APIS function.

Related

Exclude history fields from django simples history on to_representation method in django rest framework

I'm using Django Rest Framework with django-simple-history and currently I would like to return the history modifications in my Board rest API, currently it is doing well but I would like to hide some fields. This is the currently output:
But, I don't need id, history_id, etc.
my implementation is the same of alexander answer in this post.
this is my currently serializers, where I put history on my Board model
class HistoricalRecordField(serializers.ListField):
child = serializers.DictField()
def to_representation(self, data):
representation = super().to_representation(data.values())
# i've tried to do it by deleting, but does't work well.
del representation[0]['history_id']
return representation
class BoardSerializer(serializers.ModelSerializer):
history = HistoricalRecordField(read_only=True)
class Meta:
model = Board
fields = '__all__'
But it does not seem the best way to do it.
If you have some hint about how to do it the correct way I would like to know.
Thanks in advance!
You can try this for history_id at least:
def to_representation(self, data):
representation = super().to_representation(data.values())
for hist in representation['history']:
hist.pop('history_id')
return representation
I don't know django-simple-history, so they may be better solutions than mine. However, you can do this with a more DRF-friendly approach by simply using a ModelSerializer instead of a ListSerializer:
class HistoricalRecordSerializer(serializers.ModelSerializer):
class Meta:
model = HistoricalRecords
fields = ('name', 'description', 'share_with_company', [...]) # Only keep the fields you want to display here
class BoardSerializer(serializers.ModelSerializer):
history = HistoricalRecordSerializer(read_only=True, many=True)
class Meta:
model = Board
fields = ('name', 'description', 'history', [...]) # Only keep the fields you want to display here
If you want to only retrieve the latest update, you could use a SerializerMethodField (documentation here). Remember to declare it in Meta.fields instead of 'history' (or rename your SerializerMethodField "history" if you want to keep this name):
class HistoricalRecordSerializer(serializers.ModelSerializer):
class Meta:
model = HistoricalRecords
fields = ('name', 'description', 'share_with_company', [...]) # Only keep the fields you want to display here
class BoardSerializer(serializers.ModelSerializer):
latest_history = serializers.SerializerMethodField()
def get_latest_history(self, instance):
latest_history = instance.history.most_recent() # Retrieve the record you want here
return HistoricalRecordSerializer(latest_history).data
class Meta:
model = Board
fields = ('name', 'description', 'latest_history', [...]) # Only keep the fields you want to display here
Keep in mind that I don't know much about this lib, so this should work but I cannot guarantee it's the best way to to it.

Django REST API Listview

Currently I'm trying to develop personal blog with Django/REST API, and I have trouble for that.
There are a number of posts in blog and I want to control those posts with Hyperlink. I made it by using ModelViewSet, however, whole data in detailView is also shown in ListView.
The thing is, I only want "url" and "title" of posts to be shown in ListView while DetailView contains full data.
Here is my code and current results given by REST framework.
Don't mind IndexView
# serializers
class PostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Post
fields = '__all__'
# views
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = serializers.PostSerializer
permission_classes = (IsAdminUser, )
Post List in REST API:
Post instance in REST API:
As far as I'm aware, you need a separate serializer for the list view.
You could create a custom serializer that takes in a fields arg to select specific fields. But its probably simpler to just have a separate one for the ListView. Also, for the list view, if you are only showing a subset of the model fields, you can use the only() function on the queryset to only return the model data that you need. For example:
qs = MyModel.objects.all().only('field_a', 'field_b', 'field_c')
Here is the custom serializer if you decide to go that way:
class CustomSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
selected_fields = kwargs.pop('selected_fields', None)
# used pop function so selected_fields is not passed to superclass
super().__init__(*args, **kwargs)
if selected_fields:
# make sure only fields for the model are allowed
fields = set(selected_fields)
current_fields = set(self.fields.keys())
for field in current_fields - fields:
self.fields.pop(field)
class MyModelSerializer(CustomSerializer):
class Meta:
model = MyModel
fields = '__all__'
In the list view:
required_fields = ('field_a', 'field_b', 'field_c')
data_to_return = MyModelSerializer(model_queryset, many=True, fields=required_fields).data
return Response(data)

django rest extensions drf-extensions TypeError at /data/ register() got an unexpected keyword argument 'parents_query_lookups'

I am trying to get a simple nested route set up with drf-extensions but am having trouble following the docs, I am getting this error:
TypeError at /data/
register() got an unexpected keyword argument 'parents_query_lookups'
Trying to achieve /data/Survey/<survey>/Version/<version>/Product/<product>/
each survey has multiple versions and those versions will contain multiple Products e.g., /data/survey/survey1_name/version/survey1_version1/product/survey1_version_product1/
but currently I have un-nested endpoints
/data/survey/
/data/versions/
/data/products/
models.py
class Survey(models.Model):
survey = models.CharField(choices=SURVEYS, max_length=100)
class Version(models.Model):
version = models.CharField(choices=DATA_RELEASES, max_length=50)
survey = models.ForeignKey(Survey)
class Product(models.Model):
product = models.CharField(choices=PRODUCTS, max_length=100)
version = models.ForeignKey(Version)
views.py
class SurveyViewSet(NestedViewSetMixin, viewsets.ModelViewSet):
queryset = Survey.objects.all()
serializer_class = SurveySerializer
permission_classes = [permissions.AllowAny]
class VersionViewSet(NestedViewSetMixin, viewsets.ModelViewSet):
queryset = Version.objects.all()
serializer_class = VersionSerializer
permission_classes = [permissions.AllowAny]
class ProductViewSet(NestedViewSetMixin, viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [permissions.AllowAny]
serializers.py
class SurveySerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Survey
fields = ('id', 'survey')
class VersionSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Version
fields = ('id', 'version', 'survey')
class ProductSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Product
fields = ('id', 'product', 'version')
urls.py
router = ExtendDefaultRouter()
router.register(r'surveys', views.SurveyViewSet, base_name='survey')
router.register(r'versions', views.VersionViewSet, parents_query_lookups=['survey']),
router.register(r'products', views.ProductViewSet, parents_query_lookups=['version'])
urlpatterns = [
url(r'^data/', include(router.urls)),
]
You should chain the calls to register(), instead of calling them directly on the router :
urls.py
router = ExtendDefaultRouter()
(router.register(r'surveys', views.SurveyViewSet, base_name='survey')
.register(r'versions', views.VersionViewSet, parents_query_lookups=['survey']),
.register(r'products', views.ProductViewSet, parents_query_lookups=['version']))
That's because NestedRouterMixin.register() returns an instance of NestedRegistryItem, which is the class that understand the parents_query_lookups parameter.
In order to keep your project well organized in accordance with the hierarchy of your data, the first thing I would do is separate each tier of urls into a separate entity, and possibly even a separate file. The second would be to add an primary-key endpoint to each tier of the rest API. In the end it would look something like this:
survey_urls.py
router.register(r'survey', views.SurveyViewSet, base_name='survey')
router.register(r'survey-version/', include('my_app.rest_server.version_urls'))
version_urls.py
router.register(r'version', views.VersionViewSet, base_name='version')
router.register(r'version-product/', include('my_app.rest_server.product_urls'))
product_urls.py
router.register(r'product/(?P<pk>[0-9]+)$', views.ProductViewSet, base_name='product')
Then, your final url could look like: /data/survey-version/33/version-product/5/product/8
In all likelihood, this is an unnecessary complication of things, since each product has its own unique id, and then you can just access it with a url like: /data/product/pk, using a single url routing, namely the last line of code above.
My examples assumed the records have a numeric primary key, if the case is otherwise you'd have to change the regex accordingly.

Get relationship attributes with Django's REST API

There are two tables user, phone which are linked by an intermediate table owner. Here the goal is to use Rest API to get all phones from a specific user,
http://127.0.0.1/users/alice/phones/.
I use ModelSerializer as serializer and ViewSet as view. Please let me know how to get this done? I have no idea how to route /users/user_name/phones/ to get phones from a specific user.
Thanks.
Code snippet:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model=User
class PhoneSerializer(serializers.ModelSerializer):
class Meta:
model=Phone
class OwnerSerializer(serializers.ModelSerializer):
class Meta:
model=Owner
depth=1
// views
class UserViewSet(viewsets.ModelViewSet):
queryset=User.objects.all()
serializer_class=UserSerializer
class PhoneViewSet(viewsets.ModelViewSet):
queryset=Phone.objects.all()
serializer_class=PhoneSerializer
....
I'd suggest you to create a filter.
It will be something like this:
1) Create filter (Make sure that django-filter is installed.):
# filters.py
import django_filters
class PhoneFilter(django_filters.FilterSet):
user = django_filters.Filter(name="user__user_name")
class Meta:
model = Phone
fields = ('user',)
2) Add filter to your ViewSet:
# views.py
class PhoneViewSet(viewsets.ModelViewSet):
queryset=Phone.objects.all()
serializer_class=PhoneSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_class = filters.PhoneFilter
And now you may use this url: /phones/?user=user_name.
Use #detail_route to route URL to your function, e.g.,
#detail_route(methods=['get'])
def phones(self, request, pk=None):
pass
http://127.0.0.1/users/alice/phones will work!

How to represent `self` url in django-rest-framework

I want to add a link to a single resource representation which is an URL to itself, self. Like (taken from documentation):
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing')
{
'album_name': 'The Eraser',
'artist': 'Thom Yorke',
'self': 'http://www.example.com/api/album/2/',
}
How should this be done?
If you inherit serializers.HyperlinkedModelSerializer all you need to do is pass a url field to fields. See the docs here:
http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/
Alright, this solved my problem but if you have a better solution please post an answer:
from django.urls import reverse
from rest_framework import serializers
self_url = serializers.SerializerMethodField('get_self')
def get_self(self, obj):
request = self.context['request']
return reverse('album-detail', kwargs={'id': obj.id}, request=request)
here is my solution,
in your view methods create serilizer object like this:
album = AlbumSerializer(data=data, {"request":request})
in your serilizer class override to_representation method (you can read about this method on DRF docs
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
def to_representation(self, obj):
data = super().to_representation(obj)
request = self.context["request"]
return data
According to this issue, you can just add 'url' in the list of fields.
Here is a little more context than you got in the other answers so far. The key is the context argument passed to the serializer constructor and the 'url' in fields.
http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/
In your viewset:
class AlbumViewSet(viewsets.ViewSet):
def list(self, request):
queryset = Album.objects.all()
serializer = AlbumSerializer(queryset, many=True,
context={'request': request})
return Response(serializer.data)
In the serializer:
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing', 'url')
As stated above the HyperlinkedModelSerializer will convert all your related fields and remove the ID of the resource as well. I use one of the following solutions, depending on the situation.
Solution 1: import api settings and add the url field:
For more details see URL_FIELD_NAME.
from rest_framework.settings import api_settings
class AlbumSerializer(SelfFieldMixin, serializers.ModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing', api_settings.URL_FIELD_NAME)
Solution 2: a simple mixin, which only works if default fields are used:
class SelfFieldMixin:
"""
Adds the self link without converting all relations to HyperlinkedRelatedField
"""
def get_default_field_names(self, declared_fields, model_info):
"""
Return the default list of field names that will be used if the
`Meta.fields` option is not specified.
"""
default_fields = super().get_default_field_names(declared_fields, model_info)
return [self.url_field_name, *default_fields]
And it can be used like
class AlbumSerializer(SelfFieldMixin, serializers.ModelSerializer):
class Meta:
model = Album
fields = '__all__'
NOTE: It requires Python 3 due to the super() call, the a mixin must be placed before any of the serializer classes!
P.S.: To achieve the required response in the question one must also set the URL_FIELD_NAME to 'self'.
Edit: get_default_field_names must return a list object for Meta.exclude to work on ModelSerializers.
You can use the HyperlinkedIdentityField like so:
class ThingSerializer(ModelSerializer):
class Meta:
model = Thing
fields = ['self_link', ...]
self_link = HyperlinkedIdentityField(view_name='thing-detail')
You need to have your routes named appropriately but the Default routers do so automatically (documented here).
As others have pointed out the HyperlinkedModelSerializer also works. This is because it uses this field automatically. See here.