kkyr
kkyr

Reputation: 3845

Flask redirect with data

I have a Flask application with a GET handler that given a recipe id as a URL parameter, it retrieves the recipe from the database and then renders it:

@app.route('/recipe/<int:id>', methods=['GET'])
def get(id):
    recipe = get_recipe_from_db(id)
    return render_template('recipe.html', recipe=recipe)

This results in a URL like /recipe/5. Rather than showing just the id in the URL, I would like the recipe title to be part of the resulting URL, for example recipe/5/lemon-cake. On the first request only the id is known.

I'm not sure what a neat way to do this is. So far I've come up with the following:

@app.route('/recipe/<int:id>', methods=['GET'])
def get(id):
    recipe = get_recipe_from_db(id)
    return redirect(url_for('get_with_title', id=id, title=urlify(recipe.title)))

@app.route('/recipe/<int:id>/<title>', methods=['GET'])
def get_with_title(id, title=None):
    recipe = get_recipe_from_db(id)
    return render_template('recipe.html', recipe=recipe)

This works (i.e. when user visits /recipe/5 it redirects to /recipe/5/lemon-cake) but suffers from the fact that the same recipe is retrieved from the database twice.

Is there a better way to go about this?

Note: the recipe object is large containing multiple fields and I do not want to pass it over the network unecessarily.

Upvotes: 3

Views: 3904

Answers (4)

Chris
Chris

Reputation: 34055

Option 1

The easiest solution would be to modify the URL on client side, as soon as the response is received; hence, no need for redirection and/or querying the databse twice. This could be achieved by using either history.pushState() or history.replaceState().

client side

<!DOCTYPE html>
<html>
   <head>
      <script>
         function modify_url() {
           var title = {{recipe.title|tojson}};
           var id = {{recipe.id|tojson}};
           window.history.pushState('', '', id + "/" + title);
         }
      </script>
   </head>
   <h2>Recipe for {{recipe.title}}</h2>
   <body onload="modify_url()"></body>
</html>

server side

@app.route('/recipe/<int:id>', methods=['GET'])
def get(id):
    recipe = get_recipe_from_db(id)
    return render_template('recipe.html', recipe=recipe)

You may also would like to retain the get_with_title() route as well, in case users bookmark/share the URL (including the title) and want it to be accessible (otherwise, "Not Found" error would be returned when accessing it).

Option 2

If you wouldn't like to query the database every time a new request arrives—not even jsut for retrieving the title (not every single column) of a recipe entry—and you have sufficient amount of memory to hold the data, I would suggest to query the database once at startup (selecting only id and title) and create a dictionary, so that you can quickly look up for a title from the recipe id. Please note, in this way, every time an INSERT/DELETE/etc operation is performed on the table, that dictionary has to be updated accordingly. Thus, if you have such operations frequently on that table, might not be the best approach to the problem, and better keep with querying the table just for retrieving the title.

recipes = dict((row[0], row[1]) for row in result) # where row[0] is id and row[1] is title

Then in your endpoint:

@app.route('/recipe/<int:id>', methods=['GET'])
def get(id):
    title = recipes.get(id)
    return redirect(url_for('get_with_title', id=id, title=urlify(title)))

Upvotes: 3

qqNade
qqNade

Reputation: 1982

If you want the 'recipe title' to appear in the URL, your solution is not suitable for that. You should do this before the 'request'. There is probably an HTML page where you submit the recipe data to list all the recipes.

Try the following in your HTML page:

<ul> 
{% for recipe  in recipes %} 
<li><a href="/postdetail/{{recipe.id}}/{{recipe.title}}"></li>
 {% endfor %} 
</ul>

After that, when you click on that recipe you can also see the title.

Upvotes: 1

janpeterka
janpeterka

Reputation: 641

You can look into only changing URL with javascript, as shown here. This way, you don't do any redirects or reloading.

If you don't want to mess with javascript, let's think about backend solutions:

Is loading twice from a database significant performance overhead to be worthy of a complicated solution? If not, your solution is good.

If you really want to avoid loading the whole recipe, you can load only the title in your get method, instead of the whole object.
You can write custom get_recipe_title(id) which does (roughly) SELECT title FROM recipes WHERE id={id}.
If using SQLAlchemy, you may probably use load_only - here

Upvotes: 1

Alex Kosh
Alex Kosh

Reputation: 2544

Just pass loaded recipe as argument:

@app.route('/recipe/<int:id>', methods=['GET'])
def get(id):
    recipe = get_recipe_from_db(id)
    return redirect(
        url_for(
            'get_with_title',
            id=id,
            title=urlify(recipe.title),
            recipe=recipe,
        ))

@app.route('/recipe/<int:id>/<title>', methods=['GET'])
def get_with_title(id, title=None, recipe=None):
    if recipe is None:
        recipe = get_recipe_from_db(id)
    return render_template('recipe.html', recipe=recipe)

Upvotes: 1

Related Questions