From 298219ab30abe79ed677f45101fa84b4685e4bfd Mon Sep 17 00:00:00 2001 From: emaric Date: Thu, 18 Jan 2024 12:03:37 +0100 Subject: [PATCH] dodan user --- .../__pycache__/serializers.cpython-38.pyc | Bin 1677 -> 1867 bytes .../__pycache__/tests.cpython-38.pyc | Bin 2755 -> 2747 bytes plovidba_aplikacija/serializers.py | 3 + plovidba_aplikacija/tests.py | 2 +- .../__pycache__/settings.cpython-38.pyc | Bin 3671 -> 3793 bytes .../__pycache__/urls.cpython-38.pyc | Bin 1852 -> 1884 bytes plovidba_projekt/settings.py | 9 +- plovidba_projekt/urls.py | 1 + user/__init__.py | 0 user/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 166 bytes user/__pycache__/admin.cpython-38.pyc | Bin 0 -> 1623 bytes user/__pycache__/apps.cpython-38.pyc | Bin 0 -> 438 bytes user/__pycache__/forms.cpython-38.pyc | Bin 0 -> 724 bytes user/__pycache__/models.cpython-38.pyc | Bin 0 -> 3576 bytes user/__pycache__/urls.cpython-38.pyc | Bin 0 -> 1128 bytes user/__pycache__/views.cpython-38.pyc | Bin 0 -> 10441 bytes user/admin.py | 47 +++ user/apps.py | 6 + user/management/__init__.py | 0 .../commands/create_groups_and_permissions.py | 42 +++ user/management/commands/create_users.py | 71 ++++ user/migrations/0001_initial.py | 44 +++ user/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-38.pyc | Bin 0 -> 2352 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 177 bytes user/models.py | 88 +++++ user/permissions.py | 46 +++ user/serializers.py | 162 +++++++++ user/tests.py | 3 + user/urls.py | 18 + user/utils.py | 42 +++ user/views.py | 330 ++++++++++++++++++ 32 files changed, 912 insertions(+), 2 deletions(-) create mode 100644 user/__init__.py create mode 100644 user/__pycache__/__init__.cpython-38.pyc create mode 100644 user/__pycache__/admin.cpython-38.pyc create mode 100644 user/__pycache__/apps.cpython-38.pyc create mode 100644 user/__pycache__/forms.cpython-38.pyc create mode 100644 user/__pycache__/models.cpython-38.pyc create mode 100644 user/__pycache__/urls.cpython-38.pyc create mode 100644 user/__pycache__/views.cpython-38.pyc create mode 100644 user/admin.py create mode 100644 user/apps.py create mode 100644 user/management/__init__.py create mode 100644 user/management/commands/create_groups_and_permissions.py create mode 100644 user/management/commands/create_users.py create mode 100644 user/migrations/0001_initial.py create mode 100644 user/migrations/__init__.py create mode 100644 user/migrations/__pycache__/0001_initial.cpython-38.pyc create mode 100644 user/migrations/__pycache__/__init__.cpython-38.pyc create mode 100644 user/models.py create mode 100644 user/permissions.py create mode 100644 user/serializers.py create mode 100644 user/tests.py create mode 100644 user/urls.py create mode 100644 user/utils.py create mode 100644 user/views.py diff --git a/plovidba_aplikacija/__pycache__/serializers.cpython-38.pyc b/plovidba_aplikacija/__pycache__/serializers.cpython-38.pyc index d7a505164bf26ed75e4048c5698c53dce7f87fca..ff5b698f18317c2046237d37610c0ddfc63e2888 100644 GIT binary patch delta 413 zcmY+9y-EW?5XW!!cF#+4n~pTGwYOPdJ3gN?N_XA^gs-_FN>c6T21TUj4#U9@Q3-9N{pseXm6>HDS^$y@8p zjnt9Va$bHsz$>Y51j!WHK3Td-$gB*mA%Yca0=XyHn+WQ%JH{o2Ss=88vFVtpi)0vP z6qPuVQJ95BvnWTIlhte(^aojN24NChhRHbgD4p`l7Jfm!`oGv|6;O!-sNtbl_xM2< zXbacIk@zevQo<}Tmhr@HS8C*@GFM43K0eyrLX*o?0@`v1>bNJLpot%{N8DAu+Mtq; zr)s-9cN_OOjJ(@tufkyQOb0@O}NGq9@8`k+>ri#*%4*wDbv7j)rSV2 IJ8dZb0<+0a8UO$Q delta 250 zcmX@j*UQTr%FD~e00bZFm#4B$KjKz?jAIL0b0jptQERvWU#*)M(3FO{lNl7e8oP3bQ zf>C(#FP0gMl9MO1CM$xZqS*42vQo24ig2*C$pDFF$I$>MBgDoB=qBtT9m0_iN0 z01+T7i)A3HMJ7wK8#1y30028H APyhe` diff --git a/plovidba_aplikacija/__pycache__/tests.cpython-38.pyc b/plovidba_aplikacija/__pycache__/tests.cpython-38.pyc index 469cf737e89843ba9ceb67dc5e415935ef85395d..8523ccd6dda1c85677ede3e6e6c854ade9475b4f 100644 GIT binary patch delta 38 pcmX>sx?7Ytl$V!_0SIE-mZxsr$lJ-mRU{AO7AXLU%_}&hnE<$d2+sfj delta 46 zcmdljdRUY zMfu^G|HB}&LF)W`m3qC8G1uVZ2w@yyOdx_uOu2p@MwCw=Cj2|2Zz4j+^pWZ^1Q168 zGnmDktF;*ByPC`RBo_D-l30WVo5m7SNO$!X<56V92(nl{P+Lqyk&Gx<$thUnF|5(f zY3yfkjtH0i;wA!MoFsN){y=3h}VbR*9`^e`MVH0>OY!9G7{^ZrgBbBF6;^`rA zh-6Ua*&(?}WO?W+V^=SGWm($h4*M(*&9slHzvU;>@oSo< L{?&rgKXUgU%43@n delta 473 zcmYk3xk^J(5Qgu$iPwu+u!zQ(o5dxGNp9SC;~KY0M6sFrKU@-Qj1M3ph?OV@K`@0j z)`3)ZX?y@H3lT&g!N$@_z~CJI!~7pJ%pA^3SiO0-jVS*^Ii}LSL!;103+m2S U5_M(xZSk$9{2QV}o%(lw0kY0{A^-pY diff --git a/plovidba_projekt/__pycache__/urls.cpython-38.pyc b/plovidba_projekt/__pycache__/urls.cpython-38.pyc index 88c0bdce0c3e4b33347ae5e49c32b33503750a1e..4d6bd40907c5ec9ead1cc841994b6845ac1fef1a 100644 GIT binary patch delta 169 zcmdnPcZZKRl$V!_0SNBxU!K}Fkyn;cWuo@=pj5ULnN;=^*=ELOrgYXQ@l>WPi3yBF z3aR2i8M##9EU^^P6vh;>UZxcB6p3DDpr|}pR0=Ap01{1SGG~YqPf-kJ&{W#&!6?Wg z$XZ&QTBKja38M5$i*kxLN3hOeWOSSSmCZnsk>@uH2L~e)BMT!B1Ji#NCMf+E0H|py AzW@LL delta 121 zcmcb^w}+26l$V!_0SJDnE=`S@$ScbzF;V-vP&!MLcq&tt#0177kyPQ3g`kg2QWPO2TqlFvZQ0040QE9L+I literal 0 HcmV?d00001 diff --git a/user/__pycache__/admin.cpython-38.pyc b/user/__pycache__/admin.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b30f3362683e6f46ad3d9e33b2352fb7952a8f28 GIT binary patch literal 1623 zcmZuxOK;pZ5Ek{8R`Rax^pW=BLl0e~HIN>QAV`q{K@W9<1i1(c2+VSJLr0GdNjb2u z?$78UxwU_Z*Pe3fJ%^?<-1V-TSb|31aL6I&n;AYU%iP1K|L0G*6MNp@q+EX@R36}` z-=GklV2#(Zim{Ke-}r4%1#UcO!ZxZR#G#0qxJ|00O{Zr`3_I%O_`0j zXk~|BZwaOzG^o1m1vEI4Jwxp$GQVRq*|)`SYRNl!!Hw)Yd+RaR`gO0w6VPCuz^ehY znUmk^4=sIwpPrxquVTWhe8H+f_;`-M?qB!mcEgRrj<;aTdEXg6H#M|eHYd~2rgcr0 zy0&>u=NuZsMP0udc(bt-jnt+VQm-0*{+Fl5xUUoMA3gkWMltExQ!@zY%-z|~pkJDP zHCr|PnH10YtUZVBY^&BQ)t|ykGb3VVj;iC;xk}MxK{5F#Qs(qe0>>g*2R=iv3tadj zkZj>&%6${YQCSBaQZso1YF!H~oQJAOE5 z)3@7%o!EFGp%EInj2wG5U2U>;Q9ecr`i?h8vRm|+f_xzrQsy|%8h!++VI7~7KDZsDB{n?OVtl6Z6~DFIC|ypV*E?H z>XqT0ITG9e9XodiY5Jw??1%>I(_VHUurR~dQouA&he)i=uHw48gFoQ!X7rfTMm2)m~mKuf^H5|=4_jdc588*cE;Zk{c<(JpQ3tD zB?_CZsRyHN$%Ux5s@fy-$kpdUHU=pvHE=b5v6*92>?TmMTiUS#F8Z!H-I5!par2Bk7-<|JnF&y>?%G>9=dd2t4JI4~DGeWf&2ta`5q@W3<Q2VSOmvxMs|O!B7K+XY)t(_Jl29O6O%nz|5(2|6q3bQIs@nRe zb6CrArDIseg$9+|_%>UOn_5pZm4gc#q|Ty_>k3iILJ8q|LLjrry*&`(v61;9a|$Yz zO_}mAn{2li2Go4&f&)^f=y*(Zh@@rf8=^Q&A6Gdm=!;u_v2E1LMJ^KAfU>nK>i>#dHp z>-Hdd4eZ##uyBIynkzF+ zf`2e?Mtk%ZreuhQBdwX_h69@T_yARHqLpuK)TtOC-RagQZd+Y9=}xah_~5+RzIC4- ztVAl5!ak+aT&dQXp+SG4)cZjlf-Fl8xy_}zXjRR{Mz4rUZIXq9mc`f zE^b$^Y&eguulmMa)aF=M?WOH5ZbH?2cV^FHg*vNo_ho;X7>b_(ZonNBlu>^loP*WO z-$k|snkAYHOq##O+4fM7FEESEx4I_p9_9oXoZ=5|LB|Z>CSlHWw{{cY!zn0FTo-+P mT)9cDZ>xW(nj#PVJ6 z4tK%3f^`cR@i0>09!5N23FpN2ix?^L5=Kg+IVC>TGV0|s>}S-Oa{CDvy|iwr+VXCi z$*`S03RAJ23aO^Y=xG>-dl;xa6S5zrX_Un2)96JMw`rA&j|Ju--t>DQ!tfb4e3LWZ zf?lLM^X*(`-{B_o@YED-HQM0##t1BS61g~lAZ@m3$?-$xa`AqOH+=#!WNA5r*-qG> z+0f{+4$L+*-k3vn$|5$j{%Zb(y|RaP*J8%deq#^KQy3S#gV`>(UTY4D*v;enzYlv6 zUlc#>hf!~_KS;C1U9os5lcR_W{y;f`1aFyg25A)U)eEYGF9xC^Wg?ZM4XLK%ME0@H zu?X~Z<bF-zGndAMywBD|pjifxxox%h2qY zLu+Vr_BZkY+&y#xrYTU@!Yq4ae@ENDZmLozl4%xbYZZIp2yRIatJWQP8$Zg6(jd*k zPDhmq4Gx8*Bgkn8WsN4f@&;kDrko*ihsa$bvqZ>{Ihg}ewb8*QOVOlMr!AX)#mKoXr_lhkT6jEx_{6e+qS%mYwKc*9POz>snNAKJD?lDZ2HbJ??>@lTe*8O z862j{6)&PROO>PRf&39xc+2vvzMLt#8ie%quQ4a3Jq`1?LPlFcbp2gv(;yf9ZyLzCbMLfpscP>i z{CM1BjR%!ps>kEzL)5vTrO z?2+1KeIkBE8>&S*Ug-|u>Q_UrKil9;=b3O-YhweM%L17h#z!@!h;vnIG}gmB?s!2{+vSQr-ni w3*Sicx1?4hk6q^}{U6e4qV?7bU#E-mKFXWCPw2MO{bJdkHEXwK?3oY#13+9h(*OVf literal 0 HcmV?d00001 diff --git a/user/__pycache__/urls.cpython-38.pyc b/user/__pycache__/urls.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9c6f8e945254f4116e9cde6021839c64b03581c GIT binary patch literal 1128 zcmY+COK;Oa5XbE}ujb)AN=pl+Qn~feT1bdPRYgdtI3cLg$6;m3as0l3 z;5)z#zEZB7IKc%DoR}Tk5yZ-R|G$|X?fl1A-EPZ(-|rt^*raM0-?aGm*8}ky{_-a* z)SzZ)L?$s!y|09o$RZZ*t*{!^NDcN?QfDTuF>BuV)ckDFI&FNkfHZ+@(2YX1K(#R5 zE=UJR8%ehyHmjlT6}k@@9VCN-41si!Y!>7Z5F5!>LAHVPkUTEP4v;>Q-GV#;GC=ZF zliM~K(!C4g@Y#3h+Bh^<)x=kK@UATbb}wPmyW|TNUo#QR9^T$?Aw4GQhXTZspq z_}&dLJxM%0B4Uqmc2{sqoM^+Bo=y}^U2J!pO2wo1IEqU>v2z%QP9%GIXw3N{U>KU@ z)4?*Ww`ezJ4$h0tCfZ^|JO80^?j%OGChS8B9q0?4p=*eTW3;V2hRi-gd)8NPgB|e2 zDkft2)eyF}L@$5dj89yhCEW{^QWmQh?irH{#S=FP`4YZL-;Ewvyj)8!5q!=T%1z;z nt8G3`9#+lMh|@G=BNM91eONjsJe6u?Xxiowo<6R62hXp+?5b)} literal 0 HcmV?d00001 diff --git a/user/__pycache__/views.cpython-38.pyc b/user/__pycache__/views.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70b2e3105404d816fc063e7a4e43a3a41198cd17 GIT binary patch literal 10441 zcmdT~%XiyWddG_(NKq6m%d+JsY$uMH*tFw#l8Glttw{1CaU_o(PJ_BdVd6>x1rW*w zC|e9QovD4A&a_UakJCl#(<61!*6VJ%>ZY0g5jfqnJzY!}v+N>mCjEUEph(G5+Ad2T ze7J91-245$*Tur%U{S*FAAa#SZ1xRF`d7;I|1@N-tI-oVU&gRjZ2pA$Qb&HF(W> zEx2G^5c$LY#h_-@MEZz-DY$H14qmrj53X2Of;X%;f~(fm;5*iLf;X) znL^18lw5ExqU2&$a?@4jq-yQw#0gb7(K&0nj87wJL@bWI=2ArIfJKqB#3+5(Ws6SB zkL@^IVNH99H5qr}kfW-y#A3VN;us{hBQIuEB`Lq}xVM;tL8mwmc~Xe*1Uqm%A00}! z;&^qIMXO;mV)Pu&T#vJQOoOFU!DH+w=9mb|@|>+Po;HoWfVIOWb<5+qF?&S44n_~1 zr6tB~r$rs>%Pep(3qwrPt4E0*#ZKH3FE#G_;luIfI=YoUn46iN74OdIO^m)t<;U;d zzUQ%r80O$Sz4Oe2*zuZo9gkCSQW1r-Y>~5QnM!^R07+;)a@UEXhaq<{7>j4wr!5x6 ziM9}lN0jM)m+`=hA}?%KjpWQkD~iKlA`DkN)<5VRia5&yA^$9BQ(cJhS&A`N{{wR7gk*-W_@+yt$KMZrM3`zeqAL7%YHfB z4O|nrqMv}YE3vdGcjS(;rEI7lOU>h&6e}x54g}IaR$ckAtV^FrbJDL$9o3aq3j7DI z(ot=#qi*K5(553b^hTaPh>guc2a;+_LBC==&?r5Y=OkC#(9)WYwzH;bVDy}5gVC${ zbo=Z=(|O=CGY(BKo42@bu7@pdvWvvDrsHCpqxtrkTWgN*nD?B(Y&zy@7_V{HGr?`$ zHmugVhj;s*o0)TtYevir!k_&;i<#N-!zB-p^30YQt%VJ9CFEYz^uBz$;+fp}@@cDC zYfWS5d(O|c_NX))3={wIbJJOgy)}%~G(F!-b57lBL>R{Rnk#10LrX7O<<6SZtW^j3 zOBgg60A}hdDY}!w)T26EjlrM1K$Sz{1?_suR?JPb$XR2F)&id-6)z&#Ed%{t9cEoF-=AdBN*u1#Vu9owZka*kCL1eVXF^ zHNZ2ft*$4AQ^(?9k}qS}=s1X^C~`$E<8R2iY$%3Yl8cH4T2lDysIOC_{-1)(bzBjx z8;&wUE5-H|p`@H$el*2uZq ziq4nY!zk5(Gp&&)6aZ=GfHVV0Tg;GFh>L^tvo4m%H6OHM;DQ5$RNQkIShMammY(ckWGfXtExfoUzF@Yq% zEYw_79ZJfi2r>0cYV)g@U}69skQttvlvf#F40(`^lValV_`>`xd+MX{+js1HQ?s{k z+@2VpzdbXZ92Ocgo1u6+Tc8jFHZjv?o$+%RpO4T(WILVAfm`$QckL^euh`=g6H|BR zrzVqwVp1sStxY3 z4U7tersOj9)zeja59!DRkpL|~3V%f&Mp^;Pv|pBTC4fx-rLMIPrC8`I#W`06Qv(Kqg27$=Sq`kLyZMg(SlP%U zWq^&r!lFeHEe4(`f2yF)=;UdXCF&1l;toDjHwv-T$ajoIxl{N;n8;xJ@^pKU0DP@0 zTBs^q z8E!6x1Ri?aH9(;7AdZ_h8CzD_@ngoDkY*y^KH7c5R5|qHt*R#4gV0@1lsF`P@Ss69 zo{=h?0`X-k?@M@#WcWeL^Ih9{5QTmVgSWWHZ=-@=Co%wHfe~Ruij}Aq`6QJSy9#|_ z6~$OK$scQwGan1N#%y{4{sDC!gjiigDFYgLWR>V;NTbD&6IO|qp_>!9-Nn|N_<0sr z^eTu1ZU#?ln&4(#*8fw_7u78Hbx|iC*T2?tWx0KB7kBT2>S-8)hHW>UfZ29ZwCx~t zTRx>rw*6_#@w1w|ZM$LJw)t7iBB}I{Y1e(2o%|Mx#a#R5Pb!oXHu)70Vws4vi6Lu> zrWhll^aU-aq6PgT5;9#a$J9mH(pb*YKbIDveO%>K&b5rdF;xCN<%dZU}UC2}|x5$CXNOK84j6 z^2!?KsV|dxc*Jo0W2*fG1ggDYr#uGc>v0tEO+?p85QutJhln4BXjfoQ(S`q4dI3~v zzqT)P)LxivwZQLUK>mFqggZVAk__YBGS4ZU42$>)s+}i7lSu}%Y1WzVcc&`myGTt0 z3>J_IaYdw4$n4V;PG$Rpz(j2i1eVsv{yD+^Wag>K@%>ymGW!01Ab^wi!hfBJOGK>N z&i(#cw`yY_9XvB#F@cW*h_=e?cf9%aQ;cI{^a6+fm)X&@xlIF-Dxo8*9A#}$zn z?Cs_fMB0*M<(N*g##NswWW2)+R^7a7JdqyDFyjkk;3ma8;ZoR&X*UyP=EJWB5alp4 z5C6H6s8=puw$!U2SwpgE+9%Uasg(~4*fi^~37XLU1m5VmniRTdjFM6a4o|H;Sd52y zxjr4g!#ALpA~v3(7bEVDrN@%+34}Ox5z!OA0it(g!Q<6-l;E{;)BFhf&BLH2r1M_` zp?HmmPk3%s6DB7AO{!Cg*dWzhPnz^(2xnB^+aj7zqOFEe{MSects~`ic?5=Fd#JaT zHEID3xO@K(Quiz%gxHSIZ~*)=J!FVTi%9QrA|(*Mf{MSy6;(hCg%F+!B;{)@$Eo%O z?x01yjw`wg@_*xwKF%q)MP|4IPc^sX4hr5WxMeKzP;w~a6XJ<9wnA*geLRf^M4BL0 zu~*$bev`$fE5b053CF?A+-1Q@d=<@ia00ym0bmA~U~y8Q4LKT#l~`?PlR0NUoaLKWQMY%n;=MvVS zEUij!-O}Jx`&4@0cLBlIpJH^*iI56Q;MRz6giSWN9P{T7kEWWqyK{4Fk_x^~NiVj+_cvhrV=&Z+GS5<)@YbsFF}$W95tu;-ik zEttVBK!#XHd6M~v>Je8G%%@+pXO3Zz^aYM0mE=R=9Yu-i_kiron z<_QuMh-y#>N@^&;1Om1QND=RATN01K(?~beO&vBof=JP13{1G{lI+Q`(I~hYaW+~N zvsO83MT#oLf(t+0li{z!TSxm++8()FW3bo1YE1JtFc;`f_>Zeu@V!0sF%J>Q>E>%f zuq7qL+qT=lCQlDI+9$fNY_>#b8iiGp@{-E|U30W&5+^OQ`L#V>fOJ z15C#y*aHk{xEdHTmmKKs%PgIx5I?p*D4O^ISZRz75rNgvOGAC+TW7Ibo2nGd~q zd7B5IKrxEB!q#hIkR3992(vP93Esw;Bn&BVH2lvr#JZ_$Nk5gj(mqPcv?0Qwn^2ou zG6h~YatNxPK~Pm~=m@LMW?|KQ#--bb7;zCu8F&ny?&ysY;!lH{S9dQ3f z!Ob-eAo65%3S@z96ysq$?I8SIgRIe~Dh?M58v||;t%|TSo%54z^Yv%UpBgS>C$Izh_jI9#-%R1UleP8=`%$5H!gi9Vl*3<;40~9!$d^|zlecu zRMk{beSu6;$YNv6ZI`<|kZxc5DMDkfaE4r+;h>`%tQBD)^8u?n*m#OUp@C56%r$XR zlZl0l1<0BbQMW9#TPqf`FxHv}M?7V%7WXLzMl3^7`tA_dil2#h60xNXgu08pEaG$o z>Gq-9O%d}o(|{|iw^q}w@z3#U?Xju!NU2}Kh-h`KdW@fOT|aG$~GdD|Vazlu{oN zp+$jJ!^0pA?d>J*gr)xt8VbW!fGuHi=uGW*`eb8KK}Vl z{)L`91U2zv0oRAPqJIHtpBKjgFhN{-OcI9zsfeayf%JR~204V`1qkRoM6|I;r_A%c zv#-X0tGb#eld0Vmi4fE!D1bqTVIi(bP)8j)^=b^oMfAv%@e)@y55R~gaX#o8csdRh zI{E0YVw|XnQ48s)FmrO7!=hD@<_IGvw|S5Zes=)w!AN{)b94)ak9`=%k0=)-9Kln_ zpe#)b=wP)4g8q=+y0UP466d&PGmK3kY=pTlOd%nZ1Q<5j^AS93l9EARJBXw?6if1R1l(uzf6sPOoVuv{{fLfkVI{U z40m`R|($rNlP%&Z&KT4(_kMscbXNUB%^LUyI6X}dU12#8C>U}RlL5Ds*x zTSTbgh(brZ6?FunVOWK~fe>Yp!jaoHpAZRs@YydMAtb{xP43T#JR`EN1o(5*;;W+_ z5(O)D~>n;Bi)pNtd3SG70cyb3BHMWkRGrf-C0;8K~)ng z6s7Y1xj_$0f^#a9_2pwBTb~Q-9GeW4JAEc2Fzfhe8$cg2XkzKWG zutXz%h#q`$XB9qx!Z8zTJdA5;qfs>ChiMQUXF3c^AapKi_GF-cmd8X%PiRK`ICT}_ zZ4$MqQZgxZn#dUMEwm+)}OONAW~u6G;$ zkVYF!11H_GCsbCxN#A>U^=={QF~Q1xFuX)%Li!u;q^DQ!f&ZoGeUOr7DEOj6{43f0 g#e4BOt->e+1j8zU)s>8YAL<I2KRe!hX4Qo literal 0 HcmV?d00001 diff --git a/user/admin.py b/user/admin.py new file mode 100644 index 0000000..027ecb2 --- /dev/null +++ b/user/admin.py @@ -0,0 +1,47 @@ +from django.contrib import admin + +# Register your models here. + +from django.contrib import admin +from django.contrib.auth import get_user_model +from django.contrib.auth.admin import UserAdmin + +from .models import Organization, PasswordResetRequest + + +class OrganizationAdmin(admin.ModelAdmin): + list_display = ('name', 'contact_email') +admin.site.register(Organization, OrganizationAdmin) # noqa + + +class CustomUserAdmin(UserAdmin): + fieldsets = ( + (None, {'fields': ('email', 'password')}), + (('Personal info'), {'fields': ('first_name', 'last_name', 'organization')}), + (('Permissions'), { + 'fields': ('is_active', 'email_confirmed', 'is_staff', 'is_superuser', 'groups', 'user_permissions') + }), + (('Important dates'), {'fields': ('last_login', 'date_joined')}), + ) + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('email', 'password1', 'password2'), + }), + ) + list_display = ( + 'email', 'first_name', 'last_name', 'is_staff', + 'email_confirmed', 'organization', 'language_preference' + ) + list_filter = ('organization', ) + search_fields = ('email', 'first_name', 'last_name') + ordering = ('email',) + readonly_fields = ('date_joined',) + +admin.site.register(get_user_model(), CustomUserAdmin) # noqa + + +@admin.register(PasswordResetRequest) +class PasswordResetRequestAdmin(admin.ModelAdmin): + list_display = ('received_on', 'user', 'uid', 'confirmed') + list_filter = ('received_on', ) \ No newline at end of file diff --git a/user/apps.py b/user/apps.py new file mode 100644 index 0000000..36cce4c --- /dev/null +++ b/user/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'user' diff --git a/user/management/__init__.py b/user/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user/management/commands/create_groups_and_permissions.py b/user/management/commands/create_groups_and_permissions.py new file mode 100644 index 0000000..5719cdb --- /dev/null +++ b/user/management/commands/create_groups_and_permissions.py @@ -0,0 +1,42 @@ +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType +from django.core.management.base import BaseCommand + +from user.models import User + + +class Command(BaseCommand): + help = """ + Programatically set application permissions and groups\n + usage: python manage.py create_groups_and_permissions.py + """ + + def handle(self, *args, **options): + # get or create groups + admin_group, created = Group.objects.get_or_create(name='Admin') + operater_group, created = Group.objects.get_or_create(name='Operater') + viewer_group, created = Group.objects.get_or_create(name='Viewer') + + # define content types + ct_user = ContentType.objects.get_for_model(User) + + # define permissions + application_permissions = [ + # (codename, name, content_type, list_of_groups_to_assign_perm) + + ('add_user', 'Can add new users', ct_user, [admin_group]), + ('change_user', 'Can change existing user', ct_user, [admin_group]), + ('delete_user', 'Can delete existing user', ct_user, [admin_group]), + ('add_data', 'Can add new data', ct_user, [admin_group, operater_group]), + ] + + # get or create permissions and add them to appropriate groups + for permission_tuple in application_permissions: + permission_obj, created = Permission.objects.get_or_create( + codename=permission_tuple[0], + name=permission_tuple[1], + content_type=permission_tuple[2] + ) + + for group in permission_tuple[3]: + group.permissions.add(permission_obj) \ No newline at end of file diff --git a/user/management/commands/create_users.py b/user/management/commands/create_users.py new file mode 100644 index 0000000..1101490 --- /dev/null +++ b/user/management/commands/create_users.py @@ -0,0 +1,71 @@ + +import csv +import os +import sys + +from django.conf import settings +from django.contrib.auth.models import Group +from django.core.management.base import BaseCommand +from user.models import Organization, User + + +class Command(BaseCommand): + help = """ + Programatically create entries for model User + """ + + def handle(self, *args, **options): + + csv_fpath = os.path.join(settings.RESOURCES_DIR, 'users.csv') + csv.field_size_limit(sys.maxsize) + + created_entries = 0 + existing_entries = 0 + + admin_group, created = Group.objects.get_or_create(name='Admin') + operater_group, created = Group.objects.get_or_create(name='Operater') + + with open(csv_fpath) as f: + reader = csv.DictReader(f) + + default_organization = Organization.objects.get_or_create(name='Državna geodetska uprava')[0] + + for row in reader: + username = row['usrname'] + first_name = row['ime'] + last_name = row['prezime'] + email = row['email'] + email_confirmed = True + + if not email: + print("User {} {} doesn't have email but it's required. Setting fake email...".format( + first_name, last_name + )) + # set fake email + email = '{}.{}@example.com'.format(first_name, last_name) + email_confirmed = False + + obj, created = User.objects.get_or_create( + username=username, + first_name=first_name, + last_name=last_name, + email=email, + email_confirmed=email_confirmed, + organization=default_organization + ) + + if row['rola'] == 'operater': + obj.groups.add(operater_group) + elif row['rola'] == 'admin': + obj.groups.add(admin_group) + else: + obj.groups.clear() + + if created: + created_entries += 1 + print("Kreiran user {} {}".format(obj.first_name, obj.last_name)) + else: + existing_entries += 1 + + print("Created: {}".format(created_entries)) + print("Existing: {}".format(existing_entries)) \ No newline at end of file diff --git a/user/migrations/0001_initial.py b/user/migrations/0001_initial.py new file mode 100644 index 0000000..9ac6f89 --- /dev/null +++ b/user/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.9 on 2024-01-17 11:44 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/user/migrations/__init__.py b/user/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user/migrations/__pycache__/0001_initial.cpython-38.pyc b/user/migrations/__pycache__/0001_initial.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2546d9f70d2630054cd2d722eaca98f075fdb10b GIT binary patch literal 2352 zcmZuz&5s*36wgd1pPO{kZFjr0Pz#$P&1Ss-&)#ppP`Xek z{BDKgPXxj(_|#2kNPz^r&QfLwPr4bt2zg60Hh7GTCAX_1pov*hNS zBBxnEmdOeUXk*+QpBOJbZ@nlC1F}TU?3H2WssT%2p(1x+2IE{#u(Pm}k^CAaoAXqUm)`{XjYLavfFiQPKZ z=K6J~-yqk#e%J)74$Vb4&$ENo1fQ3!qB*K{< znTzY|8`l!dHI+%Gpcyc!bRzJCCKLQD;ZzL4EZTxG6Ebq&gkRljN2UR@)1)sc)|8lq zsbmu@XUR65nHrcf%vnxNV~5IKs;Kj4s#C1gZYs(C_rSADi^B-A2`W?P7=rV3$VBe? z$o1PrQ|fUnwoN25&b@7esYGQ{D($lKZ|+gWh5}ZjkRD+T4O7Stp%GRHbB@4t!j!T} zh<2GCrI|+b*_5+B(|m?tZQy{3Axx!{IBy*~M@nOzDOg2?&;ic4HnkDu(?ru}+ApSR zBU#W)ITP$@M)US^WN#Q^>x%AVTt=dh(f7E z1G-CPjJ~GYYOWx{hD^hT1VOjf;SJMfMoj3`^M=WU38a{&ao%tq<{X$BxCC(64$bK+ zeIBr`fL~5fk0R}p6bMFsW{kFkqMY{US;B!dZ4TgmKg|TNW6FM#Spdvv zGESMGBzFSxLOYbp7AS{Nc_`Cts&c#uyYqd&C6|gl%2P3f3{7Knm>-auL#qFmqHtge zs4cKX6!%AVb$}R?bFZ;+IhQD^O=O+tbz9J}i`g!F?kx4DAt|?2>WgscK!PNNf^|6? zLNd73x2&V&SfS(>o=j<<4Oss_cz&LH02ZvFO|s{i(zUi{Zwelda`8G;+%E3x+Pd2{BO;ViEj^T(A2-M z<>MWcGR-)YjfMbuDyS)nbk{6C#A3FY+SZ#bST7)208Bjv-2K)1Ye7D+5pDC=cXvPS zT07N!qBGd8-snQTZ0mH|o$_>tksj_&W>oAPipf-_W4f)oma6X2jon>`J0h9m7f)x> z{zs_6r*1$~4WnZCGAczu7@iJV!4kZeTx)wv!Ah_ito|9c!T{dCqjKwI_($~8`Djg`kg2QW//', views.activate, name='activate'), + path('token/', views.CustomObtainTokenPairView.as_view(), name='token_obtain_pair'), + path('token/refresh/', views.CustomCookieTokenRefreshView.as_view(), name='token_refresh'), + path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), + path('token/logout/', views.LogoutView.as_view(), name='token_logout'), + path('password-reset/', views.PasswordResetView.as_view(), name='password_reset'), + path('password-reset//confirm/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), + path('me/', views.RequestUserDetailView.as_view(), name='user_detail'), + path('me/change-password/', views.ChangePasswordView.as_view(), name='change_password'), + path('/delete/', views.DeleteUserView.as_view(), name='user_delete') +] \ No newline at end of file diff --git a/user/utils.py b/user/utils.py new file mode 100644 index 0000000..dff920e --- /dev/null +++ b/user/utils.py @@ -0,0 +1,42 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.tokens import default_token_generator +from django.core.mail import EmailMultiAlternatives +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode + + +class Util: + @staticmethod + def send_email(html_content, **data): + try: + email = EmailMultiAlternatives(**data) + email.attach_alternative(html_content, "text/html") + email.send() + except Exception: + raise Exception("Sending e-mail went wrong") + + @staticmethod + def get_token(user): + + try: + data = { + "uidb64": urlsafe_base64_encode(force_bytes(user.pk)), + "token": default_token_generator.make_token(user) + } + except Exception: + return None + return data + + @staticmethod + def check_token(uidb64, token): + try: + User = get_user_model() + uid = urlsafe_base64_decode(uidb64).decode() + user = User.objects.get(pk=uid) + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + user = None + + # Check token + if user is not None and default_token_generator.check_token(user, token): + return user + raise Exception("Token invalid") \ No newline at end of file diff --git a/user/views.py b/user/views.py new file mode 100644 index 0000000..bdf1d6e --- /dev/null +++ b/user/views.py @@ -0,0 +1,330 @@ +from django.shortcuts import render + +# Create your views here. + +import uuid + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.tokens import default_token_generator +from django.contrib.sites.shortcuts import get_current_site +from django.core.mail import BadHeaderError, send_mail +from django.http import HttpResponse +from django.shortcuts import redirect +from django.template.loader import render_to_string +from django.urls import reverse +from django.utils import timezone +from django.utils.translation import gettext as _ +from drf_yasg.utils import swagger_auto_schema +from rest_framework import generics, status +from rest_framework.permissions import AllowAny +from rest_framework.renderers import JSONRenderer +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework_simplejwt.views import (TokenObtainPairView, + TokenRefreshView) + +from .models import PasswordResetRequest, User +from .permissions import UserPermission +from .serializers import (CustomCookieTokenRefreshSerializer, + CustomTokenObtainPairSerializer, + PasswordResetConfirmSerializer, + PasswordResetSerializer, PasswordSerializer, + UserDetailSerializer, UserDetailUpdateSerializer, + UserSerializer) +from .utils import Util + +def activate(request, uidb64, token): # TODO: rewrite to CBV + + try: + user = Util.check_token(uidb64, token) + except Exception: + return HttpResponse("Unable to verify your e-mail adress") + if user is not None: + user.email_confirmed = True + user.save() + + if request.user.is_authenticated: + messages.success(request, "Hvala Vam na potvrdi email adrese.") + return redirect(settings.FRONTEND_URL) + else: + msg = "Hvala Vam na potvrdi email adrese. Sad se možete ulogirati u svoj korisnički račun." + messages.success(request, msg) + return redirect(settings.FRONTEND_URL) + else: + return HttpResponse('Vaš korisnički račun je već aktiviran ili aktivacijski link nije ispravan.') + +class CreateUserView(generics.CreateAPIView): + """Create a new user in the system.""" + + serializer_class = UserSerializer + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + if settings.AUTH_EMAIL_VERIFICATION in ["mandatory", "optional"]: + try: + self.send_confirmation_email(request, serializer.instance) + except Exception: + return Response( + _("User was created but server wasn't able to send e-mail!"), + status=status.HTTP_202_ACCEPTED + ) + + headers = self.get_success_headers(serializer.data) + response_msg = { + "account_created": serializer.data, + "info": _("Confirmation e-mail was sent to your e-mail address, please confirm it to start using this app!") + } + return Response(response_msg, status=status.HTTP_201_CREATED, headers=headers) + + def send_confirmation_email(self, request, user): + token_data = Util.get_token(user) + relative_link = reverse('user:activate', kwargs=token_data) + link = request.build_absolute_uri(relative_link) + + # Variables used in both templates + template_vars = { + 'email': user.email, + 'link': link, + 'greeting': _("Hi,"), + 'info': _("Please click on the link to confirm your registration!"), + 'description_msg': _("Your e-mail:"), + } + text_content = render_to_string('acc_activate_email.txt', template_vars) + + # Adding extra variables to html template + html_content = render_to_string('acc_activate_email.html', { + **template_vars, + 'bttn_text': _("Confirm email"), + 'alternate_text': _("Or go to link:") + }) + data = { + "body": text_content, + "to": [user.email], + "subject": _("Please confirm your e-mail"), + } + Util.send_email(html_content, **data) + return + +class CustomObtainTokenPairView(TokenObtainPairView): + permission_classes = (AllowAny,) + serializer_class = CustomTokenObtainPairSerializer + + def finalize_response(self, request, response, *args, **kwargs): + if response.data.get(settings.AUTH_REFRESH_TOKEN_NAME): + # # # Move refresh token from body to HttpOnly cookie + + refresh_token_name = settings.AUTH_REFRESH_TOKEN_NAME + persist = request.data.get('persist', False) + max_age = response.data['lifetime'] if persist else None + + response.set_cookie( + refresh_token_name, + response.data[refresh_token_name], + max_age=max_age, + secure=settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'], + httponly=settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'], + samesite=settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE'] + ) + # Remove from body + del response.data[refresh_token_name] + del response.data["lifetime"] + + return super().finalize_response(request, response, *args, **kwargs) + +class CustomCookieTokenRefreshView(TokenRefreshView): + serializer_class = CustomCookieTokenRefreshSerializer + + def finalize_response(self, request, response, *args, **kwargs): #purpose is to customize the HTTP response generated by the view after refreshing an access token. + if response.data.get(settings.AUTH_REFRESH_TOKEN_NAME): + # # # Move refresh token from body to HttpOnly cookie + + refresh_token_name = settings.AUTH_REFRESH_TOKEN_NAME + persist = request.data.get('persist', False) + max_age = response.data['lifetime'] if persist else None + + response.set_cookie( + refresh_token_name, + response.data[refresh_token_name], + max_age=max_age, + secure=settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'], + httponly=settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'], + samesite=settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE'] + ) + # Remove from body + del response.data[refresh_token_name] + del response.data["lifetime"] + + return super().finalize_response(request, response, *args, **kwargs) + +class LogoutView(APIView): + + renderer_classes = [JSONRenderer] + permission_classes = [] + + @swagger_auto_schema( + responses={'200': 'OK', '400': 'Bad Request'}, + operation_id='LogoutUser', + operation_description='Logout user and clean cookies' + ) + def post(self, request, *args, **kwargs): + + response = Response() + response.set_cookie(settings.AUTH_REFRESH_TOKEN_NAME, None, max_age=1, httponly=True) + response.set_cookie("sessionid", None, max_age=1, httponly=True) # logout from django admin! + return response + +class RequestUserDetailView(APIView): + + renderer_classes = [JSONRenderer] + permission_classes = [] + + @swagger_auto_schema( + responses={'200': 'OK', '400': 'Bad Request'}, + operation_id='UserDetail', + operation_description='Get details for request user' + ) + def get(self, request, *args, **kwargs): + + if not request.user.is_authenticated: + return Response(status=status.HTTP_400_BAD_REQUEST) + + user = request.user + data = UserDetailSerializer(user).data + return Response(status=status.HTTP_200_OK, data=data) + + @swagger_auto_schema( + responses={'200': 'OK', '400': 'Bad Request'}, + operation_id='UserDetailUpdate', + operation_description='Update details for request user', + request_body=UserDetailUpdateSerializer + ) + def put(self, request, *args, **kwargs): + + if not request.user.is_authenticated: + return Response(status=status.HTTP_400_BAD_REQUEST) + + user = request.user + data = self.request.data + + user.first_name = data.get('first_name', user.first_name) + user.last_name = data.get('last_name', user.last_name) + user.language_preference = data.get('language_preference', user.first_name) + user.save() + + user_data = UserDetailSerializer(user).data + return Response(status=status.HTTP_200_OK, data=user_data) + +class DeleteUserView(generics.DestroyAPIView): + permission_classes = [UserPermission] + queryset = User.objects.all() + + +class ChangePasswordView(generics.UpdateAPIView): + queryset = User.objects.all() + serializer_class = PasswordSerializer + + def update(self, request, *args, **kwargs): + user = self.request.user + serializer = self.get_serializer(data=request.data) + + if serializer.is_valid(): + + if not user.check_password(serializer.data.get("old_password")): + return Response({"old_password": ["Wrong password."]}, status=status.HTTP_400_BAD_REQUEST) + + user.set_password(serializer.data.get("new_password")) + user.save() + + response = {'message': 'Password updated successfully'} + return Response(response, status=status.HTTP_200_OK) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class PasswordResetView(generics.GenericAPIView): + """ + Use this endpoint to send email to user with password reset key. + """ + + serializer_class = PasswordResetSerializer + authentication_classes = [] + permission_classes = [] + + def post(self, request, *args, **kwargs): + serializer = PasswordResetSerializer(data=request.data) + + if serializer.is_valid(): + try: + user = User.objects.get(email=serializer.data.get('email')) + except User.DoesNotExist: + return Response(status=status.HTTP_400_BAD_REQUEST) + + # Build the password reset link + current_site = get_current_site(self.request) + domain = current_site.domain + uid = uuid.uuid4() + token = default_token_generator.make_token(user) + reset_link = "https://{domain}/password-reset/{uid}/{token}/".format(domain=domain, uid=uid, token=token) + + # create password reset obj + PasswordResetRequest.objects.create(user=user, uid=uid, confirmed=False) + + # send e-mail + subject = "Password reset" + message = ( + "You're receiving this email because you requested a password reset for your account.\n\n" + "Please visit this url to set new password:\n{reset_link}".format(reset_link=reset_link) + ) + from_email = settings.DEFAULT_FROM_EMAIL + + try: + send_mail(subject, message, from_email, [user.email]) + except BadHeaderError: + return Response({"error": "Invalid header found."}, status=status.HTTP_400_BAD_REQUEST) + + response = {"message": "E-mail successfully sent."} + return Response(response, status=status.HTTP_200_OK) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class PasswordResetConfirmView(generics.GenericAPIView): + """ + Use this endpoint to finish reset password process. + """ + + permission_classes = [] + authentication_classes = [] + + def get_serializer_class(self): + return PasswordResetConfirmSerializer + + def post(self, request, **kwargs): + try: + uid = uuid.UUID(self.kwargs['uid']) + obj = PasswordResetRequest.objects.get(uid=uid) + user = obj.user + except (ValueError, PasswordResetRequest.DoesNotExist): + return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': 'UID is not valid'}) + + serializer = self.get_serializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + # check if token is valid + token_valid = default_token_generator.check_token(obj.user, serializer.data["token"]) + + if not token_valid: + return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': 'Token not valid'}) + + # set new password + user.set_password(serializer.data["new_password"]) + user.save() + + # update object in db + obj.confirmed = True + obj.confirmed_on = timezone.now() + obj.save() + + return Response(status=status.HTTP_200_OK) \ No newline at end of file