I'm trying to implement a redirecting pattern, similar to what StackOverflow does:
@route('/<int:id>/<username>/') @route('/<int:id>/') def profile(id, username=None): user = User.query.get_or_404(id) if user.clean_username != username: return redirect(url_for('profile', id=id, username=user.clean_username)) return render_template('user/profile.html', user=user)
Here's a simple table of what should happen:
URL Redirects/points to ==================================================== /user/123 /user/123/clean_username /user/123/ /user/123/clean_username /user/123/foo /user/123/clean_username /user/123/clean_username /user/123/clean_username /user/123/clean_username/ /user/123/clean_username/ /user/125698 404
Right now, I can access the profile with /user/1/foo
, but /user/1
produces a BuildError
. I've tried the alias=True
keyword argument and something with defaults
, but I'm not quite sure what isn't working.
How would I have one route redirect to the other like this?
Update: to address the primary question "what's wrong with my routes", the simplest way to debug that is to use app.url_map
; e.g:
>>> app.url_map Map([<Rule '/user/<id>/<username>/' (HEAD, OPTIONS, GET) -> profile>, <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>, <Rule '/user/<id>/' (HEAD, OPTIONS, GET) -> profile>])
In this case, this confirms that the endpoint is correctly set. Here is an example showcasing both plain flask
and flask-classy
:
from app import app, models from flask import g, redirect, url_for, render_template, request from flask.ext.classy import FlaskView, route @app.route('/user/<int:id>', strict_slashes=False) @app.route('/user/<int:id>/<username>', strict_slashes=False) def profile(id, username=None): user = models.User.query.get_or_404(id) if user.clean_username != username: return redirect(url_for('profile', id=id, username=user.clean_username)) return render_template('profile.html', user=user) class ClassyUsersView(FlaskView): @route('/<int:id>', strict_slashes=False) @route('/<int:id>/<username>', strict_slashes=False, endpoint='classy_profile') def profile(self, id, username=None): user = models.User.query.get_or_404(id) if user.clean_username != username: return redirect(url_for('classy_profile', id=id, username=user.clean_username)) return render_template('profile.html', user=user) ClassyUsersView.register(app)
They have different endpoints, which you need to take into account for url_for
:
>>> app.url_map Map([<Rule '/classyusers/<id>/<username>' (HEAD, OPTIONS, GET) -> classy_profile>, <Rule '/user/<id>/<username>' (HEAD, OPTIONS, GET) -> profile>, <Rule '/classyusers/<id>' (HEAD, OPTIONS, GET) -> ClassyUsersView:profile_1>, <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>, <Rule '/user/<id>' (HEAD, OPTIONS, GET) -> profile>])
Without flask-classy
the name of the endpoint is the function name, but as you've found out, this is different for when using classy
, and you can either look at the endpoint name with url_map()
or assign it in your route with @route(..., endpoint='name')
.
To respond to the urls you posted while minimizing the amount of redirects, you need to use strict_slashes=False
, this will make sure to handle requests that are not terminated with a /
instead of redirecting them with a 301
redirect to their /
-terminated counterpart:
@app.route('/user/<int:id>', strict_slashes=False) @app.route('/user/<int:id>/<username>', strict_slashes=False) def profile(id, username=None): user = models.User.query.get_or_404(id) if user.clean_username != username: return redirect(url_for('profile', id=id, username=user.clean_username)) return render_template('profile.html', user=user)
here is the result:
>>> client = app.test_client() >>> def check(url): ... r = client.get(url) ... return r.status, r.headers.get('location') ... >>> check('/user/123') ('302 FOUND', 'http://localhost/user/123/johndoe') >>> check('/user/123/') ('302 FOUND', 'http://localhost/user/123/johndoe') >>> check('/user/123/foo') ('302 FOUND', 'http://localhost/user/123/johndoe') >>> check('/user/123/johndoe') ('200 OK', None) >>> check('/user/123/johndoe/') ('200 OK', None) >>> check('/user/125698') ('404 NOT FOUND', None)
Behavior of strict_slashes
:
with strict_slashes=False URL Redirects/points to # of redirects =========================================================================== /user/123 302 /user/123/clean_username 1 /user/123/ 302 /user/123/clean_username 1 /user/123/foo 302 /user/123/clean_username 1 /user/123/foo/ 302 /user/123/clean_username 1 /user/123/clean_username 302 /user/123/clean_username 1 /user/123/clean_username/ 200 /user/123/clean_username/ 0 /user/125698 404 with strict_slashes=True (the default) any non '/'-terminated urls redirect to their '/'-terminated counterpart URL Redirects/points to # of redirects =========================================================================== /user/123 301 /user/123/ 2 /user/123/foo 301 /user/123/foo/ 2 /user/123/clean_username 301 /user/123/clean_username/ 1 /user/123/ 302 /user/123/clean_username/ 1 /user/123/foo/ 302 /user/123/clean_username/ 1 /user/123/clean_username/ 200 /user/123/clean_username/ 0 /user/125698 404 example: "/user/123/foo" not terminated with '/' -> redirects to "/user/123/foo/" "/user/123/foo/" -> redirects to "/user/123/clean_username/"
I believe it does exactly what your test matrix is about :)
You've almost got it. defaults
is what you want. Here is how it works:
@route('/<int:id>/<username>/') @route('/<int:id>/', defaults={'username': None}) def profile(id, username): user = User.query.get_or_404(id) if username is None or user.clean_username != username: return redirect(url_for('profile', id=id, username=user.clean_username)) return render_template('user/profile.html', user=user)
defaults
is a dict
with default values for all route parameters that are not in the rule. Here, in the second route decorator there is no username
parameter in the rule, so you have to set it in defaults
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With