diff --git a/app/routes/api.py b/app/routes/api.py index 61aef94..3822b2c 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -1437,33 +1437,6 @@ def get_activity_stats(): 'period_days': days }) -@api_bp.route('/api/templates/') -@login_required -def get_template(template_id): - """Get a time entry template by ID""" - template = TimeEntryTemplate.query.get_or_404(template_id) - - # Check permissions - if template.user_id != current_user.id: - return jsonify({'error': 'Access denied'}), 403 - - return jsonify(template.to_dict()) - -@api_bp.route('/api/templates//use', methods=['POST']) -@login_required -def mark_template_used(template_id): - """Mark a template as used (updates last_used_at)""" - template = TimeEntryTemplate.query.get_or_404(template_id) - - # Check permissions - if template.user_id != current_user.id: - return jsonify({'error': 'Access denied'}), 403 - - template.last_used_at = datetime.utcnow() - db.session.commit() - - return jsonify({'success': True}) - # WebSocket event handlers @socketio.on('connect') def handle_connect(): diff --git a/app/templates/timer/manual_entry.html b/app/templates/timer/manual_entry.html index 50d860c..a8e6c82 100644 --- a/app/templates/timer/manual_entry.html +++ b/app/templates/timer/manual_entry.html @@ -156,32 +156,35 @@ document.addEventListener('DOMContentLoaded', async function(){ } // Apply Time Entry Template if provided via sessionStorage or query param - try { - let tpl = null; - const raw = sessionStorage.getItem('activeTemplate'); - if (raw) { - try { tpl = JSON.parse(raw); } catch(_) { tpl = null; } - } - if (!tpl) { - const params = new URLSearchParams(window.location.search); - const tplId = params.get('template'); - if (tplId) { - try { - const resp = await fetch(`/api/templates/${tplId}`); - if (resp.ok) tpl = await resp.json(); - } catch(_) {} + // Skip template application when duplicating an entry to preserve the original entry's task + const isDuplicating = {{ 'true' if is_duplicate else 'false' }}; + if (!isDuplicating) { + try { + let tpl = null; + const raw = sessionStorage.getItem('activeTemplate'); + if (raw) { + try { tpl = JSON.parse(raw); } catch(_) { tpl = null; } } - } - if (tpl && typeof tpl === 'object') { - // Preselect project and task - if (tpl.project_id && projectSelect) { - projectSelect.value = String(tpl.project_id); - // Preselect task after load - if (taskSelect) { - taskSelect.setAttribute('data-selected-task-id', tpl.task_id ? String(tpl.task_id) : ''); + if (!tpl) { + const params = new URLSearchParams(window.location.search); + const tplId = params.get('template'); + if (tplId) { + try { + const resp = await fetch(`/api/templates/${tplId}`); + if (resp.ok) tpl = await resp.json(); + } catch(_) {} } - await loadTasks(projectSelect.value); } + if (tpl && typeof tpl === 'object') { + // Preselect project and task + if (tpl.project_id && projectSelect) { + projectSelect.value = String(tpl.project_id); + // Preselect task after load + if (taskSelect) { + taskSelect.setAttribute('data-selected-task-id', tpl.task_id ? String(tpl.task_id) : ''); + } + await loadTasks(projectSelect.value); + } // Notes, tags, billable const notes = document.getElementById('notes'); @@ -217,10 +220,11 @@ document.addEventListener('DOMContentLoaded', async function(){ } } - // Clear after applying so it does not persist - try { sessionStorage.removeItem('activeTemplate'); } catch(_) {} - } - } catch(_) {} + // Clear after applying so it does not persist + try { sessionStorage.removeItem('activeTemplate'); } catch(_) {} + } + } catch(_) {} + } }); {% endblock %} diff --git a/docs/bugfixes/template_application_fix.md b/docs/bugfixes/template_application_fix.md new file mode 100644 index 0000000..d36a129 --- /dev/null +++ b/docs/bugfixes/template_application_fix.md @@ -0,0 +1,43 @@ +# Bug Fix: Template Application Error + +## Issue +When users tried to select and apply a template from the start timer interface, they received an error message stating "can't apply the template". + +## Root Cause +There were duplicate route definitions for the template API endpoints: + +1. **In `app/routes/api.py` (lines 1440-1465)** - Registered first in the application + - `/api/templates/` (GET) + - `/api/templates//use` (POST) + - **Problem**: Missing `TimeEntryTemplate` import, causing `NameError` when routes were accessed + +2. **In `app/routes/time_entry_templates.py` (lines 301-326)** - Registered later + - Same routes with proper implementation + - Had correct imports and error handling + - Never executed due to duplicate route conflict + +Since the `api_bp` blueprint was registered before `time_entry_templates_bp` in `app/__init__.py`, Flask used the broken routes from `api.py`, causing the error. + +## Solution +Removed the duplicate route definitions from `app/routes/api.py` (lines 1440-1465), allowing the proper implementation in `app/routes/time_entry_templates.py` to be used. + +### Code Changes +**File**: `app/routes/api.py` +- **Removed**: Lines 1440-1465 containing duplicate `/api/templates/` routes +- **Reason**: Eliminate route conflict and use proper implementation + +## Testing +All existing tests pass: +- ✅ `test_get_templates_api` - Get all templates +- ✅ `test_get_single_template_api` - Get specific template +- ✅ `test_use_template_api` - Mark template as used +- ✅ `test_start_timer_from_template` - Start timer from template + +## Impact +- **Users can now successfully apply templates when starting timers** +- Template usage tracking works correctly +- No other functionality affected + +## Date Fixed +October 31, 2025 + diff --git a/docs/features/TIME_ENTRY_DUPLICATION.md b/docs/features/TIME_ENTRY_DUPLICATION.md index e75bf14..519c5ea 100644 --- a/docs/features/TIME_ENTRY_DUPLICATION.md +++ b/docs/features/TIME_ENTRY_DUPLICATION.md @@ -215,6 +215,7 @@ POST /api/timer/duplicate/ **Issue**: Task not pre-selected after duplication - **Cause**: Tasks are loaded dynamically via JavaScript - **Solution**: Wait for the page to fully load; the task should auto-select +- **Note**: This issue has been resolved in the latest version - template code no longer interferes with task pre-selection during duplication **Issue**: Cannot duplicate inactive project entry - **Cause**: Project status changed to inactive after entry creation @@ -226,6 +227,11 @@ POST /api/timer/duplicate/ ## Changelog +### Version 1.1 (2025-10-31) +- **Bug Fix**: Fixed issue where duplicated time entries with assigned tasks would not have the task pre-selected +- **Technical**: Template application code now properly checks for duplication mode and doesn't interfere with pre-filled task data +- **Testing**: Added comprehensive test to ensure task pre-selection is preserved during duplication + ### Version 1.0 (2024-10-23) - Initial implementation of time entry duplication - Duplicate buttons on dashboard and edit pages diff --git a/tests/__pycache__/test_invoice_expenses.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_invoice_expenses.cpython-312-pytest-7.4.3.pyc index 1f3192d..2865d46 100644 Binary files a/tests/__pycache__/test_invoice_expenses.cpython-312-pytest-7.4.3.pyc and b/tests/__pycache__/test_invoice_expenses.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_keyboard_shortcuts_input_fix.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_keyboard_shortcuts_input_fix.cpython-312-pytest-7.4.3.pyc index 3a81abe..b50bb1b 100644 Binary files a/tests/__pycache__/test_keyboard_shortcuts_input_fix.cpython-312-pytest-7.4.3.pyc and b/tests/__pycache__/test_keyboard_shortcuts_input_fix.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_time_entry_duplication.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_time_entry_duplication.cpython-312-pytest-7.4.3.pyc index 45c97d9..a216c2c 100644 Binary files a/tests/__pycache__/test_time_entry_duplication.cpython-312-pytest-7.4.3.pyc and b/tests/__pycache__/test_time_entry_duplication.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_time_entry_templates.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_time_entry_templates.cpython-312-pytest-7.4.3.pyc index 424dc0a..9affd1b 100644 Binary files a/tests/__pycache__/test_time_entry_templates.cpython-312-pytest-7.4.3.pyc and b/tests/__pycache__/test_time_entry_templates.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/test_time_entry_duplication.py b/tests/test_time_entry_duplication.py index 9c24106..91e7329 100644 --- a/tests/test_time_entry_duplication.py +++ b/tests/test_time_entry_duplication.py @@ -458,3 +458,25 @@ def test_duplicate_entry_from_inactive_project(app, user, authenticated_client): # Both acceptable since the route exists and handles the request assert response.status_code in [200, 302] + +@pytest.mark.integration +@pytest.mark.routes +def test_duplicate_with_task_not_overridden_by_template_code(authenticated_client, time_entry_with_all_fields, task, app): + """Test that duplicating an entry with a task preserves task selection despite template code.""" + with app.app_context(): + response = authenticated_client.get(f'/timer/duplicate/{time_entry_with_all_fields.id}') + assert response.status_code == 200 + html = response.get_data(as_text=True) + + # Verify the duplicate flag is set to true in JavaScript + assert 'const isDuplicating = true;' in html or 'isDuplicating = true' in html + + # Verify the task ID is set in the data attribute + assert f'data-selected-task-id="{task.id}"' in html + + # Verify template code is wrapped in isDuplicating check + assert 'if (!isDuplicating)' in html + + # Verify the is_duplicate flag is set + assert 'Duplicating entry' in html or 'Duplicate Time Entry' in html +