From 496b6d9c8bbe26b260bd4ea29906ea95ac26e1fe Mon Sep 17 00:00:00 2001 From: Karl Determe Date: Tue, 6 Oct 2026 10:04:54 +0200 Subject: [PATCH] feat: implement observations module with views, models, UI templates, translations, and migrations --- .../0018_setup_rbc_default_territory.py | 43 +++++ ...userthematics_can_edit_management_zones.py | 18 +++ loko/home/views.py | 1 + .../locale/en/LC_MESSAGES/django.mo | Bin 15020 -> 15077 bytes .../locale/fr/LC_MESSAGES/django.mo | Bin 15951 -> 16013 bytes .../locale/nl/LC_MESSAGES/django.mo | Bin 15411 -> 15472 bytes .../locale/en/LC_MESSAGES/django.mo | Bin 20425 -> 20487 bytes .../locale/en/LC_MESSAGES/django.po | 6 + .../locale/en/LC_MESSAGES/djangojs.mo | Bin 1683 -> 1745 bytes .../locale/en/LC_MESSAGES/djangojs.po | 5 + .../locale/fr/LC_MESSAGES/django.mo | Bin 21914 -> 21978 bytes .../locale/fr/LC_MESSAGES/django.po | 6 + .../locale/fr/LC_MESSAGES/djangojs.mo | Bin 1699 -> 1763 bytes .../locale/fr/LC_MESSAGES/djangojs.po | 5 + .../locale/nl/LC_MESSAGES/django.mo | Bin 21209 -> 21269 bytes .../locale/nl/LC_MESSAGES/django.po | 6 + .../locale/nl/LC_MESSAGES/djangojs.mo | Bin 1706 -> 1766 bytes .../locale/nl/LC_MESSAGES/djangojs.po | 5 + .../0003_alter_observation_status.py | 18 +++ loko/observations/models.py | 1 + .../static/observations/observations_index.js | 2 + .../_observations_list_mobile.html | 4 + .../observations/observation_detail.html | 91 ++++++++++- .../observation_detail_mobile.html | 87 +++++++++- .../observations_index_mobile.html | 17 ++ loko/observations/tests.py | 150 ++++++++++++++++++ loko/observations/views.py | 120 ++++++++++++-- .../reporting/observations_dashboard.html | 3 + 28 files changed, 570 insertions(+), 18 deletions(-) create mode 100644 loko/common/migrations/0018_setup_rbc_default_territory.py create mode 100644 loko/common/migrations/0019_userthematics_can_edit_management_zones.py create mode 100644 loko/observations/migrations/0003_alter_observation_status.py diff --git a/loko/common/migrations/0018_setup_rbc_default_territory.py b/loko/common/migrations/0018_setup_rbc_default_territory.py new file mode 100644 index 0000000..68e9d0c --- /dev/null +++ b/loko/common/migrations/0018_setup_rbc_default_territory.py @@ -0,0 +1,43 @@ +import os +from django.db import migrations +from django.contrib.gis.geos import GEOSGeometry, Polygon, MultiPolygon + + +def create_rbc_territory(apps, schema_editor): + Territory = apps.get_model('common', 'Territory') + if Territory.objects.filter(code='RBC').exists(): + return + + from django.conf import settings + fixture_path = os.path.join(settings.BASE_DIR, 'common', 'fixtures', 'rbc_territory_wkt.txt') + if os.path.exists(fixture_path): + with open(fixture_path, 'r', encoding='utf-8') as f: + wkt = f.read().strip() + geom = GEOSGeometry(wkt, srid=3812) + if isinstance(geom, Polygon): + geom = MultiPolygon(geom, srid=3812) + Territory.objects.create( + name_fr="Région de Bruxelles-Capitale", + name_nl="Brussels Hoofdstedelijk Gewest", + code="RBC", + description="Zone d'action régionale de Bruxelles Mobilité couvrant les 19 communes bruxelloises sans discontinuités territoriales.", + geom=geom, + is_default=True, + is_active=True, + ) + + +def remove_rbc_territory(apps, schema_editor): + Territory = apps.get_model('common', 'Territory') + Territory.objects.filter(code='RBC').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0017_territory'), + ] + + operations = [ + migrations.RunPython(create_rbc_territory, reverse_code=remove_rbc_territory), + ] diff --git a/loko/common/migrations/0019_userthematics_can_edit_management_zones.py b/loko/common/migrations/0019_userthematics_can_edit_management_zones.py new file mode 100644 index 0000000..713334b --- /dev/null +++ b/loko/common/migrations/0019_userthematics_can_edit_management_zones.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.7 on 2026-10-05 13:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0018_setup_rbc_default_territory'), + ] + + operations = [ + migrations.AddField( + model_name='userthematics', + name='can_edit_management_zones', + field=models.BooleanField(default=False, verbose_name='Peut éditer les zones de gestion'), + ), + ] diff --git a/loko/home/views.py b/loko/home/views.py index 0f85272..5d826c6 100644 --- a/loko/home/views.py +++ b/loko/home/views.py @@ -1141,6 +1141,7 @@ def _get_recent_activity(user, allowed_contracts): for item in Observation.objects.filter(intervention__contract__in=allowed_contracts, created_at__gte=cutoff).select_related('thematic').order_by('-created_at')[:4]: status_classes = { 'to_process': 'bg-warning-subtle text-warning-emphasis border border-warning-subtle', + 'needs_info': 'bg-warning-subtle text-warning-emphasis border border-warning-subtle', 'in_progress': 'bg-primary-subtle text-primary border border-primary-subtle', 'processed': 'bg-success-subtle text-success-emphasis border border-success-subtle', } diff --git a/loko/notifications/locale/en/LC_MESSAGES/django.mo b/loko/notifications/locale/en/LC_MESSAGES/django.mo index f2513c56b326b1c7b4eec1dfeae89ca2cc2474f3..12c11b1e54b6e7daced722f511b8975a42d98d1b 100644 GIT binary patch delta 4322 zcmYk<2~d^QAI9;6OA3gW#R~!zS1C|HLD2{e0Ugb)ObnMak?5*nq=I|eYoxi9Sza~G zq)ju_azU+3b2l|@D#x*!7DsK&RxNZYbL#ut_c+r#|A)_c&%N(?&-uOQz4qTY`TNPv zx6zH38IBX=W-{j{W4s~8{M<~v#uP*u6OM)01noEqeA>%p56KxFtGjaTBifO0|vQY!%V+8g^&1ool(T`p5anuBMpeAq%b)#Rg zFig1q~lg!&qF2k+>OkgMFw8p1?FbW&2eI4cH8uV;bsv zxwgL$HQ^Feh00OaFThgVhK^Pi%UyLNAF2WwsDZLE0lT6G8jR^!ih3lAus5zkU4I_6 z;!DWb<~nMe7|O|)7X zYO^jxl|FzTeBJhMMeUsrP}hBe4e>kFW;}&1Rv_gg~-z|15gzhi_FDL!=AVlHIc7SD?f`GrykjBjtOULYSTrdz7UTpT{@Oy zH`E2Ss2lA?J(4eN|2fpk|3*zLnq}&d^uonhj5YW@-iC90?(0{J!T0}w?Klzaproj0 zdl?JxZ)E?NuIxZN5UqAHWjhuU;Q@D5yr zP6rwXX=pQC#R6=S;?B4?s?-Cl!>}ReWvCU8vHi19duSfE#Q^G=??xW8IfVNDIgG)- zP!*0y<@sx6v8nE}ZHL`Mcy_1(7qqb@#b8PCCi*v`uB$^0dt+qb3unrR^&x?vyG0L7^Ok*Ht3(WnYdMCN9ykvG?@M@{SyYT(nT8-9y= z8_uJ?S8qS}@cY|{^H|jQi4F~2n2ah-8fqflFbw-zi>;%q<4~oYf;=^|6m{KB48_B! z3Vn)tJ1(Fq^)H5@H{IPcj*o_3t3uRD9>8#X6gAK^q?)E0HS-OqiEc(!>Y(jEfx6-6 zsFhtrA6~``jOI@f=A$OC1X;LaR?|?))}U^%9`(9yLJf2jQ}8p?Blr`0V<`VXgkupl z!;z>7j6+ql+V(F%s&AI#NZgAZF{!;VncUwL1shy|K0err{K%P;r~xmbO4uZm$AL-M z2g^|_*@mjf9@GL(q9*o*{rtS`{|&XEdensf!6@!;LbKct60OOo(x+ntcE;w|16A@O z)QyK*N26{$5hHO5>ihF-|8mrXR-t|m)}h8fgTa6Qf25%q)muaOQP5s!j$P4*NjL(v z(wV48M@W2{oa< zsLeS9d2D6^YUMQ;jkTzOcA+MA1hvAiQ5ErKyDLpVjdvUB5p+P^FE^X|Yi8Z)@ZcaE zh-Daq+ffrbin`Hvs0sgs(Rdx(U?l(5qD`EOGcg}0;V#ts-=VW>f7Iq2gBs^ChlXY} z+kP+~^*%2~ZNBZOfsdmm_&Ms?okdOjXVglsqbAhg4tHQLsz249XCQMm-E4m)`Z#x{ z(ilc#1x~=AT+7Mb zq>Su#KL~C+H5ZcvvQZ}-`Y(#Z!It}H3LGNukzPc*Z52r;Pm||}4%PDi#|%zh*6->I zG)56Ng4sqklWa1KXeZ1gI))HE=woC#8BJ~+dP(2Eae|MKC++!m>sae}>zkNWz@Jj` z64B9v%nG*L|ErrxJC>x{zKM80=|ftPO@R0a@;}QAheVgCUJ@UUtC delta 4268 zcmYk;c~DkW9LDiOAOc2gBKl(bDuuvnEH2@OV(w-UmYNc|6jm;{d=e-psw43QG|1UlS)G~YmYjyD<D5aF`7oG3VCrR<|R=7Y#ImY$ids#0dshe58za6jaAqR_hAxVz>yeE9f#oy zsEX}Ds%=hS2wp@Dw9fYbjd!sh$*ip%&pz?1V#6%jmlAv zWUKApiJJM3sDag?9!X24y9Cp*5_jW$SmcnW<6PT66g87kn2Hln&-@+aX_`vZ@jEdNPoOG%5jC-^$-Mu1 zwxMk(O%Trxb-^(ywxk#+DTdNN7j@!7)P>ig7TtCX!;{uC*ns^@s5NyNb-qTa{f4N4 zdQz#sZrGZRFif{UWTU=#Jx~=Ih>Xn?Ag`_|Lk+AFb>YpZ8*W3r4ZBds9kajxh9T@< zMcw#M+h6a{P-%ksFl!(Qs2e9))2*GYy-}sjL!OqIh&s=Y;kX7>p$(|FqZU=EQy76) z(2IYgD(`rF?o8UFF4PBgp?sujrT{ha*HHtVkE+ya+rJ)l!;Pq!?L{vh!M0e3gRv<) z8o+o|#bzKCb4&>h-JlHhy3Ipf=mSi~Pf(BG2kejMuo3$B2jXtbMh&1h>cR!Ke+*Jw zGYOx@a%_h;F#~<+lILGQ!%N2k z{e741KZKglG1P!h+Wm8q`b2u6PMsViI2o&9E10V8c+aPZ8=1Hw*P$ScDqD zGSnli!f5;&wN}1E4d^F~!LzpidKUH9>bymV1`@*q(yDBMJSLNknt2h%;$+l?%1{Ga zf|}u4R7FmsX8J4YdUdGM-$V^WpF<5S9HY?Fp8Dt0NTWj+oP!$BQq+ysqei?HV{t#G z;4i56UH@B|1EKgLmZ9GJo7T9F?xIXbU8f^zK>cigo3-CJ4x=u7+V)?u`!|p=ng$QL$9G0A``vLkj>O66Y^R}8r)9e{e-bm)_BZM5?)OApzZmsMDli8(VBr0~L_;&YjCz0n z!n^QajK`oZ?xRRR{hoxaFb!3SLFmDJ)FPc=eF;^`d8kS*w*8eD#QtN1rOj&r(}?~d z8A9G9+O`s^7}%<;T8wYoc8PVozjI`wvxmJMw&xD7*v2mFB-AJD&c;a0TjX`}0eP6{ zQ*=37+#){xbod}J@MB8NY1Q|fInm+Z% zL?sr@qV+y$O|%UrACp0(F=|NUMAWOx|q$@7w`+Ri7X&p$(Ljx z$tK;&RPqU#Ph!bDqU}2u^R=r#E+(c~E$vFOoa`kl$+P6enw*$hVKsT49q~2IGY?1n E2N4jWDF6Tf diff --git a/loko/notifications/locale/fr/LC_MESSAGES/django.mo b/loko/notifications/locale/fr/LC_MESSAGES/django.mo index 1b10e3fd75b563d44bbd25faf7a36c747d1515b1..3b3f37b9f8006de2099b12a3ee752e683dbd6f92 100644 GIT binary patch delta 4328 zcmZ|SdvuR=0LSs~7-pAtv;AfhKh)S}b6GQNV_O*|mxf6$Gb~yynadC!L@rs%4@HJl zYEpz{cOU+ztiHk4}U&U5<0E6*s)crr9 zuD^=WSdX;lnD8iL_-8zP1Yt7jhICW|y)X>#L-pxFjKNaufls13um{zF3#bSEj*ajd z@+?!2UJT>uYBvp=@_f^sf<|^X>c)QP!9wI7Q-)1(4(i6m)|IG^R%0U8U>csc_5Ywc z9Ma64**LtN^9*c`gE5%rn{oH$Yk9XyN4c)`|d8q{DY-iFDj>$=E_;BIs@vRIy~2YFF5&;iv@I>uubs-YrG!4lMxRAFCy33dM!)QIbl zw#`jcJJC!heKrZG8R~?USnOf`GbwydMGl6ufOlbkOvMG*26td<{1n^bH7v&F%;PYe ziJGxbkXbjEFc7b!I$Ce*n=+aPoZpVx8{T;4UpFRFL1#@Ss>cJYMW`7Xjk<24btY=F zE<;Vd55sY@t$zo#clM+1JBJPNThwN}j2U>%u@|HyxD92arm&y&K~%>|u>fbIW}p@| z#Xq7(coUgK6T&;J>)WCR(gQUk`Pd4JQ1?wob<9~#L2FlSzo@b2hftgAG^#^CqZ+=B z=@`bBdT<`H8fGAB1|}kXF*C3?u0nO>B5LH9Q0-hr_L^ft*qYjOQK$==qoyte%djWv zh8om^4x^Uj3tN8~HS)hv9gAX^T9N`>fkSZv{(yJjLa+P!)u8|VueB9t{S{0pYHk0( zT>Kl^KPHPEs7*5zwVOwv9`poiN*&auTa6mXI@ERBPy;-Iy8a~Q65izE+7i zlJi2Gg4?kdwq?OJ1LIKVvryOV!(#L>EH75#o%kZ^H9d&k@E6p~wB<`}x?$KAtI+98 z;TQ#NhU=J%O%mN6_eD+hKn~$8-ayT8 zSUc8VBa3b4u5Bvj1+aFg2A8yV8(M|@Gwbx3FAl+AoQ6$t9;zeD917~;O4Kf1i|W}<)P=_|7|&V1Ky~yAGK;1T)6kRR))$~U zT!_4>rWmyZvr(IP9R}lW^rCZ!f|lSr)E@W?Lold=+rwtaGfWFqeIHbVgHgMAGHN9A zZ2c0{1Gb=MXea9a^H_rQsDX`2^|$Ak2^2H~Q&0`fLVgs@0#rlQNT19b7>1u=U%Z6s zPzu8j#Z1%^-Ggf9A=K+P7Wro?`KZJ#n29Yr>TeF~UqnHFbXFm4np4P|Xa2(07~jeL z$<0Q+Zews59>xfa;}z28O2RiRO&K;~mKF2z<@jk@muYCxZ&mhMVA^RF-JY(-<% zL7OWI)uB$PhI5ch%n;Orm!Mv|HK-Zbj{2i>2z%p~sE)K|xEgs9s+}y7c4++n)TQWH=`c37h~`QYSVp%8p!vk>;6QIG^o3KeK_WG z-V!zSC8)Q`DW{-6CTnpDUdLWo#?M?VZbh9RL0uP+<-P@@(aZTkyc54gHP|TI{iD+v zH8W+%B$^G#q?pgJGlt~&_kv?`DCANx0M+9as2N#f-Gti3TTvr^+twdLjpQ6A;YHM% z2ljOLP$=s9WQ@jK)C>nL{%j(?ZD#z`MCjvR2m@b3~8wN2y6Hl1*cBq#hO_hUL7C-0F0q8IliqD6k1JWF)w z{rm4Ri<9;GU45Ry7{X38yU7lcPKJ{XWHHe(jA-FMB5TOQpq;Ioj6=x1q&3;@3kXjrok?jkX+v}rk{WV9 z(OcGqJW6z&CZqf%_ahgcBn!wsq9eq`OhCO-L-RxRNv^wWK%E@c?;| z93rbo1&Jo}$X4Hl@Qz_`QK}@VmXj>9m)u7(Nl!9`yh)akD6*93IOAgW zyZRbNMm9e}c_Ud(-XLp9DVeq_HL^Y^rlex>w9*RixJl*ZrQWF(C67$4tts7=7~jS7 JzhB)5{TI(Cv0eZG delta 4268 zcmYkm=kz^$-^b_sx!>>S=llD;R4zC(-}$~} z@G8S`ifB#D4>2aLkul#kSFJIb(Z)2xuGkcFu@mMYmzwF=7H8vaxDu0aCx+u`)cxO} zuD^saSc|mhn0g8=sc6baC?=wANI^B=!wBq+8q*+*!%^59r=bS06*YjPsE)qJCU_a? z!qlP{Z=%|5%P7L=-=tB{%(757_Q3=kfLv=Hz%VRD-B@NVM-8+RQ*j-3!2`DbH`IXt zL9J|PtTAn{1Gd6}7*79Y90e~vit)Gvqi_?dgI%Zr?#J8lsI9L(Ws5 zT~PxrK&{XNsQc$)F>XOeGYg?tbrgkKf!3&oQZNzIQ4Qr|Iu1u|$pRdN%TV{9N6q*m z(zf{p)lLx0$rw#D)CzgA3=0xi{~QX(sK~{7%*I?cvNO)acDM2ICdfKx=LNU)Y%Q2xg_+h)QJrbz?jg8fizX54AEmsPFq*??WZ&MAVX( zU?k47^~+E>vl4aRHVnc&s3hEn-SG?ieX!$k8;V0MU3+U+WcAD)n2)1SGv9z(+5@PW zT|gGiTtZ!c12vHtGEOVu!6fvd?khkIY%*%goH_Q3a$B($l{{Ne13H9i_zY&@6;#KG zT*NMzPN)^=kMwPZV;+{G22z8X`Ce2zr%^d}$<;Bn_JTi9OBYP4PQ?V&4dtkg)}gkf z+Sczw&HNl{V6~_%Nn*Oou?tq=C)ggxdELjZ9M#T-hI5A(hpjk(+S`-p$8$)&m{>AT z$k_bzKE&rmvu`-;4wBJ=D@)!*u#L^%Ne&bW(j5F2;U% z1GNHu*?66gKwTHWBCJC%4r6*9a0aI18tjROQ7dx;m2{b0)B_*JuDB5$CBqpCe!Pep zadN7=M4hbNFo^oTs2Tfh{r#w!Ou#fOLGAf!WH(I}>iT^cgJ)4Id<8YJ-%@%0wYMQ1 zNuWmT9jd|NPHsb`$e)?bM+jCS+ic!I-FF0&@H^BC>L%*`P)4KcV^JM@Py_R!ax4c! zaY&lu-Y|j+HBf}g?y0Eke-br=3ha&RQ3JSu8o-~Zj#}LA=13eW2R!J-&KQpOVi=A= z4P+wfzG)5xW${eZ$d;lm+=$`0&039`*?vsL6W9goY<*I?JK%KW2{k=YTQCZh#IrCQ zE76NWY1{m1>%AFngBhr79)h}VjIA$5b>N^@XesLc?O2TG zQ4{OY+3m-Vtbk+grl5vKAa6x87S+%kq;InjBXB1U!oAoGgPDGFj6!Wu8&o@4sK>7l z@@IzeQHBoY;C0MJpHIIv?0+c*+BB~rPn`J@lkpnzgqgTZ_i^img`BTLt=R9V47bL?z33 zjKxW)j^<(280qpsVHn&C;*^jmm|3^kWpy zAOm*Ha0*(Y(bk7hSzLme@#D6B5o#tYFby}L_WUz!gI}Pozlt%~_zri8<53e!!rQP1 zHo{>9smm)Cv=km<6!AQvqnha6P%>tdRWGKOZFzxpTA){i$2rK^UR!gEr)^=s^)b{d z?AF23nx({Z#B0P*Vm#3>J=C$w#e9Z(L|!E}+B#97B=QMu)+>ZIxq_%9bm;l}|1p=7^?Hx4prA}qHq;RB63U1% zLX@CfX4??jznL?k4n8AJ@hI-Bi*%aOi_ zw<*mbIupkO$x)u@)s#|+PYE4mfqSAnaix^9iLr$8U&rCVf~W+ioKgbuEb#(SOe`mw z5c3Ef$6U;N)+xA~*hwrS`Vb!wLx>!Lhs(?)-XfL|Es4d1j?Z1p$F6~x=;&4lC|40H ai9^I%ViNI0O>Xo*p*01GnF)McZ~h-+Yo9#; diff --git a/loko/notifications/locale/nl/LC_MESSAGES/django.mo b/loko/notifications/locale/nl/LC_MESSAGES/django.mo index ef7f2bf29e52464c6055fe7c8f7862744ea0b204..7ae26db0dd85ddb23c71ab8981c263d7e01c9236 100644 GIT binary patch delta 4328 zcmYk<2~-tT9LMo%xPVWD2Lb|_PbpLsHy}b&mfXoS4b4jFX)jH~6j4yqNuw0Qw4Rhp z?n_c`MOkS^X1kef9{rg4p7 zJ3=lcl}(L_Yh=t%Ep*hFJg+ex%*SRp6gy%Oa;lk+m*IWb8n<9uJb=ydGt~9pqs~8r zvDkq0=a{G%WB6wh_z{k2s0%Vs9pqvp4nd9SMvTJ%_QJWS0qjK$;27#gzhDzQi`>gJ zpbsOtyZY^l5!~P8(9q1TL0wpg2{;0|#!SWtoQJw_k@aEJK-Xd_)?!zD&mR95HDFIG zx3aCV4f|P`h$FE%_ctXpd{}|;xC*0j59$U7Q3E`JX?V;YR~gh{3v7vLsPl5{@qE;P zi%}JtjJm!G{rC(znpr$|)s1|p3Uonrlz~au8`aS$?2LZYBUy@ra1HAEuTeAp4e8rl zK=l(#IT^D_MpdXgmSb@O_0OhpoCAF@iU*j9Lopo}Vtd?!?eIfP!LwM5iPUj4&PG-2 zeWdE0}4lr1gjT-TF)={VmjYFL`$vPXgSeKwm zAH*o!WRE|MT08qu*S(8j_yuY)p293V>(~=IC%YY8jVfWG^+wdd0yqHgMpfVts>I)+ zW_ST9qVez!>--edM0%krQh;r76zaNJsDU}vH1zD&+7D{&em!b&9Yzi4G^*osn1PXe zs2k@aPs0pDRbUb_7E^)!aRq81pP*)b64lQcWUV>I!_w5Ei$R@`h$>xYoQ!=@7u2F| z^fKy^oUq4Fp=N#_HLw__sYfyZmthgE!*B3%tn|6BUoD2-|3mh`k*Q6=4iO>r-_#RI6t_L<%P8?`7C8HE;O2h>2aQEMa@ zBXBgTe}5;({brlS0bMW`wb~!R2waK!=G%lCz)n;t>rpfN6SXM4e6MO?X{a0aK=qe{ z`g|~k<9O>7)cIwOJunyb2r5xCsIhKFb@Y_oKWO)lB9GsEfsDa~@xV3kbd11WsQ&t) z23~}!^hEpleAEP-#Wb`^x1mnlkInH2s-uswJDx<%EVheV%4F0)Gf?N{qn>dgwm?7X z5luy1{~(UVdQ|^y(nA$-Oh+1_l38<5&nn+N!Hhx8a6D@9RO3M0fa>Ub^x&VU>;6Rz zD50x+emi7Ym@J%$Jxx&^38EkjLYvprsmT4c|mt~-XRz}MDad)oK^JO?xoFYlI0a~bNy3{>g+BLB<{ z{LlawqdHuUn)!Ov%(r6>K93s6AE<%;Wk0_p)4e_hHNm7z>aX`Rl>@Pui+&u5y79Bv z3J;?O_$6w_KcfcFfa%yg%YA#U!fN)1U=1EezQs)m-?ys7a@37?VI1yrXlSPIp=S0e zrr{~{V#{oICO(W|KNWR;4)Uji>4$p8#i$N%Lk)C}J-!HAv0sh3xDi#6uaMv5#`%$k zewl=`GG<@^HKP-lfX%2U>(aEvn{YVZfKQ@6*Wcjks5SC?#$+MO#@vKmu^QFiepElF zk*|M~)H_r`$K=z{$cCeSxhzMGaD{apYKGfUGuern;VY6!s85W8?Aa2LED~hy))P;GTC{B zy%+6~i|A$7_L7U4hJ$QdP#W_9xsPlng`}8tB5R4Zr(Mh&s4{FI`t#=ER^m3C(DK~M z&OkDO9B@Af&0Wo9B#G?M4%;o{^-#!yU~s(9qh5`sAWBvv?IHNjiQnRvuSN2dWE%(AhqOLqP5tQOd;A1 zld++e`y&tLl7(a+(dKb66H#lv57`o25S7s9ar?<__%I10he&^-?Rv73)RPsYl*E#I z$P>Y1QQabU(kdr<+1?6zqm#Xl(n=*S6Kxg2KGDf>Q)y+An@MY;?bTpObb@m)Expo} zWGNX>mXjvrZldi?7xR>LGVUe2NHytAo+Cp@Ht9=dkSEC!5<{wpws&02e%D}_*PD2d z_Igr7wv$yPKxWpZdmF;z{H4=p21_{vKCca$Bf4b-J3t&Wcy>MxxVC=E>Y W`R|)R;URUmX9qGTy zu$>`ykOcw8v}tC{rB*s>Oj?97EioNiU@j(Nf8E8F zHb0~K@uQrK(X>QWC=TagK{WNxqH&S~xp)&ZF_#zF6(7S6xDn&=7Y3g+07s){UW+PiJ!)oO zAVo7*P}eu2CgNemsS+_5i>avl3Qz+pLA^3(u02s@57eL*&t}wsj-om~kKOPZ>cL(v z;w_j&R0Zxqo^3{8f1HULNF8eC`%(RzMXj+bu8wK27yODUoj*%;8b+gTs6stx4eFKb zwZ{*jX8tK^U=65O63cW~UQShvV=%#^G?L*BQ$&8DGY`@ffNyji^PJ#zj4FGN$8tbhH@G)5yci zs1e6^aZ8kF%|Jhn_d?A$&mO-IHIwm}gfmd@d^Pem%?8x<2hf8TP!+z0n%FO0`26eL z1|(3LX1qI8sU{@49had>T8RO;0%LJCYO%d*_b;Fp<*%s4sL^O3;ixsz7K1Pi)qghX z3vE!6#a~3W`uOD77v?b@YPWUu*ZbBCp--L!NCep#~nniw{B%s=s!qfv2EKooCOF zK~10-wMZR@hAyneVBCW0Xb-00e$>o^4j!>H%KV1sSLr=Aaf&3FhN`R7Xd!C7waucM&!4>!|DhL6(CF zP2rCd%))fsiGA@KOwjLthgA2+W-w~x<){v7P&3}glXwP_oGTX0sV2lJ-!gb z*k6pQTn%c#8&M_Sj?J-N=Xt(4K|>v$Lwy5Y#87Oo$N#|)_Wc>Yj6_u^4)vfUd%P!V zAoWKn-f%DDs~Vxz|S(Mze@70z2Fa2$(wa^KW@>e0S-ZRScsbW1k}u@ zV^6F=4dflvK=<48$5Hoxj+)?CsE_3j=)v1Esedtzs7&|4<){HIM~(0`)Qoqd25<rMpnSA*XRRI*F_h&8QZm@f5P& z%y&2zJ=Av;mZHudLv{2e^1EXGLY9kZm+k&-SAgnoA*!Dp$k)C33ROY>9Cu(Is(xp{_fDUObKAc+L7Js*-`d+{%WcDjJ9U)R^If zKf+m^pi;z;(d0RzZ7<<Il|t4d*l`iZR4QzQ5<;dBvKOd z0(pkKN`{b!NfN0g+V;4Z6R0x0Le|@3Vussxnu@!boqRHZ9B@xGttvH_6V++EcGw;w z$D3O2KXc$i@*WvLRLiv_nJgm9h&Fxh{=dyH1TJC>sQ)x$$1bb{2 zYFQ5=@nn}TA}pq)oYp3ykFd6pWGfj;v=%eSG@|VkDQarD|K!0FTI_8u7;(C?4UK5=)?ArFFrgb zVl}NUm&FM z*-4g=9P%z1OtMIC@)&uOJWnFYVxsM|i}}FS=ZT28y`J_4QcaGM8ZwDIUY8qjBe1T( NTN73HZ`$mZ{{e9Xn~wkh diff --git a/loko/observations/locale/en/LC_MESSAGES/django.mo b/loko/observations/locale/en/LC_MESSAGES/django.mo index 112750d361a88095c8d39c8df6acac82111c0721..048e4b76f339093b25aa2d63df2e9648599b7801 100644 GIT binary patch delta 6373 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 delta 6315 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 YesAz3(DMINJmaMEPB!Nx{k!nd5Aab)ApigX delta 619 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 M#<#Ff-*n&q0uU^m{{R30 delta 6320 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 Ck(=QF delta 6315 zcmYk>34F-s9mnw}k&r_okqAjdP9Y(2Mw>)STtUoL($ve=#VAJ25Jy)2=**mx#W5AM zLtSNvp)GZlhJ`s=N42iau{ncwL1{nV{2#B^{;&S>ex4`)|8xAF=l@Up>Y@)l&ad%s zF4p#1WVo7m7*ij|`5IG1xxBVojR^`jrUnLMJq*WY*b?J#5Juq)Ou$vx5X-O{-bHVG zfa<5xY9gxcbBqs#j{G7OGjI&n#ZAb)<`AaiC42?LBaNwvuVWn?jOw@$HSl!w!FATH z=uiC<^u)s$i$~DgIL6$fpv6{TAbOF8S{Qnzx6D)2z~0O% z2;(pqTcH-z8I`eqsQdHL3rC=%8%9&mgAOWH3y^G^64Zk`Q7iZyHKB6Ub7xT#x{m7j z5$X&)xApjF_qi0*gwn7%W@C4p7)|~|C>*3gsXmM9=pJgI$EXLrVt5$CP!sHdnsAs1ARN8ej(MP_D4Q-)`G?qcZvxYNGd1ht<#F9a2h~qXx=H9iFkM2^8A;Y}9~7 zsDYNE&crs<#1Em)%o$t1gL+M$qf(t3@1Ctm*q*wBy5HGHL3?%k1H3}n+t_Dyfp zj7OseoQ;~ma@3aXKyB4=`}^~#fp6nWSf6FGSkn(PaXhBrHhh)w&1DKxXoyZU=1p9I zOxiq0bsXEswJB<1FQHzm_LzjfLEXO;mBHPp2_DDBcms8&f~aW45!MzM#Q3Hw1s{I# z1}f!)&kiv~5PfL>!g>O=rRPy++?Eb#ooP(LdSENHA)^c&veqP=~k~I$Bvz z3OYnXP^Wx6@=YVtC#yWum`1hZSash)uhXx>9*VqI08{ohMLhwm81;5mE4V+^Jq#9tofVB%2U z@cyX19gQJah?>xRtcfd7{cS|uzXNp$PokcCikg6T8_tH_|N0cNJdA0LO5J#tp^hh` zGEszjU-zLpyo7oUpP>E)B;sZFbhkl0pJB~HZCO{;8S05VV#cBtv=N<93KuCT^$(GM zOhCG87u2D0upZ7qtzb2(!_BCbo<^8!QB0fQ#t=J4VQ;DdpXp_PID|MY{ z2*-Y?fkq*(teI)++p#Y7W2iH64RwDIe^IpJWYmOOpeB@S+w+lKG{bEBa@6Txhsw;B zO!8lw!VVg=vLmR|c^dUrT(#ap&G->&BA%RB-4}scKoaWtbX0#Gv1%fymFJ`Gdkb}F z$D{6_=}^#$i%_ZFV1Kd0*7u_=w0wt)Gl%hVd2eB(&M{QBF zEMvN021ejC)C5aVui-BAVSIC+f)3SV)Q!RXAKr?n1h zE8@`$+n_qkw06eo)O(=@%tbvv6t$HjP??*Co><(4{Hue-G$^(2qgJpHHS?XQ)Sf`? zUG;3&IMm8tMNK>pHDErf<1ttr3sCpHgId^P)XF!ZGQY>MH++pcgpW`UR^ue;K|j

Y64l^+;N;f6k=$|LuFzT#^Sq} zh-++nIVy!`P#L<7+PX^AM4n?R*6!|hoP|ntSJajbKu!E@RA#3m<2Ys+1iHIYU9{pZsCphMb8n+E zG65Ul)IQ{2hhZfRO7SjC#d1{pQ`Cc@echBcK;4&y%2YQDz+6-Y24gZ#MP+a!24gAe zxkKoIy7GwD+W$aW{z_#j@ip<{@_KP64IkP@zDrftdVX#vMiP?pTLZlOi ziSLQ}wy*zE))h|uORSYZK_843gbrbpy^)u|JSEx?e=eRE675WoTJ3LOjOB^Le6AuZ!YdZe*=tZpYKrAD4$~?K>M^n^0HH**@esN8EQNYh^nbT+rh;2kS zq6@K{(Dk_Z<2upZlISv-xJ=6hqL|o9=sHjQlGsQbBd!w>L@rT-XAq;@Ggr ztY}KrX=#Xw#Ea|hi-NTXd)x98tV>)Zt`<)Yi%yH+Nxjvt5xPbZezeWSy0}t}Tyd0R z30=2~Plkmw{3oT4tLl7wuo1t#h5EKMB-##OFjaqNXZb&s@w6Yld}^Y@A>4tAbHUrH#Wg{7VZH M)<%`4b+{b#6LGwZZvX%Q diff --git a/loko/observations/locale/nl/LC_MESSAGES/django.po b/loko/observations/locale/nl/LC_MESSAGES/django.po index 7ea6e24..b32c2f3 100644 --- a/loko/observations/locale/nl/LC_MESSAGES/django.po +++ b/loko/observations/locale/nl/LC_MESSAGES/django.po @@ -404,6 +404,12 @@ msgstr "In voorbereiding" msgid "À traiter" msgstr "Te behandelen" +#: observations/models.py:428 +#: observations/templates/observations/_observations_list_mobile.html:71 +msgid "Informations demandées" +msgstr "Informatie gevraagd" + + #: observations/models.py:428 #: observations/templates/observations/_observations_list_mobile.html:71 #: observations/templates/observations/observation_detail.html:876 diff --git a/loko/observations/locale/nl/LC_MESSAGES/djangojs.mo b/loko/observations/locale/nl/LC_MESSAGES/djangojs.mo index e0995ae501bd934f4811e479b5f0c1df42bfb592..91d35173f73387de1881a150a4e8809018b83dad 100644 GIT binary patch delta 697 zcmX}qKP-bm7{~GJAG9c1>Q5OONfS&G6N^|Zoh%m9o3PGTK zP>FAm$290P{{c0xg#*~eA$&&_?x9*0;z#9)V37UIygjgrO0b5DxPd&TM5hENNG@|~ zzh9sVxkNu!QH3`J~As_QAwv(&p74W;gxE#f++ z@E8ZNi;6izAMY`Z&zQh>=O<0H8;RR~FTh#hbxI}VqSY>}J518@T8jU7y;3PIs8| {% translate "À traiter" %} + {% elif observation.status == 'needs_info' %} + + {% translate "Informations demandées" %} + {% elif observation.status == 'in_progress' %} {% translate "En cours" %} diff --git a/loko/observations/templates/observations/observation_detail.html b/loko/observations/templates/observations/observation_detail.html index a1d5e20..c6cdf3c 100644 --- a/loko/observations/templates/observations/observation_detail.html +++ b/loko/observations/templates/observations/observation_detail.html @@ -134,7 +134,7 @@ {% if can_edit or can_delete %}

{% if can_edit %} - {% if observation.status == 'in_preparation' or observation.status == 'to_process' or observation.status == 'to_redirect' %} + {% if observation.status == 'in_preparation' or observation.status == 'to_process' or observation.status == 'to_redirect' or observation.status == 'needs_info' %} {% translate "Modifier" %} @@ -161,6 +161,8 @@ {{ observation.get_status_display }} {% elif observation.status == 'to_process' %} {{ observation.get_status_display }} + {% elif observation.status == 'needs_info' %} + {{ observation.get_status_display }} {% elif observation.status == 'in_progress' %} {{ observation.get_status_display }} {% elif observation.status == 'processed' %} @@ -179,6 +181,34 @@
+ {% if observation.status == 'needs_info' %} + + {% endif %} +
{% translate "Description" %}
@@ -615,6 +645,13 @@ data-icon="bi-check2-circle" data-btn-class="btn-success"> {% translate "Marquer comme traitée" %} + + + {% if can_take_action %} + + {% endif %} +
+ {% endif %} + {% endif %} + {% if is_creator or 'admin' in user_roles %} {% if observation.status == 'in_preparation' or not observation.status %} @@ -657,7 +718,7 @@ {% if 'admin' in user_roles %} - {% if observation.status == 'processed' or observation.status == 'not_relevant' or observation.status == 'to_redirect' or observation.status == 'duplicate' %} + {% if observation.status == 'processed' or observation.status == 'not_relevant' or observation.status == 'to_redirect' or observation.status == 'duplicate' or observation.status == 'needs_info' %}
{% csrf_token %} @@ -1104,6 +1165,14 @@ document.addEventListener('DOMContentLoaded', function() { if (!isSymptom) { document.getElementById('actionStatusSymptomId').value = ''; } + } else if (status === 'needs_info') { + const comment = document.getElementById('actionStatusComment').value.trim(); + if (!comment) { + e.preventDefault(); + document.getElementById('actionStatusComment').classList.add('is-invalid'); + document.getElementById('actionStatusComment').focus(); + return; + } } }); @@ -1125,6 +1194,22 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('redirectSymptomDisplay').style.display = 'none'; document.getElementById('redirectSymptomDisplay').textContent = ''; document.getElementById('redirectSymptomSearch').classList.remove('is-invalid'); + document.getElementById('actionStatusComment').classList.remove('is-invalid'); + + // Adapter le label du commentaire si needs_info + const isNeedsInfo = status === 'needs_info'; + const commentLabel = document.querySelector('label[for="actionStatusComment"]'); + if (commentLabel) { + if (isNeedsInfo) { + commentLabel.innerHTML = '{% translate "Précisez les informations demandées" %} *'; + } else { + commentLabel.innerHTML = '{% translate "Commentaire" %} ({% translate "optionnel" %})'; + } + } + document.getElementById('actionStatusComment').placeholder = isNeedsInfo ? + '{% translate "Indiquez ici les détails ou photos attendus..." %}' : + '{% translate "Ajoutez un commentaire..." %}'; + // Afficher/masquer la section choix selon le statut const isToRedirect = status === 'to_redirect'; document.getElementById('redirectChoiceSection').style.display = isToRedirect ? '' : 'none'; @@ -1224,6 +1309,7 @@ document.addEventListener('DOMContentLoaded', function() { const _nearbyStatusColors = { 'in_preparation': '#6c757d', 'to_process': '#dc3545', + 'needs_info': '#fd7e14', 'in_progress': '#ffc107', 'processed': '#28a745', 'not_relevant': '#6c757d', @@ -1256,6 +1342,7 @@ document.addEventListener('DOMContentLoaded', function() { 'match', ['get', 'status'], 'in_preparation', '#6c757d', 'to_process', '#dc3545', + 'needs_info', '#fd7e14', 'in_progress', '#ffc107', 'processed', '#28a745', 'not_relevant', '#6c757d', diff --git a/loko/observations/templates/observations/observation_detail_mobile.html b/loko/observations/templates/observations/observation_detail_mobile.html index ef2c673..6000ee0 100644 --- a/loko/observations/templates/observations/observation_detail_mobile.html +++ b/loko/observations/templates/observations/observation_detail_mobile.html @@ -148,6 +148,13 @@ data-icon="bi-files" data-btn-class="btn-info"> {% translate "Doublon" %} + +
+ {% if can_take_action %} + + {% endif %} + {% endif %} + {% endif %} + - {% if observation.status == 'processed' or observation.status == 'not_relevant' or observation.status == 'to_redirect' or observation.status == 'duplicate' %} + {% if observation.status == 'processed' or observation.status == 'not_relevant' or observation.status == 'to_redirect' or observation.status == 'duplicate' or observation.status == 'needs_info' %} {% if 'admin' in user_roles or 'operator' in user_roles or 'manager' in user_roles or 'controller' in user_roles %}
{% csrf_token %} @@ -201,7 +230,7 @@ {% if can_edit or can_delete %} {% if can_edit %} - {% if observation.status == 'in_preparation' or observation.status == 'to_process' or observation.status == 'to_redirect' %} + {% if observation.status == 'in_preparation' or observation.status == 'to_process' or observation.status == 'to_redirect' or observation.status == 'needs_info' %} {% translate "Modifier" %} @@ -225,6 +254,8 @@ {{ observation.get_status_display }} {% elif observation.status == 'to_process' %} {{ observation.get_status_display }} + {% elif observation.status == 'needs_info' %} + {{ observation.get_status_display }} {% elif observation.status == 'in_progress' %} {{ observation.get_status_display }} {% elif observation.status == 'processed' %} @@ -243,6 +274,34 @@
+{% if observation.status == 'needs_info' %} + +{% endif %} +
@@ -909,6 +968,14 @@ function lightboxNavMobile(dir) { _mobLightboxShow(_mobPhotoIdx + dir); } if (!isSymptom) { document.getElementById('actionStatusSymptomId').value = ''; } + } else if (status === 'needs_info') { + const comment = document.getElementById('actionStatusComment').value.trim(); + if (!comment) { + e.preventDefault(); + document.getElementById('actionStatusComment').classList.add('is-invalid'); + document.getElementById('actionStatusComment').focus(); + return; + } } }); @@ -930,6 +997,22 @@ function lightboxNavMobile(dir) { _mobLightboxShow(_mobPhotoIdx + dir); } document.getElementById('redirectSymptomDisplay').style.display = 'none'; document.getElementById('redirectSymptomDisplay').textContent = ''; document.getElementById('redirectSymptomSearch').classList.remove('is-invalid'); + document.getElementById('actionStatusComment').classList.remove('is-invalid'); + + // Adapter le label du commentaire si needs_info + const isNeedsInfo = status === 'needs_info'; + const commentLabel = document.querySelector('label[for="actionStatusComment"]'); + if (commentLabel) { + if (isNeedsInfo) { + commentLabel.innerHTML = '{% translate "Précisez les informations demandées" %} *'; + } else { + commentLabel.innerHTML = '{% translate "Commentaire" %} ({% translate "optionnel" %})'; + } + } + document.getElementById('actionStatusComment').placeholder = isNeedsInfo ? + '{% translate "Indiquez ici les détails ou photos attendus..." %}' : + '{% translate "Ajoutez un commentaire..." %}'; + // Afficher/masquer la section choix selon le statut const isToRedirect = status === 'to_redirect'; document.getElementById('redirectChoiceSection').style.display = isToRedirect ? '' : 'none'; diff --git a/loko/observations/templates/observations/observations_index_mobile.html b/loko/observations/templates/observations/observations_index_mobile.html index f93431c..fb2ac6f 100644 --- a/loko/observations/templates/observations/observations_index_mobile.html +++ b/loko/observations/templates/observations/observations_index_mobile.html @@ -247,6 +247,23 @@ color: #fff; } + .btn-outline-status-needs_info { + --bs-btn-color: #fd7e14; + --bs-btn-border-color: #fd7e14; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #fd7e14; + --bs-btn-hover-border-color: #fd7e14; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #fd7e14; + --bs-btn-active-border-color: #fd7e14; + } + + .btn-check:checked + .btn-outline-status-needs_info { + background-color: #fd7e14; + border-color: #fd7e14; + color: #fff; + } + .btn-outline-status-to_redirect { --bs-btn-color: #ffc107; --bs-btn-border-color: #ffc107; diff --git a/loko/observations/tests.py b/loko/observations/tests.py index 1b6c27d..7411542 100644 --- a/loko/observations/tests.py +++ b/loko/observations/tests.py @@ -639,3 +639,153 @@ class ObservationViewerPermissionsTest(TestCase): self.assertFalse(access_context.can_process_observation(self.obs_green)) +class ObservationNeedsInfoTests(TestCase): + def setUp(self): + User = get_user_model() + + self.thematic = Thematic.objects.create( + code='THEME-OBS-INFO', + name_fr='Thématique Obs Info', + name_nl='Thema Obs Info', + ) + self.category = AssetCategory.objects.create( + thematic=self.thematic, + code='CAT-OBS-INFO', + name_fr='Catégorie Obs Info', + name_nl='Categorie Obs Info', + ) + + self.creator = User.objects.create_user('creator_user', password='pass') + self.creator_config = UserConfig.objects.create(user=self.creator, is_intern=True) + UserThematics.objects.create( + user_config=self.creator_config, + thematic=self.thematic, + can_view_interventions=True, + ) + + self.manager = User.objects.create_user('manager_user', password='pass') + self.manager_config = UserConfig.objects.create(user=self.manager, is_intern=True) + manager_role, _ = Role.objects.get_or_create(name='manager') + self.manager_config.roles.add(manager_role) + UserThematics.objects.create( + user_config=self.manager_config, + thematic=self.thematic, + can_process_observations=True, + can_view_interventions=True, + ) + + self.unprivileged = User.objects.create_user('unprivileged_user', password='pass') + self.unprivileged_config = UserConfig.objects.create(user=self.unprivileged, is_intern=False) + UserThematics.objects.create( + user_config=self.unprivileged_config, + thematic=self.thematic, + ) + + self.obs = Observation.objects.create( + description='Observation to be clarified', + latitude=50.85, + longitude=4.35, + created_by=self.creator, + thematic=self.thematic, + category=self.category, + status='to_process', + ) + + def test_manager_can_request_info_with_mandatory_comment(self): + self.client.login(username='manager_user', password='pass') + url = reverse('observations:observation_change_status', args=[self.obs.id]) + + # Without comment: must fail/stay in to_process + resp = self.client.post(url, {'new_status': 'needs_info', 'comment': ''}) + self.obs.refresh_from_db() + self.assertEqual(self.obs.status, 'to_process') + + # With comment: transitions to needs_info, creates comment and discussion message + resp = self.client.post(url, { + 'new_status': 'needs_info', + 'comment': 'Pouvez-vous préciser la taille et joindre une photo ?', + }) + self.assertEqual(resp.status_code, 302) + self.obs.refresh_from_db() + self.assertEqual(self.obs.status, 'needs_info') + + # Check ObservationComment created + comment = self.obs.comments.last() + self.assertIsNotNone(comment) + self.assertEqual(comment.author, self.manager) + self.assertIn('Pouvez-vous préciser', comment.comment) + + # Check Discussion & Message created + from notifications.services import get_or_create_observation_discussion + discussion, _ = get_or_create_observation_discussion(self.obs) + last_msg = discussion.messages.last() + self.assertIsNotNone(last_msg) + self.assertEqual(last_msg.author, self.manager) + self.assertIn('Pouvez-vous préciser', last_msg.content) + + def test_unauthorized_user_cannot_request_info(self): + self.client.login(username='unprivileged_user', password='pass') + url = reverse('observations:observation_change_status', args=[self.obs.id]) + resp = self.client.post(url, { + 'new_status': 'needs_info', + 'comment': 'Commentaire non autorisé', + }) + self.assertEqual(resp.status_code, 403) + self.obs.refresh_from_db() + self.assertEqual(self.obs.status, 'to_process') + + def test_creator_answering_in_discussion_auto_resubmits_observation(self): + self.obs.status = 'needs_info' + self.obs.save(update_fields=['status']) + + self.client.login(username='creator_user', password='pass') + msg_url = reverse('observations:observation_add_message', args=[self.obs.id]) + + resp = self.client.post(msg_url, { + 'content': "Voici les informations : la fissure mesure 20 cm.", + }) + self.assertEqual(resp.status_code, 302) + + self.obs.refresh_from_db() + self.assertEqual(self.obs.status, 'to_process') + + # Check auto-comment created + comment = self.obs.comments.last() + self.assertIsNotNone(comment) + self.assertEqual(comment.author, self.creator) + self.assertIn("Réponse apportée dans la discussion", comment.comment) + + def test_creator_clicking_complements_provided_resubmits_observation(self): + self.obs.status = 'needs_info' + self.obs.save(update_fields=['status']) + + self.client.login(username='creator_user', password='pass') + url = reverse('observations:observation_change_status', args=[self.obs.id]) + + resp = self.client.post(url, { + 'new_status': 'to_process', + 'comment': 'Compléments apportés', + }) + self.assertEqual(resp.status_code, 302) + + self.obs.refresh_from_db() + self.assertEqual(self.obs.status, 'to_process') + + def test_manager_can_classify_not_relevant_from_needs_info(self): + self.obs.status = 'needs_info' + self.obs.save(update_fields=['status']) + + self.client.login(username='manager_user', password='pass') + url = reverse('observations:observation_change_status', args=[self.obs.id]) + + resp = self.client.post(url, { + 'new_status': 'not_relevant', + 'comment': 'Sans suite après absence de réponse', + }) + self.assertEqual(resp.status_code, 302) + + self.obs.refresh_from_db() + self.assertEqual(self.obs.status, 'not_relevant') + + + diff --git a/loko/observations/views.py b/loko/observations/views.py index fa85da6..3db93e2 100644 --- a/loko/observations/views.py +++ b/loko/observations/views.py @@ -103,6 +103,7 @@ def observations_index(request): _status_badge_map = { 'in_preparation': 'secondary', 'to_process': 'danger', + 'needs_info': 'warning', 'in_progress': 'warning', 'processed': 'success', 'not_relevant': 'secondary', @@ -205,6 +206,7 @@ def observations_index_mobile(request): _OBS_STATUS_COLOR = { 'in_preparation': 'secondary', 'to_process': 'danger', + 'needs_info': 'warning', 'in_progress': 'warning', 'processed': 'success', 'not_relevant': 'secondary', @@ -744,7 +746,7 @@ def observation_detail(request, observation_id): # Vérifier si l'utilisateur peut supprimer l'observation can_delete = ( - observation.status in ('in_preparation', 'to_process') and + observation.status in ('in_preparation', 'to_process', 'needs_info') and can_edit ) @@ -894,12 +896,30 @@ def observation_detail(request, observation_id): def observation_change_status(request, observation_id): observation = request.observation_obj new_status = request.POST.get('new_status') - allowed_statuses = ['to_process', 'processed', 'not_relevant', 'to_redirect', 'duplicate', 'closed', 'archived'] + allowed_statuses = ['to_process', 'processed', 'not_relevant', 'to_redirect', 'duplicate', 'needs_info', 'closed', 'archived'] if new_status not in allowed_statuses: return HttpResponseForbidden("Statut non autorisé.") - # Règles simples de transition - if new_status == 'to_process' and observation.status != 'in_preparation': - return HttpResponseForbidden("Transition non autorisée.") + + referer = request.META.get('HTTP_REFERER', '') + is_mobile = '/mobile/' in referer + redirect_target = 'mobile:observation_detail_mobile' if is_mobile else 'observations:observation_detail' + + # Règles de transition vers 'to_process' + if new_status == 'to_process': + if observation.status == 'in_preparation': + pass + elif observation.status == 'needs_info': + # Autorisé pour le créateur ou les gestionnaires habilités + is_creator = (observation.created_by == request.user) + access_context = request.observation_access + can_manage = access_context.can_process_observation(observation) or any( + access_context.user_config.has_role(r) for r in ['admin', 'operator', 'manager', 'controller', 'top_manager'] + ) + if not (is_creator or can_manage): + return HttpResponseForbidden("Vous n'avez pas la permission d'effectuer cette action.") + else: + return HttpResponseForbidden("Transition non autorisée.") + # Contrôle des rôles/permissions pour 'processed' (tous types) if new_status == 'processed': access_context = request.observation_access @@ -908,15 +928,28 @@ def observation_change_status(request, observation_id): fallback_roles = ['manager', 'controller'] if not any(access_context.user_config.has_role(role) for role in fallback_roles): return HttpResponseForbidden("Vous n'avez pas la permission de traiter cette observation.") - # Contrôle des rôles pour 'not_relevant', 'to_redirect' et 'duplicate' - if new_status in ('not_relevant', 'to_redirect', 'duplicate'): - if observation.status != 'to_process': + + # Contrôle des rôles pour 'not_relevant', 'to_redirect', 'duplicate' et 'needs_info' + if new_status in ('not_relevant', 'to_redirect', 'duplicate', 'needs_info'): + if new_status == 'not_relevant': + if observation.status not in ('to_process', 'needs_info'): + return HttpResponseForbidden("Transition non autorisée.") + elif observation.status != 'to_process': return HttpResponseForbidden("Transition non autorisée.") + access_context = request.observation_access if not access_context.can_process_observation(observation): - fallback_roles = ['manager', 'controller'] + fallback_roles = ['manager', 'controller', 'operator', 'admin'] if not any(access_context.user_config.has_role(role) for role in fallback_roles): return HttpResponseForbidden("Vous n'avez pas la permission d'effectuer cette action.") + + # Pour 'needs_info' : le motif / commentaire est obligatoire + if new_status == 'needs_info': + comment_text = (request.POST.get('comment') or '').strip() + if not comment_text: + messages.error(request, _("Veuillez préciser les informations complémentaires demandées.")) + return redirect(redirect_target, observation_id=observation.id) + # Pour "à rediriger" avec choix d'un nouveau symptôme : mettre à jour symptôme/catégorie/thématique # Dans ce cas, le statut n'est PAS modifié et un commentaire automatique est ajouté. symptom_changed = False @@ -952,6 +985,7 @@ def observation_change_status(request, observation_id): new_status = observation.status # conserver le statut actuel except Symptom.DoesNotExist: pass + # Enregistrer un commentaire optionnel (pour tous les autres changements de statut) if not symptom_changed: comment_text = (request.POST.get('comment') or '').strip() @@ -961,15 +995,41 @@ def observation_change_status(request, observation_id): author=request.user if request.user.is_authenticated else None, comment=comment_text, ) - # Archiver seulement depuis 'processed', 'not_relevant', 'to_redirect' ou 'duplicate' - if new_status == 'archived' and observation.status not in ('processed', 'not_relevant', 'to_redirect', 'duplicate'): - return HttpResponseForbidden("Seules les observations traitées, non relevantes, à rediriger ou doublons peuvent être archivées.") + + # Si passage en needs_info : publier aussi dans la discussion pour alerter l'auteur + if new_status == 'needs_info': + from notifications.services import get_or_create_observation_discussion, post_message + discussion, _ = get_or_create_observation_discussion(observation) + post_message( + discussion=discussion, + author=request.user, + content=_("Demande d'informations complémentaires :\n%(comment)s") % {'comment': comment_text} + ) + messages.success(request, _("La demande d'informations a été envoyée à l'observateur.")) + + # Si retour à to_process depuis needs_info : + elif observation.status == 'needs_info' and new_status == 'to_process': + res_content = ( + _("Compléments d'information fournis :\n%(comment)s") % {'comment': comment_text} + if comment_text else + _("Compléments d'information fournis. L'observation est à nouveau à traiter.") + ) + from notifications.services import get_or_create_observation_discussion, post_message + discussion, _ = get_or_create_observation_discussion(observation) + post_message( + discussion=discussion, + author=request.user, + content=res_content + ) + messages.success(request, _("L'observation a été renvoyée pour traitement.")) + + # Archiver seulement depuis 'processed', 'not_relevant', 'to_redirect', 'duplicate' ou 'needs_info' + if new_status == 'archived' and observation.status not in ('processed', 'not_relevant', 'to_redirect', 'duplicate', 'needs_info'): + return HttpResponseForbidden("Seules les observations traitées, non relevantes, à rediriger, en attente d'infos ou doublons peuvent être archivées.") observation.status = new_status observation.save() # Rediriger vers la vue mobile ou desktop selon l'origine de la requête - referer = request.META.get('HTTP_REFERER', '') - is_mobile = '/mobile/' in referer if is_mobile: return redirect('mobile:observation_detail_mobile', observation_id=observation.id) return redirect('observations:observation_detail', observation_id=observation.id) @@ -1125,6 +1185,7 @@ def observation_add(request, observation_id=None): observation.created_by = request.user # Gestion du statut selon le bouton utilisé + previous_status = observation_to_edit.status if observation_to_edit else None if 'save' in request.POST: observation.status = 'in_preparation' elif 'send' in request.POST: @@ -1213,6 +1274,20 @@ def observation_add(request, observation_id=None): except json.JSONDecodeError: pass # Ignore invalid JSON + if observation_to_edit and previous_status == 'needs_info' and observation.status == 'to_process': + ObservationComment.objects.create( + observation=observation, + author=request.user, + comment=_("Observation modifiée et renvoyée par l'auteur."), + ) + from notifications.services import get_or_create_observation_discussion, post_message + discussion, _ = get_or_create_observation_discussion(observation) + post_message( + discussion=discussion, + author=request.user, + content=_("L'observation a été modifiée et renvoyée par l'auteur.") + ) + # After creation/update, redirect to detail messages.success(request, "Observation enregistrée avec succès.") @@ -1933,6 +2008,23 @@ def observation_add_message(request, observation_id): files = request.FILES.getlist('attachments') or None post_message(discussion=discussion, author=request.user, content=content, files=files) + # Si l'observation était en attente d'informations et que l'auteur répond, + # basculer automatiquement l'observation à 'to_process'. + if observation.status == 'needs_info' and request.user == observation.created_by: + observation.status = 'to_process' + observation.save(update_fields=['status']) + ObservationComment.objects.create( + observation=observation, + author=request.user, + comment=_("Réponse apportée dans la discussion : l'observation repasse à l'état 'À traiter'."), + ) + messages.success(request, _("Votre réponse a été envoyée et l'observation a été renvoyée pour traitement.")) + + referer = request.META.get('HTTP_REFERER', '') + if '/mobile/' in referer: + return redirect( + reverse('mobile:observation_detail_mobile', args=[observation_id]) + '#discussion' + ) return redirect( reverse('observations:observation_detail', args=[observation_id]) + '#discussion' ) diff --git a/loko/reporting/templates/reporting/observations_dashboard.html b/loko/reporting/templates/reporting/observations_dashboard.html index 7c9a6da..c585a5a 100644 --- a/loko/reporting/templates/reporting/observations_dashboard.html +++ b/loko/reporting/templates/reporting/observations_dashboard.html @@ -969,6 +969,7 @@ fr: { 'in_preparation': 'En préparation', 'to_process': 'À traiter', + 'needs_info': 'Informations demandées', 'in_progress': 'En cours', 'processed': 'Traité', 'not_relevant': 'Non relevant', @@ -979,6 +980,7 @@ nl: { 'in_preparation': 'In voorbereiding', 'to_process': 'Te behandelen', + 'needs_info': 'Informatie gevraagd', 'in_progress': 'In behandeling', 'processed': 'Verwerkt', 'not_relevant': 'Niet relevant', @@ -991,6 +993,7 @@ const STATUS_BADGES = { 'in_preparation': 'bg-secondary', 'to_process': 'bg-danger', + 'needs_info': 'bg-warning text-dark', 'in_progress': 'bg-primary', 'processed': 'bg-success', 'not_relevant': 'bg-secondary',