225

I have a Django URL like this:

url(
    r'^project_config/(?P<product>\w+)/(?P<project_id>\w+)/$',
    'tool.views.ProjectConfig',
    name='project_config'
),

views.py:

def ProjectConfig(request, product, project_id=None, template_name='project.html'):
    ...
    # do stuff

The problem is that I want the project_id parameter to be optional.

I want /project_config/ and /project_config/12345abdce/ to be equally valid URL patterns, so that if project_id is passed, then I can use it.

As it stands at the moment, I get a 404 when I access the URL without the project_id parameter.

Sunderam Dubey
  • 1
  • 11
  • 20
  • 40
Darwin Tech
  • 18,449
  • 38
  • 112
  • 187

7 Answers7

466

There are several approaches.

One is to use a non-capturing group in the regex: (?:/(?P<title>[a-zA-Z]+)/)?
Making a Regex Django URL Token Optional

Another, easier to follow way is to have multiple rules that matches your needs, all pointing to the same view.

urlpatterns = patterns('',
    url(r'^project_config/$', views.foo),
    url(r'^project_config/(?P<product>\w+)/$', views.foo),
    url(r'^project_config/(?P<product>\w+)/(?P<project_id>\w+)/$', views.foo),
)

Keep in mind that in your view you'll also need to set a default for the optional URL parameter, or you'll get an error:

def foo(request, optional_parameter=''):
    # Your code goes here
l0b0
  • 55,365
  • 30
  • 138
  • 223
Yuji 'Tomita' Tomita
  • 115,817
  • 29
  • 282
  • 245
  • 89
    Vote for the multiple-routes option. +1 – Burhan Khalid Jan 16 '13 at 04:25
  • @BurhanKhalid thanks Burhan! Yeah, I prefer that method too... it's just easier to follow 1 year later. The only downside I see is in URL reversing. – Yuji 'Tomita' Tomita Jan 16 '13 at 04:29
  • 4
    @Yuji -- can't you solve the reversing issue by naming each url pattern? – Ted Jan 16 '13 at 04:44
  • Thankyou. Comprehensive answer. I like the second method as it just a little clearer what I am trying to achieve. – Darwin Tech Jan 16 '13 at 04:51
  • @Ted, yes but you'd have to know which type of URL you are reversing... or does the framework handle reversing multiple URLs with the same name? – Yuji 'Tomita' Tomita Jan 16 '13 at 05:15
  • @DarwinTech, I agree. The second one is much more explicit. 3 years later, you will know exactly what's going on. Python magic / coolness doesn't apply sometimes.. :) – Yuji 'Tomita' Tomita Jan 16 '13 at 05:16
  • 10
    can we give every view the same name? – eugene Jan 22 '14 at 07:48
  • @eugene It looks like only the last one you define matters :/ – nnyby Jul 29 '14 at 19:07
  • @nnyby wrong. each URL pattern is parsed, and the first one that matches is returned. – Yuji 'Tomita' Tomita Jul 29 '14 at 20:09
  • @Yuji'Tomita'Tomita oh interesting. This tends to cause problems because I had two patterns defined with the same name, but different parameters. See: http://dpaste.com/00PQQXN – nnyby Jul 29 '14 at 22:04
  • @nnyby, your problem is most likely due to the same name issue. – Yuji 'Tomita' Tomita Jul 30 '14 at 00:53
  • 2
    @Yuji'Tomita'Tomita I know, so the answer to eugene's question is unfortunately, no we can't sanely have multiple views with the same name, even if we're implementing them as a way to get optional parameters. – nnyby Jul 30 '14 at 02:11
  • The downside is maintaining redundant regex (especially if they are complex). Is there a security issue with using regex stored in variables and concatenating them in the url pattern? – user Aug 20 '14 at 17:08
  • 3
    @eugene Yes we can have two urls with same name, reversing will smartly pick up whichever is applicable depending on the args – Arpit Singh Mar 16 '15 at 08:27
  • In order to be able to use reverse names in templates with `{% url 'view_name' param1 param2 param3 %}` regardless of param2 or param3 being defined, I've used `*` instead of `+` in urls.py. The downside is that rendered link is something like `path///` (in case param2 and param3 are undefined) but it saves a lot of `if`s – Filipe Pina Jun 26 '17 at 12:39
  • That's repetition. I have 10 things like project _config, that will fill up my file. – Vishesh Mangla Jul 07 '20 at 21:39
60

Django > 2.0 version:

The approach is essentially identical with the one given in Yuji 'Tomita' Tomita's Answer. Affected, however, is the syntax:

# URLconf
...

urlpatterns = [
    path(
        'project_config/<product>/',
        views.get_product, 
        name='project_config'
    ),
    path(
        'project_config/<product>/<project_id>/',
        views.get_product,
        name='project_config'
    ),
]


# View (in views.py)
def get_product(request, product, project_id='None'):
    # Output the appropriate product
    ...

Using path() you can also pass extra arguments to a view with the optional argument kwargs that is of type dict. In this case your view would not need a default for the attribute project_id:

    ...
    path(
        'project_config/<product>/',
        views.get_product,
        kwargs={'project_id': None},
        name='project_config'
    ),
    ...

For how this is done in the most recent Django version, see the official docs about URL dispatching.

j-i-l
  • 10,281
  • 3
  • 53
  • 70
  • 2
    I think you mixed up project_id and product_id in your code, right? – Andreas Bergström Apr 15 '19 at 16:49
  • @AndreasBergström thanks a lot for pointing that out! you are quite right about this! Corrected it in a hurry, but will have a 2nd look at it later. Hope it is fine now! There was also the `project_id` still in the path in case of the default using a `dict`. This can lead to seemingly weird behavior, as the argument provided in the `dict` will always be used (if I remember correctly). – j-i-l Apr 15 '19 at 17:00
  • 1
    @jojo Does that mean a 'project_config/foo/bar' in the 2nd option will automatically pass the {'project_id': 'bar'} kwargs to the view? – Original BBQ Sauce Jul 22 '19 at 16:02
37

You can use nested routes

Django <1.8

urlpatterns = patterns(''
    url(r'^project_config/', include(patterns('',
        url(r'^$', ProjectConfigView.as_view(), name="project_config")
        url(r'^(?P<product>\w+)$', include(patterns('',
            url(r'^$', ProductView.as_view(), name="product"),
            url(r'^(?P<project_id>\w+)$', ProjectDetailView.as_view(), name="project_detail")
        ))),
    ))),
)

Django >=1.8

urlpatterns = [
    url(r'^project_config/', include([
        url(r'^$', ProjectConfigView.as_view(), name="project_config")
        url(r'^(?P<product>\w+)$', include([
            url(r'^$', ProductView.as_view(), name="product"),
            url(r'^(?P<project_id>\w+)$', ProjectDetailView.as_view(), name="project_detail")
        ])),
    ])),
]

This is a lot more DRY (Say you wanted to rename the product kwarg to product_id, you only have to change line 4, and it will affect the below URLs.

Edited for Django 1.8 and above

Jacob Valenta
  • 6,659
  • 8
  • 31
  • 42
  • 1
    Nested is good. Also, it separates different URL sections in your code more clearly (due to the use of indents) – Patrick Mar 29 '14 at 01:24
  • The problem with nested is if you have multiple optional parameters, then you end up not being DRY, since with, for example, 3 optional parameters, you have 8 different combinations of possible URLs. You have to handle parameter 1 occurring, parameter 1 not occurring but parameter 2 occurring, and parameter's 1 and 2 not occurring but parameter 3 occurring. The URL paragraph will be MUCH harder to read than a single string with multiple optional parameters. Using symbolic constants for the optional parameter substrings would make it very easy to read, and there would be just one URL. – Bogatyr Sep 29 '15 at 17:55
  • I think you're right, but that's more a result of poor view/URL design. This example could be reworked to be lot better. – Jacob Valenta Sep 29 '15 at 18:00
  • 'flat is better than nested' – pjdavis Jul 07 '18 at 19:53
34

Even simpler is to use:

(?P<project_id>\w+|)

The "(a|b)" means a or b, so in your case it would be one or more word characters (\w+) or nothing.

So it would look like:

url(
    r'^project_config/(?P<product>\w+)/(?P<project_id>\w+|)/$',
    'tool.views.ProjectConfig',
    name='project_config'
),
Juan José Brown
  • 1,196
  • 1
  • 10
  • 7
  • 10
    I like the simplicity of this solution, but beware: by doing so, the view will still receive a value for the argument, which will be `None`. Meaning that you can't rely on a default value in the view's signature for this: you have to explicitly test it inside and assign in consequence. – Anto Jul 17 '14 at 23:02
  • 1
    This is i was looking for =) – Mike Brian Olivera Feb 11 '17 at 20:15
  • 3
    what about the last slash in case project_id is not present? – iamkhush Jun 13 '17 at 10:39
  • You can just add a ? after the slash or just include the slash in the project_id pattern – Juan José Brown Sep 05 '17 at 19:13
9

Thought I'd add a bit to the answer.

If you have multiple URL definitions then you'll have to name each of them separately. So you lose the flexibility when calling reverse since one reverse will expect a parameter while the other won't.

Another way to use regex to accommodate the optional parameter:

r'^project_config/(?P<product>\w+)/((?P<project_id>\w+)/)?$'
tarequeh
  • 1,799
  • 18
  • 18
  • 4
    In Django 1.6 this throws an exception for me. I'd stay away from it `Reverse for 'edit_too_late' with arguments '()' and keyword arguments '{'pk': 128}' not found. 1 pattern(s) tried: ['orders/cannot_edit/((?P\\d+)/)?$']` – Patrick Mar 29 '14 at 01:22
5

Django = 2.2

urlpatterns = [
    re_path(r'^project_config/(?:(?P<product>\w+)/(?:(?P<project_id>\w+)/)/)?$', tool.views.ProjectConfig, name='project_config')
]
AzizAhmad
  • 637
  • 1
  • 9
  • 20
1

Use ? work well, you can check on pythex. Remember to add the parameters *args and **kwargs in the definition of the view methods

url('project_config/(?P<product>\w+)?(/(?P<project_id>\w+/)?)?', tool.views.ProjectConfig, name='project_config')