Home > Blockchain >  How to correctly call url_for and specify path parameters?
How to correctly call url_for and specify path parameters?

Time:05-05

This is the function in views.py:

@application.route("/students/<int:id>", methods=["GET", "POST"])
def students(id):
    return render_template("students.html")

I have used this code on my HTML page:

<a href="{{ url_for('students', id=student.id) }}">{{ student.id }}</a>

But url_for is generating this:

http://127.0.0.1:5000/students?id=1

I want to generate this:

http://127.0.0.1:5000/students/1

Note that I have another function which was written before that first function:

@application.route("/students")
def students():
    return render_template("students.html", students=Student.query.all())

I think url_for is using this route.

CodePudding user response:

The problem is that you have 2 endpoints that have almost the same pattern:

/students
/students/<int:id>

It then becomes a matter of in what order are those endpoints defined, because the call url_for('students', id=student.id) can correctly match on both:

  • If it matches /students first, as explained in url_for docs: "Variable arguments that are unknown to the target endpoint are appended to the generated URL as query arguments.". Since id=1234 is not a known parameter on this endpoint, it becomes a query parameter ?id=1234.
  • If it matches /students/<int:id> first, then you get your expected behavior of /students/1234

Relying on the order of the endpoints is a brittle and error-prone strategy, so if you think you should just reorder the endpoint definitions, no, that's not the way to go.

What I recommend instead are these options:

  1. Rename the functions to help differentiate url_for (and readers of your code) that one endpoint is for getting all the students, and the other one is for accesing just one student by id. Then explicitly call the correct one in url_for:

    @application.route("/students/<int:id>", methods=["GET", "POST"])
    def student_by_id(id):
        return {"student": id}
    
    @application.route("/students")
    def all_students():
        return {"students": "all"}
    
    <a href="{{ url_for('student_by_id', id=student.id) }}">{{ student.id }}</a>
    
  2. Similar to option 1, but instead of renaming the functions, pass-in an endpoint=... keyword parameter to the route(...) decorator to specify different endpoint names. See the docs for the route decorator and the section on URL Route Registrations for more details. By default, "The endpoint name for the route defaults to the name of the view function if the endpoint parameter isn’t passed.". So here, name the endpoint that accepts an id as "student_by_id" and explicitly use that in url_for.

    @application.route(
        "/students/<int:id>", 
        methods=["GET", "POST"], 
        endpoint="student_by_id",  # <---------
    )
    def students(id):
        return {"student": id}
    
    @application.route("/students")
    def students():
        return {"students": "all"}
    
    <a href="{{ url_for('student_by_id', id=student.id) }}">{{ student.id }}</a>
    
  3. Combine both into 1 endpoint. Then the endpoint function should first check if id was specified. If it was, then handle processing for just 1 student. Else, handle processing for all students. The url_for call doesn't need to be changed.

    @application.route("/students")
    @application.route("/students/<int:id>", methods=["GET", "POST"])
    def students(id=None):
        if id:
            return {"student": id}
        else:
            return {"students": "all"}
    
    <a href="{{ url_for('students', id=student.id) }}">{{ student.id }}</a>
    

Pick one which works best for you.

As a side note, as mentioned in the comments, I also recommend not using id as a parameter or variable name, because you might accidentally shadow the built-in id() function. It might be actually be more readable to change it to student_id.

  • Related