54

I'm using Django on Appengine. I'm using the django reverse() function everywhere, keeping everything as DRY as possible.

However, I'm having trouble applying this to my client-side javascript. There is a JS class that loads some data depending on a passed-in ID. Is there a standard way to not-hardcode the URL that this data should come from?

var rq = new Request.HTML({
    'update':this.element,
}).get('/template/'+template_id+'/preview'); //The part that bothers me.
Flimm
  • 136,138
  • 45
  • 251
  • 267
noio
  • 5,744
  • 7
  • 44
  • 61
  • 1
    I'd love to see more discussion of this. I, too, think a url-resolution callback is too heavyweight. Has anyone found anything else on this subject? – a paid nerd Aug 11 '10 at 06:54
  • Similar question here: http://stackoverflow.com/questions/1795701/django-reverse-for-javascript – viam0Zah Mar 09 '11 at 09:24
  • Strange I didn't find that one at the time I posted mine. Neither has satisfying answers though heh. – noio Mar 22 '11 at 12:57

10 Answers10

35

There is another method, which doesn't require exposing the entire url structure or ajax requests for resolving each url. While it's not really beautiful, it beats the others with simplicity:

var url = '{% url blog_view_post 999 %}'.replace (999, post_id);

(blog_view_post urls must not contain the magic 999 number themselves of course.)

Anatoly Rr
  • 1,136
  • 11
  • 11
  • 10
    I've been doing this and felt a little dirty, but seeing it here makes me feel a teensy bit better about that. – Josh K Feb 12 '13 at 07:02
  • Keep in mind the expected types of characters for the URL param. If the URL param accepts digits this works, but not if it expects only letters. I would also suggest using some 000, since it shouldn't exist (for an object.id, at least) – jpimentel Oct 22 '13 at 15:41
  • 1
    This is dirty. If you're going to use the url in javascript, better to make it accept GET parameters and then you don't need replace. GET has been a standard for ages and all the javascript frameworks support passing a data structure which will be turned into parameters (escaped properly too unlike this), and reads better. – dalore Mar 21 '14 at 11:17
  • @jpimental If you pass `000` to Django, it is rendered in the URL as `0`. @dalore One problem with using GET parameters instead is REST URLs eg, `/api/model/3/` is a different URL to `/api/model/` and it is convenient and tidy to separate them at the dispatcher. – Chris May 26 '16 at 13:15
  • 1
    but django urls parameters are for django templates. Javascript urls use GET parameters. If you try the otherway you're forced to do ugly dirty hacks like this. You could create a django view that automatically takes the GET parameters and puts them into kwargs so the view doesn't matter if it's a GET arg or a django url arg. Bonus is that GET arguments can be specified in any order. Think of them like python **kwargs, and the url args like *args. And if javascript is accessing your urls, you don't need nice urls – dalore May 31 '16 at 17:58
  • Be careful, this solution is brittle. For example, I had a URL with two keyword parameters, like this: `var url = '{% url "example" foo=1 bar=2 %}'.replace(1, foo).replace(2, bar);` . This broke when `foo` contained the number `2`. – Flimm Nov 24 '20 at 08:15
13

Having just struggled with this, I came up with a slightly different solution.

In my case, I wanted an external JS script to invoke an AJAX call on a button click (after doing some other processing).

In the HTML, I used an HTML-5 custom attribute thus

<button ... id="test-button" data-ajax-target="{% url 'named-url' %}">

Then, in the javascript, simply did

$.post($("#test-button").attr("data-ajax-target"), ... );

Which meant Django's template system did all the reverse() logic for me.

kdopen
  • 8,032
  • 7
  • 44
  • 52
  • +1 This is the approach I was considering before searching for what everyone else was doing. Seems like the ideal solution to me, unless legacy support is an issue. Not sure why it hasn't got more traction here. – oogles Aug 27 '16 at 22:55
  • This is really simple and your example was exactly what I was trying to do – 5uperdan Aug 11 '17 at 10:38
9

The most reasonable solution seems to be passing a list of URLs in a JavaScript file, and having a JavaScript equivalent of reverse() available on the client. The only objection might be that the entire URL structure is exposed.

Here is such a function (from this question).

Community
  • 1
  • 1
noio
  • 5,744
  • 7
  • 44
  • 61
  • [django-render-static](https://github.com/bckohan/django-render-static) provides a highly configurable JavaScript `reverse` function that is guaranteed to be functionally equivalent to Django's `reverse`. – bckohan Apr 21 '21 at 22:17
4

Good thing is to assume that all parameters from JavaScript to Django will be passed as request.GET or request.POST. You can do that in most cases, because you don't need nice formatted urls for JavaScript queries.

Then only problem is to pass url from Django to JavaScript. I have published library for that. Example code:

urls.py

def javascript_settings():
    return {
        'template_preview_url': reverse('template-preview'),
    }

javascript

$.ajax({
  type: 'POST',
  url: configuration['my_rendering_app']['template_preview_url'],
  data: { template: 'foo.html' },
});
Tomasz Wysocki
  • 11,170
  • 6
  • 47
  • 62
4

Similar to Anatoly's answer, but a little more flexible. Put at the top of the page:

<script type="text/javascript">
window.myviewURL = '{% url myview foobar %}';
</script>

Then you can do something like

url = window.myviewURL.replace('foobar','my_id');

or whatever. If your url contains multiple variables just run the replace method multiple times.

Hilton Shumway
  • 582
  • 5
  • 8
2

I like Anatoly's idea, but I think using a specific integer is dangerous. I typically want to specify an say an object id, which are always required to be positive, so I just use negative integers as placeholders. This means adding -? to the the url definition, like so:

url(r'^events/(?P<event_id>-?\d+)/$', events.views.event_details),

Then I can get the reverse url in a template by writing

{% url 'events.views.event_details' event_id=-1 %}

And use replace in javascript to replace the placeholder -1, so that in the template I would write something like

<script type="text/javascript">
var actual_event_id = 123;
var url = "{% url 'events.views.event_details' event_id=-1 %}".replace('-1', actual_event_id);
</script>

This easily extends to multiple arguments too, and the mapping for a particular argument is visible directly in the template.

leifdenby
  • 1,428
  • 1
  • 13
  • 10
  • Actually, I think this is much more dangerous... 1) Negative IDs are now allowed in the URL which means it can be passed in your view; you will need extra logic to check for it 2) "-1" is much more likely to show up in the URL as part of the static string than "999". The reason that "999" works is because there's nothing else in the static part of the URL that's actually "999". It doesn't matter that it's a valid id, as long as it gets replaced with no ambiguity. – user193130 Mar 24 '14 at 16:15
  • 1
    @user193130 If it's an object ID, you should probably be using `get_object_or_404` anyway - the end user will see the same 404 as though the URL pattern didn't accept the negative – Izkata Jul 15 '14 at 23:48
1

I've found a simple trick for this. If your url is a pattern like:

"xyz/(?P<stuff>.*)$"

and you want to reverse in the JS without actually providing stuff (deferring to the JS run time to provide this) - you can do the following:

Alter the view to give the parameter a default value - of none, and handle that by responding with an error if its not set:

views.py

def xzy(stuff=None):
  if not stuff:
    raise Http404
  ... < rest of the view code> ...
  • Alter the URL match to make the parameter optional: "xyz/(?P<stuff>.*)?$"
  • And in the template js code:

    .ajax({ url: "{{ url views.xyz }}" + js_stuff, ... ... })

The generated template should then have the URL without the parameter in the JS, and in the JS you can simply concatenate on the parameter(s).

Danny Staple
  • 7,101
  • 4
  • 43
  • 56
1

Use this package: https://github.com/ierror/django-js-reverse

You'll have an object in your JS with all the urls defined in django. It's the best approach I found so far.

The only thing you need to do is add the generated js in the head of your base template and run a management command to update the generated js everytime you add a url

Karim N Gorjux
  • 2,880
  • 22
  • 29
  • I was displeased with the single purpose and lack of maintenance of that package. Try [django-render-static](https://github.com/bckohan/django-render-static) instead. Super clean, well tested. – bckohan Apr 21 '21 at 23:21
0

One of the solutions I came with is to generate urls on backend and pass them to browser somehow.

It may not be suitable in every case, but I have a table (populated with AJAX) and clicking on a row should take the user to the single entry from this table.

(I am using django-restframework and Datatables).

Each entry from AJAX has the url attached:

class MyObjectSerializer(serializers.ModelSerializer):
    url = SerializerMethodField()
    # other elements

    def get_url(self, obj):
       return reverse("get_my_object", args=(obj.id,))

on loading ajax each url is attached as data attribute to row:

var table = $('#my-table').DataTable({
   createdRow: function ( row, data, index ) {
      $(row).data("url", data["url"])
   }
});

and on click we use this data attribute for url:

table.on( 'click', 'tbody tr', function () {
  window.location.href = $(this).data("url");
} );
Pax0r
  • 2,324
  • 2
  • 31
  • 49
0

I always use strings as opposed to integers in configuring urls, i.e. instead of something like

... r'^something/(?P<first_integer_parameter>\d+)/something_else/(?P<second_integer_parameter>\d+)/' ...

e.g: something/911/something_else/8/

I would replace 'd' for integers with 'w' for strings like so ...

... r'^something/(?P<first_integer_parameter>\w+)/something_else/(?P<second_integer_parameter>\w+)/' ...

Then, in javascript I can put strings as placeholders and the django template engine will not complain either:

...
var url     = `{% url 'myapiname:urlname' 'xxz' 'xxy' %}?first_kwarg=${first_kwarg_value}&second_kwarg=${second_kwarg_value}`.replace('xxz',first_integer_paramater_value).replace('xxy', second_integer_parameter_value);

        var x = new L.GeoJSON.AJAX(url, {
            style: function(feature){ 
...

and the url will remain the same, i.e something/911/something_else/8/. This way you avoid the integer parameters replacement issue as string placeholders (a,b,c,d,...z) are not expected in as parameters

edmakalla
  • 36
  • 5