Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build-windows-executable-app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,10 @@ jobs:
Copy-Item "openms-bin/${file}.exe" -Destination "streamlit_exe/${file}.exe"
}

- name: Remove server address for local Windows deployment
run: |
(Get-Content streamlit_exe/.streamlit/config.toml) -notmatch '^address' | Set-Content streamlit_exe/.streamlit/config.toml

- name: Generate Readme.txt
shell: bash
run: |
Expand Down
9 changes: 2 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,14 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
# Requirements file generated with python=3.11
# Requirements file generated with python=3.12; tested with python=3.11
python-version: ["3.11"]
steps:
- uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v3
- uses: actions/setup-python@v4
with:
activate-environment: openms
python-version: ${{ matrix.python-version }}
channels: defaults,bioconda,conda-forge

- name: Install OpenMS
run: |
conda install openms -y
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/test-win-exe-w-embed-py.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ jobs:
cp settings.json streamlit_exe
cp default-parameters.json streamlit_exe
cp ${{ env.APP_NAME }}.bat streamlit_exe

- name: Remove server address for local Windows deployment
run: |
(Get-Content streamlit_exe/.streamlit/config.toml) -notmatch '^address' | Set-Content streamlit_exe/.streamlit/config.toml

- name: Generate Readme.txt
shell: bash
Expand Down
3 changes: 0 additions & 3 deletions .streamlit/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ developmentMode = false
[server]
maxUploadSize = 1000 #MB
port = 8501 # should be same as configured in deployment repo
address = "0.0.0.0"
enableCORS = false
enableXsrfProtection = false


[theme]
Expand Down
2 changes: 1 addition & 1 deletion gdpr_consent/dist/bundle.js

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions gdpr_consent/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ function onRender(event: Event): void {
}
)
}
if (data.args['matomo']) {
klaroConfig.services.push(
{
name: 'matomo',
purposes: ['analytics'],
onAccept: callback,
onDecline: callback,
}
)
}

// Create a new script element
var script = document.createElement('script')
Expand Down
21 changes: 21 additions & 0 deletions hooks/hook-analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ def piwik_pro_body(piwik_tag):
"""


def matomo_head(matomo_url, matomo_tag):
return f"""
<!-- Matomo Tag Manager -->
<script>
var _mtm = window._mtm = window._mtm || [];
_mtm.push({{'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'}});
(function() {{
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src='{matomo_url}/container_{matomo_tag}.js'; s.parentNode.insertBefore(g,s);
}})();
</script>
<!-- End Matomo Tag Manager -->
"""


if __name__ == '__main__':

# Load configuration
Expand All @@ -79,6 +94,12 @@ def piwik_pro_body(piwik_tag):
piwik_tag = settings['analytics']['piwik-pro']['tag']
index = patch_body(index, piwik_pro_body(piwik_tag))

# Configure matomo tag manager
if settings['analytics']['matomo']['enabled']:
matomo_url = settings['analytics']['matomo']['url']
matomo_tag = settings['analytics']['matomo']['tag']
index = patch_head(index, matomo_head(matomo_url, matomo_tag))

# Save index.html
with open(index_path, 'w') as f:
f.write(index)
7 changes: 6 additions & 1 deletion settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@
"tag": ""
},
"piwik-pro": {
"enabled": false,
"tag": ""
},
"matomo": {
"enabled": true,
"tag": "57690c44-d635-43b0-ab43-f8bd3064ca06"
"url": "https://cdn.matomo.cloud/openms.matomo.cloud",
"tag": "yDGK8bfY"
}
},
"online_deployment": false,
Expand Down
5 changes: 3 additions & 2 deletions src/common/captcha_.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,13 @@ def captcha_control():
# Check if consent for tracking was given
ga = st.session_state.settings['analytics']['google-analytics']['enabled']
pp = st.session_state.settings['analytics']['piwik-pro']['enabled']
if (ga or pp) and (st.session_state.tracking_consent is None):
mt = st.session_state.settings['analytics']['matomo']['enabled']
if (ga or pp or mt) and (st.session_state.tracking_consent is None):
consent_component = st_components.declare_component("gdpr_consent", path=Path("gdpr_consent"))
with st.spinner():
# Ask for consent
st.session_state.tracking_consent = consent_component(
google_analytics=ga, piwik_pro=pp
google_analytics=ga, piwik_pro=pp, matomo=mt
)
if st.session_state.tracking_consent is None:
# No response by user yet
Expand Down
17 changes: 17 additions & 0 deletions src/common/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,23 @@ def page_setup(page: str = "") -> dict[str, Any]:
width=1,
height=1,
)
if (st.session_state.settings["analytics"]["matomo"]["enabled"]) and (
st.session_state.tracking_consent["matomo"] == True
Comment on lines +408 to +409
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/common/common.py | sed -n '405,415p'

Repository: OpenMS/quantms-web

Length of output: 494


🏁 Script executed:

rg "tracking_consent" src/common/common.py -B 2 -A 2

Repository: OpenMS/quantms-web

Length of output: 935


🏁 Script executed:

rg "tracking_consent\s*=" src/common/common.py -B 3 -A 3 | head -50

Repository: OpenMS/quantms-web

Length of output: 409


🏁 Script executed:

rg "tracking_consent\[" --type py -B 1 -A 1

Repository: OpenMS/quantms-web

Length of output: 744


🏁 Script executed:

rg "tracking_consent" src/ --type py -B 2 -A 2 | grep -E "(matomo|google-analytics|piwik-pro)" | head -20

Repository: OpenMS/quantms-web

Length of output: 925


🏁 Script executed:

rg "tracking_consent" src/ --type py -g "!common.py" -B 3 -A 3

Repository: OpenMS/quantms-web

Length of output: 1178


🏁 Script executed:

find . -name "gdpr_consent*" -o -name "*gdpr*" | head -20

Repository: OpenMS/quantms-web

Length of output: 76


🏁 Script executed:

ls -la gdpr_consent/ && find gdpr_consent -type f -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.tsx" | head -20

Repository: OpenMS/quantms-web

Length of output: 763


🏁 Script executed:

cat gdpr_consent/src/main.ts

Repository: OpenMS/quantms-web

Length of output: 4341


Use a truthy .get() consent check instead of direct dict indexing with equality comparison.

At line 409, directly accessing ["matomo"] with == True is non-idiomatic (Ruff E712) and risks KeyError if the key is absent. The suggested .get("matomo") approach is safer and more Pythonic. This pattern should be applied consistently to the other analytics checks at lines 398 and 403 as well.

✅ Suggested patch
-        if (st.session_state.settings["analytics"]["matomo"]["enabled"]) and (
-            st.session_state.tracking_consent["matomo"] == True
-        ):
+        if st.session_state.settings["analytics"]["matomo"]["enabled"] and (
+            st.session_state.tracking_consent.get("matomo")
+        ):
🧰 Tools
🪛 Ruff (0.15.5)

[error] 409-409: Avoid equality comparisons to True; use st.session_state.tracking_consent["matomo"]: for truth checks

Replace with st.session_state.tracking_consent["matomo"]

(E712)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common/common.py` around lines 408 - 409, Replace direct dict indexing
and equality comparisons for tracking consent with truthy .get() checks:
wherever you currently check st.session_state.tracking_consent["matomo"] == True
(and the similar checks for other analytics at the nearby analytics checks),
change them to use st.session_state.tracking_consent.get("matomo") (or
.get("<provider>") for the other providers) and combine that with the existing
analytics enabled condition (e.g., keep
st.session_state.settings["analytics"]["matomo"]["enabled"]) so the condition
becomes a safe truthy check that avoids KeyError and Ruff E712 issues.

):
html(
"""
<!DOCTYPE html>
<html lang="en">
<head></head>
<body><script>
window.parent._mtm = window.parent._mtm || [];
window.parent._mtm.push(['MTMSetConsentGiven']);
</script></body>
</html>
""",
width=1,
height=1,
)

# Determine the workspace for the current session
if ("workspace" not in st.session_state) or (
Expand Down
10 changes: 8 additions & 2 deletions test_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ def test_view_raw_ms_data(launch, example):

# Copy files from example-data/mzML to workspace mzML directory, add to selected files
for f in Path("example-data", "mzML").glob("*.mzML"):
shutil.copy(f, mzML_dir)
try:
shutil.copy(f, mzML_dir)
except shutil.SameFileError:
pass # File already exists as a symlink to the same source (on Linux)
launch.run()

## TODO: Figure out a way to select a spectrum to be displayed
Expand All @@ -119,7 +122,10 @@ def test_run_workflow(launch, example):

# Copy files from example-data/mzML to workspace mzML directory, add to selected files
for f in Path("example-data", "mzML").glob("*.mzML"):
shutil.copy(f, mzML_dir)
try:
shutil.copy(f, mzML_dir)
except shutil.SameFileError:
pass # File already exists as a symlink to the same source (on Linux)
launch.run()

## Select experiments to process
Expand Down
28 changes: 21 additions & 7 deletions tests/test_parameter_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,33 @@
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(PROJECT_ROOT)

# Create mock for streamlit before importing ParameterManager
# Mock streamlit before importing ParameterManager so that the imported module
# uses a controllable `st.session_state` (a plain dict) instead of the real one,
# which requires a running Streamlit app context. This allows unit-testing
# ParameterManager's preset logic (apply_preset, clear_parameter_session_state)
# in isolation.
mock_streamlit = MagicMock()
mock_streamlit.session_state = {}
sys.modules['streamlit'] = mock_streamlit

# Create mock for pyopenms
mock_pyopenms = MagicMock()
mock_pyopenms.__version__ = "2.9.1"
sys.modules['pyopenms'] = mock_pyopenms
# Temporarily replace streamlit in sys.modules so that ParameterManager's
# `import streamlit as st` picks up the mock. Restore immediately after import
# so other test files (e.g., test_gui.py AppTest) get the real streamlit.
_original_streamlit = sys.modules.get('streamlit')
sys.modules['streamlit'] = mock_streamlit

# Now import after mocks are set up
from src.workflow.ParameterManager import ParameterManager

if _original_streamlit is not None:
sys.modules['streamlit'] = _original_streamlit
else:
sys.modules.pop('streamlit', None)
Comment on lines +30 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard sys.modules restoration with try/finally to avoid cross-test contamination.

If the import on Line 33 fails, Lines 35-38 never run, leaving the mocked streamlit globally installed for subsequent tests.

🛡️ Suggested patch
 _original_streamlit = sys.modules.get('streamlit')
-sys.modules['streamlit'] = mock_streamlit
-
-from src.workflow.ParameterManager import ParameterManager
-
-if _original_streamlit is not None:
-    sys.modules['streamlit'] = _original_streamlit
-else:
-    sys.modules.pop('streamlit', None)
+try:
+    sys.modules['streamlit'] = mock_streamlit
+    from src.workflow.ParameterManager import ParameterManager
+finally:
+    if _original_streamlit is not None:
+        sys.modules['streamlit'] = _original_streamlit
+    else:
+        sys.modules.pop('streamlit', None)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_original_streamlit = sys.modules.get('streamlit')
sys.modules['streamlit'] = mock_streamlit
# Now import after mocks are set up
from src.workflow.ParameterManager import ParameterManager
if _original_streamlit is not None:
sys.modules['streamlit'] = _original_streamlit
else:
sys.modules.pop('streamlit', None)
_original_streamlit = sys.modules.get('streamlit')
try:
sys.modules['streamlit'] = mock_streamlit
from src.workflow.ParameterManager import ParameterManager
finally:
if _original_streamlit is not None:
sys.modules['streamlit'] = _original_streamlit
else:
sys.modules.pop('streamlit', None)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_parameter_presets.py` around lines 30 - 38, The current test
temporarily injects mock_streamlit into sys.modules and then imports
ParameterManager but does not guarantee restoration if the import raises; wrap
the import of ParameterManager (the code that uses mock_streamlit) in a
try/finally where you set sys.modules['streamlit']=mock_streamlit before the
try, perform from src.workflow.ParameterManager import ParameterManager inside
the try, and in the finally restore sys.modules to _original_streamlit (or pop
if None) using the existing _original_streamlit and mock_streamlit identifiers
so the mocked module cannot leak into other tests.


# Remove cached src.workflow modules that were imported with mocked streamlit so
# that AppTest (in test_gui.py) re-imports them fresh with the real package.
for _key in list(sys.modules.keys()):
if _key.startswith('src.workflow'):
sys.modules.pop(_key, None)


@pytest.fixture
def temp_workflow_dir():
Expand Down
11 changes: 0 additions & 11 deletions tests/test_topp_workflow_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(PROJECT_ROOT)

# Create mock for pyopenms to avoid dependency on actual OpenMS installation
mock_pyopenms = MagicMock()
mock_pyopenms.__version__ = "2.9.1" # Mock version for testing
sys.modules['pyopenms'] = mock_pyopenms

@pytest.fixture
def mock_streamlit():
"""Mock essential Streamlit components for testing parameter display."""
Expand Down Expand Up @@ -47,12 +42,6 @@ def mock_streamlit():
}


def test_mock_pyopenms():
"""Verify that pyopenms mock is working correctly."""
import pyopenms
assert hasattr(pyopenms, '__version__')


def test_topp_parameter_correctness():
"""Test that TOPP parameters are displayed with correct values."""
# Define expected parameters with values
Expand Down
Loading