I'm trying to implement a simple Django service with a RESTful API using tastypie. My problem is that when I try to create a WineResource with PUT, it works fine, but when I use POST, it returns a HTTP 501 error. Reading the tastypie documentation, it seems like it should just work, but it's not.
Here's my api.py code:
class CustomResource(ModelResource):
"""Provides customizations of ModelResource"""
def determine_format(self, request):
"""Provide logic to provide JSON responses as default"""
if 'format' in request.GET:
if request.GET['format'] in FORMATS:
return FORMATS[request.GET['format']]
else:
return 'text/html' #Hacky way to prevent incorrect formats
else:
return 'application/json'
class WineValidation(Validation):
def is_valid(self, bundle, request=None):
if not bundle.data:
return {'__all__': 'No data was detected'}
missing_fields = []
invalid_fields = []
for field in REQUIRED_WINE_FIELDS:
if not field in bundle.data.keys():
missing_fields.append(field)
for key in bundle.data.keys():
if not key in ALLOWABLE_WINE_FIELDS:
invalid_fields.append(key)
errors = missing_fields + invalid_fields if request.method != 'PATCH' \
else invalid_fields
if errors:
return 'Missing fields: %s; Invalid fields: %s' % \
(', '.join(missing_fields), ', '.join(invalid_fields))
else:
return errors
class WineProducerResource(CustomResource):
wine = fields.ToManyField('wines.api.WineResource', 'wine_set',
related_name='wine_producer')
class Meta:
queryset = WineProducer.objects.all()
resource_name = 'wine_producer'
authentication = Authentication() #allows all access
authorization = Authorization() #allows all access
class WineResource(CustomResource):
wine_producer = fields.ForeignKey(WineProducerResource, 'wine_producer')
class Meta:
queryset = Wine.objects.all()
resource_name = 'wine'
authentication = Authentication() #allows all access
authorization = Authorization() #allows all access
validation = WineValidation()
filtering = {
'percent_new_oak': ('exact', 'lt', 'gt', 'lte', 'gte'),
'percentage_alcohol': ('exact', 'lt', 'gt', 'lte', 'gte'),
'color': ('exact', 'startswith'),
'style': ('exact', 'startswith')
}
def hydrate_wine_producer(self, bundle):
"""Use the provided WineProducer ID to properly link a PUT, POST,
or PATCH to the correct WineProducer instance in the db"""
#Workaround since tastypie has bug and calls hydrate more than once
try:
int(bundle.data['wine_producer'])
except ValueError:
return bundle
bundle.data['wine_producer'] = '/api/v1/wine_producer/%s/' % \
bundle.data['wine_producer']
return bundle
Any help is greatly appreciated! :-)
This usually means that you are sending the POST to a detail uri, e.g. /api/v1/wine/1/. Since POST means treat the enclosed entity as a subordinate, sending the POST to the list uri, e.g. /api/v1/wine/, is probably what you want.
Related
I am working on a Quiz Application using DjangoRestFramework and ReactJS. My App is comprised of two apps, api and frontend. In my api app, I have multiple API views for many different things. One of my API Views is called JoinQuiz. When I call it on my frontend, I get this error in the console:
Forbidden: /api/join-quiz
[16/Dec/2020] "POST /api/join-quiz HTTP/1.1" 403 58
I don't think my problem is due to a CSRF error because my other API views are working perfectly fine. I may be wrong on this point.
[16/Dec/2020] "GET /api/get-question?id=3 HTTP/1.1" 200 10009
[17/Dec/2020 01:15:47] "POST /api/create-quiz HTTP/1.1" 201 11959
I suspect that my request.session may be the issue because when I go directly to /api/join-quiz and make a POST request with my code, nothing goes wrong and I have a successful post.
Files
Views.py
class QuizView(generics.ListAPIView):
queryset = Quiz.objects.all()
serializer_class = QuizSerializer
class GetQuiz(APIView):
""" Searches for a quiz given its code and returns the Quiz with is_host info"""
serializer_class = QuizSerializer
lookup_url_kwarg = 'code'
def get(self, request, format=None): # This is an HTTP GET request
code = request.GET.get(self.lookup_url_kwarg)
if code != None: # Check if code is not equal to None
quiz = Quiz.objects.filter(code=code)
if len(quiz) > 0: # If there is a quiz...
data = QuizSerializer(quiz[0]).data
data['is_host'] = self.request.session.session_key == quiz[0].host
return Response(data, status=status.HTTP_200_OK)
return Response({'Quiz not found': 'Invalid Code'}, status=status.HTTP_404_NOT_FOUND)
return Response({'Bad Request': 'Code Parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
class CreateQuizView(APIView):
"""Creates A new Quiz given nested question and answer data"""
serializer_class = QuizSerializer
def post(self, request, format=None):
""" Create the User's Account first"""
if not self.request.session.exists(self.request.session.session_key):
self.request.session.create()
data = request.data
data['host'] = self.request.session.session_key
serializer = self.serializer_class(data=data)
if serializer.is_valid():
quiz = serializer.create(validated_data=data)
self.request.session['quiz_code'] = quiz.code
return Response(
self.serializer_class(quiz).data,
status=status.HTTP_201_CREATED
)
return Response({'Bad Request': 'Invalid Data'}, status=status.HTTP_400_BAD_REQUEST)
class JoinQuiz(APIView):
"""Join a quiz based on the quiz code"""
lookup_url_kwarg = 'code'
def post(self, request, format=None):
if not self.request.session.exists(self.request.session.session_key):
self.request.session.create()
print(self.request.session.session_key)
code = request.data.get(self.lookup_url_kwarg)
if code != None:
quiz_result = Quiz.objects.filter(code=code)
if len(quiz_result) > 0:
self.request.session['quiz_code'] = code
return Response({'message': 'Quiz Joined!'}, status=status.HTTP_200_OK)
return Response({'Quiz Not Found': 'Invalid Quiz Code'}, status=status.HTTP_404_NOT_FOUND)
return Response({'Bad Request': 'Invalid Post Data'}, status=status.HTTP_400_BAD_REQUEST)
serializers.py
class QuizSerializer(serializers.ModelSerializer):
questions = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Quiz
fields = ['id', 'code', 'questions', 'name']
def create(self, validated_data):
questions_data = validated_data.pop('questions')
quiz = Quiz.objects.create(**validated_data)
for question_data in questions_data:
answers_data = question_data.pop('answers')
question = Question.objects.create(quiz=quiz, **question_data)
for answer_data in answers_data:
Answer.objects.create(question=question, **answer_data)
return quiz
Frontend Request
const quizButtonPressed = () => {
const requestOptions = {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
code: quizCode
})
};
fetch('/api/join-quiz', requestOptions)
.then((response) => {
if (response.ok) {
props.history.push(`/play/${quizCode}`);
} else {
setError("Quiz not found")
}
})
.catch((error) => console.log(error));
}
EDIT
I followed the solution from #Blusky and it worked!
Following the Django docs link from #Blusky solved the problem: https://docs.djangoproject.com/en/3.1/ref/csrf/#ajax
Even though you checked it, it might be a CSRF problem. You should check the body of the 403 Error, it might contains further information.
Only when authenticated, POST request on Django requires a CSRF token (it might be why your first POST is working)
If it's the case, you can check this snippet: https://docs.djangoproject.com/en/3.1/ref/csrf/#ajax
Because of project specificity I have to write my own model validator for Flask-restplus API app. Simply speaking - when validation error occurs, its format and status code (400) is not proper. It should return JSON object with messages in particular format with status code 422.
What I do is more or less this:
ns = api.namespace('somenamespace', description='blabla')
class MyModel(MyBaseModel):
def __init__(self):
self.id = fields.Integer()
self.name = fields.String()
my_model = api.model('MyModel', MyModel())
#api.marshal_list_with(my_model, envelope='json')
#ns.route('/')
class SomeClass(Resource):
#api.expect(my_model, validate=False)
#api.doc(responses={
200: 'Success',
401: 'Authentication Error',
403: 'Requested resource unavailable',
409: 'Conflict, document already exists',
422: 'Validation Error'
})
def post(self):
"""
Save single document in the database.
:return:
"""
request_payload = json.loads(request.data)
validated_payload, payload_errors = some_validation(request_payload)
if payload_errors:
return jsonify(payload_errors), 422
else:
return jsonify({'response': 'ok})
Instance of `MyModel` behaves basically like a dict, so no problem in registration. Problem is that when I send data in `-d`, be it through `curl` from command line, or swagger, I constantly get `400` instead of `422`. I assume this is due to the default, built-in validation of input data based on `MyModel`. This is cool, but I have to omit it, and apply my own validation.
in the documentation and as #CloC says, one method is specifying the model as
your_model = ns.model('YourModel', {
'id': fields.Integer(
description='The user id'
)
'name': fields.String(
description='The user name'
)
})
... > profit
#ns.route('/', methods=["post"])
#ns.doc(params={
"id": "the user id (int)",
"name": "the user name (str)"
})
class SomeClass(Resource):
#ns.expect(your_model) # this defines the request
# #ns.marshal_list_with(your_model, envelope='json') # this defines the response
#ns.response(200, 'Success')
... > reponses
def post(self):
etc...
return <response with format as model in marshal>
you might want to re-define the response model though unless you will return something of the form that you put in.. also maybe specify marshal_with as you do not return a list?
I am creating a REST-API with django and the django rest framework, therefore I am using ReadOnlyModelViewSet. In my unit test for testing create / POST method, I am expecting HTTP_405_METHOD_NOT_ALLOWED but actually it is HTTP_403_FORBIDDEN. For the update / PUT it is HTTP_405 as expected. Is this behavior the default behavior or do I have a dumb bug?
And yes the user is authenticated.
The Viewset:
class StudentSolutionReadOnlyModelViewSet(viewsets.ReadOnlyModelViewSet):
queryset = StudentSolution.objects.all()
serializer_class = StudentSolutionSerializer
def get_permissions(self):
if self.request.method == 'POST':
return (permissions.IsAuthenticated(), )
return (permissions.IsAuthenticated(), IsStudentSolutionOwnerOrAdmin(),)
def get_queryset(self):
if self.request.user.is_staff:
return StudentSolution.objects.all(
course_assignment=self.kwargs['course_assignment_pk']
).order_by('student__last_name', 'course_assignment__due_date')
return StudentSolution.objects.filter(
student=self.request.user,
course_assignment=self.kwargs['course_assignment_pk']
).order_by('course_assignment__due_date')
Edit 1:
class StudentSolutionReadOnlyModelViewSetTests(TestCase):
def setup(self):
#[..]
def test_create_user_not_allowed(self):
data = {
'course_assignment': self.course_assignment.id,
'student': self.user.id
}
url = self.generate_url(self.course_assignment.id)
self.csrf_client.credentials(HTTP_AUTHORIZATION='JWT ' + self.user_token)
resp = self.csrf_client.post(
self.url,
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(resp.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
I am using the same authentication and csrf stuff for all methods, as well as generate_url method.
I have a profile page for my users where they should be able to update their information. For now they can update their names but I also want phonenumbers, addresses, etc.
The code for updating the name of my user is
class AccountPage(BaseRequestHandler):
def get(self):
self.render('accountpage.html', {'request': self.request, 'user': self.current_user,'loggedin': self.logged_in, 'session': self.auth.get_user_by_session(),})
def post(self):
user = self.current_user
user.name = self.request.POST['name']
user.put()
self.auth.set_session(
self.auth.store.user_to_dict(user))
self.render('accountpage.html', {'request': self.request, 'loggedin': self.logged_in,'user': self.current_user})
But how can I use extra variables such as phonenumbers, address variable etc? The webapp2 User model is an expando model. It did not work to just add the variables to the model:
class User(model.Expando):
"""Stores user authentication credentials or authorization ids."""
#: The model used to ensure uniqueness.
unique_model = Unique
#: The model used to store tokens.
token_model = UserToken
created = model.DateTimeProperty(auto_now_add=True)
updated = model.DateTimeProperty(auto_now=True)
# ID for third party authentication, e.g. 'google:username'. UNIQUE.
auth_ids = model.StringProperty(repeated=True)
# Hashed password. Not required because third party authentication
# doesn't use password.
password = model.StringProperty()
phonenumber = model.StringProperty()
address = model.StringProperty()
I use simpleauth and I get this error msg from simpleauth:
INFO 2015-07-20 06:09:34,426 authhandlers.py:78] user_dict | {'name': u'DAC', 'user_id': 5620703441190912, 'token': u'c9BbE72EmrgTDpG1Dl4tlo', 'token_ts': 1437371676, 'cache_ts': 1437371676, 'remember': 0}
ERROR 2015-07-20 06:09:34,437 authhandlers.py:42] 'phonenumber'
INFO 2015-07-20 06:09:34,445 module.py:812] default: "POST /account/ HTTP/1.1" 404 -
INFO 2015-07-20 06:09:34,501 module.py:812] default: "GET /favicon.ico HTTP/1.1" 200 450
In my BaseRequestHandler I have this cached_property that creates an object.
#webapp2.cached_property
def current_user(self):
"""Returns currently logged in user"""
user_dict = self.auth.get_user_by_session()
logging.info('user_dict | %s ' % user_dict)
if user_dict:
return self.auth.store.user_model.get_by_id(user_dict['user_id'])
else:
return api.users.get_current_user()
Then I tried changing the user model but I still get the ERR phone_number when making these changes.
class BaseRequestHandler(webapp2.RequestHandler):
class User(auth_models.User):
address = ndb.StringProperty(indexed=False)
phone_number = ndb.IntegerProperty(indexed=False)
def dispatch(self):
# Get a session store for this request.
self.session_store = sessions.get_store(request=self.request)
if self.request.host.find('.br') > 0:
i18n.get_i18n().set_locale('pt-br')
elif self.request.host.find('klok') > 0:
i18n.get_i18n().set_locale('sv')
elif self.request.host.find('business') > 0:
i18n.get_i18n().set_locale('en')
else:
lang_code_get = self.request.get('hl', None)
if lang_code_get is None:
lang_code = self.session.get('HTTP_ACCEPT_LANGUAGE', None)
lang_code_browser = os.environ.get('HTTP_ACCEPT_LANGUAGE')
if lang_code:
i18n.get_i18n().set_locale(lang_code)
if lang_code_browser and lang_code is None:
self.session['HTTP_ACCEPT_LANGUAGE'] = lang_code_browser
i18n.get_i18n().set_locale(lang_code_browser)
else:
i18n.get_i18n().set_locale(lang_code_get)
try:
# Dispatch the request.
logging.info('trying to dispatch')
webapp2.RequestHandler.dispatch(self)
except Exception, ex:
logging.error(ex)
self.error(404)
finally:
# Save all sessions.
self.session_store.save_sessions(self.response)
#webapp2.cached_property
def jinja2(self):
"""Returns a Jinja2 renderer cached in the app registry"""
return jinja2.get_jinja2(app=self.app)
#webapp2.cached_property
def session(self):
"""Returns a session using the default cookie key"""
return self.session_store.get_session()
#webapp2.cached_property
def auth(self):
return auth.get_auth()
#webapp2.cached_property
def session_store(self):
return sessions.get_store(request=self.request)
#webapp2.cached_property
def auth_config(self):
"""
..........Dict to hold urls for login/logout
......"""
return {'login_url': self.uri_for('login'),
'logout_url': self.uri_for('logout')}
#webapp2.cached_property
def current_user(self):
"""Returns currently logged in user"""
user_dict = self.auth.get_user_by_session()
logging.info('user_dict | %s ' % user_dict)
if user_dict:
return self.auth.store.user_model.get_by_id(user_dict['user_id'])
else:
return api.users.get_current_user()
As mentioned in the comment above - you should NOT be making any changes in any of the built-in libraries, instead, you can extend them and then add any additional code/properties you need.
So first, you'd need to define your own User model, which would look simmilar to this:
from google.appengine.ext import ndb
import webapp2_extras.appengine.auth.models as auth_models
class User(auth_models.User):
address = ndb.StringProperty(indexed=False)
phone_number = ndb.IntegerProperty(indexed=False)
You are only adding the new properties you need or the ones you need to override, so no created / updated / etc as they're inherited from the model you were referring to.
You then need to work with this model inside your BaseRequestHandler class (I'm not sure what the line self.current_user does, you might need to include the code for that as well).
You can also read this article to get some more ideas: http://gosurob.com/post/20024043690/gaewebapp2accounts
my first question here :
So I'm using tastypie to have api's for my app.
I want to be able to use tastypie to render json and then include that in a django view so that I can bootstrap my app's data.
There is an example of this in django tastypie cookbook here : http://django-tastypie.readthedocs.org/en/latest/cookbook.html#using-your-resource-in-regular-views
The problem is that I CANNOT get this to work, I've tried variants from simpler to more complex and I just cant get it, here some code for my models :
class ChatMessage(models.Model):
content = models.TextField()
added = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(ChatUser, related_name="messages")
chat_session = models.ForeignKey(ChatSession, related_name="messages")
answer_to = models.ForeignKey('self', blank=True, null=True)
flagged = models.BooleanField(blank=True,default=False)
mododeleted = models.BooleanField(blank=True,default=False)
mododeleted_by = models.ForeignKey(ChatUser,blank=True,null=True,default=None)
mododeleted_at = models.DateTimeField(blank=True,null=True,default=None)
[...]
class ChatSession (models.Model):
title = models.CharField(max_length=200)
link_title = models.CharField(max_length=200)
description = tinymce_models.HTMLField()
date = models.DateTimeField()
online = models.BooleanField(default=False)
next_session = models.BooleanField(default=False)
meps = models.ManyToManyField(ChatMep)
uid_newsupdate = models.CharField(max_length=200,blank=True,null=True,default="")
[...]
and my resources :
class ChatMessageResource(MyModelResource):
chat_session = fields.ForeignKey(ChatSessionResource, 'chat_session')
def renderOne(self,request,pkval):
data = self.obj_get(None,pk=pkval)
dbundle = self.build_bundle(obj=data,request=request)
return self.serialize(None,self.full_dehydrate(dbundle),'application/json')
def dehydrate(self, bundle):
bundle.data['likes'] = bundle.obj.get_likes()
bundle.data['likes_count'] = len(bundle.data['likes'])
return bundle
class Meta:
authentication = Authentication()
authorization = Authorization()
queryset = ChatMessage.objects.all()
resource_name = 'message'
fields = ('content', 'added', 'flagged', 'mododeleted','author','answer_to','chat_session')
filtering = {
'chat_session': ALL_WITH_RELATIONS,
}
and my view index :
def index(request):
cur_sess = get_current_chat_session()
data1= ChatMessageResource().renderOne(request,723)
return render_to_response('test.html',
{
'all_data' : data1
},
context_instance=RequestContext(request))
What I want is my renderOne() function to give me the json of ONE ChatMessageResource
And also I'd like a renderAll() function to gice me ALL (or filtered) ChatMessageResources in json.
And I want to use tastypie internals, I KNOW i could serialize it by myself but that's not the point..
Right now the error is :
NoReverseMatch at /live/
Reverse for 'api_dispatch_detail' with arguments '()' and keyword arguments '{'pk': 14L, 'resource_name': 'session'}' not found.
I'm just getting crazy, I've been trying for hours.
So please, how to get ONE/ALL resource as JSON by code using tastypie in a django view !
If It's not clear or I need to clarify, please just ask, thanks
Really what I want to do is to be able to get the JSON returned by an API url I created, but from code, not by visiting the url .. So If I have /api/v1/messages/?chat_session=14 which return a list of messages, I want to be able to do the same by code (and not by fetching the url with curl or something please).
Note :
definition of ModelResource.obj_get from https://github.com/toastdriven/django-tastypie/blob/master/tastypie/resources.py
def obj_get(self, request=None, **kwargs):
"""
A ORM-specific implementation of ``obj_get``.
Takes optional ``kwargs``, which are used to narrow the query to find
the instance.
"""
try:
base_object_list = self.get_object_list(request).filter(**kwargs)
object_list = self.apply_authorization_limits(request, base_object_list)
stringified_kwargs = ', '.join(["%s=%s" % (k, v) for k, v in kwargs.items()])
if len(object_list) <= 0:
raise self._meta.object_class.DoesNotExist("Couldn't find an instance of '%s' which matched '%s'." % (self._meta.object_class.__name__, stringified_kwargs))
elif len(object_list) > 1:
raise MultipleObjectsReturned("More than '%s' matched '%s'." % (self._meta.object_class.__name__, stringified_kwargs))
return object_list[0]
except ValueError:
raise NotFound("Invalid resource lookup data provided (mismatched type).")
So here I found the solution, the problem was with url resolving ... I needed to add
def get_resource_uri(self, bundle_or_obj):
return '/api/v1/%s/%s/' % (self._meta.resource_name,bundle_or_obj.obj.id)
to the related object (session here) in order for it to work (don't ask why!)
So here is my working solution for renderDetail and renderList :
def renderDetail(self,pkval):
request = HttpRequest()
request.GET = {'format': 'json'}
resp = self.get_detail(request, pk=pkval)
return resp.content
def renderList(self,options={}):
request = HttpRequest()
request.GET = {'format': 'json'}
if len(options) > 0:
request.GET.update(options)
resp = self.get_list(request)
return resp.content
And here is an example usage :
cmr = ChatMessageResource()
dataOne= cmr.renderDetail("723")
dataAll = cmr.renderList({'limit':'0','chat_session':cur_sess.pk})
https://github.com/toastdriven/django-tastypie/issues/962
I've found that obj_get method needs a bundled request object. See the link.
def user_detail(request, username):
ur = UserResource()
# Add this request bundle to the obj_get() method as shown.
req_bundle = ur.build_bundle(request=request)
user = ur.obj_get(req_bundle, username=username)
....
Your problem seems to be here:
data = self.obj_get(None,pk=pkval)
The parameters to obj_get should be kwargs that can be passed directly to a standard get. None should not be in there.