While been working on my project, I want to provide user with feedback messages. This is important i.e. if user tries to do something that is prohibbited, or allowed, and must be confirmed with the appropriate message. Trying to keep everything as simple as possible, I'm using two decorators, one of which checks if user logged in and another checks is user can access given page. Thus my typical method looks like:
@my_blueprint.route('URL_Path')
@requireslogin
@can_access(roles=['administration', 'administrator'])
def method():
pass
In future I'll try to re-map everything from DB, so it will be more dynamic and more complex, but now it is OK with me. The problem I got today is about accessing flashed messages from POST request, which have been set in can_access().
def can_access(roles=[]):
def decorator(f):
@wraps(f)
def check_access(*args, **kwargs):
user_rec = db_session.query(User).\
filter(User.username == session["username"].lower()).\
first()
my_role = db_session.query(Role).filter(Role.id == user_rec.role).\
first()
logger.debug(my_role)
if my_role.name.lower() not in roles and my_role.name != 'global':
logger.debug("Access not allowed for this role.")
flash(errors.get("access_denied"), "error")
return redirect(request.headers.get("Referer", None) or url_for('central'))
return f(*args, **kwargs)
return check_access
return decorator
So the general idea was to turn user back to the previous page OR central (if no previous page) if he doesn't have access to the requested page. While I accessed page using GET requests it seems like everything has been worked OK, but with the POST requests no error message has been displayed.
Firstly I thought the problem is that flash() sends signal that lives till the end of the current request, so I decided to create some global variable in the session, that will contain such errors. Indeed it wasn't the case, so I stucked with the problem if I need to re-write each of my methods in order to process the newly-found error.
That was not acceptable, so there should be another way.
Checking log I figured out there is double call to the same page:
13 06 2015 09:30:25 DEBUG: <Role(id='3', name='administrator')>
13 06 2015 09:30:25 DEBUG: Access not allowed for this role.
13 06 2015 09:30:26 INFO: 127.0.0.1 - - [13/Jun/2015 21:30:26] "POST /customers/delete/3 HTTP/1.1" 302 -
13 06 2015 09:30:26 DEBUG: Calling list_customers
13 06 2015 09:30:26 DEBUG: GET params: 'name': None , 'page': None, 'rps': None
13 06 2015 09:30:26 DEBUG: Retrieving customers
13 06 2015 09:30:26 INFO: 127.0.0.1 - - [13/Jun/2015 21:30:26] "GET /customers/ HTTP/1.1" 200 -
13 06 2015 09:30:26 DEBUG: Calling list_customers
13 06 2015 09:30:26 DEBUG: GET params: 'name': None , 'page': None, 'rps': None
13 06 2015 09:30:26 DEBUG: Retrieving customers
13 06 2015 09:30:26 INFO: 127.0.0.1 - - [13/Jun/2015 21:30:26] "GET /customers/ HTTP/1.1" 200 -
That was not I wanted to have, so another guess was I just pop-up messages on the first request, and on the second I get nothing. And I don't see changes, because there is no delay between these requests. Such idea is correct, because each template has
get_flashed_messages() method to display errors. So next step was to figure out where such update happens. I believed this has some connection to my Javascript, so I decided to re-check application's logic.
In case of GET request situation is quite simple:
- user asks for some page with GET request;
- if page is accessible, server renders it and send it back to the user;
- if page is not accessible, then server redirects user to its previous or initial page;
- on the previous or initial page server once again tests if user can login and if user can get access to the page;
- because this page is accessible by user, it is generated and returned to the user;
- browser displays data.
As for the POST requests, I use them for 2 different cases: to process forms and to process individual requests like activate/deactivate items, delete items etc. Form processing is similar to the GET requests, so the above case is valid here. But individual requests need another processing logic. These requests are generating by JavaScript code, that expects JSON string as response, and looks like:
result = {
"performed": False,
"msg": ""
}
where result["performed"] contains True if request has been completed by server correctly and request["msg"] contains any support message. There can be other fields as well, but these are mandatory, so tracking if request contains form or not, can be performed by testing "Content-Type" of HTTP header. So, decorator "can_access" needs following changes:
def can_access(roles=[]):
def decorator(f):
@wraps(f)
def check_access(*args, **kwargs):
user_rec = db_session.query(User).\
filter(User.username == session["username"].lower()).\
first()
my_role = db_session.query(Role).filter(Role.id == user_rec.role).\
first()
logger.debug(my_role)
if my_role.name.lower() not in roles and my_role.name != 'global':
logger.debug("Access not allowed for this role.")
flash(errors.get("access_denied"), "error")
if request.method == "POST" and \
"text/plain" in request.headers["Content-Type"]:
result = {
"performed": False,
"msg": errors.get("access_denied")
}
return jsonify(result)
return redirect(request.headers.get("Referer", None) or url_for('central'))
return f(*args, **kwargs)
return check_access
return decorator
From this point GET and POST requests process flashed messages correctly.
Any programming, architecture, pythonizing and other tips are welcomed. :)