Wagtail: Serializing page model - python

I am using wagtail as a REST backend for a website. The website is built using react and fetches data via wagtails API v2.
The SPA website needs to be able to show previews of pages in wagtail. My thought was to override serve_preview on the page model and simply seralize the new page as JSON and write it to a cache which could be accessed by my frontend. But im having trouble serializing my page to json. All attempts made feel very "hackish"
I've made several attempts using extentions of wagtails built in serializers but without success:
Atempt 1:
def serve_preview(self, request, mode_name):
from wagtail.api.v2.endpoints import PagesAPIEndpoint
endpoint = PagesAPIEndpoint()
setattr(request, 'wagtailapi_router',
WagtailAPIRouter('wagtailapi_v2'))
endpoint.request = request
endpoint.action = None
endpoint.kwargs = {'slug': self.slug, 'pk': self.pk}
endpoint.lookup_field = 'pk'
serializer = endpoint.get_serializer(self)
Feels very ugly to use router here and set a bunch of attrs
Attempt 2:
def serve_preview(self, request, mode_name):
from wagtail.api.v2.endpoints import PagesAPIEndpoint
fields = PagesAPIEndpoint.get_available_fields(self)
if hasattr(self, 'api_fields'):
fields.extend(self.api_fields)
serializer_class = get_serializer_class(
type(self), fields, meta_fields=[PagesAPIEndpoint.meta_fields], base=PageSerializer)
serializer = serializer_class(self)
Better but i get context issues:
Traceback (most recent call last):
...
File "/usr/local/lib/python3.5/site-packages/wagtail/api/v2/serializers.py", line 92, in to_representation
self.context['view'].seen_types[name] = page.specific_class
KeyError: 'view'
Any toughts?

Solved it by diving through the source code.
First define an empty dummy view:
class DummyView(GenericViewSet):
def __init__(self, *args, **kwargs):
super(DummyView, self).__init__(*args, **kwargs)
# seen_types is a mapping of type name strings (format: "app_label.ModelName")
# to model classes. When an object is serialised in the API, its model
# is added to this mapping. This is used by the Admin API which appends a
# summary of the used types to the response.
self.seen_types = OrderedDict()
Then use this view and set the context of your serializer manually. Im also using the same router as in my api in my context. It has methods which are called by the PageSerializer to resolve some fields. Kinda strange it is so tightly coupled with the wagtail api but at least this works:
def serve_preview(self, request, mode_name):
import starrepublic.api as StarApi
fields = StarApi.PagesAPIEndpoint.get_available_fields(self)
if hasattr(self, 'api_fields'):
fields.extend(self.api_fields)
serializer_class = get_serializer_class(
type(self), fields, meta_fields=[StarApi.PagesAPIEndpoint.meta_fields], base=PageSerializer)
serializer = serializer_class(
self, context={'request': request, 'view': DummyView(), 'router': StarApi.api_router})
Dont forget to import:
from wagtail.api.v2.serializers import get_serializer_class
from rest_framework.viewsets import GenericViewSet
from rest_framework import status
from rest_framework.response import Response
from django.http import JsonResponse
from django.http import HttpResponse

Possibly a non-answer answer, but I too have had challenges in the area of DRF, Wagtail's layering on top of DRF, and the need to cache json results (DRF has no built-in caching as far as I can tell, so that's an additional challenge). In a recent project, I ended up just building a list of dictionaries in a view and sending them back out with HttpResponse(), bypassing DRF and Wagtail API altogether. The code ended up simple, readable, and was easy to cache:
import json
from django.http import HttpResponse
from django.core.cache import cache
data = cache.get('mydata')
if not data:
datalist = []
for foo in bar:
somedata = {}
# Populate somedata, "serializing" fields manually...
datalist.append(somedata)
# Cache for a week.
data = datalist
cache.set('mydata', datalist, 60 * 60 * 24 * 7)
return HttpResponse(json.dumps(data), content_type='application/json')
Not as elegant as using the pre-built REST framework, but sometimes the simpler approach is just more productive...

Related

Pagination in Django-Rest-Framework with API-View

It is necessary to display the configured 10 records per swagger page (book api).
The BookListView class in views.py looks like this:
class BookListView(APIView):
def get(self, request):
author = set([item.get('ID') for item in Author.objects.values('ID').all()])
saler = set([item.get('name') for item in Saler.objects.values('name').all()])
if author:
salers = Saler.objects.filter(name__in=list(saler - author))
else:
salers = Saler.objects.all()
serializer = SalerListSerializer(salers, many = True)
return Response(serializer.data)
Now all records are displayed at once, I would like to add pangination and display 10 records on one page.
I added 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.DESIRED_PAGINATION_STYLE', 'PAGE_SIZE': 100 to the settings.py file, but since I am using APIView, this does not work.
What is the way to implement the idea?
You need to call your paginator's paginate_queryset method. Since you're using a APIView, it does not have many of the built-in functions to do this for you, but the process is as follows:
Instantiate a paginator:
from rest_framework.settings import api_settings
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
paginator = pagination_class()
Get a page from your queryset and serialize it:
page = paginator.paginate_queryset(queryset, request, view=self)
serializer = self.get_serializer(page, many=True)
Return a paginated response:
return paginator.get_paginated_response(serializer.data)
This is how django-rest does it with its ListAPIView class. You can go though the code easily by checking out the ListAPIView's list method here.
However, I suggest using a ListAPIView instead of an APIView since it already handles
pagination for you.

Django Rest Framework get serializer of different model?

I have a method in one of my viewsets:
Endpoint: /api/game/{id}/sessions:
from .serializers import GameSerializer
from .models import Game
from gamesessions.models import GameSession
from gamesessions.serializers import GameSessionSerializer
from gamesessions.viewsets import GameSessionViewSet
#action(methods=['get'], detail=True)
def sessions(self, request, **id):
game = self.get_object()
sessions = []
for session in GameSession.objects.filter(game=game.id):
sessions.append(session)
serializer = GameSessionViewSet.get_serializer(sessions, many=True)
return Response(serializer.data)
But I'm getting an error because I can't figure out where the get_serializer method comes from and/or how to implement it externally.
I need to get the serializer of the session model. I can generate the list of sessions just fine, but it says the object is not JSON serializable, which is what DRF is supposed to handle.
So I just need to know what do I import to get the seralizer from the other class?
Maybe you can try, to use the serializer directly with the name like this:
serializer = GameSessionSerializer(sessions, many=True)

Python Django Rest Post API without storage

I would like to create a web api with Python and the Django Rest framework. The tutorials that I have read so far incorporate models and serializers to process and store data. I was wondering if there's a simpler way to process data that is post-ed to my api and then return a JSON response without storing any data.
Currently, this is my urls.py
from django.conf.urls import url
from rest_framework import routers
from core.views import StudentViewSet, UniversityViewSet, TestViewSet
router = routers.DefaultRouter()
router.register(r'students', StudentViewSet)
router.register(r'universities', UniversityViewSet)
router.register(r'other', TestViewSet,"other")
urlpatterns = router.urls
and this is my views.py
from rest_framework import viewsets
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import University, Student
from .serializers import UniversitySerializer, StudentSerializer
import json
from django.http import HttpResponse
class StudentViewSet(viewsets.ModelViewSet):
queryset = Student.objects.all()
serializer_class = StudentSerializer
class UniversityViewSet(viewsets.ModelViewSet):
queryset = University.objects.all()
serializer_class = UniversitySerializer
class TestViewSet(viewsets.ModelViewSet):
def retrieve(self, request, *args, **kwargs):
return Response({'something': 'my custom JSON'})
The first two parts regarding Students and Universities were created after following a tutorial on Django setup. I don't need the functionality that it provides for creating, editing and removing objects. I tried playing around with the TestViewSet which I created.
I am currently stuck trying to receive JSON data that gets posted to the url ending with "other" and processing that JSON before responding with some custom JSON.
Edit
These two links were helpful in addition to the solution provided:
Django REST framework: non-model serializer
http://jsatt.com/blog/abusing-django-rest-framework-part-1-non-model-endpoints/
You can use their generic APIView class (which doesn't have any attachment to Models or Serializers) and then handle the request yourself based on the HTTP request type. For example:
class RetrieveMessages(APIView):
def post(self, request, *args, **kwargs):
posted_data = self.request.data
city = posted_data['city']
return_data = [
{"echo": city}
]
return Response(status=200, data=return_data)
def get....

How to get api format response from Django

I followed "Writing regular Django views..." from the official documentation of Django Rest framework and got this kind of code:
#views.py file
#imports go here
class JSONResponse(HttpResponse):
"""
An HttpResponse that renders its content into JSON.
"""
def __init__(self, data, **kwargs):
content = JSONRenderer().render(data)
kwargs['content_type'] = 'application/json'
super(JSONResponse, self).__init__(content, **kwargs)
def items(request):
output = [{"a":"1","b":"2"},{"c":"3","d":"4"}]
return JSONResponse(output)
And it works well. When a user goes to /items/ page, he or she sees a nicely looking json-formated data [{"a":"1","b":"2"},{"c":"3","d":"4"}]. But, how can I get (code?) api-formated data, or check if a user requested ?format=api then render in api format manner and if not, then in json format. By api-formated data I mean this kind of view
Try using the #api_view() decorator as described here. And make sure you use the built in Response instead of JSONResponse.
Your view should then look something like this:
from rest_framework.decorators import api_view
...
#api_view()
def items(request):
output = [{"a":"1","b":"2"},{"c":"3","d":"4"}]
return Response(output)
In the case of getting the error
Cannot apply DjangoModelPermissions on a view that does not have model or queryset property
Remove DjangoModelPermissions from your rest framework permissions settings in you settings.py

Send a file through Django Class Based Views

We use class based views for most of our project. We have run into an issue when we try and create a CSV Mixin that will allow the user to export the information from pretty much any page as a CSV file. Our particular problem deals with CSV files, but I believe my question is generic enough to relate to any file type.
The problem we are having is that the response from the view is trying to go to the template (say like from django.views.generic import TemplateView). We specify the template in the urls.py file.
url(r'^$', MyClassBasedView.as_view(template_name='my_template.html'))
How can you force the response to bypass the template and just return a standard HttpResponse? I'm guessing you'll need to override a method but I'm not sure which one.
Any suggestions?
EDIT1: It appears that I was unclear as to what we are trying to do. I have rendered a page (via a class based view) and the user will see reports of information. I need to put in a button "Export to CSV" for the user to press and it will export the information on their page and download a CSV on to their machine.
It is not an option to rewrite our views as method based views. We deal with almost all class based view types (DetailView, ListView, TemplateView, View, RedirectView, etc.)
This is a generic problem when you need to provide different responses for the same data. The point at which you would want to interject is when the context data has already been resolved but the response hasn't been constructed yet.
Class based views that resolve to the TemplateResponseMixin have several attributes and class methods that control how the response object is constructed. Do not be boxed into thinking that the name implies that only HTML responses or those that need template processing can only be facilitated by this design. Solutions can range from creating custom, reusable response classes which are based on the behavior of the TemplateResponse class or creating a reusable mixin that provides custom behavior for the render_to_response method.
In lieu of writing a custom response class, developers more often provide a custom render_to_response method on the view class or separately in a mixin, as it's pretty simple and straightforward to figure out what's going on. You'll sniff the request data to see if some different kind of response has to be constructed and, if not, you'll simply delegate to the default implementation to render a template response.
Here's how one such implementation might look like:
import csv
from django.http import HttpResponse
from django.utils.text import slugify
from django.views.generic import TemplateView
class CSVResponseMixin(object):
"""
A generic mixin that constructs a CSV response from the context data if
the CSV export option was provided in the request.
"""
def render_to_response(self, context, **response_kwargs):
"""
Creates a CSV response if requested, otherwise returns the default
template response.
"""
# Sniff if we need to return a CSV export
if 'csv' in self.request.GET.get('export', ''):
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="%s.csv"' % slugify(context['title'])
writer = csv.writer(response)
# Write the data from the context somehow
for item in context['items']:
writer.writerow(item)
return response
# Business as usual otherwise
else:
return super(CSVResponseMixin, self).render_to_response(context, **response_kwargs):
Here's where you can also see when a more elaborate design with custom response classes might be needed. While this works perfectly for adding ad-hoc support for a custom response type, it doesn't scale well if you wanted to support, say, five different response types.
In that case, you'd create and test separate response classes and write a single CustomResponsesMixin class which would know about all the response classes and provide a custom render_to_response method that only configures self.response_class and delegates everything else to the response classes.
How can you force the response to bypass the template and just return
a standard HttpResponse?
This kinda defeats the point of using a TemplateView. If the thing you're trying to return isn't a templated response, then it should be a different view.
However...
I'm guessing you'll need to override a method but I'm not sure which one.
...if you prefer to hack it into an existing TemplateView, note from the source code...
class TemplateView(TemplateResponseMixin, ContextMixin, View):
"""
A view that renders a template. This view will also pass into the context
any keyword arguments passed by the url conf.
"""
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
...so you'd have to override the get() method so it doesn't call render_to_response() when returning your CSV. For example...
class MyClassBasedView(TemplateView):
def get(self, request, *args, **kwargs):
if request.GET['csv'].lower() == 'true':
# Build custom HTTP response
return my_custom_response
else:
return TemplateView.get(request, *args, **kwargs)
If you need a generic mixin for all subclasses of View, I guess you could do something like...
class MyMixin(object):
def dispatch(self, request, *args, **kwargs):
if request.GET['csv'].lower() == 'true':
# Build custom HTTP response
return my_custom_response
else:
return super(MyMixin, self).dispatch(request, *args, **kwargs)
class MyClassBasedView(MyMixin, TemplateView):
pass

Categories