web-dev-qa-db-fra.com

Flask redirigeant plusieurs itinéraires

J'essaie d'implémenter un modèle de redirection, similaire à ce que fait StackOverflow:

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

Voici un tableau simple de ce qui devrait se produire:

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

Pour le moment, je peux accéder au profil avec /user/1/foo, mais /user/1 produit un BuildError. J'ai essayé le alias=True argument de mot clé et quelque chose avec defaults, mais je ne sais pas trop ce qui ne fonctionne pas.

Comment pourrais-je avoir un itinéraire redirigé vers l'autre comme celui-ci?

30
Blender

routes de débogage:

Mise à jour: pour répondre à la question principale "qu'est-ce qui ne va pas avec mes itinéraires", la manière la plus simple de déboguer est d'utiliser app.url_map; par exemple:

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

Dans ce cas, cela confirme que le point de terminaison est correctement défini. Voici un exemple présentant à la fois les simples flask et 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)

Ils ont différents points de terminaison, dont vous devez tenir compte pour 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>])

Sans flask-classy, Le nom du point de terminaison est le nom de la fonction, mais comme vous l'avez découvert, cela est différent lorsque vous utilisez classy, et vous pouvez soit regarder le nom du point de terminaison avec url_map() ou affectez-le à votre itinéraire avec @route(..., endpoint='name').


moins de redirections:

Pour répondre aux URL que vous avez publiées tout en minimisant le nombre de redirections, vous devez utiliser strict_slashes=False, Cela veillera à gérer les demandes qui ne se terminent pas par un / Au lieu de les rediriger avec un 301 Redirigez vers leur / - homologue terminé:

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

voici le résultat:

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

Comportement de 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/"

Je crois qu'il fait exactement de quoi parle votre matrice de test :)

32
dnozay

Vous l'avez presque compris. defaults est ce que vous voulez. Voici comment cela fonctionne:

@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 est un dict avec des valeurs par défaut pour tous les paramètres de route qui ne sont pas dans la règle. Ici, dans le deuxième décorateur de route, il n'y a pas de paramètre username dans la règle, vous devez donc le définir dans defaults.

22
dAnjou

Eh bien, il semble que mon code d'origine ait réellement fonctionné. Flask-Classy était le problème ici (et puisque cette question a une prime, je ne peux pas la supprimer).

J'ai oublié que Flask-Classy renomme les itinéraires, donc au lieu de url_for('ClassName:profile'), je devrais sélectionner l'itinéraire du décorateur le plus à l'extérieur:

url_for('ClassName:profile_1')

Une alternative serait de spécifier explicitement un point de terminaison de la route:

@route('/<int:id>/<username>/', endpoint='ClassName:profile')
3
Blender

Je ne comprends pas pourquoi vous redirigez. Vous ne gagnez rien avec la redirection et comme vous l'avez mentionné vous-même, vous finissez par interroger la base de données plusieurs fois. Vous n'utilisez pas le nom d'utilisateur donné de manière significative, alors ignorez-le.

@route('/<int:id>/<username>/')
@route('/<int:id>/')
def profile(id, username=None):
    user = User.query.get_or_404(id)
    return render_template('user/profile.html', user=user)

Cela satisfera tous vos exemples donnés.

1
Michael Davis