3

I want to have a dynamic navigation menu that shows "Login" if the user is not currently logged on, and "Logout" if the user is logged in.

I'm using code similar to the following:

import flask
import flask_nav
import flask_nav.elements as fne

frontend = flask.Blueprint('frontend', __name__)

application = flask.Flask(__name__)
mySess = flask_session.Session()

flask_appconfig.AppConfig(application)
flask_bootstrap.Bootstrap(application)
application.register_blueprint(frontend)
application.config['BOOTSTRAP_SERVE_LOCAL'] = True
application.config['SSL'] = True
application.secret_key = SECRET_KEY
application.config['SESSION_TYPE'] = SESSION_TYPE

mySess.init_app(application)

nav = flask_nav.Nav()

class CustomRenderer(flask_bootstrap.nav.BootstrapRenderer):
    def visit_Navbar(self, node):
        nav_tag = super(CustomRenderer, self).visit_Navbar(node)
        nav_tag['class'] = 'navbar navbar-default navbar-fixed-top'
        return nav_tag

flask_nav.register_renderer(application, 'custom', CustomRenderer)

nav.init_app(application)

@nav.navigation()
def top_nav():
    items = [ fne.View('Home',              '.index') ]

    if 'google_token' in flask.session:
        items.append(fne.View('Logout',         '.logout'))
    elif 'auth_url' in flask.session:
        items.append(fne.View('Login',          flask.session['auth_url']))
    else:
        items.append(fne.View('Login',          '.login'))

    items.append(fne.View('About',              '.about'))
    items.append(fne.View('Contact',            '.contact'))
    items.append(fne.View('Shop',               '.shop'))
    items.append(fne.View('Help & Feedback',    '.help'))

    return fne.Navbar('', *items)

nav.register_element('frontend_top', top_nav())

Unfortunately, the Flask session variables are out-of-scope for the nav object, so I cannot access flask.session from within top_nav.

I have the same difficulty when I make any stand-alone function for accessing flask-session outside of my application, for example

def user_is_logged_in():
    if 'google_token' in flask.session:
        return True
    else:
        return False
    return False

These functions give the expected error "RuntimeError: Working outside of request context."

I do NOT want to use a global variable in my application.py code for the user for security reasons and so multiple people can access the application at the same time without errors. I believe the SESSION should be storing whether the user is currently logged in or not.

How do I get my flask_nav.Nav() to see my application's flask.session?

Glenn Strycker
  • 4,816
  • 6
  • 31
  • 51
  • I'm trying to follow the example on http://pythonhosted.org/flask-nav/advanced-topics.html#dynamic-construction, but the top_nav() function does not see flask.session even though nav is associated with application through nav.init_app(application). – Glenn Strycker Aug 29 '17 at 21:51
  • @Back2Basics I'll give that a try, but I believe it will have the same problem... I can set the flask.g variables inside of my applications functions, but they might not be in scope for other general functions. I'm also worried about multiple users using the web app at the same time, whether multiple instances would be using the same global g context. I do know that flask.sessions avoids that problem. But thanks for the tip, I'll try it out! – Glenn Strycker Aug 30 '17 at 03:07

2 Answers2

3

flask_nav registers extensions at a stage in the application lifecycle before requests start to be processed.

You can overwrite the registration of the template_global to later when a request context exists in the application.

Factor out common navigation items.

nav = Nav()

# registers the "top" menubar
navitems = [
    View('Widgits, Inc.', 'index'),
    View('Our Mission', 'about'),
]

Set a function to return an appropriate View/Link based on value in session

def with_user_session_action(items):
    return (
        items 
        + [ View('Login', 'login') if not session.get('logged') else View('Logout', 'logout')]
    )

Use this in a function that delegates to nav.register_element

def register_element(nav, navitems):
    navitems = with_user_session_action(navitems)
    return nav.register_element('top', 
        Navbar(*navitems)
    )

Supersede render_template to always pass down the computed navigation

_render_template = render_template

def render_template(*args, **kwargs):
    register_element(nav, navitems)

    return _render_template(*args, nav=nav.elems, **kwargs)

Bonus:

You can cache the computed nav for login/logout so that it isn't only computed once for each case.

Oluwafemi Sule
  • 36,144
  • 1
  • 56
  • 81
1

Flask-Login will make your life easier. It provided a current_user to point to the current user, and the user object has an is_authenticated property:

from flask_login import current_user
...
@nav.navigation()
def top_nav():
    ...
    if current_user.is_authenticated: 
        items.append(View("Logout", ".logout")) 
    else: 
        items.append(View("Login", ".login"))

The code to initialize Flask-Login will like this:

from flask import Flask
from flask_login import LoginManager, UserMixin

app = Flask(__name__)
login_manager = LoginManager(app)

# The user model
class User(db.Model, UserMixin):
    ...


@login_manager.user_loader
def load_user(user_id):
    return User.get(user_id)

Check the documentation for more detail.

Grey Li
  • 11,664
  • 4
  • 54
  • 64
  • Thanks, @Grey-Li, I'll check out this solution, as this may solve my initial problem. However, how do I get general session variables into the navigation menu? For example, what if I have a particular value loaded from a database not related to the user (say, current time at my company's location) that I would want to put from flask.session into the navbar? – Glenn Strycker Oct 04 '17 at 13:48
  • The `session` is request-based, you can't access it outside of the request context (AFAIK). If you want to load data from the database, why don't directly load it in `top_nav()`? – Grey Li Oct 04 '17 at 14:45
  • Sometimes you get freer without extensions. – Grey Li Oct 04 '17 at 14:47