1

Main page is a form and then when the user clicks "Calculate" there's calculations made from the form, stored in a dictionary which is then passed back to fill a table. briefly like this:

@app.route('/join', methods=['GET','POST'])
def my_form_post():
---calculations done and variables set---

data_dict = {
        "loco_length":l1, "loco_weight":w1, "loco_bweight":b1,
        "cargo_length":l2, "cargo_weight":w2, "cargo_bweight":b2,
        "total_length":length, "total_weight": weigth, "total_bweight": bweigth,
        "bproc":bproc
    }

    result = {str(key): value for key, value in data_dict.items()}
    return jsonify(result=result)

But then I've made a 2nd function which I'd like to be accessed with a new route /pdf. That function is to fill a pre-defined PDF-template with the values from the 'my_form_post' -dictionary.

The 2nd route looks like this, calling a function and needs the dictionary as argument:

def pdf():

    data = write_fillable_pdf(PDF_TEMP_PATH, data_dict)
    return send_file(data, mimetype='application/pdf')

How do I get the dictionary to transfer or be accessible between functions and routes? I've seen people mention sessions, json and globals. But I cant figure it out.

Thank you for any assistance.

mag37
  • 45
  • 6
  • Do you have (/ can you get) access to a redis server from where this will be hosted? I'd like to write an answer for this using redis... – v25 Jan 27 '20 at 16:35
  • I'm not sure, I'm hosting it at pythonanywhere.com atm but will hopefully host it on my own cloud nginx+gunicorn in the future. Thou I'd like it as simple as possible, the dict is small and only need it for the occasional user who wants to print it to PDF. – mag37 Jan 27 '20 at 16:40

1 Answers1

1

Okay, so as I mentioned, a pretty cool backend for this would be redis. As you're hosting on PythonAnywhere, there's actually a library called redislite which will provide this functionality using a local file, instead of an actual redis server.

It appears PythonAnywhere, don't support redis but do support persistant storage. What's even better is if you move to a host with an actual redis server later, it should be a two line modification to switch over.

The concept behind this is:

  • In the /join route, create a UUID to use as a unique_id and store that with the dict in redis, then pass the unique_id back to the template, presenting it to the user as a link.
  • In the /pdf route, get the dict from redis based on the unique_id in the URL string.
  • For bonus points we can set an expiry on the redis key, so after n seconds, the link dissapears and a user would have to hit the calculate button again.

First you'll need to add redislite to the requirements, or pip install redislite. Then at the top of your Python file include the neccessary stuff to configure the redis connection:

from redislite import StrictRedis
import os
REDIS_DB_PATH = os.path.join('/tmp/my_redis.db')
r = StrictRedis(REDIS_DB_PATH, charset='utf-8', decode_responses=True)

You should also define the expiry time, a prefix which keeps the redis keys unique to this set of routes, and import other tools:

PDF_EXPIRY = 60*60*24 # 1 day in seconds
PREFIX = 'pdf:' # A prefix for our redis keys
from uuid import uuid4
from flask import url_for

Now this next part works because your data is a simple dictionary, with no nesting. I've used a mock version here, then add it to redis with hmset and also set the expiry.

Then I add the key pdf_link to your result dict and set the value to the URL for the PDF, built by flask's url_for function:

@app.route('/join')
def my_form_post():

    # ---calculations done and variables set---

    data_dict = {
        "loco_length":3.14, "loco_weight":'str', "loco_bweight":'str',
    } # a simplified version of your dictionary

    unique_id = uuid4().__str__()

    r.hmset(PREFIX + unique_id, data_dict)
    r.expire(PREFIX + unique_id, PDF_EXPIRY)

    result = {str(key): value for key, value in data_dict.items()}

    # This creates a URL back to our PDF route, as a key in result
    result['pdf_link'] = url_for('pdf',unique_id = unique_id)  

    return jsonify(result=result)

This results in a redis hash with the key: pdf:86341e77-f655-46d8-93c4-10e945fc3586 and field names/values, as your data_dict's keys/values. See the HMSET redis docs for another visulisation of this.

Now in the frontend result.pdf_link is a valid link to the PDF which looks something like: /pdf/86341e77-f655-46d8-93c4-10e945fc3586. You can render that in an <a href=> tag on your template, next to the table.

Requests to that URL are handled by the next route:

@app.route('/pdf/<string:unique_id>')
def pdf(unique_id):
    data_dict = r.hgetall(PREFIX + unique_id)
    if not data_dict:
        return 'Not found or expired', 404
    else:
        data = write_fillable_pdf(PDF_TEMP_PATH, data_dict)
        return send_file(data, mimetype='application/pdf')

This method I think is quite clean, because it avoids building database models for the data which would have been the common approach. With this, you can add more values to data_dict, as long as you don't nest them, and nothing else needs changed.

If you wanted to add support for a real redis server in the future, you'd just need to change the two lines to:

from redis import StrictRedis
r = StrictRedis(host='newredisserver', charset='utf-8', decode_responses=True)

as the methods we've used are identical between the redis and redislite libraries.

Be aware that anyone who has the link to the PDF can download it, provided it hasn't expired, so you probably shouldn't be passing personal info through this without authentication.

v25
  • 7,096
  • 2
  • 20
  • 36
  • That's quite an extensive writeup, thank you kindly! I haven't had any time to try it yet, but I don't see how it's working. In the /join route the result is sent back to the frontend, to load the dictionary into the html-table I present. I don't want to mess with that, the Pdf-creation is not required but is optional, and needs the values from the dictionary. – mag37 Jan 28 '20 at 07:02
  • 1
    @mag37 Redis acts as a key value store which is available from any part of your app. You define the storage structure. So when you use `hmset` with the first arg `PREFIX+uniquie_id` you can get that back from anywhere, provided you supply that same key. By generating the link with `url_for` that enables you to [supply](https://flask.palletsprojects.com/en/1.1.x/quickstart/#variable-rules) the same key in the `/pdf/` route. Therefor `hmgetall` can retreive the dict, and generate the PDF, IF the user clicked the link with that `unique_id`. – v25 Jan 28 '20 at 08:27
  • 1
    Thank you for the explanation! I'll give it a go when I've got the time to really wrap my head around it ;) Juggling a full time job and a 2yo kid, this is my first ever programming project - dove too deep! ^^ – mag37 Jan 28 '20 at 08:58
  • 1
    @mag37 nice one, glad to hear! Feel free to ask any more questions here, as I keep an eye on my notifications daily :-) – v25 Jan 28 '20 at 09:12
  • You're a superstar - I've built it all locally and it works flawlessly! Thou now when I try to host it at PythonAnywhere I stumble upon an error anyway. **Exception on /pdf/81c32936-96de-4706-aefe-905935c3e62d [GET] io.UnsupportedOperation: fileno NO MATCH** Followed by: **SystemError: returned a result with an error set** Seems like they dont support it *we don't support sendfile I'm afraid. It's not just the uwsgi configuration that prevents it, our current load-balancer configuration won't allow it either.* – mag37 Jan 30 '20 at 14:14
  • 1
    Sorry for spam - but I got it solved by using **werkzeug - FileWrapper** and Flask's Response, following this example: https://www.pythonanywhere.com/forums/topic/13570/ – mag37 Jan 30 '20 at 14:21