I am trying to use Piston to provide REST support to Django.
I have implemented my handlers as per the documentation provided .
The problem is that i can "read" and "delete" my resource but i cannot "create" or "update".
Each time i hit the relevant api i get a 400 Bad request Error.
I have extended the Resource class for csrf by using this commonly available code snippet:
class CsrfExemptResource(Resource):
"""A Custom Resource that is csrf exempt"""
def __init__(self, handler, authentication=None):
super(CsrfExemptResource, self).__init__(handler, authentication)
self.csrf_exempt = getattr(self.handler, 'csrf_exempt', True)
My class (code snippet) looks like this:
user_resource = CsrfExemptResource(User)
class User(BaseHandler):
allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
#require_extended
def create(self, request):
email = request.GET['email']
password = request.GET['password']
phoneNumber = request.GET['phoneNumber']
firstName = request.GET['firstName']
lastName = request.GET['lastName']
self.createNewUser(self, email,password,phoneNumber,firstName,lastName)
return rc.CREATED
Please let me know how can i get the create method to work using the POST operation?
This is happening because Piston doesn't like the fact that ExtJS is putting "charset=UTF-8" in the content-type of the header.
Easily fixed by adding some middleware to make the content-type a bit more Piston friendly, create a file called middleware.py in your application base directory:
class ContentTypeMiddleware(object):
def process_request(self, request):
if request.META['CONTENT_TYPE'] == 'application/x-www-form-urlencoded; charset=UTF-8':
request.META['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
return None
Then simply include this middleware in your settings.py:
MIDDLEWARE_CLASSES = (
'appname.middleware.ContentTypeMiddleware',
)
Proposed solutions still did not work for me (django 1.2.3/piston 0.2.2) so I've tweaked joekrell solution and this finally works (I'm only using POST and PUT, but presumably you can add other verbs to the list):
class ContentTypeMiddleware(object):
def process_request(self, request):
if request.method in ('POST', 'PUT'):
# dont break the multi-part headers !
if not 'boundary=' in request.META['CONTENT_TYPE']:
del request.META['CONTENT_TYPE']
with:
MIDDLEWARE_CLASSES = (
'appname.middleware.ContentTypeMiddleware',
)
I haven't noticed any side-effect, but I can't promise it's bullet-proof.
I have combined some of what other people have said, and added support for any content type, json for instance...
class ContentTypeMiddleware(object):
def process_request(self, request):
if request.method in ('POST', 'PUT') and request.META['CONTENT_TYPE'].count(";") > 0:
request.META['CONTENT_TYPE'] = [c.strip() for c in request.META['CONTENT_TYPE'].split(";") ][0]
return None
I thought Eric's solution worked the best but then ran into problems when saving things in admin. This tweak seems to fix it if anyone else comes across it:
class ContentTypeMiddleware(object):
def process_request(self, request):
if request.method in ('POST') and not 'boundary=' in request.META['CONTENT_TYPE']:
request.META['CONTENT_TYPE'] = [c.strip() for c in request.META['CONTENT_TYPE'].split(";") ][0]
return None
In utils.py, change this.
def content_type(self):
"""
Returns the content type of the request in all cases where it is
different than a submitted form - application/x-www-form-urlencoded
"""
type_formencoded = "application/x-www-form-urlencoded"
ctype = self.request.META.get('CONTENT_TYPE', type_formencoded)
if ctype.strip().lower().find(type_formencoded) >= 0:
return None
return ctype
https://bitbucket.org/jespern/django-piston/issue/87/split-charset-encoding-form-content-type
This is solution which worked for me, after a tweak:
class ContentTypeMiddleware(object):
def process_request(self, request):
if 'charset=UTF-8' in request.META['CONTENT_TYPE']:
request.META['CONTENT_TYPE'] = request.META['CONTENT_TYPE'].replace('; charset=UTF-8','')
return None
We had a resource that was simply updating a timestamp based on the request credentials and PUT. It turns out that Piston does not like PUT without a payload. Adding an empty string payload '' fixed it.
A quick Google search indicates that other systems like Apache may not like PUT without a payload, as well.
Related
I have two separate endpoints that "accepts" and "declines" each applicant on the system respectively.
Endpoint #1:
...api/v1/applicants/{ID}/accept
Endpoint #2:
...api/v1/applicants/{ID}/decline
Now, I'm refactoring and trying to combine the Endpoints into one, such that the following URL can be used to accept and decline an applicant, while maintaining :
...api/v1/applicants/{ID}/
The purpose of doing this is to conform the endpoints to REST methodologies.
WHAT I TRIED:
I tried creating a hidden method [starting with underscore - eg: _someFunction() using a PUT request method]. This didn't work.
I know I could also do it through the serializers.py file but don't know how as I haven't seen an example online.
Here is the accept class. The decline is similar with minor changes:
#action(detail=True, methods=['put'])
def accept(self, request, pk):
data = request.data
if request.user.user_type == "2":
if Applicant.objects.filter(id=pk).exists():
applicant = Applicant.objects.get(id=pk)
if applicant.status == '1':
applicant.status = '2'
applicant.save()
# Hash the ID of the particular applicant so it can be used for verification
# make this a function during refactoring
hashids = Hashids(salt=HASH_SALT, min_length=16)
hashid = hashids.encode(applicant.id)
# send email with the link to the applicant
# make this a function too
SENDER="xxx#example.com"
SUBJECT="Congratulations, you've been accepted!"
MESSAGE = """
Hello {}, \n
Your application as a Journalist on example.com was accepted.\n
Copy your OTP: {} and Click here "https://example.com/verify-otp/ to enter it.\n
Cheers!
""".format(applicant.first_name, hashid)
send_mail(SUBJECT, MESSAGE, SENDER, [applicant.email], fail_silently=False)
# generate a response object
queryset = applicant
serializer = ApplicantSerializer(queryset)
return Response(jsend.success({'applicants':serializer.data}))
else:
return Response((jsend.error("Cannot perform such action for this applicant")), status=status.HTTP_400_BAD_REQUEST)
else:
return Response((jsend.error('Cannot find an applicant with ID of {}'.format(pk))), status=status.HTTP_404_NOT_FOUND)
else:
return Response((jsend.error("You are not authorized to perform this action")), status=status.HTTP_403_FORBIDDEN)
You can override the perform_update() or update() of the ModelViewSet for example using perform_update:
def perform_update(self, serializer):
applicant = self.get_object()
if applicant.status == '1':
serializer.save(status='2')
#rest of your logic
serializer.save()
Hope it helps. Note you can get the request inside perform_update like this:
self.request
I am new with tasty pie and I am trying to simply return a json structure from my API.
I have the following class:
class CaseResource(Resource):
class Meta:
authentication = SessionAuthentication()
resource_name = 'case'
allowed_methods = ['get']
def obj_get_list(self, request, **kwargs):
case = request.GET.get('q')
if case:
mycase = connect_and_retrieve_data(request, q)
return self.create_response(request, {'mycase': mycase})
connect_and_retrieve_data is a method that is returning a json_dump for a non ORM object.
When I am sending the GET request in AJAX, I got the following response:
NotImplementedError at /mydashboard/api/v1/case/
No exception message supplied
The API pointed me to "Using Tastypie With Non-ORM Data Sources", though I was hoping for something less heavy handed.
I guess that I have to overwrite all the methods:
detail_uri_kwargs
get_object_list
obj_get_list
obj_get
obj_create
obj_update
obj_delete_list
obj_delete
rollback
but this looks quite heavy. Is there an other way to do this or using a different method?
Thank you in advance for your help,
For one single request you could make none RESTful endpoint. This would be not right if you have decided for some reason that your whole service is going to be 100% RESTful.
But, if you are looking for something less heavily try standard Django view:
import json
from django.http import (HttpResponse,
HttpResponseNotAllowed, HttpResponseForbidden)
from django.views.decorators.csrf import csrf_exempt
#csrf_exempt
def connect_and_retrieve_data(request):
"""
Docstrings..
"""
if not request.user.is_authenticated():
return HttpResponseForbidden(json.dumps({'message': 'User must be authenticated'}),
content_type='application/json')
if request.method != 'GET':
return HttpResponseNotAllowed(permitted_methods=('GET',))
if request.GET.get('q'):
mycase = connect_and_retrieve_data(request, request.GET.get('q'))
mycase = json.loads(mycase)
return HttpResponse(
json.dumps({'mycase': mycase, 'success': True}),
content_type='application/json')
else:
return HttpResponse(
json.dumps({'message': 'Missing q param', 'success': False}),
content_type='application/json')
Be aware of that your project might be falling out of control soon if you will get too many undocumented and untested endpoints like this in RESTful API. At some point you might consider something else then RESTful. But if it is not more then 10% of all endpoints and nicely described you will be fine.
I'm having trouble testing a ViewSet:
class ViewSetTest(TestCase):
def test_view_set(self):
factory = APIRequestFactory()
view = CatViewSet.as_view()
cat = Cat(name="bob")
cat.save()
request = factory.get(reverse('cat-detail', args=(cat.pk,)))
response = view(request)
I'm trying to replicate the syntax here:
http://www.django-rest-framework.org/api-guide/testing#forcing-authentication
But I think their AccountDetail view is different from my ViewSet, so I'm getting this error from the last line:
AttributeError: 'NoneType' object has no attributes 'items'
Is there a correct syntax here or am I mixing up concepts? My APIClient tests work, but I'm using the factory here because I would eventually like to add "request.user = some_user". Thanks in advance!
Oh and the client test works fine:
def test_client_view(self):
response = APIClient().get(reverse('cat-detail', args=(cat.pk,)))
self.assertEqual(response.status_code, 200)
I think I found the correct syntax, but not sure if it is conventional (still new to Django):
def test_view_set(self):
request = APIRequestFactory().get("")
cat_detail = CatViewSet.as_view({'get': 'retrieve'})
cat = Cat.objects.create(name="bob")
response = cat_detail(request, pk=cat.pk)
self.assertEqual(response.status_code, 200)
So now this passes and I can assign request.user, which allows me to customize the retrieve method under CatViewSet to consider the user.
I had the same issue, and was able to find a solution.
Looking at the source code, it looks like the view expects there to be an argument 'actions' that has a method items ( so, a dict ).
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/viewsets.py#L69
This is where the error you're getting is coming from. You'll have to specify the argument actions with a dict containing the allowed actions for that viewset, and then you'll be able to test the viewset properly.
The general mapping goes:
{
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
}
http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers
In your case you'll want {'get': 'retrieve'}
Like so:
class ViewSetTest(TestCase):
def test_view_set(self):
factory = APIRequestFactory()
view = CatViewSet.as_view(actions={'get': 'retrieve'}) # <-- Changed line
cat = Cat(name="bob")
cat.save()
request = factory.get(reverse('cat-detail', args=(cat.pk,)))
response = view(request)
EDIT: You'll actually need to specify the required actions. Changed code and comments to reflect this.
I found a way to do this without needing to manually create the right viewset and give it an action mapping:
from django.core.urlresolvers import reverse, resolve
...
url = reverse('cat-list')
req = factory.get(url)
view = resolve(url).func
response = view(req)
response.render()
I think it's your last line. You need to call the CatViewSet as_view(). I would go with:
response = view(request)
given that you already defined view = CatViewSet.as_view()
EDIT:
Can you show your views.py? Specifically, what kind of ViewSet did you use? I'm digging through the DRF code and it looks like you may not have any actions mapped to your ViewSet, which is triggering the error.
I needed to get this working with force authentication, and finally got it, here is what my test case looks like:
from django.test import TestCase
from rest_framework.test import APIRequestFactory
from django.db.models.query import QuerySet
from rest_framework.test import force_authenticate
from django.contrib.auth.models import User
from config_app.models import Config
from config_app.apps import ConfigAppConfig
from config_app.views import ConfigViewSet
class ViewsTestCase(TestCase):
def setUp(self):
# Create a test instance
self.config = Config.objects.create(
ads='{"frequency": 1, "site_id": 1, "network_id": 1}',
keys={}, methods={}, sections=[], web_app='{"image": 1, "label": 1, "url": 1}',
subscriptions=[], name='test name', build='test build', version='1.0test', device='desktop',
platform='android', client_id=None)
# Create auth user for views using api request factory
self.username = 'config_tester'
self.password = 'goldenstandard'
self.user = User.objects.create_superuser(self.username, 'test#example.com', self.password)
def tearDown(self):
pass
#classmethod
def setup_class(cls):
"""setup_class() before any methods in this class"""
pass
#classmethod
def teardown_class(cls):
"""teardown_class() after any methods in this class"""
pass
def shortDescription(self):
return None
def test_view_set1(self):
"""
No auth example
"""
api_request = APIRequestFactory().get("")
detail_view = ConfigViewSet.as_view({'get': 'retrieve'})
response = detail_view(api_request, pk=self.config.pk)
self.assertEqual(response.status_code, 401)
def test_view_set2(self):
"""
Auth using force_authenticate
"""
factory = APIRequestFactory()
user = User.objects.get(username=self.username)
detail_view = ConfigViewSet.as_view({'get': 'retrieve'})
# Make an authenticated request to the view...
api_request = factory.get('')
force_authenticate(api_request, user=user)
response = detail_view(api_request, pk=self.config.pk)
self.assertEqual(response.status_code, 200)
I'm using this with the django-nose test runner and it seems to be working well. Hope it helps those that have auth enabled on their viewsets.
I am using Django, and require certain 'trial' users only to activate a certain part of the website-any ideas on an efficient way to do this?
I was thinking about giving a paying customer a certain ID and linking this to the URL of the sites for permission.
Thanks,
Tom
I'd use a view decorator like this:
def paying_only(view):
def _decorated(request, *args, **kwargs):
if not is_paying(request.user):
redirect('page_explaining_that_this_is_for_paying_users_only')
return view(request, *args, **kwargs)
return _decorated
#paying_only
def some_view(request):
...
I decided to post my solution, I might even get some feedback. I have a middleware blocking request/responses on certain paths defined in settings, first the middleware:
import re
from django.conf import settings
from django.shortcuts import redirect
class InvitationRequired(object):
def process_response(self, request, response):
if not settings.CLOSED_BETA_ACTIVE:
return response
if (hasattr(request, 'user')
and hasattr(request.user, 'is_authenticated')
and request.user.is_authenticated()):
return response
elif (request.path in
settings.CLOSED_BETA_INVITATION_MIDDLEWARE_EXCEPTED_URIS):
return response
elif response.status_code < 200 or response.status_code >= 300:
return response
else:
for regex in \
settings.CLOSED_BETA_INVITATION_MIDDLEWARE_EXCEPTED_PATTERNS:
if re.compile(regex).match(request.path):
return response
return redirect(settings.CLOSED_BETA_INVITATION_MIDDLEWARE_REDIRECT)
In settings.py I have something like this:
CLOSED_BETA_ACTIVE = True
CLOSED_BETA_INVITATION_URL = '/invitation/'
CLOSED_BETA_INVITATION_MIDDLEWARE_REDIRECT = CLOSED_BETA_INVITATION_URL
CLOSED_BETA_INVITATION_MIDDLEWARE_EXCEPTED_PATTERNS = (
r'^/api/v1/',
r'^/static/',
r'^/media/',
r'^/admin/',
r'^/registration/',
r'^/',
)
Hope it's clear, at least it can give you a a different approach.
This is a very wide-ranging question. One solution would be to store a trial flag on each user. On an authenticated request, check for User.trial in your controller (and probably view) and selectively allow/deny access to the endpoint or selectively render parts of the page.
If you wish to use built-in capabilities of Django, you could view 'trial' as a permission, or a user group.
I have a custom session class that I've built to extend the Django SessionBase. I did this in order to reuse a legacy Session table, so that sessions can pass between our Django pages and our PHP pages without having the user to log in and back out.
Everything's working perfectly so, far with one huge BUT.
I wrote some custom middleware in order to let the SessionStore.start() function have access to the Request Object. Unfortunately, in order to do that I used this answer: Access request.session from backend.get_user in order to remedy my problem.
I have learned that using the above answer (Essentially binding the request object to the settings, so you can access using import settings* and then settings.request) is totally horrible and the absolutely worst way to do this.
My core problem, is I don't understand how I can access the request from within the custom session backend I've written.
Maybe in middleware you could pass request to your custom SessionStore like this:
request.session = engine.SessionStore(session_key,request)
and in SessionStore:
class SessionStore(SessionBase):
def __init__(self, session_key=None, request):
self.request = request
super(SessionStore, self).__init__(session_key)
Later you can access request as self.request.
Django's SessionMiddleware does this:
class SessionMiddleware(object):
def process_request(self, request):
engine = import_module(settings.SESSION_ENGINE)
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
request.session = engine.SessionStore(session_key)
can't you do this?
import mycustomsessionbackend as myengine
class MyCustomSessionMiddleware(object):
def process_request(self, request):
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
request.session = myengine.SessionStore(session_key, request)
...
# mycustomsessionbackend.py
class SessionStore(SessionBase):
def __init__(self, session_key=None, request=None):
super(SessionStore, self).__init__(session_key)
self.request = request