ChatGPT解决这个技术问题 Extra ChatGPT

Django optional url parameters

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.


l
l0b0

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

Vote for the multiple-routes option. +1
@Yuji -- can't you solve the reversing issue by naming each url pattern?
can we give every view the same name?
@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.
@eugene Yes we can have two urls with same name, reversing will smartly pick up whichever is applicable depending on the args
j
jojo

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.


I think you mixed up project_id and product_id in your code, right?
@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).
@jojo Does that mean a 'project_config/foo/bar' in the 2nd option will automatically pass the {'project_id': 'bar'} kwargs to the view?
J
Jacob Valenta

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


Nested is good. Also, it separates different URL sections in your code more clearly (due to the use of indents)
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.
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.
'flat is better than nested'
J
Juan José Brown

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'
),

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.
This is i was looking for =)
what about the last slash in case project_id is not present?
You can just add a ? after the slash or just include the slash in the project_id pattern
t
tarequeh

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+)/)?$'

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<pk>\\d+)/)?$']
A
AzizAhmad

Django = 2.2

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

f
franciscorode

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')