diff --git a/routes/main_routes.py b/routes/main_routes.py index 39f1be7..a3b2efc 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -7,6 +7,13 @@ from collections import OrderedDict from urllib.parse import unquote main = Blueprint('main', __name__) + + +def _decode_path_name(name): + """Decode one path segment left encoded after Flask's URL decoding.""" + return unquote(name) + + # ----------------------------------------------------------------------------- # MODEL GROUPING FOR NAVIGATION DROPDOWN # ----------------------------------------------------------------------------- @@ -667,7 +674,7 @@ def models_in_group(group_name): model_database = current_app.config.get('MODEL_DATABASE', {}) # Some clients and previously rendered links can encode the path segment # twice, leaving a literal ``%20`` after Flask's first URL decode. - group_name = unquote(group_name) + group_name = _decode_path_name(group_name) # Validate group_name if group_name not in MODEL_GROUPS: flash(f"Invalid model group: {group_name}", "danger") @@ -690,6 +697,7 @@ def models_in_group(group_name): def model_details(model_name): """Display details for a specific model""" try: + model_name = _decode_path_name(model_name) model_info = get_model_details(model_name) if model_info: return render_template('model_details.html', @@ -703,6 +711,7 @@ def model_details(model_name): def model_interpretation(model_name): """Display interpretation guide for a specific model""" try: + model_name = _decode_path_name(model_name) model_info = get_model_details(model_name) if not model_info: return render_template('error.html', error="Model not found") @@ -720,6 +729,7 @@ def model_interpretation(model_name): def download_interpretation(model_name): """Generate and download interpretation guide as HTML file""" try: + model_name = _decode_path_name(model_name) model_info = get_model_details(model_name) if not model_info: return render_template('error.html', error="Model not found") diff --git a/tests/test_main_routes.py b/tests/test_main_routes.py index 4e34676..9f45860 100644 --- a/tests/test_main_routes.py +++ b/tests/test_main_routes.py @@ -1,7 +1,12 @@ """ Test the main routes and analysis functionality. """ +import sys +from types import ModuleType + from models import Analysis + + class TestMainRoutes: """Test main application routes.""" def test_home_page(self, client): @@ -20,6 +25,41 @@ def test_double_encoded_model_group_url(self, client): assert response.status_code == 200 assert b'Invalid model group' not in response.data assert b'Linear Regression' in response.data + + def test_double_encoded_model_detail_urls(self, client, monkeypatch): + """Encoded model links work across detail and interpretation routes.""" + interpretation_module = ModuleType('utils.interpretation') + interpretation_module.generate_interpretation_data = ( + lambda _model_name, _model_info: {} + ) + monkeypatch.setitem( + sys.modules, + 'utils.interpretation', + interpretation_module, + ) + + detail_response = client.get('/model/Linear%2520Regression') + interpretation_response = client.get( + '/model/Linear%2520Regression/interpretation' + ) + download_response = client.get( + '/model/Linear%2520Regression/download-interpretation' + ) + + for response in ( + detail_response, + interpretation_response, + download_response, + ): + assert response.status_code == 200 + assert b'Model not found' not in response.data + + assert b'Linear Regression' in detail_response.data + assert ( + 'Linear_Regression_interpretation_guide.html' + in download_response.headers['Content-Disposition'] + ) + def test_analysis_form_submission_authenticated(self, authenticated_client, sample_analysis_data, app): """Test analysis form submission with authenticated user.""" response = authenticated_client.post('/results', data=sample_analysis_data, follow_redirects=True)