Django rest serialization - Passing JSON to Javascript - django

Disclaimer - Django noob, especially REST Framework. I'm trying to create an app that for this purposes passes JSON to the template. I figured Django Rest would be ideal.
I have set up a user profile so the user can select various attributes (works fine), and now want to pass a JSON of all the user's selections to template. Think of it as a "my profile" page.
View:
profile = Profile.objects.filter(user=request.user)
serializer = ProfileSerializer(profile, many=True)
myteam = JsonResponse(serializer.data, safe=False)
print(myteam.content) ## to see what's being passed
context = {'myteam': myteam.content}
return render(request, 'main/myteam.html', context)
Template:
<script>
console.log({{myteam}});
<\script>
Django server output:
b'[{"user": "DAVE", "GK1": {"id": 1001, "ShortName": "J. Strummer", "Club": "CLUB", "Shirt": "SHIRT.png", "ShortClub": "ETC", "ShortPos": "FW", "CurrentVal": "10", "TotalPoints": 1}, "GK2": {"id": 320, "ShortName": "D. Jones", "Club": "CLUB2", "Shirt": "ETABE.png", "ShortClub": "ETABE", "ShortPos": "GK", "CurrentVal": "4.5",
Template - Google Chrome JS console:
Uncaught SyntaxError: Invalid or unexpected token
Template - Chrome details:
console.log(b'[{"user": "mikey", "GK1": {"id": 1001, "ShortName": "J. Strummer", "Club": "ETC", "Shirt": "SHIRT.png", "ShortClub": "ETC", "ShortPos": "FW", "CurrentVal": "10.0", "TotalPoints": 1}, // lot's more of this
Noob Conclusion:
I don't seem to be getting a "clean" JSON object passed to the template from the server. Possibly due to the b'[ .... is this bytes literal?
Perhaps I should be using another method to pass the JSON to the template?
TWIST - in my testing I followed the REST tutorial and was able to setup a simple view which returned JsonResponse:
player = Player.objects.all()
serializer = PlayerSerializer(player, many=True)
return JsonResponse(serializer.data, safe=False)
Now if you go to this url mapping it displays in browser a perfect JSON example. I don't see why there should be a difference between
myteam = JsonResponse(serializer.data, safe=False)
and
return JsonResponse(serializer.data, safe=False)
I've been stuck on this on and off for literally days now so any help would be hugely appreciated.

I believe you may need to do this:
from rest_framework.renderers import JSONRenderer
profile = Profile.objects.filter(user=request.user)
serializer = ProfileSerializer(profile, many=True)
content = JSONRenderer().render(serializer.data)
return JsonResponse(content)
Or this:
from rest_framework.renderers import JSONRenderer
profile = Profile.objects.filter(user=request.user)
serializer = ProfileSerializer(profile, many=True)
return JsonResponse(serializer.data)
Rather than creating a JsonResponse object and passing it back through context.
There is another alternative, however, to the above as you'll want your endpoint to be extremely flexible.
Such API querying functionality is available from a 3rd-party package. I use this a lot, and find it works really well:
pip install djangorestframework-queryfields
Declare your serializer like this:
from rest_framework.serializers import ModelSerializer
from drf_queryfields import QueryFieldsMixin
class MyModelSerializer(QueryFieldsMixin, ModelSerializer):
...
Then the fields can now be specified (client-side) by using query arguments:
GET /identities/?fields=id,data
Exclusion filtering is also possible, e.g. to return every field except id:
GET /identities/?fields!=id

There are a few things going on here.
Firstly, you have taken the rendered output of a response and passed the content back into a template. This isn't the right thing to do; you should skip the JsonResponse altogether and pass serializer.data into a renderer before sending it to the template.
serializer = ProfileSerializer(profile, many=True)
data = JSONRenderer().render(serializer.data)
context = {'myteam': data}
return render(request, 'main/myteam.html', context)
Secondly, the encoding is due to Django templates' automatic HTML escaping. You should mark your JSON as safe.
console.log({{ myteam|safe }});

Related

Test cases for Django Rest Framework; struggling to get a correct response

Update: Solved my own problem: I've learnt that Django creates its own test database, and as such, it needs to be populated with data. Ran my importer in my test cases and it all worked. So, if you're also wondering why you're tests don't work, check that you've got some data in the test db!
End Update
I am writing tests for my Django Rest Framework API but I am struggling to get my code to return a 200 OK. At the moment, my test case continually returns a 404 Not Found.
I'm in the early stages of writing tests, and have a lot to learn. I'm currently following https://www.django-rest-framework.org/api-guide/testing/
I'm trying to test an endpoint at the following URL
# Not shown here, is that all URLs here will be prepended with /api/v1
path('case/<int:pk>/', EntireCaseView.as_view(), name='case'),
I have an object in my database with an ID (primary key) of 1. I can successful query the API by going to http://localhost:8000/api/v1/case/1/
I receive a valid JSON response (Trampe is a rabbit)
{
"id": 1,
"total_points": 5000,
"passing_points": 3700,
"budget": 5000,
"description": "Saving Trampe from Trauma",
"name": "Trampe",
"signalment": "8yr, intact male, mixed breed.",
"problem": "Respiratory difficulty",
"image": {
"id": 1,
"file": "http://localhost:8000/media/images/trampe.jpg",
"description": "A lovely picture of Trampe"
},
My API requires authentication, and as such I am providing authentication in my test case.
class CaseTests(APITestCase):
def test_status_code(self):
"""
ensure that case/1 returns 200 OK
"""
# Create a test user
test_user = User(username='jim', password='monkey123', email='jim#jim.com')
test_user.save()
# build a factory and get our user Jim
factory = APIRequestFactory()
user = User.objects.get(username='jim')
# Get our view to test and the url, too
view = EntireCaseView.as_view()
url = reverse('case', kwargs={'pk': '1'})
print(url.__str__())
# Make an authenticated request to the view...
request = factory.get(url)
print(request.get_full_path())
force_authenticate(request, user=user)
response = view(request, "1")
print(response.data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
Of interest here (at least to me) are the lines
url = reverse('case', kwargs={'pk': '1'})
and
response = view(request, "1")
If I leave out either the kwargs argument in url =r everse('case', kwargs={'pk': '1'}) or the "1" in response = view(request, "1") I will receive an error saying that the get() method requires 2 positional arguments but only given.
Here is the signature of the get() method in my view.
class EntireCaseView(APIView):
def get(self, request, pk):
If I run my test, Django reports that it fails because of a 404.
self.assertEqual(response.status_code, status.HTTP_200_OK)
AssertionError: 404 != 200
What I am trying to work out is why this is the case. Printing print(url.__str__()) outputs /api/v1/case/1/ as does print(request.get_full_path())
So in summary, I am trying to understand why I'm receiving this 404, and ultimately, how I can test this, and other endpoints.
Any and all help is appreciated.
Cheers,
C

What is the difference between using Django form and manually setting date fields?

I am getting date/time info from ajax to Django. I am using 2 different views. event_edit is working fine, but event_edit_new does not work. It return an error Enter a valid date/time.
My question is what is making difference. They are getting exactly same information but one is ok while other one is not.
Javascript code making ajax request:
var ajax_test = function(event){
$.ajax({
url: '/scheduler/event/' + event.eventId + '/edit2/',
method: 'POST', // 'POST' or 'PUT'
data: {
'Note': event.title,
'OpNum': event.resourceId,
'StartTime' : event.start.format(),
'StopTime' : event.end.format(),
'ScheduleNum': event.eventId,
}
}).done(function(res) {
console.log("done", res)
}).fail(function(error) {
console.error("error", error)
console.log(event.start.format());
});
}
2 views
def event_edit(request, pk):
schedule = get_object_or_404(Schedule, pk=pk)
schedule.OpNum = request.POST.get('OpNum')
schedule.Note = request.POST.get('Note')
schedule.StartTime = request.POST.get('StartTime')
schedule.StopTime = request.POST.get('StopTime')
schedule.ScheduleNum =request.POST.get('ScheduleNum')
schedule.save()
return HttpResponse('ok')
def event_edit_new(request, pk):
schedule = get_object_or_404(Schedule, pk=pk)
if request.method =='POST':
form = ScheduleForm(request.POST, request.FILES, instance = schedule)
if form.is_valid():
form.save()
return HttpResponse('ok')
else:
return HttpResponse('error')
else:
return HttpResponse('done')
In your first view, there is no validation applied to user data input:
schedule.StartTime = request.POST.get('StartTime')
schedule.StopTime = request.POST.get('StopTime')
request.POST does not validate any data so that will grab user data exactly as submitted and set it on a model. Note that it might work but it is not guaranteed to work if user will ever send datetime format the application does not understand. For example try submitting something like "invalid date" and you should get 500 error.
Your second view on the other hand uses a Django Form to validate user input. By using a form you are validating user input before processing it in any way. You did not paste how your form is structured however if you are getting Enter a valid date/time. this means Django form did not validate one of the datetime fields. There are couple of reasons why that might be:
submitted datetime does is not one of input_formats for a DateTimeField. You can customize the formats with:
class MyForm(forms.Form):
datetime_field = forms.DateTimeField(input_formats=['%Y-%m-%dT%H:%M:%SZ', ...])
submitted datetime is not one of Django's default supported datetime formats.
You can customize them with DATETIME_INPUT_FORMATS setting.

Getting response status in Django rest framework renderer class

I have implemented my custom Renderer like this:
from rest_framework.renderers import JSONRenderer
class CustomJSONRenderer(JSONRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
//I am hardcoding status and message for now. Which I have to update according to the response.
data = {'data': data, 'message':'ok', 'status':200 }
return super(CustomJSONRenderer, self).render(data, accepted_media_type, renderer_context)
This is working pretty good. Now I want to update status using HTTP status code of response and thus providing a custom message. So how should I achieve this?
Basically I want the response like this:
{"status":200, "data":[actual data comes here.], "message":"ok"}
Well on a different note I found out that we can get the status information. The renderer_context parameter actually contains the following information-
{'view': <ViewSet object at 0x7ff3dcc3fac0>, 'args': (), 'kwargs': {}, 'request': <rest_framework.request.Request object at 0x7ff3dcc37e20>, 'response': <Response status_code=400, "application/json; charset=utf-8">}
This means the renderer_context parameter is a dictionary and can be exploited in order to modify your response. For example -
def render(self, data, accepted_media_type=None, renderer_context=None):
if renderer_context is not None:
print(renderer_context['response'].status_code)
This is not what renderers are made for. You should use a renderer to transform the response into a certain format (json, html, csv, etc) according to the request. By default it will use the Acceptheader, but you could image to pass a querystring parameter to force a different output.
I think what you are trying to do is a custom error exception http://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling
Hope this helps

django api post without bring json still work?

Because django rest framework did not support bulk create
So I write one
And I found a strange problem
if I POST the api with a json like :
[{'address':'1','name':'2','start':'3'},
{'address':'10','name':'20','start':'30'}]
it works!
But if I kust POST the api without bring json
I still got bulk create success message.
Why would this happen??
Where do I write wrong??
This is my API view
class BulkTestList(APIView):
def post(self, request, format=None):
duplicateList = []
for data in request.data:
message = {}
if not 'address' in data.keys():
message['address'] = [ "This field is required."]
elif not data['address']:
message["address"] = [ "This field may not be blank."]
if not 'name' in data.keys():
message["name"] = [ "This field is required."]
elif not data['name']:
message["name"]= [ "This field may not be blank."]
if not 'star' in data.keys():
message["star"] = [ "This field is required."]
elif not data['star']:
message["star"]= [ "This field may not be blank."]
if message:
return Response(message, status=status.HTTP_400_BAD_REQUEST)
for data in request.data:
address = data['address'].upper()
bulkCreateObjects = Data(address=address, name=data['name'], star=data['star'], datetime=datetime.datetime.now(pytz.utc))
bulkCreateObjects.save()
message = {"bulk create success"}
return Response(data=message, status=status.HTTP_201_CREATED)
Django REST framework doesn't have bulk out of the box but you have a third party app that does.
Your current view just doesn't call a serializer therefore you won't get the validation at any point. See http://www.django-rest-framework.org/tutorial/3-class-based-views/#rewriting-our-api-using-class-based-views
Note that for bulk you'll add a many=True to the serializer so it will be able to deal with list of data.
Your issue that the view returns a 201 "{bulk create success}" is due to the fact that you iteration over request.data does not check if reuqest.data is actually empty. A for loop over an empty list will just skip over the for block. As Linovia mentions, you need to add some validation to your view.

Convert POST to PUT with Tastypie

Full Disclosure: Cross posted to Tastypie Google Group
I have a situation where I have limited control over what is being sent to my api. Essentially there are two webservices that I need to be able to accept POST data from. Both use plain POST actions with urlencoded data (basic form submission essentially).
Thinking about it in "curl" terms it's like:
curl --data "id=1&foo=2" http://path/to/api
My problem is that I can't update records using POST. So I need to adjust the model resource (I believe) such that if an ID is specified, the POST acts as a PUT instead of a POST.
api.py
class urlencodeSerializer(Serializer):
formats = ['json', 'jsonp', 'xml', 'yaml', 'html', 'plist', 'urlencoded']
content_types = {
'json': 'application/json',
'jsonp': 'text/javascript',
'xml': 'application/xml',
'yaml': 'text/yaml',
'html': 'text/html',
'plist': 'application/x-plist',
'urlencoded': 'application/x-www-form-urlencoded',
}
# cheating
def to_urlencoded(self,content):
pass
# this comes from an old patch on github, it was never implemented
def from_urlencoded(self, data,options=None):
""" handles basic formencoded url posts """
qs = dict((k, v if len(v)>1 else v[0] )
for k, v in urlparse.parse_qs(data).iteritems())
return qs
class FooResource(ModelResource):
class Meta:
queryset = Foo.objects.all() # "id" = models.AutoField(primary_key=True)
resource_name = 'foo'
authorization = Authorization() # only temporary, I know.
serializer = urlencodeSerializer()
urls.py
foo_resource = FooResource
...
url(r'^api/',include(foo_resource.urls)),
)
In #tastypie on Freenode, Ghost[], suggested that I overwrite post_list() by creating a function in the model resource like so, however, I have not been successful in using this as yet.
def post_list(self, request, **kwargs):
if request.POST.get('id'):
return self.put_detail(request,**kwargs)
else:
return super(YourResource, self).post_list(request,**kwargs)
Unfortunately this method isn't working for me. I'm hoping the larger community could provide some guidance or a solution for this problem.
Note: I cannot overwrite the headers that come from the client (as per: http://django-tastypie.readthedocs.org/en/latest/resources.html#using-put-delete-patch-in-unsupported-places)
I had a similar problem on user creation where I wasn't able to check if the record already existed. I ended up creating a custom validation method which validated if the user didn't exist in which case post would work fine. If the user did exist I updated the record from the validation method. The api still returns a 400 response but the record is updated. It feels a bit hacky but...
from tastypie.validation import Validation
class MyValidation(Validation):
def is_valid(self, bundle, request=None):
errors = {}
#if this dict is empty validation passes.
my_foo = foo.objects.filter(id=1)
if not len(my_foo) == 0: #if object exists
foo[0].foo = 'bar' #so existing object updated
errors['status'] = 'object updated' #this will be returned in the api response
return errors
#so errors is empty if object does not exist and validation passes. Otherwise object
#updated and response notifies you of this
class FooResource(ModelResource):
class Meta:
queryset = Foo.objects.all() # "id" = models.AutoField(primary_key=True)
validation = MyValidation()
With Cathal's recommendation I was able to utilize a validation function to update the records I needed. While this does not return a valid code... it works.
from tastypie.validation import Validation
import string # wrapping in int() doesn't work
class Validator(Validation):
def __init__(self,**kwargs):
pass
def is_valid(self,bundle,request=None):
if string.atoi(bundle.data['id']) in Foo.objects.values_list('id',flat=True):
# ... update code here
else:
return {}
Make sure you specify the validation = Validator() in the ModelResource meta.