From 9be5f700d770b0ee2a97c6544a8f4270488a7ba0 Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Sat, 12 Oct 2024 16:27:15 +0200 Subject: [PATCH 01/29] switch from plain jquery to htmx to archive mission --- media/js/htmx-2.0.3.min.js | 1 + staffing/views.py | 31 +++++++------------ templates/core/pydici.html | 2 ++ templates/staffing/_mission_table.html | 10 ------ .../_mission_table_archive_column.html | 13 ++++---- templates/staffing/mission.html | 13 ++------ 6 files changed, 22 insertions(+), 48 deletions(-) create mode 100644 media/js/htmx-2.0.3.min.js diff --git a/media/js/htmx-2.0.3.min.js b/media/js/htmx-2.0.3.min.js new file mode 100644 index 00000000..423cf011 --- /dev/null +++ b/media/js/htmx-2.0.3.min.js @@ -0,0 +1 @@ +var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.3"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=de;Q.ajax=Rn;Q.find=r;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=h;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:dn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:i,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:dt,triggerEvent:de,triggerErrorEvent:fe,withExtensions:Ft};const o=["get","post","put","delete","patch"];const R=o.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function h(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function i(e,t){while(e&&!t(e)){e=c(e)}return e||null}function H(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;i(t,function(e){return!!(r=H(t,ue(e),n))});if(r!=="unset"){return r}}function d(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function N(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function A(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(A(e)){const t=N(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){C(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function D(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e=0}function le(e){const t=e.getRootNode&&e.getRootNode();if(t&&t instanceof window.ShadowRoot){return ne().body.contains(t.host)}else{return ne().body.contains(e)}}function F(e){return e.trim().split(/\s+/)}function ce(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){C(e);return null}}function B(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function U(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return vn(ne().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function r(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return r(ne(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(ne(),e)}}function E(){return window}function z(e,t){e=y(e);if(t){E().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function f(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ue(y(e));if(!e){return}if(n){E().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ue(y(e));if(!r){return}if(n){E().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function Z(e,t){e=y(e);se(e.parentElement.children,function(e){G(e,t)});K(ue(e),t)}function g(e,t){e=ue(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||d(e,t)){return e}}while(e=e&&ue(c(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function ge(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function p(e,t,n){e=y(e);if(t.indexOf("closest ")===0){return[g(ue(e),ge(t.substr(8)))]}else if(t.indexOf("find ")===0){return[r(f(e),ge(t.substr(5)))]}else if(t==="next"){return[ue(e).nextElementSibling]}else if(t.indexOf("next ")===0){return[pe(e,ge(t.substr(5)),!!n)]}else if(t==="previous"){return[ue(e).previousElementSibling]}else if(t.indexOf("previous ")===0){return[me(e,ge(t.substr(9)),!!n)]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else if(t==="root"){return[m(e,!!n)]}else if(t==="host"){return[e.getRootNode().host]}else if(t.indexOf("global ")===0){return p(e,t.slice(7),true)}else{return M(f(m(e,!!n)).querySelectorAll(ge(t)))}}var pe=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return p(e,t)[0]}else{return p(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return r(f(t)||document,e)}else{return e}}function xe(e,t,n,r){if(k(t)){return{target:ne().body,event:J(e),listener:t,options:n}}else{return{target:y(e),event:J(t),listener:n,options:r}}}function ye(t,n,r,o){Vn(function(){const e=xe(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function be(t,n,r){Vn(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const ve=ne().createElement("output");function we(e,t){const n=re(e,t);if(n){if(n==="this"){return[Se(e,t)]}else{const r=p(e,n);if(r.length===0){C('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ue(i(e,function(e){return te(ue(e),t)!=null}))}function Ee(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Ce(t){const n=Q.config.attributesToSettle;for(let e=0;e0){s=e.substr(0,e.indexOf(":"));n=e.substr(e.indexOf(":")+1,e.length)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=p(t,n,false);if(r){se(r,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=f(n)}const r={shouldSwap:true,target:e,fragment:t};if(!de(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}se(i.elts,function(e){de(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=r("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=r("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){se(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){if(e.moveBefore){let e=r("#--htmx-preserve-pantry--");if(e==null){ne().body.insertAdjacentHTML("afterend","
");e=r("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Le(l,e,c){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=f(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Ne(e){return function(){G(e,Q.config.addedClass);kt(ue(e));Ae(f(e));de(e,"htmx:load")}}function Ae(e){const t="[autofocus]";const n=$(d(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function u(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ne(o))}}}function Ie(e,t){let n=0;while(n0}function $e(e,t,r,o){if(!o){o={}}e=y(e);const i=o.contextElement?m(o.contextElement,false):ne();const n=document.activeElement;let s={};try{s={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const l=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=P(t);l.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t0){E().setTimeout(c,r.settleDelay)}else{c()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(D(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}de(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function w(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=w(e,Qe).trim();e.shift()}else{t=w(e,b)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{w(o,v);const l=o.length;const c=w(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};w(o,v);u.pollInterval=h(w(o,/[,\[\s]/));w(o,v);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const a={trigger:c};var i=nt(e,o,"event");if(i){a.eventFilter=i}w(o,v);while(o.length>0&&o[0]!==","){const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=h(w(o,b))}else if(f==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=w(o,b);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const d=rt(o);if(d.length>0){s+=" "+d}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=rt(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=h(w(o,b))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=w(o,b)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=rt(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=w(o,b)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}w(o,v)}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}w(o,v)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(d(e,"form")){return[{trigger:"submit"}]}else if(d(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(d(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!gt(n,e,Mt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function at(e){return g(e,Q.config.disableSelector)}function ft(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){pt(t,function(e,t){const n=ue(e);if(at(n)){a(n);return}he(r,o,n,t)},n,e,true)})}}function dt(e,t){const n=ue(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(d(n,'input[type="submit"], button')&&g(n,"form")!==null){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ht(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function gt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function pt(l,c,e,u,a){const f=ie(l);let t;if(u.from){t=p(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in f)){f.lastValue=new WeakMap}t.forEach(function(e){if(!f.lastValue.has(u)){f.lastValue.set(u,new WeakMap)}f.lastValue.get(u).set(e,e.value)})}se(t,function(i){const s=function(e){if(!le(l)){i.removeEventListener(u.trigger,s);return}if(ht(l,e)){return}if(a||dt(e,l)){e.preventDefault()}if(gt(u,l,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!d(ue(e.target),u.target)){return}}if(u.once){if(f.triggeredOnce){return}else{f.triggeredOnce=true}}if(u.changed){const n=event.target;const r=n.value;const o=f.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(f.delayed){clearTimeout(f.delayed)}if(f.throttle){return}if(u.throttle>0){if(!f.throttle){de(l,"htmx:trigger");c(l,e);f.throttle=E().setTimeout(function(){f.throttle=null},u.throttle)}}else if(u.delay>0){f.delayed=E().setTimeout(function(){de(l,"htmx:trigger");c(l,e)},u.delay)}else{de(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let xt=null;function yt(){if(!xt){xt=function(){mt=true};window.addEventListener("scroll",xt);window.addEventListener("resize",xt);setInterval(function(){if(mt){mt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&X(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){de(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){de(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;se(o,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(g(n,Q.config.disableSelector)){a(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){yt();pt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ue(r),n,e)}else{pt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Tt(e){const t=g(ue(e.target),"button, input[type='submit']");const n=Lt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){const t=g(ue(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function Nt(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function At(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(at(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function It(t){ke(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Vt(t){if(!B()){return null}t=U(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){de(ne().body,"htmx:historyCacheMissLoad",i);const e=P(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=Ut();const r=xn(n);kn(e.title);qe(e);Ve(n,t,r);Te();Kt(r.tasks);Bt=o;de(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Wt(e){zt();e=e||location.pathname+location.search;const t=Vt(e);if(t){const n=P(t.content);const r=Ut();const o=xn(r);kn(t.title);qe(n);Ve(r,n,o);Te();Kt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Bt=e;de(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Gt(e)}}}function Zt(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Yt(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function Qt(e,t){se(e.concat(t),function(e){const t=ie(e);t.requestCount=(t.requestCount||1)-1});se(e,function(e){const t=ie(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function en(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function on(t,n,r,o,i){if(o==null||en(t,o)){return}else{t.push(o)}if(tn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=M(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=M(o.files)}nn(s,e,n);if(i){sn(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){rn(e.name,e.value,n)}else{t.push(e)}if(i){sn(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}nn(t,e,n)})}}function sn(e,t){const n=e;if(n.willValidate){de(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});de(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function ln(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){on(n,o,i,g(e,"form"),l)}on(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const a=ee(u,"name");nn(a,u.value,o)}const c=we(e,"hx-include");se(c,function(e){on(n,r,i,ue(e),l);if(!d(e,"form")){se(f(e).querySelectorAll(ot),function(e){on(n,r,i,e,l)})}});ln(r,o);return{errors:i,formData:r,values:Nn(r)}}function un(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function an(e){e=qn(e);let n="";e.forEach(function(e,t){n=un(n,t,e)});return n}function fn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};bn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function dn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.substr(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function hn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function gn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!hn(e)){r.show="top"}if(n){const s=F(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.substr(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const d=l.substr("focus-scroll:".length);r.focusScroll=d=="true"}else if(e==0){r.swapStyle=l}else{C("Unknown modifier in hx-swap: "+l)}}}}return r}function pn(e){return re(e,"hx-encoding")==="multipart/form-data"||d(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function mn(t,n,r){let o=null;Ft(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(pn(n)){return ln(new FormData,qn(r))}else{return an(r)}}}function xn(e){return{tasks:[],elts:[e]}}function yn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function bn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.substr(11);t=true}else if(e.indexOf("js:")===0){e=e.substr(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return bn(ue(c(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function wn(e,t){return bn(e,"hx-vars",true,t)}function Sn(e,t){return bn(e,"hx-vals",false,t)}function En(e){return ce(wn(e),Sn(e))}function Cn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function On(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Rn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:y(r)||ve,returnPromise:true})}else{let e=y(r.target);if(r.target&&!e||!e&&!y(r.source)){e=ve}return he(t,n,y(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(t,n,null,null,{returnPromise:true})}}function Hn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Tn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return de(e,"htmx:validateUrl",ce({url:o,sameHost:r},n))}function qn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Ln(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function Nn(r){return new Proxy(r,{get:function(e,t){if(typeof t==="symbol"){return Reflect.get(e,t)}if(t==="toJSON"){return()=>Object.fromEntries(r)}if(t in e){if(typeof e[t]==="function"){return function(){return r[t].apply(r,arguments)}}else{return e[t]}}const n=r.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Ln(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Dn;const X=i.select||null;if(!le(r)){oe(s);return e}const c=i.targetOverride||ue(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let u=ie(r);const a=u.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const N=ee(a,"formmethod");if(N!=null){if(N.toLowerCase()!=="dialog"){t=N}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(de(r,"htmx:confirm",G)===false){oe(s);return e}}let d=r;let h=re(r,"hx-sync");let g=null;let F=false;if(h){const A=h.split(":");const I=A[0].trim();if(I==="this"){d=Se(r,"hx-sync")}else{d=ue(ae(r,I))}h=(A[1]||"drop").trim();u=ie(d);if(h==="drop"&&u.xhr&&u.abortable!==true){oe(s);return e}else if(h==="abort"){if(u.xhr){oe(s);return e}else{F=true}}else if(h==="replace"){de(d,"htmx:abort")}else if(h.indexOf("queue")===0){const W=h.split(" ");g=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){de(d,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="all"){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){he(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;u.xhr=p;u.abortable=F;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var x=prompt(B);if(x===null||!de(r,"htmx:prompt",{prompt:x,target:c})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let y=fn(r,c,x);if(t!=="get"&&!pn(r)){y["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){y=ce(y,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){ln(j,qn(i.values))}const V=qn(En(r));const v=ln(j,V);let w=dn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=bn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:Nn(w),unfilteredFormData:v,unfilteredParameters:Nn(v),headers:y,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!de(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;y=C.headers;w=qn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){de(r,"htmx:validation:halted",C);oe(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=an(w);if(O){R+="#"+O}}}if(!Tn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in y){if(y.hasOwnProperty(k)){const Y=y[k];Cn(p,k,Y)}}}const H={xhr:p,target:c,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Hn(r);H.pathInfo.responsePath=On(p);M(r,H);if(H.keepIndicators!==true){Qt(T,q)}de(r,"htmx:afterRequest",H);de(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){de(e,"htmx:afterRequest",H);de(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ce({error:e},H));throw e}};p.onerror=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!de(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Zt(r);var q=Yt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){de(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});de(r,"htmx:beforeSend",H);const J=E?null:mn(p,r,w);p.send(J);return e}function An(e,t){const n=t.xhr;let r=null;let o=null;if(O(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(O(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(O(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const c=re(e,"hx-replace-url");const u=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(c){a="replace";f=c}else if(u){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function In(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Pn(e){for(var t=0;t0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend"," ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){de(e,"htmx:restored",{document:ne(),triggerEvent:de})})}else{if(n){n(e)}}};E().setTimeout(function(){de(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/staffing/views.py b/staffing/views.py index 5f8f69e0..bd76eb0f 100644 --- a/staffing/views.py +++ b/staffing/views.py @@ -123,27 +123,19 @@ def check_user_timesheet_access(user, consultant, timesheet_month): @pydici_non_public -def missions(request, only_active=True, consultant_id=None): +def missions(request, only_active=True): """List of missions""" - if consultant_id: - consultant = Consultant.objects.get(id=consultant_id) - if only_active: - data_url = reverse('staffing:consultant_active_mission_table_DT', args=(consultant_id,)) - else: - data_url = reverse('staffing:consultant_all_mission_table_DT', args=(consultant_id,)) + if only_active: + data_url = reverse('staffing:active_mission_table_DT') else: - consultant = None - if only_active: - data_url = reverse('staffing:active_mission_table_DT') - else: - data_url = reverse('staffing:all_mission_table_DT') + data_url = reverse('staffing:all_mission_table_DT') return render(request, "staffing/missions.html", {"all": not only_active, - "consultant": consultant, "data_url": data_url, "datatable_options": ''' "columnDefs": [{ "orderable": false, "targets": [4, 8, 9] }, { className: "hidden-xs hidden-sm hidden-md", "targets": [6,7,8,9]}], - "order": [[0, "asc"]] ''', + "order": [[0, "asc"]], + "fnDrawCallback": function( oSettings ) {htmx.process(document.body); }''', "user": request.user}) @@ -284,7 +276,8 @@ def consultant_missions(request, only_active=True, consultant_id=None): "data_url": data_url, "datatable_options": ''' "columnDefs": [{ "orderable": false, "targets": [4, 8, 9] }, { className: "hidden-xs hidden-sm hidden-md", "targets": [6,7,8,9]}], - "order": [[3, "asc"]] + "order": [[3, "asc"]], + "fnDrawCallback": function( oSettings ) {htmx.process(document.body); } ''', "user": request.user}) @@ -866,16 +859,14 @@ def fixed_price_missions_report(request): @pydici_non_public def deactivate_mission(request, mission_id): - """Deactivate the given mission""" + """Deactivate the given mission. Fragment for htmx call""" try: - error = False mission = Mission.objects.get(id=mission_id) mission.active = False mission.save() + return HttpResponse(_("mission archived")) except Mission.DoesNotExist: - error = True - return HttpResponse(json.dumps({"error": error, "id": mission_id}), - content_type="application/json") + return HttpResponse(_("mission not found")) @cache_control(no_store=True) diff --git a/templates/core/pydici.html b/templates/core/pydici.html index 5b3e3bb2..078b7f45 100644 --- a/templates/core/pydici.html +++ b/templates/core/pydici.html @@ -40,6 +40,8 @@ + + diff --git a/templates/staffing/_mission_table.html b/templates/staffing/_mission_table.html index 0d761b26..4f92d953 100644 --- a/templates/staffing/_mission_table.html +++ b/templates/staffing/_mission_table.html @@ -36,16 +36,6 @@ From 80963dfc2db7c14c6983e0f75530b758f66fc342 Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Sat, 12 Oct 2024 18:02:54 +0200 Subject: [PATCH 02/29] translation update --- locale/fr/LC_MESSAGES/django.mo | Bin 84783 -> 84833 bytes locale/fr/LC_MESSAGES/django.po | 274 ++++++++++++++++---------------- 2 files changed, 140 insertions(+), 134 deletions(-) diff --git a/locale/fr/LC_MESSAGES/django.mo b/locale/fr/LC_MESSAGES/django.mo index 618f0843d1a6dec40bab752525a4e25858035edf..39be18e17d8bb37c9c70d2e353212ba5a56a343b 100644 GIT binary patch delta 25000 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 delta 24956 zcmZA92Y65Chtly>HKz>p9nRd)<4VlhEJ)xjD;s{VZSiT40iy4$s&mjuVP8VUFXM)NyK5 zQLf{3ZtXb5u?t4y5-ft}@B^l3<2ZNFudU-;#D~}f543Zfx|p@SFtI4OQ_MWTwsyER1P7n-#@l8Pa_*2X4d|JcsHpc^Ah? zh50Zkmc(QjXVcX%knx>5L;|oqX2AZK1}9;5oQImgZp?;{Py_mRHRS~{HR-w-gl$kO z9gIEjYvlhq@A)GocI;;A^+i`Rok&Ct)?!B7XT6Nt!hca6JKarrMpXR*s1?Oya;%Ao zu@S0%Gt@%*qUw*e={cB^^y=>He;|>qWN2%Tpa%Sa>M#Y<*UqHJR2YWpC>k?iB~<&? z=#TwS6B>=WJCm%dQAcnD{qZE~4qffR{%gyhkRg408bhqns0mcY6xay;uq~>iu2>QW zqgK4xmLEe+^bD%rJ=B0NQ490yWp*a5OGLLi2j;?}sEIU34b&I4!l4+9U!zvG!ln<} z@@uHq_ZezuQuj9LNDLue5%XaiOoG!eGrBWuWIbwT$1o1hp(Ym8$DCCZYC=U(`DHLQ z)Mh5#_zSB2?-+n*jjnT>h$=qEH0amYoOLk9lFo(Nfp)0dKM+-a z7HY*mqVC3FRLAEqE#5($`76{!|3lr4RQ=4+WWWHu|4}xhBx(Y+Q603#qSzPJ!D{s2 z&!~a^MAiQX1MwZIo_~Ln4#h&G3!v^uThs&=U?i@5e8r~zuCI&6Vj zd3V%+Q&0mgK<&T=)YhIrt@u3Z`)~#II)6glnT!KX|3y)kxh%Tss2UMnh8C#HF&Go! z2-EFyTfPA`!QH5Sj@$Hg3?ls)H4)!Ij#COFu?V(DmCqfd?>~`^WaxGu zMy>n?s^h0N{mG^S2Alk>n2z#%HXVoBfiF;(uQB>!M{9S~!uq1>4MJUZcQE^}Gnq(+ zw&Yt>#|uys*=Q^7MeprJ&HOCtjyyoE`~xn>fFax)+=}WiXsCHBvZFrbu^5f5Q0=C> zM6{L5P+PkZ3*v5!z{kkf*9jVC1{{ORpMjdtF4XIH0JUS+P)GI%ygcl#01OoyR57;g(^qW8>EXSfUt;9=B`y+&=dGs3JeHEL&~Q0?NZRWKvz+8BqIT*679}{3_(NwOHp*;iLCi+FjI|}I;Yid`j6-edD%8&XW<8H=sq+9Y zpxu~eAG@vj4_}1XmoYv{fKCWV^AxfkE*!Fx(yqU{tdOFpt1HSP)Ah^wS!eL zG1f-ik$R|=Hb?DDKh%UrVj}!*Ea$JSnni}TbRMdsrKk>8+x$(a4tAg>b`Ue;Nz9KA zY!Cy@sby@8wn0N}t+%pK<1BQe#K* zLs36Y$D-p0YH%6V z;X~BGA2137zA`^UN}}#SYYfK`sCLUxcWfJKBKt5gy1x_gBXSab@ggR{tEl(;7V3-m z6xC6Z38teU)C6*1VJw0=k}jw-{}Of9^HBY5K-J%hy4*+5qxb(lk)&iKpJ-;5230W= zYQVgx36w$YNL`!X6_b%3j%q&+HP95)N~fc~9}7_RwxJfZ$CjVKq>S(UNklWff!f-a zs5A1NWR5014)I}oQ3EWTY%bw4tVwzuD*pqfM4u^UNBylCPz%U{nn*Niypouf@txX4 zwBk0Xj{0LY9D+mf02aftUz@WXgqq+C)C87dPJD*BF~d}|b7fHrs)HK14W_`ZmH93Ns`7P9Cd4;+o?@%2&(@aOnQT+s=>gPb+fug9_tR`w=v>Y3#om9w0+oatd|EcQG~ohuWD`-h*bu9!&SGS#b;|CmoC0iOT4QH8BtySUaQ2 zhoS0^M(xOC)J`lx?bJ$_h-SRg7Mwy=JdZl7hp4l9jXIJf-88l#hDm}D^cx_qmJY< zrop$UAG0ZDnmZ7QT0lIe!m%wBWj@3v&;u2 z8)^s2q9#_&+6y)C4Ak9PhdS~@SW55zZ6Z-*WSecarW)#Y*GH|a1qNX^%!Ol6TffS> z6$44{LoMVy>g=ClUVMeRYaR0q)cfxeQH9y4j+bIEZa}U4DC*K&N1fpv zjKehFoASD-o$HM1XDI5m8;_dM64aLOM(xmP)J|SOS1Wx;L@RiSs+er9S$QaGLQ$vz zilHt~Rn)}lq287*sE#M2CNK+CZwcz>{2H6T9#wBAs=q^X+5g%^E|H-bMb0xF7Dsi| z6g}9{=6{8{WUEnMyd$Wcd5AiyXBdy4Fb3o2o4YgwRX!RufoYfz=gw#U^`SXJh9+bDm;;ZYZucY9(F86u9c4pJI6tahJnH@Kgt@UNY6s?^7O(n0c{zuVYZJQ+?FVG)1kfJF268s0ocn?Z86RrQVA9@mHJw2(_@p zOU&I0L66@52qN0jDyU1<19casqdHuSI?L^-ow|=&`3F?JluOMDLr@dVjapCvjKtD5 zzXR%0cSG&SAPiu9XPhE96LnV0Q4LRGIKIIs%(l#wSHlR>{jKv+J97Yawii%4bQ3ku zebkP;!lw8MRj=uC`~G(zl7x(&sEPDLtzazbmVbl3I2TpE2zBNgP-nUqb@mrf16@NM z;XTZOJ}b;+4ns|>EUI3m73_awBDKlTK&vnpZnEi1s4csNTFGP7#6F-Vl6<8(`%I`$ zY9Um9Rn!EVSi7Ry4Z{jJ0rgrQU&;Ra5&4G<&FC3wYu?$k?<%u1snM7GbQq4AQ0>a0 zE>#tq-x_n0{t9&@KcRMFH)?_xQJ3~H>TdbCtIYrzQ8Nrh-R9h=%T^vkup#OL(-*Z9 zKVULkgF3pcsGZt@r@4z)8~Q0?8JM6wYXiymBxYIp#3%g9j-%7C~$*WKq&g_{m(^2 z9Tipv#-r{)U7O#{riY_eI@9K_M_s<(td~#&JVhPhJ4}r!Hkt)xLhWRJRDK+~x;$SH zQAZ6>XW0WIa0cqu?!pdu7B#UFo6JP(ViD4<(Sx&WdOND0N2vPgH=CWyXDyCeXobz} zzh+#AjLO&y)!|aq<@wX5KVenUMYfn=UZvfUV}TWI?^>VK$u?b@>WoS}cuvdupL}uqWz>#$!5Mg!<5I z!ALxfx;yVs69{s*nMe^-#rl{Y`=C}Z9t+}R)CzW?cI+r>;%6}p-a<|Exy^r%s-I%J zF(Yb1VW=G_jyfVYo`@QDvjzQ7zj(N)3CzVhxDfN>Gt_PO{A_k24Anj#YGOrDuUT1} zUkSBiwQPAK)Q+`5j>>gL6VXh+vK3~a2L1tcBugelB&wJ(gBFcvjnBUJybQEy8p)P(w?tF0eNL|Z)>b(Zr`Ghd1---Nn6doTpgqJFMF zK@AYN+YAtb+Q}TK{0N(05VfFUHeChPf9>6zzalNk&;UJ9D;RB^jGE9)n_hrg!AexS zO{lkJFRI=J)WFwJXMYd1b01L)N%gC#mkHHR_Fvh5%`7h&T3IaWY?`1}(%(7+HL;P_ z38)?W7Ih>`P!rjKIq@W_{d3gB-dLSIW~Y*&#tm?7h6mLk0(CZdQ8O%#T2TeefHhF{ z+M>?96Ka4SsLMM5ReuO-L1Qo%{$R@wpmyX4Y8>|h5p{G6wW9l|t$l-9f%q@ zJ8FP%R0jpDrBIiwvQ5`Q)o+SAidLu{9Eh6mD6FFQe*%$2WSmBw(K%GdH&H8igjzxJ zy{2ISRL8|o6D^I}`s%2Kv_=io5j9Y6)ItWMz6)bf?PhxOU4EVuQN{Jv?cM@zJF4O_ z)WrToeHZ>l?Z79SpKPBQI0I^+5Yz&~ZGIutgo~qgDAuOqRnGWMRU&Fw8+DnQ+6sM9 z0}VoTI0|({Q&20Mk2=d0sE)VT^iI@-kD?ZK!RB8>_4m;F621TZ?;{aSAnktBAQaU> z7;1oMn=XqQxQ0zPMD0jB)C7j3j$kZm=O&`+&#~o8P!m{m3 zPyICeO*#}qN#{b=ToRV;x?v4Txk!z`rhqqev&s)M06?V>*2U!f*257pl?)DCU3>Ak3hoy5d=69@Qk z{&$I#BBR+6en#VLEQ?n#w-4KQl!jR882_k9e&gSn4AwnP8(fL8nD~VGA0EYFQPT6U zG@ijY4EV$RCz?7KLwXKoV|?cX5&Cx?p?+*WL9HnHNwcCXSch~tHpju(9?zrhPRUc| za#coM%6eD?JKOvpumI^FQ9FDY^Whzg()%BH+RQi>wUYj*2Ai-Z-m>X}XUyMTTVo#b zm!Ud5g-P);>P&Ci^h4BTe2%)jDbE_iF%9WBbh8twOT;@cYG#|PH?R=t)aT63#G&dp zLw%Y@pce2w&cRIQ&CYE?-I+tEiQd6%nC60MUl5h9dx7S9&-;>*0VktoycjjWFQ`j* z9<{QosLOXBHNYcOy_6TtuWAvfbS2aZ+hPRv!}>T6_4+-oEuqVqUz0T4C}(%|we}YSOXjk2O#eX^LsFn{|YBnoA@l1&gg~QLoW2 z7=Zgwci;?a=AO%@JRUWXny3NVqgFHkbtfjGCj1>{!qpgs`!N_FqjuKy`^!`eKy6(v z497Us8Fj`$oPgSqd6*e@peA%4Bk?U3#~fEo$IVdn+gZn;CbSrv;dbN*T_@dDGxKQF zPE)ov18QK%h>L+wBVo9<;Di`8{~XA{v@ z-bUT-cbFAJuba1`6lw>CSf^k{(hE^ry&bisdr?Ps*_PkN^rYWoHcWHF>_9=(!YW}! zmq=qGs`vxy_1lb^`At;*OVrk-xM?QhL2Z3uY>17pHm*ZW;3Jkozgzr-!wRUQ`3dzF z-9g=%C+O-do!jPDtPs>r#G&#V+x&j0t^FSLp;?a6xCT9V4Rhfq)Bw5em@i{N)IwrW zcd8ERhtgn7gd^{;|1m^HlcA2cpE!M?1_3? zcBAe_%D>I~pBudsK=n5m)&G>g*?&bAkf9Z?vmUh-@1V}=4eE7CanIBb#H6HiTB9%t z>1foQD2jUj%h_}-)Xp?U9c4SzLWj7vU;?V)cc{y>0Cj7(p(bz&`Di(}a2oEoZ#t~@ zk9qwXpcXI^%i;H^yK@;e&=b@Ve#D}f;(__=ycKz-EK zcSH{k!IU@`b?H{1>aE8nxEE7k$U{>v9QE23LhV#}Os@C8GLigb)WI4!$`m*!P#s>x zocIssLjOnR5*0>2(j`#!;;;bLL%qIZ(Su7c1oxvBa2>U?|9ZM%0S(B3tTILfwTgumpBP^|u@Ya3gwfFS>e7ZV}OK zcAlBbNGDMf$o`z4c$f!OJ|45-Jk*Zv#58#N zIp?32$SpFa;tSNkV_%rdI3JbXg6jALYKJbOw;g6D{Q}i-&`UGnyr>iu4oI3O=AF=>N*J3r7#>7!1b-SPO@v z>i>?ae+Biny>xAX|7&wevSLdL!muiiMKwHu?eH4r#`15>%Dbb=r=l*^Le$FEp^kQk z^%Cm#zq0A%Z_QifMi2?2pgLy6wx}~7ikWaW#^Oed#3!hg1-~<|TUpcudZ7j$h1&8d zm>6fGF5UO&i>pyPz8;yd>ue{Ifs8|#92Mh8Y^R|X zFcJ8!wxVXf$L1f#9Hh^p zR{RRJfKR9ur~l6k9EQQ9^Pr9(9yQTM7^L^V7m??@PUBG@kfEQZtP0I1qcIJCVpeBK5G3kB|3^527y9V^scs zHos0HAMe%S553=e*f=8hE_fb zwUy&gE1QZMU?!@gm8ehd&(@0=LHZSHWm%K>cz^hWqb~I*)DFx>9l;vZzz0!BaXX1? z3SN<+fdhPfyq6;wb%_e0wyq_rqhT0^-=GHGiaN`ySPfrd6D*h1$NL`|=c0%7HLQ-G zusBxt^YOk_U%EteOZTB}^&?b=iIbTf2tlnZ2WrMqs0qc|@}{WE*#`<95d*e|({lN~dmUaL~50c&F!Y>PVM#i$9bK@GSCb+#ulFJ4Ez zHK|hgcrR@*Y9d)tJ696**0e&}yUrLQ+JObO;0S6$|HESVpG_A@X^x;Ws=PVsEPJBf z=P8&ESD?O-=g@;6u@GiUWfoQ!wSbQ3egAuUBV0PvC7X|0(OR3{jT-0=)LZco^#y#7 zk(k5Z>|AwJeh*as3~Y+KF$RNEo1LkNx@%1^y-TDm5e+a1HS?LMt>1%sjn1G}cpbHM zZ&7bcXn>FRSGDY@`pvN}_Cif$A8I21p!x|+V|E}L>Zl8&_uu~-5Ye}~Ge%=~)NTC% zJ@^1Mft38v*JaCv`jxBzYU1@!XWR$1LtmpV@qE;dZb7}a2W|cx)Z6qSE$_cBTiQT# zt8<~YzC3Ecx~P?ON3GyH)U92F>UbS$;(Jg7??-if2DMZ7a0I4EXI}FOsJCG;>JA-E z$NOK9$X{gWm&;V?%`GluZG}3kDX0NvVq08>bP88zf{ddJ6)Q&d9aQ*xrN8pi`>+)A<2KZ#zl*LSLD@_}WmI|~M&nAFo=+8EPKk9>Z0`(T1K^@Ue5AVM^cxfvn%5DboLsiUz znt5JSy|So*YoLy#9u~oVsMmKj>h(H_I{S;*25;DO`5fkpSRHjq`{!`Y*^eYcw|o+6 zpt-0Su0;K~-D$mq+KHE_UocYUH0^Sr?oc7Dhhi;M7 zeu$9E?KtfU}Sq>r--x1cJv$Zam+Y}BtzN3j$piZT-^gXKwAN99jN4R{Q-#b>b< z-a<{Zd>*r)2B>sv^ws;{kBAx!@n-OQKk9N!MZJD=P`^C>h?@C+>m}5VJVLGb9qNZs zU|#!{pmx%OI?5QVh2?GjbkzS1k+YnLX8s3i3ol{>UPs+>zkKEhB2XRV!zdh%lW-+Y z#zOhcW&9O&x&A;+;4 zqTYgh)_BwoHAD5=8#R#$s0GbH-GyDKBYRzt_g|mX!qGm?SgeD3kI$nn-FwuB$fuC` zVNw`NlWu{!6LV3Y+_R|H>j`Rt?@+I^Ut#kl&4}GdM`9zKfsy#IFzp;{vU%ECj74>DC zgSwpyQ4KbsR`?rg##c}s`xZ4T^PtkvsP9A#)YcD29pMa{UW@u397O&2{R{O;cAaA8 zOngxTmP4(e7V7dewf00^&N1lyQlje5wXQ<_Slx!Y1D8<~euCPW;NoT{a-lvz#gK7a zrzsKrZL}j+#jB{-D58Yf`aGzCV^CXG+NLX^&b*dQH$fd$8`MO)qIPsD>Ii0{+OM?v z8!^4!|DTCuqTmE-%kH5rp;OZQ^b1E#peCxL2B`X-(1W8f)?#Le05&VHV%D+() zd5c<@Q_75!1ikNn79x5rDq%$&jWKu_^}c_=E?B*^`3+_(Y71YYcWYzK%5tIi@?kN` zOJQ#8hnm0w)Q&Af9pQR(wX!`#)bKE>B!nj*Fqrt{iFt)vOI{d27_s zbwRCk3~FH$QT3;zF7tfUgf`gxJ!M_f@i8*Af^)XuDyrgr)a&&c)nQ;cQ!g{>)0`7^ zCn}%@Xn>kP7t{)Sqh7mFm>p+Xe@4B2mt7+IfLuq-^e$?pPf-(khk9QV$C(|9ME#0Z z5jCNKs4X9fdL1XB+Rs5vYzeB~7Ms7z<{v^$(7i-NGro`7!Y8PKla)87wPr@$-khkN zi9mh2i=qeX+wuXZ{)V9HjY9P|&8Fv|F6~NWVy?4=h&tMZn)zi^gFC1W9-&tD0kuOZ zDwvsPKy6_z)ZK|fwXcj?aUJY{O;8ixgSuP$QJ3)+djI#oABlL#2#q%b$D%r}glVuk zszGyGJ{Yxv(Wrr@qK;q|dbb>Pq+4wM4%EaBq3WMPE$k9{|NeiEh&uRyx-7{mnu%mZ zy+#qJ36(?*T-I6xHBb}O3R|JJz8z`@yV~-8s88|`)DBEV)mw|Mccw%%fgPw7971() z3AOb%Q4OA;I(UWJkqnj0%A!#dErqIA3$>62sQTSdJ2?rp1M_TpX(ir&&1?%9>fk79 zB3Dqa(|y#X{D7KJhRUWqE2=ySm0t!maAnk1*Fx<`W7N(yNA=$ZHL?Df6DL)6%?j6% zp_%Qn6%U}k;TO@na$Eii)!|3fK#8lEOX-iAcrdDd80z)PWAjU)CR!OakuOk}xt(h> zdZK=n8jb2`4(ceDp&D*OHQa;x@SL&v|Dsm(9yM^BBB*FLUl9#>0R2WV{Hapl{Zz-=5 z{!WM{ETB$X+ffT!Zzyi0uf60?$7a;~gZLFf3F2AlI~(zZ#4i!PBK*bo$N7gy1clRZ z3>AK*F@Mi+0_?=dcIuF?=Nt0R+Welx^&}^~i1fH9)1Qnzu*)a*C*bOcqD$Jd@SxK|8LY&!?t;h{0+@{ zMc#ay_dn_lBh+%qxK8Br!`tIrBCn;LrSem#V?t)a54P@R;t8Le3DP_4APLJGQGSPv z%Nh?e*nV6O8P6!3XESaSKSR*h>kMHK6Uj%YOZY&3CF*t805~1ua62KGys^sX>4f`n zvNvV^;BRtH0DV*>=*ekto%a;zX<%?l)3`PrCZ%Fp+-J*o5r1LhQPe3w(Es(yX4*9; zlp~}eZz`b+LCaL*d6~UkQc`W4e$8Lh29)zRbnEB%X zl`>QIh3)t^2C8cZQ;lfKH`)66sWYGWTk6*J;G+fDAEa^+|=nsMmox}()J;?Cg`b2*|)^Uk>8)Vp3c@* zt9NnR-nUdDqXoOvVpX9z)>opU!9oO+sE0%waoFXP=)AM5fZgXgp%en^^bL z?mOarDf<>Hk@lhPQ|$J+13anvc6{eY3fq&xKU_F#h<|=ss}fIg98D+HD0@v@KOtvg z9O_rBJcOg9Yf?wgGiyqdbZ#c-GY5;>W7FajzGO2H!hn^@xbvx^y@dJeo z=%g1F_2eS2AL%FL2VyzWuL%6pkoTFwpQWgqg}gGv+Yw)mHEjDb*38!O)ccRNZYwI~ z)l7JbQ+R@cuL)@>e1TtJeezS`A242!EiK~HMxF0gesQuaUcUJ!N@Ruc3) zCBGi^`OBAC|2Es;BZc33tMCz`!tdm_rNd@~oy1Stvf7ljB%Q(zs_ce@mxP}P2N+-= zx^>>G+SeiUN-w|Ro{!BXALE;>ZMiO5^_?h?!^6Jvy4C%bYo03dG^ z_>3aGm$J{#HR>lLe12|Hp7wJBSJ=8EiD%XO- z0U3=+e11Ap$(N9uN_s95*R#-?gu4IQL9>x}f%rarL3nGbIt{5)p7u9LZ?%1=Cmlq5 zF7?8RFH>Eu{{|W72zp`(T?wJ2!w9tqs|mHq3$cUtBJEE)GvOE=mbC+r{D03pTUQzR zseg~~z}CA+SzTMx{ejFQWHch9A(h^e-a>jLGyMFtA$^ZT4C?ugdbfzzCA24=hq@~I zWCs{cq6GP!$g5=YekJcb@ml1)rtCewOl;3TCmDLKP+^dXI*sTwE#a^&OJzH<#m-h6 z38#D@ov*d!Z%LP>-C51ZK7SMMm>{j}45UkH{81#15K<9-qw#F)LuZR^gKFd@BAtRx zL&;l9Iwfu(^df{(|0l`^5uOrnNk~DRIt2a8$xwoxI+Vu|T9JR8cB#oLuJd0?W>K6* zrk?avNKI%>dN*YSZN3i^SVMj*^77fdL{_z{LD^8s`eA$O9wtx!CZy-M!5K#V5aQJc z*9aQl=}4ow6g(szj-3fHbkKqP&(8+pN9i~xWqR6>&P&KmygzjpC#b9PSqah;6U1B4 z$8$n*+H_CC`0Yp(rokO5eMS5x@xK_PyYldNLKX72kp7oCLx`6ntR%l8;q%ko4l;wX zLxc#*){xhR3W21@5GIh0WZ+c9`%!*3G3y^jW@U4%M^ zh%Z7tTlM?j5gP5Hu)WPx!5iYka3uLvY)9?zG~ogD%V8JXg30hJ3?U?Za+9y;1AP@T zQD-uBY7jawsdVI@GV3>`^C(zGqrrrqDC~yQ6LeaT^c6zFXEODM5|)y=4ZBi)9lZ~h zV4#FgN%BH$AB`z{EC zJ9#MyafEn6M#>&iKa_ZX+IFH|WAfa>WELf}Dv6VXffVXlLHM5?Oz9ysXs3vMvQW3J z9jqeprsR*u&(D9vbJFjx)OkQZy9r+q{vdxEd8Y{5_5II8i+)s`YC9{4kI37G=V|c7 z<`2e$G%ibMO1>TsVTmozjsppmZMr#q>ZwE6XzRDbtTz9Ww;uO@8x^|QM)&F5SFL!G z5?d^r%FGzQ$JiFSE_a*IT;sDaC zyzd{0^;F72C{N*h{D;Cmwu9|>kUTwA?Cj$x*Hex37g*D#y_x(mfpmI83*t}kHXg(r z1U\n" "Language-Team: French \n" "Language: fr\n" @@ -123,7 +123,7 @@ msgstr "" #: billing/models.py:74 billing/views.py:152 expense/forms.py:71 #: expense/forms.py:76 expense/models.py:104 leads/models.py:294 -#: staffing/models.py:72 staffing/views.py:1428 +#: staffing/models.py:72 staffing/views.py:1419 #: templates/billing/bill_review.html:207 #: templates/billing/client_bill_detail.html:25 #: templates/billing/client_bills_archive.html:15 @@ -334,8 +334,8 @@ msgstr "Libellé" #: billing/tests.py:268 billing/tests.py:277 billing/tests.py:283 #: billing/tests.py:289 billing/utils.py:164 billing/utils.py:182 #: billing/utils.py:198 billing/utils.py:218 billing/utils.py:225 -#: core/views.py:376 core/views.py:397 crm/views.py:485 staffing/views.py:1633 -#: staffing/views.py:1672 staffing/views.py:1687 +#: core/views.py:376 core/views.py:397 crm/views.py:485 staffing/views.py:1624 +#: staffing/views.py:1663 staffing/views.py:1678 #: templates/billing/_lead_billing.html:24 #: templates/billing/client_billing_control_pivotable.html:56 #: templates/billing/client_billing_control_pivotable.html:62 @@ -353,7 +353,7 @@ msgstr "Libellé" msgid "amount" msgstr "montant" -#: billing/utils.py:152 leads/views.py:621 staffing/views.py:2051 +#: billing/utils.py:152 leads/views.py:621 staffing/views.py:2042 #: templates/billing/client_billing_control_pivotable.html:52 #: templates/crm/clientcompany_detail.html:297 #: templates/expense/expense_list.html:16 templates/leads/leads.html:17 @@ -362,13 +362,13 @@ msgstr "montant" msgid "deal id" msgstr "N° affaire" -#: billing/utils.py:153 leads/views.py:623 staffing/views.py:2053 +#: billing/utils.py:153 leads/views.py:623 staffing/views.py:2044 msgid "client organisation" msgstr "organisation client" #: billing/utils.py:154 billing/views.py:155 leads/views.py:624 -#: staffing/views.py:2054 staffing/views.py:2087 staffing/views.py:2103 -#: staffing/views.py:2104 +#: staffing/views.py:2045 staffing/views.py:2078 staffing/views.py:2094 +#: staffing/views.py:2095 #: templates/billing/client_billing_control_pivotable.html:52 #: templates/billing/client_billing_control_pivotable.html:67 #: templates/billing/payment_delay.html:50 @@ -381,19 +381,19 @@ msgstr "organisation client" msgid "client company" msgstr "entreprise" -#: billing/utils.py:155 leads/views.py:628 staffing/views.py:2057 +#: billing/utils.py:155 leads/views.py:628 staffing/views.py:2048 #: templates/staffing/turnover_pivotable.html:71 msgid "broker" msgstr "apporteur" -#: billing/utils.py:155 staffing/views.py:2057 +#: billing/utils.py:155 staffing/views.py:2048 msgid "Direct" msgstr "Direct" #: billing/utils.py:156 core/views.py:373 core/views.py:394 crm/views.py:482 -#: leads/views.py:632 staffing/models.py:456 staffing/views.py:1630 -#: staffing/views.py:1669 staffing/views.py:1684 staffing/views.py:1732 -#: staffing/views.py:2058 staffing/views.py:2091 staffing/views.py:2144 +#: leads/views.py:632 staffing/models.py:456 staffing/views.py:1621 +#: staffing/views.py:1660 staffing/views.py:1675 staffing/views.py:1723 +#: staffing/views.py:2049 staffing/views.py:2082 staffing/views.py:2135 #: templates/billing/bill_review.html:34 templates/billing/bill_review.html:82 #: templates/billing/bill_review.html:127 #: templates/billing/bill_review.html:173 @@ -435,7 +435,7 @@ msgid "subsidiary" msgstr "filiale" #: billing/utils.py:157 billing/utils.py:190 leads/views.py:627 -#: staffing/views.py:2055 templates/crm/clientcompany_detail.html:268 +#: staffing/views.py:2046 templates/crm/clientcompany_detail.html:268 #: templates/crm/clientcompany_detail.html:299 templates/leads/leads.html:19 #: templates/leads/leads_pivotable.html:58 #: templates/leads/leads_pivotable.html:65 @@ -449,9 +449,9 @@ msgid "manager" msgstr "manager" #: billing/utils.py:159 billing/utils.py:216 billing/utils.py:227 -#: crm/views.py:480 staffing/models.py:455 staffing/views.py:1629 -#: staffing/views.py:1667 staffing/views.py:1682 staffing/views.py:1731 -#: staffing/views.py:2143 templates/billing/_lead_billing.html:20 +#: crm/views.py:480 staffing/models.py:455 staffing/views.py:1620 +#: staffing/views.py:1658 staffing/views.py:1673 staffing/views.py:1722 +#: staffing/views.py:2134 templates/billing/_lead_billing.html:20 #: templates/billing/pre_billing.html:92 templates/billing/pre_billing.html:159 #: templates/crm/_clientcompany_rates_margin.html:102 #: templates/crm/_clientcompany_rates_margin.html:108 @@ -468,8 +468,8 @@ msgid "consultant" msgstr "consultant" #: billing/utils.py:165 billing/utils.py:179 billing/utils.py:196 -#: billing/utils.py:217 billing/utils.py:224 staffing/views.py:1729 -#: staffing/views.py:2084 staffing/views.py:2092 staffing/views.py:2142 +#: billing/utils.py:217 billing/utils.py:224 staffing/views.py:1720 +#: staffing/views.py:2075 staffing/views.py:2083 staffing/views.py:2133 #: templates/billing/_lead_billing.html:21 #: templates/billing/client_billing_control_pivotable.html:52 #: templates/billing/client_billing_control_pivotable.html:55 @@ -491,8 +491,8 @@ msgstr "mois" #: billing/utils.py:166 billing/utils.py:180 billing/utils.py:197 #: billing/utils.py:219 billing/utils.py:226 core/views.py:372 -#: core/views.py:393 staffing/views.py:1534 staffing/views.py:1631 -#: staffing/views.py:1670 staffing/views.py:1685 staffing/views.py:1730 +#: core/views.py:393 staffing/views.py:1525 staffing/views.py:1622 +#: staffing/views.py:1661 staffing/views.py:1676 staffing/views.py:1721 #: templates/billing/_lead_billing.html:20 #: templates/billing/client_billing_control_pivotable.html:53 #: templates/core/risks.html:41 templates/core/risks.html:48 @@ -519,7 +519,7 @@ msgid "mission" msgstr "mission" #: billing/utils.py:170 billing/utils.py:181 billing/utils.py:191 -#: staffing/models.py:457 staffing/views.py:2056 +#: staffing/models.py:457 staffing/views.py:2047 #: templates/billing/_lead_billing.html:20 #: templates/billing/client_billing_control_pivotable.html:52 #: templates/billing/client_billing_control_pivotable.html:74 @@ -551,7 +551,7 @@ msgstr "Montant réalisé" #: templates/core/index.html:63 templates/core/index.html:87 #: templates/leads/lead_detail.html:150 templates/leads/review.html:48 #: templates/leads/review.html:81 templates/staffing/fixed_price_report.html:18 -#: templates/staffing/mission.html:97 +#: templates/staffing/mission.html:98 msgid "Responsible" msgstr "Responsable" @@ -568,7 +568,7 @@ msgstr "Responsable" #: templates/billing/supplier_bills_archive.html:14 #: templates/leads/lead_detail.html:147 templates/leads/review.html:47 #: templates/leads/review.html:80 templates/staffing/fixed_price_report.html:17 -#: templates/staffing/mission.html:94 +#: templates/staffing/mission.html:95 msgid "Subsidiary" msgstr "Filiale" @@ -578,13 +578,13 @@ msgstr "Filiale" msgid "Paying authority" msgstr "Organisme payeur" -#: billing/views.py:157 staffing/models.py:76 staffing/views.py:1428 +#: billing/views.py:157 staffing/models.py:76 staffing/views.py:1419 #: templates/billing/payment_delay.html:50 #: templates/billing/payment_delay.html:58 #: templates/billing/payment_delay.html:66 #: templates/billing/payment_delay.html:74 #: templates/billing/payment_delay.html:89 templates/leads/lead_detail.html:232 -#: templates/staffing/mission.html:124 templates/staffing/mission.html:168 +#: templates/staffing/mission.html:125 templates/staffing/mission.html:169 msgid "Billing mode" msgstr "Mode de facturation" @@ -654,14 +654,14 @@ msgstr "" "Impossible de supprimer une facture dans l'état %s. Vous pouvez par contre " "l'annuler." -#: billing/views.py:606 people/utils.py:32 staffing/views.py:607 -#: staffing/views.py:825 staffing/views.py:1562 +#: billing/views.py:606 people/utils.py:32 staffing/views.py:600 +#: staffing/views.py:818 staffing/views.py:1553 #, python-format msgid "team %(manager_name)s" msgstr "équipe %(manager_name)s" -#: billing/views.py:618 people/utils.py:22 staffing/views.py:626 -#: staffing/views.py:837 staffing/views.py:1607 +#: billing/views.py:618 people/utils.py:22 staffing/views.py:619 +#: staffing/views.py:830 staffing/views.py:1598 msgid "Everybody" msgstr "Tout le monde" @@ -959,7 +959,7 @@ msgstr "Clé" msgid "Value" msgstr "Valeur" -#: core/models.py:81 staffing/models.py:75 templates/staffing/mission.html:91 +#: core/models.py:81 staffing/models.py:75 templates/staffing/mission.html:92 msgid "Type" msgstr "Type" @@ -992,7 +992,7 @@ msgid "deal" msgstr "affaire" #: core/views.py:377 core/views.py:398 crm/models.py:482 crm/models.py:511 -#: staffing/views.py:1534 templates/core/risks.html:42 +#: staffing/views.py:1525 templates/core/risks.html:42 #: templates/crm/_contact_list.html:10 #: templates/crm/businessbroker_list.html:13 templates/crm/contact_list.html:13 #: templates/crm/supplier_list.html:13 @@ -1117,7 +1117,7 @@ msgstr "Nom" msgid "Code" msgstr "Code" -#: crm/models.py:88 crm/models.py:106 crm/models.py:151 staffing/views.py:2060 +#: crm/models.py:88 crm/models.py:106 crm/models.py:151 staffing/views.py:2051 #: templates/crm/clientcompany_ranking.html:14 #: templates/staffing/turnover_pivotable.html:109 #: templates/staffing/turnover_pivotable.html:117 @@ -1319,11 +1319,11 @@ msgstr "bientôt en retard" msgid "last 12 months" msgstr "12 derniers mois" -#: crm/views.py:481 staffing/views.py:1668 staffing/views.py:1683 +#: crm/views.py:481 staffing/views.py:1659 staffing/views.py:1674 msgid "profile" msgstr "profile" -#: crm/views.py:484 staffing/views.py:1671 staffing/views.py:1686 +#: crm/views.py:484 staffing/views.py:1662 staffing/views.py:1677 #: templates/crm/_clientcompany_rates_margin.html:101 #: templates/crm/_clientcompany_rates_margin.html:107 #: templates/crm/_clientcompany_rates_margin.html:113 @@ -1474,7 +1474,7 @@ msgid "state" msgstr "état" #: expense/tables.py:122 expense/tables.py:190 people/models.py:273 -#: staffing/views.py:1429 templates/billing/bill.html:164 +#: staffing/views.py:1420 templates/billing/bill.html:164 #: templates/expense/expense.html:31 templates/expense/expense_archive.html:22 #: templates/expense/expense_payments.html:48 #: templates/staffing/mission_consultants.html:9 @@ -1537,7 +1537,7 @@ msgstr "La note de frais %s a été supprimée" msgid "Successfully update expense" msgstr "Note de frais mise à jour avec succès" -#: expense/views.py:304 staffing/views.py:1811 +#: expense/views.py:304 staffing/views.py:1802 msgid "Incorrect value" msgstr "Valeur incorrecte" @@ -1666,7 +1666,7 @@ msgid "Sleeping" msgstr "En sommeil" #: leads/models.py:67 templates/leads/lead_detail.html:213 -#: templates/staffing/mission.html:144 +#: templates/staffing/mission.html:145 msgid "Administrative notes" msgstr "Commentaires administratifs" @@ -1694,7 +1694,7 @@ msgstr "Échéance" msgid "Creation" msgstr "Création" -#: leads/models.py:82 staffing/views.py:1054 staffing/views.py:1428 +#: leads/models.py:82 staffing/views.py:1045 staffing/views.py:1419 #: templates/billing/client_bills_archive.html:14 #: templates/leads/lead_detail.html:139 templates/leads/review.html:46 #: templates/leads/review.html:79 @@ -1793,7 +1793,7 @@ msgstr "prix (interval)" msgid "sales (k€)" msgstr "C.A. (k€)" -#: leads/views.py:622 staffing/views.py:1534 staffing/views.py:2052 +#: leads/views.py:622 staffing/views.py:1525 staffing/views.py:2043 #: templates/crm/_contact_list.html:9 #: templates/crm/clientcompany_detail.html:266 #: templates/crm/clientcompany_detail.html:296 @@ -1855,7 +1855,7 @@ msgstr "Profil" msgid "Subcontractor" msgstr "Sous-traitant" -#: people/models.py:317 staffing/views.py:1685 +#: people/models.py:317 staffing/views.py:1676 #: templates/people/consultant_detail.html:237 #: templates/people/consultant_detail.html:243 #: templates/staffing/_consultant_prod_tooltip.html:2 @@ -1863,7 +1863,7 @@ msgstr "Sous-traitant" msgid "daily rate" msgstr "taux journalier" -#: people/models.py:318 staffing/views.py:1670 staffing/views.py:2215 +#: people/models.py:318 staffing/views.py:1661 staffing/views.py:2206 #: templates/staffing/_consultant_prod_tooltip.html:1 #: templates/staffing/graph_timesheet_rates_bar.html:15 #: templates/staffing/graph_timesheet_rates_bar.html:25 @@ -2171,7 +2171,7 @@ msgstr "Dates de staffing" msgid "mission id: %s" msgstr "n° de mission : %s" -#: staffing/forms.py:324 staffing/views.py:1367 +#: staffing/forms.py:324 staffing/views.py:1358 msgid "Days without lunch ticket" msgstr "Jours sans ticket restaurant" @@ -2385,15 +2385,15 @@ msgctxt "masculine" msgid "None" msgstr "Aucun" -#: staffing/models.py:73 staffing/views.py:1428 +#: staffing/models.py:73 staffing/views.py:1419 #: templates/crm/clientcompany_detail.html:270 #: templates/staffing/_mission_table.html:20 #: templates/staffing/fixed_price_report.html:16 -#: templates/staffing/mission.html:100 +#: templates/staffing/mission.html:101 msgid "Mission id" msgstr "N° de mission" -#: staffing/models.py:77 templates/staffing/mission.html:127 +#: staffing/models.py:77 templates/staffing/mission.html:128 msgid "Management mode" msgstr "Mode de gestion" @@ -2434,7 +2434,7 @@ msgid "Min charge multiple per day" msgstr "Multiple mini de charge par jour" #: staffing/models.py:432 templates/crm/clientcompany_detail.html:253 -#: templates/staffing/mission.html:104 +#: templates/staffing/mission.html:105 msgid "undefined" msgstr "À définir" @@ -2467,10 +2467,10 @@ msgstr "prévu (jours)" msgid "forecast (€)" msgstr "prévu (€)" -#: staffing/models.py:470 staffing/views.py:1054 staffing/views.py:1329 -#: staffing/views.py:1428 templates/leads/lead_detail.html:227 +#: staffing/models.py:470 staffing/views.py:1045 staffing/views.py:1320 +#: staffing/views.py:1419 templates/leads/lead_detail.html:227 #: templates/staffing/consultant_staffing.html:11 -#: templates/staffing/mission.html:163 templates/staffing/optimise_pdc.html:84 +#: templates/staffing/mission.html:164 templates/staffing/optimise_pdc.html:84 #: templates/staffing/optimise_pdc.html:109 #: templates/staffing/pdc_detail.html:8 msgid "Mission" @@ -2499,14 +2499,14 @@ msgstr "Pas de ticket restaurant" msgid "Lunch ticket" msgstr "Ticket restaurant" -#: staffing/models.py:543 staffing/views.py:1429 +#: staffing/models.py:543 staffing/views.py:1420 #: templates/crm/_clientcompany_rates_margin.html:17 #: templates/crm/clientcompany_ranking.html:18 #: templates/staffing/rates_report.html:23 msgid "Daily rate" msgstr "Taux journalier" -#: staffing/models.py:544 staffing/views.py:1429 +#: staffing/models.py:544 staffing/views.py:1420 msgid "Bought daily rate" msgstr "Taux journalier d'achat" @@ -2557,8 +2557,8 @@ msgstr "Toutes les missions" #: templates/leads/lead_mail.html:8 templates/leads/lead_mail.html:12 #: templates/leads/lead_mail.txt:9 templates/leads/lead_mail.txt:13 #: templates/leads/mail.html:8 templates/leads/mail.txt:7 -#: templates/leads/mail.txt:8 templates/staffing/mission.html:124 -#: templates/staffing/mission.html:177 templates/staffing/mission.html:179 +#: templates/leads/mail.txt:8 templates/staffing/mission.html:125 +#: templates/staffing/mission.html:178 templates/staffing/mission.html:180 #: templates/staffing/mission_timesheet.html:90 msgid "To be defined" msgstr "À définir" @@ -2586,7 +2586,7 @@ msgstr "" msgid "Consultants" msgstr "Consultants" -#: staffing/utils.py:285 staffing/views.py:1055 staffing/views.py:1534 +#: staffing/utils.py:285 staffing/views.py:1046 staffing/views.py:1525 #: templates/billing/pre_billing.html:92 templates/billing/pre_billing.html:100 #: templates/billing/pre_billing.html:159 #: templates/billing/pre_billing.html:167 templates/staffing/pdc_detail.html:19 @@ -2610,11 +2610,11 @@ msgstr "La charge doit être un multiple (%s)" msgid "Charge cannot exceed forecast (%s)" msgstr "La charge ne peut pas excéder la prévision (%s)" -#: staffing/views.py:212 staffing/views.py:256 +#: staffing/views.py:204 staffing/views.py:248 msgid "Duplicate data error" msgstr "Erreur de donn es en doublon" -#: staffing/views.py:318 +#: staffing/views.py:311 #, python-format msgid "" "Staffing has been ignored for mission %(mission_name)s because " @@ -2624,7 +2624,7 @@ msgstr "" "date prévue %(staffing_date)s est antérieure à la fin de la mission " "(%(start_date)s)" -#: staffing/views.py:325 +#: staffing/views.py:318 #, python-format msgid "" "Staffing has been ignored for mission %(mission_name)s because " @@ -2634,29 +2634,29 @@ msgstr "" "date prévue %(staffing_date)s est postérieure à la fin de la mission " "(%(end_date)s)" -#: staffing/views.py:343 +#: staffing/views.py:336 msgid "Staffing has been updated" msgstr "Les prévisions de charge ont été mises à jour" -#: staffing/views.py:381 +#: staffing/views.py:374 #, python-format msgid "Staffing has been shifted by %s month" msgstr "Les prévisions de charge ont été décalées de %s mois" -#: staffing/views.py:405 +#: staffing/views.py:398 msgid "Only won leads" msgstr "Uniquement les affaires gagnées" -#: staffing/views.py:405 +#: staffing/views.py:398 msgid "Only consider won leads for staffing forecasting" msgstr "" "Ne prend en compte que les missions gagnées pour les prévisions de charge" -#: staffing/views.py:406 +#: staffing/views.py:399 msgid "Balanced staffing projection" msgstr "Projections de staffing pondérées" -#: staffing/views.py:406 +#: staffing/views.py:399 msgid "" "Add missions forcecast staffing even if still not won with a ponderation " "based on the mission won probability" @@ -2664,11 +2664,11 @@ msgstr "" "Ajoute les prévisions de charge des missions même si elles ne sont pas " "gagnées en pondérant avec la probabilité de gagner." -#: staffing/views.py:407 +#: staffing/views.py:400 msgid "Full staffing projection" msgstr "Projections de staffing complètes" -#: staffing/views.py:407 +#: staffing/views.py:400 msgid "" "Add missions forcecast staffing even if still not won without any " "ponderation. All forecast is considered." @@ -2676,31 +2676,39 @@ msgstr "" "Ajoute les prévisions de charge des missions même si elles ne sont pas " "gagnées sans aucun pondération. Toutes les prévisions sont prises en compte." -#: staffing/views.py:410 +#: staffing/views.py:403 msgid "Group by Manager" msgstr "Groupement par manager" -#: staffing/views.py:411 +#: staffing/views.py:404 msgid "Group by Level" msgstr "Groupement par position" -#: staffing/views.py:794 templates/staffing/prod_report.html:119 +#: staffing/views.py:787 templates/staffing/prod_report.html:119 msgid "Prod rate delta" msgstr "Écart de taux de prod." -#: staffing/views.py:795 templates/staffing/prod_report.html:120 +#: staffing/views.py:788 templates/staffing/prod_report.html:120 msgid "Daily rate delta" msgstr "Écart de taux journalier" -#: staffing/views.py:813 +#: staffing/views.py:806 msgid "Missing timesheet" msgstr "Pointages manquants" -#: staffing/views.py:1044 staffing/views.py:1392 staffing/views.py:1415 +#: staffing/views.py:867 +msgid "mission archived" +msgstr "mission archivée" + +#: staffing/views.py:869 +msgid "mission not found" +msgstr "mission inexistante" + +#: staffing/views.py:1035 staffing/views.py:1383 staffing/views.py:1406 msgid "timesheet.csv" msgstr "feuille-de-temps.csv" -#: staffing/views.py:1354 templates/billing/_lead_billing.html:66 +#: staffing/views.py:1345 templates/billing/_lead_billing.html:66 #: templates/billing/bill.html:179 templates/leads/lead_detail.html:256 #: templates/staffing/mission_timesheet.html:38 #: templates/staffing/mission_timesheet.html:117 @@ -2708,61 +2716,61 @@ msgstr "feuille-de-temps.csv" msgid "Total" msgstr "Total" -#: staffing/views.py:1428 +#: staffing/views.py:1419 msgid "Lead Price (k€)" msgstr "Montant affaire (k€)" -#: staffing/views.py:1428 +#: staffing/views.py:1419 msgid "Mission Price (k€)" msgstr "Montant mission (k€)" -#: staffing/views.py:1429 +#: staffing/views.py:1420 msgid "Past done days" msgstr "Jours réalisés dans le passé" -#: staffing/views.py:1429 templates/billing/bill_review.html:211 -#: templates/leads/lead_detail.html:229 templates/staffing/mission.html:165 +#: staffing/views.py:1420 templates/billing/bill_review.html:211 +#: templates/leads/lead_detail.html:229 templates/staffing/mission.html:166 #: templates/staffing/mission_timesheet.html:12 msgid "Done days" msgstr "Jours réalisés" -#: staffing/views.py:1429 templates/staffing/mission_timesheet.html:14 +#: staffing/views.py:1420 templates/staffing/mission_timesheet.html:14 msgid "Days to be done" msgstr "Jours à faire" -#: staffing/views.py:1480 +#: staffing/views.py:1471 msgid "holidays_timesheet.csv" msgstr "feuille-de-temps-congés.csv" -#: staffing/views.py:1534 +#: staffing/views.py:1525 msgid "trigramme" msgstr "trigramme" -#: staffing/views.py:1534 staffing/views.py:1733 +#: staffing/views.py:1525 staffing/views.py:1724 msgid "profil" msgstr "profil" -#: staffing/views.py:1534 +#: staffing/views.py:1525 msgid "start" msgstr "début" -#: staffing/views.py:1534 +#: staffing/views.py:1525 msgid "end" msgstr "fin" -#: staffing/views.py:1625 +#: staffing/views.py:1616 msgid "current" msgstr "actuel" -#: staffing/views.py:1625 +#: staffing/views.py:1616 msgid "next" msgstr "prochain" -#: staffing/views.py:1632 templates/staffing/rate_objective_report.html:41 +#: staffing/views.py:1623 templates/staffing/rate_objective_report.html:41 msgid "horizon" msgstr "horizon" -#: staffing/views.py:1734 staffing/views.py:2077 +#: staffing/views.py:1725 staffing/views.py:2068 #: templates/billing/pre_billing.html:92 templates/billing/pre_billing.html:159 #: templates/staffing/missions_report.html:67 #: templates/staffing/missions_report.html:73 @@ -2770,37 +2778,37 @@ msgstr "horizon" msgid "days" msgstr "jours" -#: staffing/views.py:1761 +#: staffing/views.py:1752 msgid "This lead has no mission defined" msgstr "Cette affaire n'a aucune mission définie" -#: staffing/views.py:1788 +#: staffing/views.py:1779 msgid "You are not allowed to do that" msgstr "Vous n'êtes pas autorisé à faire cela" -#: staffing/views.py:1797 +#: staffing/views.py:1788 #, python-brace-format msgid "daily rate for {consultant}" msgstr "Taux journalier pour {consultant}" -#: staffing/views.py:1800 +#: staffing/views.py:1791 #, python-brace-format msgid "bought daily rate for {consultant}" msgstr "Taux journalier d'achat pour {consultant}" -#: staffing/views.py:1809 +#: staffing/views.py:1800 msgid "Mission or consultant does not exist" msgstr "La mission ou le consultant n'existe pas" -#: staffing/views.py:1943 +#: staffing/views.py:1934 msgid "Predefined assignment must be in consultant list" msgstr "Les affectations prédéfinis doivent être dans la liste des consultants" -#: staffing/views.py:1947 +#: staffing/views.py:1938 msgid "Excluded consultant must be in consultant list" msgstr "Les affectations exclus être dans la liste des consultants" -#: staffing/views.py:1968 +#: staffing/views.py:1959 msgid "" "There's no solution. Add consultants, remove mission, exclusions or relax " "experience ratio constraint" @@ -2808,18 +2816,18 @@ msgstr "" "Aucune solution. Ajoutez des consultants, retirez des missions, des " "exclusions ou relâchez les contraintes de ratio d'expérience." -#: staffing/views.py:2059 templates/staffing/mission.html:104 +#: staffing/views.py:2050 templates/staffing/mission.html:105 #: templates/staffing/turnover_pivotable.html:101 #: templates/staffing/turnover_pivotable.html:118 #: templates/staffing/turnover_pivotable.html:125 msgid "Marketing product" msgstr "Produit marketing" -#: staffing/views.py:2059 staffing/views.py:2060 +#: staffing/views.py:2050 staffing/views.py:2051 msgid "Undefined" msgstr "À définir" -#: staffing/views.py:2076 staffing/views.py:2087 +#: staffing/views.py:2067 staffing/views.py:2078 #: templates/billing/graph_yearly_billing.html:37 #: templates/staffing/turnover_pivotable.html:91 #: templates/staffing/turnover_pivotable.html:97 @@ -2830,23 +2838,23 @@ msgstr "À définir" msgid "turnover (€)" msgstr "Chiffre d'affaire (€)" -#: staffing/views.py:2078 +#: staffing/views.py:2069 msgid "external subcontractor turnover (€)" msgstr "CA sous-traitants externes (€)" -#: staffing/views.py:2079 +#: staffing/views.py:2070 msgid "external subcontractor days" msgstr "jours sous-traitants externes" -#: staffing/views.py:2080 +#: staffing/views.py:2071 msgid "internal subcontractor turnover (€)" msgstr "CA sous-traitants internes (€)" -#: staffing/views.py:2081 +#: staffing/views.py:2072 msgid "internal subcontractor days" msgstr "jours sous-traitants internes" -#: staffing/views.py:2082 staffing/views.py:2097 +#: staffing/views.py:2073 staffing/views.py:2088 #: templates/staffing/turnover_pivotable.html:61 #: templates/staffing/turnover_pivotable.html:67 #: templates/staffing/turnover_pivotable.html:75 @@ -2855,39 +2863,39 @@ msgstr "jours sous-traitants internes" msgid "own turnover (€)" msgstr "Chiffre d'affaire propre (€)" -#: staffing/views.py:2083 +#: staffing/views.py:2074 msgid "own days" msgstr "Jours effectif propre" -#: staffing/views.py:2085 staffing/views.py:2093 +#: staffing/views.py:2076 staffing/views.py:2084 msgid "fiscal year" msgstr "Année fiscale" -#: staffing/views.py:2104 staffing/views.py:2106 +#: staffing/views.py:2095 staffing/views.py:2097 #: templates/staffing/turnover_pivotable.html:87 msgid "top client company" msgstr "top entreprise" -#: staffing/views.py:2106 +#: staffing/views.py:2097 msgid "others" msgstr "autres" -#: staffing/views.py:2145 staffing/views.py:2147 +#: staffing/views.py:2136 staffing/views.py:2138 #: templates/staffing/lunch_tickets_pivotable.html:59 msgid "days off previous month" msgstr "jours de congés du mois précédent" -#: staffing/views.py:2146 staffing/views.py:2147 +#: staffing/views.py:2137 staffing/views.py:2138 #: templates/staffing/lunch_tickets_pivotable.html:65 msgid "days without tickets previous month" msgstr "jours sans ticket restaurant le mois précédent" -#: staffing/views.py:2147 templates/staffing/lunch_tickets_pivotable.html:47 +#: staffing/views.py:2138 templates/staffing/lunch_tickets_pivotable.html:47 #: templates/staffing/lunch_tickets_pivotable.html:53 msgid "deserved tickets" msgstr "tickets mérités" -#: staffing/views.py:2301 +#: staffing/views.py:2292 msgid "Global" msgstr "Global" @@ -4142,7 +4150,7 @@ msgstr "Ajouter ou modifier un client" #: templates/crm/clientcompany_detail.html:253 #: templates/expense/_make_vat_editable.html:8 #: templates/leads/lead_detail.html:44 templates/leads/lead_detail.html:46 -#: templates/staffing/mission.html:217 +#: templates/staffing/mission.html:218 #: templates/staffing/mission_consultants.html:37 msgid "click to edit..." msgstr "Cliquez pour modifier..." @@ -4625,7 +4633,7 @@ msgstr "Rien" msgid "Profitability" msgstr "Rentabilité" -#: templates/leads/lead_detail.html:194 templates/staffing/mission.html:121 +#: templates/leads/lead_detail.html:194 templates/staffing/mission.html:122 #: templates/staffing/mission_timesheet.html:88 msgid "Sold" msgstr "Vendu" @@ -4660,23 +4668,23 @@ msgstr "Affaires similaires" msgid "Missions of this lead:" msgstr "Missions de cette affaire : " -#: templates/leads/lead_detail.html:228 templates/staffing/mission.html:164 +#: templates/leads/lead_detail.html:228 templates/staffing/mission.html:165 msgid "id" msgstr "n°" -#: templates/leads/lead_detail.html:230 templates/staffing/mission.html:166 +#: templates/leads/lead_detail.html:230 templates/staffing/mission.html:167 msgid "Done work (k€)" msgstr "Montant réalisé (k€)" #: templates/leads/lead_detail.html:231 #: templates/staffing/fixed_price_report.html:19 -#: templates/staffing/mission.html:167 +#: templates/staffing/mission.html:168 msgid "Sold (k€)" msgstr "Vendu (k€)" #: templates/leads/lead_detail.html:233 #: templates/staffing/_mission_table.html:23 -#: templates/staffing/mission.html:169 +#: templates/staffing/mission.html:170 msgid "product" msgstr "produit" @@ -5049,11 +5057,6 @@ msgstr "Afficher uniquement les missions actives" msgid "Display all missions" msgstr "Afficher toutes les missions" -#: templates/staffing/_mission_table.html:43 -#: templates/staffing/mission.html:232 -msgid "Archiving failed" -msgstr "Échec de l'archivage" - #: templates/staffing/_mission_table_archive_column.html:11 msgid "Archive" msgstr "Archiver" @@ -5301,43 +5304,43 @@ msgstr "Cette mission n'a plus de staffing défini dans le futur" msgid "Still to be billed" msgstr "Reste à facturer" -#: templates/staffing/mission.html:63 +#: templates/staffing/mission.html:64 msgid "Archive this mission" msgstr "Archiver cette mission" -#: templates/staffing/mission.html:70 +#: templates/staffing/mission.html:71 msgid "Staffing has not been updated recently" msgstr "Les prévisions n'ont pas été mises à jour récemment" -#: templates/staffing/mission.html:77 +#: templates/staffing/mission.html:78 msgid "Marketing product is not yet defined" msgstr "Le produit marketing de cette mission n'est pas défini" -#: templates/staffing/mission.html:81 +#: templates/staffing/mission.html:82 msgid "This mission is archived" msgstr "Cette mission est archivée" -#: templates/staffing/mission.html:108 +#: templates/staffing/mission.html:109 msgid "Analytic code" msgstr "Code analytique" -#: templates/staffing/mission.html:113 templates/staffing/mission.html:117 +#: templates/staffing/mission.html:114 templates/staffing/mission.html:118 msgid "Client lead id" msgstr "Référence affaire client" -#: templates/staffing/mission.html:130 +#: templates/staffing/mission.html:131 msgid "Probability" msgstr "Probabilité" -#: templates/staffing/mission.html:133 +#: templates/staffing/mission.html:134 msgid "Lead of this mission" msgstr "Affaire de cette mission" -#: templates/staffing/mission.html:140 +#: templates/staffing/mission.html:141 msgid "Lead's description" msgstr "Description de l'affaire" -#: templates/staffing/mission.html:159 +#: templates/staffing/mission.html:160 msgid "Other missions linked to this lead" msgstr "Autres missions liées à cette affaire" @@ -5711,6 +5714,9 @@ msgstr "Proportion de sous traitance par produit" msgid "Sum ratio" msgstr "Ratio de sommes" +#~ msgid "Archiving failed" +#~ msgstr "Échec de l'archivage" + #~ msgid "expert" #~ msgstr "expert" From 762079ef568c3afaf0db6887b73e78bccb244292 Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Sat, 12 Oct 2024 18:03:24 +0200 Subject: [PATCH 03/29] switch expense workflow transitions UI to htmx. Add transitions to expense detail page --- expense/tables.py | 40 +++++++----------- expense/views.py | 41 ++++++++----------- .../expense/_expense_transitions_column.html | 25 +++++++++++ templates/expense/expense.html | 4 ++ 4 files changed, 59 insertions(+), 51 deletions(-) create mode 100644 templates/expense/_expense_transitions_column.html diff --git a/expense/tables.py b/expense/tables.py index a41207c8..10317a05 100644 --- a/expense/tables.py +++ b/expense/tables.py @@ -6,10 +6,8 @@ """ from django.utils.translation import gettext as _ -from django.urls import reverse from django.db.models import Q from django.utils.safestring import mark_safe -from django.utils.encoding import smart_str from django.template import Template, RequestContext from django.template.loader import get_template from django_datatables_view.base_datatable_view import BaseDatatableView @@ -22,7 +20,7 @@ from core.templatetags.pydici_filters import link_to_consultant from core.utils import TABLES2_HIDE_COL_MD, to_int_or_round from core.decorator import PydiciFeatureMixin, PydiciNonPublicdMixin, PydiciSubcontractordMixin -from expense.utils import expense_transition_to_state_display, user_expense_perm +from expense.utils import expense_transition_to_state_display, user_expense_perm, can_edit_expense, expense_next_states class ExpenseTableDT(PydiciSubcontractordMixin, PydiciFeatureMixin, BaseDatatableView): @@ -126,6 +124,7 @@ class ExpenseTable(tables.Table): expense_date = tables.TemplateColumn("""{{ record.expense_date }}""") # Title attr is just used to have an easy to parse hidden value for sorting update_date = tables.TemplateColumn("""{{ record.update_date }}""", attrs=TABLES2_HIDE_COL_MD) # Title attr is just used to have an easy to parse hidden value for sorting vat = tables.TemplateColumn("""{% load l10n %}
{{record.vat}}
""") + transitions_template = get_template("expense/_expense_transitions_column.html") def render_user(self, value): return link_to_consultant(value) @@ -142,35 +141,18 @@ class Meta: class ExpenseWorkflowTable(ExpenseTable): transitions = tables.Column(accessor="pk") - def render_transitions(self, record): - result = [] - for transition in self.transitionsData[record.id]: - result.append("""%s""" - % (expense_transition_to_state_display(transition), reverse("expense:update_expense_state", args=[record.id, transition]), expense_transition_to_state_display(transition)[0:2])) - if self.expenseEditPerm[record.id]: - result.append("%s" - % (smart_str(_("Edit")), - reverse("expense:expenses", kwargs={"expense_id": record.id}), - # Translators: Ed is the short term for Edit - smart_str(_("Ed")))) - result.append("%s" % - (smart_str(_("Delete")), - reverse("expense:expense_delete", kwargs={"expense_id": record.id}), - # Translators: De is the short term for Delete - smart_str(_("De")))) - result.append("%s" % - (smart_str(_("Clone")), - reverse("expense:clone_expense", kwargs={"clone_from": record.id}), - # Translators: Cl is the short term for Clone - smart_str(_("Cl")))) - return mark_safe(" ".join(result)) - class Meta: sequence = ("id", "user", "description", "lead", "amount", "chargeable", "corporate_card", "receipt", "state", "transitions", "expense_date", "update_date", "comment") fields = sequence class UserExpenseWorkflowTable(ExpenseWorkflowTable): + def render_transitions(self, record): + return self.transitions_template.render(context={"record": record, + "transitions": [], + "expense_edit_perm": can_edit_expense(record, self.request.user)}, + request=self.request) + class Meta: attrs = {"class": "pydici-tables2 table table-hover table-striped table-sm", "id": "user_expense_workflow_table"} prefix = "user_expense_workflow_table" @@ -180,6 +162,12 @@ class Meta: class ManagedExpenseWorkflowTable(ExpenseWorkflowTable): description = tables.TemplateColumn("""{% load l10n %} {{ record.description }}""", attrs={"td": {"class": "description"}}) + def render_transitions(self, record): + transitions = [(t, expense_transition_to_state_display(t), expense_transition_to_state_display(t)[0:2]) for t in expense_next_states(record, self.request.user)] + return self.transitions_template.render(context={"record": record, + "transitions": transitions, + "expense_edit_perm": can_edit_expense(record, self.request.user)}, + request=self.request) class Meta: attrs = {"class": "pydici-tables2 table table-hover table-striped table-sm", "id": "managed_expense_workflow_table"} prefix = "managed_expense_workflow_table" diff --git a/expense/views.py b/expense/views.py index 4499d5e2..4c5b39bc 100644 --- a/expense/views.py +++ b/expense/views.py @@ -6,7 +6,6 @@ """ from datetime import date -import json from io import BytesIO import decimal @@ -25,7 +24,7 @@ from leads.models import Lead from people.models import Consultant from core.decorator import pydici_non_public, pydici_feature, pydici_subcontractor -from expense.utils import expense_next_states, can_edit_expense, in_terminal_state, user_expense_perm +from expense.utils import expense_next_states, can_edit_expense, in_terminal_state, user_expense_perm, expense_transition_to_state_display from people.utils import users_are_in_same_company from crm.utils import get_subsidiary_from_session @@ -53,10 +52,14 @@ def expense(request, expense_id): (expense_subsidiary_manager and users_are_in_same_company(expense.user, request.user))): return HttpResponseRedirect(reverse("core:forbidden")) + transitions = [(t, expense_transition_to_state_display(t), expense_transition_to_state_display(t)[0:2]) for t in + expense_next_states(expense, request.user)] + return render(request, "expense/expense.html", {"expense": expense, "can_edit": can_edit_expense(expense, request.user), "can_edit_vat": expense_administrator or expense_paymaster, + "transitions": transitions, "user": request.user}) @@ -137,13 +140,9 @@ def expenses(request, expense_id=None, clone_from=None): managed_expenses = team_expenses userExpenseTable = UserExpenseWorkflowTable(user_expenses) - userExpenseTable.transitionsData = dict([(e.id, []) for e in user_expenses]) # Inject expense allowed transitions. Always empty for own expense - userExpenseTable.expenseEditPerm = dict([(e.id, can_edit_expense(e, request.user)) for e in user_expenses]) # Inject expense edit permissions RequestConfig(request, paginate={"per_page": 50}).configure(userExpenseTable) managedExpenseTable = ManagedExpenseWorkflowTable(managed_expenses) - managedExpenseTable.transitionsData = dict([(e.id, expense_next_states(e, request.user)) for e in managed_expenses]) # Inject expense allowed transitions - managedExpenseTable.expenseEditPerm = dict([(e.id, can_edit_expense(e, request.user)) for e in managed_expenses]) # Inject expense edit permissions RequestConfig(request, paginate={"per_page": 100}).configure(managedExpenseTable) return render(request, "expense/expenses.html", {"user_expense_table": userExpenseTable, @@ -253,32 +252,24 @@ def chargeable_expenses(request): @pydici_feature("reports") def update_expense_state(request, expense_id, target_state): """Do workflow transition for that expense.""" - error = False message = "" try: expense = Expense.objects.get(id=expense_id) except Expense.DoesNotExist: - message = _("Expense %s does not exist" % expense_id) - error = True - - if not error: - next_states = expense_next_states(expense, request.user) - if target_state in next_states: - expense.state = target_state - if in_terminal_state(expense): - expense.workflow_in_progress = False - expense.save() - message = _("Successfully update expense") - else: - message = ("Transition %s is not allowed" % target_state) - error = True + return HttpResponse(_("Expense %s does not exist" % expense_id), error_code=404) - response = {"message": message, - "expense_id": expense_id, - "error": error} + next_states = expense_next_states(expense, request.user) + if target_state in next_states: + expense.state = target_state + if in_terminal_state(expense): + expense.workflow_in_progress = False + expense.save() + message = _("Successfully update expense") + else: + message = ("Transition %s is not allowed" % target_state) - return HttpResponse(json.dumps(response), content_type="application/json") + return HttpResponse(message) @pydici_non_public diff --git a/templates/expense/_expense_transitions_column.html b/templates/expense/_expense_transitions_column.html new file mode 100644 index 00000000..ef76f689 --- /dev/null +++ b/templates/expense/_expense_transitions_column.html @@ -0,0 +1,25 @@ +{% load i18n %} +{% load l10n %} +{# context : record (expense object) #} +
+{% for transition, transition_label, transition_short_label in transitions %} + + {{ transition_short_label }} + +{% endfor %} + +{% if expense_edit_perm %} + + {% trans "Ed" %} + + + {% trans "De" %} + +{% endif %} + + + {% trans "Cl" %} + +
\ No newline at end of file diff --git a/templates/expense/expense.html b/templates/expense/expense.html index 14026c5b..ce26bf41 100644 --- a/templates/expense/expense.html +++ b/templates/expense/expense.html @@ -88,6 +88,10 @@

{% trans "No" %} {% endif %} + + {% trans "Transitions" %} + {% include "expense/_expense_transitions_column.html" with record=expense %} +
From 38a115f5af2977e1846edb594aec36fb4ccc8169 Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Sat, 12 Oct 2024 18:56:36 +0200 Subject: [PATCH 04/29] display workflow next states and update status instead of just displaying a success messsage --- expense/views.py | 10 +++++++--- templates/expense/_expense_state_column.html | 6 ++++-- .../expense/_expense_transitions_column.html | 5 +++++ templates/expense/expense.html | 2 +- templates/expense/expenses.html | 16 ---------------- 5 files changed, 17 insertions(+), 22 deletions(-) diff --git a/expense/views.py b/expense/views.py index 4c5b39bc..10dcdb75 100644 --- a/expense/views.py +++ b/expense/views.py @@ -265,11 +265,15 @@ def update_expense_state(request, expense_id, target_state): if in_terminal_state(expense): expense.workflow_in_progress = False expense.save() - message = _("Successfully update expense") + transitions = [(t, expense_transition_to_state_display(t), expense_transition_to_state_display(t)[0:2]) for t in + expense_next_states(expense, request.user)] + return render(request, "expense/_expense_transitions_column.html", + {"record": expense, + "transitions": transitions, + "expense_edit_perm": can_edit_expense(expense, request.user)}) else: message = ("Transition %s is not allowed" % target_state) - - return HttpResponse(message) + return HttpResponse(message) @pydici_non_public diff --git a/templates/expense/_expense_state_column.html b/templates/expense/_expense_state_column.html index f562105d..1d2017a2 100644 --- a/templates/expense/_expense_state_column.html +++ b/templates/expense/_expense_state_column.html @@ -1,8 +1,10 @@ {% load i18n %} +{% load l10n %} {# context : record (expense object) #} - +
{% if record.state == "PAID" %} {% trans "Paid" %} {% else %} {{ record.get_state_display }} -{% endif %} \ No newline at end of file +{% endif %} +
\ No newline at end of file diff --git a/templates/expense/_expense_transitions_column.html b/templates/expense/_expense_transitions_column.html index ef76f689..7e393aef 100644 --- a/templates/expense/_expense_transitions_column.html +++ b/templates/expense/_expense_transitions_column.html @@ -1,6 +1,11 @@ {% load i18n %} {% load l10n %} {# context : record (expense object) #} + +{# out of band swap for expense state #} + + +
{% for transition, transition_label, transition_short_label in transitions %} {% trans "State" %} - {{ expense.get_state_display }} +
{{ expense.get_state_display }}
{% trans "Comment" %} diff --git a/templates/expense/expenses.html b/templates/expense/expenses.html index 23f127f8..7b38a262 100644 --- a/templates/expense/expenses.html +++ b/templates/expense/expenses.html @@ -55,20 +55,4 @@

{% trans "Expenses I manage" %}

{% include "expense/_expense_receipt_modal.html" %} - - {% endblock %} \ No newline at end of file From 9d20cca9a96fb8fcb7bf620a4a3520ec2c825f2f Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Sat, 12 Oct 2024 22:14:13 +0200 Subject: [PATCH 05/29] handle properly error when expense file is not found --- expense/models.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/expense/models.py b/expense/models.py index d50c8d8a..a62f4036 100644 --- a/expense/models.py +++ b/expense/models.py @@ -136,14 +136,16 @@ def receipt_data(self): if self.receipt: content_type = self.receipt_content_type() data = BytesIO() - for chunk in self.receipt.chunks(): - data.write(chunk) - - data = b64encode(data.getvalue()).decode() - if content_type == "application/pdf": - response = "" % data - else: - response = "" % (content_type, data) + try: + for chunk in self.receipt.chunks(): + data.write(chunk) + data = b64encode(data.getvalue()).decode() + if content_type == "application/pdf": + response = "" % data + else: + response = "" % (content_type, data) + except FileNotFoundError: + response = "Expense file not found" return response @@ -151,7 +153,6 @@ def receipt_content_type(self): if self.receipt: return mimetypes.guess_type(self.receipt.name)[0] or "application/stream" - def get_absolute_url(self): return reverse('expense:expense', args=[str(self.id)]) From 0ebf8408dab5b960124bb5504b5cd4d1e25c2b43 Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Sun, 13 Oct 2024 11:47:47 +0200 Subject: [PATCH 06/29] upgrade to datatables 2.1.8 --- templates/core/_datatables.html | 12 ++++-------- templates/core/pydici.html | 5 +++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/templates/core/_datatables.html b/templates/core/_datatables.html index f957f918..f3ab35bd 100644 --- a/templates/core/_datatables.html +++ b/templates/core/_datatables.html @@ -12,8 +12,9 @@ "responsive": true, "fixedHeader": true, "ajax": "{{ data_url }}", - buttons: ['excel', 'csv'], - dom: "<'row'<'col-md-12'f>><'row'<'col-md-12'tr>><'row'<'col-md-3'l><'col-md-3'B><'col-md-6'p>>", + layout: { + topStart: ['pageLength', { buttons: [ 'copy', 'excel', 'pdf', 'csv' ]}], + }, {% if datatable_options %}{{ datatable_options|safe }}, {% endif %} "language": { "decimal": ",", @@ -25,12 +26,7 @@ "infoFiltered": "{% trans '(filtered from _MAX_ total records)' %}", "search": "{% trans 'Search:' %}", "lengthMenu": "{% trans 'Show _MENU_ entries' %}", - paginate:{ - first:'<<', - last: '>>', - next: '>', - previous: '<' - }, + }, }); }); diff --git a/templates/core/pydici.html b/templates/core/pydici.html index 078b7f45..2ba7d375 100644 --- a/templates/core/pydici.html +++ b/templates/core/pydici.html @@ -23,7 +23,8 @@ - + + {% block extracss %}{% endblock %} @@ -49,7 +50,7 @@ {% endif %} - + {% block extrajs %}{% endblock %} From 1630e3957195d5f13c606481a07d3ed490682c87 Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Sun, 13 Oct 2024 11:50:31 +0200 Subject: [PATCH 07/29] fnDrawCallback is deprecated --- expense/views.py | 2 +- staffing/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/expense/views.py b/expense/views.py index 10dcdb75..0318e020 100644 --- a/expense/views.py +++ b/expense/views.py @@ -212,7 +212,7 @@ def expenses_history(request): { className: "hidden-xs hidden-sm hidden-md", "targets": [2, 10, 12, 13]}, { className: "description", "targets": [3]}, { className: "amount", "targets": [5]}], - "fnDrawCallback": function( oSettings ) {make_vat_editable(); }''', + "drawCallback": function( oSettings ) {make_vat_editable(); }''', "can_edit_vat": expense_administrator or expense_paymaster, "user": request.user}) diff --git a/staffing/views.py b/staffing/views.py index bd76eb0f..a0186135 100644 --- a/staffing/views.py +++ b/staffing/views.py @@ -135,7 +135,7 @@ def missions(request, only_active=True): "datatable_options": ''' "columnDefs": [{ "orderable": false, "targets": [4, 8, 9] }, { className: "hidden-xs hidden-sm hidden-md", "targets": [6,7,8,9]}], "order": [[0, "asc"]], - "fnDrawCallback": function( oSettings ) {htmx.process(document.body); }''', + "drawCallback": function( oSettings ) {htmx.process(document.body); }''', "user": request.user}) @@ -277,7 +277,7 @@ def consultant_missions(request, only_active=True, consultant_id=None): "datatable_options": ''' "columnDefs": [{ "orderable": false, "targets": [4, 8, 9] }, { className: "hidden-xs hidden-sm hidden-md", "targets": [6,7,8,9]}], "order": [[3, "asc"]], - "fnDrawCallback": function( oSettings ) {htmx.process(document.body); } + "drawCallback": function( oSettings ) {htmx.process(document.body); } ''', "user": request.user}) From 4c81beffb7ecce4598e9a5b6fc2e185e0f591b5b Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Sun, 13 Oct 2024 16:34:52 +0200 Subject: [PATCH 08/29] switch from jeditable to htmx for VAT edit --- expense/forms.py | 12 +++++++- expense/tables.py | 13 ++++++-- expense/urls.py | 2 +- expense/views.py | 35 +++++++++++++--------- templates/expense/_expense_vat_column.html | 11 +++++++ templates/expense/_make_vat_editable.html | 18 ----------- templates/expense/expense.html | 12 +------- templates/expense/expense_archive.html | 5 ---- 8 files changed, 56 insertions(+), 52 deletions(-) create mode 100644 templates/expense/_expense_vat_column.html delete mode 100644 templates/expense/_make_vat_editable.html diff --git a/expense/forms.py b/expense/forms.py index 90bb0e12..115b8edc 100644 --- a/expense/forms.py +++ b/expense/forms.py @@ -51,6 +51,17 @@ def get_queryset(self): return expenses +class ExpenseVATForm(forms.ModelForm): + class Meta: + model = Expense + fields = ("vat",) + + def __init__(self, *args, **kwargs): + super(ExpenseVATForm, self).__init__(*args, **kwargs) + self.fields["vat"].label = False + self.fields["vat"].widget.attrs["autofocus"] = True + + class ExpenseForm(forms.ModelForm): """Expense form based on Expense model""" class Meta: @@ -60,7 +71,6 @@ class Meta: "comment": Textarea(attrs={'cols': 17, 'rows': 2}), # Reduce height and increase width } - def __init__(self, *args, **kwargs): subcontractor = kwargs.pop("subcontractor") super(ExpenseForm, self).__init__(*args, **kwargs) diff --git a/expense/tables.py b/expense/tables.py index 10317a05..1043cd21 100644 --- a/expense/tables.py +++ b/expense/tables.py @@ -35,11 +35,14 @@ class ExpenseTableDT(PydiciSubcontractordMixin, PydiciFeatureMixin, BaseDatatabl state_template = get_template("expense/_expense_state_column.html") ko_sign = mark_safe("""No""") ok_sign = mark_safe("""Yes""") + vat_template = get_template("expense/_expense_vat_column.html") def get_initial_queryset(self): expense_administrator, expense_subsidiary_manager, expense_manager, expense_paymaster, expense_requester = user_expense_perm(self.request.user) consultant = Consultant.objects.get(trigramme__iexact=self.request.user.username) + self.can_edit_vat = expense_administrator or expense_paymaster # Used for vat column render + if expense_subsidiary_manager: user_team = consultant.user_team(subsidiary=True) elif expense_manager: @@ -108,7 +111,7 @@ def render_column(self, row, column): elif column == "amount": return to_int_or_round(row.amount, 2) elif column == "vat": - return """
{1}
""".format(row.id, row.vat) + return self.vat_template.render(context={"expense": row, "can_edit_vat": self.can_edit_vat}, request=self.request) else: return super(ExpenseTableDT, self).render_column(row, column) @@ -123,12 +126,16 @@ class ExpenseTable(tables.Table): state = tables.TemplateColumn(template_name="expense/_expense_state_column.html", orderable=False) expense_date = tables.TemplateColumn("""{{ record.expense_date }}""") # Title attr is just used to have an easy to parse hidden value for sorting update_date = tables.TemplateColumn("""{{ record.update_date }}""", attrs=TABLES2_HIDE_COL_MD) # Title attr is just used to have an easy to parse hidden value for sorting - vat = tables.TemplateColumn("""{% load l10n %}
{{record.vat}}
""") transitions_template = get_template("expense/_expense_transitions_column.html") + vat_template = get_template("expense/_expense_vat_column.html") + def render_user(self, value): return link_to_consultant(value) + def render_vat(self, record): + return self.vat_template.render(context={"expense": record, "can_edit_vat": True}, request=self.request) + class Meta: model = Expense sequence = ("id", "user", "description", "lead", "amount", "vat", "chargeable", "corporate_card", "receipt", "state", "expense_date", "update_date", "comment") @@ -162,12 +169,14 @@ class Meta: class ManagedExpenseWorkflowTable(ExpenseWorkflowTable): description = tables.TemplateColumn("""{% load l10n %} {{ record.description }}""", attrs={"td": {"class": "description"}}) + def render_transitions(self, record): transitions = [(t, expense_transition_to_state_display(t), expense_transition_to_state_display(t)[0:2]) for t in expense_next_states(record, self.request.user)] return self.transitions_template.render(context={"record": record, "transitions": transitions, "expense_edit_perm": can_edit_expense(record, self.request.user)}, request=self.request) + class Meta: attrs = {"class": "pydici-tables2 table table-hover table-striped table-sm", "id": "managed_expense_workflow_table"} prefix = "managed_expense_workflow_table" diff --git a/expense/urls.py b/expense/urls.py index aaeced5d..fce650b0 100644 --- a/expense/urls.py +++ b/expense/urls.py @@ -14,8 +14,8 @@ re_path(r'^(?P\d+)/receipt$', v.expense_receipt, name="expense_receipt"), re_path(r'^(?P\d+)/delete$', v.expense_delete, name="expense_delete"), re_path(r'^(?P\d+)/change$', v.expenses, name="expenses"), + re_path(r'^(?P\d+)/expense_vat$', v.update_expense_vat, name="update_expense_vat"), re_path(r'^(?P\d+)/(?P\w+)$', v.update_expense_state, name="update_expense_state"), - re_path(r'^expense_vat$', v.update_expense_vat, name="update_expense_vat"), re_path(r'^clone/(?P\d+)$', v.expenses, name="clone_expense"), re_path(r'^mission/(?P\d+)$', v.lead_expenses, name="lead_expenses"), re_path(r'^history/?$', v.expenses_history, name="expenses_history"), diff --git a/expense/views.py b/expense/views.py index 0318e020..8c557d51 100644 --- a/expense/views.py +++ b/expense/views.py @@ -18,7 +18,7 @@ from django.shortcuts import render, redirect from django.contrib import messages -from expense.forms import ExpenseForm, ExpensePaymentForm +from expense.forms import ExpenseForm, ExpensePaymentForm, ExpenseVATForm from expense.models import Expense, ExpensePayment from expense.tables import ExpenseTable, UserExpenseWorkflowTable, ManagedExpenseWorkflowTable from leads.models import Lead @@ -212,7 +212,7 @@ def expenses_history(request): { className: "hidden-xs hidden-sm hidden-md", "targets": [2, 10, 12, 13]}, { className: "description", "targets": [3]}, { className: "amount", "targets": [5]}], - "drawCallback": function( oSettings ) {make_vat_editable(); }''', + "drawCallback": function( oSettings ) {htmx.process(document.body); }''', "can_edit_vat": expense_administrator or expense_paymaster, "user": request.user}) @@ -278,27 +278,34 @@ def update_expense_state(request, expense_id, target_state): @pydici_non_public @pydici_feature("management") -def update_expense_vat(request): +def update_expense_vat(request, expense_id): """Update expense VAT.""" - - expense_administrator, expense_subsidiary_manager, expense_manager, expense_paymaster, expense_requester = user_expense_perm(request.user) + expense_administrator, expense_subsidiary_manager, expense_manager, expense_paymaster, expense_requester = user_expense_perm( + request.user) if not (expense_administrator or expense_paymaster): return HttpResponseForbidden() try: - expense_id = request.POST["id"] - value = request.POST["value"].replace(",", ".") expense = Expense.objects.get(id=expense_id) - expense.vat = decimal.Decimal(value) - expense.save() - message = value except Expense.DoesNotExist: - message = _("Expense %s does not exist" % expense_id) - except (ValueError, decimal.InvalidOperation): - message = _("Incorrect value") + return HttpResponse(_("Expense does not exist"), error_code=404) - return HttpResponse(message) + if request.method == "GET": + form = ExpenseVATForm(instance=expense) + return HttpResponse("
%s
" % ( + reverse("expense:update_expense_vat", args=[expense_id]), form)) + + else: + form = ExpenseVATForm(request.POST, instance=expense) + if form.is_valid(): + form.save() + form = ExpenseVATForm(instance=expense) + return render(request, "expense/_expense_vat_column.html", + {"expense": expense, + "can_edit_vat": expense_administrator or expense_paymaster, + "form": form, + "user": request.user}) @pydici_non_public diff --git a/templates/expense/_expense_vat_column.html b/templates/expense/_expense_vat_column.html new file mode 100644 index 00000000..86cf1930 --- /dev/null +++ b/templates/expense/_expense_vat_column.html @@ -0,0 +1,11 @@ +{# display VAT as htmx editable field #} +{# context: expense object #} +{% load l10n %} +
+ {{ expense.vat }} + diff --git a/templates/expense/_make_vat_editable.html b/templates/expense/_make_vat_editable.html deleted file mode 100644 index 6495584c..00000000 --- a/templates/expense/_make_vat_editable.html +++ /dev/null @@ -1,18 +0,0 @@ -{# fragment to make vat editable through jquery jeditable #} -{% load i18n %} - - \ No newline at end of file diff --git a/templates/expense/expense.html b/templates/expense/expense.html index 9d883a13..9e1954ee 100644 --- a/templates/expense/expense.html +++ b/templates/expense/expense.html @@ -41,7 +41,7 @@

{% trans "VAT (€)" %} -
{{ expense.vat }}
+ {% include "expense/_expense_vat_column.html" %} {% trans "Creation date" %} @@ -105,14 +105,4 @@

{% include "core/_object_history.html" %} {% endwith %} - -{% if can_edit_vat %} - {% include "expense/_make_vat_editable.html" %} - -{% endif %} - {% endblock %} \ No newline at end of file diff --git a/templates/expense/expense_archive.html b/templates/expense/expense_archive.html index 61c9219a..2f320082 100644 --- a/templates/expense/expense_archive.html +++ b/templates/expense/expense_archive.html @@ -47,11 +47,6 @@

{% trans "Expenses archive" %}

- {% include "expense/_expense_receipt_modal.html" %} - -{% include "expense/_make_vat_editable.html" %} - - {% endblock %} \ No newline at end of file From c7cd2c4d5a4b8d2dd177712d6061d0ac898ead27 Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Sun, 13 Oct 2024 17:05:33 +0200 Subject: [PATCH 09/29] fix editable vat for expense payement --- expense/tables.py | 2 +- templates/expense/expense.html | 5 ----- templates/expense/expense_payment_detail.html | 13 ------------- templates/expense/expense_payments.html | 10 ---------- 4 files changed, 1 insertion(+), 29 deletions(-) diff --git a/expense/tables.py b/expense/tables.py index 1043cd21..34cabdcc 100644 --- a/expense/tables.py +++ b/expense/tables.py @@ -134,7 +134,7 @@ def render_user(self, value): return link_to_consultant(value) def render_vat(self, record): - return self.vat_template.render(context={"expense": record, "can_edit_vat": True}, request=self.request) + return self.vat_template.render(context={"expense": record, "can_edit_vat": True}) class Meta: model = Expense diff --git a/templates/expense/expense.html b/templates/expense/expense.html index 9e1954ee..347aac51 100644 --- a/templates/expense/expense.html +++ b/templates/expense/expense.html @@ -6,13 +6,8 @@ {% load render_table from django_tables2 %} {% load crispy_forms_tags %} -{% block extrajs %} - -{% endblock %} - {% block title %}{% trans "Expense" %}{% endblock %} - {% block content %}

diff --git a/templates/expense/expense_payment_detail.html b/templates/expense/expense_payment_detail.html index cbff19a7..d5d09b49 100644 --- a/templates/expense/expense_payment_detail.html +++ b/templates/expense/expense_payment_detail.html @@ -4,10 +4,6 @@ {% load pydici_filters %} {% load render_table from django_tables2 %} -{% block extrajs %} - -{% endblock %} - {% block extrastyle %} @@ -26,15 +22,6 @@

{% trans "Expenses of payment n°" %}{{ expense_payment.id }} ({{ expense_pa

{% trans "Total: " %} {{ expense_payment.amount }} €

{% endif %} -{% if can_edit_vat %} - {% include "expense/_make_vat_editable.html" %} - -{% endif %} - {% endblock %} \ No newline at end of file diff --git a/templates/expense/expense_payments.html b/templates/expense/expense_payments.html index ed358f88..3955648f 100644 --- a/templates/expense/expense_payments.html +++ b/templates/expense/expense_payments.html @@ -69,14 +69,4 @@

{% trans "Expenses payments" %}

{% include "core/_datepicker.html" %} -{% if can_edit_vat %} - {% include "expense/_make_vat_editable.html" %} - -{% endif %} - - {% endblock %} \ No newline at end of file From 4234411345df724e7464a23c22ef493a4d507496 Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Sun, 13 Oct 2024 17:10:42 +0200 Subject: [PATCH 10/29] remove unused jeditale imports --- templates/expense/expense_archive.html | 5 ----- templates/expense/expense_payments.html | 4 ---- 2 files changed, 9 deletions(-) diff --git a/templates/expense/expense_archive.html b/templates/expense/expense_archive.html index 2f320082..53b4d0eb 100644 --- a/templates/expense/expense_archive.html +++ b/templates/expense/expense_archive.html @@ -5,11 +5,6 @@ {% block title %}{% trans "Expenses history" %}{% endblock %} -{% block extrajs %} - -{% endblock %} - - {% block content %}
diff --git a/templates/expense/expense_payments.html b/templates/expense/expense_payments.html index 3955648f..66f9ba51 100644 --- a/templates/expense/expense_payments.html +++ b/templates/expense/expense_payments.html @@ -5,10 +5,6 @@ {% load render_table from django_tables2 %} {% load crispy_forms_tags %} -{% block extrajs %} - -{% endblock %} - {% block title %}{% trans "Expenses payments" %}{% endblock %} {% block content %} From ecb8a14564e0c547d730ba4b7aacae0b7d598b77 Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Sun, 13 Oct 2024 20:23:33 +0200 Subject: [PATCH 11/29] fix http error parameter --- expense/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/expense/views.py b/expense/views.py index 8c557d51..708ffefd 100644 --- a/expense/views.py +++ b/expense/views.py @@ -257,7 +257,7 @@ def update_expense_state(request, expense_id, target_state): try: expense = Expense.objects.get(id=expense_id) except Expense.DoesNotExist: - return HttpResponse(_("Expense %s does not exist" % expense_id), error_code=404) + return HttpResponse(_("Expense %s does not exist" % expense_id), status=404) next_states = expense_next_states(expense, request.user) if target_state in next_states: @@ -289,7 +289,7 @@ def update_expense_vat(request, expense_id): try: expense = Expense.objects.get(id=expense_id) except Expense.DoesNotExist: - return HttpResponse(_("Expense does not exist"), error_code=404) + return HttpResponse(_("Expense does not exist"), status=404) if request.method == "GET": form = ExpenseVATForm(instance=expense) From aecde613a46a0ef65c987d94ef65a3974edfc72d Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Sun, 13 Oct 2024 20:41:01 +0200 Subject: [PATCH 12/29] switch to htmx for rate inline edit --- staffing/urls.py | 2 +- staffing/utils.py | 14 ++++ staffing/views.py | 67 ++++++++++--------- .../staffing/_mission_consultants_rate.html | 25 +++++++ templates/staffing/mission_consultants.html | 36 ++-------- 5 files changed, 82 insertions(+), 62 deletions(-) create mode 100644 templates/staffing/_mission_consultants_rate.html diff --git a/staffing/urls.py b/staffing/urls.py index b214acd5..9ec52f2b 100644 --- a/staffing/urls.py +++ b/staffing/urls.py @@ -48,7 +48,7 @@ re_path(r'^non-prod_report/?$', v.missions_report, {"nature": "NONPROD"}, name="nonprod-pivotable"), re_path(r'^non-prod_report/all$', v.missions_report, {"nature": "NONPROD", "year": "all"}, name="nonprod-pivotable-all"), re_path(r'^contacts/mission/(?P\d+)/$', v.mission_contacts, name="mission_contacts"), - re_path(r'^rate/?$', v.mission_consultant_rate, name="mission_consultant_rate"), + re_path(r'^rate/mission/(?P\d+)/consultant/(?P\d+)/$', v.mission_consultant_rate, name="mission_consultant_rate"), re_path(r'^pdc-detail/(?P\d+)/(?P\d+)/?$', v.pdc_detail, name="pdc_detail"), re_path(r'^datatable/all-missions/data/$', t.MissionsTableDT.as_view(), name='all_mission_table_DT'), re_path(r'^datatable/consultant-all-missions/(?P\d+)/data/$', t.MissionsTableDT.as_view(), name='consultant_all_mission_table_DT'), diff --git a/staffing/utils.py b/staffing/utils.py index 26bc6914..b894f417 100644 --- a/staffing/utils.py +++ b/staffing/utils.py @@ -397,3 +397,17 @@ def check_timesheet_validity(missions, consultant, month): if limited_individual_mode_offending_missions: return _("Charge cannot exceed forecast (%s)") %\ ", ".join([str(m) for m in limited_individual_mode_offending_missions]) + + +def compute_mission_consultant_rates(mission): + """helper function to compute mission rates for each consultant for tab display and htmx edit form""" + rates = {} + objective_rates = mission.consultant_objective_rates() + for consultant, rate in mission.consultant_rates().items(): + rates[consultant] = (rate, objective_rates.get(consultant)) + try: + objective_dates = [i[0] for i in list(objective_rates.values())[0]] + except IndexError: + # No consultant or no objective on mission timeframe + objective_dates = [] + return objective_dates, rates diff --git a/staffing/views.py b/staffing/views.py index a0186135..4017df55 100644 --- a/staffing/views.py +++ b/staffing/views.py @@ -15,7 +15,7 @@ from django.core.cache import cache from django.shortcuts import render, redirect -from django.http import HttpResponseRedirect, HttpResponse, Http404 +from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponseForbidden from django.contrib.auth.decorators import permission_required from django.forms.models import inlineformset_factory from django.forms import formset_factory @@ -46,10 +46,10 @@ from core.utils import working_days, nextMonth, previousMonth, daysOfMonth, previousWeek, nextWeek, monthWeekNumber, \ to_int_or_round, COLORS, cumulateList, user_has_feature, get_parameter, \ get_fiscal_years_from_qs, get_fiscal_year -from core.decorator import pydici_non_public, pydici_feature, PydiciNonPublicdMixin, pydici_subcontractor +from core.decorator import pydici_non_public, pydici_feature, PydiciNonPublicdMixin from staffing.utils import gatherTimesheetData, saveTimesheetData, saveFormsetAndLog, \ sortMissions, holidayDays, staffingDates, time_string_for_day_percent, \ - timesheet_report_data, timesheet_report_data_grouped, check_timesheet_validity + timesheet_report_data, timesheet_report_data_grouped, check_timesheet_validity, compute_mission_consultant_rates from staffing.forms import MissionForm, OptimiserForm, MissionOptimiserForm, MissionOptimiserFormsetHelper from staffing.optim import solve_pdc, solver_solution_format, compute_consultant_freetime, compute_consultant_rates, solver_apply_forecast from staffing.optim import OPTIM_NEWBIE_SENIOR_LIMIT, OPTIM_SENIOR_DIRECTOR_LIMIT @@ -155,15 +155,7 @@ def mission_home(request, mission_id): @pydici_non_public def mission_consultants(request, mission_id): mission = Mission.objects.get(id=mission_id) - rates = {} - objective_rates = mission.consultant_objective_rates() - for consultant, rate in mission.consultant_rates().items(): - rates[consultant] = (rate, objective_rates.get(consultant)) - try: - objective_dates = [i[0] for i in list(objective_rates.values())[0]] - except IndexError: - # No consultant or no objective on mission timeframe - objective_dates = [] + objective_dates, rates = compute_mission_consultant_rates(mission) return render(request, "staffing/mission_consultants.html", {"mission": mission, "objective_dates": objective_dates, @@ -1537,7 +1529,6 @@ def holiday_csv_timesheet(request, year=None, month=None): return response - @pydici_non_public @pydici_feature("management") def holidays_planning(request, year=None, month=None): @@ -1732,7 +1723,6 @@ def missions_report(request, year=None, nature="HOLIDAYS"): "derivedAttributes": [],}) - @pydici_non_public @pydici_feature("leads") @permission_required("staffing.add_mission") @@ -1771,35 +1761,48 @@ def create_new_mission_from_lead(request, lead_id): @pydici_non_public -def mission_consultant_rate(request): - """Select or create financial condition for this consultant/mission tuple and update it - This is intended to be used through a jquery jeditable call""" +def mission_consultant_rate(request, mission_id, consultant_id): + """Select or create financial condition for this consultant/mission tuple and update it with htmx""" if not (request.user.has_perm("staffing.add_financialcondition") and - request.user.has_perm("staffing.change_financialcondition")): - return HttpResponse(_("You are not allowed to do that")) + request.user.has_perm("staffing.change_financialcondition")): + return HttpResponseForbidden() + try: - sold, mission_id, consultant_id = request.POST["id"].split("-") mission = Mission.objects.get(id=mission_id) consultant = Consultant.objects.get(id=consultant_id) condition, created = FinancialCondition.objects.get_or_create(mission=mission, consultant=consultant, defaults={"daily_rate": 0}) - value = escape(request.POST["value"].replace(" ", "")) - if sold == "sold": + except (Mission.DoesNotExist, Consultant.DoesNotExist): + return HttpResponse(_("Mission or consultant does not exist"), status=404) + + if request.method == "GET": + edit = True + else: + edit = False + change = None + if request.POST.get("sold"): + value = request.POST["sold"] change = {_(f"daily rate for {consultant}"): [condition.daily_rate, value]} condition.daily_rate = value - else: + + if request.POST.get("bought"): + value = request.POST["bought"] change = {_(f"bought daily rate for {consultant}"): [condition.daily_rate, value]} condition.bought_daily_rate = value - condition.save() - if mission.responsible: - compute_consultant_tasks.delay(mission.responsible.id) - LogEntry.objects.log_create(instance=mission, actor=request.user, action=LogEntry.Action.UPDATE, changes=json.dumps(change)) - return HttpResponse(value) - except (Mission.DoesNotExist, Consultant.DoesNotExist): - return HttpResponse(_("Mission or consultant does not exist")) - except ValueError: - return HttpResponse(_("Incorrect value")) + if change: + try: + condition.save() + cache.delete("Mission.consultant_rates%s" % mission.id) # flush rate cache + except ValueError: + return HttpResponse(status=400) + if mission.responsible: + compute_consultant_tasks.delay(mission.responsible.id) + LogEntry.objects.log_create(instance=mission, actor=request.user, action=LogEntry.Action.UPDATE, changes=json.dumps(change)) + + objective_dates, rates = compute_mission_consultant_rates(mission) + return render(request, "staffing/_mission_consultants_rate.html", {"mission": mission, "consultant": consultant, + "rate": rates[consultant], "edit": edit}) @pydici_non_public diff --git a/templates/staffing/_mission_consultants_rate.html b/templates/staffing/_mission_consultants_rate.html new file mode 100644 index 00000000..dfba404b --- /dev/null +++ b/templates/staffing/_mission_consultants_rate.html @@ -0,0 +1,25 @@ +{# fragment to display and allow inline edit of consultant rate for a mission #} +{# context: consultant, rate #} +{% load l10n %} +{% load i18n %} + + + {% include "people/__consultant_name.html" %} + {% if edit %} + + + {% if consultant.subcontractor %}{% endif %} + + {% else %} + {{ rate.0.0 }} + + {% if consultant.subcontractor %}{{ rate.0.1 }}{% endif %} + {% endif %} + {% with o_rates=rate.1 %} + {% for date, o_rate in o_rates %} + {{ o_rate|default_if_none:"-" }} + {% endfor %} + {% endwith %} + diff --git a/templates/staffing/mission_consultants.html b/templates/staffing/mission_consultants.html index caee2b32..9380f63a 100644 --- a/templates/staffing/mission_consultants.html +++ b/templates/staffing/mission_consultants.html @@ -5,6 +5,7 @@

{% trans "Consultants currently implicated in this mission" %}

+ @@ -13,38 +14,15 @@

{% trans "Consultants currently implicated in this mission" %}<

{% endfor %} + + {% for consultant, rate in rates.items %} - - - - - {% with o_rates=rate.1 %} - {% for date, o_rate in o_rates %} - - {% endfor %} - {% endwith %} - + {% include "staffing/_mission_consultants_rate.html" %} {% endfor %} +
{% trans "Consultant" %} {% trans "Daily rate (€)" %}{% blocktranslate with d=date|date:"M Y" %}Objective {{ d }} (€){% endblocktranslate %}
{% include "people/__consultant_name.html" %}
{{ rate.0.0|unlocalize }}
{% if consultant.subcontractor %}
{{ rate.0.1|unlocalize }}
{% endif %}
{{ o_rate|default_if_none:"-" }}
\ No newline at end of file + $(document).ready(function() { htmx.process(document.body); }); + From 3f2fa6c69812dd7411db938f9192cc5e9add2f9f Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Tue, 15 Oct 2024 08:56:40 +0200 Subject: [PATCH 13/29] partian tag banner switch to htmx --- leads/forms.py | 26 ++++++++++++- leads/models.py | 11 ++++++ leads/urls.py | 3 +- leads/views.py | 63 ++++++++++++++------------------ templates/leads/lead_detail.html | 30 +-------------- 5 files changed, 65 insertions(+), 68 deletions(-) diff --git a/leads/forms.py b/leads/forms.py index 0577aaa0..b4c3f658 100644 --- a/leads/forms.py +++ b/leads/forms.py @@ -12,11 +12,11 @@ from django.utils.encoding import smart_str from django import forms -from crispy_forms.layout import Layout, Div, Column, Fieldset, Field, HTML, Row +from crispy_forms.layout import Layout, Column, Fieldset, Field, HTML, Row from crispy_forms.bootstrap import AppendedText, TabHolder, Tab, FieldWithButtons from django_select2.forms import ModelSelect2Widget from taggit.forms import TagField - +from taggit.models import Tag from leads.models import Lead from people.models import Consultant, SalesMan @@ -66,6 +66,28 @@ def get_queryset(self): return qs.distinct() +class LeadTagChoices(PydiciSelect2WidgetMixin, ModelSelect2Widget): + model = Tag + search_fields = ["name__icontains"] + + def __init__(self, *args, **kwargs): + self.lead = kwargs.pop("lead", None) + super(LeadTagChoices, self).__init__(*args, **kwargs) + + def get_queryset(self): + qs = super(LeadTagChoices, self).get_queryset() + if self.lead: + qs = qs.exclude(lead__id=self.lead.id) # Exclude existing tags + return qs + + +class LeadTagForm(forms.Form): + def __init__(self, *args, **kwargs): + lead = kwargs.pop("lead", None) + super(LeadTagForm, self).__init__(*args, **kwargs) + self.fields["tag"] = forms.ModelChoiceField(widget=LeadTagChoices(lead=lead), queryset=Tag.objects.all(), label=False) + + class LeadForm(PydiciCrispyModelForm): class Meta: model = Lead diff --git a/leads/models.py b/leads/models.py index 3bfb5b8d..786fd536 100644 --- a/leads/models.py +++ b/leads/models.py @@ -286,6 +286,17 @@ def getDocURL(self): else: return "" + def suggested_tags(self): + """Find suggested tags for this lead except if it has already at least two tags""" + from leads.learn import predict_tags # Late import to avoid circular dependency + tags = self.tags.all() + if tags.count() < 3: + suggestedTags = set(predict_tags(self)) + suggestedTags -= set(tags) + else: + suggestedTags = [] + return suggestedTags + def get_absolute_url(self): return reverse('leads:detail', args=[str(self.id)]) diff --git a/leads/urls.py b/leads/urls.py index 9dbab8a3..0753f53a 100644 --- a/leads/urls.py +++ b/leads/urls.py @@ -14,7 +14,8 @@ re_path(r'^tag/(?P\d+)/$', v.tag, name="tag"), re_path(r'^tags/(?P\d+)$', v.tags, name="tags"), re_path(r'^tag/add$', v.add_tag, name="add_tag"), - re_path(r'^tag/remove/(?P\d+)/(?P\d+)$', v.remove_tag, name="remove_tag"), + re_path(r'^tag/add/(?P\d+)/(?P\d+)$', v.add_tag, name="add_tag"), + re_path(r'^tag/remove/(?P\d+)/(?P\d+)$', v.remove_tag, name="remove_tag"), re_path(r'^tag/manage$', v.manage_tags, name="manage_tags"), re_path(r'^(?P\d+)/$', v.detail, name="detail"), re_path(r'^leads$', v.leads, name="leads"), diff --git a/leads/views.py b/leads/views.py index b1421c15..8d02e786 100644 --- a/leads/views.py +++ b/leads/views.py @@ -29,10 +29,10 @@ from core.utils import sortedValues, COLORS, get_parameter, moving_average, nextMonth from crm.utils import get_subsidiary_from_session from leads.models import Lead -from leads.forms import LeadForm +from leads.forms import LeadForm, LeadTagForm from leads.utils import post_save_lead, leads_state_stat from leads.learn import compute_leads_state, compute_lead_similarity -from leads.learn import predict_tags, predict_similar +from leads.learn import predict_similar from core.utils import capitalize, getLeadDirs, createProjectTree, get_fiscal_years_from_qs, to_int_or_round from core.decorator import pydici_non_public, pydici_feature from people.models import Consultant @@ -88,15 +88,6 @@ def detail(request, lead_id): previous_lead = None active_count = None - # Find suggested tags for this lead except if it has already at least two tags - tags = lead.tags.all() - if tags.count() < 3: - suggestedTags = set(predict_tags(lead)) - suggestedTags -= set(tags) - else: - suggestedTags = [] - - return render(request, "leads/lead_detail.html", {"lead": lead, "active_count": active_count, @@ -105,9 +96,9 @@ def detail(request, lead_id): "previous_lead": previous_lead, "link_root": reverse("core:index"), "completion_url": reverse("leads:tags", args=[lead.id, ]), - "suggested_tags": suggestedTags, "similar_leads": predict_similar(lead), "enable_doc_tab": bool(settings.DOCUMENT_PROJECT_PATH), + "lead_tag_form": LeadTagForm(lead=lead), "user": request.user}) @pydici_non_public @@ -260,33 +251,32 @@ def tag(request, tag_id): @pydici_non_public @pydici_feature("leads") @permission_required("leads.change_lead") -def add_tag(request): - """Add a tag to a lead. Create the tag if needed""" - answer = {"tag_created": True, "tag_url": "", "tag_name": ""} - if request.POST["tag"]: - tagName = capitalize(request.POST["tag"]) - lead = Lead.objects.get(id=int(request.POST["lead_id"])) - if tagName in lead.tags.all().values_list("name", flat=True): - answer["tag_created"] = False - lead.tags.add(tagName) - if lead.state not in ("WON", "LOST", "FORGIVEN"): - compute_leads_state.delay(relearn=False, leads_id=[lead.id,]) # Update (in background) lead proba state as tag are used in computation - compute_lead_similarity.delay() # update lead similarity model in background - compute_consultant_tasks.delay(lead.responsible.id) # update consultants tasks in background - tag = Tag.objects.filter(name=tagName)[0] # We should have only one, but in case of bad data, just take the first one - answer["tag_url"] = reverse("leads:tag", args=[tag.id, ]) - answer["tag_remove_url"] = reverse("leads:remove_tag", args=[tag.id, lead.id]) - answer["tag_name"] = tag.name - answer["id"] = tag.id - return HttpResponse(json.dumps(answer), content_type="application/json") +def add_tag(request, lead_id, tag_id=None): + """Add a tag by id to a lead or create (through POST) a new one and attach it and return tag banner.""" + #TODO: handle tag creation + try: + lead = Lead.objects.get(id=lead_id) + if tag_id: + tag = Tag.objects.get(id=tag_id) + lead.tags.add(tag) + else: + return HttpResponse(_("No tag provided"), status=400) + except (Lead.DoesNotExist, Tag.DoesNotExist): + return Http404() + + if lead.state not in ("WON", "LOST", "FORGIVEN"): + compute_leads_state.delay(relearn=False, leads_id=[lead.id,]) # Update (in background) lead proba state as tag are used in computation + compute_lead_similarity.delay() # update lead similarity model in background + compute_consultant_tasks.delay(lead.responsible.id) # update consultants tasks in background + + return render(request, "leads/_tags_banner.html", {"lead": lead, "lead_tag_form": LeadTagForm(lead=lead)}) @pydici_non_public @pydici_feature("leads") @permission_required("leads.change_lead") -def remove_tag(request, tag_id, lead_id): - """Remove a tag to a lead""" - answer = {"error": False, "id": tag_id} +def remove_tag(request, lead_id, tag_id): + """Remove a tag to a lead and return tag banner""" try: tag = Tag.objects.get(id=tag_id) lead = Lead.objects.get(id=lead_id) @@ -295,8 +285,8 @@ def remove_tag(request, tag_id, lead_id): compute_leads_state.delay(relearn=False, leads_id=[lead.id, ]) # Update (in background) lead proba state as tag are used in computation compute_lead_similarity.delay() # update lead similarity model in background except (Tag.DoesNotExist, Lead.DoesNotExist): - answer["error"] = True - return HttpResponse(json.dumps(answer), content_type="application/json") + return Http404() + return render(request, "leads/_tags_banner.html", {"lead": lead, "lead_tag_form": LeadTagForm(lead=lead)}) @pydici_non_public @@ -329,6 +319,7 @@ def manage_tags(request): @pydici_feature("leads") def tags(request, lead_id): """@return: all tags that contains q parameter and are not already associated to this lead as a simple text list""" + #TODO: remove this function tags = Tag.objects.all().exclude(lead__id=lead_id) # Exclude existing tags tags = tags.filter(name__icontains=request.GET["term"]) tags = tags.values_list("name", flat=True) diff --git a/templates/leads/lead_detail.html b/templates/leads/lead_detail.html index e51166de..ca2c592b 100644 --- a/templates/leads/lead_detail.html +++ b/templates/leads/lead_detail.html @@ -60,35 +60,7 @@

-
-
- - {% if perms.leads.change_lead %} -
-
- -
-
- {% endif %} -
- {% if perms.leads.change_lead %} -
- {% if suggested_tags %} - {% trans "Suggested tags: " %} - {% for tag in suggested_tags %} - {{ tag }}   - {% endfor %} - {% endif %} -
- {% endif %} -
+ {% include "leads/_tags_banner.html" %}
{% with lead as lead %}{% include "leads/_lead_checkdoc.html" %}{% endwith %} From b3525558c7ec20cf77e7068a4941ffbad9ad9784 Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Thu, 17 Oct 2024 22:53:48 +0200 Subject: [PATCH 14/29] working tab banner with htmx. Still some polish to be done --- leads/forms.py | 24 ++++++++++++++++---- leads/urls.py | 2 +- leads/views.py | 28 ++++++++++++++--------- requirements.txt | 2 +- templates/leads/_tags_banner.html | 37 +++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 16 deletions(-) create mode 100644 templates/leads/_tags_banner.html diff --git a/leads/forms.py b/leads/forms.py index b4c3f658..41bcba67 100644 --- a/leads/forms.py +++ b/leads/forms.py @@ -14,7 +14,7 @@ from crispy_forms.layout import Layout, Column, Fieldset, Field, HTML, Row from crispy_forms.bootstrap import AppendedText, TabHolder, Tab, FieldWithButtons -from django_select2.forms import ModelSelect2Widget +from django_select2.forms import ModelSelect2Widget, ModelSelect2TagWidget from taggit.forms import TagField from taggit.models import Tag @@ -24,6 +24,7 @@ from people.forms import ConsultantChoices, ConsultantMChoices, SalesManChoices from crm.forms import ClientChoices, BusinessBrokerChoices from core.forms import PydiciCrispyModelForm, PydiciSelect2WidgetMixin +from core.utils import capitalize class LeadChoices(PydiciSelect2WidgetMixin, ModelSelect2Widget): @@ -66,9 +67,10 @@ def get_queryset(self): return qs.distinct() -class LeadTagChoices(PydiciSelect2WidgetMixin, ModelSelect2Widget): +class LeadTagChoices(PydiciSelect2WidgetMixin, ModelSelect2TagWidget): model = Tag search_fields = ["name__icontains"] + queryset = Tag.objects.all() def __init__(self, *args, **kwargs): self.lead = kwargs.pop("lead", None) @@ -80,12 +82,27 @@ def get_queryset(self): qs = qs.exclude(lead__id=self.lead.id) # Exclude existing tags return qs + def value_from_datadict(self, data, files, name): + """Create objects for given non-pimary-key values. Return list of all primary keys.""" + cleaned_values = [] + values = set(super().value_from_datadict(data, files, name)) + for value in values: + try: # Tag exists + cleaned_values.append(self.queryset.get(pk=int(value)).pk) + except (ValueError, TypeError) or self.model.DoesNotExist: + # We need to create it needed + tag, created = self.queryset.get_or_create(name=capitalize(value)) + cleaned_values.append(tag.pk) + + return cleaned_values + class LeadTagForm(forms.Form): + def __init__(self, *args, **kwargs): lead = kwargs.pop("lead", None) super(LeadTagForm, self).__init__(*args, **kwargs) - self.fields["tag"] = forms.ModelChoiceField(widget=LeadTagChoices(lead=lead), queryset=Tag.objects.all(), label=False) + self.fields["tag"] = forms.ModelMultipleChoiceField(widget=LeadTagChoices(lead=lead), queryset=Tag.objects, label=False) class LeadForm(PydiciCrispyModelForm): @@ -142,7 +159,6 @@ def clean_sales(self): # We can't tolerate that sale amount is not known at this step of the process raise ValidationError(_("Sales amount must be defined at this step of the commercial process")) - def clean_start_date(self): """Ensure start_date amount is defined at lead when commercial proposition has been sent""" if self.cleaned_data["start_date"] or self.data["state"] in ('QUALIF', 'WRITE_OFFER', 'SLEEPING', 'LOST', 'FORGIVEN'): diff --git a/leads/urls.py b/leads/urls.py index 0753f53a..379a9955 100644 --- a/leads/urls.py +++ b/leads/urls.py @@ -14,7 +14,7 @@ re_path(r'^tag/(?P\d+)/$', v.tag, name="tag"), re_path(r'^tags/(?P\d+)$', v.tags, name="tags"), re_path(r'^tag/add$', v.add_tag, name="add_tag"), - re_path(r'^tag/add/(?P\d+)/(?P\d+)$', v.add_tag, name="add_tag"), + re_path(r'^tag/add/(?P\d+)/(?P\d+)?$', v.add_tag, name="add_tag"), re_path(r'^tag/remove/(?P\d+)/(?P\d+)$', v.remove_tag, name="remove_tag"), re_path(r'^tag/manage$', v.manage_tags, name="manage_tags"), re_path(r'^(?P\d+)/$', v.detail, name="detail"), diff --git a/leads/views.py b/leads/views.py index 8d02e786..4f168081 100644 --- a/leads/views.py +++ b/leads/views.py @@ -33,7 +33,7 @@ from leads.utils import post_save_lead, leads_state_stat from leads.learn import compute_leads_state, compute_lead_similarity from leads.learn import predict_similar -from core.utils import capitalize, getLeadDirs, createProjectTree, get_fiscal_years_from_qs, to_int_or_round +from core.utils import getLeadDirs, createProjectTree, get_fiscal_years_from_qs, to_int_or_round from core.decorator import pydici_non_public, pydici_feature from people.models import Consultant from people.tasks import compute_consultant_tasks @@ -252,24 +252,32 @@ def tag(request, tag_id): @pydici_feature("leads") @permission_required("leads.change_lead") def add_tag(request, lead_id, tag_id=None): - """Add a tag by id to a lead or create (through POST) a new one and attach it and return tag banner.""" - #TODO: handle tag creation + """Add a tag by id (PUT) to a lead or create (through POST) a new one and attach it and return tag banner.""" try: lead = Lead.objects.get(id=lead_id) - if tag_id: + except Lead.DoesNotExist: + return Http404() + if request.method == "POST": + form = LeadTagForm(request.POST) + if form.is_valid(): + tags = form.cleaned_data["tag"] + lead.tags.add(*tags) + else: # Returns forms with errors + return render(request, "leads/_tags_banner.html", {"lead": lead, "lead_tag_form": form}) + elif tag_id: # PUT, we add an existing tag + try: tag = Tag.objects.get(id=tag_id) lead.tags.add(tag) - else: - return HttpResponse(_("No tag provided"), status=400) - except (Lead.DoesNotExist, Tag.DoesNotExist): - return Http404() + except Tag.DoesNotExist: + return HttpResponse(_("Invalid tag"), status=400) + else: + return HttpResponse(_("No tag provided"), status=400) if lead.state not in ("WON", "LOST", "FORGIVEN"): compute_leads_state.delay(relearn=False, leads_id=[lead.id,]) # Update (in background) lead proba state as tag are used in computation compute_lead_similarity.delay() # update lead similarity model in background compute_consultant_tasks.delay(lead.responsible.id) # update consultants tasks in background - - return render(request, "leads/_tags_banner.html", {"lead": lead, "lead_tag_form": LeadTagForm(lead=lead)}) + return render(request, "leads/_tags_banner.html", {"lead": lead, "lead_tag_form": LeadTagForm(lead=lead) }) @pydici_non_public diff --git a/requirements.txt b/requirements.txt index 3ffc9a13..cb0673ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ django-taggit==4.0 django-taggit-templatetags2==1.6.1 django-crispy-forms==2.0 crispy-bootstrap5==0.7 -Django-Select2==8.1.2 +Django-Select2==8.2.1 pymemcache==4.0 django-datatables-view==1.20.0 WeasyPrint==62.3 diff --git a/templates/leads/_tags_banner.html b/templates/leads/_tags_banner.html new file mode 100644 index 00000000..f3a0a6be --- /dev/null +++ b/templates/leads/_tags_banner.html @@ -0,0 +1,37 @@ +{# lead tag banner #} +{# Context/Argument: lead #} +{% load i18n %} +{% load l10n %} + +
+
+
+ {% for tag in lead.tags.all %} +
+ {{ tag }} + +   +
+ {% endfor %} +
+ {% if perms.leads.change_lead %} +
+
+
+
{{ lead_tag_form }}
+
+
+
+
+ + {% endif %} +
+ {% if perms.leads.change_lead %} + {% for tag in lead.suggested_tags %} + {% if forloop.first %}{% trans "Suggested tags: " %}{% endif %} + {{ tag }} + {% endfor %} + {% endif %} +
+ +{% include "core/_select2.html" %} From 20102d0d55a27aef3490d719769d4875ab414494 Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Thu, 17 Oct 2024 23:54:40 +0200 Subject: [PATCH 15/29] make it responsive. Only load select2 once to avoid flickering --- leads/forms.py | 3 ++- templates/leads/_tags_banner.html | 37 ++++++++++++++++--------------- templates/leads/lead_detail.html | 1 + 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/leads/forms.py b/leads/forms.py index 41bcba67..52c78f52 100644 --- a/leads/forms.py +++ b/leads/forms.py @@ -102,7 +102,8 @@ class LeadTagForm(forms.Form): def __init__(self, *args, **kwargs): lead = kwargs.pop("lead", None) super(LeadTagForm, self).__init__(*args, **kwargs) - self.fields["tag"] = forms.ModelMultipleChoiceField(widget=LeadTagChoices(lead=lead), queryset=Tag.objects, label=False) + self.fields["tag"] = forms.ModelMultipleChoiceField(widget=LeadTagChoices(lead=lead, attrs={"data-placeholder": _("New tags"), "style": "min-width: 200px;"}), + queryset=Tag.objects, label=False) class LeadForm(PydiciCrispyModelForm): diff --git a/templates/leads/_tags_banner.html b/templates/leads/_tags_banner.html index f3a0a6be..e7d1f414 100644 --- a/templates/leads/_tags_banner.html +++ b/templates/leads/_tags_banner.html @@ -4,26 +4,28 @@ {% load l10n %}
-
-
- {% for tag in lead.tags.all %} -
- {{ tag }} - -   +
+
+
+ {% for tag in lead.tags.all %} +
+ {{ tag }} + +   +
+ {% endfor %}
- {% endfor %}
- {% if perms.leads.change_lead %} -
-
-
-
{{ lead_tag_form }}
-
-
-
-
+ {% if perms.leads.change_lead %} +
+
+
+
{{ lead_tag_form }}
+
+
+
+
{% endif %}
{% if perms.leads.change_lead %} @@ -34,4 +36,3 @@ {% endif %}
-{% include "core/_select2.html" %} diff --git a/templates/leads/lead_detail.html b/templates/leads/lead_detail.html index ca2c592b..1ab21c62 100644 --- a/templates/leads/lead_detail.html +++ b/templates/leads/lead_detail.html @@ -10,6 +10,7 @@ {% include "core/_billboard.html" %} {% include "core/_pivotable_header.html" %} + {% endblock %} From ba127ed989c9d3900f37a6f512b63ef65cabb1f2 Mon Sep 17 00:00:00 2001 From: Sebastien Renard Date: Thu, 17 Oct 2024 23:58:32 +0200 Subject: [PATCH 16/29] remove useless javascript, youpi ! --- templates/leads/lead_detail.html | 48 -------------------------------- 1 file changed, 48 deletions(-) diff --git a/templates/leads/lead_detail.html b/templates/leads/lead_detail.html index 1ab21c62..7980b15a 100644 --- a/templates/leads/lead_detail.html +++ b/templates/leads/lead_detail.html @@ -262,54 +262,6 @@

{% trans "Administrative notes" %}