29

I'm using Django to create a web-based app for a project, and I'm running into issues returning an array from a Django view to a template.

The array will be used by a JavaScript (JQuery) script for drawing boxes on an image shown in the page. Therefore, this array will have, among other things, coordinates for the boxes to be drawn.

This is the code in the Django view used to get the required data and serialize it as JSON:

def annotate(request, ...):
    ...
    oldAnnotations = lastFrame.videoannotation_set.filter(ParentVideoLabel=label)
    tags = serializers.serialize("json", oldAnnotations)
    ...
    return render_to_response('vannotate.html', {'tags': tags, ...})

As a way of debugging, using {{ tags }} in the HTML portion of the template gives this as output (sorry for the long line):

[{"pk": 491, "model": "va.videoannotation", "fields": {"ParentVideoFile": 4, "ParentVideoFrame": 201, "ParentVideoLabel": 4, "top": 220, "height": 30, "width": 30, "ParentVideoSequence": 16, "left": 242}}, {"pk": 492, "model": "va.videoannotation", "fields": {"ParentVideoFile": 4, "ParentVideoFrame": 201, "ParentVideoLabel": 4, "top": 218, "height": 30, "width": 30, "ParentVideoSequence": 16, "left": 307}}]

which I assume is the correct format for a JSON array.

Later on in the template, I attempt to actually use the tags variable in the JavaScript portion of the template, as follows:

{% if tags %}
  var tagbs = {{ tags|safe }};
  var tgs = JSON.parse(tagbs);
  alert("done");
{% endif %}

If I remove the var tgs = JSON.parse(tagbs); line, then the alert box pops up fine, and the rest of the JavaScript works as expected. Leaving this line in breaks the script, however.

I want to be able to iterate through all the objects in the Django model and get values of fields in JavaScript.

I'm not sure what I'm doing wrong here, could someone point out the right way to do this?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
DefPlayr
  • 295
  • 1
  • 3
  • 6
  • 1
    You are not sending the data to JavaScript, you are injecting on the server side into the script, and hence the code is interpreted as JavaScript on the client. If you have a look at the source, you will see that it is a normal array literal. It is not JSON in this context, so you cannot parse it. – Felix Kling Mar 23 '13 at 22:36

3 Answers3

44

Edit with update for Django 2.1+ and the modern web:

The modern way to do this is:

1) Pass the raw data to the template, not the JSON-serialised data. I.e.:

def annotate(request, ...):
    ...
    oldAnnotations = lastFrame.videoannotation_set.filter(ParentVideoLabel=label)
    ...
    return render_to_response('vannotate.html', {'tags': oldAnnotations, ...})

2) In your template, use the new "json_script" filter to include the JSON data:

{{ tags|json_script:"tags-data" }}

That will result in HTML that looks like this:

<script id="tags-data" type="application/json">{"foo": "bar"}</script>

This tag has special handling of strings containing "</script>" to make sure they work.

3) In your Javascript code, get that tags data like this:

var tags = JSON.parse(document.getElementById('tags-data').textContent);

4) Move your Javascript code to an external .js file, and set up the Content-Security-Policy header to prohibit inline Javascript because it's a security risk. Note that since the json_script tag generates JSON, not Javascript, it's safe and is allowed regardless of your Content-Security-Policy setting.

Original Answer:

WARNING: If any of the strings are user-controlled, this is insecure

JSON is Javascript source code. I.e. the JSON representation of an array is the Javascript source code you need to define the array.

So after:

var tagbs = {{ tags|safe }};

tagbs is a JavaScript array containing the data you want. There's no need to call JSON.parse(), because the web browser already parsed it as JavaScript source code.

So you should be able to do

var tagbs = {{ tags|safe }};
alert(tagbs[0].fields.ParentVideoFile);

and that should show "4".

WARNING: With this old method, strings containing "</script>" will not work, they will go horribly wrong. This is because the browser will treat </script> as the end of the script. If any of the strings are user-entered data, this is an exploitable security flaw - see comment 14 here for more details. Use the more modern method above, instead.

user9876
  • 10,954
  • 6
  • 44
  • 66
  • 2
    Man, I love StackOverflow. That little fix solved everything! I'm new to this, I thought JSON.parse was to parse a JSON variable to an array, not the other way around. Thanks a bunch! – DefPlayr Mar 23 '13 at 22:49
  • You should not use `|safe` here; that is only needed for HTML sanitizing. JSON already made this safe JavaScript, any HTML content should *not* be escaped. – Martijn Pieters Mar 23 '13 at 22:51
  • 11
    `|safe` means that Django SHOULDN'T try to HTML-escape it - it's already safe. Without that, Django will replace `"` with `"` and it'll break. – user9876 Mar 24 '13 at 00:52
  • For a long JSON string Django seems to be truncating strings – CantGetANick Aug 05 '15 at 07:53
  • @CantGetANick: Please can you post your problem as a separate question? – user9876 Aug 06 '15 at 10:24
  • This does not(or no longer) works with unicode vars, so you end up with `[u'a', u'b']` which is invalid javascript. – Mike Shultz Mar 10 '16 at 16:22
  • 1
    @Mike: Note that in the question, "tags" already contains JSON (i.e. it's the output of `serializers.serialize("json", ...)` or `json.dumps(...)`). I think you're passing a raw Python array, which won't work. Try doing `json.dumps(...)` in your Python code and passing that. If that doesn't work, try creating a separate new question. – user9876 Apr 01 '16 at 00:06
  • 1
    This may result in invalid JS syntax if the JSON representation contains "", like ```var tagbs = {"k": 12, "p": ""};```. – Vsevolod Kulaga Aug 08 '19 at 20:55
  • @VsevolodKulaga, yes, you're right, this old answer contained a bug which was also an exploitable security vulnerability. However, at the time, there was no simple and safe way to do it (and I wasn't aware of that vulnerability). I've edited in the modern, safe way to do it. – user9876 Aug 27 '19 at 08:34
4

You want to JSON-ify the data in the template; JSON is already Javascript really (it's a subset:

{% if tags %}
  var tgs = {{ tags }};
{% endif %}

Note that tags is already JSON (thus JavaScript) data and can be inserted directly; no need to escape (there is no HTML here, it's JavaScript instead).

Or you could use this Django snippet and do it straight in the template (no need to call serializers.serialize in the annotate method):

var tgs = {{ tags|jsonify }};
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Am I supposed to put the `jsonify` filter definition in the `views` file? I tried following the instructions on the link, but Django tells me the `jsonify` filter is invalid. – DefPlayr Mar 23 '13 at 22:59
  • @user2203255: put it in a new `templatetags` sub-package (a directory with `__init__.py` file) next to your `models.py` and `views.py`, see https://docs.djangoproject.com/en/dev/howto/custom-template-tags/ – Martijn Pieters Mar 23 '13 at 23:02
  • Great, that works too! And it does simplify things later on. One thing, though. I had to add an extra pipe to `safe` (i.e. `{{ tags|jsonify|safe }}`) to get it to work. Perhaps it's something to do with my object itself. – DefPlayr Mar 23 '13 at 23:16
  • I had to JSON.parse the returned jsonified string: var packet_json={{the_packet|jsonify|safe}}; alert(packet_json); var tgs = JSON.parse(packet_json); for (e in tgs['results']) { alert("First: "+e); } – jonincanada Dec 10 '13 at 21:04
  • @jonincanada: `JSON` is a subset of JavaScript; you should not have to parse it, it can be used directly. – Martijn Pieters Dec 10 '13 at 21:38
2

You can also use simplejson from django.utils. Like:

oldAnnotations = lastFrame.videoannotation_set.filter(ParentVideoLabel=label)
dump = simplejson.dumps(oldAnnotations)

return HttpResponse(dump, mimetype='application/json')

You can parse and reach all data in this from JS side.

alioguzhan
  • 7,657
  • 10
  • 46
  • 67
  • 4
    That is deprecated in [Django 1.5](https://docs.djangoproject.com/en/dev/releases/1.5/#system-version-of-simplejson-no-longer-used) – Ngenator Mar 24 '13 at 00:44