From 890df2f4bcda7470cd488143397b956ad2053457 Mon Sep 17 00:00:00 2001 From: Dries Peeters Date: Fri, 31 Oct 2025 13:22:24 +0100 Subject: [PATCH] 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 --- app/routes/api.py | 27 -------- app/templates/timer/manual_entry.html | 58 ++++++++++-------- docs/bugfixes/template_application_fix.md | 43 +++++++++++++ docs/features/TIME_ENTRY_DUPLICATION.md | 6 ++ ...oice_expenses.cpython-312-pytest-7.4.3.pyc | Bin 25949 -> 25949 bytes ...uts_input_fix.cpython-312-pytest-7.4.3.pyc | Bin 38173 -> 38173 bytes ...y_duplication.cpython-312-pytest-7.4.3.pyc | Bin 56372 -> 60459 bytes ...try_templates.cpython-312-pytest-7.4.3.pyc | Bin 83968 -> 97915 bytes tests/test_time_entry_duplication.py | 22 +++++++ 9 files changed, 102 insertions(+), 54 deletions(-) create mode 100644 docs/bugfixes/template_application_fix.md 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 1f3192d7fcfede5534659b1ebba1c11e2bd710ee..2865d46a761929b126589246f8c1aa1ed8546250 100644 GIT binary patch delta 20 acmcb6it+9#M$Xf`yj%=GaC;+XNGbqOQwEm+ delta 20 acmcb6it+9#M$Xf`yj%=GFkvHSNGbqM+y+bl 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 3a81abef053c783407f9b6b24f10a31d8428da11..b50bb1bcc3b762b850c27bb36d16bd5312c5e6d0 100644 GIT binary patch delta 20 acmbQcifQgDCeG8myj%=GaC;-C$W#DAgayd} delta 20 acmbQcifQgDCeG8myj%=GkhqakWGVnaA_Y$X 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 45c97d9fd131e93639d45e90557db6c6d469339c..a216c2c998526c3f45a716b553ca7fdbb20c1867 100644 GIT binary patch delta 2303 zcmc&#drXs86z^?I`?UoGi<9y!k4&nlIHmysS<(5Ls7t^p=$hK^11iwg`+b1LlKQ|I zGGBOfn+`VJ%P{utZC!W)`h`9RTXOaoA0f+O-`IBjI_AW)E?$MxiAMOcG*sT0@v9bwH5uX6tXQ%F_GeMD z21aHVL@Z~1R6r<4SPQ;vf5g$|8tticZ~#;wFSS}!;cz!? zYPu6XEh%FMNH5J|2M|hU8}>58N37qtjvGjjzboanl)?&wS#qRGK4KXM2Q)S4qevxzJmv)p*ep(;FY-M{y;>LWE#uSD~~Tx>w{48{ow8Vgv`m z3sBgUYvAo-#bURe^0Y_xtZddAE~D)ZbLxYc(GiaqWSXk~hKb39g|`61E4Md&Ka}i* z?Ja%eTiD#%POiw4Uz4MW7y9Oh!}um?eg{!_U_#Y*}!8~NFeC?=5Z3tk(~ zCt+&vA7AuPY;oXjihxm_;gJ8CX2>s$ZQWqB^`{m-6m9;Hn~ZAPeN<(g{py(G%upXO z!)13!&RW4dm%5h-4#}dVzk&a38aV+yb;FhPA{;)O&TgZ9Su|+6oJ{g`tydBv_jj!# zaAe_Q%77ls(&Kv;X_YFD4}+hc%-l2ST_uTum3byzY^$mv)EDa0!=YzJ=)!%5GQ!9C zLScn%6ucOthlaG7aBYGPzL+pNGJ=n9QPyh|iUuQ}z$f~Q5P5QjA+*83n;(rMo^g~b z8bbJ_27}Mw)7L5*G(HXE)QB9PJh0<Pvv642Mlexrud7 zslseGOLno+?4g1v&{{zp7>Ys_i=5N#GV_AyaY}+&5~@8_b|yI7yZ{|XGsf4)I@~T% zGCRd7HJ&P`!!9{p<>oxIL~Dec`l!Lw17)@BuJ0f3*kEO~1r5PU9IRG5aQIKjt2a8! z%xOvYtYD!B&}SLK76z6m*lEoi6TI(tm3=f1TA0!BHv71fpU#p5QL^!WpGmM`m~0p% zo69ZP+_eIwPM#NBw#8oC?Wou=DpbWTWhd+6R3+AJ|8u09oA>`U|2=D;;pX*e6uR7Q zF6U9&87>EDMt5`R|J6)`dvtN>(4Vm>`3yI!FHxb7?bgMd*2UZsLs<%%i?zi`s;`nl zibrZK<6!kUC;1b8JXh++r=A9%>sFNTg$zD(6yGO`?+rc1Ai#-&qlP*qFTH@eOQ;JG ztKF3X?Lh_B+kmE;Wvfc0S5VW7fJKjDiK19sD8cH2*=$qL(Wo$uVi@3Z*~FSEVG=bm q8*2)&QNGwcN6lSRr;sPE%_4C4e4Htg6p|Y%g)$1kbX|?(AkAOKl*&&4 delta 1341 zcmb7^VN6q36vsK`wbXV+Vlk<1Y#=VuIHh1rK(l16A4ax_(Xb8IW(jK_f=qGuX{1QR zqAoGjDI8@p2GPxh6l9{8gk^qE7mXo{h>emVEt zd;aI#b8lYP)zsc!QqzBTxg06#X>A#+`fN#ax`)b-&sjM|i(0%{^8S?G*;FrooZmoH z&)L2nipxvB9D9CLMTmM}6VRZLp!&>r^VUTRd|o5FdTq987kZ6^MKp3wv7l|dw>Xet z2I&Y|1$@JP-*g@;UTyszL+%RflM{=&Y${!se};1xoMk|i58rZozlFIQ*aLhHIDuxM z1z-iQo8P&;pkQ(?OjiMWfqg(L@STF;R^STZ=3_A%PXse~X{l2)YQzEA5%3f3Z<#hj zIzHN+%FQ3fRCVr>ux*$q&Lh1S|#)13xGjE(I=F z&+BrX1saoF} zEK)UI43^Rf`F1F7SA)(-9+_QrKG#(qjjVhFeN1ZVD2qiCf2uE}t-Mn&dim&vbieIs zEBVdJjgB)a>^Jop4crshBi~z<>o8j+D)@fgEZ+F_X*$J4B0^VWo9MREdHHc&4be5( z7hRg>tW%Ny9kplXW{kH0A;3K47_27t$KIcg-ir$0OCStv=PkEqalAi+&&4w7hD^qq z(=x7OW|H{qSBqoy))&R@?Z~8?a(su?P3^3;J)&FuPg@TS%kG2HLv>uyw}JZQpM8%l zbe9Kzg)1kJMT4An@(X$(D^HHty+Vq)-j+Q=mCy%ZU0-meIR?9x_YC^ z(@4rIG1ZH^@z>ea+G~k@F!iAd-Z@#trSb5E&}%87tlX)8E4BSEstpcIXmpMTFMH@Q z-@BYQ^E}Ee1Qq}*0CVqGIcLDbONKJ(5tk3e6q7@{EaNG%`i~~EczqlgsirYLIudM* zBgp(G9DsEM=m0u_6TlgTq}|ZN8$|W`sJH^(HTbN?n%Wv&+<@aI9JV#J5&bJe48!vP tKx+j$Bd{Scu8>F(^I*?aNOJs+!;!A`sooT0xop3)OmmECxm0K_dIIwCki!4~ 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 424dc0ae4e79a5218e239ffe15d53d9e4855fc1b..9affd1b755993940f8ca70b412900001d305b6a1 100644 GIT binary patch delta 13133 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 F{|Bm=`?UZ7 delta 6958 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