From 09fc4bbc3ef66b02f0711dadaf3b10ed53d9d968 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Wed, 10 Dec 2025 12:57:14 -0600 Subject: Feature Flags: Generically deal with HTTP-based feature flags. * Define a default `FEATURE_FLAGS_HTTP` configuration variable that's an empty list to help defining http-based feature flags that can be used to turn on/off features * Build macro to include hidden fields for feature flags where necessary. * Extend flask's `url_for` function to deal with defined feature flags in a mostly transparent way --- uploader/__init__.py | 12 ++++++-- uploader/base_routes.py | 5 ++-- uploader/default_settings.py | 4 +++ uploader/expression_data/views.py | 2 +- uploader/flask_extensions.py | 33 ++++++++++++++++++++++ uploader/genotypes/views.py | 2 +- uploader/oauth2/views.py | 2 +- uploader/phenotypes/views.py | 4 +-- uploader/platforms/views.py | 2 +- uploader/population/views.py | 2 +- uploader/publications/views.py | 2 +- uploader/route_utils.py | 2 +- uploader/samples/views.py | 2 +- uploader/species/views.py | 12 ++++---- uploader/templates/base.html | 2 +- uploader/templates/macro-forms.html | 9 ++++++ uploader/templates/populations/sui-base.html | 3 +- .../templates/populations/sui-view-population.html | 9 ++---- uploader/templates/species/sui-base.html | 2 +- uploader/templates/species/sui-view-species.html | 1 + uploader/templates/sui-base.html | 2 +- uploader/templates/sui-index.html | 4 +-- 22 files changed, 82 insertions(+), 36 deletions(-) create mode 100644 uploader/flask_extensions.py create mode 100644 uploader/templates/macro-forms.html (limited to 'uploader') diff --git a/uploader/__init__.py b/uploader/__init__.py index b7f0ab9..7425b38 100644 --- a/uploader/__init__.py +++ b/uploader/__init__.py @@ -22,6 +22,7 @@ from .files.views import files from .species import speciesbp from .publications import pubbp from .oauth2.views import oauth2 +from .flask_extensions import url_for from .expression_data import exprdatabp from .errors import register_error_handlers from .background_jobs import background_jobs_bp @@ -109,17 +110,22 @@ def create_app(config: Optional[dict] = None): setup_logging(app) setup_modules_logging(app.logger, ( "uploader.base_routes", + "uploader.flask_extensions", "uploader.publications.models", "uploader.publications.datatables", "uploader.phenotypes.models")) # setup jinja2 symbols - app.add_template_global(lambda : request.url, name="request_url") + app.add_template_global(user_logged_in) + app.add_template_global(url_for, name="url_for") app.add_template_global(authserver_authorise_uri) + app.add_template_global(lambda : request.url, name="request_url") app.add_template_global(lambda: app.config["GN2_SERVER_URL"], name="gn2server_uri") - app.add_template_global(user_logged_in) - app.add_template_global(lambda : session.user_details()["email"], name="user_email") + app.add_template_global(lambda : session.user_details()["email"], + name="user_email") + app.add_template_global(lambda: app.config["FEATURE_FLAGS_HTTP"], + name="http_feature_flags") Session(app) diff --git a/uploader/base_routes.py b/uploader/base_routes.py index 3d0e1b2..80a15a0 100644 --- a/uploader/base_routes.py +++ b/uploader/base_routes.py @@ -6,12 +6,12 @@ from urllib.parse import urljoin from gn_libs.mysqldb import database_connection from flask import (flash, request, - url_for, redirect, Blueprint, current_app as app, send_from_directory) +from uploader.flask_extensions import url_for from uploader.ui import make_template_renderer from uploader.oauth2.client import user_logged_in from uploader.species.models import all_species, species_by_id @@ -53,8 +53,7 @@ def index(): return redirect(url_for("base.index", streamlined_ui=streamlined_ui)) return redirect(url_for("species.view_species", - species_id=species["SpeciesId"], - streamlined_ui=streamlined_ui)) + species_id=species["SpeciesId"])) def appenv(): diff --git a/uploader/default_settings.py b/uploader/default_settings.py index c5986ab..bb3a967 100644 --- a/uploader/default_settings.py +++ b/uploader/default_settings.py @@ -29,3 +29,7 @@ SESSION_FILESYSTEM_CACHE_HASH_METHOD = None # default: hashlib.md5 JWKS_ROTATION_AGE_DAYS = 7 # Days (from creation) to keep a JWK in use. JWKS_DELETION_AGE_DAYS = 14 # Days (from creation) to keep a JWK around before deleting it. + + +## --- Feature flags --- +FEATURE_FLAGS_HTTP = [] diff --git a/uploader/expression_data/views.py b/uploader/expression_data/views.py index 7629f3e..0b318b7 100644 --- a/uploader/expression_data/views.py +++ b/uploader/expression_data/views.py @@ -11,7 +11,6 @@ from werkzeug.utils import secure_filename from gn_libs.mysqldb import database_connection from flask import (flash, request, - url_for, redirect, Blueprint, current_app as app) @@ -19,6 +18,7 @@ from flask import (flash, from quality_control.errors import InvalidValue, DuplicateHeading from uploader import jobs +from uploader.flask_extensions import url_for from uploader.datautils import order_by_family from uploader.ui import make_template_renderer from uploader.authorisation import require_login diff --git a/uploader/flask_extensions.py b/uploader/flask_extensions.py new file mode 100644 index 0000000..30fbad7 --- /dev/null +++ b/uploader/flask_extensions.py @@ -0,0 +1,33 @@ +"""Custom extensions to the default flask functions/classes.""" +import logging +from typing import Any, Optional + +from flask import (request, current_app as app, url_for as flask_url_for) + +logger = logging.getLogger(__name__) + + +def url_for( + endpoint: str, + _anchor: Optional[str] = None, + _method: Optional[str] = None, + _scheme: Optional[str] = None, + _external: Optional[bool] = None, + **values: Any) -> str: + """Extension to flask's `url_for` function.""" + flags = {} + for flag in app.config["FEATURE_FLAGS_HTTP"]: + flag_value = (request.args.get(flag) or request.form.get(flag) or "").strip() + if bool(flag_value): + flags[flag] = flag_value + continue + continue + + logger.debug("HTTP FEATURE FLAGS: %s, other variables: %s", flags, values) + return flask_url_for(endpoint=endpoint, + _anchor=_anchor, + _method=_method, + _scheme=_scheme, + _external=_external, + **values, + **flags) diff --git a/uploader/genotypes/views.py b/uploader/genotypes/views.py index 54c2444..d991614 100644 --- a/uploader/genotypes/views.py +++ b/uploader/genotypes/views.py @@ -3,12 +3,12 @@ from MySQLdb.cursors import DictCursor from gn_libs.mysqldb import database_connection from flask import (flash, request, - url_for, redirect, Blueprint, render_template, current_app as app) +from uploader.flask_extensions import url_for from uploader.ui import make_template_renderer from uploader.oauth2.client import oauth2_post from uploader.authorisation import require_login diff --git a/uploader/oauth2/views.py b/uploader/oauth2/views.py index 1ee4257..05f8542 100644 --- a/uploader/oauth2/views.py +++ b/uploader/oauth2/views.py @@ -4,13 +4,13 @@ from urllib.parse import urljoin, urlparse, urlunparse from flask import ( flash, jsonify, - url_for, request, redirect, Blueprint, current_app as app) from uploader import session +from uploader.flask_extensions import url_for from uploader import monadic_requests as mrequests from uploader.monadic_requests import make_error_handler diff --git a/uploader/phenotypes/views.py b/uploader/phenotypes/views.py index 15d2b6c..2afd8a3 100644 --- a/uploader/phenotypes/views.py +++ b/uploader/phenotypes/views.py @@ -23,7 +23,6 @@ from gn_libs.mysqldb import database_connection from flask import (flash, request, - url_for, jsonify, redirect, Blueprint, @@ -35,7 +34,8 @@ from r_qtl import exceptions as rqe from uploader import jobs from uploader import session -from uploader.files import save_file#, fullpath +from uploader.files import save_file +from uploader.flask_extensions import url_for from uploader.ui import make_template_renderer from uploader.oauth2.client import oauth2_post from uploader.oauth2.tokens import request_token diff --git a/uploader/platforms/views.py b/uploader/platforms/views.py index d12a9ef..ba0f0ef 100644 --- a/uploader/platforms/views.py +++ b/uploader/platforms/views.py @@ -4,11 +4,11 @@ from gn_libs.mysqldb import database_connection from flask import ( flash, request, - url_for, redirect, Blueprint, current_app as app) +from uploader.flask_extensions import url_for from uploader.ui import make_template_renderer from uploader.authorisation import require_login from uploader.species.models import all_species, species_by_id diff --git a/uploader/population/views.py b/uploader/population/views.py index cfc3b70..8d4ceb7 100644 --- a/uploader/population/views.py +++ b/uploader/population/views.py @@ -7,12 +7,12 @@ from MySQLdb.cursors import DictCursor from gn_libs.mysqldb import database_connection from flask import (flash, request, - url_for, redirect, Blueprint, current_app as app) from uploader.samples.views import samplesbp +from uploader.flask_extensions import url_for from uploader.oauth2.client import oauth2_post from uploader.ui import make_template_renderer from uploader.authorisation import require_login diff --git a/uploader/publications/views.py b/uploader/publications/views.py index 805d6f0..4ec832f 100644 --- a/uploader/publications/views.py +++ b/uploader/publications/views.py @@ -5,12 +5,12 @@ from gn_libs.mysqldb import database_connection from flask import ( flash, request, - url_for, redirect, Blueprint, render_template, current_app as app) +from uploader.flask_extensions import url_for from uploader.authorisation import require_login from uploader.route_utils import redirect_to_next diff --git a/uploader/route_utils.py b/uploader/route_utils.py index 53247e6..4449475 100644 --- a/uploader/route_utils.py +++ b/uploader/route_utils.py @@ -3,7 +3,6 @@ import logging from json.decoder import JSONDecodeError from flask import (flash, - url_for, request, redirect, render_template, @@ -11,6 +10,7 @@ from flask import (flash, from gn_libs.mysqldb import database_connection +from uploader.flask_extensions import url_for from uploader.datautils import base64_encode_dict, base64_decode_to_dict from uploader.population.models import (populations_by_species, population_by_species_and_id) diff --git a/uploader/samples/views.py b/uploader/samples/views.py index 4705a96..f8baf7e 100644 --- a/uploader/samples/views.py +++ b/uploader/samples/views.py @@ -7,13 +7,13 @@ from pathlib import Path from redis import Redis from flask import (flash, request, - url_for, redirect, Blueprint, current_app as app) from uploader import jobs from uploader.files import save_file +from uploader.flask_extensions import url_for from uploader.ui import make_template_renderer from uploader.authorisation import require_login from uploader.input_validation import is_integer_input diff --git a/uploader/species/views.py b/uploader/species/views.py index a490b0f..20acd01 100644 --- a/uploader/species/views.py +++ b/uploader/species/views.py @@ -4,13 +4,13 @@ from pymonad.either import Left, Right, Either from gn_libs.mysqldb import database_connection from flask import (flash, request, - url_for, redirect, Blueprint, current_app as app) from uploader.population import popbp from uploader.platforms import platformsbp +from uploader.flask_extensions import url_for from uploader.ui import make_template_renderer from uploader.oauth2.client import oauth2_get, oauth2_post from uploader.authorisation import require_login, require_token @@ -52,21 +52,19 @@ def view_species(species_id: int): if bool(population): return redirect(url_for("species.populations.view_population", species_id=species_id, - population_id=population["Id"], - streamlined_ui=streamlined_ui)) + population_id=population["Id"])) return render_template( ("species/sui-view-species.html" if bool(streamlined_ui) else "species/view-species.html"), species=species, activelink="view-species", - streamlined_ui=streamlined_ui, populations=populations_by_species(conn, species["SpeciesId"])) flash("Could not find a species with the given identifier.", "alert-danger") - return redirect(url_for( - ("base.index" if streamlined_ui else "species.view_species"), - streamlined_ui=streamlined_ui)) + return redirect(url_for("base.index" + if streamlined_ui + else "species.view_species")) @speciesbp.route("/create", methods=["GET", "POST"]) @require_login diff --git a/uploader/templates/base.html b/uploader/templates/base.html index 3c0d0d4..d521ccb 100644 --- a/uploader/templates/base.html +++ b/uploader/templates/base.html @@ -45,7 +45,7 @@
Upload Samples @@ -88,8 +87,7 @@ Upload Phenotypes @@ -102,8 +100,7 @@ system will be added. This does not delete any existing data. upload genotypes diff --git a/uploader/templates/species/sui-base.html b/uploader/templates/species/sui-base.html index 5d2e6e3..f7b4fef 100644 --- a/uploader/templates/species/sui-base.html +++ b/uploader/templates/species/sui-base.html @@ -3,7 +3,7 @@ {%block breadcrumbs%} {{super()}}