diff --git a/app/pages/settings.py b/app/pages/settings.py index 8db6912..dab9d71 100644 --- a/app/pages/settings.py +++ b/app/pages/settings.py @@ -2,19 +2,17 @@ import streamlit as st import queries.crud as crud import themes as th - -is_login_enabled = hasattr(st, 'user') and hasattr(st.user, 'is_logged_in') -is_logged_in = is_login_enabled and st.user.is_logged_in +from user import is_logged_in, is_login_enabled st.title("Settings") -if hasattr(st.session_state, 'user_name'): +if hasattr(st.session_state, 'user_name') and st.session_state.user_name: st.markdown(f"Currently logged in as **{st.session_state.user_name}**") st.header("Theme") themes = st.session_state.themes with st.container(horizontal=True, width="stretch"): - for theme in ["light", "dark"]: + for theme in themes.keys(): if st.button(label=themes[theme]["button_face_label"], icon = themes[theme]["button_face_icon"], disabled = (theme == st.session_state.current_theme), @@ -61,8 +59,9 @@ with st.container(key="settings-color-selector"): """, width=400) -st.header("Actions") -with st.container(): - if is_logged_in: - if st.button("Logout", icon=":material/logout:", width="stretch"): - st.logout() \ No newline at end of file +if is_login_enabled(): + st.header("Actions") + with st.container(): + if is_logged_in(): + if st.button("Logout", icon=":material/logout:", width="stretch"): + st.logout() \ No newline at end of file diff --git a/app/queries/connection.py b/app/queries/connection.py index 9870a4a..7109d10 100644 --- a/app/queries/connection.py +++ b/app/queries/connection.py @@ -2,9 +2,10 @@ from os import getenv import streamlit as st from sqlalchemy.sql import text -from streamlit.connections import BaseConnection +from streamlit.connections import SQLConnection -def connection() -> BaseConnection: + +def connection() -> SQLConnection: _connection = st.connection("sql", url=getenv('DATABASE_URL'), ttl=0, autocommit=True) with _connection.session as configured_session: configured_session.execute(text('PRAGMA foreign_keys=ON')) diff --git a/app/queries/crud.py b/app/queries/crud.py index e346f51..f07df1a 100644 --- a/app/queries/crud.py +++ b/app/queries/crud.py @@ -139,6 +139,7 @@ def set_theme(theme:str): logger.error(e) session.rollback() + def get_theme() -> str: user_id = int(st.session_state.user_id) try: diff --git a/app/queries/user.py b/app/queries/user.py index 01bffbd..66ee28d 100644 --- a/app/queries/user.py +++ b/app/queries/user.py @@ -1,6 +1,7 @@ import logging import streamlit as st +from pandas import DataFrame from sqlalchemy.sql import text from streamlit.user_info import UserInfoProxy @@ -11,11 +12,9 @@ logger = logging.getLogger(__name__) def find_user_by_oidc_id(oidc_user_id): return connection().query('SELECT * FROM users WHERE oidc_user_id = :id', params={'id': oidc_user_id}) - def find_user_by_email(email): return connection().query('SELECT * FROM users WHERE email = :email', params={'email': email}) - def find_default_user(): return find_user_by_email('default') @@ -30,40 +29,12 @@ def update_default_user(email, name, oidc_user_id): raise e -def create_user(email, name, oidc_user_id): +def create_user(email, name, oidc_user_id) -> DataFrame: with connection().session as session: try: logger.info("Creating new user %s", email) - query = text('INSERT INTO users (email, name, oidc_user_id) VALUES (:email, :name, :user_id)') - session.execute(query, {'email': email, 'name': name, 'user_id': oidc_user_id}) - return connection().query('SELECT * FROM users WHERE oidc_user_id = :id', params={'id': oidc_user_id}) + query = text('INSERT INTO users (email, name, oidc_user_id) VALUES (:email, :name, :user_id) RETURNING *') + return DataFrame(session.execute(query, {'email': email, 'name': name, 'user_id': oidc_user_id})) except Exception as e: session.rollback() raise e - - -def set_user_in_session(user: UserInfoProxy): - - email = user.email if hasattr(user, "email") else None - user_id = user.sub if hasattr(user, "sub") else None - name = user.name if hasattr(user, "name") else None - - user_entity = find_user_by_oidc_id(user_id) if user_id else st.dataframe() - if user_entity.empty: - user_entity = find_user_by_email(email) if email else st.dataframe() - if user_entity.empty: - user_entity = find_default_user() - if user_entity.empty and email and name and user_id: - user_entity = create_user(email, name, user_id) - elif name: - update_default_user(email, name, user_id) - user_entity = find_user_by_oidc_id(user_id) - - if not user_entity.empty: - st.session_state.user_id = user_entity["id"][0] - st.session_state.user_name = user_entity["name"][0] - st.session_state.user_email = user_entity["email"][0] - st.session_state.user_external_id = user_entity["oidc_user_id"][0] - st.session_state.current_theme = user_entity["theme"][0] - else: - logger.warn("No active user found!") \ No newline at end of file diff --git a/app/streamlit_app.py b/app/streamlit_app.py index e58488c..1353e01 100644 --- a/app/streamlit_app.py +++ b/app/streamlit_app.py @@ -1,10 +1,11 @@ import streamlit as st from streamlit import dialog import queries.user as user_queries +import random from logger import init_logger from styles import init_styles -from user import init_user, is_login_enabled, is_logged_in +from user import init_user, is_login_enabled, is_logged_in, init_demo_user from themes import init_themes init_logger() @@ -12,18 +13,21 @@ init_user() init_styles() init_themes() -if is_login_enabled and not is_logged_in: - with st.container(width="stretch", height="stretch", horizontal_alignment="center"): - st.title("Daily Counter", width="stretch", text_alignment="center") - st.text("Please log in to use this app", width="stretch", text_alignment="center") - st.space() - if st.button("Log in"): - st.login() - -else: +if not is_login_enabled() or is_logged_in(): counters = st.Page("pages/counters.py", title="Counters", icon=":material/update:") stats = st.Page("pages/stats.py", title="Statistics", icon=":material/chart_data:") settings = st.Page("pages/settings.py", title=" ", icon=":material/menu:") pages = [counters, stats, settings] pg = st.navigation(position="top", pages=pages) pg.run() + +else: + with st.container(width="stretch", height="stretch", horizontal_alignment="center"): + st.title("Daily Counter", width="stretch", text_alignment="center") + st.text("Please log in to use this app", width="stretch", text_alignment="center") + st.space() + if st.button("Log in", width="stretch",icon=":material/login:"): + st.login() + if st.button("Demo", width="stretch", icon=":material/account_box:"): + init_demo_user() + st.rerun() diff --git a/app/styles.py b/app/styles.py index 938fa24..8dfa420 100644 --- a/app/styles.py +++ b/app/styles.py @@ -1,6 +1,6 @@ import streamlit as st -import queries from queries import crud +from user import is_logged_in def _load_css(filepath): with open(filepath) as file: @@ -8,16 +8,17 @@ def _load_css(filepath): def _load_color_selector_styles(): - colors = crud.get_colors() - for idx, c in enumerate(colors.keys()): - css_color = '#' + colors[c][0] - st.html(f""" - - """) + if is_logged_in(): + colors = crud.get_colors() + for idx, c in enumerate(colors.keys()): + css_color = '#' + colors[c][0] + st.html(f""" + + """) def init_styles(): diff --git a/app/themes.py b/app/themes.py index e6b4472..9715ceb 100644 --- a/app/themes.py +++ b/app/themes.py @@ -1,28 +1,28 @@ import streamlit as st from queries import crud +from user import is_logged_in, is_login_enabled + def init_themes(): if 'themes' not in st.session_state: st.session_state.themes = { - "dark": { - "theme.base": "dark", - "theme.backgroundColor": "black", - "theme.primaryColor": "#c98bdb", - "theme.secondaryBackgroundColor": "#5591f5", - "theme.textColor": "white", - "button_face_label": "Dark", - "button_face_icon": ":material/dark_mode:" - }, "light": { "theme.base": "light", "theme.backgroundColor": "white", "theme.primaryColor": "#5591f5", - "theme.secondaryBackgroundColor": "#82E1D7", "theme.textColor": "#0a1464", "button_face_label": "Light", "button_face_icon": ":material/light_mode:" }, + "dark": { + "theme.base": "dark", + "theme.backgroundColor": "black", + "theme.primaryColor": "#c98bdb", + "theme.textColor": "white", + "button_face_label": "Dark", + "button_face_icon": ":material/dark_mode:" + } } if 'current_theme' not in st.session_state: @@ -30,7 +30,10 @@ def init_themes(): change_theme('light') def change_theme(theme): - crud.set_theme(theme) + if is_logged_in(): + crud.set_theme(theme) + + st.session_state.current_theme = theme for key, val in st.session_state.themes[theme].items(): if key.startswith("theme"): st._config.set_option(key, val) diff --git a/app/user.py b/app/user.py index 45c3cc7..9c03de1 100644 --- a/app/user.py +++ b/app/user.py @@ -1,8 +1,60 @@ +import logging +import random import queries.user as user_queries import streamlit as st -is_login_enabled = hasattr(st, 'user') and hasattr(st.user, 'is_logged_in') -is_logged_in = is_login_enabled and st.user.is_logged_in +from pandas import DataFrame +from streamlit.user_info import UserInfoProxy + +logger = logging.getLogger(__name__) + +def is_login_enabled() -> bool: + return hasattr(st, 'user') and hasattr(st.user, 'is_logged_in') + +def is_demo_user() -> bool: + return hasattr(st.session_state, 'user_is_demo') and st.session_state.user_is_demo + +def is_logged_in() -> bool: + return not is_login_enabled() or is_demo_user() or (is_login_enabled() and st.user.is_logged_in) def init_user(): - user_queries.set_user_in_session(st.user) \ No newline at end of file + if not is_demo_user(): + set_user_in_session(st.user) + +def set_user_in_session(user: UserInfoProxy): + + email = user.email if hasattr(user, "email") else None + user_id = user.sub if hasattr(user, "sub") else None + name = user.name if hasattr(user, "name") else None + + user_entity = user_queries.find_user_by_oidc_id(user_id) if user_id else DataFrame() + if user_entity.empty: + user_entity = user_queries.find_user_by_email(email) if email else DataFrame() + if user_entity.empty: + user_entity = user_queries.find_default_user() + if user_entity.empty and email and name and user_id: + user_entity = user_queries.create_user(email, name, user_id) + elif name: + user_queries.update_default_user(email, name, user_id) + user_entity = user_queries.find_user_by_oidc_id(user_id) + + if not user_entity.empty: + st.session_state.user_id = int(user_entity["id"][0]) + st.session_state.user_name = user_entity["name"][0] + st.session_state.user_email = user_entity["email"][0] + st.session_state.user_external_id = user_entity["oidc_user_id"][0] + st.session_state.current_theme = user_entity["theme"][0] + st.session_state.user_is_demo = False + else: + logger.warning("No active user found!") + + +def init_demo_user(): + demo_id = ''.join(random.choice('0123456789abcdefghijklmnopqrstuvwxyz') for i in range(6)) + demo_user = user_queries.create_user(f"demo+{demo_id}@internal", f"Demo user {demo_id}", None) + st.session_state.user_id = int(demo_user["id"][0]) + st.session_state.user_name = demo_user["name"][0] + st.session_state.user_email = demo_user["email"][0] + st.session_state.user_external_id = demo_user["oidc_user_id"][0] + st.session_state.current_theme = demo_user["theme"][0] + st.session_state.user_is_demo = True \ No newline at end of file diff --git a/tests/database/stats_db_test.py b/tests/database/stats_db_test.py index 86d6d67..6676ee4 100644 --- a/tests/database/stats_db_test.py +++ b/tests/database/stats_db_test.py @@ -17,17 +17,17 @@ def test_all_daily_stats(): query = text(""" INSERT INTO entries (counter_id, user_id, "timestamp", increment) VALUES - (1, 1, date(), 1), - (1, 1, date(date(), '-1 days'), 2), - (1, 1, date(date(), '-3 days'), 3), - (2, 1, date(), 2), - (2, 1, date(date(), '-1 days'), 4), - (2, 1, date(date(), '-3 days'), 6) + (1, 1, date('2026-06-15'), 1), + (1, 1, date(date('2026-06-15'), '-1 days'), 2), + (1, 1, date(date('2026-06-15'), '-3 days'), 3), + (2, 1, date('2026-06-15'), 2), + (2, 1, date(date('2026-06-15'), '-1 days'), 4), + (2, 1, date(date('2026-06-15'), '-3 days'), 6) """) session.execute(query) session.commit() - stats = daily_stats.get_all_daily_analytics() + stats = daily_stats.get_all_daily_analytics('2026-06-15') assert json.loads(stats[::-1]["counters"].iloc[0])["Test"] == 1 assert json.loads(stats[::-1]["counters"].iloc[0])["Test2"] == 2 assert json.loads(stats[::-1]["counters"].iloc[1])["Test"] == 2 @@ -43,14 +43,14 @@ def test_daily_stats(): query = text(""" INSERT INTO entries (counter_id, user_id, "timestamp", increment) VALUES - (1, 1, date(), 1), - (1, 1, date(date(), '-1 days'), 2), - (1, 1, date(date(), '-3 days'), 3) + (1, 1, date('2026-06-15'), 1), + (1, 1, date(date('2026-06-15'), '-1 days'), 2), + (1, 1, date(date('2026-06-15'), '-3 days'), 3) """) session.execute(query) session.commit() - stats = daily_stats.get_daily_analytics(1) + stats = daily_stats.get_daily_analytics(1, '2026-06-15') assert stats["count"][0] == 1 assert stats["count"][1] == 2 assert stats["count"][2] == 0 @@ -64,17 +64,17 @@ def test_all_monthly_stats(): query = text(""" INSERT INTO entries (counter_id, user_id, "timestamp", increment) VALUES - (1, 1, date(), 1), - (1, 1, date(date(), 'start of month', '-1 month'), 2), - (1, 1, date(date(), 'start of month', '-3 months'), 3), - (2, 1, date(), 2), - (2, 1, date(date(), 'start of month', '-2 months'), 4), - (2, 1, date(date(), 'start of month', '-3 months'), 6) + (1, 1, date('2026-06-15'), 1), + (1, 1, date(date('2026-06-15'), 'start of month', '-1 month'), 2), + (1, 1, date(date('2026-06-15'), 'start of month', '-3 months'), 3), + (2, 1, date('2026-06-15'), 2), + (2, 1, date(date('2026-06-15'), 'start of month', '-2 months'), 4), + (2, 1, date(date('2026-06-15'), 'start of month', '-3 months'), 6) """) session.execute(query) session.commit() - stats = monthly_stats.get_all_monthly_analytics() + stats = monthly_stats.get_all_monthly_analytics('2026-06-15') assert json.loads(stats[::-1]["counters"].iloc[0])["Test"] == 1 assert json.loads(stats[::-1]["counters"].iloc[0])["Test2"] == 2 assert json.loads(stats[::-1]["counters"].iloc[1])["Test"] == 2 @@ -89,14 +89,14 @@ def test_monthly_stats(): query = text(""" INSERT INTO entries (counter_id, user_id, "timestamp", increment) VALUES - (1, 1, date(), 1), - (1, 1, date(date(), '-1 months'), 2), - (1, 1, date(date(), '-3 months'), 3) + (1, 1, date('2026-06-15'), 1), + (1, 1, date(date('2026-06-15'), '-1 months'), 2), + (1, 1, date(date('2026-06-15'), '-3 months'), 3) """) session.execute(query) session.commit() - stats = monthly_stats.get_monthly_analytics(1) + stats = monthly_stats.get_monthly_analytics(1, '2026-06-15') assert stats[::-1]["count"].iloc[0] == 1 assert stats[::-1]["count"].iloc[1] == 2 assert stats[::-1]["count"].iloc[2] == 0 @@ -110,17 +110,17 @@ def test_all_yearly_stats(): query = text(""" INSERT INTO entries (counter_id, user_id, "timestamp", increment) VALUES - (1, 1, date(), 1), - (1, 1, date(date(), '-1 year'), 2), - (1, 1, date(date(), '-3 years'), 3), - (2, 1, date(), 2), - (2, 1, date(date(), '-2 years'), 4), - (2, 1, date(date(), '-3 years'), 6) + (1, 1, date('2026-06-15'), 1), + (1, 1, date(date('2026-06-15'), '-1 year'), 2), + (1, 1, date(date('2026-06-15'), '-3 years'), 3), + (2, 1, date('2026-06-15'), 2), + (2, 1, date(date('2026-06-15'), '-2 years'), 4), + (2, 1, date(date('2026-06-15'), '-3 years'), 6) """) session.execute(query) session.commit() - stats = yearly_stats.get_all_yearly_analytics() + stats = yearly_stats.get_all_yearly_analytics('2026-06-15') assert json.loads(stats[::-1]["counters"].iloc[0])["Test"] == 1 assert json.loads(stats[::-1]["counters"].iloc[0])["Test2"] == 2 assert json.loads(stats[::-1]["counters"].iloc[1])["Test"] == 2 @@ -135,14 +135,14 @@ def test_yearly_stats(): query = text(""" INSERT INTO entries (counter_id, user_id, "timestamp", increment) VALUES - (1, 1, date(), 1), - (1, 1, date(date(), '-1 years'), 2), - (1, 1, date(date(), '-3 years'), 3) + (1, 1, date('2026-06-15'), 1), + (1, 1, date(date('2026-06-15'), '-1 years'), 2), + (1, 1, date(date('2026-06-15'), '-3 years'), 3) """) session.execute(query) session.commit() - stats = yearly_stats.get_yearly_analytics(1) + stats = yearly_stats.get_yearly_analytics(1, '2026-06-15') assert stats[::-1]["count"].iloc[0] == 1 assert stats[::-1]["count"].iloc[1] == 2 assert stats[::-1]["count"].iloc[2] == 0 @@ -156,17 +156,17 @@ def test_all_weekly_stats(): query = text(""" INSERT INTO entries (counter_id, user_id, "timestamp", increment) VALUES - (1, 1, date(), 1), - (1, 1, date(date(), '-7 days'), 2), - (1, 1, date(date(), '-21 days'), 3), - (2, 1, date(), 2), - (2, 1, date(date(), '-14 days'), 4), - (2, 1, date(date(), '-21 days'), 6) + (1, 1, date('2026-06-15'), 1), + (1, 1, date(date('2026-06-15'), '-7 days'), 2), + (1, 1, date(date('2026-06-15'), '-21 days'), 3), + (2, 1, date('2026-06-15'), 2), + (2, 1, date(date('2026-06-15'), '-14 days'), 4), + (2, 1, date(date('2026-06-15'), '-21 days'), 6) """) session.execute(query) session.commit() - stats = weekly_stats.get_all_weekly_analytics() + stats = weekly_stats.get_all_weekly_analytics('2026-06-15') assert json.loads(stats[::-1]["counters"].iloc[0])["Test"] == 1 assert json.loads(stats[::-1]["counters"].iloc[0])["Test2"] == 2 assert json.loads(stats[::-1]["counters"].iloc[1])["Test"] == 2 @@ -181,14 +181,14 @@ def test_weekly_stats(): query = text(""" INSERT INTO entries (counter_id, user_id, "timestamp", increment) VALUES - (1, 1, date(), 1), - (1, 1, date(date(), '-7 days'), 2), - (1, 1, date(date(), '-21 days'), 3) + (1, 1, date('2026-06-15'), 1), + (1, 1, date(date('2026-06-15'), '-7 days'), 2), + (1, 1, date(date('2026-06-15'), '-21 days'), 3) """) session.execute(query) session.commit() - stats = weekly_stats.get_weekly_analytics(1) + stats = weekly_stats.get_weekly_analytics(1, '2026-06-15') assert stats["count"][0] == 1 assert stats["count"][1] == 2 assert stats["count"][2] == 0 diff --git a/tests/database/user_db_test.py b/tests/database/user_db_test.py index a8e9e42..bd182fe 100644 --- a/tests/database/user_db_test.py +++ b/tests/database/user_db_test.py @@ -1,40 +1,41 @@ import streamlit -import queries.user as user +import queries.user as user_query +import user def test_get_default_user(): - users = user.find_default_user() + users = user_query.find_default_user() assert len(users) == 1 assert users["email"][0] == "default" def test_update_default_user_and_find_user(): - user.update_default_user(email="test@testbase.com", name="Test User", oidc_user_id="1111-2222-3333") + user_query.update_default_user(email="test@testbase.com", name="Test User", oidc_user_id="1111-2222-3333") - users = user.find_default_user() + users = user_query.find_default_user() assert len(users) == 0 - users = user.find_user_by_oidc_id("1111-2222-3333") + users = user_query.find_user_by_oidc_id("1111-2222-3333") assert len(users) == 1 assert users["email"][0] == "test@testbase.com" assert users["name"][0] == "Test User" assert users["oidc_user_id"][0] == "1111-2222-3333" - users = user.find_user_by_email("test@testbase.com") + users = user_query.find_user_by_email("test@testbase.com") assert len(users) == 1 assert users["email"][0] == "test@testbase.com" assert users["name"][0] == "Test User" assert users["oidc_user_id"][0] == "1111-2222-3333" def test_add_user(): - user.create_user(email="test@testbase.com", name="Test User", oidc_user_id="333-4444-5555") + user_query.create_user(email="test@testbase.com", name="Test User", oidc_user_id="333-4444-5555") - users = user.find_user_by_oidc_id("333-4444-5555") + users = user_query.find_user_by_oidc_id("333-4444-5555") assert len(users) == 1 assert users["email"][0] == "test@testbase.com" assert users["name"][0] == "Test User" assert users["oidc_user_id"][0] == "333-4444-5555" - users = user.find_user_by_email("test@testbase.com") + users = user_query.find_user_by_email("test@testbase.com") assert len(users) == 1 assert users["email"][0] == "test@testbase.com" assert users["name"][0] == "Test User" diff --git a/tests/ui/settings_ui_test.py b/tests/ui/settings_ui_test.py index d09d29c..611071f 100644 --- a/tests/ui/settings_ui_test.py +++ b/tests/ui/settings_ui_test.py @@ -5,7 +5,7 @@ def test_change_theme(app): app.run() app.switch_page("pages/settings.py").run() - assert app.session_state.current_theme =="light", "Light theme should be default" + assert app.session_state.current_theme == "light", "Light theme should be default" assert app.button[0].label == "Light" assert app.button[0].disabled == True, "Light theme should be selected" @@ -16,6 +16,8 @@ def test_change_theme(app): app.button[1].click().run() assert "dark" == crud.get_theme() + assert app.button[0].disabled == False, "Light theme should be de-selected" + assert app.button[1].disabled == True, "Dark theme should be selected" def test_change_color_palette(app): app.run()