mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-12 07:19:49 -05:00
Fix: Preserve task selection when duplicating time entries
Fix: Preserve task selection when duplicating time entriesWhen duplicating a time entry with an assigned task, the task was notbeing pre-selected in the duplicate form. This was caused by thetemplate application code interfering with the duplication logic.The template code would run after duplication data was set, overwritingthe `data-selected-task-id` attribute and clearing the task selectioneven when no template was being applied.Changes:- Added isDuplicating flag check in manual_entry.html to prevent template application code from running during duplication- Template functionality continues to work normally for non-duplicate manual entries- Added comprehensive test to verify task pre-selection is preserved- Updated documentation with fix notes and changelog entryImpact:- Users can now duplicate time entries with tasks and the task will be correctly pre-selected, saving time and improving UX- No breaking changes - all existing tests pass (54/54)- Clean separation between duplication and template featuresTests:- test_duplicate_with_task_not_overridden_by_template_code (new)- All 22 duplication tests passing- All 32 template tests passing
This commit is contained in:
@@ -1437,33 +1437,6 @@ def get_activity_stats():
|
||||
'period_days': days
|
||||
})
|
||||
|
||||
@api_bp.route('/api/templates/<int:template_id>')
|
||||
@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/<int:template_id>/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():
|
||||
|
||||
@@ -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(_) {}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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/<int:template_id>` (GET)
|
||||
- `/api/templates/<int:template_id>/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/<int:template_id>` 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
|
||||
|
||||
@@ -215,6 +215,7 @@ POST /api/timer/duplicate/<id>
|
||||
**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/<id>
|
||||
|
||||
## 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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user