Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d14d19e7d9 | |||
| 0a8d27957f | |||
| 7e04aebd5d | |||
| b7c01bc373 | |||
| e0faf20a56 | |||
| 7380d9a8a9 | |||
| 85e30671d4 | |||
| b259837af4 | |||
| aad02f0d36 | |||
| 30750710b8 | |||
| fd1f9a43c6 | |||
| 21dd1fc9c8 | |||
| 5243352867 | |||
| 387981aa47 | |||
| 13b2c9c416 | |||
| 4c04bfb92e | |||
| 2fe45e65eb | |||
| 036b4d1215 | |||
| ce986db27b |
@@ -14,6 +14,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Fetch full history for release notes generation
|
||||||
|
|
||||||
- name: Install Docker
|
- name: Install Docker
|
||||||
run: curl -fsSL https://get.docker.com | sh
|
run: curl -fsSL https://get.docker.com | sh
|
||||||
@@ -55,3 +57,49 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=registry,ref=gitea-http.taildb3494.ts.net/will/thechart:buildcache
|
cache-from: type=registry,ref=gitea-http.taildb3494.ts.net/will/thechart:buildcache
|
||||||
cache-to: type=registry,ref=gitea-http.taildb3494.ts.net/will/thechart:buildcache,mode=max
|
cache-to: type=registry,ref=gitea-http.taildb3494.ts.net/will/thechart:buildcache,mode=max
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
id: release_notes
|
||||||
|
if: startsWith(gitea.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
# Get the current tag
|
||||||
|
CURRENT_TAG=${GITEA_REF#refs/tags/}
|
||||||
|
|
||||||
|
# Get the previous tag
|
||||||
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Generate release notes from commits
|
||||||
|
if [ -n "$PREVIOUS_TAG" ]; then
|
||||||
|
echo "## Changes from $PREVIOUS_TAG to $CURRENT_TAG" > release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
git log --pretty=format:"- %s (%h)" $PREVIOUS_TAG..$CURRENT_TAG >> release_notes.md
|
||||||
|
else
|
||||||
|
echo "## Initial Release $CURRENT_TAG" > release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
git log --pretty=format:"- %s (%h)" >> release_notes.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add Docker image information
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
echo "## Docker Images" >> release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
echo "This release includes multi-platform Docker images:" >> release_notes.md
|
||||||
|
echo "- \`gitea-http.taildb3494.ts.net/will/thechart:$CURRENT_TAG\`" >> release_notes.md
|
||||||
|
echo "- \`gitea-http.taildb3494.ts.net/will/thechart:latest\`" >> release_notes.md
|
||||||
|
|
||||||
|
# Output the release notes content for use in next step
|
||||||
|
echo "release_notes<<EOF" >> $GITEA_OUTPUT
|
||||||
|
cat release_notes.md >> $GITEA_OUTPUT
|
||||||
|
echo "EOF" >> $GITEA_OUTPUT
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
if: startsWith(gitea.ref, 'refs/tags/')
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ gitea.ref_name }}
|
||||||
|
release_name: Release ${{ gitea.ref_name }}
|
||||||
|
body: ${{ steps.release_notes.outputs.release_notes }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ htmlcov/
|
|||||||
.pylint.d/
|
.pylint.d/
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
.vscode/
|
#.vscode/
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
Vendored
+7
-1
@@ -4,7 +4,13 @@
|
|||||||
{
|
{
|
||||||
"label": "Run TheChart App",
|
"label": "Run TheChart App",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "cd /home/will/Code/thechart && python -m src.main",
|
"command": "/home/will/Code/thechart/.venv/bin/python",
|
||||||
|
"args": [
|
||||||
|
"src/main.py"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "/home/will/Code/thechart"
|
||||||
|
},
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"isBackground": false,
|
"isBackground": false,
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ RUN sh -c "pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidd
|
|||||||
RUN chown -R ${UID}:${GUID} /home/docker_user/
|
RUN chown -R ${UID}:${GUID} /home/docker_user/
|
||||||
RUN chmod -R 777 /home/docker_user/${TARGET}
|
RUN chmod -R 777 /home/docker_user/${TARGET}
|
||||||
|
|
||||||
|
RUN mkdir -p /app/logs && \
|
||||||
|
touch /app/logs/app.log && \
|
||||||
|
chown -R ${UID}:${GUID} /app/logs && \
|
||||||
|
chmod 666 /app/logs/app.log
|
||||||
|
|
||||||
# Set environment variables for X11 forwarding
|
# Set environment variables for X11 forwarding
|
||||||
ENV DISPLAY=:0
|
ENV DISPLAY=:0
|
||||||
ENV XAUTHORITY=/tmp/.docker.xauth
|
ENV XAUTHORITY=/tmp/.docker.xauth
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Medicine Dose Graph Plots Feature
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Added graph plots for medicine dose tracking with toggle buttons to control display, similar to the existing symptom plots. The feature displays actual daily dosages rather than just binary intake indicators.
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. Graph Manager Updates (`src/graph_manager.py`)
|
||||||
|
|
||||||
|
#### Added Medicine Toggle Variables
|
||||||
|
- Added toggle variables for all 5 medicines: bupropion, hydroxyzine, gabapentin, propranolol, quetiapine
|
||||||
|
- Set bupropion and propranolol to show by default (most commonly used medicines)
|
||||||
|
|
||||||
|
#### Enhanced Toggle UI
|
||||||
|
- Organized toggles into two labeled sections: "Symptoms" and "Medicines"
|
||||||
|
- Symptoms section: Depression, Anxiety, Sleep, Appetite
|
||||||
|
- Medicines section: All 5 medicines with individual toggle buttons
|
||||||
|
|
||||||
|
#### Medicine Dose Visualization
|
||||||
|
- Medicine doses displayed as colored bars positioned at the bottom of the graph
|
||||||
|
- Each medicine has a distinct color:
|
||||||
|
- Bupropion: Red (#FF6B6B)
|
||||||
|
- Hydroxyzine: Teal (#4ECDC4)
|
||||||
|
- Gabapentin: Blue (#45B7D1)
|
||||||
|
- Propranolol: Green (#96CEB4)
|
||||||
|
- Quetiapine: Yellow (#FFEAA7)
|
||||||
|
|
||||||
|
#### Dose Calculation Logic
|
||||||
|
- Parses dose strings in format: `timestamp:dose|timestamp:dose`
|
||||||
|
- Handles various formats including `•` symbols and missing timestamps
|
||||||
|
- Calculates total daily dose by summing all individual doses
|
||||||
|
- Extracts numeric values from dose strings (e.g., "150mg" → 150)
|
||||||
|
|
||||||
|
#### Graph Layout Improvements
|
||||||
|
- Doses scaled by 1/10 for better visibility (labeled as "mg/10")
|
||||||
|
- Bars positioned below main chart area with dynamic positioning
|
||||||
|
- Y-axis label updated to "Rating (0-10) / Dose (mg)"
|
||||||
|
- Semi-transparent bars (alpha=0.6) to avoid overwhelming the main data
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Dose Parsing
|
||||||
|
- Automatically calculates total daily doses from timestamp:dose entries
|
||||||
|
- Handles multiple formats:
|
||||||
|
- Standard: `2025-07-30 08:00:00:150mg|2025-07-30 20:00:00:150mg`
|
||||||
|
- With symbols: `• • • • 2025-07-30 07:50:00:300`
|
||||||
|
- Mixed formats and missing data (NaN values)
|
||||||
|
|
||||||
|
### Toggle Controls
|
||||||
|
- Users can independently show/hide each medicine dose from the graph
|
||||||
|
- Organized into logical groups (Symptoms vs Medicines)
|
||||||
|
- Changes take effect immediately when toggled
|
||||||
|
|
||||||
|
### Visual Design
|
||||||
|
- Medicine doses appear as colored bars scaled to fit with symptom data
|
||||||
|
- Clear legend showing all visible elements with "(mg/10)" notation
|
||||||
|
- Does not interfere with existing symptom line plots
|
||||||
|
- Dynamic positioning based on actual dose ranges
|
||||||
|
|
||||||
|
### Data Integration
|
||||||
|
- Uses existing dose data columns (`bupropion_doses`, `propranolol_doses`, etc.)
|
||||||
|
- Compatible with current data structure
|
||||||
|
- No changes needed to data collection or storage
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
1. Run the app: `.venv/bin/python src/main.py` or use the VS Code task
|
||||||
|
2. Use the "Medicines" toggle buttons to show/hide specific medicine doses
|
||||||
|
3. Medicine doses appear as colored bars at the bottom of the graph
|
||||||
|
4. Doses are scaled by 1/10 for visibility (e.g., 150mg shows as 15 on the chart)
|
||||||
|
5. Combine with symptom data to see correlations between dosage and symptoms
|
||||||
|
|
||||||
|
## Technical Notes
|
||||||
|
- Dose data is read from existing CSV columns (`*_doses`)
|
||||||
|
- Daily totals calculated by parsing and summing individual dose entries
|
||||||
|
- Bars positioned using dynamic `bottom` parameter based on scaled dose values
|
||||||
|
- Y-axis automatically adjusted to accommodate bars
|
||||||
|
- Maintains backward compatibility with existing functionality
|
||||||
|
- Robust parsing handles various dose string formats and edge cases
|
||||||
@@ -88,7 +88,7 @@ build: ## Build the Docker image
|
|||||||
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE} --push .
|
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE} --push .
|
||||||
deploy: ## Deploy the application as a standalone executable
|
deploy: ## Deploy the application as a standalone executable
|
||||||
@echo "Deploying the application..."
|
@echo "Deploying the application..."
|
||||||
pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon='${ICON}' --add-data="./.env:." --add-data='./chart-671.png:.' --add-data='./thechart_data.csv:.' src/main.py
|
pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon='${ICON}' --add-data="./.env:." --add-data='./chart-671.png:.' --add-data='./thechart_data.csv:.' --log-level=DEBUG src/main.py
|
||||||
cp -f ./thechart_data.csv ${ROOT}/Documents/
|
cp -f ./thechart_data.csv ${ROOT}/Documents/
|
||||||
cp -f ./dist/${TARGET} ${ROOT}/Applications/
|
cp -f ./dist/${TARGET} ${ROOT}/Applications/
|
||||||
cp -f ./deploy/${TARGET}.desktop ${ROOT}/.local/share/applications/
|
cp -f ./deploy/${TARGET}.desktop ${ROOT}/.local/share/applications/
|
||||||
@@ -133,14 +133,9 @@ test-edit-functionality: ## Test the enhanced edit functionality
|
|||||||
test-edit-window: $(VENV_ACTIVATE) ## Test edit window functionality (save and delete)
|
test-edit-window: $(VENV_ACTIVATE) ## Test edit window functionality (save and delete)
|
||||||
@echo "Running edit window functionality test..."
|
@echo "Running edit window functionality test..."
|
||||||
$(PYTHON) scripts/test_edit_window_functionality.py
|
$(PYTHON) scripts/test_edit_window_functionality.py
|
||||||
|
|
||||||
test-dose-editing: $(VENV_ACTIVATE) ## Test dose editing functionality in edit window
|
test-dose-editing: $(VENV_ACTIVATE) ## Test dose editing functionality in edit window
|
||||||
@echo "Running dose editing functionality test..."
|
@echo "Running dose editing functionality test..."
|
||||||
$(PYTHON) scripts/test_dose_editing_functionality.py
|
$(PYTHON) scripts/test_dose_editing_functionality.py
|
||||||
|
|
||||||
migrate-csv: $(VENV_ACTIVATE) ## Migrate CSV to new format with dose tracking
|
|
||||||
@echo "Migrating CSV to new format..."
|
|
||||||
.venv/bin/python migrate_csv.py
|
|
||||||
lint: ## Run the linter
|
lint: ## Run the linter
|
||||||
@echo "Running the linter..."
|
@echo "Running the linter..."
|
||||||
docker-compose exec ${TARGET} pipenv run pre-commit run --all-files
|
docker-compose exec ${TARGET} pipenv run pre-commit run --all-files
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Test Updates for Medicine Dose Plotting Feature
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Updated the test suite to accommodate the new medicine dose plotting functionality in the GraphManager class.
|
||||||
|
|
||||||
|
## Files Updated
|
||||||
|
|
||||||
|
### 1. `/tests/test_graph_manager.py`
|
||||||
|
|
||||||
|
#### Updated Tests:
|
||||||
|
- **`test_init`**:
|
||||||
|
- Added checks for all 5 medicine toggle variables (bupropion, hydroxyzine, gabapentin, propranolol, quetiapine)
|
||||||
|
- Verified that bupropion and propranolol are enabled by default
|
||||||
|
- Verified that hydroxyzine, gabapentin, and quetiapine are disabled by default
|
||||||
|
|
||||||
|
- **`test_toggle_controls_creation`**:
|
||||||
|
- Updated to check for all 9 toggle variables (4 symptoms + 5 medicines)
|
||||||
|
|
||||||
|
#### New Test Methods Added:
|
||||||
|
- **`test_calculate_daily_dose_empty_input`**: Tests dose calculation with empty/invalid inputs
|
||||||
|
- **`test_calculate_daily_dose_standard_format`**: Tests standard timestamp:dose format parsing
|
||||||
|
- **`test_calculate_daily_dose_with_symbols`**: Tests parsing with bullet symbols (•)
|
||||||
|
- **`test_calculate_daily_dose_no_timestamp`**: Tests parsing without timestamps
|
||||||
|
- **`test_calculate_daily_dose_decimal_values`**: Tests decimal dose values
|
||||||
|
- **`test_medicine_dose_plotting`**: Tests that medicine doses are plotted correctly
|
||||||
|
- **`test_medicine_toggle_functionality`**: Tests that medicine toggles affect dose display
|
||||||
|
- **`test_dose_calculation_comprehensive`**: Tests all sample dose data cases
|
||||||
|
- **`test_dose_calculation_edge_cases`**: Tests malformed and edge case inputs
|
||||||
|
|
||||||
|
### 2. `/tests/conftest.py`
|
||||||
|
|
||||||
|
#### Updated Fixtures:
|
||||||
|
- **`sample_dataframe`**: Enhanced with realistic dose data:
|
||||||
|
- Added proper dose strings in various formats
|
||||||
|
- Included multiple dose entries per day
|
||||||
|
- Added decimal doses and different timestamp formats
|
||||||
|
|
||||||
|
#### New Fixtures:
|
||||||
|
- **`sample_dose_data`**: Comprehensive test cases for dose calculation including:
|
||||||
|
- Standard format: `'2025-07-28 18:59:45:150mg|2025-07-28 19:34:19:75mg'`
|
||||||
|
- With bullets: `'• • • • 2025-07-30 07:50:00:300'`
|
||||||
|
- Decimal doses: `'2025-07-28 18:59:45:12.5mg|2025-07-28 19:34:19:7.5mg'`
|
||||||
|
- No timestamp: `'100mg|50mg'`
|
||||||
|
- Mixed format: `'• 2025-07-30 22:50:00:10|75mg'`
|
||||||
|
- Edge cases: empty strings, 'nan' values, no units
|
||||||
|
|
||||||
|
## Test Coverage Areas
|
||||||
|
|
||||||
|
### Dose Calculation Logic:
|
||||||
|
- ✅ Empty/null inputs return 0.0
|
||||||
|
- ✅ Standard timestamp:dose format parsing
|
||||||
|
- ✅ Multiple dose entries separated by `|`
|
||||||
|
- ✅ Bullet symbol (•) handling and removal
|
||||||
|
- ✅ Decimal dose values
|
||||||
|
- ✅ Doses without timestamps
|
||||||
|
- ✅ Doses without units (mg)
|
||||||
|
- ✅ Mixed format handling
|
||||||
|
- ✅ Malformed data graceful handling
|
||||||
|
|
||||||
|
### Graph Plotting:
|
||||||
|
- ✅ Medicine dose bars are plotted when toggles are enabled
|
||||||
|
- ✅ No plotting occurs when toggles are disabled
|
||||||
|
- ✅ No plotting occurs when dose data is empty
|
||||||
|
- ✅ Canvas redraw is called appropriately
|
||||||
|
- ✅ Axis clearing occurs before plotting
|
||||||
|
|
||||||
|
### Toggle Functionality:
|
||||||
|
- ✅ All 9 toggle variables are properly initialized
|
||||||
|
- ✅ Default states are correct (symptoms on, some medicines on/off)
|
||||||
|
- ✅ Toggle changes trigger graph updates
|
||||||
|
- ✅ Toggle states affect what gets plotted
|
||||||
|
|
||||||
|
## Expected Test Results
|
||||||
|
|
||||||
|
### Dose Calculation Examples:
|
||||||
|
- `'2025-07-28 18:59:45:150mg|2025-07-28 19:34:19:75mg'` → 225.0mg
|
||||||
|
- `'• • • • 2025-07-30 07:50:00:300'` → 300.0mg
|
||||||
|
- `'2025-07-28 18:59:45:12.5mg|2025-07-28 19:34:19:7.5mg'` → 20.0mg
|
||||||
|
- `'100mg|50mg'` → 150.0mg
|
||||||
|
- `'• 2025-07-30 22:50:00:10|75mg'` → 85.0mg
|
||||||
|
- `''` → 0.0mg
|
||||||
|
- `'nan'` → 0.0mg
|
||||||
|
- `'2025-07-28 18:59:45:10|2025-07-28 19:34:19:5'` → 15.0mg
|
||||||
|
|
||||||
|
## Running the Tests
|
||||||
|
|
||||||
|
To run the updated tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all graph manager tests
|
||||||
|
.venv/bin/python -m pytest tests/test_graph_manager.py -v
|
||||||
|
|
||||||
|
# Run specific dose calculation tests
|
||||||
|
.venv/bin/python -m pytest tests/test_graph_manager.py -k "dose_calculation" -v
|
||||||
|
|
||||||
|
# Run all tests with coverage
|
||||||
|
.venv/bin/python -m pytest tests/ --cov=src --cov-report=html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All tests are designed to work with mocked matplotlib components to avoid GUI dependencies
|
||||||
|
- Tests use the existing fixture system and follow established patterns
|
||||||
|
- New functionality is thoroughly covered while maintaining backward compatibility
|
||||||
|
- Edge cases and error conditions are properly tested
|
||||||
-756
@@ -1,756 +0,0 @@
|
|||||||
<?xml version="1.0" ?>
|
|
||||||
<coverage version="7.10.1" timestamp="1753820440721" lines-valid="702" lines-covered="511" line-rate="0.7279" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
|
|
||||||
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.10.1 -->
|
|
||||||
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
|
|
||||||
<sources>
|
|
||||||
<source>/home/will/Code/thechart/src</source>
|
|
||||||
</sources>
|
|
||||||
<packages>
|
|
||||||
<package name="." line-rate="0.7279" branch-rate="0" complexity="0">
|
|
||||||
<classes>
|
|
||||||
<class name="__init__.py" filename="__init__.py" complexity="0" line-rate="0" branch-rate="0">
|
|
||||||
<methods/>
|
|
||||||
<lines>
|
|
||||||
<line number="3" hits="0"/>
|
|
||||||
<line number="4" hits="0"/>
|
|
||||||
<line number="5" hits="0"/>
|
|
||||||
</lines>
|
|
||||||
</class>
|
|
||||||
<class name="constants.py" filename="constants.py" complexity="0" line-rate="1" branch-rate="0">
|
|
||||||
<methods/>
|
|
||||||
<lines>
|
|
||||||
<line number="1" hits="1"/>
|
|
||||||
<line number="3" hits="1"/>
|
|
||||||
<line number="5" hits="1"/>
|
|
||||||
<line number="7" hits="1"/>
|
|
||||||
<line number="8" hits="1"/>
|
|
||||||
<line number="9" hits="1"/>
|
|
||||||
</lines>
|
|
||||||
</class>
|
|
||||||
<class name="data_manager.py" filename="data_manager.py" complexity="0" line-rate="0.5673" branch-rate="0">
|
|
||||||
<methods/>
|
|
||||||
<lines>
|
|
||||||
<line number="1" hits="1"/>
|
|
||||||
<line number="2" hits="1"/>
|
|
||||||
<line number="3" hits="1"/>
|
|
||||||
<line number="5" hits="1"/>
|
|
||||||
<line number="8" hits="1"/>
|
|
||||||
<line number="11" hits="1"/>
|
|
||||||
<line number="12" hits="1"/>
|
|
||||||
<line number="13" hits="1"/>
|
|
||||||
<line number="14" hits="1"/>
|
|
||||||
<line number="16" hits="1"/>
|
|
||||||
<line number="18" hits="1"/>
|
|
||||||
<line number="19" hits="1"/>
|
|
||||||
<line number="20" hits="1"/>
|
|
||||||
<line number="21" hits="1"/>
|
|
||||||
<line number="42" hits="1"/>
|
|
||||||
<line number="44" hits="1"/>
|
|
||||||
<line number="45" hits="1"/>
|
|
||||||
<line number="46" hits="1"/>
|
|
||||||
<line number="48" hits="1"/>
|
|
||||||
<line number="49" hits="1"/>
|
|
||||||
<line number="70" hits="1"/>
|
|
||||||
<line number="71" hits="1"/>
|
|
||||||
<line number="72" hits="0"/>
|
|
||||||
<line number="73" hits="0"/>
|
|
||||||
<line number="74" hits="1"/>
|
|
||||||
<line number="75" hits="1"/>
|
|
||||||
<line number="76" hits="1"/>
|
|
||||||
<line number="78" hits="1"/>
|
|
||||||
<line number="80" hits="1"/>
|
|
||||||
<line number="82" hits="1"/>
|
|
||||||
<line number="83" hits="1"/>
|
|
||||||
<line number="85" hits="1"/>
|
|
||||||
<line number="86" hits="1"/>
|
|
||||||
<line number="87" hits="1"/>
|
|
||||||
<line number="89" hits="1"/>
|
|
||||||
<line number="90" hits="1"/>
|
|
||||||
<line number="91" hits="1"/>
|
|
||||||
<line number="92" hits="1"/>
|
|
||||||
<line number="93" hits="1"/>
|
|
||||||
<line number="94" hits="1"/>
|
|
||||||
<line number="95" hits="1"/>
|
|
||||||
<line number="97" hits="1"/>
|
|
||||||
<line number="99" hits="1"/>
|
|
||||||
<line number="100" hits="1"/>
|
|
||||||
<line number="101" hits="1"/>
|
|
||||||
<line number="104" hits="1"/>
|
|
||||||
<line number="105" hits="1"/>
|
|
||||||
<line number="108" hits="1"/>
|
|
||||||
<line number="112" hits="1"/>
|
|
||||||
<line number="114" hits="0"/>
|
|
||||||
<line number="135" hits="1"/>
|
|
||||||
<line number="150" hits="0"/>
|
|
||||||
<line number="151" hits="0"/>
|
|
||||||
<line number="152" hits="1"/>
|
|
||||||
<line number="153" hits="1"/>
|
|
||||||
<line number="154" hits="1"/>
|
|
||||||
<line number="156" hits="1"/>
|
|
||||||
<line number="158" hits="1"/>
|
|
||||||
<line number="159" hits="1"/>
|
|
||||||
<line number="161" hits="1"/>
|
|
||||||
<line number="163" hits="1"/>
|
|
||||||
<line number="164" hits="1"/>
|
|
||||||
<line number="165" hits="0"/>
|
|
||||||
<line number="166" hits="0"/>
|
|
||||||
<line number="167" hits="0"/>
|
|
||||||
<line number="169" hits="1"/>
|
|
||||||
<line number="171" hits="0"/>
|
|
||||||
<line number="173" hits="0"/>
|
|
||||||
<line number="174" hits="0"/>
|
|
||||||
<line number="175" hits="0"/>
|
|
||||||
<line number="176" hits="0"/>
|
|
||||||
<line number="179" hits="0"/>
|
|
||||||
<line number="181" hits="0"/>
|
|
||||||
<line number="197" hits="0"/>
|
|
||||||
<line number="200" hits="0"/>
|
|
||||||
<line number="201" hits="0"/>
|
|
||||||
<line number="202" hits="0"/>
|
|
||||||
<line number="204" hits="0"/>
|
|
||||||
<line number="205" hits="0"/>
|
|
||||||
<line number="207" hits="0"/>
|
|
||||||
<line number="210" hits="0"/>
|
|
||||||
<line number="212" hits="0"/>
|
|
||||||
<line number="213" hits="0"/>
|
|
||||||
<line number="214" hits="0"/>
|
|
||||||
<line number="215" hits="0"/>
|
|
||||||
<line number="216" hits="0"/>
|
|
||||||
<line number="218" hits="1"/>
|
|
||||||
<line number="222" hits="0"/>
|
|
||||||
<line number="223" hits="0"/>
|
|
||||||
<line number="224" hits="0"/>
|
|
||||||
<line number="225" hits="0"/>
|
|
||||||
<line number="227" hits="0"/>
|
|
||||||
<line number="228" hits="0"/>
|
|
||||||
<line number="230" hits="0"/>
|
|
||||||
<line number="231" hits="0"/>
|
|
||||||
<line number="233" hits="0"/>
|
|
||||||
<line number="234" hits="0"/>
|
|
||||||
<line number="235" hits="0"/>
|
|
||||||
<line number="236" hits="0"/>
|
|
||||||
<line number="237" hits="0"/>
|
|
||||||
<line number="239" hits="0"/>
|
|
||||||
<line number="240" hits="0"/>
|
|
||||||
<line number="241" hits="0"/>
|
|
||||||
<line number="242" hits="0"/>
|
|
||||||
</lines>
|
|
||||||
</class>
|
|
||||||
<class name="graph_manager.py" filename="graph_manager.py" complexity="0" line-rate="0.971" branch-rate="0">
|
|
||||||
<methods/>
|
|
||||||
<lines>
|
|
||||||
<line number="1" hits="1"/>
|
|
||||||
<line number="2" hits="1"/>
|
|
||||||
<line number="4" hits="1"/>
|
|
||||||
<line number="5" hits="1"/>
|
|
||||||
<line number="6" hits="1"/>
|
|
||||||
<line number="7" hits="1"/>
|
|
||||||
<line number="8" hits="1"/>
|
|
||||||
<line number="11" hits="1"/>
|
|
||||||
<line number="14" hits="1"/>
|
|
||||||
<line number="15" hits="1"/>
|
|
||||||
<line number="18" hits="1"/>
|
|
||||||
<line number="19" hits="1"/>
|
|
||||||
<line number="22" hits="1"/>
|
|
||||||
<line number="30" hits="1"/>
|
|
||||||
<line number="31" hits="1"/>
|
|
||||||
<line number="34" hits="1"/>
|
|
||||||
<line number="37" hits="1"/>
|
|
||||||
<line number="38" hits="1"/>
|
|
||||||
<line number="41" hits="1"/>
|
|
||||||
<line number="42" hits="1"/>
|
|
||||||
<line number="45" hits="1"/>
|
|
||||||
<line number="46" hits="1"/>
|
|
||||||
<line number="47" hits="1"/>
|
|
||||||
<line number="48" hits="1"/>
|
|
||||||
<line number="51" hits="1"/>
|
|
||||||
<line number="54" hits="1"/>
|
|
||||||
<line number="56" hits="1"/>
|
|
||||||
<line number="58" hits="1"/>
|
|
||||||
<line number="62" hits="1"/>
|
|
||||||
<line number="69" hits="1"/>
|
|
||||||
<line number="70" hits="1"/>
|
|
||||||
<line number="76" hits="1"/>
|
|
||||||
<line number="78" hits="1"/>
|
|
||||||
<line number="80" hits="0"/>
|
|
||||||
<line number="81" hits="0"/>
|
|
||||||
<line number="83" hits="1"/>
|
|
||||||
<line number="85" hits="1"/>
|
|
||||||
<line number="86" hits="1"/>
|
|
||||||
<line number="88" hits="1"/>
|
|
||||||
<line number="90" hits="1"/>
|
|
||||||
<line number="91" hits="1"/>
|
|
||||||
<line number="93" hits="1"/>
|
|
||||||
<line number="94" hits="1"/>
|
|
||||||
<line number="95" hits="1"/>
|
|
||||||
<line number="96" hits="1"/>
|
|
||||||
<line number="99" hits="1"/>
|
|
||||||
<line number="102" hits="1"/>
|
|
||||||
<line number="103" hits="1"/>
|
|
||||||
<line number="106" hits="1"/>
|
|
||||||
<line number="107" hits="1"/>
|
|
||||||
<line number="108" hits="1"/>
|
|
||||||
<line number="109" hits="1"/>
|
|
||||||
<line number="110" hits="1"/>
|
|
||||||
<line number="111" hits="1"/>
|
|
||||||
<line number="112" hits="1"/>
|
|
||||||
<line number="113" hits="1"/>
|
|
||||||
<line number="114" hits="1"/>
|
|
||||||
<line number="117" hits="1"/>
|
|
||||||
<line number="120" hits="1"/>
|
|
||||||
<line number="121" hits="1"/>
|
|
||||||
<line number="122" hits="1"/>
|
|
||||||
<line number="123" hits="1"/>
|
|
||||||
<line number="124" hits="1"/>
|
|
||||||
<line number="125" hits="1"/>
|
|
||||||
<line number="128" hits="1"/>
|
|
||||||
<line number="130" hits="1"/>
|
|
||||||
<line number="139" hits="1"/>
|
|
||||||
<line number="147" hits="1"/>
|
|
||||||
<line number="149" hits="1"/>
|
|
||||||
</lines>
|
|
||||||
</class>
|
|
||||||
<class name="init.py" filename="init.py" complexity="0" line-rate="0.9524" branch-rate="0">
|
|
||||||
<methods/>
|
|
||||||
<lines>
|
|
||||||
<line number="1" hits="1"/>
|
|
||||||
<line number="3" hits="1"/>
|
|
||||||
<line number="4" hits="1"/>
|
|
||||||
<line number="6" hits="1"/>
|
|
||||||
<line number="7" hits="1"/>
|
|
||||||
<line number="8" hits="1"/>
|
|
||||||
<line number="9" hits="1"/>
|
|
||||||
<line number="10" hits="1"/>
|
|
||||||
<line number="11" hits="1"/>
|
|
||||||
<line number="13" hits="1"/>
|
|
||||||
<line number="19" hits="1"/>
|
|
||||||
<line number="21" hits="1"/>
|
|
||||||
<line number="23" hits="1"/>
|
|
||||||
<line number="24" hits="1"/>
|
|
||||||
<line number="25" hits="1"/>
|
|
||||||
<line number="26" hits="1"/>
|
|
||||||
<line number="27" hits="1"/>
|
|
||||||
<line number="28" hits="0"/>
|
|
||||||
<line number="29" hits="1"/>
|
|
||||||
<line number="30" hits="1"/>
|
|
||||||
<line number="31" hits="1"/>
|
|
||||||
</lines>
|
|
||||||
</class>
|
|
||||||
<class name="logger.py" filename="logger.py" complexity="0" line-rate="1" branch-rate="0">
|
|
||||||
<methods/>
|
|
||||||
<lines>
|
|
||||||
<line number="1" hits="1"/>
|
|
||||||
<line number="3" hits="1"/>
|
|
||||||
<line number="5" hits="1"/>
|
|
||||||
<line number="8" hits="1"/>
|
|
||||||
<line number="9" hits="1"/>
|
|
||||||
<line number="10" hits="1"/>
|
|
||||||
<line number="12" hits="1"/>
|
|
||||||
<line number="13" hits="1"/>
|
|
||||||
<line number="14" hits="1"/>
|
|
||||||
<line number="15" hits="1"/>
|
|
||||||
<line number="17" hits="1"/>
|
|
||||||
<line number="18" hits="1"/>
|
|
||||||
<line number="20" hits="1"/>
|
|
||||||
<line number="22" hits="1"/>
|
|
||||||
<line number="23" hits="1"/>
|
|
||||||
<line number="24" hits="1"/>
|
|
||||||
<line number="25" hits="1"/>
|
|
||||||
<line number="26" hits="1"/>
|
|
||||||
<line number="28" hits="1"/>
|
|
||||||
<line number="29" hits="1"/>
|
|
||||||
<line number="30" hits="1"/>
|
|
||||||
<line number="31" hits="1"/>
|
|
||||||
<line number="32" hits="1"/>
|
|
||||||
<line number="34" hits="1"/>
|
|
||||||
<line number="35" hits="1"/>
|
|
||||||
<line number="36" hits="1"/>
|
|
||||||
<line number="37" hits="1"/>
|
|
||||||
<line number="38" hits="1"/>
|
|
||||||
<line number="40" hits="1"/>
|
|
||||||
</lines>
|
|
||||||
</class>
|
|
||||||
<class name="main.py" filename="main.py" complexity="0" line-rate="0.7857" branch-rate="0">
|
|
||||||
<methods/>
|
|
||||||
<lines>
|
|
||||||
<line number="1" hits="1"/>
|
|
||||||
<line number="2" hits="1"/>
|
|
||||||
<line number="3" hits="1"/>
|
|
||||||
<line number="4" hits="1"/>
|
|
||||||
<line number="5" hits="1"/>
|
|
||||||
<line number="6" hits="1"/>
|
|
||||||
<line number="8" hits="1"/>
|
|
||||||
<line number="10" hits="1"/>
|
|
||||||
<line number="11" hits="1"/>
|
|
||||||
<line number="12" hits="1"/>
|
|
||||||
<line number="13" hits="1"/>
|
|
||||||
<line number="14" hits="1"/>
|
|
||||||
<line number="17" hits="1"/>
|
|
||||||
<line number="18" hits="1"/>
|
|
||||||
<line number="19" hits="1"/>
|
|
||||||
<line number="20" hits="1"/>
|
|
||||||
<line number="21" hits="1"/>
|
|
||||||
<line number="22" hits="1"/>
|
|
||||||
<line number="25" hits="1"/>
|
|
||||||
<line number="26" hits="1"/>
|
|
||||||
<line number="28" hits="1"/>
|
|
||||||
<line number="29" hits="1"/>
|
|
||||||
<line number="30" hits="1"/>
|
|
||||||
<line number="31" hits="1"/>
|
|
||||||
<line number="32" hits="1"/>
|
|
||||||
<line number="34" hits="1"/>
|
|
||||||
<line number="39" hits="1"/>
|
|
||||||
<line number="40" hits="1"/>
|
|
||||||
<line number="41" hits="1"/>
|
|
||||||
<line number="42" hits="1"/>
|
|
||||||
<line number="45" hits="1"/>
|
|
||||||
<line number="46" hits="1"/>
|
|
||||||
<line number="49" hits="1"/>
|
|
||||||
<line number="50" hits="1"/>
|
|
||||||
<line number="51" hits="1"/>
|
|
||||||
<line number="52" hits="1"/>
|
|
||||||
<line number="55" hits="1"/>
|
|
||||||
<line number="57" hits="1"/>
|
|
||||||
<line number="59" hits="1"/>
|
|
||||||
<line number="62" hits="1"/>
|
|
||||||
<line number="63" hits="1"/>
|
|
||||||
<line number="66" hits="1"/>
|
|
||||||
<line number="67" hits="1"/>
|
|
||||||
<line number="70" hits="1"/>
|
|
||||||
<line number="71" hits="1"/>
|
|
||||||
<line number="72" hits="1"/>
|
|
||||||
<line number="73" hits="1"/>
|
|
||||||
<line number="76" hits="1"/>
|
|
||||||
<line number="77" hits="1"/>
|
|
||||||
<line number="80" hits="1"/>
|
|
||||||
<line number="81" hits="1"/>
|
|
||||||
<line number="82" hits="1"/>
|
|
||||||
<line number="83" hits="1"/>
|
|
||||||
<line number="84" hits="1"/>
|
|
||||||
<line number="85" hits="1"/>
|
|
||||||
<line number="88" hits="1"/>
|
|
||||||
<line number="102" hits="1"/>
|
|
||||||
<line number="103" hits="1"/>
|
|
||||||
<line number="104" hits="1"/>
|
|
||||||
<line number="107" hits="1"/>
|
|
||||||
<line number="109" hits="1"/>
|
|
||||||
<line number="111" hits="1"/>
|
|
||||||
<line number="112" hits="1"/>
|
|
||||||
<line number="113" hits="1"/>
|
|
||||||
<line number="114" hits="1"/>
|
|
||||||
<line number="115" hits="1"/>
|
|
||||||
<line number="116" hits="1"/>
|
|
||||||
<line number="118" hits="1"/>
|
|
||||||
<line number="120" hits="0"/>
|
|
||||||
<line number="123" hits="0"/>
|
|
||||||
<line number="124" hits="0"/>
|
|
||||||
<line number="125" hits="0"/>
|
|
||||||
<line number="127" hits="0"/>
|
|
||||||
<line number="147" hits="0"/>
|
|
||||||
<line number="150" hits="0"/>
|
|
||||||
<line number="156" hits="0"/>
|
|
||||||
<line number="158" hits="1"/>
|
|
||||||
<line number="176" hits="0"/>
|
|
||||||
<line number="195" hits="0"/>
|
|
||||||
<line number="196" hits="0"/>
|
|
||||||
<line number="197" hits="0"/>
|
|
||||||
<line number="200" hits="0"/>
|
|
||||||
<line number="201" hits="0"/>
|
|
||||||
<line number="204" hits="0"/>
|
|
||||||
<line number="205" hits="0"/>
|
|
||||||
<line number="206" hits="0"/>
|
|
||||||
<line number="213" hits="0"/>
|
|
||||||
<line number="215" hits="1"/>
|
|
||||||
<line number="216" hits="1"/>
|
|
||||||
<line number="219" hits="1"/>
|
|
||||||
<line number="220" hits="1"/>
|
|
||||||
<line number="222" hits="1"/>
|
|
||||||
<line number="225" hits="1"/>
|
|
||||||
<line number="226" hits="1"/>
|
|
||||||
<line number="227" hits="1"/>
|
|
||||||
<line number="228" hits="1"/>
|
|
||||||
<line number="229" hits="1"/>
|
|
||||||
<line number="230" hits="1"/>
|
|
||||||
<line number="232" hits="1"/>
|
|
||||||
<line number="233" hits="1"/>
|
|
||||||
<line number="234" hits="1"/>
|
|
||||||
<line number="237" hits="1"/>
|
|
||||||
<line number="238" hits="1"/>
|
|
||||||
<line number="241" hits="1"/>
|
|
||||||
<line number="243" hits="1"/>
|
|
||||||
<line number="244" hits="1"/>
|
|
||||||
<line number="247" hits="1"/>
|
|
||||||
<line number="248" hits="1"/>
|
|
||||||
<line number="249" hits="1"/>
|
|
||||||
<line number="251" hits="1"/>
|
|
||||||
<line number="269" hits="1"/>
|
|
||||||
<line number="272" hits="1"/>
|
|
||||||
<line number="273" hits="1"/>
|
|
||||||
<line number="274" hits="1"/>
|
|
||||||
<line number="276" hits="0"/>
|
|
||||||
<line number="277" hits="0"/>
|
|
||||||
<line number="280" hits="0"/>
|
|
||||||
<line number="281" hits="0"/>
|
|
||||||
<line number="284" hits="0"/>
|
|
||||||
<line number="285" hits="0"/>
|
|
||||||
<line number="286" hits="0"/>
|
|
||||||
<line number="293" hits="0"/>
|
|
||||||
<line number="295" hits="1"/>
|
|
||||||
<line number="297" hits="1"/>
|
|
||||||
<line number="298" hits="1"/>
|
|
||||||
<line number="304" hits="1"/>
|
|
||||||
<line number="305" hits="0"/>
|
|
||||||
<line number="307" hits="0"/>
|
|
||||||
<line number="308" hits="0"/>
|
|
||||||
<line number="309" hits="0"/>
|
|
||||||
<line number="312" hits="0"/>
|
|
||||||
<line number="314" hits="0"/>
|
|
||||||
<line number="316" hits="1"/>
|
|
||||||
<line number="318" hits="1"/>
|
|
||||||
<line number="319" hits="1"/>
|
|
||||||
<line number="320" hits="1"/>
|
|
||||||
<line number="321" hits="1"/>
|
|
||||||
<line number="322" hits="1"/>
|
|
||||||
<line number="323" hits="1"/>
|
|
||||||
<line number="324" hits="1"/>
|
|
||||||
<line number="326" hits="1"/>
|
|
||||||
<line number="328" hits="1"/>
|
|
||||||
<line number="331" hits="1"/>
|
|
||||||
<line number="332" hits="1"/>
|
|
||||||
<line number="335" hits="1"/>
|
|
||||||
<line number="338" hits="1"/>
|
|
||||||
<line number="340" hits="1"/>
|
|
||||||
<line number="355" hits="1"/>
|
|
||||||
<line number="356" hits="0"/>
|
|
||||||
<line number="359" hits="1"/>
|
|
||||||
<line number="361" hits="1"/>
|
|
||||||
<line number="362" hits="1"/>
|
|
||||||
<line number="363" hits="1"/>
|
|
||||||
<line number="366" hits="1"/>
|
|
||||||
</lines>
|
|
||||||
</class>
|
|
||||||
<class name="ui_manager.py" filename="ui_manager.py" complexity="0" line-rate="0.6614" branch-rate="0">
|
|
||||||
<methods/>
|
|
||||||
<lines>
|
|
||||||
<line number="1" hits="1"/>
|
|
||||||
<line number="2" hits="1"/>
|
|
||||||
<line number="3" hits="1"/>
|
|
||||||
<line number="4" hits="1"/>
|
|
||||||
<line number="5" hits="1"/>
|
|
||||||
<line number="6" hits="1"/>
|
|
||||||
<line number="7" hits="1"/>
|
|
||||||
<line number="8" hits="1"/>
|
|
||||||
<line number="10" hits="1"/>
|
|
||||||
<line number="13" hits="1"/>
|
|
||||||
<line number="16" hits="1"/>
|
|
||||||
<line number="17" hits="1"/>
|
|
||||||
<line number="18" hits="1"/>
|
|
||||||
<line number="20" hits="1"/>
|
|
||||||
<line number="22" hits="1"/>
|
|
||||||
<line number="23" hits="1"/>
|
|
||||||
<line number="26" hits="1"/>
|
|
||||||
<line number="28" hits="1"/>
|
|
||||||
<line number="29" hits="1"/>
|
|
||||||
<line number="33" hits="1"/>
|
|
||||||
<line number="34" hits="1"/>
|
|
||||||
<line number="35" hits="1"/>
|
|
||||||
<line number="36" hits="1"/>
|
|
||||||
<line number="37" hits="1"/>
|
|
||||||
<line number="39" hits="1"/>
|
|
||||||
<line number="40" hits="1"/>
|
|
||||||
<line number="43" hits="1"/>
|
|
||||||
<line number="44" hits="1"/>
|
|
||||||
<line number="45" hits="0"/>
|
|
||||||
<line number="46" hits="0"/>
|
|
||||||
<line number="47" hits="1"/>
|
|
||||||
<line number="48" hits="1"/>
|
|
||||||
<line number="49" hits="1"/>
|
|
||||||
<line number="50" hits="1"/>
|
|
||||||
<line number="51" hits="1"/>
|
|
||||||
<line number="52" hits="1"/>
|
|
||||||
<line number="54" hits="1"/>
|
|
||||||
<line number="57" hits="1"/>
|
|
||||||
<line number="58" hits="1"/>
|
|
||||||
<line number="59" hits="1"/>
|
|
||||||
<line number="60" hits="1"/>
|
|
||||||
<line number="63" hits="1"/>
|
|
||||||
<line number="64" hits="1"/>
|
|
||||||
<line number="67" hits="1"/>
|
|
||||||
<line number="70" hits="1"/>
|
|
||||||
<line number="71" hits="1"/>
|
|
||||||
<line number="74" hits="1"/>
|
|
||||||
<line number="75" hits="0"/>
|
|
||||||
<line number="77" hits="1"/>
|
|
||||||
<line number="78" hits="0"/>
|
|
||||||
<line number="80" hits="1"/>
|
|
||||||
<line number="81" hits="1"/>
|
|
||||||
<line number="82" hits="1"/>
|
|
||||||
<line number="83" hits="1"/>
|
|
||||||
<line number="86" hits="1"/>
|
|
||||||
<line number="87" hits="1"/>
|
|
||||||
<line number="90" hits="1"/>
|
|
||||||
<line number="93" hits="1"/>
|
|
||||||
<line number="94" hits="0"/>
|
|
||||||
<line number="95" hits="0"/>
|
|
||||||
<line number="97" hits="1"/>
|
|
||||||
<line number="100" hits="1"/>
|
|
||||||
<line number="108" hits="1"/>
|
|
||||||
<line number="115" hits="1"/>
|
|
||||||
<line number="116" hits="1"/>
|
|
||||||
<line number="119" hits="1"/>
|
|
||||||
<line number="128" hits="1"/>
|
|
||||||
<line number="131" hits="1"/>
|
|
||||||
<line number="132" hits="1"/>
|
|
||||||
<line number="133" hits="1"/>
|
|
||||||
<line number="136" hits="1"/>
|
|
||||||
<line number="144" hits="1"/>
|
|
||||||
<line number="146" hits="1"/>
|
|
||||||
<line number="151" hits="1"/>
|
|
||||||
<line number="152" hits="1"/>
|
|
||||||
<line number="154" hits="1"/>
|
|
||||||
<line number="157" hits="1"/>
|
|
||||||
<line number="161" hits="1"/>
|
|
||||||
<line number="164" hits="1"/>
|
|
||||||
<line number="169" hits="1"/>
|
|
||||||
<line number="172" hits="1"/>
|
|
||||||
<line number="180" hits="1"/>
|
|
||||||
<line number="182" hits="1"/>
|
|
||||||
<line number="185" hits="1"/>
|
|
||||||
<line number="188" hits="1"/>
|
|
||||||
<line number="189" hits="1"/>
|
|
||||||
<line number="191" hits="1"/>
|
|
||||||
<line number="205" hits="1"/>
|
|
||||||
<line number="207" hits="1"/>
|
|
||||||
<line number="221" hits="1"/>
|
|
||||||
<line number="222" hits="1"/>
|
|
||||||
<line number="224" hits="1"/>
|
|
||||||
<line number="238" hits="1"/>
|
|
||||||
<line number="239" hits="1"/>
|
|
||||||
<line number="241" hits="1"/>
|
|
||||||
<line number="244" hits="1"/>
|
|
||||||
<line number="245" hits="1"/>
|
|
||||||
<line number="246" hits="1"/>
|
|
||||||
<line number="248" hits="1"/>
|
|
||||||
<line number="250" hits="1"/>
|
|
||||||
<line number="252" hits="1"/>
|
|
||||||
<line number="253" hits="1"/>
|
|
||||||
<line number="254" hits="1"/>
|
|
||||||
<line number="256" hits="1"/>
|
|
||||||
<line number="260" hits="1"/>
|
|
||||||
<line number="261" hits="1"/>
|
|
||||||
<line number="263" hits="1"/>
|
|
||||||
<line number="264" hits="1"/>
|
|
||||||
<line number="275" hits="1"/>
|
|
||||||
<line number="277" hits="1"/>
|
|
||||||
<line number="281" hits="1"/>
|
|
||||||
<line number="282" hits="1"/>
|
|
||||||
<line number="283" hits="1"/>
|
|
||||||
<line number="284" hits="1"/>
|
|
||||||
<line number="287" hits="1"/>
|
|
||||||
<line number="290" hits="1"/>
|
|
||||||
<line number="292" hits="1"/>
|
|
||||||
<line number="293" hits="1"/>
|
|
||||||
<line number="300" hits="1"/>
|
|
||||||
<line number="301" hits="0"/>
|
|
||||||
<line number="303" hits="0"/>
|
|
||||||
<line number="319" hits="0"/>
|
|
||||||
<line number="320" hits="0"/>
|
|
||||||
<line number="322" hits="0"/>
|
|
||||||
<line number="342" hits="0"/>
|
|
||||||
<line number="344" hits="0"/>
|
|
||||||
<line number="345" hits="0"/>
|
|
||||||
<line number="365" hits="1"/>
|
|
||||||
<line number="368" hits="1"/>
|
|
||||||
<line number="369" hits="1"/>
|
|
||||||
<line number="372" hits="1"/>
|
|
||||||
<line number="375" hits="1"/>
|
|
||||||
<line number="376" hits="1"/>
|
|
||||||
<line number="387" hits="1"/>
|
|
||||||
<line number="390" hits="1"/>
|
|
||||||
<line number="391" hits="1"/>
|
|
||||||
<line number="392" hits="1"/>
|
|
||||||
<line number="395" hits="1"/>
|
|
||||||
<line number="400" hits="1"/>
|
|
||||||
<line number="401" hits="1"/>
|
|
||||||
<line number="404" hits="1"/>
|
|
||||||
<line number="405" hits="1"/>
|
|
||||||
<line number="406" hits="1"/>
|
|
||||||
<line number="408" hits="1"/>
|
|
||||||
<line number="410" hits="1"/>
|
|
||||||
<line number="420" hits="1"/>
|
|
||||||
<line number="423" hits="1"/>
|
|
||||||
<line number="424" hits="1"/>
|
|
||||||
<line number="425" hits="0"/>
|
|
||||||
<line number="426" hits="0"/>
|
|
||||||
<line number="427" hits="0"/>
|
|
||||||
<line number="429" hits="1"/>
|
|
||||||
<line number="437" hits="1"/>
|
|
||||||
<line number="445" hits="1"/>
|
|
||||||
<line number="446" hits="1"/>
|
|
||||||
<line number="447" hits="1"/>
|
|
||||||
<line number="448" hits="1"/>
|
|
||||||
<line number="449" hits="1"/>
|
|
||||||
<line number="450" hits="1"/>
|
|
||||||
<line number="451" hits="0"/>
|
|
||||||
<line number="452" hits="0"/>
|
|
||||||
<line number="453" hits="0"/>
|
|
||||||
<line number="457" hits="1"/>
|
|
||||||
<line number="458" hits="0"/>
|
|
||||||
<line number="459" hits="0"/>
|
|
||||||
<line number="460" hits="0"/>
|
|
||||||
<line number="464" hits="1"/>
|
|
||||||
<line number="465" hits="1"/>
|
|
||||||
<line number="469" hits="1"/>
|
|
||||||
<line number="470" hits="1"/>
|
|
||||||
<line number="472" hits="1"/>
|
|
||||||
<line number="476" hits="1"/>
|
|
||||||
<line number="478" hits="1"/>
|
|
||||||
<line number="482" hits="1"/>
|
|
||||||
<line number="483" hits="1"/>
|
|
||||||
<line number="484" hits="1"/>
|
|
||||||
<line number="486" hits="1"/>
|
|
||||||
<line number="489" hits="1"/>
|
|
||||||
<line number="492" hits="1"/>
|
|
||||||
<line number="493" hits="1"/>
|
|
||||||
<line number="496" hits="1"/>
|
|
||||||
<line number="497" hits="1"/>
|
|
||||||
<line number="499" hits="1"/>
|
|
||||||
<line number="500" hits="1"/>
|
|
||||||
<line number="501" hits="1"/>
|
|
||||||
<line number="502" hits="1"/>
|
|
||||||
<line number="504" hits="1"/>
|
|
||||||
<line number="515" hits="1"/>
|
|
||||||
<line number="518" hits="1"/>
|
|
||||||
<line number="519" hits="1"/>
|
|
||||||
<line number="521" hits="1"/>
|
|
||||||
<line number="529" hits="1"/>
|
|
||||||
<line number="530" hits="1"/>
|
|
||||||
<line number="531" hits="1"/>
|
|
||||||
<line number="532" hits="1"/>
|
|
||||||
<line number="536" hits="1"/>
|
|
||||||
<line number="538" hits="1"/>
|
|
||||||
<line number="546" hits="1"/>
|
|
||||||
<line number="547" hits="1"/>
|
|
||||||
<line number="550" hits="1"/>
|
|
||||||
<line number="552" hits="0"/>
|
|
||||||
<line number="554" hits="0"/>
|
|
||||||
<line number="561" hits="0"/>
|
|
||||||
<line number="563" hits="0"/>
|
|
||||||
<line number="566" hits="0"/>
|
|
||||||
<line number="567" hits="0"/>
|
|
||||||
<line number="571" hits="0"/>
|
|
||||||
<line number="573" hits="0"/>
|
|
||||||
<line number="589" hits="1"/>
|
|
||||||
<line number="596" hits="1"/>
|
|
||||||
<line number="601" hits="1"/>
|
|
||||||
<line number="607" hits="1"/>
|
|
||||||
<line number="611" hits="1"/>
|
|
||||||
<line number="615" hits="1"/>
|
|
||||||
<line number="616" hits="1"/>
|
|
||||||
<line number="617" hits="1"/>
|
|
||||||
<line number="619" hits="1"/>
|
|
||||||
<line number="621" hits="1"/>
|
|
||||||
<line number="623" hits="1"/>
|
|
||||||
<line number="624" hits="1"/>
|
|
||||||
<line number="627" hits="1"/>
|
|
||||||
<line number="628" hits="1"/>
|
|
||||||
<line number="629" hits="1"/>
|
|
||||||
<line number="632" hits="1"/>
|
|
||||||
<line number="635" hits="1"/>
|
|
||||||
<line number="636" hits="1"/>
|
|
||||||
<line number="639" hits="1"/>
|
|
||||||
<line number="642" hits="1"/>
|
|
||||||
<line number="645" hits="1"/>
|
|
||||||
<line number="646" hits="0"/>
|
|
||||||
<line number="648" hits="1"/>
|
|
||||||
<line number="650" hits="1"/>
|
|
||||||
<line number="656" hits="1"/>
|
|
||||||
<line number="659" hits="1"/>
|
|
||||||
<line number="660" hits="0"/>
|
|
||||||
<line number="661" hits="0"/>
|
|
||||||
<line number="662" hits="0"/>
|
|
||||||
<line number="663" hits="0"/>
|
|
||||||
<line number="664" hits="0"/>
|
|
||||||
<line number="666" hits="0"/>
|
|
||||||
<line number="667" hits="0"/>
|
|
||||||
<line number="668" hits="0"/>
|
|
||||||
<line number="669" hits="0"/>
|
|
||||||
<line number="670" hits="0"/>
|
|
||||||
<line number="671" hits="0"/>
|
|
||||||
<line number="673" hits="0"/>
|
|
||||||
<line number="674" hits="0"/>
|
|
||||||
<line number="676" hits="0"/>
|
|
||||||
<line number="678" hits="1"/>
|
|
||||||
<line number="681" hits="1"/>
|
|
||||||
<line number="687" hits="1"/>
|
|
||||||
<line number="689" hits="1"/>
|
|
||||||
<line number="691" hits="1"/>
|
|
||||||
<line number="698" hits="0"/>
|
|
||||||
<line number="701" hits="0"/>
|
|
||||||
<line number="703" hits="0"/>
|
|
||||||
<line number="704" hits="0"/>
|
|
||||||
<line number="709" hits="0"/>
|
|
||||||
<line number="712" hits="0"/>
|
|
||||||
<line number="713" hits="0"/>
|
|
||||||
<line number="716" hits="0"/>
|
|
||||||
<line number="719" hits="0"/>
|
|
||||||
<line number="721" hits="0"/>
|
|
||||||
<line number="722" hits="0"/>
|
|
||||||
<line number="723" hits="0"/>
|
|
||||||
<line number="725" hits="0"/>
|
|
||||||
<line number="728" hits="0"/>
|
|
||||||
<line number="731" hits="0"/>
|
|
||||||
<line number="737" hits="1"/>
|
|
||||||
<line number="739" hits="0"/>
|
|
||||||
<line number="740" hits="0"/>
|
|
||||||
<line number="742" hits="0"/>
|
|
||||||
<line number="743" hits="0"/>
|
|
||||||
<line number="745" hits="0"/>
|
|
||||||
<line number="748" hits="0"/>
|
|
||||||
<line number="750" hits="0"/>
|
|
||||||
<line number="751" hits="0"/>
|
|
||||||
<line number="756" hits="0"/>
|
|
||||||
<line number="759" hits="0"/>
|
|
||||||
<line number="760" hits="0"/>
|
|
||||||
<line number="763" hits="0"/>
|
|
||||||
<line number="766" hits="0"/>
|
|
||||||
<line number="768" hits="0"/>
|
|
||||||
<line number="769" hits="0"/>
|
|
||||||
<line number="770" hits="0"/>
|
|
||||||
<line number="772" hits="0"/>
|
|
||||||
<line number="775" hits="0"/>
|
|
||||||
<line number="778" hits="0"/>
|
|
||||||
<line number="784" hits="1"/>
|
|
||||||
<line number="786" hits="0"/>
|
|
||||||
<line number="787" hits="0"/>
|
|
||||||
<line number="789" hits="0"/>
|
|
||||||
<line number="790" hits="0"/>
|
|
||||||
<line number="792" hits="0"/>
|
|
||||||
<line number="793" hits="0"/>
|
|
||||||
<line number="794" hits="0"/>
|
|
||||||
<line number="795" hits="0"/>
|
|
||||||
<line number="798" hits="0"/>
|
|
||||||
<line number="799" hits="0"/>
|
|
||||||
<line number="802" hits="0"/>
|
|
||||||
<line number="805" hits="0"/>
|
|
||||||
<line number="807" hits="0"/>
|
|
||||||
<line number="808" hits="0"/>
|
|
||||||
<line number="809" hits="0"/>
|
|
||||||
<line number="811" hits="0"/>
|
|
||||||
<line number="813" hits="0"/>
|
|
||||||
<line number="816" hits="0"/>
|
|
||||||
<line number="818" hits="0"/>
|
|
||||||
<line number="820" hits="0"/>
|
|
||||||
<line number="823" hits="0"/>
|
|
||||||
<line number="824" hits="0"/>
|
|
||||||
<line number="828" hits="0"/>
|
|
||||||
<line number="829" hits="0"/>
|
|
||||||
<line number="830" hits="0"/>
|
|
||||||
<line number="832" hits="0"/>
|
|
||||||
<line number="834" hits="0"/>
|
|
||||||
</lines>
|
|
||||||
</class>
|
|
||||||
</classes>
|
|
||||||
</package>
|
|
||||||
</packages>
|
|
||||||
</coverage>
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "thechart"
|
name = "thechart"
|
||||||
version = "1.0.1"
|
version = "1.3.4"
|
||||||
description = "Chart to monitor your medication intake over time."
|
description = "Chart to monitor your medication intake over time."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ui_manager import UIManager
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
def test_automated_multiple_punches():
|
def test_automated_multiple_punches():
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import sys
|
|||||||
# Add the src directory to the Python path
|
# Add the src directory to the Python path
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||||
|
|
||||||
from data_manager import DataManager
|
from src.data_manager import DataManager
|
||||||
|
|
||||||
# Set up simple logging
|
# Set up simple logging
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import sys
|
|||||||
# Add the src directory to the path so we can import our modules
|
# Add the src directory to the path so we can import our modules
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||||
|
|
||||||
from data_manager import DataManager
|
from src.data_manager import DataManager
|
||||||
|
|
||||||
|
|
||||||
def test_delete_functionality():
|
def test_delete_functionality():
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ui_manager import UIManager
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
def demonstrate_multiple_doses():
|
def demonstrate_multiple_doses():
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import sys
|
|||||||
# Add the src directory to the path so we can import our modules
|
# Add the src directory to the path so we can import our modules
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||||
|
|
||||||
from data_manager import DataManager
|
from src.data_manager import DataManager
|
||||||
|
|
||||||
|
|
||||||
def test_dose_editing_functionality():
|
def test_dose_editing_functionality():
|
||||||
@@ -100,7 +100,7 @@ def test_dose_editing_functionality():
|
|||||||
try:
|
try:
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
|
||||||
from ui_manager import UIManager
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
# Create a temporary UI manager to test the parsing
|
# Create a temporary UI manager to test the parsing
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ from datetime import datetime
|
|||||||
|
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), "src"))
|
sys.path.append(os.path.join(os.path.dirname(__file__), "src"))
|
||||||
|
|
||||||
from data_manager import DataManager
|
from src.data_manager import DataManager
|
||||||
from init import logger
|
from src.init import logger
|
||||||
|
|
||||||
|
|
||||||
def test_dose_tracking():
|
def test_dose_tracking():
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import sys
|
|||||||
# Add src to path
|
# Add src to path
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), "src"))
|
sys.path.append(os.path.join(os.path.dirname(__file__), "src"))
|
||||||
|
|
||||||
from data_manager import DataManager
|
from src.data_manager import DataManager
|
||||||
from init import logger
|
from src.init import logger
|
||||||
|
|
||||||
|
|
||||||
def test_edit_functionality():
|
def test_edit_functionality():
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import sys
|
|||||||
# Add the src directory to the path so we can import our modules
|
# Add the src directory to the path so we can import our modules
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||||
|
|
||||||
from data_manager import DataManager
|
from src.data_manager import DataManager
|
||||||
|
|
||||||
|
|
||||||
def test_edit_window_functionality():
|
def test_edit_window_functionality():
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ui_manager import UIManager
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
def test_edit_window_punch_buttons():
|
def test_edit_window_punch_buttons():
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ui_manager import UIManager
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
def final_verification_test():
|
def final_verification_test():
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ui_manager import UIManager
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
def test_parse_dose_text():
|
def test_parse_dose_text():
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ui_manager import UIManager
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_punch_and_save():
|
def test_multiple_punch_and_save():
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ui_manager import UIManager
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
def test_programmatic_punch():
|
def test_programmatic_punch():
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ui_manager import UIManager
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
def test_punch_button_step_by_step():
|
def test_punch_button_step_by_step():
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ui_manager import UIManager
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
def test_punch_button_only():
|
def test_punch_button_only():
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ui_manager import UIManager
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
def test_save_functionality():
|
def test_save_functionality():
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "src"))
|
|||||||
|
|
||||||
def test_scrollable_input():
|
def test_scrollable_input():
|
||||||
"""Test the scrollable input frame."""
|
"""Test the scrollable input frame."""
|
||||||
from init import logger
|
from src.init import logger
|
||||||
from ui_manager import UIManager
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
# Create a test window
|
# Create a test window
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
"""The Chart - Medication tracking application."""
|
|
||||||
|
|
||||||
__version__ = "1.1.0"
|
|
||||||
__author__ = "Will"
|
|
||||||
__description__ = "Chart to monitor your medication intake over time."
|
|
||||||
+5
-1
@@ -1,8 +1,12 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv(override=True)
|
extDataDir = os.getcwd()
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
extDataDir = sys._MEIPASS
|
||||||
|
load_dotenv(dotenv_path=os.path.join(extDataDir, ".env"))
|
||||||
|
|
||||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||||
LOG_PATH = os.getenv("LOG_PATH", "/tmp/logs/thechart")
|
LOG_PATH = os.getenv("LOG_PATH", "/tmp/logs/thechart")
|
||||||
|
|||||||
+30
-5
@@ -11,9 +11,9 @@ class DataManager:
|
|||||||
def __init__(self, filename: str, logger: logging.Logger) -> None:
|
def __init__(self, filename: str, logger: logging.Logger) -> None:
|
||||||
self.filename: str = filename
|
self.filename: str = filename
|
||||||
self.logger: logging.Logger = logger
|
self.logger: logging.Logger = logger
|
||||||
self.initialize_csv()
|
self._initialize_csv_file()
|
||||||
|
|
||||||
def initialize_csv(self) -> None:
|
def _initialize_csv_file(self) -> None:
|
||||||
"""Create CSV file with headers if it doesn't exist."""
|
"""Create CSV file with headers if it doesn't exist."""
|
||||||
if not os.path.exists(self.filename):
|
if not os.path.exists(self.filename):
|
||||||
with open(self.filename, mode="w", newline="") as file:
|
with open(self.filename, mode="w", newline="") as file:
|
||||||
@@ -108,9 +108,32 @@ class DataManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Find the row to update using original_date as a unique identifier
|
# Find the row to update using original_date as a unique identifier
|
||||||
# Handle both old format (10 columns) and new format (14 columns)
|
# Handle both old format (10 columns) and new format (16 columns)
|
||||||
if len(values) == 14:
|
if len(values) == 16:
|
||||||
# New format with dose columns
|
# New format with all dose columns including quetiapine
|
||||||
|
df.loc[
|
||||||
|
df["date"] == original_date,
|
||||||
|
[
|
||||||
|
"date",
|
||||||
|
"depression",
|
||||||
|
"anxiety",
|
||||||
|
"sleep",
|
||||||
|
"appetite",
|
||||||
|
"bupropion",
|
||||||
|
"bupropion_doses",
|
||||||
|
"hydroxyzine",
|
||||||
|
"hydroxyzine_doses",
|
||||||
|
"gabapentin",
|
||||||
|
"gabapentin_doses",
|
||||||
|
"propranolol",
|
||||||
|
"propranolol_doses",
|
||||||
|
"quetiapine",
|
||||||
|
"quetiapine_doses",
|
||||||
|
"note",
|
||||||
|
],
|
||||||
|
] = values
|
||||||
|
elif len(values) == 14:
|
||||||
|
# Format without quetiapine
|
||||||
df.loc[
|
df.loc[
|
||||||
df["date"] == original_date,
|
df["date"] == original_date,
|
||||||
[
|
[
|
||||||
@@ -192,6 +215,8 @@ class DataManager:
|
|||||||
"gabapentin_doses": "",
|
"gabapentin_doses": "",
|
||||||
"propranolol": 0,
|
"propranolol": 0,
|
||||||
"propranolol_doses": "",
|
"propranolol_doses": "",
|
||||||
|
"quetiapine": 0,
|
||||||
|
"quetiapine_doses": "",
|
||||||
"note": "",
|
"note": "",
|
||||||
}
|
}
|
||||||
df = pd.concat([df, pd.DataFrame([new_entry])], ignore_index=True)
|
df = pd.concat([df, pd.DataFrame([new_entry])], ignore_index=True)
|
||||||
|
|||||||
+126
-9
@@ -24,6 +24,11 @@ class GraphManager:
|
|||||||
"anxiety": tk.BooleanVar(value=True),
|
"anxiety": tk.BooleanVar(value=True),
|
||||||
"sleep": tk.BooleanVar(value=True),
|
"sleep": tk.BooleanVar(value=True),
|
||||||
"appetite": tk.BooleanVar(value=True),
|
"appetite": tk.BooleanVar(value=True),
|
||||||
|
"bupropion": tk.BooleanVar(value=True), # Show by default (most used)
|
||||||
|
"hydroxyzine": tk.BooleanVar(value=False),
|
||||||
|
"gabapentin": tk.BooleanVar(value=False),
|
||||||
|
"propranolol": tk.BooleanVar(value=True), # Show by default (commonly used)
|
||||||
|
"quetiapine": tk.BooleanVar(value=False),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create control frame for toggles
|
# Create control frame for toggles
|
||||||
@@ -31,7 +36,7 @@ class GraphManager:
|
|||||||
self.control_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
self.control_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
||||||
|
|
||||||
# Create toggle checkboxes
|
# Create toggle checkboxes
|
||||||
self._create_toggle_controls()
|
self._create_chart_toggles()
|
||||||
|
|
||||||
# Create graph frame
|
# Create graph frame
|
||||||
self.graph_frame: ttk.Frame = ttk.Frame(self.parent_frame)
|
self.graph_frame: ttk.Frame = ttk.Frame(self.parent_frame)
|
||||||
@@ -53,29 +58,54 @@ class GraphManager:
|
|||||||
# Store current data for replotting
|
# Store current data for replotting
|
||||||
self.current_data: pd.DataFrame = pd.DataFrame()
|
self.current_data: pd.DataFrame = pd.DataFrame()
|
||||||
|
|
||||||
def _create_toggle_controls(self) -> None:
|
def _create_chart_toggles(self) -> None:
|
||||||
"""Create toggle controls for chart elements."""
|
"""Create toggle controls for chart elements."""
|
||||||
ttk.Label(self.control_frame, text="Show/Hide Elements:").pack(
|
ttk.Label(self.control_frame, text="Show/Hide Elements:").pack(
|
||||||
side="left", padx=5
|
side="left", padx=5
|
||||||
)
|
)
|
||||||
|
|
||||||
toggle_configs = [
|
# Symptoms toggles
|
||||||
|
symptoms_frame = ttk.LabelFrame(self.control_frame, text="Symptoms")
|
||||||
|
symptoms_frame.pack(side="left", padx=5, pady=2)
|
||||||
|
|
||||||
|
symptom_configs = [
|
||||||
("depression", "Depression"),
|
("depression", "Depression"),
|
||||||
("anxiety", "Anxiety"),
|
("anxiety", "Anxiety"),
|
||||||
("sleep", "Sleep"),
|
("sleep", "Sleep"),
|
||||||
("appetite", "Appetite"),
|
("appetite", "Appetite"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for key, label in toggle_configs:
|
for key, label in symptom_configs:
|
||||||
checkbox = ttk.Checkbutton(
|
checkbox = ttk.Checkbutton(
|
||||||
self.control_frame,
|
symptoms_frame,
|
||||||
text=label,
|
text=label,
|
||||||
variable=self.toggle_vars[key],
|
variable=self.toggle_vars[key],
|
||||||
command=self._on_toggle_changed,
|
command=self._handle_toggle_changed,
|
||||||
)
|
)
|
||||||
checkbox.pack(side="left", padx=5)
|
checkbox.pack(side="left", padx=3)
|
||||||
|
|
||||||
def _on_toggle_changed(self) -> None:
|
# Medicines toggles
|
||||||
|
medicines_frame = ttk.LabelFrame(self.control_frame, text="Medicines")
|
||||||
|
medicines_frame.pack(side="left", padx=5, pady=2)
|
||||||
|
|
||||||
|
medicine_configs = [
|
||||||
|
("bupropion", "Bupropion"),
|
||||||
|
("hydroxyzine", "Hydroxyzine"),
|
||||||
|
("gabapentin", "Gabapentin"),
|
||||||
|
("propranolol", "Propranolol"),
|
||||||
|
("quetiapine", "Quetiapine"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for key, label in medicine_configs:
|
||||||
|
checkbox = ttk.Checkbutton(
|
||||||
|
medicines_frame,
|
||||||
|
text=label,
|
||||||
|
variable=self.toggle_vars[key],
|
||||||
|
command=self._handle_toggle_changed,
|
||||||
|
)
|
||||||
|
checkbox.pack(side="left", padx=3)
|
||||||
|
|
||||||
|
def _handle_toggle_changed(self) -> None:
|
||||||
"""Handle toggle changes by replotting the graph."""
|
"""Handle toggle changes by replotting the graph."""
|
||||||
if not self.current_data.empty:
|
if not self.current_data.empty:
|
||||||
self._plot_graph_data(self.current_data)
|
self._plot_graph_data(self.current_data)
|
||||||
@@ -116,12 +146,59 @@ class GraphManager:
|
|||||||
)
|
)
|
||||||
has_plotted_series = True
|
has_plotted_series = True
|
||||||
|
|
||||||
|
# Plot medicine dose data
|
||||||
|
medicine_colors = {
|
||||||
|
"bupropion": "#FF6B6B", # Red
|
||||||
|
"hydroxyzine": "#4ECDC4", # Teal
|
||||||
|
"gabapentin": "#45B7D1", # Blue
|
||||||
|
"propranolol": "#96CEB4", # Green
|
||||||
|
"quetiapine": "#FFEAA7", # Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
medicines = [
|
||||||
|
"bupropion",
|
||||||
|
"hydroxyzine",
|
||||||
|
"gabapentin",
|
||||||
|
"propranolol",
|
||||||
|
"quetiapine",
|
||||||
|
]
|
||||||
|
|
||||||
|
for medicine in medicines:
|
||||||
|
dose_column = f"{medicine}_doses"
|
||||||
|
if self.toggle_vars[medicine].get() and dose_column in df.columns:
|
||||||
|
# Calculate daily dose totals
|
||||||
|
daily_doses = []
|
||||||
|
for dose_str in df[dose_column]:
|
||||||
|
total_dose = self._calculate_daily_dose(dose_str)
|
||||||
|
daily_doses.append(total_dose)
|
||||||
|
|
||||||
|
# Only plot if there are non-zero doses
|
||||||
|
if any(dose > 0 for dose in daily_doses):
|
||||||
|
# Scale doses for better visibility
|
||||||
|
# (divide by 10 to fit with 0-10 scale)
|
||||||
|
scaled_doses = [dose / 10 for dose in daily_doses]
|
||||||
|
self.ax.bar(
|
||||||
|
df.index,
|
||||||
|
scaled_doses,
|
||||||
|
alpha=0.6,
|
||||||
|
color=medicine_colors.get(medicine, "#DDA0DD"),
|
||||||
|
label=f"{medicine.capitalize()} (mg/10)",
|
||||||
|
width=0.6,
|
||||||
|
bottom=-max(scaled_doses) * 1.1 if scaled_doses else -1,
|
||||||
|
)
|
||||||
|
has_plotted_series = True
|
||||||
|
|
||||||
# Configure graph appearance
|
# Configure graph appearance
|
||||||
if has_plotted_series:
|
if has_plotted_series:
|
||||||
self.ax.legend()
|
self.ax.legend()
|
||||||
self.ax.set_title("Medication Effects Over Time")
|
self.ax.set_title("Medication Effects Over Time")
|
||||||
self.ax.set_xlabel("Date")
|
self.ax.set_xlabel("Date")
|
||||||
self.ax.set_ylabel("Rating (0-10)")
|
self.ax.set_ylabel("Rating (0-10) / Dose (mg)")
|
||||||
|
|
||||||
|
# Adjust y-axis to accommodate medicine bars at bottom
|
||||||
|
current_ylim = self.ax.get_ylim()
|
||||||
|
self.ax.set_ylim(bottom=current_ylim[0], top=max(10, current_ylim[1]))
|
||||||
|
|
||||||
self.fig.autofmt_xdate()
|
self.fig.autofmt_xdate()
|
||||||
|
|
||||||
# Redraw the canvas
|
# Redraw the canvas
|
||||||
@@ -144,6 +221,46 @@ class GraphManager:
|
|||||||
label=label,
|
label=label,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _calculate_daily_dose(self, dose_str: str) -> float:
|
||||||
|
"""Calculate total daily dose from dose string format."""
|
||||||
|
if not dose_str or pd.isna(dose_str) or str(dose_str).lower() == "nan":
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
total_dose = 0.0
|
||||||
|
# Handle different separators and clean the string
|
||||||
|
dose_str = str(dose_str).replace("•", "").strip()
|
||||||
|
|
||||||
|
# Split by | or by spaces if no | present
|
||||||
|
dose_entries = dose_str.split("|") if "|" in dose_str else [dose_str]
|
||||||
|
|
||||||
|
for entry in dose_entries:
|
||||||
|
entry = entry.strip()
|
||||||
|
if not entry:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if ":" in entry:
|
||||||
|
# Extract dose part after the timestamp
|
||||||
|
_, dose_part = entry.split(":", 1)
|
||||||
|
else:
|
||||||
|
# Handle cases where there's no timestamp
|
||||||
|
dose_part = entry
|
||||||
|
|
||||||
|
# Extract numeric part from dose (e.g., "150mg" -> 150)
|
||||||
|
dose_value = ""
|
||||||
|
for char in dose_part:
|
||||||
|
if char.isdigit() or char == ".":
|
||||||
|
dose_value += char
|
||||||
|
elif dose_value: # Stop at first non-digit after finding digits
|
||||||
|
break
|
||||||
|
|
||||||
|
if dose_value:
|
||||||
|
total_dose += float(dose_value)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return total_dose
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Clean up resources."""
|
"""Clean up resources."""
|
||||||
plt.close(self.fig)
|
plt.close(self.fig)
|
||||||
|
|||||||
+16
-16
@@ -19,7 +19,7 @@ class MedTrackerApp:
|
|||||||
self.root: tk.Tk = root
|
self.root: tk.Tk = root
|
||||||
self.root.resizable(True, True)
|
self.root.resizable(True, True)
|
||||||
self.root.title("Thechart - medication tracker")
|
self.root.title("Thechart - medication tracker")
|
||||||
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
self.root.protocol("WM_DELETE_WINDOW", self.handle_window_closing)
|
||||||
|
|
||||||
# Set up data file
|
# Set up data file
|
||||||
self.filename: str = "thechart_data.csv"
|
self.filename: str = "thechart_data.csv"
|
||||||
@@ -49,7 +49,7 @@ class MedTrackerApp:
|
|||||||
icon_path: str = "chart-671.png"
|
icon_path: str = "chart-671.png"
|
||||||
if not os.path.exists(icon_path) and os.path.exists("./chart-671.png"):
|
if not os.path.exists(icon_path) and os.path.exists("./chart-671.png"):
|
||||||
icon_path = "./chart-671.png"
|
icon_path = "./chart-671.png"
|
||||||
self.ui_manager.setup_icon(img_path=icon_path)
|
self.ui_manager.setup_application_icon(img_path=icon_path)
|
||||||
|
|
||||||
# Set up the main application UI
|
# Set up the main application UI
|
||||||
self._setup_main_ui()
|
self._setup_main_ui()
|
||||||
@@ -85,28 +85,28 @@ class MedTrackerApp:
|
|||||||
self.date_var: tk.StringVar = input_ui["date_var"]
|
self.date_var: tk.StringVar = input_ui["date_var"]
|
||||||
|
|
||||||
# Add buttons to input frame
|
# Add buttons to input frame
|
||||||
self.ui_manager.add_buttons(
|
self.ui_manager.add_action_buttons(
|
||||||
self.input_frame,
|
self.input_frame,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"text": "Add Entry",
|
"text": "Add Entry",
|
||||||
"command": self.add_entry,
|
"command": self.add_new_entry,
|
||||||
"fill": "both",
|
"fill": "both",
|
||||||
"expand": True,
|
"expand": True,
|
||||||
},
|
},
|
||||||
{"text": "Quit", "command": self.on_closing},
|
{"text": "Quit", "command": self.handle_window_closing},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Create Table Frame ---
|
# --- Create Table Frame ---
|
||||||
table_ui: dict[str, Any] = self.ui_manager.create_table_frame(main_frame)
|
table_ui: dict[str, Any] = self.ui_manager.create_table_frame(main_frame)
|
||||||
self.tree: ttk.Treeview = table_ui["tree"]
|
self.tree: ttk.Treeview = table_ui["tree"]
|
||||||
self.tree.bind("<Double-1>", self.on_double_click)
|
self.tree.bind("<Double-1>", self.handle_double_click)
|
||||||
|
|
||||||
# Load data
|
# Load data
|
||||||
self.load_data()
|
self.refresh_data_display()
|
||||||
|
|
||||||
def on_double_click(self, event: tk.Event) -> None:
|
def handle_double_click(self, event: tk.Event) -> None:
|
||||||
"""Handle double-click event to edit an entry."""
|
"""Handle double-click event to edit an entry."""
|
||||||
logger.debug("Double-click event triggered on treeview.")
|
logger.debug("Double-click event triggered on treeview.")
|
||||||
if len(self.tree.get_children()) > 0:
|
if len(self.tree.get_children()) > 0:
|
||||||
@@ -138,8 +138,8 @@ class MedTrackerApp:
|
|||||||
full_row["gabapentin_doses"],
|
full_row["gabapentin_doses"],
|
||||||
full_row["propranolol"],
|
full_row["propranolol"],
|
||||||
full_row["propranolol_doses"],
|
full_row["propranolol_doses"],
|
||||||
full_row.get("quetiapine", 0),
|
full_row["quetiapine"],
|
||||||
full_row.get("quetiapine_doses", ""),
|
full_row["quetiapine_doses"],
|
||||||
full_row["note"],
|
full_row["note"],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -198,7 +198,7 @@ class MedTrackerApp:
|
|||||||
"Success", "Entry updated successfully!", parent=self.root
|
"Success", "Entry updated successfully!", parent=self.root
|
||||||
)
|
)
|
||||||
self._clear_entries()
|
self._clear_entries()
|
||||||
self.load_data()
|
self.refresh_data_display()
|
||||||
else:
|
else:
|
||||||
# Check if it's a duplicate date issue
|
# Check if it's a duplicate date issue
|
||||||
df = self.data_manager.load_data()
|
df = self.data_manager.load_data()
|
||||||
@@ -212,14 +212,14 @@ class MedTrackerApp:
|
|||||||
else:
|
else:
|
||||||
messagebox.showerror("Error", "Failed to save changes", parent=edit_win)
|
messagebox.showerror("Error", "Failed to save changes", parent=edit_win)
|
||||||
|
|
||||||
def on_closing(self) -> None:
|
def handle_window_closing(self) -> None:
|
||||||
if messagebox.askokcancel(
|
if messagebox.askokcancel(
|
||||||
"Quit", "Do you want to quit the application?", parent=self.root
|
"Quit", "Do you want to quit the application?", parent=self.root
|
||||||
):
|
):
|
||||||
self.graph_manager.close()
|
self.graph_manager.close()
|
||||||
self.root.destroy()
|
self.root.destroy()
|
||||||
|
|
||||||
def add_entry(self) -> None:
|
def add_new_entry(self) -> None:
|
||||||
"""Add a new entry to the CSV file."""
|
"""Add a new entry to the CSV file."""
|
||||||
# Get current doses for today
|
# Get current doses for today
|
||||||
today = self.date_var.get()
|
today = self.date_var.get()
|
||||||
@@ -278,7 +278,7 @@ class MedTrackerApp:
|
|||||||
"Success", "Entry added successfully!", parent=self.root
|
"Success", "Entry added successfully!", parent=self.root
|
||||||
)
|
)
|
||||||
self._clear_entries()
|
self._clear_entries()
|
||||||
self.load_data()
|
self.refresh_data_display()
|
||||||
else:
|
else:
|
||||||
# Check if it's a duplicate date by trying to load existing data
|
# Check if it's a duplicate date by trying to load existing data
|
||||||
df = self.data_manager.load_data()
|
df = self.data_manager.load_data()
|
||||||
@@ -309,7 +309,7 @@ class MedTrackerApp:
|
|||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"Success", "Entry deleted successfully!", parent=self.root
|
"Success", "Entry deleted successfully!", parent=self.root
|
||||||
)
|
)
|
||||||
self.load_data()
|
self.refresh_data_display()
|
||||||
else:
|
else:
|
||||||
messagebox.showerror("Error", "Failed to delete entry", parent=edit_win)
|
messagebox.showerror("Error", "Failed to delete entry", parent=edit_win)
|
||||||
|
|
||||||
@@ -323,7 +323,7 @@ class MedTrackerApp:
|
|||||||
self.medicine_vars[key][0].set(0)
|
self.medicine_vars[key][0].set(0)
|
||||||
self.note_var.set("")
|
self.note_var.set("")
|
||||||
|
|
||||||
def load_data(self) -> None:
|
def refresh_data_display(self) -> None:
|
||||||
"""Load data from the CSV file into the table and graph."""
|
"""Load data from the CSV file into the table and graph."""
|
||||||
logger.debug("Loading data from CSV.")
|
logger.debug("Loading data from CSV.")
|
||||||
|
|
||||||
|
|||||||
+838
-410
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple test script to verify dose calculation functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
|
||||||
|
from src.graph_manager import GraphManager
|
||||||
|
|
||||||
|
|
||||||
|
def test_dose_calculation():
|
||||||
|
print("Testing dose calculation...")
|
||||||
|
|
||||||
|
# Create a minimal test setup
|
||||||
|
root = tk.Tk()
|
||||||
|
frame = ttk.LabelFrame(root, text="Test")
|
||||||
|
frame.pack()
|
||||||
|
|
||||||
|
# Create GraphManager instance
|
||||||
|
gm = GraphManager(frame)
|
||||||
|
|
||||||
|
# Test dose calculations
|
||||||
|
test_cases = [
|
||||||
|
("2025-07-28 18:59:45:150mg", 150.0),
|
||||||
|
("2025-07-28 18:59:45:150mg|2025-07-28 19:34:19:75mg", 225.0),
|
||||||
|
("• • • • 2025-07-30 07:50:00:300", 300.0),
|
||||||
|
("• 2025-07-30 22:50:00:10", 10.0),
|
||||||
|
("", 0.0),
|
||||||
|
("nan", 0.0),
|
||||||
|
("12.5mg", 12.5),
|
||||||
|
("100|50", 150.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
all_passed = True
|
||||||
|
for dose_str, expected in test_cases:
|
||||||
|
result = gm._calculate_daily_dose(dose_str)
|
||||||
|
passed = result == expected
|
||||||
|
status = "PASS" if passed else "FAIL"
|
||||||
|
print(f'{status}: "{dose_str[:30]}..." -> Expected: {expected}, Got: {result}')
|
||||||
|
if not passed:
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
root.destroy()
|
||||||
|
|
||||||
|
if all_passed:
|
||||||
|
print("\n✅ All dose calculation tests passed!")
|
||||||
|
else:
|
||||||
|
print("\n❌ Some tests failed!")
|
||||||
|
|
||||||
|
return all_passed
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_dose_calculation()
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test script to demonstrate the improved edit window."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import tkinter as tk
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add src directory to path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||||
|
|
||||||
|
from src.logger import logger
|
||||||
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_window():
|
||||||
|
"""Test the improved edit window."""
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title("Edit Window Test")
|
||||||
|
root.geometry("400x300")
|
||||||
|
|
||||||
|
ui_manager = UIManager(root, logger)
|
||||||
|
|
||||||
|
# Sample data for testing (16 fields format)
|
||||||
|
test_values = (
|
||||||
|
"12/25/2024", # date
|
||||||
|
7, # depression
|
||||||
|
5, # anxiety
|
||||||
|
6, # sleep
|
||||||
|
4, # appetite
|
||||||
|
1, # bupropion
|
||||||
|
"09:00:00:150|18:00:00:150", # bupropion_doses
|
||||||
|
1, # hydroxyzine
|
||||||
|
"21:30:00:25", # hydroxyzine_doses
|
||||||
|
0, # gabapentin
|
||||||
|
"", # gabapentin_doses
|
||||||
|
1, # propranolol
|
||||||
|
"07:00:00:10|14:00:00:10", # propranolol_doses
|
||||||
|
0, # quetiapine
|
||||||
|
"", # quetiapine_doses
|
||||||
|
# Had a good day overall, feeling better with new medication routine
|
||||||
|
"Had a good day overall, feeling better with the new medication routine.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock callbacks
|
||||||
|
def save_callback(win, *args):
|
||||||
|
print("Save called with args:", args)
|
||||||
|
win.destroy()
|
||||||
|
|
||||||
|
def delete_callback(win):
|
||||||
|
print("Delete called")
|
||||||
|
win.destroy()
|
||||||
|
|
||||||
|
callbacks = {"save": save_callback, "delete": delete_callback}
|
||||||
|
|
||||||
|
# Create the improved edit window
|
||||||
|
edit_win = ui_manager.create_edit_window(test_values, callbacks)
|
||||||
|
|
||||||
|
# Center the edit window
|
||||||
|
edit_win.update_idletasks()
|
||||||
|
x = (edit_win.winfo_screenwidth() // 2) - (edit_win.winfo_width() // 2)
|
||||||
|
y = (edit_win.winfo_screenheight() // 2) - (edit_win.winfo_height() // 2)
|
||||||
|
edit_win.geometry(f"+{x}+{y}")
|
||||||
|
|
||||||
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_edit_window()
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to verify mouse wheel scrolling works in both the new entry window
|
||||||
|
and edit window of TheChart application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
|
def test_scrolling():
|
||||||
|
"""Test both new entry and edit window scrolling."""
|
||||||
|
print("Testing mouse wheel scrolling functionality...")
|
||||||
|
|
||||||
|
# Create test root window
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title("Scrolling Test")
|
||||||
|
root.geometry("800x600")
|
||||||
|
|
||||||
|
# Create logger
|
||||||
|
logger = logging.getLogger("test")
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Create UI manager
|
||||||
|
ui_manager = UIManager(root, logger)
|
||||||
|
|
||||||
|
# Create main frame
|
||||||
|
main_frame = tk.Frame(root)
|
||||||
|
main_frame.pack(fill="both", expand=True)
|
||||||
|
main_frame.grid_rowconfigure(0, weight=1)
|
||||||
|
main_frame.grid_rowconfigure(1, weight=1)
|
||||||
|
main_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
main_frame.grid_columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
# Test 1: Create input frame (new entry window)
|
||||||
|
print("✓ Creating new entry input frame with mouse wheel scrolling...")
|
||||||
|
ui_manager.create_input_frame(main_frame)
|
||||||
|
|
||||||
|
# Test 2: Create edit window
|
||||||
|
def test_edit_window():
|
||||||
|
print("✓ Creating edit window with mouse wheel scrolling...")
|
||||||
|
# Sample data for edit window
|
||||||
|
test_values = (
|
||||||
|
"01/15/2025", # date
|
||||||
|
"3", # depression
|
||||||
|
"5", # anxiety
|
||||||
|
"7", # sleep
|
||||||
|
"4", # appetite
|
||||||
|
"1", # bupropion
|
||||||
|
"09:00: 150", # bup_doses
|
||||||
|
"0", # hydroxyzine
|
||||||
|
"", # hydro_doses
|
||||||
|
"1", # gabapentin
|
||||||
|
"20:00: 100", # gaba_doses
|
||||||
|
"0", # propranolol
|
||||||
|
"", # prop_doses
|
||||||
|
"0", # quetiapine
|
||||||
|
"", # quet_doses
|
||||||
|
"Test note", # note
|
||||||
|
)
|
||||||
|
|
||||||
|
callbacks = {
|
||||||
|
"save": lambda *args: print("Save callback called"),
|
||||||
|
"delete": lambda *args: print("Delete callback called"),
|
||||||
|
}
|
||||||
|
|
||||||
|
edit_window = ui_manager.create_edit_window(test_values, callbacks)
|
||||||
|
return edit_window
|
||||||
|
|
||||||
|
# Add test button
|
||||||
|
test_button = tk.Button(
|
||||||
|
main_frame,
|
||||||
|
text="Test Edit Window Scrolling",
|
||||||
|
command=test_edit_window,
|
||||||
|
font=("TkDefaultFont", 12),
|
||||||
|
bg="#4CAF50",
|
||||||
|
fg="white",
|
||||||
|
padx=20,
|
||||||
|
pady=10,
|
||||||
|
)
|
||||||
|
test_button.grid(row=2, column=0, columnspan=2, pady=20)
|
||||||
|
|
||||||
|
# Add instructions
|
||||||
|
instructions = tk.Label(
|
||||||
|
main_frame,
|
||||||
|
text="Instructions:\n\n"
|
||||||
|
"1. Use mouse wheel anywhere in the 'New Entry' section to test scrolling\n"
|
||||||
|
"2. Click 'Test Edit Window Scrolling' button\n"
|
||||||
|
"3. Use mouse wheel anywhere in the edit window to test scrolling\n"
|
||||||
|
"4. Both windows should scroll smoothly with mouse wheel\n\n"
|
||||||
|
"✓ Mouse wheel scrolling has been enhanced for both windows!",
|
||||||
|
font=("TkDefaultFont", 10),
|
||||||
|
justify="left",
|
||||||
|
bg="#E8F5E8",
|
||||||
|
padx=20,
|
||||||
|
pady=15,
|
||||||
|
)
|
||||||
|
instructions.grid(row=3, column=0, columnspan=2, padx=20, pady=10, sticky="ew")
|
||||||
|
|
||||||
|
print("✓ Test setup complete!")
|
||||||
|
print("\nMouse wheel scrolling features implemented:")
|
||||||
|
print(" • Recursive binding to all child widgets")
|
||||||
|
print(" • Platform-specific event handling (Windows/Linux)")
|
||||||
|
print(" • Focus management for consistent scrolling")
|
||||||
|
print(" • Works anywhere within the scrollable areas")
|
||||||
|
print("\nTest the scrolling by moving your mouse wheel over any part of the")
|
||||||
|
print("'New Entry' section or the edit window when opened.")
|
||||||
|
|
||||||
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_scrolling()
|
||||||
+22
-5
@@ -40,15 +40,17 @@ def sample_dataframe():
|
|||||||
'sleep': [4, 3, 5],
|
'sleep': [4, 3, 5],
|
||||||
'appetite': [3, 4, 2],
|
'appetite': [3, 4, 2],
|
||||||
'bupropion': [1, 1, 0],
|
'bupropion': [1, 1, 0],
|
||||||
'bupropion_doses': ['', '', ''],
|
'bupropion_doses': ['2024-01-01 08:00:00:150mg', '2024-01-02 08:00:00:300mg', ''],
|
||||||
'hydroxyzine': [0, 1, 0],
|
'hydroxyzine': [0, 1, 0],
|
||||||
'hydroxyzine_doses': ['', '', ''],
|
'hydroxyzine_doses': ['', '2024-01-02 20:00:00:25mg', ''],
|
||||||
'gabapentin': [2, 2, 1],
|
'gabapentin': [2, 2, 1],
|
||||||
'gabapentin_doses': ['', '', ''],
|
'gabapentin_doses': ['2024-01-01 12:00:00:100mg|2024-01-01 20:00:00:100mg',
|
||||||
|
'2024-01-02 12:00:00:100mg|2024-01-02 20:00:00:100mg',
|
||||||
|
'2024-01-03 12:00:00:100mg'],
|
||||||
'propranolol': [1, 0, 1],
|
'propranolol': [1, 0, 1],
|
||||||
'propranolol_doses': ['', '', ''],
|
'propranolol_doses': ['2024-01-01 12:00:00:10mg', '', '2024-01-03 12:00:00:20mg'],
|
||||||
'quetiapine': [0, 1, 0],
|
'quetiapine': [0, 1, 0],
|
||||||
'quetiapine_doses': ['', '', ''],
|
'quetiapine_doses': ['', '2024-01-02 22:00:00:50mg', ''],
|
||||||
'note': ['Test note 1', 'Test note 2', '']
|
'note': ['Test note 1', 'Test note 2', '']
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -72,3 +74,18 @@ def mock_env_vars(monkeypatch):
|
|||||||
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
||||||
monkeypatch.setenv("LOG_PATH", "/tmp/test_logs")
|
monkeypatch.setenv("LOG_PATH", "/tmp/test_logs")
|
||||||
monkeypatch.setenv("LOG_CLEAR", "False")
|
monkeypatch.setenv("LOG_CLEAR", "False")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_dose_data():
|
||||||
|
"""Sample dose data for testing dose calculation."""
|
||||||
|
return {
|
||||||
|
'standard_format': '2025-07-28 18:59:45:150mg|2025-07-28 19:34:19:75mg', # Should sum to 225
|
||||||
|
'with_bullets': '• • • • 2025-07-30 07:50:00:300', # Should be 300
|
||||||
|
'decimal_doses': '2025-07-28 18:59:45:12.5mg|2025-07-28 19:34:19:7.5mg', # Should sum to 20
|
||||||
|
'no_timestamp': '100mg|50mg', # Should sum to 150
|
||||||
|
'mixed_format': '• 2025-07-30 22:50:00:10|75mg', # Should sum to 85
|
||||||
|
'empty_string': '', # Should be 0
|
||||||
|
'nan_value': 'nan', # Should be 0
|
||||||
|
'no_units': '2025-07-28 18:59:45:10|2025-07-28 19:34:19:5', # Should sum to 15
|
||||||
|
}
|
||||||
|
|||||||
+25
-25
@@ -20,9 +20,9 @@ class TestConstants:
|
|||||||
if 'constants' in sys.modules:
|
if 'constants' in sys.modules:
|
||||||
importlib.reload(sys.modules['constants'])
|
importlib.reload(sys.modules['constants'])
|
||||||
else:
|
else:
|
||||||
import constants
|
import src.constants
|
||||||
|
|
||||||
assert constants.LOG_LEVEL == "INFO"
|
assert src.constants.LOG_LEVEL == "INFO"
|
||||||
|
|
||||||
def test_custom_log_level(self):
|
def test_custom_log_level(self):
|
||||||
"""Test custom LOG_LEVEL from environment."""
|
"""Test custom LOG_LEVEL from environment."""
|
||||||
@@ -31,9 +31,9 @@ class TestConstants:
|
|||||||
if 'constants' in sys.modules:
|
if 'constants' in sys.modules:
|
||||||
importlib.reload(sys.modules['constants'])
|
importlib.reload(sys.modules['constants'])
|
||||||
else:
|
else:
|
||||||
import constants
|
import src.constants
|
||||||
|
|
||||||
assert constants.LOG_LEVEL == "DEBUG"
|
assert src.constants.LOG_LEVEL == "DEBUG"
|
||||||
|
|
||||||
def test_default_log_path(self):
|
def test_default_log_path(self):
|
||||||
"""Test default LOG_PATH when not set in environment."""
|
"""Test default LOG_PATH when not set in environment."""
|
||||||
@@ -42,9 +42,9 @@ class TestConstants:
|
|||||||
if 'constants' in sys.modules:
|
if 'constants' in sys.modules:
|
||||||
importlib.reload(sys.modules['constants'])
|
importlib.reload(sys.modules['constants'])
|
||||||
else:
|
else:
|
||||||
import constants
|
import src.constants
|
||||||
|
|
||||||
assert constants.LOG_PATH == "/tmp/logs/thechart"
|
assert src.constants.LOG_PATH == "/tmp/logs/thechart"
|
||||||
|
|
||||||
def test_custom_log_path(self):
|
def test_custom_log_path(self):
|
||||||
"""Test custom LOG_PATH from environment."""
|
"""Test custom LOG_PATH from environment."""
|
||||||
@@ -53,9 +53,9 @@ class TestConstants:
|
|||||||
if 'constants' in sys.modules:
|
if 'constants' in sys.modules:
|
||||||
importlib.reload(sys.modules['constants'])
|
importlib.reload(sys.modules['constants'])
|
||||||
else:
|
else:
|
||||||
import constants
|
import src.constants
|
||||||
|
|
||||||
assert constants.LOG_PATH == "/custom/log/path"
|
assert src.constants.LOG_PATH == "/custom/log/path"
|
||||||
|
|
||||||
def test_default_log_clear(self):
|
def test_default_log_clear(self):
|
||||||
"""Test default LOG_CLEAR when not set in environment."""
|
"""Test default LOG_CLEAR when not set in environment."""
|
||||||
@@ -64,9 +64,9 @@ class TestConstants:
|
|||||||
if 'constants' in sys.modules:
|
if 'constants' in sys.modules:
|
||||||
importlib.reload(sys.modules['constants'])
|
importlib.reload(sys.modules['constants'])
|
||||||
else:
|
else:
|
||||||
import constants
|
import src.constants
|
||||||
|
|
||||||
assert constants.LOG_CLEAR == "False"
|
assert src.constants.LOG_CLEAR == "False"
|
||||||
|
|
||||||
def test_custom_log_clear_true(self):
|
def test_custom_log_clear_true(self):
|
||||||
"""Test LOG_CLEAR when set to true in environment."""
|
"""Test LOG_CLEAR when set to true in environment."""
|
||||||
@@ -75,9 +75,9 @@ class TestConstants:
|
|||||||
if 'constants' in sys.modules:
|
if 'constants' in sys.modules:
|
||||||
importlib.reload(sys.modules['constants'])
|
importlib.reload(sys.modules['constants'])
|
||||||
else:
|
else:
|
||||||
import constants
|
import src.constants
|
||||||
|
|
||||||
assert constants.LOG_CLEAR == "True"
|
assert src.constants.LOG_CLEAR == "True"
|
||||||
|
|
||||||
def test_custom_log_clear_false(self):
|
def test_custom_log_clear_false(self):
|
||||||
"""Test LOG_CLEAR when set to false in environment."""
|
"""Test LOG_CLEAR when set to false in environment."""
|
||||||
@@ -86,9 +86,9 @@ class TestConstants:
|
|||||||
if 'constants' in sys.modules:
|
if 'constants' in sys.modules:
|
||||||
importlib.reload(sys.modules['constants'])
|
importlib.reload(sys.modules['constants'])
|
||||||
else:
|
else:
|
||||||
import constants
|
import src.constants
|
||||||
|
|
||||||
assert constants.LOG_CLEAR == "False"
|
assert src.constants.LOG_CLEAR == "False"
|
||||||
|
|
||||||
def test_log_level_case_insensitive(self):
|
def test_log_level_case_insensitive(self):
|
||||||
"""Test that LOG_LEVEL is converted to uppercase."""
|
"""Test that LOG_LEVEL is converted to uppercase."""
|
||||||
@@ -97,9 +97,9 @@ class TestConstants:
|
|||||||
if 'constants' in sys.modules:
|
if 'constants' in sys.modules:
|
||||||
importlib.reload(sys.modules['constants'])
|
importlib.reload(sys.modules['constants'])
|
||||||
else:
|
else:
|
||||||
import constants
|
import src.constants
|
||||||
|
|
||||||
assert constants.LOG_LEVEL == "WARNING"
|
assert src.constants.LOG_LEVEL == "WARNING"
|
||||||
|
|
||||||
def test_dotenv_override(self):
|
def test_dotenv_override(self):
|
||||||
"""Test that dotenv override parameter is set to True."""
|
"""Test that dotenv override parameter is set to True."""
|
||||||
@@ -109,22 +109,22 @@ class TestConstants:
|
|||||||
if 'constants' in sys.modules:
|
if 'constants' in sys.modules:
|
||||||
importlib.reload(sys.modules['constants'])
|
importlib.reload(sys.modules['constants'])
|
||||||
else:
|
else:
|
||||||
import constants
|
import src.constants
|
||||||
|
|
||||||
mock_load_dotenv.assert_called_once_with(override=True)
|
mock_load_dotenv.assert_called_once_with(override=True)
|
||||||
|
|
||||||
def test_all_constants_are_strings(self):
|
def test_all_constants_are_strings(self):
|
||||||
"""Test that all constants are string type."""
|
"""Test that all constants are string type."""
|
||||||
import constants
|
import src.constants
|
||||||
|
|
||||||
assert isinstance(constants.LOG_LEVEL, str)
|
assert isinstance(src.constants.LOG_LEVEL, str)
|
||||||
assert isinstance(constants.LOG_PATH, str)
|
assert isinstance(src.constants.LOG_PATH, str)
|
||||||
assert isinstance(constants.LOG_CLEAR, str)
|
assert isinstance(src.constants.LOG_CLEAR, str)
|
||||||
|
|
||||||
def test_constants_not_empty(self):
|
def test_constants_not_empty(self):
|
||||||
"""Test that constants are not empty strings."""
|
"""Test that constants are not empty strings."""
|
||||||
import constants
|
import src.constants
|
||||||
|
|
||||||
assert constants.LOG_LEVEL != ""
|
assert src.constants.LOG_LEVEL != ""
|
||||||
assert constants.LOG_PATH != ""
|
assert src.constants.LOG_PATH != ""
|
||||||
assert constants.LOG_CLEAR != ""
|
assert src.constants.LOG_CLEAR != ""
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import pytest
|
||||||
|
from datetime import datetime
|
||||||
|
import tkinter as tk
|
||||||
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def root_window():
|
||||||
|
root = tk.Tk()
|
||||||
|
yield root
|
||||||
|
root.destroy()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ui_manager(root_window):
|
||||||
|
class DummyLogger:
|
||||||
|
def debug(self, *a, **k): pass
|
||||||
|
def warning(self, *a, **k): pass
|
||||||
|
def error(self, *a, **k): pass
|
||||||
|
return UIManager(root_window, DummyLogger())
|
||||||
|
|
||||||
|
def test_parse_dose_history_for_saving_bullet_and_delete(ui_manager):
|
||||||
|
# Simulate user editing: add, delete, and custom lines
|
||||||
|
date_str = "07/30/2025"
|
||||||
|
# User deletes one line, adds a custom one
|
||||||
|
text = """
|
||||||
|
• 09:00 AM - 150mg
|
||||||
|
• 06:00 PM - 150mg
|
||||||
|
Custom note
|
||||||
|
""".strip()
|
||||||
|
result = ui_manager._parse_dose_history_for_saving(text, date_str)
|
||||||
|
# Should parse both bullets and keep the custom line
|
||||||
|
assert "2025-07-30 09:00:00:150mg" in result
|
||||||
|
assert "2025-07-30 18:00:00:150mg" in result
|
||||||
|
assert "Custom note" in result
|
||||||
|
# If user deletes all, should return empty string
|
||||||
|
assert ui_manager._parse_dose_history_for_saving("", date_str) == ""
|
||||||
|
assert ui_manager._parse_dose_history_for_saving("No doses recorded today", date_str) == ""
|
||||||
|
|
||||||
|
def test_parse_dose_history_for_saving_simple_time(ui_manager):
|
||||||
|
date_str = "07/30/2025"
|
||||||
|
text = "09:00 150mg\n18:00 150mg"
|
||||||
|
result = ui_manager._parse_dose_history_for_saving(text, date_str)
|
||||||
|
assert "2025-07-30 09:00:00:150mg" in result
|
||||||
|
assert "2025-07-30 18:00:00:150mg" in result
|
||||||
|
|
||||||
|
def test_parse_dose_history_for_saving_mixed(ui_manager):
|
||||||
|
date_str = "07/30/2025"
|
||||||
|
text = "• 09:00 AM - 150mg\n18:00 150mg\nJust a note"
|
||||||
|
result = ui_manager._parse_dose_history_for_saving(text, date_str)
|
||||||
|
assert "2025-07-30 09:00:00:150mg" in result
|
||||||
|
assert "2025-07-30 18:00:00:150mg" in result
|
||||||
|
assert "Just a note" in result
|
||||||
+205
-6
@@ -12,7 +12,7 @@ import matplotlib.pyplot as plt
|
|||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||||
|
|
||||||
from graph_manager import GraphManager
|
from src.graph_manager import GraphManager
|
||||||
|
|
||||||
|
|
||||||
class TestGraphManager:
|
class TestGraphManager:
|
||||||
@@ -38,14 +38,32 @@ class TestGraphManager:
|
|||||||
|
|
||||||
assert gm.parent_frame == parent_frame
|
assert gm.parent_frame == parent_frame
|
||||||
assert isinstance(gm.toggle_vars, dict)
|
assert isinstance(gm.toggle_vars, dict)
|
||||||
|
|
||||||
|
# Check symptom toggles
|
||||||
assert "depression" in gm.toggle_vars
|
assert "depression" in gm.toggle_vars
|
||||||
assert "anxiety" in gm.toggle_vars
|
assert "anxiety" in gm.toggle_vars
|
||||||
assert "sleep" in gm.toggle_vars
|
assert "sleep" in gm.toggle_vars
|
||||||
assert "appetite" in gm.toggle_vars
|
assert "appetite" in gm.toggle_vars
|
||||||
|
|
||||||
# Check that all toggles are initially True
|
# Check medicine toggles
|
||||||
for var in gm.toggle_vars.values():
|
assert "bupropion" in gm.toggle_vars
|
||||||
assert var.get() is True
|
assert "hydroxyzine" in gm.toggle_vars
|
||||||
|
assert "gabapentin" in gm.toggle_vars
|
||||||
|
assert "propranolol" in gm.toggle_vars
|
||||||
|
assert "quetiapine" in gm.toggle_vars
|
||||||
|
|
||||||
|
# Check that symptom toggles are initially True
|
||||||
|
for symptom in ["depression", "anxiety", "sleep", "appetite"]:
|
||||||
|
assert gm.toggle_vars[symptom].get() is True
|
||||||
|
|
||||||
|
# Check that some medicine toggles are True by default
|
||||||
|
assert gm.toggle_vars["bupropion"].get() is True
|
||||||
|
assert gm.toggle_vars["propranolol"].get() is True
|
||||||
|
|
||||||
|
# Check that some medicine toggles are False by default
|
||||||
|
assert gm.toggle_vars["hydroxyzine"].get() is False
|
||||||
|
assert gm.toggle_vars["gabapentin"].get() is False
|
||||||
|
assert gm.toggle_vars["quetiapine"].get() is False
|
||||||
|
|
||||||
def test_toggle_controls_creation(self, parent_frame):
|
def test_toggle_controls_creation(self, parent_frame):
|
||||||
"""Test that toggle controls are created properly."""
|
"""Test that toggle controls are created properly."""
|
||||||
@@ -55,8 +73,9 @@ class TestGraphManager:
|
|||||||
assert hasattr(gm, 'control_frame')
|
assert hasattr(gm, 'control_frame')
|
||||||
assert isinstance(gm.control_frame, ttk.Frame)
|
assert isinstance(gm.control_frame, ttk.Frame)
|
||||||
|
|
||||||
# Check that toggle variables exist
|
# Check that all toggle variables exist
|
||||||
expected_toggles = ["depression", "anxiety", "sleep", "appetite"]
|
expected_toggles = ["depression", "anxiety", "sleep", "appetite",
|
||||||
|
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "quetiapine"]
|
||||||
for toggle in expected_toggles:
|
for toggle in expected_toggles:
|
||||||
assert toggle in gm.toggle_vars
|
assert toggle in gm.toggle_vars
|
||||||
assert isinstance(gm.toggle_vars[toggle], tk.BooleanVar)
|
assert isinstance(gm.toggle_vars[toggle], tk.BooleanVar)
|
||||||
@@ -265,3 +284,183 @@ class TestGraphManager:
|
|||||||
# Verify the graph was updated in each case
|
# Verify the graph was updated in each case
|
||||||
assert mock_ax.clear.call_count >= 2
|
assert mock_ax.clear.call_count >= 2
|
||||||
assert mock_canvas.draw.call_count >= 2
|
assert mock_canvas.draw.call_count >= 2
|
||||||
|
|
||||||
|
def test_calculate_daily_dose_empty_input(self, parent_frame):
|
||||||
|
"""Test dose calculation with empty/invalid input."""
|
||||||
|
gm = GraphManager(parent_frame)
|
||||||
|
|
||||||
|
# Test empty string
|
||||||
|
assert gm._calculate_daily_dose("") == 0.0
|
||||||
|
|
||||||
|
# Test NaN values
|
||||||
|
assert gm._calculate_daily_dose("nan") == 0.0
|
||||||
|
assert gm._calculate_daily_dose("NaN") == 0.0
|
||||||
|
|
||||||
|
# Test None (will be converted to string)
|
||||||
|
assert gm._calculate_daily_dose(None) == 0.0
|
||||||
|
|
||||||
|
def test_calculate_daily_dose_standard_format(self, parent_frame):
|
||||||
|
"""Test dose calculation with standard timestamp:dose format."""
|
||||||
|
gm = GraphManager(parent_frame)
|
||||||
|
|
||||||
|
# Single dose
|
||||||
|
dose_str = "2025-07-28 18:59:45:150mg"
|
||||||
|
assert gm._calculate_daily_dose(dose_str) == 150.0
|
||||||
|
|
||||||
|
# Multiple doses
|
||||||
|
dose_str = "2025-07-28 18:59:45:150mg|2025-07-28 19:34:19:75mg"
|
||||||
|
assert gm._calculate_daily_dose(dose_str) == 225.0
|
||||||
|
|
||||||
|
# Doses without units
|
||||||
|
dose_str = "2025-07-28 18:59:45:10|2025-07-28 19:34:19:5"
|
||||||
|
assert gm._calculate_daily_dose(dose_str) == 15.0
|
||||||
|
|
||||||
|
def test_calculate_daily_dose_with_symbols(self, parent_frame):
|
||||||
|
"""Test dose calculation with bullet symbols."""
|
||||||
|
gm = GraphManager(parent_frame)
|
||||||
|
|
||||||
|
# With bullet symbols
|
||||||
|
dose_str = "• • • • 2025-07-30 07:50:00:300"
|
||||||
|
assert gm._calculate_daily_dose(dose_str) == 300.0
|
||||||
|
|
||||||
|
# Multiple bullets
|
||||||
|
dose_str = "• 2025-07-30 22:50:00:10|• 2025-07-30 23:50:00:5"
|
||||||
|
assert gm._calculate_daily_dose(dose_str) == 15.0
|
||||||
|
|
||||||
|
def test_calculate_daily_dose_no_timestamp(self, parent_frame):
|
||||||
|
"""Test dose calculation without timestamp."""
|
||||||
|
gm = GraphManager(parent_frame)
|
||||||
|
|
||||||
|
# Just dose value
|
||||||
|
dose_str = "150mg"
|
||||||
|
assert gm._calculate_daily_dose(dose_str) == 150.0
|
||||||
|
|
||||||
|
# Multiple values without timestamp
|
||||||
|
dose_str = "100|50"
|
||||||
|
assert gm._calculate_daily_dose(dose_str) == 150.0
|
||||||
|
|
||||||
|
def test_calculate_daily_dose_decimal_values(self, parent_frame):
|
||||||
|
"""Test dose calculation with decimal values."""
|
||||||
|
gm = GraphManager(parent_frame)
|
||||||
|
|
||||||
|
# Decimal dose
|
||||||
|
dose_str = "2025-07-28 18:59:45:12.5mg"
|
||||||
|
assert gm._calculate_daily_dose(dose_str) == 12.5
|
||||||
|
|
||||||
|
# Multiple decimal doses
|
||||||
|
dose_str = "2025-07-28 18:59:45:12.5mg|2025-07-28 19:34:19:7.5mg"
|
||||||
|
assert gm._calculate_daily_dose(dose_str) == 20.0
|
||||||
|
|
||||||
|
def test_medicine_dose_plotting(self, parent_frame):
|
||||||
|
"""Test that medicine doses are plotted correctly."""
|
||||||
|
# Create a DataFrame with dose data
|
||||||
|
df_with_doses = pd.DataFrame({
|
||||||
|
'date': ['2024-01-01', '2024-01-02', '2024-01-03'],
|
||||||
|
'depression': [3, 2, 4],
|
||||||
|
'anxiety': [2, 3, 1],
|
||||||
|
'sleep': [4, 3, 5],
|
||||||
|
'appetite': [3, 4, 2],
|
||||||
|
'bupropion': [1, 1, 0],
|
||||||
|
'bupropion_doses': ['2024-01-01 08:00:00:150mg', '2024-01-02 08:00:00:300mg', ''],
|
||||||
|
'hydroxyzine': [0, 1, 0],
|
||||||
|
'hydroxyzine_doses': ['', '2024-01-02 20:00:00:25mg', ''],
|
||||||
|
'gabapentin': [0, 0, 0],
|
||||||
|
'gabapentin_doses': ['', '', ''],
|
||||||
|
'propranolol': [1, 0, 1],
|
||||||
|
'propranolol_doses': ['2024-01-01 12:00:00:10mg', '', '2024-01-03 12:00:00:20mg'],
|
||||||
|
'quetiapine': [0, 0, 0],
|
||||||
|
'quetiapine_doses': ['', '', ''],
|
||||||
|
})
|
||||||
|
|
||||||
|
with patch('matplotlib.pyplot.subplots') as mock_subplots:
|
||||||
|
mock_fig = Mock()
|
||||||
|
mock_ax = Mock()
|
||||||
|
mock_subplots.return_value = (mock_fig, mock_ax)
|
||||||
|
|
||||||
|
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
|
||||||
|
mock_canvas = Mock()
|
||||||
|
mock_canvas_class.return_value = mock_canvas
|
||||||
|
|
||||||
|
gm = GraphManager(parent_frame)
|
||||||
|
gm.update_graph(df_with_doses)
|
||||||
|
|
||||||
|
# Verify that bar plots were called (for medicines with doses)
|
||||||
|
mock_ax.bar.assert_called()
|
||||||
|
|
||||||
|
# Verify canvas was redrawn
|
||||||
|
mock_canvas.draw.assert_called()
|
||||||
|
|
||||||
|
def test_medicine_toggle_functionality(self, parent_frame):
|
||||||
|
"""Test that medicine toggles affect dose display."""
|
||||||
|
df_with_doses = pd.DataFrame({
|
||||||
|
'date': ['2024-01-01'],
|
||||||
|
'depression': [3],
|
||||||
|
'anxiety': [2],
|
||||||
|
'sleep': [4],
|
||||||
|
'appetite': [3],
|
||||||
|
'bupropion': [1],
|
||||||
|
'bupropion_doses': ['2024-01-01 08:00:00:150mg'],
|
||||||
|
'hydroxyzine': [0],
|
||||||
|
'hydroxyzine_doses': [''],
|
||||||
|
'gabapentin': [0],
|
||||||
|
'gabapentin_doses': [''],
|
||||||
|
'propranolol': [1],
|
||||||
|
'propranolol_doses': ['2024-01-01 12:00:00:10mg'],
|
||||||
|
'quetiapine': [0],
|
||||||
|
'quetiapine_doses': [''],
|
||||||
|
})
|
||||||
|
|
||||||
|
with patch('matplotlib.pyplot.subplots') as mock_subplots:
|
||||||
|
mock_fig = Mock()
|
||||||
|
mock_ax = Mock()
|
||||||
|
mock_subplots.return_value = (mock_fig, mock_ax)
|
||||||
|
|
||||||
|
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
|
||||||
|
mock_canvas = Mock()
|
||||||
|
mock_canvas_class.return_value = mock_canvas
|
||||||
|
|
||||||
|
gm = GraphManager(parent_frame)
|
||||||
|
|
||||||
|
# Turn off bupropion toggle
|
||||||
|
gm.toggle_vars["bupropion"].set(False)
|
||||||
|
gm.update_graph(df_with_doses)
|
||||||
|
|
||||||
|
# Turn on hydroxyzine toggle (though it has no doses)
|
||||||
|
gm.toggle_vars["hydroxyzine"].set(True)
|
||||||
|
gm.update_graph(df_with_doses)
|
||||||
|
|
||||||
|
# Verify the graph was updated
|
||||||
|
assert mock_ax.clear.call_count >= 2
|
||||||
|
assert mock_canvas.draw.call_count >= 2
|
||||||
|
|
||||||
|
def test_dose_calculation_comprehensive(self, parent_frame, sample_dose_data):
|
||||||
|
"""Test dose calculation with comprehensive test cases."""
|
||||||
|
gm = GraphManager(parent_frame)
|
||||||
|
|
||||||
|
# Test all sample dose data cases
|
||||||
|
assert gm._calculate_daily_dose(sample_dose_data['standard_format']) == 225.0
|
||||||
|
assert gm._calculate_daily_dose(sample_dose_data['with_bullets']) == 300.0
|
||||||
|
assert gm._calculate_daily_dose(sample_dose_data['decimal_doses']) == 20.0
|
||||||
|
assert gm._calculate_daily_dose(sample_dose_data['no_timestamp']) == 150.0
|
||||||
|
assert gm._calculate_daily_dose(sample_dose_data['mixed_format']) == 85.0
|
||||||
|
assert gm._calculate_daily_dose(sample_dose_data['empty_string']) == 0.0
|
||||||
|
assert gm._calculate_daily_dose(sample_dose_data['nan_value']) == 0.0
|
||||||
|
assert gm._calculate_daily_dose(sample_dose_data['no_units']) == 15.0
|
||||||
|
|
||||||
|
def test_dose_calculation_edge_cases(self, parent_frame):
|
||||||
|
"""Test dose calculation with edge cases."""
|
||||||
|
gm = GraphManager(parent_frame)
|
||||||
|
|
||||||
|
# Test with malformed data
|
||||||
|
assert gm._calculate_daily_dose("malformed:data") == 0.0
|
||||||
|
assert gm._calculate_daily_dose("::::") == 0.0
|
||||||
|
assert gm._calculate_daily_dose("2025-07-28:") == 0.0
|
||||||
|
assert gm._calculate_daily_dose("2025-07-28::mg") == 0.0
|
||||||
|
|
||||||
|
# Test with partial data
|
||||||
|
assert gm._calculate_daily_dose("2025-07-28 18:59:45:150") == 150.0 # no units
|
||||||
|
assert gm._calculate_daily_dose("150mg") == 150.0 # no timestamp
|
||||||
|
|
||||||
|
# Test with spaces and special characters
|
||||||
|
assert gm._calculate_daily_dose(" 2025-07-28 18:59:45:150mg ") == 150.0
|
||||||
|
assert gm._calculate_daily_dose("••• 2025-07-28 18:59:45:150mg •••") == 150.0
|
||||||
|
|||||||
+19
-19
@@ -24,7 +24,7 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
mock_mkdir.assert_called_once()
|
mock_mkdir.assert_called_once()
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
mock_mkdir.assert_not_called()
|
mock_mkdir.assert_not_called()
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
mock_print.assert_called()
|
mock_print.assert_called()
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
mock_init_logger.assert_called_once_with('init', testing_mode=False)
|
mock_init_logger.assert_called_once_with('init', testing_mode=False)
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
mock_init_logger.assert_called_once_with('init', testing_mode=True)
|
mock_init_logger.assert_called_once_with('init', testing_mode=True)
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
expected_files = (
|
expected_files = (
|
||||||
f"{temp_log_dir}/thechart.log",
|
f"{temp_log_dir}/thechart.log",
|
||||||
@@ -106,7 +106,7 @@ class TestInit:
|
|||||||
f"{temp_log_dir}/thechart.error.log",
|
f"{temp_log_dir}/thechart.error.log",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert init.log_files == expected_files
|
assert src.init.log_files == expected_files
|
||||||
|
|
||||||
def test_testing_mode_detection(self, temp_log_dir):
|
def test_testing_mode_detection(self, temp_log_dir):
|
||||||
"""Test that testing mode is detected correctly."""
|
"""Test that testing mode is detected correctly."""
|
||||||
@@ -117,14 +117,14 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
assert init.testing_mode is True
|
assert src.init.testing_mode is True
|
||||||
|
|
||||||
# Test with non-DEBUG level
|
# Test with non-DEBUG level
|
||||||
with patch('init.LOG_LEVEL', 'INFO'):
|
with patch('init.LOG_LEVEL', 'INFO'):
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
assert init.testing_mode is False
|
assert src.init.testing_mode is False
|
||||||
|
|
||||||
def test_log_clear_true(self, temp_log_dir):
|
def test_log_clear_true(self, temp_log_dir):
|
||||||
"""Test log file clearing when LOG_CLEAR is True."""
|
"""Test log file clearing when LOG_CLEAR is True."""
|
||||||
@@ -147,7 +147,7 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
# Check that files were truncated
|
# Check that files were truncated
|
||||||
for log_file in log_files:
|
for log_file in log_files:
|
||||||
@@ -176,7 +176,7 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
# Check that files were not truncated
|
# Check that files were not truncated
|
||||||
for log_file in log_files:
|
for log_file in log_files:
|
||||||
@@ -203,7 +203,7 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
def test_log_clear_permission_error(self, temp_log_dir):
|
def test_log_clear_permission_error(self, temp_log_dir):
|
||||||
"""Test handling of permission errors during log clearing."""
|
"""Test handling of permission errors during log clearing."""
|
||||||
@@ -226,7 +226,7 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
def test_module_exports(self, temp_log_dir):
|
def test_module_exports(self, temp_log_dir):
|
||||||
"""Test that module exports expected objects."""
|
"""Test that module exports expected objects."""
|
||||||
@@ -235,12 +235,12 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
# Check that expected objects are available
|
# Check that expected objects are available
|
||||||
assert hasattr(init, 'logger')
|
assert hasattr(src.init, 'logger')
|
||||||
assert hasattr(init, 'log_files')
|
assert hasattr(src.init, 'log_files')
|
||||||
assert hasattr(init, 'testing_mode')
|
assert hasattr(src.init, 'testing_mode')
|
||||||
|
|
||||||
def test_log_path_printing(self, temp_log_dir):
|
def test_log_path_printing(self, temp_log_dir):
|
||||||
"""Test that LOG_PATH is printed when directory is created."""
|
"""Test that LOG_PATH is printed when directory is created."""
|
||||||
@@ -253,6 +253,6 @@ class TestInit:
|
|||||||
if 'init' in sys.modules:
|
if 'init' in sys.modules:
|
||||||
importlib.reload(sys.modules['init'])
|
importlib.reload(sys.modules['init'])
|
||||||
else:
|
else:
|
||||||
import init
|
import src.init
|
||||||
|
|
||||||
mock_print.assert_called_with(temp_log_dir + '/new_dir')
|
mock_print.assert_called_with(temp_log_dir + '/new_dir')
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from unittest.mock import patch, Mock
|
|||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||||
|
|
||||||
from logger import init_logger
|
from src.logger import init_logger
|
||||||
|
|
||||||
|
|
||||||
class TestLogger:
|
class TestLogger:
|
||||||
|
|||||||
+26
-26
@@ -10,7 +10,7 @@ import pandas as pd
|
|||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||||
|
|
||||||
from main import MedTrackerApp
|
from src.main import MedTrackerApp
|
||||||
|
|
||||||
|
|
||||||
class TestMedTrackerApp:
|
class TestMedTrackerApp:
|
||||||
@@ -90,8 +90,8 @@ class TestMedTrackerApp:
|
|||||||
|
|
||||||
app = MedTrackerApp(root_window)
|
app = MedTrackerApp(root_window)
|
||||||
|
|
||||||
# Check that setup_icon was called on UI manager
|
# Check that setup_application_icon was called on UI manager
|
||||||
app.ui_manager.setup_icon.assert_called()
|
app.ui_manager.setup_application_icon.assert_called()
|
||||||
|
|
||||||
def test_icon_setup_fallback_path(self, root_window, mock_managers):
|
def test_icon_setup_fallback_path(self, root_window, mock_managers):
|
||||||
"""Test icon setup with fallback path."""
|
"""Test icon setup with fallback path."""
|
||||||
@@ -103,10 +103,10 @@ class TestMedTrackerApp:
|
|||||||
|
|
||||||
app = MedTrackerApp(root_window)
|
app = MedTrackerApp(root_window)
|
||||||
|
|
||||||
# Check that setup_icon was called with fallback path
|
# Check that setup_application_icon was called with fallback path
|
||||||
app.ui_manager.setup_icon.assert_called_with(img_path="./chart-671.png")
|
app.ui_manager.setup_application_icon.assert_called_with(img_path="./chart-671.png")
|
||||||
|
|
||||||
def test_add_entry_success(self, root_window, mock_managers):
|
def test_add_new_entry_success(self, root_window, mock_managers):
|
||||||
"""Test successful entry addition."""
|
"""Test successful entry addition."""
|
||||||
with patch('sys.argv', ['main.py']):
|
with patch('sys.argv', ['main.py']):
|
||||||
app = MedTrackerApp(root_window)
|
app = MedTrackerApp(root_window)
|
||||||
@@ -136,15 +136,15 @@ class TestMedTrackerApp:
|
|||||||
|
|
||||||
with patch('tkinter.messagebox.showinfo') as mock_info, \
|
with patch('tkinter.messagebox.showinfo') as mock_info, \
|
||||||
patch.object(app, '_clear_entries') as mock_clear, \
|
patch.object(app, '_clear_entries') as mock_clear, \
|
||||||
patch.object(app, 'load_data') as mock_load:
|
patch.object(app, 'refresh_data_display') as mock_load:
|
||||||
|
|
||||||
app.add_entry()
|
app.add_new_entry()
|
||||||
|
|
||||||
mock_info.assert_called_once()
|
mock_info.assert_called_once()
|
||||||
mock_clear.assert_called_once()
|
mock_clear.assert_called_once()
|
||||||
mock_load.assert_called_once()
|
mock_load.assert_called_once()
|
||||||
|
|
||||||
def test_add_entry_empty_date(self, root_window, mock_managers):
|
def test_add_new_entry_empty_date(self, root_window, mock_managers):
|
||||||
"""Test adding entry with empty date."""
|
"""Test adding entry with empty date."""
|
||||||
with patch('sys.argv', ['main.py']):
|
with patch('sys.argv', ['main.py']):
|
||||||
app = MedTrackerApp(root_window)
|
app = MedTrackerApp(root_window)
|
||||||
@@ -153,13 +153,13 @@ class TestMedTrackerApp:
|
|||||||
app.date_var.get.return_value = " " # Empty/whitespace date
|
app.date_var.get.return_value = " " # Empty/whitespace date
|
||||||
|
|
||||||
with patch('tkinter.messagebox.showerror') as mock_error:
|
with patch('tkinter.messagebox.showerror') as mock_error:
|
||||||
app.add_entry()
|
app.add_new_entry()
|
||||||
|
|
||||||
mock_error.assert_called_once_with(
|
mock_error.assert_called_once_with(
|
||||||
"Error", "Please enter a date.", parent=app.root
|
"Error", "Please enter a date.", parent=app.root
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_add_entry_duplicate_date(self, root_window, mock_managers):
|
def test_add_new_entry_duplicate_date(self, root_window, mock_managers):
|
||||||
"""Test adding entry with duplicate date."""
|
"""Test adding entry with duplicate date."""
|
||||||
with patch('sys.argv', ['main.py']):
|
with patch('sys.argv', ['main.py']):
|
||||||
app = MedTrackerApp(root_window)
|
app = MedTrackerApp(root_window)
|
||||||
@@ -186,12 +186,12 @@ class TestMedTrackerApp:
|
|||||||
app.data_manager.load_data.return_value = mock_df
|
app.data_manager.load_data.return_value = mock_df
|
||||||
|
|
||||||
with patch('tkinter.messagebox.showerror') as mock_error:
|
with patch('tkinter.messagebox.showerror') as mock_error:
|
||||||
app.add_entry()
|
app.add_new_entry()
|
||||||
|
|
||||||
mock_error.assert_called_once()
|
mock_error.assert_called_once()
|
||||||
assert "already exists" in mock_error.call_args[0][1]
|
assert "already exists" in mock_error.call_args[0][1]
|
||||||
|
|
||||||
def test_on_double_click(self, root_window, mock_managers):
|
def test_handle_double_click(self, root_window, mock_managers):
|
||||||
"""Test double-click event handling."""
|
"""Test double-click event handling."""
|
||||||
with patch('sys.argv', ['main.py']):
|
with patch('sys.argv', ['main.py']):
|
||||||
app = MedTrackerApp(root_window)
|
app = MedTrackerApp(root_window)
|
||||||
@@ -205,11 +205,11 @@ class TestMedTrackerApp:
|
|||||||
mock_event = Mock()
|
mock_event = Mock()
|
||||||
|
|
||||||
with patch.object(app, '_create_edit_window') as mock_create_edit:
|
with patch.object(app, '_create_edit_window') as mock_create_edit:
|
||||||
app.on_double_click(mock_event)
|
app.handle_double_click(mock_event)
|
||||||
|
|
||||||
mock_create_edit.assert_called_once()
|
mock_create_edit.assert_called_once()
|
||||||
|
|
||||||
def test_on_double_click_empty_tree(self, root_window, mock_managers):
|
def test_handle_double_click_empty_tree(self, root_window, mock_managers):
|
||||||
"""Test double-click when tree is empty."""
|
"""Test double-click when tree is empty."""
|
||||||
with patch('sys.argv', ['main.py']):
|
with patch('sys.argv', ['main.py']):
|
||||||
app = MedTrackerApp(root_window)
|
app = MedTrackerApp(root_window)
|
||||||
@@ -220,7 +220,7 @@ class TestMedTrackerApp:
|
|||||||
mock_event = Mock()
|
mock_event = Mock()
|
||||||
|
|
||||||
with patch.object(app, '_create_edit_window') as mock_create_edit:
|
with patch.object(app, '_create_edit_window') as mock_create_edit:
|
||||||
app.on_double_click(mock_event)
|
app.handle_double_click(mock_event)
|
||||||
|
|
||||||
mock_create_edit.assert_not_called()
|
mock_create_edit.assert_not_called()
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ class TestMedTrackerApp:
|
|||||||
|
|
||||||
with patch('tkinter.messagebox.showinfo') as mock_info, \
|
with patch('tkinter.messagebox.showinfo') as mock_info, \
|
||||||
patch.object(app, '_clear_entries') as mock_clear, \
|
patch.object(app, '_clear_entries') as mock_clear, \
|
||||||
patch.object(app, 'load_data') as mock_load:
|
patch.object(app, 'refresh_data_display') as mock_load:
|
||||||
|
|
||||||
app._save_edit(
|
app._save_edit(
|
||||||
mock_edit_win, "2024-01-01", "2024-01-01",
|
mock_edit_win, "2024-01-01", "2024-01-01",
|
||||||
@@ -286,7 +286,7 @@ class TestMedTrackerApp:
|
|||||||
|
|
||||||
with patch('tkinter.messagebox.askyesno', return_value=True) as mock_confirm, \
|
with patch('tkinter.messagebox.askyesno', return_value=True) as mock_confirm, \
|
||||||
patch('tkinter.messagebox.showinfo') as mock_info, \
|
patch('tkinter.messagebox.showinfo') as mock_info, \
|
||||||
patch.object(app, 'load_data') as mock_load:
|
patch.object(app, 'refresh_data_display') as mock_load:
|
||||||
|
|
||||||
app._delete_entry(mock_edit_win, 'item1')
|
app._delete_entry(mock_edit_win, 'item1')
|
||||||
|
|
||||||
@@ -328,7 +328,7 @@ class TestMedTrackerApp:
|
|||||||
for med_var in app.medicine_vars.values():
|
for med_var in app.medicine_vars.values():
|
||||||
med_var[0].set.assert_called_with(0)
|
med_var[0].set.assert_called_with(0)
|
||||||
|
|
||||||
def test_load_data(self, root_window, mock_managers):
|
def test_refresh_data_display(self, root_window, mock_managers):
|
||||||
"""Test loading data into tree and graph."""
|
"""Test loading data into tree and graph."""
|
||||||
with patch('sys.argv', ['main.py']):
|
with patch('sys.argv', ['main.py']):
|
||||||
app = MedTrackerApp(root_window)
|
app = MedTrackerApp(root_window)
|
||||||
@@ -345,7 +345,7 @@ class TestMedTrackerApp:
|
|||||||
})
|
})
|
||||||
app.data_manager.load_data.return_value = mock_df
|
app.data_manager.load_data.return_value = mock_df
|
||||||
|
|
||||||
app.load_data()
|
app.refresh_data_display()
|
||||||
|
|
||||||
# Check that tree was cleared and populated
|
# Check that tree was cleared and populated
|
||||||
app.tree.delete.assert_called()
|
app.tree.delete.assert_called()
|
||||||
@@ -354,7 +354,7 @@ class TestMedTrackerApp:
|
|||||||
# Check that graph was updated
|
# Check that graph was updated
|
||||||
app.graph_manager.update_graph.assert_called_with(mock_df)
|
app.graph_manager.update_graph.assert_called_with(mock_df)
|
||||||
|
|
||||||
def test_load_data_empty_dataframe(self, root_window, mock_managers):
|
def test_refresh_data_display_empty_dataframe(self, root_window, mock_managers):
|
||||||
"""Test loading empty data."""
|
"""Test loading empty data."""
|
||||||
with patch('sys.argv', ['main.py']):
|
with patch('sys.argv', ['main.py']):
|
||||||
app = MedTrackerApp(root_window)
|
app = MedTrackerApp(root_window)
|
||||||
@@ -366,29 +366,29 @@ class TestMedTrackerApp:
|
|||||||
empty_df = pd.DataFrame()
|
empty_df = pd.DataFrame()
|
||||||
app.data_manager.load_data.return_value = empty_df
|
app.data_manager.load_data.return_value = empty_df
|
||||||
|
|
||||||
app.load_data()
|
app.refresh_data_display()
|
||||||
|
|
||||||
# Graph should still be updated even with empty data
|
# Graph should still be updated even with empty data
|
||||||
app.graph_manager.update_graph.assert_called_with(empty_df)
|
app.graph_manager.update_graph.assert_called_with(empty_df)
|
||||||
|
|
||||||
def test_on_closing_confirmed(self, root_window, mock_managers):
|
def test_handle_window_closing_confirmed(self, root_window, mock_managers):
|
||||||
"""Test application closing when confirmed."""
|
"""Test application closing when confirmed."""
|
||||||
with patch('sys.argv', ['main.py']):
|
with patch('sys.argv', ['main.py']):
|
||||||
app = MedTrackerApp(root_window)
|
app = MedTrackerApp(root_window)
|
||||||
|
|
||||||
with patch('tkinter.messagebox.askokcancel', return_value=True) as mock_confirm:
|
with patch('tkinter.messagebox.askokcancel', return_value=True) as mock_confirm:
|
||||||
app.on_closing()
|
app.handle_window_closing()
|
||||||
|
|
||||||
mock_confirm.assert_called_once()
|
mock_confirm.assert_called_once()
|
||||||
app.graph_manager.close.assert_called_once()
|
app.graph_manager.close.assert_called_once()
|
||||||
|
|
||||||
def test_on_closing_cancelled(self, root_window, mock_managers):
|
def test_handle_window_closing_cancelled(self, root_window, mock_managers):
|
||||||
"""Test application closing when cancelled."""
|
"""Test application closing when cancelled."""
|
||||||
with patch('sys.argv', ['main.py']):
|
with patch('sys.argv', ['main.py']):
|
||||||
app = MedTrackerApp(root_window)
|
app = MedTrackerApp(root_window)
|
||||||
|
|
||||||
with patch('tkinter.messagebox.askokcancel', return_value=False) as mock_confirm:
|
with patch('tkinter.messagebox.askokcancel', return_value=False) as mock_confirm:
|
||||||
app.on_closing()
|
app.handle_window_closing()
|
||||||
|
|
||||||
mock_confirm.assert_called_once()
|
mock_confirm.assert_called_once()
|
||||||
app.graph_manager.close.assert_not_called()
|
app.graph_manager.close.assert_not_called()
|
||||||
|
|||||||
+37
-50
@@ -37,7 +37,7 @@ class TestUIManager:
|
|||||||
|
|
||||||
@patch('os.path.exists')
|
@patch('os.path.exists')
|
||||||
@patch('PIL.Image.open')
|
@patch('PIL.Image.open')
|
||||||
def test_setup_icon_success(self, mock_image_open, mock_exists, ui_manager):
|
def test_setup_application_icon_success(self, mock_image_open, mock_exists, ui_manager):
|
||||||
"""Test successful icon setup."""
|
"""Test successful icon setup."""
|
||||||
mock_exists.return_value = True
|
mock_exists.return_value = True
|
||||||
mock_image = Mock()
|
mock_image = Mock()
|
||||||
@@ -48,39 +48,42 @@ class TestUIManager:
|
|||||||
mock_photo_instance = Mock()
|
mock_photo_instance = Mock()
|
||||||
mock_photo.return_value = mock_photo_instance
|
mock_photo.return_value = mock_photo_instance
|
||||||
|
|
||||||
result = ui_manager.setup_icon("test_icon.png")
|
with patch.object(ui_manager.root, 'iconphoto') as mock_iconphoto, \
|
||||||
|
patch.object(ui_manager.root, 'wm_iconphoto') as mock_wm_iconphoto:
|
||||||
|
|
||||||
assert result is True
|
result = ui_manager.setup_application_icon("test_icon.png")
|
||||||
mock_image_open.assert_called_once_with("test_icon.png")
|
|
||||||
mock_image.resize.assert_called_once_with(size=(32, 32), resample=Mock())
|
assert result is True
|
||||||
ui_manager.logger.info.assert_called_with("Trying to load icon from: test_icon.png")
|
mock_image_open.assert_called_once_with("test_icon.png")
|
||||||
|
mock_image.resize.assert_called_once()
|
||||||
|
ui_manager.logger.info.assert_called_with("Trying to load icon from: test_icon.png")
|
||||||
|
|
||||||
@patch('os.path.exists')
|
@patch('os.path.exists')
|
||||||
def test_setup_icon_file_not_found(self, mock_exists, ui_manager):
|
def test_setup_application_icon_file_not_found(self, mock_exists, ui_manager):
|
||||||
"""Test icon setup when file is not found."""
|
"""Test icon setup when file is not found."""
|
||||||
mock_exists.return_value = False
|
mock_exists.return_value = False
|
||||||
|
|
||||||
result = ui_manager.setup_icon("nonexistent_icon.png")
|
result = ui_manager.setup_application_icon("nonexistent_icon.png")
|
||||||
|
|
||||||
assert result is False
|
assert result is False
|
||||||
ui_manager.logger.warning.assert_called_with("Icon file not found at nonexistent_icon.png")
|
ui_manager.logger.warning.assert_called_with("Icon file not found at nonexistent_icon.png")
|
||||||
|
|
||||||
@patch('os.path.exists')
|
@patch('os.path.exists')
|
||||||
@patch('PIL.Image.open')
|
@patch('PIL.Image.open')
|
||||||
def test_setup_icon_exception(self, mock_image_open, mock_exists, ui_manager):
|
def test_setup_application_icon_exception(self, mock_image_open, mock_exists, ui_manager):
|
||||||
"""Test icon setup with exception."""
|
"""Test icon setup with exception."""
|
||||||
mock_exists.return_value = True
|
mock_exists.return_value = True
|
||||||
mock_image_open.side_effect = Exception("Test error")
|
mock_image_open.side_effect = Exception("Test error")
|
||||||
|
|
||||||
result = ui_manager.setup_icon("test_icon.png")
|
result = ui_manager.setup_application_icon("test_icon.png")
|
||||||
|
|
||||||
assert result is False
|
assert result is False
|
||||||
ui_manager.logger.error.assert_called_with("Error setting up icon: Test error")
|
ui_manager.logger.error.assert_called_with("Error setting icon: Test error")
|
||||||
|
|
||||||
@patch('sys._MEIPASS', '/test/bundle/path', create=True)
|
@patch('sys._MEIPASS', '/test/bundle/path', create=True)
|
||||||
@patch('os.path.exists')
|
@patch('os.path.exists')
|
||||||
@patch('PIL.Image.open')
|
@patch('PIL.Image.open')
|
||||||
def test_setup_icon_pyinstaller_bundle(self, mock_image_open, mock_exists, ui_manager):
|
def test_setup_application_icon_pyinstaller_bundle(self, mock_image_open, mock_exists, ui_manager):
|
||||||
"""Test icon setup in PyInstaller bundle."""
|
"""Test icon setup in PyInstaller bundle."""
|
||||||
# Mock exists to return False for original path, True for bundle path
|
# Mock exists to return False for original path, True for bundle path
|
||||||
def mock_exists_side_effect(path):
|
def mock_exists_side_effect(path):
|
||||||
@@ -97,9 +100,12 @@ class TestUIManager:
|
|||||||
mock_photo_instance = Mock()
|
mock_photo_instance = Mock()
|
||||||
mock_photo.return_value = mock_photo_instance
|
mock_photo.return_value = mock_photo_instance
|
||||||
|
|
||||||
result = ui_manager.setup_icon("test_icon.png")
|
with patch.object(ui_manager.root, 'iconphoto') as mock_iconphoto, \
|
||||||
|
patch.object(ui_manager.root, 'wm_iconphoto') as mock_wm_iconphoto:
|
||||||
|
|
||||||
assert result is True
|
result = ui_manager.setup_application_icon("test_icon.png")
|
||||||
|
|
||||||
|
assert result is True
|
||||||
ui_manager.logger.info.assert_called_with("Found icon in PyInstaller bundle: /test/bundle/path/test_icon.png")
|
ui_manager.logger.info.assert_called_with("Found icon in PyInstaller bundle: /test/bundle/path/test_icon.png")
|
||||||
|
|
||||||
def test_create_graph_frame(self, ui_manager, root_window):
|
def test_create_graph_frame(self, ui_manager, root_window):
|
||||||
@@ -149,23 +155,25 @@ class TestUIManager:
|
|||||||
input_ui = ui_manager.create_input_frame(main_frame)
|
input_ui = ui_manager.create_input_frame(main_frame)
|
||||||
medicine_vars = input_ui["medicine_vars"]
|
medicine_vars = input_ui["medicine_vars"]
|
||||||
|
|
||||||
expected_medicines = ["bupropion", "hydroxyzine", "gabapentin", "propranolol"]
|
expected_medicines = ["bupropion", "hydroxyzine", "gabapentin", "propranolol", "quetiapine"]
|
||||||
for medicine in expected_medicines:
|
for medicine in expected_medicines:
|
||||||
assert medicine in medicine_vars
|
assert medicine in medicine_vars
|
||||||
assert isinstance(medicine_vars[medicine], list)
|
assert isinstance(medicine_vars[medicine], tuple)
|
||||||
assert len(medicine_vars[medicine]) == 2 # IntVar and Spinbox
|
assert len(medicine_vars[medicine]) == 2 # IntVar and display text
|
||||||
assert isinstance(medicine_vars[medicine][0], tk.IntVar)
|
assert isinstance(medicine_vars[medicine][0], tk.IntVar)
|
||||||
assert isinstance(medicine_vars[medicine][1], ttk.Spinbox)
|
assert isinstance(medicine_vars[medicine][1], str)
|
||||||
|
|
||||||
@patch('ui_manager.datetime')
|
@patch('src.ui_manager.datetime')
|
||||||
def test_create_input_frame_default_date(self, mock_datetime, ui_manager, root_window):
|
def test_create_input_frame_default_date(self, mock_datetime, ui_manager, root_window):
|
||||||
"""Test that default date is set to today."""
|
"""Test that default date is set to today."""
|
||||||
mock_datetime.now.return_value.strftime.return_value = "2024-01-15"
|
mock_datetime.now.return_value.strftime.return_value = "07/30/2025"
|
||||||
|
|
||||||
main_frame = ttk.Frame(root_window)
|
main_frame = ttk.Frame(root_window)
|
||||||
input_ui = ui_manager.create_input_frame(main_frame)
|
input_ui = ui_manager.create_input_frame(main_frame)
|
||||||
|
|
||||||
assert input_ui["date_var"].get() == "2024-01-15"
|
# The actual date will be today's date, not the mocked value
|
||||||
|
# because the datetime import is within the function
|
||||||
|
assert input_ui["date_var"].get() == "07/30/2025"
|
||||||
|
|
||||||
def test_create_table_frame(self, ui_manager, root_window):
|
def test_create_table_frame(self, ui_manager, root_window):
|
||||||
"""Test creation of table frame."""
|
"""Test creation of table frame."""
|
||||||
@@ -185,8 +193,8 @@ class TestUIManager:
|
|||||||
tree = table_ui["tree"]
|
tree = table_ui["tree"]
|
||||||
|
|
||||||
expected_columns = [
|
expected_columns = [
|
||||||
"date", "depression", "anxiety", "sleep", "appetite",
|
"Date", "Depression", "Anxiety", "Sleep", "Appetite",
|
||||||
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "note"
|
"Bupropion", "Hydroxyzine", "Gabapentin", "Propranolol", "Quetiapine", "Note"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Check that columns are configured
|
# Check that columns are configured
|
||||||
@@ -203,9 +211,9 @@ class TestUIManager:
|
|||||||
|
|
||||||
ui_manager.add_buttons(frame, buttons_config)
|
ui_manager.add_buttons(frame, buttons_config)
|
||||||
|
|
||||||
# Check that buttons were added (basic structure test)
|
# Check that a button frame was added
|
||||||
children = frame.winfo_children()
|
children = frame.winfo_children()
|
||||||
assert len(children) >= 2
|
assert len(children) >= 1 # At least the button frame should be added
|
||||||
|
|
||||||
def test_create_edit_window(self, ui_manager):
|
def test_create_edit_window(self, ui_manager):
|
||||||
"""Test creation of edit window."""
|
"""Test creation of edit window."""
|
||||||
@@ -248,27 +256,6 @@ class TestUIManager:
|
|||||||
assert edit_window is not None
|
assert edit_window is not None
|
||||||
# More detailed testing would require examining the internal widgets
|
# More detailed testing would require examining the internal widgets
|
||||||
|
|
||||||
def test_create_scale_with_var(self, ui_manager, root_window):
|
|
||||||
"""Test creation of scale widget with variable."""
|
|
||||||
frame = ttk.Frame(root_window)
|
|
||||||
var = tk.IntVar()
|
|
||||||
|
|
||||||
scale = ui_manager._create_scale_with_var(frame, var, "Test Label", 0, 0)
|
|
||||||
|
|
||||||
assert isinstance(scale, ttk.Scale)
|
|
||||||
|
|
||||||
def test_create_spinbox_with_var(self, ui_manager, root_window):
|
|
||||||
"""Test creation of spinbox widget with variable."""
|
|
||||||
frame = ttk.Frame(root_window)
|
|
||||||
var = tk.IntVar()
|
|
||||||
|
|
||||||
result = ui_manager._create_spinbox_with_var(frame, var, "Test Label", 0, 0)
|
|
||||||
|
|
||||||
assert isinstance(result, list)
|
|
||||||
assert len(result) == 2
|
|
||||||
assert isinstance(result[0], tk.IntVar)
|
|
||||||
assert isinstance(result[1], ttk.Spinbox)
|
|
||||||
|
|
||||||
def test_frame_positioning(self, ui_manager, root_window):
|
def test_frame_positioning(self, ui_manager, root_window):
|
||||||
"""Test that frames are positioned correctly."""
|
"""Test that frames are positioned correctly."""
|
||||||
main_frame = ttk.Frame(root_window)
|
main_frame = ttk.Frame(root_window)
|
||||||
@@ -293,15 +280,15 @@ class TestUIManager:
|
|||||||
assert var.get() == 0
|
assert var.get() == 0
|
||||||
|
|
||||||
for medicine_data in input_ui["medicine_vars"].values():
|
for medicine_data in input_ui["medicine_vars"].values():
|
||||||
assert medicine_data[0].get() == 0
|
assert medicine_data[0].get() == 0 # IntVar should be 0
|
||||||
|
|
||||||
@patch('tkinter.messagebox.showerror')
|
@patch('tkinter.messagebox.showerror')
|
||||||
def test_error_handling_in_setup_icon(self, mock_showerror, ui_manager):
|
def test_error_handling_in_setup_application_icon(self, mock_showerror, ui_manager):
|
||||||
"""Test error handling in setup_icon method."""
|
"""Test error handling in setup_application_icon method."""
|
||||||
with patch('PIL.Image.open') as mock_open:
|
with patch('PIL.Image.open') as mock_open:
|
||||||
mock_open.side_effect = Exception("Image error")
|
mock_open.side_effect = Exception("Image error")
|
||||||
|
|
||||||
result = ui_manager.setup_icon("test.png")
|
result = ui_manager.setup_application_icon("test.png")
|
||||||
|
|
||||||
assert result is False
|
assert result is False
|
||||||
ui_manager.logger.error.assert_called()
|
ui_manager.logger.error.assert_called()
|
||||||
|
|||||||
Reference in New Issue
Block a user