209 lines
11 KiB
Python
209 lines
11 KiB
Python
import streamlit as st
|
|
from streamlit.delta_generator import DeltaGenerator
|
|
|
|
from typing import Dict, List, Any
|
|
import re
|
|
|
|
from streamlit_constants import DASHBOARD_CONFIG_OBJECT_KEY
|
|
from utility_classes.dashboard_config_parser import DashboardConfig, ExportModulePair
|
|
from utility_classes.base_report_page import BaseReportPage
|
|
from components.neoserra_export_link_grabber import NeoserraExportLinkGrabber
|
|
|
|
class AdminPanelPage(BaseReportPage):
|
|
"""
|
|
Provides a centralized administrative interface for configuring dashboard data sources.
|
|
|
|
Unlike standard report pages, this class does not generate or render data visualizations.
|
|
Instead, it hooks into the standard BaseReportPage lifecycle to render a configuration
|
|
form. It manages the retrieval, validation, and updating of Neoserra API export URLs
|
|
stored within the global DashboardConfig session state.
|
|
|
|
It employs strict regex validation to ensure data pipeline integrity, while also
|
|
providing a deliberate bypass mechanism to future-proof against external API changes.
|
|
"""
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__("Admin Panel")
|
|
|
|
self.neoserra_export_link_regex = re.compile(r"https://pasbdc.neoserra.com/api/export\?userid=[0-9]+&appid=[0-9]+&appkey=(([a-z]+)|([0-9]+)|([-]))+")
|
|
|
|
@staticmethod
|
|
def get_page_name():
|
|
"""
|
|
Retrieves the human-readable name of the admin panel for UI navigation.
|
|
|
|
:return: The static display name "Admin Panel".
|
|
:rtype: str
|
|
"""
|
|
return "Admin Panel"
|
|
|
|
def save_settings(self):
|
|
"""
|
|
Commits the current configuration state to persistent storage.
|
|
|
|
Extracts the DashboardConfig object from the Streamlit session state and triggers
|
|
its internal save mechanism, providing visual feedback to the user upon completion.
|
|
"""
|
|
try:
|
|
self.app_config.save()
|
|
st.toast("Settings saved!", icon='✅')
|
|
except Exception as e:
|
|
self.logger.exception(f"Error encountered while trying to save the dashboard config. Got {e}")
|
|
st.toast(f"Failed to save settings, a detailed message has been added to the log. {self.app_config.get_errors_contact_string()}")
|
|
st.stop()
|
|
|
|
def is_valid_neoserra_url(self, url):
|
|
"""
|
|
Validates a given URL against the expected Neoserra export API format.
|
|
|
|
Used as a gatekeeper during form submission to prevent malformed URLs from breaking
|
|
the dashboard's data ingestion pipeline.
|
|
|
|
:param url: The raw URL string to validate.
|
|
:type url: str
|
|
:return: True if the URL matches the expected Neoserra API pattern, False otherwise.
|
|
:rtype: bool
|
|
"""
|
|
if not url:
|
|
return False
|
|
|
|
return bool(re.match(self.neoserra_export_link_regex, url))
|
|
|
|
def render_controls(self, container: DeltaGenerator) -> Dict[str, Any]:
|
|
"""
|
|
Builds the administrative form and handles the validation/submission pipeline.
|
|
|
|
This method constructs a unified Streamlit form containing URL grabber widgets for
|
|
various data modules (Clients, Trainings, NBS, Funding). By wrapping the inputs
|
|
in a single form, it ensures that configuration updates are processed as a single
|
|
batch rather than triggering piecemeal app reruns.
|
|
|
|
During submission, it utilizes an "all-or-nothing" validation strategy. If any link
|
|
fails regex validation (and the manual bypass is not checked), the entire save
|
|
operation is aborted to prevent partial configuration states.
|
|
|
|
:param container: The Streamlit container to draw the administrative form onto.
|
|
:type container: DeltaGenerator
|
|
:return: An empty dictionary, as this page does not pass parameters to generate_figures.
|
|
:rtype: Dict[str, Any]
|
|
"""
|
|
|
|
admin_form = container.form(key=self.get_widget_key("admin_settings_form"))
|
|
|
|
client_list_links:ExportModulePair = self.app_config.get_clients_list_urls()
|
|
client_list_export_module_control = NeoserraExportLinkGrabber("Client List Export Module URLs", prev_fy_initial_value=client_list_links.prev_fy, current_fy_initial_value=client_list_links.current_fy)
|
|
client_list_export_module_control.render(admin_form)
|
|
|
|
trainings_links:ExportModulePair = self.app_config.get_trainings_urls()
|
|
trainings_export_module_control = NeoserraExportLinkGrabber("Trainings Export Module URLs", prev_fy_initial_value=trainings_links.prev_fy, current_fy_initial_value=trainings_links.current_fy)
|
|
trainings_export_module_control.render(admin_form)
|
|
|
|
nbs_links:ExportModulePair = self.app_config.get_nbs_milestones_urls()
|
|
nbs_export_module_control = NeoserraExportLinkGrabber("New Business Starts Export Module URLs", prev_fy_initial_value=nbs_links.prev_fy, current_fy_initial_value=nbs_links.current_fy)
|
|
nbs_export_module_control.render(admin_form)
|
|
|
|
funding_links:ExportModulePair = self.app_config.get_funding_milestones_urls()
|
|
funding_export_module_control = NeoserraExportLinkGrabber("Capital Funding Export Module URLs", prev_fy_initial_value=funding_links.prev_fy, current_fy_initial_value=funding_links.current_fy)
|
|
funding_export_module_control.render(admin_form)
|
|
|
|
api_container = admin_form.container(key=self.get_widget_key("api_container"))
|
|
|
|
api_container.write("These settings control the external APIs used by the application. The census year is used for the US census, USDA, and BLS APIs")
|
|
api_container.write("The census year must be a year for which a census was actually conducted in the US. This cannot be an arbitrary year.")
|
|
|
|
census_year = api_container.text_input(label="Census Year", value=self.app_config.get_census_year(), key=self.get_widget_key("census_year"))
|
|
|
|
api_container.write("This is the api key for the USDA api which can be found at: https://www.ers.usda.gov/developer/data-apis")
|
|
|
|
usda_api_key = api_container.text_input(label="USDA API Key", value=self.app_config.get_usda_api_key(), key=self.get_widget_key("usda_api_key"))
|
|
|
|
submitted = admin_form.form_submit_button("Save", type="primary", key=self.get_widget_key("save_settings_form"))
|
|
|
|
advanced_settings_expander = container.expander(label="Advanced Settings", key=self.get_widget_key("advanced_settings_expander"))
|
|
advanced_settings_expander.markdown("Selecting this check box will disable the link validation done on export module URLS on this page. This setting is present so that, in the future, if Neoserra changes the format of export url links, this application can still be used.")
|
|
advanced_settings_expander.markdown("Be warned that no other module does validation on these values, so only check this if you know what you are doing. Preferably you should modify the regex in the AdminPanelPage class to fit the new link format")
|
|
bypass_link_validation = advanced_settings_expander.checkbox(label="Bypass export URL validation", value=False, key=self.get_widget_key("bypass_link_validation"))
|
|
|
|
if submitted:
|
|
# Define the links to validate: (URL, Error Message, Config Setter)
|
|
links_to_process = [
|
|
(client_list_export_module_control.current_fy_url, "The current fiscal year clients list link",
|
|
self.app_config.set_clients_list_current_fy_url),
|
|
(client_list_export_module_control.prev_fy_url, "The previous fiscal year clients list link",
|
|
self.app_config.set_clients_list_prev_fy_url),
|
|
(trainings_export_module_control.current_fy_url, "The current fiscal year trainings link",
|
|
self.app_config.set_trainings_current_fy_url),
|
|
(trainings_export_module_control.prev_fy_url, "The previous fiscal year trainings link",
|
|
self.app_config.set_trainings_prev_fy_url),
|
|
(nbs_export_module_control.current_fy_url, "The current fiscal year NBS milestones link",
|
|
self.app_config.set_nbs_milestones_current_fy_url),
|
|
(nbs_export_module_control.prev_fy_url, "The previous fiscal year NBS milestones link",
|
|
self.app_config.set_nbs_milestones_prev_fy_url),
|
|
(funding_export_module_control.current_fy_url, "The current fiscal year funding milestones link",
|
|
self.app_config.set_funding_milestones_current_fy_url),
|
|
(funding_export_module_control.prev_fy_url, "The previous fiscal year funding milestones link",
|
|
self.app_config.set_funding_milestones_prev_fy_url),
|
|
]
|
|
|
|
# Validate all links before saving any
|
|
if not bypass_link_validation:
|
|
for url, label, setter in links_to_process:
|
|
if not self.is_valid_neoserra_url(url):
|
|
st.toast(f"{label} is not a valid Neoserra export module URL.", icon='❌')
|
|
return {}
|
|
|
|
# If all validations pass (or are bypassed), save the values
|
|
for url, label, setter in links_to_process:
|
|
setter(url)
|
|
|
|
if usda_api_key is None or usda_api_key == "":
|
|
st.toast(f"USDA API Key cannot be blank!", icon='❌')
|
|
return {}
|
|
|
|
if census_year is None or census_year == "":
|
|
st.toast(f"Census year cannot be blank!", icon='❌')
|
|
return {}
|
|
|
|
# Set the API configuration values
|
|
self.app_config.set_usda_api_key(usda_api_key)
|
|
self.app_config.set_census_year(census_year)
|
|
|
|
self.save_settings()
|
|
return {
|
|
}
|
|
|
|
def generate_figures(self, parameters: Dict[str, Any]):
|
|
"""
|
|
Overrides the standard data generation step as a no-op.
|
|
|
|
Because this page is strictly for configuration management, it circumvents
|
|
the data processing phase of the BaseReportPage lifecycle.
|
|
|
|
:param parameters: The parameter dictionary (empty from render_controls).
|
|
:type parameters: Dict[str, Any]
|
|
:return: An empty dictionary representing no figure data.
|
|
:rtype: Dict[str, Any]
|
|
"""
|
|
return {}
|
|
|
|
def render_figures(self, container: DeltaGenerator, output_data: Dict[str, Any]):
|
|
"""
|
|
Overrides the standard figure rendering step as a no-op.
|
|
|
|
Does nothing, as there are no visual charts or graphs to draw for the Admin Panel.
|
|
|
|
:param container: The Streamlit container.
|
|
:type container: DeltaGenerator
|
|
:param output_data: The empty output data from generate_figures.
|
|
:type output_data: Dict[str, Any]
|
|
"""
|
|
pass
|
|
|
|
def get_syncable_figure_keys(self) -> List[str]:
|
|
"""
|
|
Declares that this page has no visual figures requiring axis synchronization.
|
|
|
|
:return: An empty list.
|
|
:rtype: List[str]
|
|
"""
|
|
return [] |