jk0104
jk0104

Reputation: 151

Flask redirect url_for changing type of parameter

I have the following code in Flask/python that involves a variable years that is a list of ints. However, when I use return redirect(url_for(..., years becomes unicode and if it had been [2015, 2014], it becomes '[','2','0','1','5',' ', etc...

@main.route('/search', methods=('GET','POST'))
def fysearch():
    form = SelectYear()
    if form.validate_on_submit():
        years = form.years.data
        return redirect(url_for('.yearresults', years=years))
    return render_template('select_year.html', form=form)

When I print the type of each element in years above, it is a normal int as I want it to be. Once it is passed into the redirect url_for, that is when years turns into unicode.

@main.route('/searchresults/<years>')
def yearresults(years):
    page = request.args.get('page', 1, type=int)
    print type(years)
    pagination = Grant.query.filter(Grant.fy.in_(years)).\
            paginate(page, per_page=current_app.config['POSTS_PER_PAGE'],
        error_out=False)
    return render_template('yearresults.html', years=years, entries=pagination.items, pagination=pagination

I know there are ways to revert years back to a list of ints after it has been passed to yearresults(years) like years=json.loads(years) or replacing the [ and ] and splitting, but I was wondering if there is a different way to fix this issue. I have thought about a converter in the url routing, but I am not sure how that works since I am using flask blueprints. Thanks in advance!

Upvotes: 0

Views: 1464

Answers (1)

metatoaster
metatoaster

Reputation: 18908

The function url_for returns a URL, which is effectively a string - you can't mix a list of ints into a value that's effectively a string (in better languages/frameworks you will get a type error, more work that way but less prone to conceptual errors like what you are experiencing). You can check simply by returning the result of url_for('.yearresults', years=years) and see that the value looks something like /yearresults/%5B2014%2C%202015%5D. Clearly that value in place for year is a string as that is the default converter (since you did not define one). So the lazy way out is to encode years with JSON or some sort of string format and decode that on the yearresults handler, however you had the right idea with using a converter which is from the werkzeug package.

Anyway, putting that together you could do something like this:

from werkzeug.routing import BaseConverter
from werkzeug.routing import ValidationError

class ListOfIntConverter(BaseConverter):
    def __init__(self, url_map):
        super(ListOfIntConverter, self).__init__(url_map)

    def validate(self, value):
        if not isinstance(value, list):
            return False

        for i in value:
            if not isinstance(i, int):
                return False

        return True

    def to_python(self, value):
        try:
            return [int(i) for i in value.split(',')]
        except (TypeError, ValueError) as e:
            raise ValidationError()

    def to_url(self, value):
        if not self.validate(value):
            # Or your specific exception because this should be from the
            # program.
            raise ValueError
        return ','.join(unicode(v) for v in value)

app.url_map.converters['listofint'] = ListOfIntConverter

@app.route('/years/<listofint:years>')
def years(years):
    return '%d, %s' % (len(years), years)

This has the advantage of generating a 404 directly when the input to this route do not match (i.e. someone supplying a string for <years>), and avoids the % encoded form of [, ], and from a JSON (or the repr) based encoding (%5B2014%2C%202015%5D looks way ugly when compared to 2014,2015).

Getting the converter into a specific blueprint (and also the unit tests for that) is your exercise.

Upvotes: 1

Related Questions