How can I get the user agent string in a Dash app?

Hi, I’m trying to change the app.layout depending if the device is mobile or PC. My Dash app is a page with 2 graphs and a dbc.Col of info. If it’s mobile, the layout should be in 3 dbc.Rows. if it’s PC, the layout should be in 3 dbc.Col’s. Rows vs. Columns. So just using xs, xl, md, etc. won’t help me here, because the width isn’t the problem, it’s the actual layout.

I keep getting


RuntimeError: Working outside of request context.

This typically means that you attempted to use functionality that needed
an active HTTP request.  Consult the documentation on testing for
information about how to avoid this problem.

when I try to use


parse(request.headers.get('User-Agent'))

to get the user agent.

My code is:

def serve_layout():
  user_agent = parse(request.headers.get('User-Agent'))
  # if the user is a PC
  if user_agent.is_pc == True:
    app.layout = dbc.Container(children=[
      dbc.Row( # First row: title card
        [
          dbc.Col([title_card]),
        ]
      ),
      dbc.Row( # Second row: the rest
        [
          dbc.Col([user_options_card], width = 4),
          dbc.Col([map_card], width = 8)
        ]
      ),
    ],
    fluid = True,
    className = "dbc",
    id = 'layout'
    )
  elif user_agent.is_mobile == True:
    app.layout = dbc.Container(children=[
      dbc.Row( # First row: title card
        [
          dbc.Col([title_card]),
        ]
      ),
      dbc.Row( # Second row: the rest
        [
          dbc.Col([user_options_card]),
          dbc.Col([map_card])
        ]
      ),
    ],
    fluid = True,
    className = "dbc",
    id = 'layout'
    )
  return app.layout

app.layout = serve_layout

@app.callback(
  Output(component_id='cluster', component_property='children'),
  Output('layout', 'children'),
  [
    Input(component_id='subtype_checklist', component_property='value'),
    Input(component_id='pets_radio', component_property='value'),
    Input(component_id='terms_checklist', component_property='value'),
    Input(component_id='garage_spaces_slider', component_property='value'),
    Input(component_id='rental_price_slider', component_property='value'),
    Input(component_id='bedrooms_slider', component_property='value'),
    Input(component_id='bathrooms_slider', component_property='value'),
    Input(component_id='sqft_slider', component_property='value'),
    Input(component_id='yrbuilt_slider', component_property='value'),
    Input(component_id='sqft_missing_radio', component_property='value'),
    Input(component_id='yrbuilt_missing_radio', component_property='value'),
    Input(component_id='garage_missing_radio', component_property='value'),
    Input(component_id='ppsqft_slider', component_property='value'),
    Input(component_id='ppsqft_missing_radio', component_property='value'),
    Input(component_id='furnished_checklist', component_property='value'),
    Input(component_id='security_deposit_slider', component_property='value'),
    Input(component_id='security_deposit_missing_radio', component_property='value'),
    Input(component_id='pet_deposit_slider', component_property='value'),
    Input(component_id='pet_deposit_missing_radio', component_property='value'),
    Input(component_id='key_deposit_slider', component_property='value'),
    Input(component_id='key_deposit_missing_radio', component_property='value'),
    Input(component_id='other_deposit_slider', component_property='value'),
    Input(component_id='other_deposit_missing_radio', component_property='value'),
    Input(component_id='listed_date_datepicker', component_property='start_date'),
    Input(component_id='listed_date_datepicker', component_property='end_date'),
    Input(component_id='listed_date_radio', component_property='value'),
    Input(component_id='layout', component_property='children'),
  ]
)
# The following function arguments are positional related to the Inputs in the callback above
# Their order must match
def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental_price, bedrooms_chosen, bathrooms_chosen, sqft_chosen, years_chosen, sqft_missing_radio_choice, yrbuilt_missing_radio_choice, garage_missing_radio_choice, ppsqft_chosen, ppsqft_missing_radio_choice, furnished_choice, security_deposit_chosen, security_deposit_radio_choice, pet_deposit_chosen, pet_deposit_radio_choice, key_deposit_chosen, key_deposit_radio_choice, other_deposit_chosen, other_deposit_radio_choice, listed_date_datepicker_start, listed_date_datepicker_end, listed_date_radio, user_agent):
  df_filtered = df[
    (df['Sub Type'].isin(subtypes_chosen)) &
    pets_radio_button(pets_chosen) &
    (df['Terms'].isin(terms_chosen)) &
    # For the slider, we need to filter the dataframe by an integer range this time and not a string like the ones aboves
    # To do this, we can use the Pandas .between function
    # See https://stackoverflow.com/a/40442778
    (((df['Garage Spaces'].between(garage_spaces[0], garage_spaces[1])) | garage_radio_button(garage_missing_radio_choice, garage_spaces[0], garage_spaces[1]))) & # for this one, combine a dataframe of both the slider inputs and the radio button input
    # Repeat but for rental price
    (df['List Price'].between(rental_price[0], rental_price[1])) &
    (df['Bedrooms'].between(bedrooms_chosen[0], bedrooms_chosen[1])) &
    (df['Total Bathrooms'].between(bathrooms_chosen[0], bathrooms_chosen[1])) &
    (((df['Sqft'].between(sqft_chosen[0], sqft_chosen[1])) | sqft_radio_button(sqft_missing_radio_choice, sqft_chosen[0], sqft_chosen[1]))) &
    (((df['YrBuilt'].between(years_chosen[0], years_chosen[1])) | yrbuilt_radio_button(yrbuilt_missing_radio_choice, years_chosen[0], years_chosen[1]))) &
    (((df['Price Per Square Foot'].between(ppsqft_chosen[0], ppsqft_chosen[1])) | ppsqft_radio_button(ppsqft_missing_radio_choice, ppsqft_chosen[0], ppsqft_chosen[1]))) &
    furnished_checklist_function(furnished_choice) &
    security_deposit_function(security_deposit_radio_choice, security_deposit_chosen[0], security_deposit_chosen[1]) &
    pet_deposit_function(pet_deposit_radio_choice, pet_deposit_chosen[0], pet_deposit_chosen[1]) &
    key_deposit_function(key_deposit_radio_choice, key_deposit_chosen[0], key_deposit_chosen[1]) &
    other_deposit_function(other_deposit_radio_choice, other_deposit_chosen[0], other_deposit_chosen[1]) &
    listed_date_function(listed_date_radio, listed_date_datepicker_start, listed_date_datepicker_end)
  ]

  # Create markers & associated popups from dataframe
  markers = [dl.Marker(children=dl.Popup(popup_html(row)), position=[row.Latitude, row.Longitude]) for row in df_filtered.itertuples()]

  # Debug print a statement to check if we have all markers in the dataframe displayed
  print(f"The original dataframe has {len(df.index)} rows. There are {len(df_filtered.index)} rows in the filtered dataframe. There are {len(markers)} markers on the map.")

  user_agent = parse(request.headers.get('User-Agent'))
  
  # Generate the map & app layout
  return dl.MarkerClusterGroup(id=str(uuid.uuid4()), children=markers), user_agent



# Launch the Flask app
if __name__ == '__main__':
    app.run_server(mode='external', host='192.168.4.196', port='9208', debug='false')

I wish it wasn’t so complicated to just get the user agent string :frowning: I honestly have no idea what I’m doing and would welcome any advice… or a much simpler solution, lol.

hi @the.oldest.house

It looks like the layout is the same except for the screen size. Try having a single layout, with the rows like this:

   dbc.Row( # Second row: the rest
        [
          dbc.Col([user_options_card], lg = 4),
          dbc.Col([map_card], lg = 8)
        ]
      ),

This will put the rows side by side on large and extra large screens, then anything else will be a full width column.

You can see a live example here in the dash example index Try it in fullscreen mode and then change the browser window to see how the layout changes.

This looks fine on a computer, but on mobile it looks awful:

I want these dbc cards to be in columns (side by side) on a computer, because that’s where they look best with all that horizontal screen space. But not on mobile - on mobile, the screens are tiny and vertical so I want the cards to be in rows (vertically stacked) on mobile. So I’ve been playing around with the width options like xs, lg, etc. like you’ve mentioned but with those I can only ever get my app to look good on the computer OR on mobile; never both. So I figured I could change the actual structure of the layout (rather than the width) with an if/elif clause based on the user agent string which led me down this incredibly painful useragent/RuntimeError rabbit hole :tired_face:

The server where the python code is run would have no idea about the User Agent unless you created a clientside callback that stored the User Agent, or directly whether the device is mobile, somewhere in the app (say in a dcc.Store) then added a callback that would update the layout based on the content of that dcc.Store.

That is a possibility but I would recommend trying doing this directly via styling with a single layout. Either with dash_bootstrap_components utilities or directly via a stylesheet with media queries.

2 Likes

@the.oldest.house

I understand what you are trying to do, and css frameworks such as Bootstrap are designed to solve this exact problem. You do not need to have two layouts or try to get the User Agent to accomplish this goal.

I recommend copying and pasting the example from the link I also sent earlier and running it on your own computer without making any changes. You should see the following. Notice that the layout dynamically changes with the screen size

dynamic_layout

Next, you can use the layout in this example as a model and apply it to your app. You can also see more information in the dash bootstrap documentation on the layout page.

If you still have issues, please make a minimal example (like the one above) demonstrating how it’s not working - then someone here will be able to help you further.

3 Likes

I am sooo sorry, I have no idea why I didn’t just implement the column width parameters like you showed me… I must’ve tried and it didn’t work for some reason so I gave up.

Anyways I tried again and this time it works as I want - side by side on a PC, fully vertical on a mobile device.

Thank you very much for your patience and for linking the Dash example index; that’s super helpful. I had no idea that existed.

1 Like

I’m so glad it worked for you :partying_face:

The Dash Example Index is new and still under development - so you just got a Top Secret Preview :shushing_face: