From 25ed1cc86d354f1e48a16ffc64d51af23c18bf25 Mon Sep 17 00:00:00 2001 From: OBohutskyi Date: Wed, 1 Jan 2020 14:01:10 +0200 Subject: [PATCH 01/14] Init endpoints register and login --- README.md | 31 +++++++++++ .../books/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 155 bytes .../books/__pycache__/settings.cpython-37.pyc | Bin 0 -> 2207 bytes books/books/__pycache__/urls.cpython-37.pyc | Bin 0 -> 1001 bytes books/books/__pycache__/wsgi.cpython-37.pyc | Bin 0 -> 554 bytes books/books/settings.py | 2 +- books/books/urls.py | 2 +- books/{polls => books_management}/__init__.py | 0 .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 166 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 135 bytes .../__pycache__/admin.cpython-38.pyc | Bin 0 -> 176 bytes .../__pycache__/apps.cpython-38.pyc | Bin 0 -> 376 bytes .../__pycache__/models.cpython-38.pyc | Bin 0 -> 172 bytes .../__pycache__/urls.cpython-37.pyc | Bin 0 -> 350 bytes .../__pycache__/urls.cpython-38.pyc | Bin 0 -> 320 bytes .../__pycache__/views.cpython-37.pyc | Bin 0 -> 1576 bytes .../__pycache__/views.cpython-38.pyc | Bin 0 -> 1558 bytes books/{polls => books_management}/admin.py | 6 +-- books/books_management/apps.py | 5 ++ .../migrations/0001_initial.py | 20 +++++++ .../migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-38.pyc | Bin 0 -> 584 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 146 bytes books/books_management/models.py | 8 +++ books/{polls => books_management}/tests.py | 6 +-- books/books_management/urls.py | 8 +++ books/books_management/views.py | 49 ++++++++++++++++++ .../polls/__pycache__/__init__.cpython-38.pyc | Bin 123 -> 0 bytes books/polls/__pycache__/urls.cpython-38.pyc | Bin 254 -> 0 bytes books/polls/__pycache__/views.cpython-38.pyc | Bin 322 -> 0 bytes books/polls/apps.py | 5 -- books/polls/models.py | 3 -- books/polls/urls.py | 7 --- books/polls/views.py | 6 --- requirements.txt | 1 + 35 files changed, 130 insertions(+), 29 deletions(-) create mode 100644 README.md create mode 100644 books/books/__pycache__/__init__.cpython-37.pyc create mode 100644 books/books/__pycache__/settings.cpython-37.pyc create mode 100644 books/books/__pycache__/urls.cpython-37.pyc create mode 100644 books/books/__pycache__/wsgi.cpython-37.pyc rename books/{polls => books_management}/__init__.py (100%) create mode 100644 books/books_management/__pycache__/__init__.cpython-37.pyc create mode 100644 books/books_management/__pycache__/__init__.cpython-38.pyc create mode 100644 books/books_management/__pycache__/admin.cpython-38.pyc create mode 100644 books/books_management/__pycache__/apps.cpython-38.pyc create mode 100644 books/books_management/__pycache__/models.cpython-38.pyc create mode 100644 books/books_management/__pycache__/urls.cpython-37.pyc create mode 100644 books/books_management/__pycache__/urls.cpython-38.pyc create mode 100644 books/books_management/__pycache__/views.cpython-37.pyc create mode 100644 books/books_management/__pycache__/views.cpython-38.pyc rename books/{polls => books_management}/admin.py (95%) create mode 100644 books/books_management/apps.py create mode 100644 books/books_management/migrations/0001_initial.py rename books/{polls => books_management}/migrations/__init__.py (100%) create mode 100644 books/books_management/migrations/__pycache__/0001_initial.cpython-38.pyc create mode 100644 books/books_management/migrations/__pycache__/__init__.cpython-38.pyc create mode 100644 books/books_management/models.py rename books/{polls => books_management}/tests.py (95%) create mode 100644 books/books_management/urls.py create mode 100644 books/books_management/views.py delete mode 100644 books/polls/__pycache__/__init__.cpython-38.pyc delete mode 100644 books/polls/__pycache__/urls.cpython-38.pyc delete mode 100644 books/polls/__pycache__/views.cpython-38.pyc delete mode 100644 books/polls/apps.py delete mode 100644 books/polls/models.py delete mode 100644 books/polls/urls.py delete mode 100644 books/polls/views.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f91f95 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Books project + +One Paragraph of project description goes here - TODO + +## Getting Started + +### Prerequisites +Ensure that you have installed the 3rd python version. + +Ensure that you have installed `virtualenv`. If you haven't, run `pip install virtualenv`. + +Ensure that you have installed [Django](https://www.djangoproject.com/). Run `python3 -m django --version` to check it. Or run `pip install Django` to install. + +### Installing + +Create local virtual environment. TODO + +Run `env/Scripts activate` on Windows or `source env/bin/activate` on Mac OS. + +After it, run `pip install -r requirements.txt` to install all necessary tools. Enter your virtual environment. + + +## Running project + +Ensure that you are using your virtual environment. + +Run +``` +python3 manage.py runserver +``` +to start server work. \ No newline at end of file diff --git a/books/books/__pycache__/__init__.cpython-37.pyc b/books/books/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e7b1ef4587a97779a61d61625ff09d7c6c669c6 GIT binary patch literal 155 zcmZ?b<>g`kf}^cGaUl9Jh=2h`Aj1KOi&=m~3PUi1CZpd2 QKczG$)edC(XCP((06sY;5dZ)H literal 0 HcmV?d00001 diff --git a/books/books/__pycache__/settings.cpython-37.pyc b/books/books/__pycache__/settings.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8a7b819d301e06dffec373b0174bc333ddd0b1b GIT binary patch literal 2207 zcmb7F$#UC95aj}q6s^UJyu@~FINl;90wl{@r4mOF3Ck>VD*?0;9E<|dBWNTnG-hB$ z_<{U{5AiX7l5fB@r~E=rX@I1}qI^&|#9*eE*F8ONFkAEU)g1nw{XX}8aW0qpJDrSw z6&!5h!~Vh!az+mFP;m1`p5zY-e-tNrfy{i)r_Crq(I|spRGgeK3o}rHGE`s|s!qwM z!UdRvd02o&xQO!$aLJg1%T5jixB^RX6|TW`N1%DQ@kwy<#yoswEWk~;l|@=4CAj^$ z0AGJ9(7bUG?!aBNyaQs^-^==MXxLa*kaT@p{{`o$|aMUH=V{}s8=X}6^kYxyLCJkeZ zGq3|sCccb-G7kpSX7V^)c?i$Aw{_b)eAk2d4=7w&!A zd#=Yz!}UmZ;Pa67rL1=Gm1$|j_v2er!qkQMoF5Mei?2^98DR{oc~;?}!gA2#=ClXL z`rB#MS-d*Zc@)4UM^;EAM(il0{8-XPgVu>IemV^`3NB6fYeU(D*YepQxSd|$OSSAZ zVftb!fQ9v#1Xpm5rJ#03p$W(7%*LyqbZ!kRGU4_73z@mpK0L7OL!`0v%j?kcNN14p zl}-3nv*RH}q03@FSM6TZ^v^i*>{tDp`X8);HU{qzq)wid=p|Q0Sl5j(u<`CmV4Ug`@Fu zAt|Zt?M7QogtpRB!!n-6_{~(>8Dj<(V@E*_wbt)T6GI()``f zA_(JG=S6B!pL&tkB)-G$RlXYSimZbywL@lewKvM>l&r z&044ZI+@kgR=26>s+L^b)wUa{eX~(hQkEpbK|uXvwyx+(8eB_mC_Vj+*;O=cx3g0> z-zm)oDmzHA`KHp|?kU@95vV1+DR#u=?1Pu$4Ip_ zn?G~mU8GLd{acq)VlX*y$sxn5B9!ZajrH|8leMlU4==(Ec`LmThqF;XzJR2`aZ>rw z3m|gIs|yIsy4VSYO7V7CC<+CkRLGB8NytWpv@aEfiCHRHC<*2Cy;Lm~(^jpZ_pp`z E1I}9JMgRZ+ literal 0 HcmV?d00001 diff --git a/books/books/__pycache__/urls.cpython-37.pyc b/books/books/__pycache__/urls.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08d3a8872bbc03024f63d0e3ff6ada1f6dd0659a GIT binary patch literal 1001 zcma)5L2uJA6n4^dEm;S5IhV&Eh)UNWm#dZspRS!mW9$#ids{5KtV0wmx6v(3A_*zG9f7}_*_Wl zAYfHetSD5JjzehHQtL{M5h5a230Yyj5S0`wn(HJN3{j3wlE+Aks>l_x#mHQ*Q26>A zW(G@*!;6~dmfiMR2l^y|FA0H|!-7>p>M?|KDHvq88Lp}}xmW+Rt#^`*fIWrgs1dsy zM$xF%wG@nwAjgb8kL*5*!}Ag=^=M{mZg zhVeg)E_q(oq~Hr6u-31ST|2(R94{!NT%+|vm&w@Q zl*3vdH~{oMzSs1!%yd+irT1GAM!S~9Oq%Wn&EcLhX$A^NJ4H>P3Qe29>TM49df*3; zcK%%WJwaEBN`*v8ZxrT4qE{lXEiZ+pAEszKJ16MO&d~O+jTSelZkzro6QnNbiw6c& O^-T!8fp^>)cz*zn%{VOp literal 0 HcmV?d00001 diff --git a/books/books/__pycache__/wsgi.cpython-37.pyc b/books/books/__pycache__/wsgi.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f569be5261d74a4b08e639143c8379e712cf19f7 GIT binary patch literal 554 zcmYjO!A=`75cN8NL|Z^Tpq_n=)UJ`ZRaHe%Xq2i1qy<&Vp&PTFY#h9vmFAIio1IOZ3?+T%iNaixI?#{?@;t~9*Sum;W6!m3Tlh!P~5%|umcl?$)X z%w~b_W6&tk0#hc)m74-X6UdcgbEV-ma{%$e`^vrLTw?ALxfG_rCr>$+91K594)_9Z zJaP$@#@iB%=QplUJbnz#Ad^84!l>UaD!gm%d%h~%W8_Qe%abc^58ycNG%OAbq~y1Su}MVE=rh-mG^OU#kKjg>FH?-M*Y!s8~w(h@c8~(l@NL>S6 K+NV2okNyLuysdTs literal 0 HcmV?d00001 diff --git a/books/books/settings.py b/books/books/settings.py index 05ac9f6..4b307ec 100644 --- a/books/books/settings.py +++ b/books/books/settings.py @@ -43,7 +43,7 @@ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', + # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', diff --git a/books/books/urls.py b/books/books/urls.py index 9cfe693..9eee23c 100644 --- a/books/books/urls.py +++ b/books/books/urls.py @@ -17,6 +17,6 @@ from django.urls import include, path urlpatterns = [ - path('polls/', include('polls.urls')), + path('booksManagement/', include('books_management.urls')), path('admin/', admin.site.urls), ] diff --git a/books/polls/__init__.py b/books/books_management/__init__.py similarity index 100% rename from books/polls/__init__.py rename to books/books_management/__init__.py diff --git a/books/books_management/__pycache__/__init__.cpython-37.pyc b/books/books_management/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27c8715ee57ccfa5c3d86e305c7f5c9bcd6de87a GIT binary patch literal 166 zcmZ?b<>g`kf-Of_<3RLd5CH>>K!yVl7qb9~6oz01O-8?!3`HPe1o6vXKeRZts8~Ng zCpEh`F)yV^-z7h}G&eP`q*y;VAXq;sKR>%z!C2o3L_>HGGCnskFEKq8q*6aVJ~J<~ aBtBlRpz;=nO>TZlX-=vg$fD0c%m4s9VJg1> literal 0 HcmV?d00001 diff --git a/books/books_management/__pycache__/__init__.cpython-38.pyc b/books/books_management/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a5666d4e4f21f1147c7c6e2d2b43f52794b59e0 GIT binary patch literal 135 zcmWIL<>g`kf-Of_<3RLd5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HHvsH{!etvdw zOcIzZElw?pFG@|%EG{WZEXmBzi;0iV%*!l^kJl@xyv1RYo1apelWGStg`kf-Of_V-N=!FabFZKwK;UBvKes7;_kM8KW2(8B&;n88n$+0!0}# z8E>&BrsQVk`Drpm@ug%X=B4NBCFkdr6lEqAfecv5P{a(Rz{D?2XR8>e{QT_Vm?SV+ sTAW%GUzD1jSzJ<-Sdy8a7Xvm+ub}c4hfQvNN@-529V1ZpXCP((0ES^H^8f$< literal 0 HcmV?d00001 diff --git a/books/books_management/__pycache__/apps.cpython-38.pyc b/books/books_management/__pycache__/apps.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1baf61d014ba201fbf0fef42766ee19d6e0cb314 GIT binary patch literal 376 zcmZWly-ve05I)DLl$KV4g_*Hg`Tz)_LI<`Epe|N~Ww40?N$lFqzzi?L&Li*?UYU4> zPMjU2M&hLV?&rHddo!C&0Oj**biYOW9fF~x7%WlU8G-~!YG?=}1a3eklD>naio8*l z$n*~K(GONZPW<@Fn3Zmys`4>phC(n{qPk-Q1&m0*Zo;Tca02zG8>j58Dl50MVpr+5 zpTkZ%%yT;+)_LkP&Rfwa&i#b*M$4`a{*?1=C+a^Y4fX?Uh6^L?=d1JLTI&~AJVg9A zx)8>=g?Y6Ud%_>M1R*0aUgMQV@>#T{UW7UuxX2t<^xhZ=pI!8KU51y(IaB%zWMNpS literal 0 HcmV?d00001 diff --git a/books/books_management/__pycache__/models.cpython-38.pyc b/books/books_management/__pycache__/models.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86fa78d7b32a4d13c4b06be1ec8551e14d53503f GIT binary patch literal 172 zcmWIL<>g`kf)W=BK3Q6#Hp1-QrBiO3X{o*Gow%0%=~!P{abHz{D?2XR8>e{QT_Vm?Q)lpPQJM gn4X%OnpYA7F-EVT@)m~;kX@RSYR3rF{TYZE0JX6wQvd(} literal 0 HcmV?d00001 diff --git a/books/books_management/__pycache__/urls.cpython-37.pyc b/books/books_management/__pycache__/urls.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49c2e859c5a32d90f4a0b5b058c89b1507d58909 GIT binary patch literal 350 zcmXw!y-ve06ou_LPSPZO1SHlh^}vb{irA14QpA!0WUv*|#&%^V{B&jJL3j(E!z)w8 z8!&O*s8{lL&$W(^?RvS)5!CNVasLhdCk)3*L0rOeH9#PN2c&R|F%pp=L!?McFp^lr zlC0y82owcH4=M?JA{E&u=}4Q1MT4q*4>_p9KHl>2d5`CTzV!0d?G{#cy&I)vyJRq3 zubD@l_FBlda6)q>Z>rQ6V$F4DR+Al^jUd6&fEeJjV}z%N1Ydv)u7XhQzK|`0GvM!< z-8pF;GlSeXu7zb+=4nz=kB&9B4SO_Z<4(_59r`@Yd{A8TP6j6nPgrf=eRi&lm9RCCHiC$^| literal 0 HcmV?d00001 diff --git a/books/books_management/__pycache__/urls.cpython-38.pyc b/books/books_management/__pycache__/urls.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65bac0dad216d4f067b7150dee3aa2ae4f2cc020 GIT binary patch literal 320 zcmYk2&q~BF5XLi2({|ggZ{S&a*}aR1tamTsrIeM(gxa-9m!#{Ty?XXRd<&l=S5J#? zAP7!YL6Gm!vQKi1Xl{D=z+t}XOhzM2$iAj9+i|}Gk@C9|zRRlHemDw_M2KhWX uKW-MoaOawvKfTt{$X01(#wOrr>nER`=m9sqIt`C?;k!yGc@8-oe0>AH6H>ka literal 0 HcmV?d00001 diff --git a/books/books_management/__pycache__/views.cpython-37.pyc b/books/books_management/__pycache__/views.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4cb2c770e8670e84f31b07a7fb390bcc7a72094 GIT binary patch literal 1576 zcma)6OOG2x5bnp!c>IFRYl8uaIYsgzF$xdm5Jf0(h!9#uYjGJNRufmpJN7(GcN=F{ z-dw_U4;(=pyhr{5|A4>HS5EsEI8oKJc2E$48Fh76S3j!0ueu)h`yGZ~_v7xjVT-Xp zNZEb@2cKax-=F{nykI3yc_~uCNf(7IWh%=c4ZJRkFb#2D7Ev1IENx+nA%O4)mbM{+ z7WN&8p^bePI?%+(t`004`tgh!K`DpZ*`bKNyQJGcQRME=O_qke`dy5knZ?7YK z4f(}{rt!{SiAspg`+JB3%Qk$?W@00OU*Lbn*Br!vor$%u!93FUZ3q&AoI?oFxfnCF zw+74xYw?hITZ}e@nry%@^3K6&xU|pjK5#KbU1f+}t5n9Izo&O7-ei^A0rE^Oz^Ph%c3|*Cb-Ee@W)*Kj`0j5 z7tD}P7Z!B}#s$x6IB^lENe#;FZ2?l?88Mnb8cL06$n&Gb+( zex1ko?};AwmL0kQ8h7aahRT1S+xjQEgzMl6WVBQfsXy)^*g2MXr@O$}9agt{ZkVp{%cwAU5<35;sW@QLanm9NBuV zDx(Q)C#!npt~F`kDQ&F7;5DWC*6Xc!Ehf<$^ir-&^Xgq(X5L4!Bbe{u@2TcJB-<1I z9@=+=lT$mwKSaf{89t{q)~x~7Dv;-LBQBVgn*e1`uULAm&0S;MkWkEnneqWTJI5%569QQV1d`~{pQXbb=V literal 0 HcmV?d00001 diff --git a/books/books_management/__pycache__/views.cpython-38.pyc b/books/books_management/__pycache__/views.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c56488e0e37c3c810d9d785e97e5c0836ffd0364 GIT binary patch literal 1558 zcma)6OOG2x5bnpk{6bDb63jy~mk1vcp(r4PB812#;$Wd&tS`Sf%%uMrky}T0LJXV_z{%vtNJm=*zut z>UwVWo?goRGOMzwDph6oe#q6z+*=&FWV;n%Xy{BPG>xysBhe6I^YQH9z_JZrvzgdP z;HUT>^EC%CV8>!DY%q`XeH(&=Amc`r`q!!CE|I-j|>c;Uph0%&a|r9WL#& zyI;73vZ^w~CoYvS81vDcCukb3a(gf|%gMoTJ7bs?T4iuJR4)(^lmCLqz2Yu1HnU6P zl0|0Bs@8Bc*j-L0xY@I1Q5+5@xX3E-+noJ@`HUhL%#bq|7Ig;31y5@@bP=dY4a)Vm zASv*O8%;osq{i#U^t&irKoNQH8_^<9grbklbWt#VohSJBMVEWe6@&wvaDnjesQd@Q z@jnqJYzOB!&!yhEKzYl6un{uztBq*hv0O`NAtUi7Ic4)e|7b(Bv>QfjYt3JBnn`Rm zEB=qqGuavI4rZV!Yu!Wf@AN^f%&@9$!(aY0(x=~{Wdz&N&F{|X8|G%1SCd+6HL}&sAkKA?;*UuiUlf8hA<@?lAb10$T&i$GfKZ{Rh(mjUUaW|flbKqZ`X}r3S;$5e zN{aUa6zFRK8s$n~=|Y3~D5j%d+$*Mv^2SxZ>|NIeN}3L^4$DYAjEsvT&hVT8MIk{H z3S^-xQ5YPpk~jj#s}v+Z21bzFmYoFh5c|M!Xp4mGEhwH76zc1ko2Mnn) y$`~V+dUUJ)n%jXrg`kf-Of_<3RLd5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HyvsH{!etvdw zOcIzZElw?pFG@|%EG{WZEXmBzi^uc;^Q;(GE3s)^$IF)aoFVMr datetime.now(): + return JsonResponse(user_record, status=201) + else: + user_tokens[user] = create_user_token(body) + return JsonResponse(user_tokens[user], status=201) + else: + user_tokens[user] = create_user_token(body) + return JsonResponse(user_tokens[user], status=201) + + +def create_user_token(body): + user_record = {'access_token': jwt.encode(body, body['password'], algorithm='HS256').decode(), + # 'expires_in': datetime.now() + timedelta(days=1)} + 'expires_in': datetime.now() + timedelta(seconds=5)} + return user_record diff --git a/books/polls/__pycache__/__init__.cpython-38.pyc b/books/polls/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 939731c8412161c0d912c2a576decb6c74484f78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmWIL<>g`kf<>B4aUl9Jh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2D3*($~%T oCJ9Uyg`kf<0PHaiu`|F^GcU1=BD~-vfbiN$x6&i&(|w0$|){l0xB;8Szp8gBG`b$EpDI;(5#ZwqP*gj z3`HQPgNa`<&Q>u_`T5z!F-c&uAU`LkI0j^nUP0w84x8Nkl+v73JCGBKK_>Aq@i6oK F2LK3SJCpzb diff --git a/books/polls/__pycache__/views.cpython-38.pyc b/books/polls/__pycache__/views.cpython-38.pyc deleted file mode 100644 index 50ee168f62049d86434ac91603b116e857e6cfbd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 322 zcmYjMF-`+95M29mB!Z-;LGo4T0Nn!+LWne}QV^v`bc&SV6$v=!!}bxNg@Ui7#`rLm+&g^Vt{mpD^r$({Mv&kBKBmreMvcjDh4b`GhH#tr6tu zFR8F)zB_QPzC&B<%Hll>+kb>}2 Date: Thu, 9 Jan 2020 16:02:50 +0200 Subject: [PATCH 02/14] Init updates --- .gitignore | 5 +- README.md | 18 +++- {books/books => app}/__init__.py | 0 app/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 120 bytes .../books_management => app/app}/__init__.py | 0 .../app}/__pycache__/__init__.cpython-37.pyc | Bin app/app/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 128 bytes .../app}/__pycache__/settings.cpython-37.pyc | Bin .../app}/__pycache__/settings.cpython-38.pyc | Bin 2256 -> 2204 bytes .../app}/__pycache__/urls.cpython-37.pyc | Bin .../app}/__pycache__/urls.cpython-38.pyc | Bin 950 -> 949 bytes .../app}/__pycache__/wsgi.cpython-37.pyc | Bin app/app/__pycache__/wsgi.cpython-38.pyc | Bin 0 -> 519 bytes {books/books => app/app}/asgi.py | 4 +- {books/books => app/app}/settings.py | 8 +- {books/books => app/app}/urls.py | 4 +- {books/books => app/app}/wsgi.py | 4 +- {books => app}/db.sqlite3 | 0 {books => app}/manage.py | 2 +- .../migrations => app/users}/__init__.py | 0 app/users/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 128 bytes app/users/__pycache__/admin.cpython-38.pyc | Bin 0 -> 169 bytes app/users/__pycache__/data.cpython-38.pyc | Bin 0 -> 1466 bytes app/users/__pycache__/models.cpython-38.pyc | Bin 0 -> 1014 bytes app/users/__pycache__/tests.cpython-38.pyc | Bin 0 -> 167 bytes app/users/__pycache__/urls.cpython-38.pyc | Bin 0 -> 514 bytes app/users/__pycache__/views.cpython-38.pyc | Bin 0 -> 4238 bytes .../books_management => app/users}/admin.py | 6 +- app/users/apps.py | 5 + app/users/data.py | 32 ++++++ app/users/migrations/__init__.py | 0 .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 139 bytes app/users/models.py | 21 ++++ .../books_management => app/users}/tests.py | 7 +- app/users/urls.py | 21 ++++ app/users/views.py | 84 +++++++++++++++ .../books/__pycache__/__init__.cpython-38.pyc | Bin 123 -> 0 bytes books/books/__pycache__/wsgi.cpython-38.pyc | Bin 522 -> 0 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 166 -> 0 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 135 -> 0 bytes .../__pycache__/admin.cpython-38.pyc | Bin 176 -> 0 bytes .../__pycache__/apps.cpython-38.pyc | Bin 376 -> 0 bytes .../__pycache__/models.cpython-38.pyc | Bin 172 -> 0 bytes .../__pycache__/urls.cpython-37.pyc | Bin 350 -> 0 bytes .../__pycache__/urls.cpython-38.pyc | Bin 320 -> 0 bytes .../__pycache__/views.cpython-37.pyc | Bin 1576 -> 0 bytes .../__pycache__/views.cpython-38.pyc | Bin 1558 -> 0 bytes books/books_management/apps.py | 5 - .../migrations/0001_initial.py | 20 ---- .../__pycache__/0001_initial.cpython-38.pyc | Bin 584 -> 0 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 146 -> 0 bytes books/books_management/models.py | 8 -- books/books_management/urls.py | 8 -- books/books_management/views.py | 49 --------- requirements.txt | 17 ++- setup.cfg | 4 + tests/__init__.py | 0 tests/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 122 bytes tests/users/__init__.py | 0 .../users/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 128 bytes .../test_data.cpython-38-pytest-5.3.2.pyc | Bin 0 -> 6513 bytes .../__pycache__/test_data.cpython-38.pyc | Bin 0 -> 1965 bytes tests/users/test_data.py | 100 ++++++++++++++++++ tests/users/test_views.py | 0 64 files changed, 317 insertions(+), 115 deletions(-) rename {books/books => app}/__init__.py (100%) create mode 100644 app/__pycache__/__init__.cpython-38.pyc rename {books/books_management => app/app}/__init__.py (100%) rename {books/books => app/app}/__pycache__/__init__.cpython-37.pyc (100%) create mode 100644 app/app/__pycache__/__init__.cpython-38.pyc rename {books/books => app/app}/__pycache__/settings.cpython-37.pyc (100%) rename {books/books => app/app}/__pycache__/settings.cpython-38.pyc (57%) rename {books/books => app/app}/__pycache__/urls.cpython-37.pyc (100%) rename {books/books => app/app}/__pycache__/urls.cpython-38.pyc (69%) rename {books/books => app/app}/__pycache__/wsgi.cpython-37.pyc (100%) create mode 100644 app/app/__pycache__/wsgi.cpython-38.pyc rename {books/books => app/app}/asgi.py (74%) rename {books/books => app/app}/settings.py (95%) rename {books/books => app/app}/urls.py (88%) rename {books/books => app/app}/wsgi.py (74%) rename {books => app}/db.sqlite3 (100%) rename {books => app}/manage.py (88%) rename {books/books_management/migrations => app/users}/__init__.py (100%) create mode 100644 app/users/__pycache__/__init__.cpython-38.pyc create mode 100644 app/users/__pycache__/admin.cpython-38.pyc create mode 100644 app/users/__pycache__/data.cpython-38.pyc create mode 100644 app/users/__pycache__/models.cpython-38.pyc create mode 100644 app/users/__pycache__/tests.cpython-38.pyc create mode 100644 app/users/__pycache__/urls.cpython-38.pyc create mode 100644 app/users/__pycache__/views.cpython-38.pyc rename {books/books_management => app/users}/admin.py (95%) create mode 100644 app/users/apps.py create mode 100644 app/users/data.py create mode 100644 app/users/migrations/__init__.py create mode 100644 app/users/migrations/__pycache__/__init__.cpython-38.pyc create mode 100644 app/users/models.py rename {books/books_management => app/users}/tests.py (95%) create mode 100644 app/users/urls.py create mode 100644 app/users/views.py delete mode 100644 books/books/__pycache__/__init__.cpython-38.pyc delete mode 100644 books/books/__pycache__/wsgi.cpython-38.pyc delete mode 100644 books/books_management/__pycache__/__init__.cpython-37.pyc delete mode 100644 books/books_management/__pycache__/__init__.cpython-38.pyc delete mode 100644 books/books_management/__pycache__/admin.cpython-38.pyc delete mode 100644 books/books_management/__pycache__/apps.cpython-38.pyc delete mode 100644 books/books_management/__pycache__/models.cpython-38.pyc delete mode 100644 books/books_management/__pycache__/urls.cpython-37.pyc delete mode 100644 books/books_management/__pycache__/urls.cpython-38.pyc delete mode 100644 books/books_management/__pycache__/views.cpython-37.pyc delete mode 100644 books/books_management/__pycache__/views.cpython-38.pyc delete mode 100644 books/books_management/apps.py delete mode 100644 books/books_management/migrations/0001_initial.py delete mode 100644 books/books_management/migrations/__pycache__/0001_initial.cpython-38.pyc delete mode 100644 books/books_management/migrations/__pycache__/__init__.cpython-38.pyc delete mode 100644 books/books_management/models.py delete mode 100644 books/books_management/urls.py delete mode 100644 books/books_management/views.py create mode 100644 setup.cfg create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-38.pyc create mode 100644 tests/users/__init__.py create mode 100644 tests/users/__pycache__/__init__.cpython-38.pyc create mode 100644 tests/users/__pycache__/test_data.cpython-38-pytest-5.3.2.pyc create mode 100644 tests/users/__pycache__/test_data.cpython-38.pyc create mode 100644 tests/users/test_data.py create mode 100644 tests/users/test_views.py diff --git a/.gitignore b/.gitignore index 77aa56e..a66ca99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -/env -/.idea +/env* +/.* +/coverage diff --git a/README.md b/README.md index 4f91f95..49fc500 100644 --- a/README.md +++ b/README.md @@ -13,19 +13,27 @@ Ensure that you have installed [Django](https://www.djangoproject.com/). Run `py ### Installing -Create local virtual environment. TODO - -Run `env/Scripts activate` on Windows or `source env/bin/activate` on Mac OS. +Create local virtual environment. Run `virtualenv env`. Enter created environment with running `env\Scripts\activate` on Windows or `source env/bin/activate` on Mac OS. After it, run `pip install -r requirements.txt` to install all necessary tools. Enter your virtual environment. - ## Running project Ensure that you are using your virtual environment. +Move to books folder. + Run ``` python3 manage.py runserver ``` -to start server work. \ No newline at end of file +on MAC OS/Linux or +``` +python manage.py runserver +``` +on Windows +to start server work. + +## Tests + +TODO \ No newline at end of file diff --git a/books/books/__init__.py b/app/__init__.py similarity index 100% rename from books/books/__init__.py rename to app/__init__.py diff --git a/app/__pycache__/__init__.cpython-38.pyc b/app/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7cb012bb006c4d1c913dbdc663ef2e19d79a862f GIT binary patch literal 120 zcmWIL<>g`kf@#*maUl9Jh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2BY*(#PO2S9+h-tV001C-7svnr literal 0 HcmV?d00001 diff --git a/books/books_management/__init__.py b/app/app/__init__.py similarity index 100% rename from books/books_management/__init__.py rename to app/app/__init__.py diff --git a/books/books/__pycache__/__init__.cpython-37.pyc b/app/app/__pycache__/__init__.cpython-37.pyc similarity index 100% rename from books/books/__pycache__/__init__.cpython-37.pyc rename to app/app/__pycache__/__init__.cpython-37.pyc diff --git a/app/app/__pycache__/__init__.cpython-38.pyc b/app/app/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..80d4d3f2098d9246a664b7db77dc8e7dd289de44 GIT binary patch literal 128 zcmWIL<>g`kf|oOR<3RLd5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HjvsFw-7 oQ+|GSaZC~t86TgSmst`YuUAlci^C>2KczG$)efZnGY~TX08@+`u>b%7 literal 0 HcmV?d00001 diff --git a/books/books/__pycache__/settings.cpython-37.pyc b/app/app/__pycache__/settings.cpython-37.pyc similarity index 100% rename from books/books/__pycache__/settings.cpython-37.pyc rename to app/app/__pycache__/settings.cpython-37.pyc diff --git a/books/books/__pycache__/settings.cpython-38.pyc b/app/app/__pycache__/settings.cpython-38.pyc similarity index 57% rename from books/books/__pycache__/settings.cpython-38.pyc rename to app/app/__pycache__/settings.cpython-38.pyc index 57070c2a61e10d821aa83d092f6deef0546033b7..5975843ed0083f9f1bee81757cbc42984cbe0d31 100644 GIT binary patch delta 229 zcmca0I7g5-l$V!_0SIn62*)){E!5=v2t5>8=>5&>dSAQnqejS^2`3TDt$OVnUwV3_z{lsU1WV6!A+ z5!2*t%*$CgK)hrYbw(Bu5NiVK3??zfWUH8x)Z&sDr~Lfv;uxSf2v4@H#VTlp}Vo@L#OHqpwPhkpX&{R*oSIfhw;r?@Ch&lyOBWu}%-u3_H9#syJe#G=l~ rCI(^0vd&-$j3txRIh2?gnHVQ~aRdPX;6X^_ diff --git a/books/books/__pycache__/urls.cpython-37.pyc b/app/app/__pycache__/urls.cpython-37.pyc similarity index 100% rename from books/books/__pycache__/urls.cpython-37.pyc rename to app/app/__pycache__/urls.cpython-37.pyc diff --git a/books/books/__pycache__/urls.cpython-38.pyc b/app/app/__pycache__/urls.cpython-38.pyc similarity index 69% rename from books/books/__pycache__/urls.cpython-38.pyc rename to app/app/__pycache__/urls.cpython-38.pyc index 77d6230ed324f9fc2f6b0fab5c3f96d47108cc81..5e87df873040dd631a62f306aae4c6b76fd7a2da 100644 GIT binary patch delta 77 zcmdnSzLlLfl$V!_0SF`SE$4Elw>e*016M(US$3bHrqm Ztzt@2i%Vjh^7FHcV}R;Fc=AMMRR9U<7Wn`G delta 78 zcmdnWzKxwXl$V!_0SH*Mnc_?*@}_X+F)=VC<>zM?Z>;EI;wi|_$tl*a;sVi=`IvJ= UWt^>IoIuhsU_}se@&sm8027B72><{9 diff --git a/books/books/__pycache__/wsgi.cpython-37.pyc b/app/app/__pycache__/wsgi.cpython-37.pyc similarity index 100% rename from books/books/__pycache__/wsgi.cpython-37.pyc rename to app/app/__pycache__/wsgi.cpython-37.pyc diff --git a/app/app/__pycache__/wsgi.cpython-38.pyc b/app/app/__pycache__/wsgi.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1660c71d231d5c73e198ac485d2f6d3b312b7bb6 GIT binary patch literal 519 zcmYjO!D<^Z5Y@^~>~51%Xeqe_bTzcQf!;zXX-R4v66fHAf-zmItMOW{R1L~qORvNdi+45CH*eoM90)&r-k^@EMK6(6zQ_TBIJ^Qb=%Iog>n#0+a8t4dTg zE+F?w91pyJ?-e?5!p}jho24=Gs{tXMkfKC&ZQzZ8B^a@kwO*C1EJ2Csl+cXMrPpYu zQ^Na13?R>g?oh zhnf-x-fLU9UMF<2jOep0aCG7MT11kQ_N0NVIgx}8*rl$~hL{Ew%;dH4;pqn3FIG1P sT!p{;`QgNa^OKK=-`s@SR^GrR)otj$TkrBc9h$i=h4zy!-{H^rKd`l)qW}N^ literal 0 HcmV?d00001 diff --git a/books/books/asgi.py b/app/app/asgi.py similarity index 74% rename from books/books/asgi.py rename to app/app/asgi.py index 4f094bb..0c71a7f 100644 --- a/books/books/asgi.py +++ b/app/app/asgi.py @@ -1,5 +1,5 @@ """ -ASGI config for books project. +ASGI config for app project. It exposes the ASGI callable as a module-level variable named ``application``. @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'books.settings') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') application = get_asgi_application() diff --git a/books/books/settings.py b/app/app/settings.py similarity index 95% rename from books/books/settings.py rename to app/app/settings.py index 4b307ec..717be33 100644 --- a/books/books/settings.py +++ b/app/app/settings.py @@ -1,5 +1,5 @@ """ -Django settings for books project. +Django settings for app project. Generated by 'django-admin startproject' using Django 3.0.1. @@ -36,7 +36,7 @@ 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', - 'django.contrib.staticfiles', + 'django.contrib.staticfiles' ] MIDDLEWARE = [ @@ -49,7 +49,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -ROOT_URLCONF = 'books.urls' +ROOT_URLCONF = 'app.urls' TEMPLATES = [ { @@ -67,7 +67,7 @@ }, ] -WSGI_APPLICATION = 'books.wsgi.application' +WSGI_APPLICATION = 'app.wsgi.application' # Database diff --git a/books/books/urls.py b/app/app/urls.py similarity index 88% rename from books/books/urls.py rename to app/app/urls.py index 9eee23c..2229166 100644 --- a/books/books/urls.py +++ b/app/app/urls.py @@ -1,4 +1,4 @@ -"""books URL Configuration +"""app URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.0/topics/http/urls/ @@ -17,6 +17,6 @@ from django.urls import include, path urlpatterns = [ - path('booksManagement/', include('books_management.urls')), + path('users/', include('users.urls')), path('admin/', admin.site.urls), ] diff --git a/books/books/wsgi.py b/app/app/wsgi.py similarity index 74% rename from books/books/wsgi.py rename to app/app/wsgi.py index 60b2a6a..863ddf4 100644 --- a/books/books/wsgi.py +++ b/app/app/wsgi.py @@ -1,5 +1,5 @@ """ -WSGI config for books project. +WSGI config for app project. It exposes the WSGI callable as a module-level variable named ``application``. @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'books.settings') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') application = get_wsgi_application() diff --git a/books/db.sqlite3 b/app/db.sqlite3 similarity index 100% rename from books/db.sqlite3 rename to app/db.sqlite3 diff --git a/books/manage.py b/app/manage.py similarity index 88% rename from books/manage.py rename to app/manage.py index e357ea9..a3f0719 100644 --- a/books/manage.py +++ b/app/manage.py @@ -5,7 +5,7 @@ def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'books.settings') + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/books/books_management/migrations/__init__.py b/app/users/__init__.py similarity index 100% rename from books/books_management/migrations/__init__.py rename to app/users/__init__.py diff --git a/app/users/__pycache__/__init__.cpython-38.pyc b/app/users/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f63ebd380697a8fe59c52d1d19f08b62ccaf64bd GIT binary patch literal 128 zcmWIL<>g`kg6tE#aUl9Jh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2DN*(#g`kg6tE#aZW(`F^Gc(44TX@fuanW zjJH@5Q*tx&{4|-O_)@YG^V0M6lJoOQiZYXmKnAR2C}IXuVB(jOvsFw-7Q+|GS laZD1JEG2KczG$)s7LU?K2QF001kZCd2>$ literal 0 HcmV?d00001 diff --git a/app/users/__pycache__/data.cpython-38.pyc b/app/users/__pycache__/data.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc96403fc3df85297f8f01f18b2351cb82e4b254 GIT binary patch literal 1466 zcmZ`(&u<$=6n^u=>$Q^@M+8f$1O#p?RaV6TArwUwv>+tpODLr)!)oQ3q}!}_w==tK zu-prd1Q!n6dw^s9rMYs-g+G83@6E=IA!4k3n%RBxzW2TNJwMvn=>x9O)#!)6B*33^ z*j)Gy?&D+0C{QeiIg1&CQ;7MOptuqjpu~#DLQ5sjxH;5f1#w3Qy7R27Ix6^;t$wyY3867s4z{7ofJeY=Wr0QHq1Q9w=!E3I%D#UYF z^;G`?;!tfN*tY8tihYpQDM2;_$v5z5PmrGzN7>jB5CuE!iKGH9JO*$8aeo z-+U2qH#GV*v$Zx_B^1xPAgwB0s7vmo)%l5`f?a~4s{Z}{?9*wjZ9V<0EYIu|J*WQC zRAEZv>PIJ1rFw)Oq5&4NK9g**^Cs;ve!rD6`TCSU!p&lMlkAc`hGURW^RvFW%Yc40 zR&T|AVo#vp-(dr>E_|{W5W+!G)(0nLQ>aLq0Asnp=y~}vZ=6RdW2u?HX86GZ_{kuW{g~}7io;hP_gV0nqR<#HMU`~_cNR@ zg~GnT^8;9|o5zsOC)drx>H1IedH|raV-LHxlX`ZPA9~b3eBGD>r>ua2Zrt;D zCC&zCdnB!m2|=Gp)N^5y6zNaE9xcR9Qr@M}T$hY<} zIHX73htpm;^%XcVW7iM?W9{tBtas*{Z}zCy+Xa&Ehqpi8mjFN5*_MEu*Rtix9NCZM6QSf~X zE+A2;Fr+=i2s^Y#sBwq(4(?L^J+F>jX04#hBL>(C+q~Y=at}$BpkO9YJOc?vlw?<6 zWW_qDV52H|$##f9Y31OGGscnS9A3UZ^m2+`uc+1{oo8N;v(ijbH}Sn?>fEQoV(rF( zS1!v>Y=`gZ_Us$jE(Lzx8@(8onJb5HisIA_)8%r=Dc!JPr@#E-wK3Dhv@}N31;+}+ zzR)5P>mIk-Z_h`Juobp{w|k465-Mu(oLZU@n_qbrau7=!(OC_SkOr=_TLEEt{x#4> z^~m)>aXi~6!||X%taS~7f}=XXK@zq@S~cBw$S7eWDzTy3m-KCd%ZAo| zFnjs**hg-XK6!fJqiivvH{y3D*=L-Ni6w%|h`hqIO#i}VnVFlkT3ih|?O6T+#{g?^ zJO?5LDlvmorbP^5(J*pdE-cCkJt_Wii-*XV?93SMtu48CJr9l#V$GxD$6<`$HHId- z%GvH3bGAzJ=EO2dmie?dSaBEjE~`J~YKytAA0JPQDVsN=Cpc5s$1#zh~G aEw^Wxj{59>$-I8qKzhY*#VJxg`kf_!V?I7cA;7{oyaOhAqU5Elyoi4=wu#vF!R#wbQch7_h?22JLdKv4!w z##G^skK(Qi_0V^4bn1K|S_@&@%6$6rqamvrnE{;ho iD2OR7PAw`13l{4YRNmsS$<0qG%}KRm1Zw&W#0&s}6eVo{ literal 0 HcmV?d00001 diff --git a/app/users/__pycache__/urls.cpython-38.pyc b/app/users/__pycache__/urls.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5437b8038ac2dc52204193f57d7efc0a657c4e8 GIT binary patch literal 514 zcmYk2O)msN5Qh6}W_HIeivw4QgNazKL=dhb65%k`ZiZ@;VZJimL+pR>4;(o9OMP{6 zb`xLK!$EiYO;2yQ5H%EymGLta;s&UdHPV$4G9roZV z%HyJcQRJ6>G^Z-u9WkU5c4qt7&OAY%#*5U*8V)Cqk~~hOB^LbF?Av~30vnW(*0Rdg zGpTykyOsBay^VpEN)L96;#v*P8?|=Sz|QQKcc!(O6;P*g%Wg-lp>$}6`;1Y%pW>Uj X>|J&cq literal 0 HcmV?d00001 diff --git a/app/users/__pycache__/views.cpython-38.pyc b/app/users/__pycache__/views.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d4601025a87e2c1fe949db26ba92b0afbb34596 GIT binary patch literal 4238 zcma)9OLH5?5uVvyEEXV0fwU!=c{s3QOF=4T9LIJXhfx?wso1Url?qEaY{@~Em?a2s z@zBg5q)^Z$bd}_T4n8KO19{|6$zPbOPdw+QOY(IuK>{Qz$)ct)Jv%c!-ShQhA1*GI z1w9|U_UCUFg!qP;84{qsfoA>zqJ)x}=*d|2yx5b(dzs(!W4{-~0rP%V=!J2}{U9sG zMa&7ZD318Q6qni}F8@}jf(pMBDm?Jw1#m?bfs4}cz>h27OR5aM%={ww1yuoGVSY(1 zs--VQd`(?bRs2^~uqo=xCpe#|d#>`i8T9{{n&F^tQdjxZ+ToPL+ejrgwVht-N<>wf z*`zLA@lTy}Z!@)x7VR?ocwjcrObsLzu~Z`Vl#G4lDId}U6{rIKp$|?`biqTDYArFj z<%3Kg>QlRqW_}1_MIgFT$)5>NG^Is&fwaf`#mn7x5k%+`Gum^9ngxLDz$>APd0s~C87>;5N?Y-;SI zdxOEQd9=lpHZ_m_3UipX;S(1%o1K2gHk(UWVImMAmt-VEIbI}9*QW1Vbj0aH{WEP> zz*sSXhNM#$#(U|pR&`Gk_V{!-s&A~G8IYPp@=0^D4A%LBt?o^94Bd;6Ikp6c*4}fnbHN6# zyqQpuPsK*{( zsVnOA@d&uPDAUJ=-z8yekW+xMiAdBXGtn$I6XAr+bRea$@*sB^7s9twn?l3FP|>D?hq%zLcjzk!J(5qoiKo0(@s+TFE!gmx*p`gG z$QBO}NDibb?0QfAiLaF{9!9p5&j$@x#esK3b}JJ;JCY*r3%Doq3*aj`&w*&M@v1Pg z+wXqNE<1-j@_5qaHBu_KNmo1xl5t( zUua@{=iDSS=9~pwzJg<|zJ?{P*h`H`+Ua<8bJS{~zdg#bC$)UJs*%i-e_|pI5E`4< zks046DYdQh5QtqG`Lb{MEfasetkZ;JUH@<$QnHu$?zK*$?LWrh-scS?C>iQibwC9W?utXO2 z23~v{je~vUMSc`SzNXO@+T24=@A7x&; zm1alHWlMs50gXNEV|1)|AilUw5bjC~xF5(}PX`vFCjMb?Bq%C+Lc;YQ(zEoQKBQYx)I7ogB>=jBQoJ@K4cB6Jvzb zcteg?=9T2&MW&iQQ|)NX`~~EqLw_ZniDz;fPK^>HvH`{Yr@k#re7JG|&F|Te3MjZI zoR;0S_#8zOoEi2|!K!FRJfnPSg@uN`4LNfjONx?>*<&ezajtlqas_#=607iD9e{(y zpToX6cbY>8j&+}XI?vg^!~g>PxA3QbkoO7Onk;wj==wWgX^1UG31tbtas7hDMnfvo zW_^8{Tyk%x+Z?Yv>?d1UTFWQ1=jxx3v>y|pfN&uWk}#?@;c;qgLbEXDd`Y87^8nf z=PHaM&CDXy147}MjL?mX2t~31F1tk=O_93m>+LB@&u~ns3G*25p<0;0kCB2Hdx0l z7c)4#{)f}Dyw))_SbCs!+Olya;Vjj(gWa^R*RZf&{(oReh|~lyw>*;?^KIskN+6BP z9AR@Atmu7O&f30%9&v`o39T!P)*C=^tuN#hT7M-#@8JbCu5Ti#sXrHe zU?0?zr_$d;C;tNKdY0q4Ef7p!$eZFYJQB)(>EX+vNXg`de;6H6i5E6b2%wZg3^H(E z85ExUq2s6f!?^J2=KCLhgtw(6YY#LMe6L<|es>S)ImHdAG!I@BjGf;f>}lF9|0+an z&>OPf=TU((F`JGFo9+Q-H_%Q)e@zC$`?sdiOM24AY-Os**^k5_zJjQ9JM>u>FGn`q zu}GhBS!W9c%1#0W^ygUPN;~Pk>a)2wQkwORZ=&L`&25d62cM0*Yy9DtTBGT5ax8vEz26hz=t$q7Tn=!|;hSZ( oS>(*I&P?T^_554xE?v5zN}|dDi%`czUf?fPt2e9FY9%WC2Wag`kg6tE#aUl9Jh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6wj*(#[0-9]+)/?$', views.SingleUserView.as_view( + { + 'get': 'get', + 'put': 'update' + }), name='user'), + url('me/', views.UserLogin.as_view( + { + 'post': 'post' + }), name='login') +] diff --git a/app/users/views.py b/app/users/views.py new file mode 100644 index 0000000..edf949f --- /dev/null +++ b/app/users/views.py @@ -0,0 +1,84 @@ +from django.http import JsonResponse, HttpResponse +from datetime import datetime, timedelta +from rest_framework.viewsets import ViewSet +import json +import jwt +import hashlib + +registered_users = [] + + +class User: + ID = 0 + + def __init__(self, username, password): + User.ID += 1 + self.id = User.ID + self.username = username + self.password_hash = get_hash(password) + + # def __str__(self): + # # return json.dumps({'id': str(self.id), 'username': self.username}) + + def obj(self): + return {'id': str(self.id), 'username': self.username} + + +class UsersView(ViewSet): + + def get(self, request): + return JsonResponse({'users': [i.obj() for i in registered_users]}) + + def delete(self, request): + print('delete') + return HttpResponse() + + def post(self, request): + return self.create_user(request) + + def create_user(self, request): + body = json.loads(request.body.decode('utf-8')) + user = body['username'] + is_user_present = len(list(filter(lambda x: x.username == user, registered_users))) == 0 + if is_user_present: + registered_users.append(User(body['username'], body['password'])) + return JsonResponse({'message': 'Successfully created user'}, status=201) + else: + return JsonResponse({'message': 'User with such username already exists'}, status=409) + + +class SingleUserView(ViewSet): + def get(self, request, user_id: int): + for u in registered_users: + if u.id == int(user_id): + return JsonResponse({'user': u.obj()}) + return JsonResponse({'message': 'User wasn\'t found'}) + + def update(self, request, user_id): + body = json.loads(request.body.decode('utf-8')) + user = [x for x in registered_users if x.username == body['username'] and x.id == int(user_id)] + if len(user) == 0: + return JsonResponse({'message': 'Unable update user'}, status=409) + user[0].password_hash = get_hash(body['password']) + return JsonResponse({'message': 'Successfully updated user'}) + + +class UserLogin(ViewSet): + def post(self, request): + body = json.loads(request.body.decode('utf-8')) + user = [x for x in registered_users if x.username == body['username']] + if len(user) == 0: + return JsonResponse({'message': 'User wasn\'t found'}, status=401) + elif user[0].password_hash != get_hash(body['password']): + return JsonResponse({'message': 'Password is incorrect'}, status=401) + user_token = create_user_token(user[0]) + return JsonResponse({'access_token': user_token}, status=201) + + +def create_user_token(user): + return jwt.encode({'username': user.username, 'exp': (datetime.now() + timedelta(seconds=5)).timestamp()}, + user.password_hash, algorithm='HS256').decode() + + +def get_hash(data): + return hashlib.sha256(data.encode('utf-8')).hexdigest() diff --git a/books/books/__pycache__/__init__.cpython-38.pyc b/books/books/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 344a05863aca200078b73e58a4dc2b444e56e7d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmWIL<>g`kg6(QdaUl9Jh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2D3*($~%T iCJBj*kI&4@EQycTE2zB1VUwGmQks)$2h#Z&h#3HC-5B8j diff --git a/books/books/__pycache__/wsgi.cpython-38.pyc b/books/books/__pycache__/wsgi.cpython-38.pyc deleted file mode 100644 index d563047c9e4ee597a519a03886f9a450554d3b83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 522 zcmYjO&2AGh5cWE06Bb%=K%9E;2`LinR^nD6#7|2U1nD8IkfMr>yPj+uyq?wCO=+)v z1YQ7+eS$s-7hgH?3Y?hTfRvH^S>v(4@AJ&*&Q8oI?w;_^-y+6-HOXdlDS1W9?Gq?A zWhzqKMAL}&j^Slz+KCuM%kK9r)m2+RIAxt1Hr-Zy!Uo&dKjTl6;Yj3Y=eiJcti?Iv z7bmJ3myr7;jz?a=*9sjt;TIs**QGJ?+<=fyNKvA?F!0R43XE9ETDK)DOHg7qlT~GO zF17^@l8vmwdu<;-Yj|%WvbCzw`2#*inza9^dv&qrP$!I*BWFLg`kf-Of_<3RLd5CH>>K!yVl7qb9~6oz01O-8?!3`HPe1o6vXKeRZts8~Ng zCpEh`F)yV^-z7h}G&eP`q*y;VAXq;sKR>%z!C2o3L_>HGGCnskFEKq8q*6aVJ~J<~ aBtBlRpz;=nO>TZlX-=vg$fD0c%m4s9VJg1> diff --git a/books/books_management/__pycache__/__init__.cpython-38.pyc b/books/books_management/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 1a5666d4e4f21f1147c7c6e2d2b43f52794b59e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135 zcmWIL<>g`kf-Of_<3RLd5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HHvsH{!etvdw zOcIzZElw?pFG@|%EG{WZEXmBzi;0iV%*!l^kJl@xyv1RYo1apelWGStg`kf-Of_V-N=!FabFZKwK;UBvKes7;_kM8KW2(8B&;n88n$+0!0}# z8E>&BrsQVk`Drpm@ug%X=B4NBCFkdr6lEqAfecv5P{a(Rz{D?2XR8>e{QT_Vm?SV+ sTAW%GUzD1jSzJ<-Sdy8a7Xvm+ub}c4hfQvNN@-529V1ZpXCP((0ES^H^8f$< diff --git a/books/books_management/__pycache__/apps.cpython-38.pyc b/books/books_management/__pycache__/apps.cpython-38.pyc deleted file mode 100644 index 1baf61d014ba201fbf0fef42766ee19d6e0cb314..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 376 zcmZWly-ve05I)DLl$KV4g_*Hg`Tz)_LI<`Epe|N~Ww40?N$lFqzzi?L&Li*?UYU4> zPMjU2M&hLV?&rHddo!C&0Oj**biYOW9fF~x7%WlU8G-~!YG?=}1a3eklD>naio8*l z$n*~K(GONZPW<@Fn3Zmys`4>phC(n{qPk-Q1&m0*Zo;Tca02zG8>j58Dl50MVpr+5 zpTkZ%%yT;+)_LkP&Rfwa&i#b*M$4`a{*?1=C+a^Y4fX?Uh6^L?=d1JLTI&~AJVg9A zx)8>=g?Y6Ud%_>M1R*0aUgMQV@>#T{UW7UuxX2t<^xhZ=pI!8KU51y(IaB%zWMNpS diff --git a/books/books_management/__pycache__/models.cpython-38.pyc b/books/books_management/__pycache__/models.cpython-38.pyc deleted file mode 100644 index 86fa78d7b32a4d13c4b06be1ec8551e14d53503f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172 zcmWIL<>g`kf)W=BK3Q6#Hp1-QrBiO3X{o*Gow%0%=~!P{abHz{D?2XR8>e{QT_Vm?Q)lpPQJM gn4X%OnpYA7F-EVT@)m~;kX@RSYR3rF{TYZE0JX6wQvd(} diff --git a/books/books_management/__pycache__/urls.cpython-37.pyc b/books/books_management/__pycache__/urls.cpython-37.pyc deleted file mode 100644 index 49c2e859c5a32d90f4a0b5b058c89b1507d58909..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 350 zcmXw!y-ve06ou_LPSPZO1SHlh^}vb{irA14QpA!0WUv*|#&%^V{B&jJL3j(E!z)w8 z8!&O*s8{lL&$W(^?RvS)5!CNVasLhdCk)3*L0rOeH9#PN2c&R|F%pp=L!?McFp^lr zlC0y82owcH4=M?JA{E&u=}4Q1MT4q*4>_p9KHl>2d5`CTzV!0d?G{#cy&I)vyJRq3 zubD@l_FBlda6)q>Z>rQ6V$F4DR+Al^jUd6&fEeJjV}z%N1Ydv)u7XhQzK|`0GvM!< z-8pF;GlSeXu7zb+=4nz=kB&9B4SO_Z<4(_59r`@Yd{A8TP6j6nPgrf=eRi&lm9RCCHiC$^| diff --git a/books/books_management/__pycache__/urls.cpython-38.pyc b/books/books_management/__pycache__/urls.cpython-38.pyc deleted file mode 100644 index 65bac0dad216d4f067b7150dee3aa2ae4f2cc020..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 320 zcmYk2&q~BF5XLi2({|ggZ{S&a*}aR1tamTsrIeM(gxa-9m!#{Ty?XXRd<&l=S5J#? zAP7!YL6Gm!vQKi1Xl{D=z+t}XOhzM2$iAj9+i|}Gk@C9|zRRlHemDw_M2KhWX uKW-MoaOawvKfTt{$X01(#wOrr>nER`=m9sqIt`C?;k!yGc@8-oe0>AH6H>ka diff --git a/books/books_management/__pycache__/views.cpython-37.pyc b/books/books_management/__pycache__/views.cpython-37.pyc deleted file mode 100644 index f4cb2c770e8670e84f31b07a7fb390bcc7a72094..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1576 zcma)6OOG2x5bnp!c>IFRYl8uaIYsgzF$xdm5Jf0(h!9#uYjGJNRufmpJN7(GcN=F{ z-dw_U4;(=pyhr{5|A4>HS5EsEI8oKJc2E$48Fh76S3j!0ueu)h`yGZ~_v7xjVT-Xp zNZEb@2cKax-=F{nykI3yc_~uCNf(7IWh%=c4ZJRkFb#2D7Ev1IENx+nA%O4)mbM{+ z7WN&8p^bePI?%+(t`004`tgh!K`DpZ*`bKNyQJGcQRME=O_qke`dy5knZ?7YK z4f(}{rt!{SiAspg`+JB3%Qk$?W@00OU*Lbn*Br!vor$%u!93FUZ3q&AoI?oFxfnCF zw+74xYw?hITZ}e@nry%@^3K6&xU|pjK5#KbU1f+}t5n9Izo&O7-ei^A0rE^Oz^Ph%c3|*Cb-Ee@W)*Kj`0j5 z7tD}P7Z!B}#s$x6IB^lENe#;FZ2?l?88Mnb8cL06$n&Gb+( zex1ko?};AwmL0kQ8h7aahRT1S+xjQEgzMl6WVBQfsXy)^*g2MXr@O$}9agt{ZkVp{%cwAU5<35;sW@QLanm9NBuV zDx(Q)C#!npt~F`kDQ&F7;5DWC*6Xc!Ehf<$^ir-&^Xgq(X5L4!Bbe{u@2TcJB-<1I z9@=+=lT$mwKSaf{89t{q)~x~7Dv;-LBQBVgn*e1`uULAm&0S;MkWkEnneqWTJI5%569QQV1d`~{pQXbb=V diff --git a/books/books_management/__pycache__/views.cpython-38.pyc b/books/books_management/__pycache__/views.cpython-38.pyc deleted file mode 100644 index c56488e0e37c3c810d9d785e97e5c0836ffd0364..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1558 zcma)6OOG2x5bnpk{6bDb63jy~mk1vcp(r4PB812#;$Wd&tS`Sf%%uMrky}T0LJXV_z{%vtNJm=*zut z>UwVWo?goRGOMzwDph6oe#q6z+*=&FWV;n%Xy{BPG>xysBhe6I^YQH9z_JZrvzgdP z;HUT>^EC%CV8>!DY%q`XeH(&=Amc`r`q!!CE|I-j|>c;Uph0%&a|r9WL#& zyI;73vZ^w~CoYvS81vDcCukb3a(gf|%gMoTJ7bs?T4iuJR4)(^lmCLqz2Yu1HnU6P zl0|0Bs@8Bc*j-L0xY@I1Q5+5@xX3E-+noJ@`HUhL%#bq|7Ig;31y5@@bP=dY4a)Vm zASv*O8%;osq{i#U^t&irKoNQH8_^<9grbklbWt#VohSJBMVEWe6@&wvaDnjesQd@Q z@jnqJYzOB!&!yhEKzYl6un{uztBq*hv0O`NAtUi7Ic4)e|7b(Bv>QfjYt3JBnn`Rm zEB=qqGuavI4rZV!Yu!Wf@AN^f%&@9$!(aY0(x=~{Wdz&N&F{|X8|G%1SCd+6HL}&sAkKA?;*UuiUlf8hA<@?lAb10$T&i$GfKZ{Rh(mjUUaW|flbKqZ`X}r3S;$5e zN{aUa6zFRK8s$n~=|Y3~D5j%d+$*Mv^2SxZ>|NIeN}3L^4$DYAjEsvT&hVT8MIk{H z3S^-xQ5YPpk~jj#s}v+Z21bzFmYoFh5c|M!Xp4mGEhwH76zc1ko2Mnn) y$`~V+dUUJ)n%jXrg`kf-Of_<3RLd5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HyvsH{!etvdw zOcIzZElw?pFG@|%EG{WZEXmBzi^uc;^Q;(GE3s)^$IF)aoFVMr datetime.now(): - return JsonResponse(user_record, status=201) - else: - user_tokens[user] = create_user_token(body) - return JsonResponse(user_tokens[user], status=201) - else: - user_tokens[user] = create_user_token(body) - return JsonResponse(user_tokens[user], status=201) - - -def create_user_token(body): - user_record = {'access_token': jwt.encode(body, body['password'], algorithm='HS256').decode(), - # 'expires_in': datetime.now() + timedelta(days=1)} - 'expires_in': datetime.now() + timedelta(seconds=5)} - return user_record diff --git a/requirements.txt b/requirements.txt index e4c6d85..85ecceb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,20 @@ -PyJWT asgiref==3.2.3 +atomicwrites==1.3.0 +attrs==19.3.0 +colorama==0.4.3 +coverage==5.0.1 Django==3.0.1 +djangorestframework==3.11.0 +mock==3.0.5 +more-itertools==8.0.2 +packaging==20.0 +pluggy==0.13.1 +py==1.8.1 +PyJWT==1.7.1 +pyparsing==2.4.6 pytz==2019.3 +six==1.13.0 sqlparse==0.3.0 +wcwidth==0.1.8 +pytest==5.3.2 +pytest-cov==2.8.1 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..04f818c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[tool:pytest] +DJANGO_SETTINGS_MODULE=app.books.settings +testpaths = tests/ +console_output_style = progress \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/__init__.cpython-38.pyc b/tests/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04d467b198898ac0f77ca9294d35e00be249f650 GIT binary patch literal 122 zcmWIL<>g`k0@c@IaUl9Jh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2DW*(#z literal 0 HcmV?d00001 diff --git a/tests/users/__init__.py b/tests/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/users/__pycache__/__init__.cpython-38.pyc b/tests/users/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab9be40b81ddea921581b64de4669589879a1ec4 GIT binary patch literal 128 zcmWIL<>g`kg4qtjaUl9Jh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2DN*(#3aALW>Z(7W%l?2_wwoI0qHC}EopXo78Hw_vtstJcbMBox zckcPlch0$2Cnmgq{%h{DmnSvtAKJb@6Ze|I?rqHtH+(Q8hsaYd0eg;e8- z5^LO4Qx+4fDW;l=m}HHYYNo_AYf7nRM$EFNoNA7UqpX=oHFM%A)>Kl>F>#zVle`g6 z#}g|-7^Ry)TjIzd>dMxtp4kLy(>}a~Uw8~6(ss0-=Iasf#EyZsDKu=Yxg5K#;FjOT zR>p3p9)`EtQdBKz;HlUnb?6K1x1{-Eixjl=VbEOL(_{(C2QR$!>a_^Vy7p$feIw*% zc#WUrHb1`5xf9zVR@y1zkLz$UxP6^GOoPAYihJxm!)jWc-CKQ%dL9k2e&)TdaE9xC!SFY=NH3*w;IvLUO{@a zvZJ(kydiI1IZb^fT9$frDSNZ1Zq$p;WN*$f9F-{36w~Ufh46Mn0-pW`MPVRz^fk$&NrPh<+(PKQ8 z6_fZk>&kX9&utsgocm zpCR!z63>$OI*Ah`=1F{m#5W;o(+O5u?I^*Ee2zL#k|46lSa&Y9PjDOb&}gkQJJ9Nw z^I&4Va=^%*zJ2l#Gv}r`+}3}^3r4oaK4v%ucXnc5gIVS)FhW7-Y+oa_ubHyU3i=hx ze8$Y8SO#$_9dVOKXyBor@(8r{@sRmH@X((Az_@s^s&BtMq@jx!nPSdVL+>Y~Lt|d4 zmZ~AMve#FLAQ!Hhauye2qa_k&wzE8St{T>$JXf6@p@qn8t9>(2Eaixx8GzcK1SJfK zs-h72Jl=kQFiy)CX;DleiYU(3GB@Y)NN#XETX;k5!4G0zlkx+p{p&QL$)NIeqghG?TA$|^4OB8fu z_+1AlD&pNdm|%Xb-9=RUHuenh@&f`T0-!=C8VNcPLQf^} zQ*|MbCS8jFE|Z$quX_FY1mPDbh=!4j^dG-X*?Y}D&PX@^+p(kaq;r))u2PsP!Z!9dIe!1ws~FhyQFwbl4SVr z6*+<`_`gO^zd+Mvg60{8c(wZT*_(4&U-(XqTULsDSi8;!+qqyd^tbKC3e5ZJI0Z&(02_4RU;Qve-5H7zym0#S}Ez; zDW#4lU3guR+Br#31)|fS`naiGaXFr*Y-5;8`y2IeW91RSG)s?mqa}jd2|IQ>NPzzf zb{rcp70an9^L8XWrYtwLKP?)^a^q&nS&wqiG%f!VTQARY0;$jrY)2t;;PjAM!;+@3 z5jJ5tB23Dk(2cY<9rwF5-!h(kAj^%9-}1#p68qu(@&=pf#o# z_$_W=Yc9@Ub)%;vUYR{J{AFYeOr%IAY|MJLu}N`|U zp<_goN1#P1Qc|K>T%uWQeQ*FqAXOe(ABa;08HX3-L1{UbLs zd`JmA)j34RpEDhASI3a?_!)ABBqOGV{0cXevcQUny-oSA%7izVh*c)+q@+Bq(m9Nj%|S(sT-jTeFJaC1=~RTvZt?hZtfF+u_pwWj zDA*d7HXmWG$2IWbCCq%zgj9GngpdE!ko1~_m}lUeZoXb!c!)K}Om?_Q2Y5|Bx^7yk zfbARWTF#7LL}2SNu@!l0a%^J5zsOs!VI&)rvP*7L!8$Isme5-sTd+xO|_^ zedG^FJiIuqWT^KF4X0=%|35Z6nYS6S=|Q`Fg7>8X^#GwRJ_*&8P0gq%U8^AeDi0B2D2?zE0wM^z_^aOL6H#oN#tK zD$^eUYjn=>20ivB39^ks+}qT8;sjq_rpNZh@#hX=uc5waL>WGw3+bqZqIpF>Br9Kh z6a){}}c<+Z7i5<3$#LzU^# dy2pJ`?7pTp@O6qxA$_B(&EOEyg~iI|e*ju!2;~3( literal 0 HcmV?d00001 diff --git a/tests/users/__pycache__/test_data.cpython-38.pyc b/tests/users/__pycache__/test_data.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1298d241db39223b7ea09edfb4b85d9e18431b11 GIT binary patch literal 1965 zcmZ`)OOF#r5bmCr$Fuf?UC0U&AqYZ5lSn>93W`t^kpRglRvs*%(JHOUbT8ODo-y5R z7A$j#_ZsmBICzizrM@PYw+QXH`nq-Vlmrqod-dE!Y2qD+Kt#W-*v@jT<91L;trICw`Sev|)h z-Mh1|0oML^RduNBQ0?1DJGSAS;Va`SKsszfi1NS?8q)E_wR}2u$M{+{9Cqvh9lW|D z#nYbBLJDqisvNus25GNUJCLK$S)cV`}0;QzHVf!q&6byEj_&CP>;wb)B0SUTSl@Fw>q}c(|V9xTaC;;J6 zPd|I-N=(yyh^Mjl;o~P$qe5HuYO{sAw~<5!_psA1(gwG}ONkRXvD2dCYwLWSiP%g{ z7fxAHqe-8lQ2zufm-8krgBH zeG*ATEn_qvfH5t`@~YHXuT+d#GUWwa#JuD-m~?y2U|F5AD0gu2A{vy~3Q4<*l*LOB zMX!8nHL(l3R0|9+MPtiVM8`Kbu(cMtnX<3oxFuzf`kIv6mXb#om)`t^fqRPrQVp(}cXYO)0;uK*|$3|}D=X&|$n63R3myv)TAtFV^T zN3f}`PgiMae5*z)FUxIGSERgzXHoIKs;;iGj51tf`ZEYp_V33E>$0;$$DeQJ`b>Nh znUJwE8wkcs#Mq$XqXNfJ8qY^rQLjjpucg5F!%XMTB#K&M`<5T0xq)U`4t9i%Lt;0@ zCZ(`yLMQe@8invi^{-t|8uBXc#o|ok^#*I3eR^T(1c0JYw&mO4^KLca*L5D>;J=L( LVhA0*MO*Iwft=d4 literal 0 HcmV?d00001 diff --git a/tests/users/test_data.py b/tests/users/test_data.py new file mode 100644 index 0000000..37df02a --- /dev/null +++ b/tests/users/test_data.py @@ -0,0 +1,100 @@ +import pytest +import mock +from app.users.data import UsersData +from app.users.models import User + + +class TestUsersData: + + def setup(self) -> None: + self.users_data = UsersData() + + def setup_class(cls): + cls.user = User('new_user', 'password') + + @mock.patch('app.users.data.registered_users') + def test_add_new_user(self, mock_registered_users): + mock_registered_users.append = mock.Mock() + + result = self.users_data.add(self.user) + + assert result is True + mock_registered_users.append.assert_called_with(self.user) + + @mock.patch('app.users.data.registered_users') + def test_add_existing_user(self, mock_registered_users): + mock_registered_users.append = mock.Mock() + mock_registered_users.__contains__ = mock.Mock(return_value=True) + + result = self.users_data.add(self.user) + + assert result is False + mock_registered_users.append.assert_not_called() + mock_registered_users.__contains__.assert_called_with(self.user) + + @mock.patch('app.users.data.registered_users') + def test_delete_existing_user(self, mock_registered_users): + mock_registered_users.remove = mock.Mock() + + result = self.users_data.delete(self.user) + + assert result == self.user + mock_registered_users.remove.assert_called_with(self.user) + + @mock.patch('app.users.data.registered_users') + def test_delete_not_existing_user(self, mock_registered_users): + mock_registered_users.remove = mock.Mock(side_effect=ValueError) + + with pytest.raises(Exception) as e: + self.users_data.delete(self.user) + + assert str(e.value) == 'User not found' + mock_registered_users.remove.assert_called_with(self.user) + + @mock.patch('app.users.data.registered_users') + def test_update_existing_user(self, mock_registered_users): + mock_registered_users.index = mock.Mock() + + result = self.users_data.update(self.user, 'new_password_hash') + + assert result is True + mock_registered_users.index.assert_called_with(self.user) + + @mock.patch('app.users.data.registered_users') + def test_update_not_existing_user(self, mock_registered_users): + mock_registered_users.index = mock.Mock(side_effect=ValueError) + + new_user = User('new_user2', 'password2') + result = self.users_data.update(new_user, 'new_password_hash') + + assert result is False + mock_registered_users.index.assert_called_with(new_user) + + def test_get_without_parameters_returns_registered_users(self): + result = self.users_data.get() + + assert result == [] + + @mock.patch('app.users.data.registered_users') + def test_get_with_valid_id_parameter(self, mock_registered_users): + mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) + + result = self.users_data.get(id=1) + + assert result == [self.user] + + @mock.patch('app.users.data.registered_users') + def test_get_with_not_valid_id_parameter(self, mock_registered_users): + mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) + + result = self.users_data.get(id=2) + + assert result == [] + + @mock.patch('app.users.data.registered_users') + def test_get_with_invalid_parameter(self, mock_registered_users): + mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) + + result = self.users_data.get(id_d=2) + + assert result == [self.user] diff --git a/tests/users/test_views.py b/tests/users/test_views.py new file mode 100644 index 0000000..e69de29 From bcc810424a0600d4ea9e81468cb7c988910cefc9 Mon Sep 17 00:00:00 2001 From: vdruzhinin Date: Fri, 10 Jan 2020 15:56:23 +0200 Subject: [PATCH 03/14] Fix pytest runner --- .gitignore | 1 + app/__pycache__/__init__.cpython-38.pyc | Bin 120 -> 0 bytes app/app/__init__.py | 0 app/app/__pycache__/__init__.cpython-37.pyc | Bin 155 -> 0 bytes app/app/__pycache__/__init__.cpython-38.pyc | Bin 128 -> 0 bytes app/app/__pycache__/settings.cpython-37.pyc | Bin 2207 -> 0 bytes app/app/__pycache__/settings.cpython-38.pyc | Bin 2204 -> 0 bytes app/app/__pycache__/urls.cpython-37.pyc | Bin 1001 -> 0 bytes app/app/__pycache__/urls.cpython-38.pyc | Bin 949 -> 0 bytes app/app/__pycache__/wsgi.cpython-37.pyc | Bin 554 -> 0 bytes app/app/__pycache__/wsgi.cpython-38.pyc | Bin 519 -> 0 bytes app/{app => }/asgi.py | 0 app/{app => }/settings.py | 0 app/{app => }/urls.py | 4 ++-- app/users/__pycache__/__init__.cpython-38.pyc | Bin 128 -> 0 bytes app/users/__pycache__/admin.cpython-38.pyc | Bin 169 -> 0 bytes app/users/__pycache__/data.cpython-38.pyc | Bin 1466 -> 0 bytes app/users/__pycache__/models.cpython-38.pyc | Bin 1014 -> 0 bytes app/users/__pycache__/tests.cpython-38.pyc | Bin 167 -> 0 bytes app/users/__pycache__/urls.cpython-38.pyc | Bin 514 -> 0 bytes app/users/__pycache__/views.cpython-38.pyc | Bin 4238 -> 0 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 139 -> 0 bytes app/{app => }/wsgi.py | 0 app/manage.py => manage.py | 0 requirements.txt | 8 ++++++-- setup.cfg | 7 +++++-- tests/users/test_views.py | 9 +++++++++ 27 files changed, 23 insertions(+), 6 deletions(-) delete mode 100644 app/__pycache__/__init__.cpython-38.pyc delete mode 100644 app/app/__init__.py delete mode 100644 app/app/__pycache__/__init__.cpython-37.pyc delete mode 100644 app/app/__pycache__/__init__.cpython-38.pyc delete mode 100644 app/app/__pycache__/settings.cpython-37.pyc delete mode 100644 app/app/__pycache__/settings.cpython-38.pyc delete mode 100644 app/app/__pycache__/urls.cpython-37.pyc delete mode 100644 app/app/__pycache__/urls.cpython-38.pyc delete mode 100644 app/app/__pycache__/wsgi.cpython-37.pyc delete mode 100644 app/app/__pycache__/wsgi.cpython-38.pyc rename app/{app => }/asgi.py (100%) rename app/{app => }/settings.py (100%) rename app/{app => }/urls.py (89%) delete mode 100644 app/users/__pycache__/__init__.cpython-38.pyc delete mode 100644 app/users/__pycache__/admin.cpython-38.pyc delete mode 100644 app/users/__pycache__/data.cpython-38.pyc delete mode 100644 app/users/__pycache__/models.cpython-38.pyc delete mode 100644 app/users/__pycache__/tests.cpython-38.pyc delete mode 100644 app/users/__pycache__/urls.cpython-38.pyc delete mode 100644 app/users/__pycache__/views.cpython-38.pyc delete mode 100644 app/users/migrations/__pycache__/__init__.cpython-38.pyc rename app/{app => }/wsgi.py (100%) rename app/manage.py => manage.py (100%) diff --git a/.gitignore b/.gitignore index a66ca99..917efab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /env* /.* /coverage +*.pyc diff --git a/app/__pycache__/__init__.cpython-38.pyc b/app/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 7cb012bb006c4d1c913dbdc663ef2e19d79a862f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120 zcmWIL<>g`kf@#*maUl9Jh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2BY*(#PO2S9+h-tV001C-7svnr diff --git a/app/app/__init__.py b/app/app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/app/__pycache__/__init__.cpython-37.pyc b/app/app/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index 7e7b1ef4587a97779a61d61625ff09d7c6c669c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155 zcmZ?b<>g`kf}^cGaUl9Jh=2h`Aj1KOi&=m~3PUi1CZpd2 QKczG$)edC(XCP((06sY;5dZ)H diff --git a/app/app/__pycache__/__init__.cpython-38.pyc b/app/app/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 80d4d3f2098d9246a664b7db77dc8e7dd289de44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128 zcmWIL<>g`kf|oOR<3RLd5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HjvsFw-7 oQ+|GSaZC~t86TgSmst`YuUAlci^C>2KczG$)efZnGY~TX08@+`u>b%7 diff --git a/app/app/__pycache__/settings.cpython-37.pyc b/app/app/__pycache__/settings.cpython-37.pyc deleted file mode 100644 index f8a7b819d301e06dffec373b0174bc333ddd0b1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2207 zcmb7F$#UC95aj}q6s^UJyu@~FINl;90wl{@r4mOF3Ck>VD*?0;9E<|dBWNTnG-hB$ z_<{U{5AiX7l5fB@r~E=rX@I1}qI^&|#9*eE*F8ONFkAEU)g1nw{XX}8aW0qpJDrSw z6&!5h!~Vh!az+mFP;m1`p5zY-e-tNrfy{i)r_Crq(I|spRGgeK3o}rHGE`s|s!qwM z!UdRvd02o&xQO!$aLJg1%T5jixB^RX6|TW`N1%DQ@kwy<#yoswEWk~;l|@=4CAj^$ z0AGJ9(7bUG?!aBNyaQs^-^==MXxLa*kaT@p{{`o$|aMUH=V{}s8=X}6^kYxyLCJkeZ zGq3|sCccb-G7kpSX7V^)c?i$Aw{_b)eAk2d4=7w&!A zd#=Yz!}UmZ;Pa67rL1=Gm1$|j_v2er!qkQMoF5Mei?2^98DR{oc~;?}!gA2#=ClXL z`rB#MS-d*Zc@)4UM^;EAM(il0{8-XPgVu>IemV^`3NB6fYeU(D*YepQxSd|$OSSAZ zVftb!fQ9v#1Xpm5rJ#03p$W(7%*LyqbZ!kRGU4_73z@mpK0L7OL!`0v%j?kcNN14p zl}-3nv*RH}q03@FSM6TZ^v^i*>{tDp`X8);HU{qzq)wid=p|Q0Sl5j(u<`CmV4Ug`@Fu zAt|Zt?M7QogtpRB!!n-6_{~(>8Dj<(V@E*_wbt)T6GI()``f zA_(JG=S6B!pL&tkB)-G$RlXYSimZbywL@lewKvM>l&r z&044ZI+@kgR=26>s+L^b)wUa{eX~(hQkEpbK|uXvwyx+(8eB_mC_Vj+*;O=cx3g0> z-zm)oDmzHA`KHp|?kU@95vV1+DR#u=?1Pu$4Ip_ zn?G~mU8GLd{acq)VlX*y$sxn5B9!ZajrH|8leMlU4==(Ec`LmThqF;XzJR2`aZ>rw z3m|gIs|yIsy4VSYO7V7CC<+CkRLGB8NytWpv@aEfiCHRHC<*2Cy;Lm~(^jpZ_pp`z E1I}9JMgRZ+ diff --git a/app/app/__pycache__/settings.cpython-38.pyc b/app/app/__pycache__/settings.cpython-38.pyc deleted file mode 100644 index 5975843ed0083f9f1bee81757cbc42984cbe0d31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2204 zcmb7FTXWM!6qX&uc1!|+giC>fDA&+NP6!2tX=xN&1P{JsBAK)r292z3& zV3gqmOv4PEgi|n!>l1L=n1(Zs03yu6Je-AdaNZGV4lcYCot!ZPUl}LiB3#NMogxLe z{5}s~zsu8{F$-7VDq3CvDI2e4<2N+th{kESZk&M|#vJA`kIz|r&cRLNJmfWD;g)qL zCkW-*j^#Un#0clqcbK#tgp$?oOZ_m|AvTxG<#pnd(BcH7?w)iTGW$gfJnBn~TOl7> z-j*VU2+|O6MP8DZ(fcXlc|k}d>LavgaT@p%{&R(Q~-HmmF>TN%Lf9+9>q*Ogq#xD;$3ZpQKf0}&cZZ!I?>?El0H$@WN~ zzwzjWR($XKYb#6r-s;=?k0O)OZQ}4<=kC2*mfelKH$UF!-o1H$>D8l^#og6-`JsE= z_8#c*{9r$_9r!$?T{){=JU1?lcrU&*CQMz3&-q@Tu=xC#k`czRnnx8LDl7**E{=O} ztiPRBoyChooksy&vTKDzX2gy{%J*b#IB6c};``%J!{G9Wzc!GKcr6?ag2(9vzFf`z zMob@$1+cIlqu>gzu@uzKC^X_Yp4tBHN1dC4ii~(Y_(EnbwO{X8_G_fE{PWY$@<_X% z@|BJFm9RPFD0Ep|86t4v^e^F)Qh|Z@g3ef^#AF&{sYEu40AjLSUoO4EAmK4?V|{j#9xet<)O zQEj)LCR4iF+-NAeswJ~q+Il^;Z`7+w%8^9e38grlGXfJIcChR@*fdVe3uRG}=guQU}wnFF$GxW{u^gR#Hf1x`8{4*4|A=#lKe!A3 E0Wg~7YXATM diff --git a/app/app/__pycache__/urls.cpython-37.pyc b/app/app/__pycache__/urls.cpython-37.pyc deleted file mode 100644 index 08d3a8872bbc03024f63d0e3ff6ada1f6dd0659a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1001 zcma)5L2uJA6n4^dEm;S5IhV&Eh)UNWm#dZspRS!mW9$#ids{5KtV0wmx6v(3A_*zG9f7}_*_Wl zAYfHetSD5JjzehHQtL{M5h5a230Yyj5S0`wn(HJN3{j3wlE+Aks>l_x#mHQ*Q26>A zW(G@*!;6~dmfiMR2l^y|FA0H|!-7>p>M?|KDHvq88Lp}}xmW+Rt#^`*fIWrgs1dsy zM$xF%wG@nwAjgb8kL*5*!}Ag=^=M{mZg zhVeg)E_q(oq~Hr6u-31ST|2(R94{!NT%+|vm&w@Q zl*3vdH~{oMzSs1!%yd+irT1GAM!S~9Oq%Wn&EcLhX$A^NJ4H>P3Qe29>TM49df*3; zcK%%WJwaEBN`*v8ZxrT4qE{lXEiZ+pAEszKJ16MO&d~O+jTSelZkzro6QnNbiw6c& O^-T!8fp^>)cz*zn%{VOp diff --git a/app/app/__pycache__/urls.cpython-38.pyc b/app/app/__pycache__/urls.cpython-38.pyc deleted file mode 100644 index 5e87df873040dd631a62f306aae4c6b76fd7a2da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 949 zcma)5O^?$s5Ovb@gKQU&xNt%8EsazXU~dtig%vASLL7j-L={qSTmHAMVBI;1L zbhw+kj#&@N0rygm57VK8*pPb6qr;uEb74JV?q;+Z@7$G}PU!d}3MZs=5h8VsFF(A) zXQEmbt40!Ch|2drtr@;*WLXofnXJ?mE(@ix6pd!e1SqTp{#vkaD#GVNVlE`ZMYR-? z+Z4Di8Bq)?#u6XHSZiIYB#x=bRYW(WT8UbU4a;?u3m&s7K8c>hTGU0ZVgrt$yo&ud z-wB6YD)G;oDmU!*Wo^=@5q?D}CKWDtEu>B`UP{3+zsZcS{b3LPtixN$AfTrZLP6|$ z76kK7*IIBk#}(o1MPSxJ=%1BDsmIqKjP|nrkA=`{CJm9u8g-!a7IInkw`61R5mA{* zG7JA6!9WGtW3X%%;$>%LvbSsBj>s5(AlYH()|6Yb zy)MNn1_KY30&`&V24U#`f@_OHryUZ?iz;;6affR?WBqp2C?-|B_3ht=ZATUxzYp)U z<1B;AlV$1MjxWg6vbdITjZu5l6CvUCAZeaU>#0Js)-%%Dqh7mzAZZ?({rrBECkwdO odhuF_FKPiDv(p4c^-Vi@&IN5s_R@g}Wx~9BQ|JC*>P-Lq1;fW9RR910 diff --git a/app/app/__pycache__/wsgi.cpython-37.pyc b/app/app/__pycache__/wsgi.cpython-37.pyc deleted file mode 100644 index f569be5261d74a4b08e639143c8379e712cf19f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 554 zcmYjO!A=`75cN8NL|Z^Tpq_n=)UJ`ZRaHe%Xq2i1qy<&Vp&PTFY#h9vmFAIio1IOZ3?+T%iNaixI?#{?@;t~9*Sum;W6!m3Tlh!P~5%|umcl?$)X z%w~b_W6&tk0#hc)m74-X6UdcgbEV-ma{%$e`^vrLTw?ALxfG_rCr>$+91K594)_9Z zJaP$@#@iB%=QplUJbnz#Ad^84!l>UaD!gm%d%h~%W8_Qe%abc^58ycNG%OAbq~y1Su}MVE=rh-mG^OU#kKjg>FH?-M*Y!s8~w(h@c8~(l@NL>S6 K+NV2okNyLuysdTs diff --git a/app/app/__pycache__/wsgi.cpython-38.pyc b/app/app/__pycache__/wsgi.cpython-38.pyc deleted file mode 100644 index 1660c71d231d5c73e198ac485d2f6d3b312b7bb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 519 zcmYjO!D<^Z5Y@^~>~51%Xeqe_bTzcQf!;zXX-R4v66fHAf-zmItMOW{R1L~qORvNdi+45CH*eoM90)&r-k^@EMK6(6zQ_TBIJ^Qb=%Iog>n#0+a8t4dTg zE+F?w91pyJ?-e?5!p}jho24=Gs{tXMkfKC&ZQzZ8B^a@kwO*C1EJ2Csl+cXMrPpYu zQ^Na13?R>g?oh zhnf-x-fLU9UMF<2jOep0aCG7MT11kQ_N0NVIgx}8*rl$~hL{Ew%;dH4;pqn3FIG1P sT!p{;`QgNa^OKK=-`s@SR^GrR)otj$TkrBc9h$i=h4zy!-{H^rKd`l)qW}N^ diff --git a/app/app/asgi.py b/app/asgi.py similarity index 100% rename from app/app/asgi.py rename to app/asgi.py diff --git a/app/app/settings.py b/app/settings.py similarity index 100% rename from app/app/settings.py rename to app/settings.py diff --git a/app/app/urls.py b/app/urls.py similarity index 89% rename from app/app/urls.py rename to app/urls.py index 2229166..d7ffde2 100644 --- a/app/app/urls.py +++ b/app/urls.py @@ -17,6 +17,6 @@ from django.urls import include, path urlpatterns = [ - path('users/', include('users.urls')), - path('admin/', admin.site.urls), + path('^users/', include('app.users.urls')), + path('^admin/', admin.site.urls), ] diff --git a/app/users/__pycache__/__init__.cpython-38.pyc b/app/users/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index f63ebd380697a8fe59c52d1d19f08b62ccaf64bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128 zcmWIL<>g`kg6tE#aUl9Jh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2DN*(#g`kg6tE#aZW(`F^Gc(44TX@fuanW zjJH@5Q*tx&{4|-O_)@YG^V0M6lJoOQiZYXmKnAR2C}IXuVB(jOvsFw-7Q+|GS laZD1JEG2KczG$)s7LU?K2QF001kZCd2>$ diff --git a/app/users/__pycache__/data.cpython-38.pyc b/app/users/__pycache__/data.cpython-38.pyc deleted file mode 100644 index cc96403fc3df85297f8f01f18b2351cb82e4b254..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1466 zcmZ`(&u<$=6n^u=>$Q^@M+8f$1O#p?RaV6TArwUwv>+tpODLr)!)oQ3q}!}_w==tK zu-prd1Q!n6dw^s9rMYs-g+G83@6E=IA!4k3n%RBxzW2TNJwMvn=>x9O)#!)6B*33^ z*j)Gy?&D+0C{QeiIg1&CQ;7MOptuqjpu~#DLQ5sjxH;5f1#w3Qy7R27Ix6^;t$wyY3867s4z{7ofJeY=Wr0QHq1Q9w=!E3I%D#UYF z^;G`?;!tfN*tY8tihYpQDM2;_$v5z5PmrGzN7>jB5CuE!iKGH9JO*$8aeo z-+U2qH#GV*v$Zx_B^1xPAgwB0s7vmo)%l5`f?a~4s{Z}{?9*wjZ9V<0EYIu|J*WQC zRAEZv>PIJ1rFw)Oq5&4NK9g**^Cs;ve!rD6`TCSU!p&lMlkAc`hGURW^RvFW%Yc40 zR&T|AVo#vp-(dr>E_|{W5W+!G)(0nLQ>aLq0Asnp=y~}vZ=6RdW2u?HX86GZ_{kuW{g~}7io;hP_gV0nqR<#HMU`~_cNR@ zg~GnT^8;9|o5zsOC)drx>H1IedH|raV-LHxlX`ZPA9~b3eBGD>r>ua2Zrt;D zCC&zCdnB!m2|=Gp)N^5y6zNaE9xcR9Qr@M}T$hY<} zIHX73htpm;^%XcVW7iM?W9{tBtas*{Z}zCy+Xa&Ehqpi8mjFN5*_MEu*Rtix9NCZM6QSf~X zE+A2;Fr+=i2s^Y#sBwq(4(?L^J+F>jX04#hBL>(C+q~Y=at}$BpkO9YJOc?vlw?<6 zWW_qDV52H|$##f9Y31OGGscnS9A3UZ^m2+`uc+1{oo8N;v(ijbH}Sn?>fEQoV(rF( zS1!v>Y=`gZ_Us$jE(Lzx8@(8onJb5HisIA_)8%r=Dc!JPr@#E-wK3Dhv@}N31;+}+ zzR)5P>mIk-Z_h`Juobp{w|k465-Mu(oLZU@n_qbrau7=!(OC_SkOr=_TLEEt{x#4> z^~m)>aXi~6!||X%taS~7f}=XXK@zq@S~cBw$S7eWDzTy3m-KCd%ZAo| zFnjs**hg-XK6!fJqiivvH{y3D*=L-Ni6w%|h`hqIO#i}VnVFlkT3ih|?O6T+#{g?^ zJO?5LDlvmorbP^5(J*pdE-cCkJt_Wii-*XV?93SMtu48CJr9l#V$GxD$6<`$HHId- z%GvH3bGAzJ=EO2dmie?dSaBEjE~`J~YKytAA0JPQDVsN=Cpc5s$1#zh~G aEw^Wxj{59>$-I8qKzhY*#VJxg`kf_!V?I7cA;7{oyaOhAqU5Elyoi4=wu#vF!R#wbQch7_h?22JLdKv4!w z##G^skK(Qi_0V^4bn1K|S_@&@%6$6rqamvrnE{;ho iD2OR7PAw`13l{4YRNmsS$<0qG%}KRm1Zw&W#0&s}6eVo{ diff --git a/app/users/__pycache__/urls.cpython-38.pyc b/app/users/__pycache__/urls.cpython-38.pyc deleted file mode 100644 index d5437b8038ac2dc52204193f57d7efc0a657c4e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 514 zcmYk2O)msN5Qh6}W_HIeivw4QgNazKL=dhb65%k`ZiZ@;VZJimL+pR>4;(o9OMP{6 zb`xLK!$EiYO;2yQ5H%EymGLta;s&UdHPV$4G9roZV z%HyJcQRJ6>G^Z-u9WkU5c4qt7&OAY%#*5U*8V)Cqk~~hOB^LbF?Av~30vnW(*0Rdg zGpTykyOsBay^VpEN)L96;#v*P8?|=Sz|QQKcc!(O6;P*g%Wg-lp>$}6`;1Y%pW>Uj X>|J&cq diff --git a/app/users/__pycache__/views.cpython-38.pyc b/app/users/__pycache__/views.cpython-38.pyc deleted file mode 100644 index 9d4601025a87e2c1fe949db26ba92b0afbb34596..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4238 zcma)9OLH5?5uVvyEEXV0fwU!=c{s3QOF=4T9LIJXhfx?wso1Url?qEaY{@~Em?a2s z@zBg5q)^Z$bd}_T4n8KO19{|6$zPbOPdw+QOY(IuK>{Qz$)ct)Jv%c!-ShQhA1*GI z1w9|U_UCUFg!qP;84{qsfoA>zqJ)x}=*d|2yx5b(dzs(!W4{-~0rP%V=!J2}{U9sG zMa&7ZD318Q6qni}F8@}jf(pMBDm?Jw1#m?bfs4}cz>h27OR5aM%={ww1yuoGVSY(1 zs--VQd`(?bRs2^~uqo=xCpe#|d#>`i8T9{{n&F^tQdjxZ+ToPL+ejrgwVht-N<>wf z*`zLA@lTy}Z!@)x7VR?ocwjcrObsLzu~Z`Vl#G4lDId}U6{rIKp$|?`biqTDYArFj z<%3Kg>QlRqW_}1_MIgFT$)5>NG^Is&fwaf`#mn7x5k%+`Gum^9ngxLDz$>APd0s~C87>;5N?Y-;SI zdxOEQd9=lpHZ_m_3UipX;S(1%o1K2gHk(UWVImMAmt-VEIbI}9*QW1Vbj0aH{WEP> zz*sSXhNM#$#(U|pR&`Gk_V{!-s&A~G8IYPp@=0^D4A%LBt?o^94Bd;6Ikp6c*4}fnbHN6# zyqQpuPsK*{( zsVnOA@d&uPDAUJ=-z8yekW+xMiAdBXGtn$I6XAr+bRea$@*sB^7s9twn?l3FP|>D?hq%zLcjzk!J(5qoiKo0(@s+TFE!gmx*p`gG z$QBO}NDibb?0QfAiLaF{9!9p5&j$@x#esK3b}JJ;JCY*r3%Doq3*aj`&w*&M@v1Pg z+wXqNE<1-j@_5qaHBu_KNmo1xl5t( zUua@{=iDSS=9~pwzJg<|zJ?{P*h`H`+Ua<8bJS{~zdg#bC$)UJs*%i-e_|pI5E`4< zks046DYdQh5QtqG`Lb{MEfasetkZ;JUH@<$QnHu$?zK*$?LWrh-scS?C>iQibwC9W?utXO2 z23~v{je~vUMSc`SzNXO@+T24=@A7x&; zm1alHWlMs50gXNEV|1)|AilUw5bjC~xF5(}PX`vFCjMb?Bq%C+Lc;YQ(zEoQKBQYx)I7ogB>=jBQoJ@K4cB6Jvzb zcteg?=9T2&MW&iQQ|)NX`~~EqLw_ZniDz;fPK^>HvH`{Yr@k#re7JG|&F|Te3MjZI zoR;0S_#8zOoEi2|!K!FRJfnPSg@uN`4LNfjONx?>*<&ezajtlqas_#=607iD9e{(y zpToX6cbY>8j&+}XI?vg^!~g>PxA3QbkoO7Onk;wj==wWgX^1UG31tbtas7hDMnfvo zW_^8{Tyk%x+Z?Yv>?d1UTFWQ1=jxx3v>y|pfN&uWk}#?@;c;qgLbEXDd`Y87^8nf z=PHaM&CDXy147}MjL?mX2t~31F1tk=O_93m>+LB@&u~ns3G*25p<0;0kCB2Hdx0l z7c)4#{)f}Dyw))_SbCs!+Olya;Vjj(gWa^R*RZf&{(oReh|~lyw>*;?^KIskN+6BP z9AR@Atmu7O&f30%9&v`o39T!P)*C=^tuN#hT7M-#@8JbCu5Ti#sXrHe zU?0?zr_$d;C;tNKdY0q4Ef7p!$eZFYJQB)(>EX+vNXg`de;6H6i5E6b2%wZg3^H(E z85ExUq2s6f!?^J2=KCLhgtw(6YY#LMe6L<|es>S)ImHdAG!I@BjGf;f>}lF9|0+an z&>OPf=TU((F`JGFo9+Q-H_%Q)e@zC$`?sdiOM24AY-Os**^k5_zJjQ9JM>u>FGn`q zu}GhBS!W9c%1#0W^ygUPN;~Pk>a)2wQkwORZ=&L`&25d62cM0*Yy9DtTBGT5ax8vEz26hz=t$q7Tn=!|;hSZ( oS>(*I&P?T^_554xE?v5zN}|dDi%`czUf?fPt2e9FY9%WC2Wag`kg6tE#aUl9Jh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6wj*(# Date: Fri, 10 Jan 2020 16:01:04 +0200 Subject: [PATCH 04/14] Update README with run test info --- README.md | 14 ++++---------- app/db.sqlite3 => db.sqlite3 | 0 requirements.txt | 1 - 3 files changed, 4 insertions(+), 11 deletions(-) rename app/db.sqlite3 => db.sqlite3 (100%) diff --git a/README.md b/README.md index 49fc500..b535286 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,16 @@ One Paragraph of project description goes here - TODO ### Prerequisites Ensure that you have installed the 3rd python version. -Ensure that you have installed `virtualenv`. If you haven't, run `pip install virtualenv`. - -Ensure that you have installed [Django](https://www.djangoproject.com/). Run `python3 -m django --version` to check it. Or run `pip install Django` to install. - ### Installing -Create local virtual environment. Run `virtualenv env`. Enter created environment with running `env\Scripts\activate` on Windows or `source env/bin/activate` on Mac OS. - -After it, run `pip install -r requirements.txt` to install all necessary tools. Enter your virtual environment. +- Run `virtualenv env` +- Enter created environment with running `env\Scripts\activate` on Windows or `source env/bin/activate` on Mac OS. +- Install all necessary requirements `pip install -r requirements.txt` ## Running project Ensure that you are using your virtual environment. -Move to books folder. - Run ``` python3 manage.py runserver @@ -36,4 +30,4 @@ to start server work. ## Tests -TODO \ No newline at end of file +Run `pytest` command \ No newline at end of file diff --git a/app/db.sqlite3 b/db.sqlite3 similarity index 100% rename from app/db.sqlite3 rename to db.sqlite3 diff --git a/requirements.txt b/requirements.txt index 59c6b66..126218f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,6 @@ pyparsing==2.4.6 pytest==5.3.2 pytest-cov==2.8.1 pytest-django==3.7.0 -pytest-pythonpath==0.7.3 pytz==2019.3 six==1.13.0 sqlparse==0.3.0 From 961304efa2fc7ae3f08bdd213ab6363d9b7d192b Mon Sep 17 00:00:00 2001 From: Oleksandr Bohutskyi Date: Mon, 13 Jan 2020 13:00:28 +0200 Subject: [PATCH 05/14] Add tests and new views implementation --- .gitignore | 1 + app/urls.py | 4 +- app/users/data.py | 33 ++-- app/users/models.py | 18 +- app/users/urls.py | 2 +- app/users/views.py | 108 +++++------ setup.cfg | 2 +- tests/__pycache__/__init__.cpython-38.pyc | Bin 122 -> 132 bytes .../users/__pycache__/__init__.cpython-38.pyc | Bin 128 -> 138 bytes .../test_data.cpython-38-pytest-5.3.2.pyc | Bin 6513 -> 7256 bytes tests/users/test_data.py | 58 +++--- tests/users/test_views.py | 170 +++++++++++++++++- 12 files changed, 286 insertions(+), 110 deletions(-) diff --git a/.gitignore b/.gitignore index 917efab..6ce13ad 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /.* /coverage *.pyc +*.coverage \ No newline at end of file diff --git a/app/urls.py b/app/urls.py index d7ffde2..8cba7bc 100644 --- a/app/urls.py +++ b/app/urls.py @@ -17,6 +17,6 @@ from django.urls import include, path urlpatterns = [ - path('^users/', include('app.users.urls')), - path('^admin/', admin.site.urls), + path('users/', include('app.users.urls')), + path('admin/', admin.site.urls), ] diff --git a/app/users/data.py b/app/users/data.py index 1c4c14d..b5140c0 100644 --- a/app/users/data.py +++ b/app/users/data.py @@ -1,4 +1,3 @@ -# from .models import User from app.users.models import User registered_users = [] @@ -7,26 +6,34 @@ class UsersData: def add(self, user: User): - if user not in registered_users: + if user.username not in list(map(lambda x: x.username, registered_users)): registered_users.append(user) return True return False - def delete(self, user: User): - try: - registered_users.remove(user) - return user - except ValueError: + def delete(self, user_id: str): + temp = [x for x in registered_users if str(x.id) == user_id] + if temp: + registered_users.remove(temp[0]) + return temp[0] + else: raise Exception('User not found') - def update(self, user: User, new_password_hash: str): - try: - i = registered_users.index(user) - registered_users[i].password_hash = new_password_hash + def update(self, user_id: str, new_password_hash: str): + temp = [x for x in registered_users if str(x.id) == user_id] + if temp: + temp[0].password_hash = new_password_hash return True - except ValueError: + else: return False def get(self, **params): user_id = params.get('id') - return [x for x in registered_users if user_id is None or x.id == user_id] + return [x for x in registered_users if user_id is None or str(x.id) == user_id] + + def is_user_present(self, username: str): + temp = [x for x in registered_users if x.username == username] + if temp: + return temp[0] + else: + return False diff --git a/app/users/models.py b/app/users/models.py index 379b755..1b2a0ac 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -1,7 +1,9 @@ -from django.db import models +# from django.db import models +from datetime import datetime, timedelta import hashlib +import jwt + -# Create your models here. class User: ID = 0 @@ -14,8 +16,14 @@ def __init__(self, username, password): def obj(self): return {'id': str(self.id), 'username': self.username} - def get_hash(self, data): - return hashlib.sha256(data.encode('utf-8')).hexdigest() - def __eq__(self, other): return self.username == other.username + + @staticmethod + def create_user_token(user): + return jwt.encode({'username': user.username, 'exp': (datetime.now() + timedelta(seconds=5)).timestamp()}, + user.password_hash, algorithm='HS256').decode() + + @staticmethod + def get_hash(data): + return hashlib.sha256(data.encode('utf-8')).hexdigest() diff --git a/app/users/urls.py b/app/users/urls.py index a2012ee..8f2f238 100644 --- a/app/users/urls.py +++ b/app/users/urls.py @@ -5,13 +5,13 @@ url('^$', views.UsersView.as_view( { 'get': 'get', - 'delete': 'delete', 'post': 'post' } ), name='users_list'), url('^(?P[0-9]+)/?$', views.SingleUserView.as_view( { 'get': 'get', + 'delete': 'delete', 'put': 'update' }), name='user'), url('me/', views.UserLogin.as_view( diff --git a/app/users/views.py b/app/users/views.py index edf949f..f9798ea 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -1,84 +1,64 @@ from django.http import JsonResponse, HttpResponse -from datetime import datetime, timedelta from rest_framework.viewsets import ViewSet -import json -import jwt -import hashlib +from rest_framework.exceptions import AuthenticationFailed +from app.users.data import UsersData +from app.users.models import User -registered_users = [] - - -class User: - ID = 0 - - def __init__(self, username, password): - User.ID += 1 - self.id = User.ID - self.username = username - self.password_hash = get_hash(password) - - # def __str__(self): - # # return json.dumps({'id': str(self.id), 'username': self.username}) - - def obj(self): - return {'id': str(self.id), 'username': self.username} +users_data = UsersData() class UsersView(ViewSet): def get(self, request): - return JsonResponse({'users': [i.obj() for i in registered_users]}) - - def delete(self, request): - print('delete') - return HttpResponse() + return JsonResponse({'users': [i.obj() for i in users_data.get()]}) def post(self, request): - return self.create_user(request) - - def create_user(self, request): - body = json.loads(request.body.decode('utf-8')) - user = body['username'] - is_user_present = len(list(filter(lambda x: x.username == user, registered_users))) == 0 - if is_user_present: - registered_users.append(User(body['username'], body['password'])) - return JsonResponse({'message': 'Successfully created user'}, status=201) - else: - return JsonResponse({'message': 'User with such username already exists'}, status=409) + username = request.data.get('username') + password = request.data.get('password') + if username and password: + if users_data.add(User(username, password)): + return JsonResponse({'message': 'Successfully created user'}, status=201) + else: + return JsonResponse({'message': 'User with such username already exists'}, status=409) + return JsonResponse({'message': 'Invalid data'}, status=400) class SingleUserView(ViewSet): def get(self, request, user_id: int): - for u in registered_users: - if u.id == int(user_id): - return JsonResponse({'user': u.obj()}) + result = users_data.get(id=user_id) + if result: + return JsonResponse({'user': [x.obj() for x in result]}) return JsonResponse({'message': 'User wasn\'t found'}) - def update(self, request, user_id): - body = json.loads(request.body.decode('utf-8')) - user = [x for x in registered_users if x.username == body['username'] and x.id == int(user_id)] - if len(user) == 0: - return JsonResponse({'message': 'Unable update user'}, status=409) - user[0].password_hash = get_hash(body['password']) - return JsonResponse({'message': 'Successfully updated user'}) + def update(self, request, user_id: int): + new_password = request.data.get('password') + if new_password: + if users_data.update(user_id, User.get_hash(new_password)): + return JsonResponse({'message': 'Successfully updated user'}) + else: + return JsonResponse({'message': 'Unable update user'}, status=409) + return JsonResponse({'message': 'Invalid data'}, status=400) + + def delete(self, request, user_id): + try: + removed_user = users_data.delete(user_id) + return JsonResponse({'message': 'Removed user: ' + removed_user.username}) + except Exception as e: + return JsonResponse({'message': str(e)}, status=409) class UserLogin(ViewSet): def post(self, request): - body = json.loads(request.body.decode('utf-8')) - user = [x for x in registered_users if x.username == body['username']] - if len(user) == 0: - return JsonResponse({'message': 'User wasn\'t found'}, status=401) - elif user[0].password_hash != get_hash(body['password']): - return JsonResponse({'message': 'Password is incorrect'}, status=401) - user_token = create_user_token(user[0]) - return JsonResponse({'access_token': user_token}, status=201) - - -def create_user_token(user): - return jwt.encode({'username': user.username, 'exp': (datetime.now() + timedelta(seconds=5)).timestamp()}, - user.password_hash, algorithm='HS256').decode() - - -def get_hash(data): - return hashlib.sha256(data.encode('utf-8')).hexdigest() + username = request.data.get('username') + password = request.data.get('password') + if username and password: + user = users_data.is_user_present(username) + if user: + if User.get_hash(password) == user.password_hash: + return JsonResponse({'access_token': User.create_user_token(user)}, status=201) + else: + # return JsonResponse({'message': 'Password is incorrect'}, status=401) + raise AuthenticationFailed('Password is incorrect') + else: + return JsonResponse({'message': 'User wasn\'t found'}, status=401) + return JsonResponse({'message': 'Invalid data'}, status=400) diff --git a/setup.cfg b/setup.cfg index fd2e24a..1a45469 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,6 +2,6 @@ DJANGO_SETTINGS_MODULE=app.settings testpaths = tests/ console_output_style = progress -addopts = -s --cov=app/ +addopts = -s --cov-report=html:coverage --cov=app/ filterwarnings = ignore::DeprecationWarning diff --git a/tests/__pycache__/__init__.cpython-38.pyc b/tests/__pycache__/__init__.cpython-38.pyc index 04d467b198898ac0f77ca9294d35e00be249f650..3481cbfcc2760d9ddf76a9e28e3f2ead88a79aac 100644 GIT binary patch delta 40 tcmbg`k0!JyCiQG0qn$A`+C8@ delta 30 kcmZo+tl|#k<>g`k0@c@I6S-|TYnUCPr!l0AE=LR{#J2 diff --git a/tests/users/__pycache__/__init__.cpython-38.pyc b/tests/users/__pycache__/__init__.cpython-38.pyc index ab9be40b81ddea921581b64de4669589879a1ec4..5ac5b5910dffc09750bd1852a6408050554da34b 100644 GIT binary patch delta 41 ucmZo*>|*2&<>lpK00KuTnTgysLi)~DF(s+RB{6xalpK0D{>L!V|e|I8>djVoFkrOD0Ad0svm-2RHx# diff --git a/tests/users/__pycache__/test_data.cpython-38-pytest-5.3.2.pyc b/tests/users/__pycache__/test_data.cpython-38-pytest-5.3.2.pyc index 90154597c534ddaba91b2497ecf1c815263c6525..2b0ec60b74f051478b5eb8ad21d4c99cc705c3f7 100644 GIT binary patch literal 7256 zcmb_h-*X#R72aQ3t=6*qD{+%FY21?1h_!5X^RNmk$u;;6j>5? z*G(ei7o1^upujK;!!VwX%ybz1#sg3M0l?p|kHAabcx3X#cg~g8T6v{9fD;|xy?giW z)&0KnopY{lkB__B_0u2ybwbnrp$+_)xPAkl;1-O~gkIB1I{!8{O+(YQO{uMnH5j*okHAmLY(BD ziMZ#Kc$#}A9NS7ZtQ`%S_^pD%Fr63F@Bx5TEjE61-(U`Oj(o~)jC^lHs|@9 z&C{05OMkr*gue6zzdu;z?}fSOC)@q-+JA?OQ0wS>T3h$9`l{KnU^9FAHrTmsw#~M2 z*V(q(R`8S1?qr40w${K{>R&VVEc7{T6I`{zT*uwh?%F{KePeB7PnUx4qE$D@d3KFi zZ5tdH*>a{WnkW6HTy1PN%hHc>UQk}c<~C)N$4GBwt5ORq zb*!S+z~CS{;RPFwt(xGcy!BdRrCjssLZ;aNW9$o0axS%f7i!SwFTppy}l={{} zcs=SuS+(F8jApn-!LT$3Al=mRT0wL4*4ZQ_4?R7ccYnf+2fPclsvep^+*$_{g#L*R zHfC@Q#!)Y`(MYiI)6nVUz(ZoA6|>RlxXi{(IM&HC8=Dw6&TMR<1caO+GfU=6D`mcpg(wK22jzlQ~1?88TLQX6iQE@k48zeQhxneZ9kJz#>#y425vNsO3EWx}-L zW6TEmGQR1){@A#7t*EzNB0;)P4BQ`f8*|rKe-?@X>$AH(cTM$NDhBM*Oqrrd>a;-S z0+~y#e(}1j#Orc#=7340c$rV)M6#GgBAxJ-)OMFAfU0Zn4#6lga|r6pJl>j5yG@;uIldTy7qy#q`jlPt9^(( z5JWVpW7{BG+uG5$Gmx3gU1QsB+rdA>@lFBqV|V2z!@+PI1XQG(>`XB|A7b2emCIf5x=JqOv^vgh^|1{+=lS+Z-ir!-OYfeAgRc;vm8F1a0YjewVnR(i4bbz zVH3sZX+trK{V#p8e52c7qLBH?l*f;wad?|JrbkQB{2f+ zc^U;iDt@EUy#+SUvlzPDMR!|%8FNOuU(84f9r8GtD`c+H4G{clYeK0Pd&eT&qhuj) zFBFgWQQUo>*{kIBndQ8!o%H^>qzV$Y|?`EGVmjR%mog zp|?%o!u=ds43uR^ScDnkaDZ|77RF`UNSfsPXrXB0$bzE;#s%dTP_;NtWI_ic;AVYs z0`Fp6vdbI*`pc?r0D#)3rn7FhT3YwRqk>Sh4^kc0QdUe^#5Tw;kr_x?FR?)41zh_y zwnDhPMEL|a4N&~hsR8{?7C|>KQ9z}*rn^b^dNV~YpK$Wwo;O-XtB_F~3jnuIRB{WU zO{BNGT*eH_Xs?X1<1(fmmofMI%9wN+O)X>kW%Lmpv+V1V<&NUNe2+|8@iIB+h7?{8 zQcZb_E-Bm}l?$m3IhO#O%BqU+@IQwA8+b#kp3{a3hpG&1&5wXLWvby&cJPwn5wU8M zkP?h=SamLYwP9|}v z%@tQKl~fM&XV$=hgGDakmE)}7ex6JG8mnNh#HWKDRc-ZL?58{uaZ>)8M~*7-hpME! zfUi>g9N~*5DgmFPcugEV`t?wLozOXOq|t>o(?KgC;y<&r?rt}=>T9V6}U9uEH!`+bz-IMKsJQbhYNj^ikfNve?!BXp1< z-l6T~NHu`U$ht4px1Jk`${LP){ptZM)5c^n_gHuWSa`DHOoT&0DqTGe>0*U7w1<~w9af8}j5ACe(Kkl!QoeKH@B zImp4dMaL)7f}yN6AeV5Ag6Wu!?mGD7x*uw1`ICuTy&j#PFOEk}?}VHWGozE9s^jNX z{fzP*f3UbGaS$5i;;ty$?NdH=la@nYwNyy{t%Ls_RGK{K#D@9!ql!rn(E8K9SmeB} X_Vp{23GrW99BmpW+K!&r^X7j5;flNf literal 6513 zcmb_gOK%(36`nVTLyCIYik&pI+Bk{X1ZpHdo5rd0aMHYl){&EfFfyTaM-pX3aALW>Z(7W%l?2_wwoI0qHC}EopXo78Hw_vtstJcbMBox zckcPlch0$2Cnmgq{%h{DmnSvtAKJb@6Ze|I?rqHtH+(Q8hsaYd0eg;e8- z5^LO4Qx+4fDW;l=m}HHYYNo_AYf7nRM$EFNoNA7UqpX=oHFM%A)>Kl>F>#zVle`g6 z#}g|-7^Ry)TjIzd>dMxtp4kLy(>}a~Uw8~6(ss0-=Iasf#EyZsDKu=Yxg5K#;FjOT zR>p3p9)`EtQdBKz;HlUnb?6K1x1{-Eixjl=VbEOL(_{(C2QR$!>a_^Vy7p$feIw*% zc#WUrHb1`5xf9zVR@y1zkLz$UxP6^GOoPAYihJxm!)jWc-CKQ%dL9k2e&)TdaE9xC!SFY=NH3*w;IvLUO{@a zvZJ(kydiI1IZb^fT9$frDSNZ1Zq$p;WN*$f9F-{36w~Ufh46Mn0-pW`MPVRz^fk$&NrPh<+(PKQ8 z6_fZk>&kX9&utsgocm zpCR!z63>$OI*Ah`=1F{m#5W;o(+O5u?I^*Ee2zL#k|46lSa&Y9PjDOb&}gkQJJ9Nw z^I&4Va=^%*zJ2l#Gv}r`+}3}^3r4oaK4v%ucXnc5gIVS)FhW7-Y+oa_ubHyU3i=hx ze8$Y8SO#$_9dVOKXyBor@(8r{@sRmH@X((Az_@s^s&BtMq@jx!nPSdVL+>Y~Lt|d4 zmZ~AMve#FLAQ!Hhauye2qa_k&wzE8St{T>$JXf6@p@qn8t9>(2Eaixx8GzcK1SJfK zs-h72Jl=kQFiy)CX;DleiYU(3GB@Y)NN#XETX;k5!4G0zlkx+p{p&QL$)NIeqghG?TA$|^4OB8fu z_+1AlD&pNdm|%Xb-9=RUHuenh@&f`T0-!=C8VNcPLQf^} zQ*|MbCS8jFE|Z$quX_FY1mPDbh=!4j^dG-X*?Y}D&PX@^+p(kaq;r))u2PsP!Z!9dIe!1ws~FhyQFwbl4SVr z6*+<`_`gO^zd+Mvg60{8c(wZT*_(4&U-(XqTULsDSi8;!+qqyd^tbKC3e5ZJI0Z&(02_4RU;Qve-5H7zym0#S}Ez; zDW#4lU3guR+Br#31)|fS`naiGaXFr*Y-5;8`y2IeW91RSG)s?mqa}jd2|IQ>NPzzf zb{rcp70an9^L8XWrYtwLKP?)^a^q&nS&wqiG%f!VTQARY0;$jrY)2t;;PjAM!;+@3 z5jJ5tB23Dk(2cY<9rwF5-!h(kAj^%9-}1#p68qu(@&=pf#o# z_$_W=Yc9@Ub)%;vUYR{J{AFYeOr%IAY|MJLu}N`|U zp<_goN1#P1Qc|K>T%uWQeQ*FqAXOe(ABa;08HX3-L1{UbLs zd`JmA)j34RpEDhASI3a?_!)ABBqOGV{0cXevcQUny-oSA%7izVh*c)+q@+Bq(m9Nj%|S(sT-jTeFJaC1=~RTvZt?hZtfF+u_pwWj zDA*d7HXmWG$2IWbCCq%zgj9GngpdE!ko1~_m}lUeZoXb!c!)K}Om?_Q2Y5|Bx^7yk zfbARWTF#7LL}2SNu@!l0a%^J5zsOs!VI&)rvP*7L!8$Isme5-sTd+xO|_^ zedG^FJiIuqWT^KF4X0=%|35Z6nYS6S=|Q`Fg7>8X^#GwRJ_*&8P0gq%U8^AeDi0B2D2?zE0wM^z_^aOL6H#oN#tK zD$^eUYjn=>20ivB39^ks+}qT8;sjq_rpNZh@#hX=uc5waL>WGw3+bqZqIpF>Br9Kh z6a){}}c<+Z7i5<3$#LzU^# dy2pJ`?7pTp@O6qxA$_B(&EOEyg~iI|e*ju!2;~3( diff --git a/tests/users/test_data.py b/tests/users/test_data.py index 37df02a..083d7cb 100644 --- a/tests/users/test_data.py +++ b/tests/users/test_data.py @@ -18,27 +18,27 @@ def test_add_new_user(self, mock_registered_users): result = self.users_data.add(self.user) - assert result is True + assert True is result mock_registered_users.append.assert_called_with(self.user) @mock.patch('app.users.data.registered_users') def test_add_existing_user(self, mock_registered_users): mock_registered_users.append = mock.Mock() - mock_registered_users.__contains__ = mock.Mock(return_value=True) + mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) result = self.users_data.add(self.user) - assert result is False + assert False is result mock_registered_users.append.assert_not_called() - mock_registered_users.__contains__.assert_called_with(self.user) @mock.patch('app.users.data.registered_users') def test_delete_existing_user(self, mock_registered_users): mock_registered_users.remove = mock.Mock() + mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) - result = self.users_data.delete(self.user) + result = self.users_data.delete(str(self.user.id)) - assert result == self.user + assert self.user == result mock_registered_users.remove.assert_called_with(self.user) @mock.patch('app.users.data.registered_users') @@ -46,42 +46,38 @@ def test_delete_not_existing_user(self, mock_registered_users): mock_registered_users.remove = mock.Mock(side_effect=ValueError) with pytest.raises(Exception) as e: - self.users_data.delete(self.user) + self.users_data.delete(self.user.username) - assert str(e.value) == 'User not found' - mock_registered_users.remove.assert_called_with(self.user) + assert 'User not found' == str(e.value) @mock.patch('app.users.data.registered_users') def test_update_existing_user(self, mock_registered_users): - mock_registered_users.index = mock.Mock() + mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) - result = self.users_data.update(self.user, 'new_password_hash') + result = self.users_data.update(str(self.user.id), 'new_password_hash') - assert result is True - mock_registered_users.index.assert_called_with(self.user) + assert True is result @mock.patch('app.users.data.registered_users') def test_update_not_existing_user(self, mock_registered_users): - mock_registered_users.index = mock.Mock(side_effect=ValueError) + mock_registered_users.__iter__ = mock.Mock(return_value=iter([])) - new_user = User('new_user2', 'password2') - result = self.users_data.update(new_user, 'new_password_hash') + result = self.users_data.update(2, 'new_password_hash') - assert result is False - mock_registered_users.index.assert_called_with(new_user) + assert False is result def test_get_without_parameters_returns_registered_users(self): result = self.users_data.get() - assert result == [] + assert [] == result @mock.patch('app.users.data.registered_users') def test_get_with_valid_id_parameter(self, mock_registered_users): mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) - result = self.users_data.get(id=1) + result = self.users_data.get(id=str(1)) - assert result == [self.user] + assert [self.user] == result @mock.patch('app.users.data.registered_users') def test_get_with_not_valid_id_parameter(self, mock_registered_users): @@ -89,7 +85,7 @@ def test_get_with_not_valid_id_parameter(self, mock_registered_users): result = self.users_data.get(id=2) - assert result == [] + assert [] == result @mock.patch('app.users.data.registered_users') def test_get_with_invalid_parameter(self, mock_registered_users): @@ -97,4 +93,20 @@ def test_get_with_invalid_parameter(self, mock_registered_users): result = self.users_data.get(id_d=2) - assert result == [self.user] + assert [self.user] == result + + @mock.patch('app.users.data.registered_users') + def test_is_user_present_returns_user(self, mock_registered_users): + mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) + + result = self.users_data.is_user_present(self.user.username) + + assert self.user == result + + @mock.patch('app.users.data.registered_users') + def test_is_user_present_returns_false(self, mock_registered_users): + mock_registered_users.__iter__ = mock.Mock(return_value=iter([])) + + result = self.users_data.is_user_present(self.user.username) + + assert False is result diff --git a/tests/users/test_views.py b/tests/users/test_views.py index 5540a37..a32dd80 100644 --- a/tests/users/test_views.py +++ b/tests/users/test_views.py @@ -1,9 +1,177 @@ from django.test import Client from django.urls import reverse +from app.users.data import User +import mock +import pytest class TestUsersView: - def test_get_returns_all_users(self): + def setup_class(cls): + cls.user = User('new_user', 'password') + + def test_get_when_users_arent_created_returns_empty_list(self): resp = Client().get(reverse('users_list')) + assert {'users': []} == resp.json() + + @mock.patch('app.users.views.users_data') + def test_get_returns_all_users(self, mock_users_data): + self.user.obj = mock.Mock(return_value={'id': 1, 'username': 'user'}) + mock_users_data.get = mock.Mock(return_value=iter([self.user])) + + resp = Client().get(reverse('users_list')) + + assert {'users': [{'id': 1, 'username': 'user'}]} == resp.json() + mock_users_data.get.assert_called() + self.user.obj.assert_called() + + @mock.patch('app.users.views.users_data') + def test_post_new_user(self, mock_users_data): + mock_users_data.add = mock.Mock(return_value=True) + + resp = Client().post(reverse('users_list'), {'username': 'username', 'password': 'password'}) + + assert {'message': 'Successfully created user'} == resp.json() + assert 201 == resp.status_code + mock_users_data.add.assert_called() + + @mock.patch('app.users.views.users_data') + def test_post_existing_user(self, mock_users_data): + mock_users_data.add = mock.Mock(return_value=False) + + resp = Client().post(reverse('users_list'), {'username': 'username', 'password': 'password'}) + + assert {'message': 'User with such username already exists'} == resp.json() + assert 409 == resp.status_code + mock_users_data.add.assert_called() + + def test_post_not_valid_body_data(self): + resp = Client().post(reverse('users_list'), {'test': 'test_data'}) + + assert {'message': 'Invalid data'} == resp.json() + assert 400 == resp.status_code + + +class TestSingleUserView: + + def setup_class(cls): + cls.user = User('new_user', 'password') + + @mock.patch('app.users.views.users_data') + def test_get_valid_user_id(self, mock_users_data): + mock_users_data.get = mock.Mock(return_value=iter([self.user])) + + resp = Client().get(reverse('user', args=[1])) + + assert {'user': [{'id': str(self.user.id), 'username': self.user.username}]} == resp.json() + mock_users_data.get.assert_called_with(id='1') + + @mock.patch('app.users.views.users_data') + def test_get_not_valid_user_id(self, mock_users_data): + mock_users_data.get = mock.Mock(return_value=[]) + + resp = Client().get(reverse('user', args=[1])) + + assert {'message': 'User wasn\'t found'} == resp.json() + mock_users_data.get.assert_called_with(id='1') + + @mock.patch('app.users.views.users_data') + def test_update_existing_user(self, mock_users_data): + mock_users_data.update = mock.Mock(return_value=True) + + resp = Client().put( + reverse('user', args=[1]), + {'password': 'new_password'}, + content_type='application/json') + + assert {'message': 'Successfully updated user'} == resp.json() + mock_users_data.update.assert_called() + + @mock.patch('app.users.views.users_data') + def test_update_not_existing_user(self, mock_users_data): + mock_users_data.update = mock.Mock(return_value=False) + + resp = Client().put( + reverse('user', args=[1]), + {'password': 'new_password'}, + content_type='application/json') + + assert {'message': 'Unable update user'} == resp.json() + mock_users_data.update.assert_called() + + def test_update_invalid_data(self): + resp = Client().put( + reverse('user', args=[1]), + {'test': 'test_data'}, + content_type='application/json') + + assert {'message': 'Invalid data'} == resp.json() + + @mock.patch('app.users.views.users_data') + def test_delete_existing_user(self, mock_users_data): + mock_users_data.delete = mock.Mock(return_value=self.user) + + resp = Client().delete(reverse('user', args=[str(self.user.id)])) + + assert {'message': 'Removed user: ' + self.user.username} == resp.json() + assert 200 == resp.status_code + mock_users_data.delete.assert_called_with(str(self.user.id)) + + @mock.patch('app.users.views.users_data') + def test_delete_not_existing_user(self, mock_users_data): + mock_users_data.delete = mock.Mock(side_effect=Exception('User not found')) + + resp = Client().delete(reverse('user', args=[str(self.user.id)])) + + assert {'message': 'User not found'} == resp.json() + assert 409 == resp.status_code + mock_users_data.delete.assert_called_with(str(self.user.id)) + + +class TestUserLogin: + + def setup_class(cls): + cls.user = User('new_user', 'password') + + @mock.patch('app.users.views.User.create_user_token', return_value='test_token') + @mock.patch('app.users.views.users_data') + @mock.patch('app.users.views.User') + def test_post_valid_username_and_password(self, mock_user, users_data, mock_create_user_token): + mock_user.get_hash = mock.Mock(return_value=self.user.password_hash) + users_data.is_user_present = mock.Mock(return_value=self.user) + + resp = Client().post(reverse('login'), {'username': self.user.username, 'password': 'password'}) + + assert {'access_token': 'test_token'} == resp.json() + users_data.is_user_present.assert_called_with(self.user.username) + mock_user.get_hash.assert_called_with('password') + mock_create_user_token.assert_called() + + @mock.patch('app.users.views.users_data') + @mock.patch('app.users.views.User') + def test_post_valid_username_not_valid_password(self, mock_user, users_data): + mock_user.get_hash = mock.Mock(return_value='') + users_data.is_user_present = mock.Mock(return_value=self.user) + + resp = Client().post(reverse('login'), {'username': self.user.username, 'password': 'password'}) + + assert {'detail': 'Password is incorrect'} == resp.json() + users_data.is_user_present.assert_called_with(self.user.username) + mock_user.get_hash.assert_called_with('password') + + @mock.patch('app.users.views.users_data') + def test_post_not_valid_username(self, users_data): + users_data.is_user_present = mock.Mock(return_value=False) + + resp = Client().post(reverse('login'), {'username': self.user.username, 'password': 'password'}) + + assert {'message': 'User wasn\'t found'} == resp.json() + assert 401 == resp.status_code + users_data.is_user_present.assert_called_with(self.user.username) + + def test_post_invalid_data(self): + resp = Client().post(reverse('login'), {'test': 'test'}) + + assert {'message': 'Invalid data'} == resp.json() + assert 400 == resp.status_code From 4e0d844b9823c109403949fbbb0d86ef8279908e Mon Sep 17 00:00:00 2001 From: Oleksandr Bohutskyi Date: Mon, 13 Jan 2020 16:47:28 +0200 Subject: [PATCH 06/14] Init books model --- app/{users/migrations => books}/__init__.py | 0 app/books/data.py | 39 +++++++++++ app/books/migrations/0001_initial.py | 24 +++++++ .../migrations/0002_auto_20200113_1507.py | 18 +++++ app/books/migrations/__init__.py | 0 app/books/models.py | 8 +++ app/books/urls.py | 18 +++++ app/books/views.py | 64 ++++++++++++++++++ app/settings.py | 1 + app/urls.py | 6 ++ app/users/admin.py | 3 - app/users/apps.py | 5 -- app/users/tests.py | 4 -- app/users/urls.py | 5 +- db.sqlite3 | Bin 0 -> 135168 bytes 15 files changed, 179 insertions(+), 16 deletions(-) rename app/{users/migrations => books}/__init__.py (100%) create mode 100644 app/books/data.py create mode 100644 app/books/migrations/0001_initial.py create mode 100644 app/books/migrations/0002_auto_20200113_1507.py create mode 100644 app/books/migrations/__init__.py create mode 100644 app/books/models.py create mode 100644 app/books/urls.py create mode 100644 app/books/views.py delete mode 100644 app/users/admin.py delete mode 100644 app/users/apps.py delete mode 100644 app/users/tests.py diff --git a/app/users/migrations/__init__.py b/app/books/__init__.py similarity index 100% rename from app/users/migrations/__init__.py rename to app/books/__init__.py diff --git a/app/books/data.py b/app/books/data.py new file mode 100644 index 0000000..b5140c0 --- /dev/null +++ b/app/books/data.py @@ -0,0 +1,39 @@ +from app.users.models import User + +registered_users = [] + + +class UsersData: + + def add(self, user: User): + if user.username not in list(map(lambda x: x.username, registered_users)): + registered_users.append(user) + return True + return False + + def delete(self, user_id: str): + temp = [x for x in registered_users if str(x.id) == user_id] + if temp: + registered_users.remove(temp[0]) + return temp[0] + else: + raise Exception('User not found') + + def update(self, user_id: str, new_password_hash: str): + temp = [x for x in registered_users if str(x.id) == user_id] + if temp: + temp[0].password_hash = new_password_hash + return True + else: + return False + + def get(self, **params): + user_id = params.get('id') + return [x for x in registered_users if user_id is None or str(x.id) == user_id] + + def is_user_present(self, username: str): + temp = [x for x in registered_users if x.username == username] + if temp: + return temp[0] + else: + return False diff --git a/app/books/migrations/0001_initial.py b/app/books/migrations/0001_initial.py new file mode 100644 index 0000000..b7df47d --- /dev/null +++ b/app/books/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.1 on 2020-01-13 13:05 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('description', models.TextField()), + ('creation_date', models.DateTimeField(default=datetime.datetime.now)), + ], + ), + ] diff --git a/app/books/migrations/0002_auto_20200113_1507.py b/app/books/migrations/0002_auto_20200113_1507.py new file mode 100644 index 0000000..ccd7b43 --- /dev/null +++ b/app/books/migrations/0002_auto_20200113_1507.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.1 on 2020-01-13 13:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('books', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='book', + name='description', + field=models.TextField(blank=True), + ), + ] diff --git a/app/books/migrations/__init__.py b/app/books/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/books/models.py b/app/books/models.py new file mode 100644 index 0000000..158b766 --- /dev/null +++ b/app/books/models.py @@ -0,0 +1,8 @@ +from django.db import models +from datetime import datetime + + +class Book(models.Model): + name = models.CharField(max_length=50) + description = models.TextField(blank=True) + creation_date = models.DateTimeField(default=datetime.now) diff --git a/app/books/urls.py b/app/books/urls.py new file mode 100644 index 0000000..f846112 --- /dev/null +++ b/app/books/urls.py @@ -0,0 +1,18 @@ +from django.conf.urls import url +from . import views + +urlpatterns = [ + url('^$', views.UsersView.as_view( + { + 'get': 'get', + 'post': 'post' + } + ), name='users_list'), + url('^(?P[0-9]+)/?$', views.SingleUserView.as_view( + { + 'get': 'get', + 'delete': 'delete', + 'put': 'update' + }), name='user'), + +] diff --git a/app/books/views.py b/app/books/views.py new file mode 100644 index 0000000..f9798ea --- /dev/null +++ b/app/books/views.py @@ -0,0 +1,64 @@ +from django.http import JsonResponse, HttpResponse +from rest_framework.viewsets import ViewSet +from rest_framework.exceptions import AuthenticationFailed +from app.users.data import UsersData +from app.users.models import User + +users_data = UsersData() + + +class UsersView(ViewSet): + + def get(self, request): + return JsonResponse({'users': [i.obj() for i in users_data.get()]}) + + def post(self, request): + username = request.data.get('username') + password = request.data.get('password') + if username and password: + if users_data.add(User(username, password)): + return JsonResponse({'message': 'Successfully created user'}, status=201) + else: + return JsonResponse({'message': 'User with such username already exists'}, status=409) + return JsonResponse({'message': 'Invalid data'}, status=400) + + +class SingleUserView(ViewSet): + def get(self, request, user_id: int): + result = users_data.get(id=user_id) + if result: + return JsonResponse({'user': [x.obj() for x in result]}) + return JsonResponse({'message': 'User wasn\'t found'}) + + def update(self, request, user_id: int): + new_password = request.data.get('password') + if new_password: + if users_data.update(user_id, User.get_hash(new_password)): + return JsonResponse({'message': 'Successfully updated user'}) + else: + return JsonResponse({'message': 'Unable update user'}, status=409) + return JsonResponse({'message': 'Invalid data'}, status=400) + + def delete(self, request, user_id): + try: + removed_user = users_data.delete(user_id) + return JsonResponse({'message': 'Removed user: ' + removed_user.username}) + except Exception as e: + return JsonResponse({'message': str(e)}, status=409) + + +class UserLogin(ViewSet): + def post(self, request): + username = request.data.get('username') + password = request.data.get('password') + if username and password: + user = users_data.is_user_present(username) + if user: + if User.get_hash(password) == user.password_hash: + return JsonResponse({'access_token': User.create_user_token(user)}, status=201) + else: + # return JsonResponse({'message': 'Password is incorrect'}, status=401) + raise AuthenticationFailed('Password is incorrect') + else: + return JsonResponse({'message': 'User wasn\'t found'}, status=401) + return JsonResponse({'message': 'Invalid data'}, status=400) diff --git a/app/settings.py b/app/settings.py index 717be33..b0aa258 100644 --- a/app/settings.py +++ b/app/settings.py @@ -31,6 +31,7 @@ # Application definition INSTALLED_APPS = [ + 'app.books', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/app/urls.py b/app/urls.py index 8cba7bc..47d22da 100644 --- a/app/urls.py +++ b/app/urls.py @@ -15,8 +15,14 @@ """ from django.contrib import admin from django.urls import include, path +from django.conf.urls import url +from app.users.views import UserLogin urlpatterns = [ path('users/', include('app.users.urls')), path('admin/', admin.site.urls), + url('token/', UserLogin.as_view( + { + 'post': 'post' + }), name='login') ] diff --git a/app/users/admin.py b/app/users/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/app/users/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/app/users/apps.py b/app/users/apps.py deleted file mode 100644 index 4ce1fab..0000000 --- a/app/users/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class UsersConfig(AppConfig): - name = 'users' diff --git a/app/users/tests.py b/app/users/tests.py deleted file mode 100644 index 7c72b39..0000000 --- a/app/users/tests.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.test import TestCase - - -# Create your tests here. diff --git a/app/users/urls.py b/app/users/urls.py index 8f2f238..f846112 100644 --- a/app/users/urls.py +++ b/app/users/urls.py @@ -14,8 +14,5 @@ 'delete': 'delete', 'put': 'update' }), name='user'), - url('me/', views.UserLogin.as_view( - { - 'post': 'post' - }), name='login') + ] diff --git a/db.sqlite3 b/db.sqlite3 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f393bf7eb1fad39ddf5202f13a25c52d783c1b45 100644 GIT binary patch literal 135168 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 literal 0 HcmV?d00001 From 48c0b358d5099179121dd1989feaaeac2d9e626e Mon Sep 17 00:00:00 2001 From: Oleksandr Bohutskyi Date: Tue, 14 Jan 2020 17:12:13 +0200 Subject: [PATCH 07/14] Init tests for books --- README.md | 4 ++-- app/books/data.py | 39 --------------------------------------- app/books/models.py | 7 +++++++ app/books/urls.py | 8 ++++---- app/books/views.py | 12 ++++++++++++ app/urls.py | 1 + tests/books/__init__.py | 0 tests/books/mocks.py | 16 ++++++++++++++++ tests/books/test_books.py | 21 +++++++++++++++++++++ tests/users/test_views.py | 1 - 10 files changed, 63 insertions(+), 46 deletions(-) delete mode 100644 app/books/data.py create mode 100644 tests/books/__init__.py create mode 100644 tests/books/mocks.py create mode 100644 tests/books/test_books.py diff --git a/README.md b/README.md index b535286..90c2a49 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Ensure that you have installed the 3rd python version. ### Installing -- Run `virtualenv env` +- Run `virtualenv --python=python3 env` - Enter created environment with running `env\Scripts\activate` on Windows or `source env/bin/activate` on Mac OS. - Install all necessary requirements `pip install -r requirements.txt` @@ -19,7 +19,7 @@ Ensure that you are using your virtual environment. Run ``` -python3 manage.py runserver +python manage.py runserver ``` on MAC OS/Linux or ``` diff --git a/app/books/data.py b/app/books/data.py deleted file mode 100644 index b5140c0..0000000 --- a/app/books/data.py +++ /dev/null @@ -1,39 +0,0 @@ -from app.users.models import User - -registered_users = [] - - -class UsersData: - - def add(self, user: User): - if user.username not in list(map(lambda x: x.username, registered_users)): - registered_users.append(user) - return True - return False - - def delete(self, user_id: str): - temp = [x for x in registered_users if str(x.id) == user_id] - if temp: - registered_users.remove(temp[0]) - return temp[0] - else: - raise Exception('User not found') - - def update(self, user_id: str, new_password_hash: str): - temp = [x for x in registered_users if str(x.id) == user_id] - if temp: - temp[0].password_hash = new_password_hash - return True - else: - return False - - def get(self, **params): - user_id = params.get('id') - return [x for x in registered_users if user_id is None or str(x.id) == user_id] - - def is_user_present(self, username: str): - temp = [x for x in registered_users if x.username == username] - if temp: - return temp[0] - else: - return False diff --git a/app/books/models.py b/app/books/models.py index 158b766..e88b1c8 100644 --- a/app/books/models.py +++ b/app/books/models.py @@ -6,3 +6,10 @@ class Book(models.Model): name = models.CharField(max_length=50) description = models.TextField(blank=True) creation_date = models.DateTimeField(default=datetime.now) + + def obj(self): + return {'id': self.id, 'name': self.name, 'description': self.description} + + def __str__(self): + return self.name + diff --git a/app/books/urls.py b/app/books/urls.py index f846112..9d43763 100644 --- a/app/books/urls.py +++ b/app/books/urls.py @@ -2,17 +2,17 @@ from . import views urlpatterns = [ - url('^$', views.UsersView.as_view( + url('^$', views.BooksView.as_view( { 'get': 'get', 'post': 'post' } - ), name='users_list'), - url('^(?P[0-9]+)/?$', views.SingleUserView.as_view( + ), name='books_list'), + url('^(?P[0-9]+)/?$', views.SingleUserView.as_view( { 'get': 'get', 'delete': 'delete', 'put': 'update' - }), name='user'), + }), name='book'), ] diff --git a/app/books/views.py b/app/books/views.py index f9798ea..d498807 100644 --- a/app/books/views.py +++ b/app/books/views.py @@ -1,12 +1,24 @@ from django.http import JsonResponse, HttpResponse from rest_framework.viewsets import ViewSet from rest_framework.exceptions import AuthenticationFailed +from .models import Book from app.users.data import UsersData from app.users.models import User users_data = UsersData() +class BooksView(ViewSet): + + def get(self, request): + all_books = list(Book.objects.all()) + return JsonResponse({'books': [b.obj() for b in all_books]}) + + def post(self, request): + print('post') + return HttpResponse() + + class UsersView(ViewSet): def get(self, request): diff --git a/app/urls.py b/app/urls.py index 47d22da..5f5e1e5 100644 --- a/app/urls.py +++ b/app/urls.py @@ -20,6 +20,7 @@ urlpatterns = [ path('users/', include('app.users.urls')), + path('books/', include('app.books.urls')), path('admin/', admin.site.urls), url('token/', UserLogin.as_view( { diff --git a/tests/books/__init__.py b/tests/books/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/books/mocks.py b/tests/books/mocks.py new file mode 100644 index 0000000..771b80c --- /dev/null +++ b/tests/books/mocks.py @@ -0,0 +1,16 @@ +import mock + +from app.books.models import Book + + +class BookMock: + + def __init__(self): + self.objects = BookMock.BookMockObjects() + + class BookMockObjects: + + def __init__(self): + test_book = Book(name='test book') + test_book.obj = mock.Mock(return_value={'id': 'None', 'name': 'test book', 'description': ''}) + self.all = mock.Mock(return_value=[test_book]) diff --git a/tests/books/test_books.py b/tests/books/test_books.py new file mode 100644 index 0000000..d3ccb47 --- /dev/null +++ b/tests/books/test_books.py @@ -0,0 +1,21 @@ +from django.test import Client +from django.urls import reverse +from mock import PropertyMock, patch +from .mocks import BookMock +from app.books.models import Book + + +class TestBooksView: + + def setup_class(cls): + cls.book = Book(name='test_book', description='book for testing') + + def test_get(self): + with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: + mock_books_objects.return_value = BookMock().objects + + resp = Client().get(reverse('books_list')) + + assert {'books': [{'description': '', 'id': 'None', 'name': 'test book'}]} == resp.json() + assert 200 == resp.status_code + mock_books_objects.all.assert_called() diff --git a/tests/users/test_views.py b/tests/users/test_views.py index a32dd80..cd34f58 100644 --- a/tests/users/test_views.py +++ b/tests/users/test_views.py @@ -2,7 +2,6 @@ from django.urls import reverse from app.users.data import User import mock -import pytest class TestUsersView: From 9cb7c457b3ca88bb039001d2c5737076d1ad8ada Mon Sep 17 00:00:00 2001 From: Oleksandr Bohutskyi Date: Wed, 15 Jan 2020 19:48:19 +0200 Subject: [PATCH 08/14] Init books functionality, part 2 --- app/books/models.py | 3 +- app/books/urls.py | 3 +- app/books/views.py | 85 ++++++++++++-------------------------- db.sqlite3 | Bin 135168 -> 135168 bytes tests/books/mocks.py | 18 +++++++- tests/books/test_books.py | 68 +++++++++++++++++++++++++++--- 6 files changed, 108 insertions(+), 69 deletions(-) diff --git a/app/books/models.py b/app/books/models.py index e88b1c8..ff67f9a 100644 --- a/app/books/models.py +++ b/app/books/models.py @@ -11,5 +11,4 @@ def obj(self): return {'id': self.id, 'name': self.name, 'description': self.description} def __str__(self): - return self.name - + return 'id: ' + str(self.id) + ', name:' + str(self.name) + ', description:' + str(self.description) diff --git a/app/books/urls.py b/app/books/urls.py index 9d43763..a5b7dc5 100644 --- a/app/books/urls.py +++ b/app/books/urls.py @@ -8,11 +8,10 @@ 'post': 'post' } ), name='books_list'), - url('^(?P[0-9]+)/?$', views.SingleUserView.as_view( + url('^(?P[0-9]+)/?$', views.SingleBookView.as_view( { 'get': 'get', 'delete': 'delete', 'put': 'update' }), name='book'), - ] diff --git a/app/books/views.py b/app/books/views.py index d498807..c4ea570 100644 --- a/app/books/views.py +++ b/app/books/views.py @@ -1,11 +1,7 @@ from django.http import JsonResponse, HttpResponse from rest_framework.viewsets import ViewSet -from rest_framework.exceptions import AuthenticationFailed from .models import Book -from app.users.data import UsersData -from app.users.models import User - -users_data = UsersData() +from django.core.exceptions import ObjectDoesNotExist class BooksView(ViewSet): @@ -15,62 +11,35 @@ def get(self, request): return JsonResponse({'books': [b.obj() for b in all_books]}) def post(self, request): - print('post') - return HttpResponse() - - -class UsersView(ViewSet): - - def get(self, request): - return JsonResponse({'users': [i.obj() for i in users_data.get()]}) - - def post(self, request): - username = request.data.get('username') - password = request.data.get('password') - if username and password: - if users_data.add(User(username, password)): - return JsonResponse({'message': 'Successfully created user'}, status=201) - else: - return JsonResponse({'message': 'User with such username already exists'}, status=409) + name = request.data.get('name') + description = request.data.get('description') + if name: + new_book = Book.objects.create(name=name, description=description) + return JsonResponse({'message': 'Successfully created book, id: ' + str(new_book.id)}, status=201) return JsonResponse({'message': 'Invalid data'}, status=400) -class SingleUserView(ViewSet): - def get(self, request, user_id: int): - result = users_data.get(id=user_id) - if result: - return JsonResponse({'user': [x.obj() for x in result]}) - return JsonResponse({'message': 'User wasn\'t found'}) +class SingleBookView(ViewSet): - def update(self, request, user_id: int): - new_password = request.data.get('password') - if new_password: - if users_data.update(user_id, User.get_hash(new_password)): - return JsonResponse({'message': 'Successfully updated user'}) - else: - return JsonResponse({'message': 'Unable update user'}, status=409) - return JsonResponse({'message': 'Invalid data'}, status=400) - - def delete(self, request, user_id): + def get(self, request, book_id): try: - removed_user = users_data.delete(user_id) - return JsonResponse({'message': 'Removed user: ' + removed_user.username}) - except Exception as e: - return JsonResponse({'message': str(e)}, status=409) + book = Book.objects.get(id=book_id) + return JsonResponse({'book': book.obj()}) + except ObjectDoesNotExist: + return JsonResponse({'message': 'Book doesn\'t exist'}, status=401) - -class UserLogin(ViewSet): - def post(self, request): - username = request.data.get('username') - password = request.data.get('password') - if username and password: - user = users_data.is_user_present(username) - if user: - if User.get_hash(password) == user.password_hash: - return JsonResponse({'access_token': User.create_user_token(user)}, status=201) - else: - # return JsonResponse({'message': 'Password is incorrect'}, status=401) - raise AuthenticationFailed('Password is incorrect') - else: - return JsonResponse({'message': 'User wasn\'t found'}, status=401) - return JsonResponse({'message': 'Invalid data'}, status=400) + def delete(self, request, book_id): + try: + removed_book = Book.objects.get(id=book_id) + result = Book.objects.filter(id=book_id).delete() + if result[0] == 0: + return JsonResponse({'message': 'Unable to remove book'}, status=204) + response_message = {'message': 'removed book'} + response_message['book'] = removed_book.obj() + return JsonResponse(response_message) + except ObjectDoesNotExist: + return JsonResponse({'message': 'Book doesn\'t exist'}, status=401) + + def update(self, request): + print('update') + return HttpResponse() diff --git a/db.sqlite3 b/db.sqlite3 index f393bf7eb1fad39ddf5202f13a25c52d783c1b45..8c83d23a21f89665a3d7d25f92884aa14bc640d3 100644 GIT binary patch delta 330 zcmZozz|pXPV}dlJ%|sbzMw^WZi{wRW7})vKnfYJxU*X@)pUyvvzng#cW<`TQ{>kb3 zqAUyy4E&QD^tDC#_?Q(Llk)Ski{nA0fH*6&G2`TeJi?nV>+3o2^Ur2r;@4r|PvO_$ zKf!-}v!a3wf4vziD}$_~c3x_^0!WXMfsui(fuXLUse+-2m9epviLsukg{7&HDM&?1 zYH@N=WoHd708OB0|rLo)%KjxzZ>4W<`TQ{>kb3 zqAa|8%!-p6^tDBWITA}tGU5wTi*hrIi!<}ZS(%L)Cm-Yy-h5eK&w-nPfq{|#ECUd+ zZx&2A$Zu@O$jTt;sOprTpRHhMWME{VYhb8rXslppYGr6_Wn`geX>4p{WKxoms$f`L PqL2hs*VM4AfpG!=wO=g= diff --git a/tests/books/mocks.py b/tests/books/mocks.py index 771b80c..ee4aeb1 100644 --- a/tests/books/mocks.py +++ b/tests/books/mocks.py @@ -1,5 +1,5 @@ import mock - +from django.db.models.query import QuerySet from app.books.models import Book @@ -12,5 +12,21 @@ class BookMockObjects: def __init__(self): test_book = Book(name='test book') + test_book.id = 1 test_book.obj = mock.Mock(return_value={'id': 'None', 'name': 'test book', 'description': ''}) self.all = mock.Mock(return_value=[test_book]) + self.create = mock.Mock(return_value=test_book) + self.get = mock.Mock(return_value=test_book) + self.filter = mock.Mock(return_value=FilterMock(test_book)) + + +class FilterMock: + + def __init__(self, test_book): + self.return_value = mock.Mock(return_value=iter([test_book])) + self.delete = mock.Mock(return_value=(1, {'books.Book': 1})) + + class QueryMock(QuerySet): + + def __init__(self): + super().__init__() diff --git a/tests/books/test_books.py b/tests/books/test_books.py index d3ccb47..0c2d8ea 100644 --- a/tests/books/test_books.py +++ b/tests/books/test_books.py @@ -2,20 +2,76 @@ from django.urls import reverse from mock import PropertyMock, patch from .mocks import BookMock -from app.books.models import Book +import mock +from django.core.exceptions import ObjectDoesNotExist class TestBooksView: - def setup_class(cls): - cls.book = Book(name='test_book', description='book for testing') - def test_get(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: - mock_books_objects.return_value = BookMock().objects + book_mock = BookMock() + mock_books_objects.return_value = book_mock.objects resp = Client().get(reverse('books_list')) assert {'books': [{'description': '', 'id': 'None', 'name': 'test book'}]} == resp.json() assert 200 == resp.status_code - mock_books_objects.all.assert_called() + book_mock.objects.all.assert_called() + + def test_post(self): + with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: + book_mock = BookMock() + mock_books_objects.return_value = book_mock.objects + + resp = Client().post(reverse('books_list'), {'name': 'test name', 'description': 'test description'}) + + assert {'message': 'Successfully created book, id: 1'} == resp.json() + assert 201 == resp.status_code + book_mock.objects.create.assert_called() + + def test_post_with_invalid_data(self): + resp = Client().post(reverse('books_list'), {'test': 'test data'}) + + assert {'message': 'Invalid data'} == resp.json() + assert 400 == resp.status_code + + +class TestSingleBookView: + + def test_get_valid_book_id(self): + with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: + book_mock = BookMock() + mock_books_objects.return_value = book_mock.objects + + resp = Client().get(reverse('book', args=[1])) + + assert {'book': {'id': 'None', 'name': 'test book', 'description': ''}} == resp.json() + assert 200 == resp.status_code + book_mock.objects.get.assert_called_with(id='1') + + def test_get_with_invalid_data(self): + with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: + book_mock = BookMock() + mock_books_objects.return_value = book_mock.objects + book_mock.objects.get = mock.Mock(side_effect=ObjectDoesNotExist) + + resp = Client().get(reverse('book', args=[1])) + + assert {'message': 'Book doesn\'t exist'} == resp.json() + assert 401 == resp.status_code + book_mock.objects.get.assert_called_with(id='1') + + def test_delete(self): + with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: + book_mock = BookMock() + mock_books_objects.return_value = book_mock.objects + + resp = Client().delete(reverse('book', args=[1])) + + assert {'message': 'removed book', 'book': {'id': 'None', 'name': 'test book', 'description': ''}} \ + == resp.json() + assert 200 == resp.status_code + book_mock.objects.get.assert_called_with(id='1') + book_mock.objects.filter.assert_called_with(id='1') + book_mock.objects.filter().delete.assert_called() From 0d74d0cb7de075b07b9434cf450c5c88f276cd69 Mon Sep 17 00:00:00 2001 From: Oleksandr Bohutskyi Date: Thu, 16 Jan 2020 14:52:26 +0200 Subject: [PATCH 09/14] Complete init books endpoint --- README.md | 5 ---- app/books/views.py | 23 +++++++++++----- db.sqlite3 | Bin 135168 -> 135168 bytes tests/books/mocks.py | 7 +---- tests/books/test_books.py | 54 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 70 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 90c2a49..1534eb5 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,6 @@ Run ``` python manage.py runserver ``` -on MAC OS/Linux or -``` -python manage.py runserver -``` -on Windows to start server work. ## Tests diff --git a/app/books/views.py b/app/books/views.py index c4ea570..162f3e9 100644 --- a/app/books/views.py +++ b/app/books/views.py @@ -31,15 +31,26 @@ def get(self, request, book_id): def delete(self, request, book_id): try: removed_book = Book.objects.get(id=book_id) - result = Book.objects.filter(id=book_id).delete() - if result[0] == 0: - return JsonResponse({'message': 'Unable to remove book'}, status=204) + Book.objects.filter(id=book_id).delete() response_message = {'message': 'removed book'} response_message['book'] = removed_book.obj() return JsonResponse(response_message) except ObjectDoesNotExist: return JsonResponse({'message': 'Book doesn\'t exist'}, status=401) - def update(self, request): - print('update') - return HttpResponse() + def update(self, request, book_id): + name = request.data.get('name') + description = request.data.get('description') + if name or description: + try: + filter = Book.objects.filter(id=book_id) + if name: + updated_book = filter.update(name=name) + if description: + updated_book = filter.update(description=description) + response_message = {'message': 'Successfully updated book'} + response_message['book'] = updated_book[0].obj() + return JsonResponse(response_message) + except ObjectDoesNotExist: + return JsonResponse({'message': 'Book doesn\'t exist'}, status=401) + return JsonResponse({'message': 'Invalid data'}, status=400) diff --git a/db.sqlite3 b/db.sqlite3 index 8c83d23a21f89665a3d7d25f92884aa14bc640d3..24a66987fe61af23bd63e4668fbdfe6d52048e03 100644 GIT binary patch delta 128 zcmZozz|pXPV}dlJ>qHr6M%RrAQTl8`;;hWZjFS)Y2yecuZ|K0o%&)`1pTe)he~kb7 zW<>=V{>eJ^MU2{$=hv%SSqQN*$U5rhrIst?CFZ6Y85kMp8W`#tnkg6>Ss58ynVRTX dn3x%u8G=-#q!uR^DHNrGRHigFENft#006E=BmV#Z delta 108 zcmV-y0F(cKpa_7V2#^~AJdqqj0X(r_S}z6=7XhH#svmH85^vcOYbCb7OL8 OaCB*JZi0ZOfB}$Da37ce diff --git a/tests/books/mocks.py b/tests/books/mocks.py index ee4aeb1..0ef5998 100644 --- a/tests/books/mocks.py +++ b/tests/books/mocks.py @@ -1,5 +1,4 @@ import mock -from django.db.models.query import QuerySet from app.books.models import Book @@ -25,8 +24,4 @@ class FilterMock: def __init__(self, test_book): self.return_value = mock.Mock(return_value=iter([test_book])) self.delete = mock.Mock(return_value=(1, {'books.Book': 1})) - - class QueryMock(QuerySet): - - def __init__(self): - super().__init__() + self.update = mock.Mock(return_value=[test_book]) diff --git a/tests/books/test_books.py b/tests/books/test_books.py index 0c2d8ea..f576eb0 100644 --- a/tests/books/test_books.py +++ b/tests/books/test_books.py @@ -69,9 +69,59 @@ def test_delete(self): resp = Client().delete(reverse('book', args=[1])) - assert {'message': 'removed book', 'book': {'id': 'None', 'name': 'test book', 'description': ''}} \ - == resp.json() + assert {'message': 'removed book', 'book': {'id': 'None', 'name': 'test book', 'description': ''}} == \ + resp.json() assert 200 == resp.status_code book_mock.objects.get.assert_called_with(id='1') book_mock.objects.filter.assert_called_with(id='1') book_mock.objects.filter().delete.assert_called() + + def test_delete_book_do_not_exists(self): + with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: + book_mock = BookMock() + mock_books_objects.return_value = book_mock.objects + book_mock.objects.get = mock.Mock(side_effect=ObjectDoesNotExist) + + resp = Client().delete(reverse('book', args=[1])) + + assert {'message': 'Book doesn\'t exist'} == resp.json() + assert 401 == resp.status_code + book_mock.objects.get.assert_called_with(id='1') + + def test_update(self): + with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: + book_mock = BookMock() + mock_books_objects.return_value = book_mock.objects + + resp = Client().put( + reverse('book', args=[1]), + {'name': 'updated_name', 'description': 'updated_description'}, + content_type='application/json') + + assert {'message': 'Successfully updated book', 'book': {'id': 'None', 'name': 'test book', + 'description': ''}} == resp.json() + assert 200 == resp.status_code + book_mock.objects.filter.assert_called_with(id='1') + book_mock.objects.filter().update.assert_called() + + def test_update_book_do_not_exists(self): + with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: + book_mock = BookMock() + mock_books_objects.return_value = book_mock.objects + book_mock.objects.filter = mock.Mock(side_effect=ObjectDoesNotExist) + + resp = Client().put( + reverse('book', args=[1]), + {'name': 'updated_name'}, + content_type='application/json') + + assert {'message': "Book doesn't exist"} == resp.json() + assert 401 == resp.status_code + book_mock.objects.filter.assert_called_with(id='1') + + + def test_update_invalid_data(self): + resp = Client().put(reverse('book', args=[1])) + + assert {'message': 'Invalid data'} == resp.json() + assert 400 == resp.status_code From 9f6a51f93086b8c055b1d19187eceaf4cdcac1ca Mon Sep 17 00:00:00 2001 From: OBohutskyi Date: Thu, 16 Jan 2020 21:15:56 +0200 Subject: [PATCH 10/14] Code rafactor --- README.md | 5 ++-- app/books/views.py | 2 +- tests/books/test_books.py | 59 +++++++++++++++++++-------------------- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 1534eb5..08c9ffa 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Ensure that you have installed the 3rd python version. ### Installing -- Run `virtualenv --python=python3 env` +- Run `virtualenv --python=python3 env`. Please name your environment starting with env. - Enter created environment with running `env\Scripts\activate` on Windows or `source env/bin/activate` on Mac OS. - Install all necessary requirements `pip install -r requirements.txt` @@ -25,4 +25,5 @@ to start server work. ## Tests -Run `pytest` command \ No newline at end of file +Run `pytest` command. +You will receive general testing status in console. Command will create coverage folder with coverage of all modules. \ No newline at end of file diff --git a/app/books/views.py b/app/books/views.py index 162f3e9..58e728a 100644 --- a/app/books/views.py +++ b/app/books/views.py @@ -1,4 +1,4 @@ -from django.http import JsonResponse, HttpResponse +from django.http import JsonResponse from rest_framework.viewsets import ViewSet from .models import Book from django.core.exceptions import ObjectDoesNotExist diff --git a/tests/books/test_books.py b/tests/books/test_books.py index f576eb0..491c420 100644 --- a/tests/books/test_books.py +++ b/tests/books/test_books.py @@ -8,27 +8,28 @@ class TestBooksView: + def setup(self) -> None: + self.book_mock = BookMock() + def test_get(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: - book_mock = BookMock() - mock_books_objects.return_value = book_mock.objects + mock_books_objects.return_value = self.book_mock.objects resp = Client().get(reverse('books_list')) assert {'books': [{'description': '', 'id': 'None', 'name': 'test book'}]} == resp.json() assert 200 == resp.status_code - book_mock.objects.all.assert_called() + self.book_mock.objects.all.assert_called() def test_post(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: - book_mock = BookMock() - mock_books_objects.return_value = book_mock.objects + mock_books_objects.return_value = self.book_mock.objects resp = Client().post(reverse('books_list'), {'name': 'test name', 'description': 'test description'}) assert {'message': 'Successfully created book, id: 1'} == resp.json() assert 201 == resp.status_code - book_mock.objects.create.assert_called() + self.book_mock.objects.create.assert_called() def test_post_with_invalid_data(self): resp = Client().post(reverse('books_list'), {'test': 'test data'}) @@ -39,59 +40,57 @@ def test_post_with_invalid_data(self): class TestSingleBookView: + def setup(self) -> None: + self.book_mock = BookMock() + def test_get_valid_book_id(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: - book_mock = BookMock() - mock_books_objects.return_value = book_mock.objects + mock_books_objects.return_value = self.book_mock.objects resp = Client().get(reverse('book', args=[1])) assert {'book': {'id': 'None', 'name': 'test book', 'description': ''}} == resp.json() assert 200 == resp.status_code - book_mock.objects.get.assert_called_with(id='1') + self.book_mock.objects.get.assert_called_with(id='1') def test_get_with_invalid_data(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: - book_mock = BookMock() - mock_books_objects.return_value = book_mock.objects - book_mock.objects.get = mock.Mock(side_effect=ObjectDoesNotExist) + mock_books_objects.return_value = self.book_mock.objects + self.book_mock.objects.get = mock.Mock(side_effect=ObjectDoesNotExist) resp = Client().get(reverse('book', args=[1])) assert {'message': 'Book doesn\'t exist'} == resp.json() assert 401 == resp.status_code - book_mock.objects.get.assert_called_with(id='1') + self.book_mock.objects.get.assert_called_with(id='1') def test_delete(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: - book_mock = BookMock() - mock_books_objects.return_value = book_mock.objects + mock_books_objects.return_value = self.book_mock.objects resp = Client().delete(reverse('book', args=[1])) assert {'message': 'removed book', 'book': {'id': 'None', 'name': 'test book', 'description': ''}} == \ resp.json() assert 200 == resp.status_code - book_mock.objects.get.assert_called_with(id='1') - book_mock.objects.filter.assert_called_with(id='1') - book_mock.objects.filter().delete.assert_called() + self.book_mock.objects.get.assert_called_with(id='1') + self.book_mock.objects.filter.assert_called_with(id='1') + self.book_mock.objects.filter().delete.assert_called() def test_delete_book_do_not_exists(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: - book_mock = BookMock() - mock_books_objects.return_value = book_mock.objects - book_mock.objects.get = mock.Mock(side_effect=ObjectDoesNotExist) + mock_books_objects.return_value = self.book_mock.objects + self.book_mock.objects.get = mock.Mock(side_effect=ObjectDoesNotExist) resp = Client().delete(reverse('book', args=[1])) assert {'message': 'Book doesn\'t exist'} == resp.json() assert 401 == resp.status_code - book_mock.objects.get.assert_called_with(id='1') + self.book_mock.objects.get.assert_called_with(id='1') def test_update(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: - book_mock = BookMock() - mock_books_objects.return_value = book_mock.objects + mock_books_objects.return_value = self.book_mock.objects resp = Client().put( reverse('book', args=[1]), @@ -101,14 +100,13 @@ def test_update(self): assert {'message': 'Successfully updated book', 'book': {'id': 'None', 'name': 'test book', 'description': ''}} == resp.json() assert 200 == resp.status_code - book_mock.objects.filter.assert_called_with(id='1') - book_mock.objects.filter().update.assert_called() + self.book_mock.objects.filter.assert_called_with(id='1') + self.book_mock.objects.filter().update.assert_called() def test_update_book_do_not_exists(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: - book_mock = BookMock() - mock_books_objects.return_value = book_mock.objects - book_mock.objects.filter = mock.Mock(side_effect=ObjectDoesNotExist) + mock_books_objects.return_value = self.book_mock.objects + self.book_mock.objects.filter = mock.Mock(side_effect=ObjectDoesNotExist) resp = Client().put( reverse('book', args=[1]), @@ -117,8 +115,7 @@ def test_update_book_do_not_exists(self): assert {'message': "Book doesn't exist"} == resp.json() assert 401 == resp.status_code - book_mock.objects.filter.assert_called_with(id='1') - + self.book_mock.objects.filter.assert_called_with(id='1') def test_update_invalid_data(self): resp = Client().put(reverse('book', args=[1])) From 42a2ceb1db5d6364314d7b1ce3ee8322044425c0 Mon Sep 17 00:00:00 2001 From: Oleksandr Bohutskyi Date: Wed, 22 Jan 2020 13:16:59 +0200 Subject: [PATCH 11/14] Init users model, viiew and tests --- app/auth.py | 26 ++ app/books/migrations/0003_book_creator.py | 20 ++ .../migrations/0004_auto_20200120_1518.py | 20 ++ app/books/models.py | 4 +- app/settings.py | 2 + app/users/data.py | 39 --- app/users/migrations/0001_initial.py | 22 ++ app/users/migrations/__init__.py | 0 app/users/models.py | 24 +- app/users/views.py | 54 +++-- db.sqlite3 | Bin 135168 -> 159744 bytes db.zip | Bin 0 -> 51174 bytes .../test_data.cpython-38-pytest-5.3.2.pyc | Bin 7256 -> 7265 bytes tests/users/mocks.py | 35 +++ tests/users/test_data.py | 112 --------- tests/users/test_views.py | 223 +++++++++--------- 16 files changed, 286 insertions(+), 295 deletions(-) create mode 100644 app/auth.py create mode 100644 app/books/migrations/0003_book_creator.py create mode 100644 app/books/migrations/0004_auto_20200120_1518.py delete mode 100644 app/users/data.py create mode 100644 app/users/migrations/0001_initial.py create mode 100644 app/users/migrations/__init__.py create mode 100644 db.zip create mode 100644 tests/users/mocks.py delete mode 100644 tests/users/test_data.py diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..9c09ee2 --- /dev/null +++ b/app/auth.py @@ -0,0 +1,26 @@ +from rest_framework import authentication +from rest_framework import exceptions +import jwt + +key = 'secret' + + +class UserAuthentication(authentication.BaseAuthentication): + + def authenticate(self, request): + auth = authentication.get_authorization_header(request).split() + + if not auth or auth[0].lower() != b'bearer' or len(auth) == 1: + raise exceptions.AuthenticationFailed('Invalid token header. No credentials provided.') + elif len(auth) > 2: + raise exceptions.AuthenticationFailed('Invalid token header. Token string should not contain spaces.') + + try: + token = auth[1].decode() + decoded_token = jwt.decode(token, key, algorithm='HS256') + except jwt.ExpiredSignatureError: + raise exceptions.AuthenticationFailed('Token was expired') + except (jwt.DecodeError, UnicodeError): + raise exceptions.AuthenticationFailed('Invalid token header.') + + return decoded_token, None diff --git a/app/books/migrations/0003_book_creator.py b/app/books/migrations/0003_book_creator.py new file mode 100644 index 0000000..d378af0 --- /dev/null +++ b/app/books/migrations/0003_book_creator.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.1 on 2020-01-20 13:16 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ('books', '0002_auto_20200113_1507'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='creator', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='users.User'), + ), + ] diff --git a/app/books/migrations/0004_auto_20200120_1518.py b/app/books/migrations/0004_auto_20200120_1518.py new file mode 100644 index 0000000..6125449 --- /dev/null +++ b/app/books/migrations/0004_auto_20200120_1518.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.1 on 2020-01-20 13:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ('books', '0003_book_creator'), + ] + + operations = [ + migrations.AlterField( + model_name='book', + name='creator', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.User'), + ), + ] diff --git a/app/books/models.py b/app/books/models.py index ff67f9a..9e9ac33 100644 --- a/app/books/models.py +++ b/app/books/models.py @@ -1,14 +1,16 @@ from django.db import models from datetime import datetime +from app.users.models import User class Book(models.Model): name = models.CharField(max_length=50) description = models.TextField(blank=True) creation_date = models.DateTimeField(default=datetime.now) + creator = models.ForeignKey(User, on_delete=models.CASCADE) def obj(self): - return {'id': self.id, 'name': self.name, 'description': self.description} + return {'id': self.id, 'name': self.name, 'description': self.description, 'creator': self.creator.obj()} def __str__(self): return 'id: ' + str(self.id) + ', name:' + str(self.name) + ', description:' + str(self.description) diff --git a/app/settings.py b/app/settings.py index b0aa258..3a0ae65 100644 --- a/app/settings.py +++ b/app/settings.py @@ -32,6 +32,8 @@ INSTALLED_APPS = [ 'app.books', + 'app.users', + 'rest_framework.authtoken', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/app/users/data.py b/app/users/data.py deleted file mode 100644 index b5140c0..0000000 --- a/app/users/data.py +++ /dev/null @@ -1,39 +0,0 @@ -from app.users.models import User - -registered_users = [] - - -class UsersData: - - def add(self, user: User): - if user.username not in list(map(lambda x: x.username, registered_users)): - registered_users.append(user) - return True - return False - - def delete(self, user_id: str): - temp = [x for x in registered_users if str(x.id) == user_id] - if temp: - registered_users.remove(temp[0]) - return temp[0] - else: - raise Exception('User not found') - - def update(self, user_id: str, new_password_hash: str): - temp = [x for x in registered_users if str(x.id) == user_id] - if temp: - temp[0].password_hash = new_password_hash - return True - else: - return False - - def get(self, **params): - user_id = params.get('id') - return [x for x in registered_users if user_id is None or str(x.id) == user_id] - - def is_user_present(self, username: str): - temp = [x for x in registered_users if x.username == username] - if temp: - return temp[0] - else: - return False diff --git a/app/users/migrations/0001_initial.py b/app/users/migrations/0001_initial.py new file mode 100644 index 0000000..e99d122 --- /dev/null +++ b/app/users/migrations/0001_initial.py @@ -0,0 +1,22 @@ +# Generated by Django 3.0.1 on 2020-01-20 12:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('username', models.CharField(max_length=30)), + ('password_hash', models.TextField(max_length=64)), + ], + ), + ] diff --git a/app/users/migrations/__init__.py b/app/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/users/models.py b/app/users/models.py index 1b2a0ac..c952ec0 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -1,17 +1,14 @@ -# from django.db import models from datetime import datetime, timedelta import hashlib import jwt +from django.db import models +key = 'secret' -class User: - ID = 0 - def __init__(self, username, password): - User.ID += 1 - self.id = User.ID - self.username = username - self.password_hash = self.get_hash(password) +class User(models.Model): + username = models.CharField(max_length=30) + password_hash = models.TextField(max_length=64) def obj(self): return {'id': str(self.id), 'username': self.username} @@ -19,10 +16,13 @@ def obj(self): def __eq__(self, other): return self.username == other.username - @staticmethod - def create_user_token(user): - return jwt.encode({'username': user.username, 'exp': (datetime.now() + timedelta(seconds=5)).timestamp()}, - user.password_hash, algorithm='HS256').decode() + def __hash__(self): + return self.id + + def create_user_token(self): + return jwt.encode({'username': self.username, + 'exp': (datetime.now() + timedelta(days=1)).timestamp()}, + key, algorithm='HS256').decode() @staticmethod def get_hash(data): diff --git a/app/users/views.py b/app/users/views.py index f9798ea..210e84e 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -1,64 +1,72 @@ -from django.http import JsonResponse, HttpResponse +from django.http import JsonResponse from rest_framework.viewsets import ViewSet from rest_framework.exceptions import AuthenticationFailed -from app.users.data import UsersData from app.users.models import User - -users_data = UsersData() +from django.core.exceptions import ObjectDoesNotExist +from app.auth import UserAuthentication class UsersView(ViewSet): + # authentication_classes = (UserAuthentication,) def get(self, request): - return JsonResponse({'users': [i.obj() for i in users_data.get()]}) + all_users = list(User.objects.all()) + return JsonResponse({'users': [b.obj() for b in all_users]}) def post(self, request): username = request.data.get('username') password = request.data.get('password') if username and password: - if users_data.add(User(username, password)): - return JsonResponse({'message': 'Successfully created user'}, status=201) - else: + is_user_present = User.objects.filter(username=username) + if len(is_user_present): return JsonResponse({'message': 'User with such username already exists'}, status=409) + new_user = User.objects.create(username=username, password_hash=User.get_hash(password)) + return JsonResponse({'message': 'Successfully created user, id: ' + str(new_user.id)}, status=201) return JsonResponse({'message': 'Invalid data'}, status=400) class SingleUserView(ViewSet): + def get(self, request, user_id: int): - result = users_data.get(id=user_id) - if result: - return JsonResponse({'user': [x.obj() for x in result]}) - return JsonResponse({'message': 'User wasn\'t found'}) + try: + user = User.objects.get(id=user_id) + return JsonResponse({'user': user.obj()}) + except ObjectDoesNotExist: + return JsonResponse({'message': 'User doesn\'t exist'}, status=401) def update(self, request, user_id: int): new_password = request.data.get('password') if new_password: - if users_data.update(user_id, User.get_hash(new_password)): - return JsonResponse({'message': 'Successfully updated user'}) - else: - return JsonResponse({'message': 'Unable update user'}, status=409) + existing_user = User.objects.filter(id=user_id) + if not existing_user: + return JsonResponse({'message': 'User doesn\'t exist'}, status=401) + existing_user.update(password_hash=User.get_hash(new_password)) + return JsonResponse({'message': 'Successfully updated user'}) return JsonResponse({'message': 'Invalid data'}, status=400) def delete(self, request, user_id): try: - removed_user = users_data.delete(user_id) - return JsonResponse({'message': 'Removed user: ' + removed_user.username}) + removed_user = User.objects.get(id=user_id) + User.objects.filter(id=user_id).delete() + response_message = {'message': 'removed user'} + response_message['user'] = removed_user.obj() + return JsonResponse(response_message) except Exception as e: return JsonResponse({'message': str(e)}, status=409) class UserLogin(ViewSet): + def post(self, request): username = request.data.get('username') password = request.data.get('password') if username and password: - user = users_data.is_user_present(username) - if user: - if User.get_hash(password) == user.password_hash: + try: + user = User.objects.get(username=username) + if user.password_hash == User.get_hash(password): return JsonResponse({'access_token': User.create_user_token(user)}, status=201) else: - # return JsonResponse({'message': 'Password is incorrect'}, status=401) raise AuthenticationFailed('Password is incorrect') - else: + except ObjectDoesNotExist: return JsonResponse({'message': 'User wasn\'t found'}, status=401) return JsonResponse({'message': 'Invalid data'}, status=400) diff --git a/db.sqlite3 b/db.sqlite3 index 24a66987fe61af23bd63e4668fbdfe6d52048e03..52a94bfcfc4c933d6895085012828195f83d243c 100644 GIT binary patch delta 3048 zcmcImdu$`c9o~s|ZLfXaapEMtOPYB7N{F1~_UYYCID*UN(A|YRj-2iefteHSjV zueP~BImwqRD$tW4u&TE34O)1UKq^&LsdQ$Y z*hyM7sA?_kFPWL|_suuo%=~sQKkdDIonGs0*9n3cL{A(&06lj;HY@`DrG=-Uw?BZ| z`+bb%2@d*vfAPKIyW%_UW7hfLyL4@QKShMcGr3afe1+pUDXZU8EoB8x;5c63vb@Zz zxG}=eW(tSDG@$iKD74ajML9qqJn^ zo{Cup73-NuXTCwriDOcFR8@FY5+=GRA~K#qYR*+l=glJ0CTwZr)0`lrv%DfHJ15yB zNK2w1N}Y%x*&@L6bnKW%-5vD%9Zf zL>FM7D+;jSN)|_7$n8o2EG~P&EH5usDvPCJ0@)1d7Rb7KFxuIU@*Hy=Z#u)2O9a;U z_k{p$y?rp%yWfYBXn2Nz@59&NkKqgO4Ez{A1~0*7H2VaMhsa3SnaFX$2{c27yBcEf zWM1pi#6#UQJWarl;0F8=TEn;C8oVDa!PAK2CK&IaNg@(%TIrAkyd)9sa>U|b-9PiN zD-k$hd1kd>;HQCC1K$Wd5qLOoF^~^nAKC)7APJ&?zPMHOSA>oGv{5$*vt^7myJEXG*TEn&P^&nH-ZJnfx1|b|eaG-E-y?mLRbtT}4 z<#|gBqHUgo$KfQLK-(;2~zVi3GR zzy$LP`W^SRz{PJxRd z9xb!8?Lh*bWUd8138eP9{3E_Edw=D9&KvS~anaxEuG~ zo?QZQ3U5!vDtS`1p;d6)6F!R8kz6bq=Dn>;uvH+ki$>NgEHlJ*!~>2doS^ahE|7fb@Hyn z9h018IcKl+Uu>r}tpO{uPqu#G?H?FsV;A&FWu;U$vgh>5xfokD@2#@aGxO~9$*HNK zLr2`vatFA!9n9|n53Cjsd+;8ADbzee+wi;4 z$(;+?HHs}gZ{8PUFY0A;6bupaex+qW%&9{0)VqjM)Gt*HA( zFl67c0Brl4I=IRHcmaIZEz0ijtgjsEuaPcKlFdyf5mV3kN9K0fA}`gfqukS>rYSwhz@NlPPgbq z7S|Yd9=|+daWtMB*-@)7qEU4=F|h__d-1}UM!SLeTohpam#8Whv_g&(b)HuY z$>0n@Pism+6~we^=zKvDm9(JU?xTnnp`t449)z{M7z+xooT4d$-y!oeFn<=}+o{@i zz5fFPu-w~R6w$XOcMm^ndl-uOjsf%u)ZpVeO)lsrr{xNws!66Q=5#?&@}{Bbx-7~C zNs|T1%!_G7&Wjo+nw)CnG(nbBiIcRnjNSrUKQEjoi1j#o7~Y|ExQV`l85)&rwx}BOg5UKkU^FrlnqAW@{Ho0ZYEem73G^$Ao`mzfkg(R~EY^+nZzbZdKY2{x#$%xr7mSq`jB8y7Dhy-;)@mZU@y^w)Ng_KJNyoZb2#VwIcIp)I(*wQ zUXU9ign0PhdJSn`C4{EhZlOi7@uVVqHPOCb_qaDD-COY4><}?d@jEW!G{*5dj^RT+ zx2+kA6@hvoa=yQ_wO8#+bfr>VJxLdzfYxYF&zV$f#%p7KPhJl7z;?>v%%YW`&15Se zYKwlaQ;~R+h(iQd@CClXNB9fVIK)LdP#ZdWi31E0+`wg==LnNHj)T0WqBhugnYwIC zn%Gpsac|o6rEsysZo?adqK{w6zV`{3s&)ugc zBMCM3c-Y)TT^QEHh0Lfz)Wa+H+V)8134+BrAFLmRS=&JN6(KL{lzBq_y=A6NV_z-Y z0k#|hQGW|_rm2N8lQCNlRbYZu{f}5i&3w7{TB$rIS$RZ8N9>Bo>S|%!7&{3U6hA#i z@<tq8Skd}11lr?t%V|(N9OgK~{ zI2%i}m+}rK#KB$Ea8Pu5JTMyWk3`R^+c)2iEX>~y-Asx?5&Ic|e7R&KH}Jn{B%zhd zJ6W(3&N(=!quC3jD=Ab98EnFq<4`43R0__}Zmw5o%y&XiH0J8yorxVe0n9G2@@#!lt+o?-lW8wm2E_zos*1o{5z(1_=y!!wE diff --git a/db.zip b/db.zip new file mode 100644 index 0000000000000000000000000000000000000000..05d9e40ac9a2aace8ec3aed41eb00c9e07c30928 GIT binary patch literal 51174 zcmbrlb95)lwk;gnw$-t1+qP}nwrzFPNyoPBbZpy6hhO%6=iI%I?z?Y%zcDJwA5}?Z zty#0?Ldi=3gP;KX@kx-Rk^iqB{`Lj_>$`!yJ)NGOg{_6No*u2ehy33rLjcf7(a7(0 zNj2zvO$z)P|ejb2>RgQzz_gP>{|vq!h1R( zEIm*yFaml#KmjU;zHy;l2&Cd=_z`

Y}KnMr`0P(-sp@ECD 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¬G}B<#1x%L)eX$7vyL7x99?*1V`X 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

w9fQW*gKs*YZ73k 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 literal 0 HcmV?d00001 diff --git a/tests/users/__pycache__/test_data.cpython-38-pytest-5.3.2.pyc b/tests/users/__pycache__/test_data.cpython-38-pytest-5.3.2.pyc index 2b0ec60b74f051478b5eb8ad21d4c99cc705c3f7..6a5e2cd2b5bfe4be0705a5c39a9b83396ade28d5 100644 GIT binary patch delta 432 zcmY+;J5Rz;6bJC0S`FAzf!ffNMS~ifFbP9_z~GAr5k$ZTDvhxnOpKCN!k~j6Kp8yW zz{MHv`6@2;r?>-zvpq^%y%Z>Jlt-C&E)2pq?)_{WR- zTyi$pyghWsUvn1NT!k})Vm)x~w5^usxcxx$*z?*Ku5ExT`t_{G7X;|DuaTSpXvk)>1KTJCNhgnRb}_` z3_uB=#~ZkniKXGFsjWddrrDX+0-B?aMadfEF|6T^f$Db)Oo z7O|#U1%=8Cs0*jIW&ci#@opWvipB!55iM~U4eCI;~rbm->37K!2ge@AK)tx#4Og(k(KB^_R!+g)$TnLS+4OX84>7 z51w+RT#5!fQ5JT9?v!G89@?;ZQ*X@W6EyLtp!d8&WIf4DXmqtZJ@gvh-(DP>vJ*Q1CTK4{spk9= xrNTDmQqWRV2Rp^;~!*eRCfRX diff --git a/tests/users/mocks.py b/tests/users/mocks.py new file mode 100644 index 0000000..acf1242 --- /dev/null +++ b/tests/users/mocks.py @@ -0,0 +1,35 @@ +import mock +from app.users.models import User + + +class UserMock: + + def __init__(self): + self.objects = UserMock.UserMockObjects() + # self.get_hash = mock.Mock(return_value='test_hash') + + class UserMockObjects: + + def __init__(self): + test_user = User(username='test', password_hash='test_hash') + self.all = mock.Mock(return_value=[test_user]) + self.create = mock.Mock(return_value=test_user) + self.get = mock.Mock(return_value=test_user) + self.filter = mock.Mock(return_value=NonEmptyFilterMock(test_user)) + + +class NonEmptyFilterMock: + + def __init__(self, test_user): + self.return_value = mock.Mock(return_value=iter([test_user])) + self.delete = mock.Mock(return_value=(1, {'users.User': 1})) + self.update = mock.Mock(return_value=[test_user]) + + def __len__(self): + return 1 + + +class EmptyFilterMock: + + def __len__(self): + return 0 diff --git a/tests/users/test_data.py b/tests/users/test_data.py deleted file mode 100644 index 083d7cb..0000000 --- a/tests/users/test_data.py +++ /dev/null @@ -1,112 +0,0 @@ -import pytest -import mock -from app.users.data import UsersData -from app.users.models import User - - -class TestUsersData: - - def setup(self) -> None: - self.users_data = UsersData() - - def setup_class(cls): - cls.user = User('new_user', 'password') - - @mock.patch('app.users.data.registered_users') - def test_add_new_user(self, mock_registered_users): - mock_registered_users.append = mock.Mock() - - result = self.users_data.add(self.user) - - assert True is result - mock_registered_users.append.assert_called_with(self.user) - - @mock.patch('app.users.data.registered_users') - def test_add_existing_user(self, mock_registered_users): - mock_registered_users.append = mock.Mock() - mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) - - result = self.users_data.add(self.user) - - assert False is result - mock_registered_users.append.assert_not_called() - - @mock.patch('app.users.data.registered_users') - def test_delete_existing_user(self, mock_registered_users): - mock_registered_users.remove = mock.Mock() - mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) - - result = self.users_data.delete(str(self.user.id)) - - assert self.user == result - mock_registered_users.remove.assert_called_with(self.user) - - @mock.patch('app.users.data.registered_users') - def test_delete_not_existing_user(self, mock_registered_users): - mock_registered_users.remove = mock.Mock(side_effect=ValueError) - - with pytest.raises(Exception) as e: - self.users_data.delete(self.user.username) - - assert 'User not found' == str(e.value) - - @mock.patch('app.users.data.registered_users') - def test_update_existing_user(self, mock_registered_users): - mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) - - result = self.users_data.update(str(self.user.id), 'new_password_hash') - - assert True is result - - @mock.patch('app.users.data.registered_users') - def test_update_not_existing_user(self, mock_registered_users): - mock_registered_users.__iter__ = mock.Mock(return_value=iter([])) - - result = self.users_data.update(2, 'new_password_hash') - - assert False is result - - def test_get_without_parameters_returns_registered_users(self): - result = self.users_data.get() - - assert [] == result - - @mock.patch('app.users.data.registered_users') - def test_get_with_valid_id_parameter(self, mock_registered_users): - mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) - - result = self.users_data.get(id=str(1)) - - assert [self.user] == result - - @mock.patch('app.users.data.registered_users') - def test_get_with_not_valid_id_parameter(self, mock_registered_users): - mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) - - result = self.users_data.get(id=2) - - assert [] == result - - @mock.patch('app.users.data.registered_users') - def test_get_with_invalid_parameter(self, mock_registered_users): - mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) - - result = self.users_data.get(id_d=2) - - assert [self.user] == result - - @mock.patch('app.users.data.registered_users') - def test_is_user_present_returns_user(self, mock_registered_users): - mock_registered_users.__iter__ = mock.Mock(return_value=iter([self.user])) - - result = self.users_data.is_user_present(self.user.username) - - assert self.user == result - - @mock.patch('app.users.data.registered_users') - def test_is_user_present_returns_false(self, mock_registered_users): - mock_registered_users.__iter__ = mock.Mock(return_value=iter([])) - - result = self.users_data.is_user_present(self.user.username) - - assert False is result diff --git a/tests/users/test_views.py b/tests/users/test_views.py index cd34f58..250981b 100644 --- a/tests/users/test_views.py +++ b/tests/users/test_views.py @@ -1,52 +1,49 @@ from django.test import Client from django.urls import reverse -from app.users.data import User +from mock import PropertyMock, patch +from .mocks import UserMock, EmptyFilterMock +from app.users.models import User import mock +from django.core.exceptions import ObjectDoesNotExist class TestUsersView: - def setup_class(cls): - cls.user = User('new_user', 'password') + def setup(self) -> None: + self.user_mock = UserMock() def test_get_when_users_arent_created_returns_empty_list(self): - resp = Client().get(reverse('users_list')) + with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: + mock_users_objects.return_value = self.user_mock.objects - assert {'users': []} == resp.json() + resp = Client().get(reverse('users_list')) - @mock.patch('app.users.views.users_data') - def test_get_returns_all_users(self, mock_users_data): - self.user.obj = mock.Mock(return_value={'id': 1, 'username': 'user'}) - mock_users_data.get = mock.Mock(return_value=iter([self.user])) + assert {'users': [{'id': 'None', 'username': 'test'}]} == resp.json() + assert 200 == resp.status_code + self.user_mock.objects.all.assert_called() - resp = Client().get(reverse('users_list')) + def test_post_new_user(self): + with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: + mock_users_objects.return_value = self.user_mock.objects + self.user_mock.objects.filter = mock.Mock(return_value=EmptyFilterMock()) - assert {'users': [{'id': 1, 'username': 'user'}]} == resp.json() - mock_users_data.get.assert_called() - self.user.obj.assert_called() + resp = Client().post(reverse('users_list'), {'username': 'test username', 'password': 'test password'}) - @mock.patch('app.users.views.users_data') - def test_post_new_user(self, mock_users_data): - mock_users_data.add = mock.Mock(return_value=True) + assert {'message': 'Successfully created user, id: None'} == resp.json() + assert 201 == resp.status_code + self.user_mock.objects.create.assert_called() - resp = Client().post(reverse('users_list'), {'username': 'username', 'password': 'password'}) + def test_post_existing_user(self): + with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: + mock_users_objects.return_value = self.user_mock.objects - assert {'message': 'Successfully created user'} == resp.json() - assert 201 == resp.status_code - mock_users_data.add.assert_called() + resp = Client().post(reverse('users_list'), {'username': 'test username', 'password': 'test password'}) - @mock.patch('app.users.views.users_data') - def test_post_existing_user(self, mock_users_data): - mock_users_data.add = mock.Mock(return_value=False) + assert {'message': 'User with such username already exists'} == resp.json() + assert 409 == resp.status_code - resp = Client().post(reverse('users_list'), {'username': 'username', 'password': 'password'}) - - assert {'message': 'User with such username already exists'} == resp.json() - assert 409 == resp.status_code - mock_users_data.add.assert_called() - - def test_post_not_valid_body_data(self): - resp = Client().post(reverse('users_list'), {'test': 'test_data'}) + def test_post_with_invalid_data(self): + resp = Client().post(reverse('users_list'), {'test': 'test data'}) assert {'message': 'Invalid data'} == resp.json() assert 400 == resp.status_code @@ -54,120 +51,130 @@ def test_post_not_valid_body_data(self): class TestSingleUserView: - def setup_class(cls): - cls.user = User('new_user', 'password') + def setup(self) -> None: + self.user_mock = UserMock() - @mock.patch('app.users.views.users_data') - def test_get_valid_user_id(self, mock_users_data): - mock_users_data.get = mock.Mock(return_value=iter([self.user])) + def test_get_valid_user_id(self): + with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: + mock_users_objects.return_value = self.user_mock.objects - resp = Client().get(reverse('user', args=[1])) + resp = Client().get(reverse('user', args=[1])) - assert {'user': [{'id': str(self.user.id), 'username': self.user.username}]} == resp.json() - mock_users_data.get.assert_called_with(id='1') + assert {'user': {'id': 'None', 'username': 'test'}} == resp.json() + assert 200 == resp.status_code + self.user_mock.objects.get.assert_called_with(id='1') - @mock.patch('app.users.views.users_data') - def test_get_not_valid_user_id(self, mock_users_data): - mock_users_data.get = mock.Mock(return_value=[]) + def test_get_not_valid_user_id(self): + with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: + mock_users_objects.return_value = self.user_mock.objects + self.user_mock.objects.get = mock.Mock(side_effect=ObjectDoesNotExist) - resp = Client().get(reverse('user', args=[1])) + resp = Client().get(reverse('user', args=[1])) - assert {'message': 'User wasn\'t found'} == resp.json() - mock_users_data.get.assert_called_with(id='1') + assert {'message': "User doesn't exist"} == resp.json() + assert 401 == resp.status_code + self.user_mock.objects.get.assert_called_with(id='1') - @mock.patch('app.users.views.users_data') - def test_update_existing_user(self, mock_users_data): - mock_users_data.update = mock.Mock(return_value=True) + def test_update_existing_user(self): + with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: + mock_users_objects.return_value = self.user_mock.objects - resp = Client().put( - reverse('user', args=[1]), - {'password': 'new_password'}, - content_type='application/json') + resp = Client().put( + reverse('user', args=[1]), + {'password': 'updated_password'}, + content_type='application/json') - assert {'message': 'Successfully updated user'} == resp.json() - mock_users_data.update.assert_called() + assert {'message': 'Successfully updated user'} == resp.json() + assert 200 == resp.status_code + self.user_mock.objects.filter.assert_called_with(id='1') + self.user_mock.objects.filter().update.assert_called() - @mock.patch('app.users.views.users_data') - def test_update_not_existing_user(self, mock_users_data): - mock_users_data.update = mock.Mock(return_value=False) + def test_update_not_existing_user(self): + with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: + mock_users_objects.return_value = self.user_mock.objects + self.user_mock.objects.filter = mock.Mock(return_value=EmptyFilterMock()) - resp = Client().put( - reverse('user', args=[1]), - {'password': 'new_password'}, - content_type='application/json') + resp = Client().put( + reverse('user', args=[1]), + {'password': 'updated_password'}, + content_type='application/json') - assert {'message': 'Unable update user'} == resp.json() - mock_users_data.update.assert_called() + assert {'message': "User doesn't exist"} == resp.json() + assert 401 == resp.status_code + self.user_mock.objects.filter.assert_called_with(id='1') def test_update_invalid_data(self): resp = Client().put( - reverse('user', args=[1]), - {'test': 'test_data'}, - content_type='application/json') + reverse('user', args=[1]), + {'test': 'test_data'}, + content_type='application/json') assert {'message': 'Invalid data'} == resp.json() - @mock.patch('app.users.views.users_data') - def test_delete_existing_user(self, mock_users_data): - mock_users_data.delete = mock.Mock(return_value=self.user) + def test_delete_existing_user(self): + with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: + mock_users_objects.return_value = self.user_mock.objects - resp = Client().delete(reverse('user', args=[str(self.user.id)])) + resp = Client().delete(reverse('user', args=[1])) - assert {'message': 'Removed user: ' + self.user.username} == resp.json() - assert 200 == resp.status_code - mock_users_data.delete.assert_called_with(str(self.user.id)) + assert {'message': 'removed user', 'user': {'id': 'None', 'username': 'test'}} == resp.json() + assert 200 == resp.status_code + self.user_mock.objects.get.assert_called_with(id='1') + self.user_mock.objects.filter.assert_called_with(id='1') - @mock.patch('app.users.views.users_data') - def test_delete_not_existing_user(self, mock_users_data): - mock_users_data.delete = mock.Mock(side_effect=Exception('User not found')) + def test_delete_not_existing_user(self): + with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: + mock_users_objects.return_value = self.user_mock.objects + self.user_mock.objects.get = mock.Mock(side_effect=ObjectDoesNotExist('Test Exception')) - resp = Client().delete(reverse('user', args=[str(self.user.id)])) + resp = Client().delete(reverse('user', args=[1])) - assert {'message': 'User not found'} == resp.json() - assert 409 == resp.status_code - mock_users_data.delete.assert_called_with(str(self.user.id)) + assert {'message': 'Test Exception'} == resp.json() + assert 409 == resp.status_code + self.user_mock.objects.get.assert_called_with(id='1') class TestUserLogin: - def setup_class(cls): - cls.user = User('new_user', 'password') + def setup(self) -> None: + self.user_mock = UserMock() + @mock.patch('app.users.views.User.get_hash', return_value='test_hash') @mock.patch('app.users.views.User.create_user_token', return_value='test_token') - @mock.patch('app.users.views.users_data') - @mock.patch('app.users.views.User') - def test_post_valid_username_and_password(self, mock_user, users_data, mock_create_user_token): - mock_user.get_hash = mock.Mock(return_value=self.user.password_hash) - users_data.is_user_present = mock.Mock(return_value=self.user) + def test_post_valid_username_and_password(self, mock_create_user_token, mock_get_hash): + with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: + mock_users_objects.return_value = self.user_mock.objects - resp = Client().post(reverse('login'), {'username': self.user.username, 'password': 'password'}) + resp = Client().post(reverse('login'), {'username': 'username', 'password': 'password'}) - assert {'access_token': 'test_token'} == resp.json() - users_data.is_user_present.assert_called_with(self.user.username) - mock_user.get_hash.assert_called_with('password') - mock_create_user_token.assert_called() + assert {'access_token': 'test_token'} == resp.json() + assert 201 == resp.status_code + self.user_mock.objects.get.assert_called_with(username='username') + mock_create_user_token.assert_called_with(self.user_mock.objects.get.return_value) + mock_get_hash.assert_called_with('password') - @mock.patch('app.users.views.users_data') - @mock.patch('app.users.views.User') - def test_post_valid_username_not_valid_password(self, mock_user, users_data): - mock_user.get_hash = mock.Mock(return_value='') - users_data.is_user_present = mock.Mock(return_value=self.user) + @mock.patch('app.users.views.User.get_hash', return_value='test_incorrect_hash') + def test_post_valid_username_not_valid_password(self, mock_get_hash): + with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: + mock_users_objects.return_value = self.user_mock.objects - resp = Client().post(reverse('login'), {'username': self.user.username, 'password': 'password'}) + resp = Client().post(reverse('login'), {'username': 'username', 'password': 'password'}) - assert {'detail': 'Password is incorrect'} == resp.json() - users_data.is_user_present.assert_called_with(self.user.username) - mock_user.get_hash.assert_called_with('password') + assert {'detail': 'Password is incorrect'} == resp.json() + assert 403 == resp.status_code + self.user_mock.objects.get.assert_called_with(username='username') + mock_get_hash.assert_called_with('password') - @mock.patch('app.users.views.users_data') - def test_post_not_valid_username(self, users_data): - users_data.is_user_present = mock.Mock(return_value=False) + def test_post_not_valid_username(self): + with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: + mock_users_objects.return_value = self.user_mock.objects + self.user_mock.objects.get = mock.Mock(side_effect=ObjectDoesNotExist('Test Exception')) - resp = Client().post(reverse('login'), {'username': self.user.username, 'password': 'password'}) + resp = Client().post(reverse('login'), {'username': 'username', 'password': 'password'}) - assert {'message': 'User wasn\'t found'} == resp.json() - assert 401 == resp.status_code - users_data.is_user_present.assert_called_with(self.user.username) + assert {'message': "User wasn't found"} == resp.json() + assert 401 == resp.status_code + self.user_mock.objects.get.assert_called_with(username='username') def test_post_invalid_data(self): resp = Client().post(reverse('login'), {'test': 'test'}) From e783611094da42d49ba92e006fa75a2affcaa3bf Mon Sep 17 00:00:00 2001 From: Oleksandr Bohutskyi Date: Thu, 23 Jan 2020 16:55:01 +0200 Subject: [PATCH 12/14] Init authentication --- app/books/views.py | 25 ++++++++++++------- app/users/views.py | 4 ++- db.sqlite3 | Bin 159744 -> 159744 bytes db.zip | Bin 51174 -> 0 bytes tests/books/mocks.py | 4 +++ tests/books/test_books.py | 3 ++- tests/users/{test_views.py => test_users.py} | 1 - 7 files changed, 25 insertions(+), 12 deletions(-) delete mode 100644 db.zip rename tests/users/{test_views.py => test_users.py} (99%) diff --git a/app/books/views.py b/app/books/views.py index 58e728a..81156e8 100644 --- a/app/books/views.py +++ b/app/books/views.py @@ -2,9 +2,11 @@ from rest_framework.viewsets import ViewSet from .models import Book from django.core.exceptions import ObjectDoesNotExist +from app.auth import UserAuthentication class BooksView(ViewSet): + authentication_classes = (UserAuthentication,) def get(self, request): all_books = list(Book.objects.all()) @@ -13,13 +15,15 @@ def get(self, request): def post(self, request): name = request.data.get('name') description = request.data.get('description') - if name: - new_book = Book.objects.create(name=name, description=description) + creator_id = request.data.get('creator') + if name and creator_id: + new_book = Book.objects.create(name=name, description=description, creator_id=creator_id) return JsonResponse({'message': 'Successfully created book, id: ' + str(new_book.id)}, status=201) return JsonResponse({'message': 'Invalid data'}, status=400) class SingleBookView(ViewSet): + authentication_classes = (UserAuthentication,) def get(self, request, book_id): try: @@ -41,16 +45,19 @@ def delete(self, request, book_id): def update(self, request, book_id): name = request.data.get('name') description = request.data.get('description') - if name or description: + creator_id = request.data.get('creator') + if name or description or creator_id: try: - filter = Book.objects.filter(id=book_id) + existing_book = Book.objects.filter(id=book_id) + if not existing_book: + return JsonResponse({'message': 'Book doesn\'t exist'}, status=401) if name: - updated_book = filter.update(name=name) + existing_book.update(name=name) if description: - updated_book = filter.update(description=description) - response_message = {'message': 'Successfully updated book'} - response_message['book'] = updated_book[0].obj() - return JsonResponse(response_message) + existing_book.update(description=description) + if creator_id: + existing_book.update(creator=creator_id) + return JsonResponse({'message': 'Successfully updated book', 'book': existing_book[0].obj()}) except ObjectDoesNotExist: return JsonResponse({'message': 'Book doesn\'t exist'}, status=401) return JsonResponse({'message': 'Invalid data'}, status=400) diff --git a/app/users/views.py b/app/users/views.py index 210e84e..5624ac9 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -7,7 +7,7 @@ class UsersView(ViewSet): - # authentication_classes = (UserAuthentication,) + authentication_classes = (UserAuthentication,) def get(self, request): all_users = list(User.objects.all()) @@ -26,6 +26,7 @@ def post(self, request): class SingleUserView(ViewSet): + authentication_classes = (UserAuthentication,) def get(self, request, user_id: int): try: @@ -56,6 +57,7 @@ def delete(self, request, user_id): class UserLogin(ViewSet): + authentication_classes = (UserAuthentication,) def post(self, request): username = request.data.get('username') diff --git a/db.sqlite3 b/db.sqlite3 index 52a94bfcfc4c933d6895085012828195f83d243c..26b2e7db150a933b59e6e4e9eee5d85a2d478c3f 100644 GIT binary patch delta 242 zcmZp8z}fJCbAmKu?nD`9#@vkwQTlA6;;hWZjFS)Y2yecuujjze|Av8;{{aL4Z~kxm zANe0_7AUyDFTl#o$jB@VA~{7F7#LWaKh$skP|ql|K|q>;m2VLP{|WwnzB~N>e2X?K zDoo(xun=Wqkkxi%oIYKjN#4T9Si#W1%D}+N$V|_`#N5opoYPi_jX}}akuxu~Tp=$p zH&ww1#7apmPA*a?N(G6eD40xVv}clH0orFWUEZEaU(Mc57_5sCzb-4FE=K0<{q{_S F1^^{tKzjfH delta 161 zcmV;S0ABxq;0b`>36L8BVUZj|0b#LVS}z6^7X;Mn@ z5B3l75A3rLaL^ABF)}(bGdchO00sgS1Oo{HgYbX1@P7dkun<%L1P$`!yJ)NGOg{_6No*u2ehy33rLjcf7(a7(0 zNj2zvO$z)P|ejb2>RgQzz_gP>{|vq!h1R( zEIm*yFaml#KmjU;zHy;l2&Cd=_z`

Y}KnMr`0P(-sp@ECD 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¬G}B<#1x%L)eX$7vyL7x99?*1V`X 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

w9fQW*gKs*YZ73k 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 diff --git a/tests/books/mocks.py b/tests/books/mocks.py index 0ef5998..c60893a 100644 --- a/tests/books/mocks.py +++ b/tests/books/mocks.py @@ -22,6 +22,10 @@ def __init__(self): class FilterMock: def __init__(self, test_book): + self.test_book = test_book self.return_value = mock.Mock(return_value=iter([test_book])) self.delete = mock.Mock(return_value=(1, {'books.Book': 1})) self.update = mock.Mock(return_value=[test_book]) + + def __getitem__(self, indices): + return self.test_book diff --git a/tests/books/test_books.py b/tests/books/test_books.py index 491c420..7990240 100644 --- a/tests/books/test_books.py +++ b/tests/books/test_books.py @@ -25,7 +25,8 @@ def test_post(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: mock_books_objects.return_value = self.book_mock.objects - resp = Client().post(reverse('books_list'), {'name': 'test name', 'description': 'test description'}) + resp = Client().post(reverse('books_list'), {'name': 'test name', 'description': 'test description', + 'creator': 'test creator'}) assert {'message': 'Successfully created book, id: 1'} == resp.json() assert 201 == resp.status_code diff --git a/tests/users/test_views.py b/tests/users/test_users.py similarity index 99% rename from tests/users/test_views.py rename to tests/users/test_users.py index 250981b..3dc9e30 100644 --- a/tests/users/test_views.py +++ b/tests/users/test_users.py @@ -2,7 +2,6 @@ from django.urls import reverse from mock import PropertyMock, patch from .mocks import UserMock, EmptyFilterMock -from app.users.models import User import mock from django.core.exceptions import ObjectDoesNotExist From 41961d6e77e3540c31eb7f718dbcec04e3569248 Mon Sep 17 00:00:00 2001 From: Oleksandr Bohutskyi Date: Fri, 24 Jan 2020 17:09:50 +0200 Subject: [PATCH 13/14] Init auth, part 2 --- app/auth.py | 8 ++++++++ app/books/models.py | 2 +- app/books/permissions.py | 13 +++++++++++++ app/books/views.py | 8 ++++---- app/users/models.py | 5 +++++ app/users/views.py | 5 ++--- db.sqlite3 | Bin 159744 -> 159744 bytes tests/test_auth.py | 0 8 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 app/books/permissions.py create mode 100644 tests/test_auth.py diff --git a/app/auth.py b/app/auth.py index 9c09ee2..5ffc421 100644 --- a/app/auth.py +++ b/app/auth.py @@ -18,9 +18,17 @@ def authenticate(self, request): try: token = auth[1].decode() decoded_token = jwt.decode(token, key, algorithm='HS256') + request.META['HTTP_CUSTOM_HEADER'] = decoded_token['books_ids'] except jwt.ExpiredSignatureError: raise exceptions.AuthenticationFailed('Token was expired') except (jwt.DecodeError, UnicodeError): raise exceptions.AuthenticationFailed('Invalid token header.') return decoded_token, None + + +class UsersViewAuthentication(UserAuthentication): + + def authenticate(self, request): + if request.method == 'GET': + return super().authenticate(request) diff --git a/app/books/models.py b/app/books/models.py index 9e9ac33..c420c4c 100644 --- a/app/books/models.py +++ b/app/books/models.py @@ -7,7 +7,7 @@ class Book(models.Model): name = models.CharField(max_length=50) description = models.TextField(blank=True) creation_date = models.DateTimeField(default=datetime.now) - creator = models.ForeignKey(User, on_delete=models.CASCADE) + creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name='books') def obj(self): return {'id': self.id, 'name': self.name, 'description': self.description, 'creator': self.creator.obj()} diff --git a/app/books/permissions.py b/app/books/permissions.py new file mode 100644 index 0000000..5d9a95d --- /dev/null +++ b/app/books/permissions.py @@ -0,0 +1,13 @@ +from rest_framework.exceptions import AuthenticationFailed + + +def book_write_permission(f): + + def check_permission(self, request, book_id): + if int(book_id) in request.META['HTTP_CUSTOM_HEADER']: + return f(self, request, book_id) + else: + raise AuthenticationFailed() + + return check_permission + diff --git a/app/books/views.py b/app/books/views.py index 81156e8..c11087c 100644 --- a/app/books/views.py +++ b/app/books/views.py @@ -3,6 +3,7 @@ from .models import Book from django.core.exceptions import ObjectDoesNotExist from app.auth import UserAuthentication +from .permissions import book_write_permission class BooksView(ViewSet): @@ -33,6 +34,7 @@ def get(self, request, book_id): return JsonResponse({'message': 'Book doesn\'t exist'}, status=401) def delete(self, request, book_id): + try: removed_book = Book.objects.get(id=book_id) Book.objects.filter(id=book_id).delete() @@ -42,11 +44,11 @@ def delete(self, request, book_id): except ObjectDoesNotExist: return JsonResponse({'message': 'Book doesn\'t exist'}, status=401) + @book_write_permission def update(self, request, book_id): name = request.data.get('name') description = request.data.get('description') - creator_id = request.data.get('creator') - if name or description or creator_id: + if name or description: try: existing_book = Book.objects.filter(id=book_id) if not existing_book: @@ -55,8 +57,6 @@ def update(self, request, book_id): existing_book.update(name=name) if description: existing_book.update(description=description) - if creator_id: - existing_book.update(creator=creator_id) return JsonResponse({'message': 'Successfully updated book', 'book': existing_book[0].obj()}) except ObjectDoesNotExist: return JsonResponse({'message': 'Book doesn\'t exist'}, status=401) diff --git a/app/users/models.py b/app/users/models.py index c952ec0..6282fc3 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -20,7 +20,12 @@ def __hash__(self): return self.id def create_user_token(self): + from app.books.models import Book + # print(User.objects.get(id=1).books) + books = Book.objects.filter(creator_id=self.id) + books_ids = [x.id for x in books] return jwt.encode({'username': self.username, + 'books_ids': books_ids, 'exp': (datetime.now() + timedelta(days=1)).timestamp()}, key, algorithm='HS256').decode() diff --git a/app/users/views.py b/app/users/views.py index 5624ac9..2a2def4 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -3,11 +3,11 @@ from rest_framework.exceptions import AuthenticationFailed from app.users.models import User from django.core.exceptions import ObjectDoesNotExist -from app.auth import UserAuthentication +from app.auth import UserAuthentication, UsersViewAuthentication class UsersView(ViewSet): - authentication_classes = (UserAuthentication,) + authentication_classes = (UsersViewAuthentication,) def get(self, request): all_users = list(User.objects.all()) @@ -57,7 +57,6 @@ def delete(self, request, user_id): class UserLogin(ViewSet): - authentication_classes = (UserAuthentication,) def post(self, request): username = request.data.get('username') diff --git a/db.sqlite3 b/db.sqlite3 index 26b2e7db150a933b59e6e4e9eee5d85a2d478c3f..1eb8e8148e4e16b2a02f88e1215ed0e630db4cb7 100644 GIT binary patch delta 481 zcmZp8z}fJCbAmKu*+dy<#XMpmF|ASu@Tpnm& zw(}+Naq+I_jo+-O;Le-m$IHqf-^g2 zYHF5dk&Zfs^?oMMn-mYA57l#*g*lxS#}nr3cfm||#ZVw7SL$-~NE3N#OB zT)7b#OwW^J^5hTWW@XSvh=T>UC&@9*7Zl~oVPNOI#lU}pzn||8zdzq1-dlVcK-V4N zt@jaQV=%RLWXww~R{*(A!N^EKSD~~ZC9xzGB$kp|oLr<(lnRnd0ZJMfnHU)u8R!}q z>Kd6S7@Arc8C#hc>scCGn3`F#I*GF}D597~zzk2QAtqLaMtYV8MyBQlto+gpW|HcT ej7j36L8BZIK*90d28h1}^~$lV>ke1{D_t12X}W!3-0#(=RbV4h0Y3 z01x{Q_7Cw7;1A}rAs`wLlPrIz3}rGfGB7PLF)cARAcNq4x8Q#PB#;mV4q^Zgv=3zu z5DvEvVh$0bAs`L`|CbaW0!<843IzZufem$YWpa0y!5sogx3?YwN)Ztc8vq3kq5u!b W504Jq4^IxFvmqdm4!5E{0$wq^xiOCb diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..e69de29 From f46aae882f7dd2a53bf8433843f292dd559fa244 Mon Sep 17 00:00:00 2001 From: Oleksandr Bohutskyi Date: Mon, 27 Jan 2020 16:19:18 +0200 Subject: [PATCH 14/14] Init auth, tests completed --- app/books/views.py | 2 +- db.sqlite3 | Bin 159744 -> 159744 bytes tests/books/mocks.py | 7 ++++ tests/books/test_books.py | 16 ++++++--- tests/test_auth.py | 71 ++++++++++++++++++++++++++++++++++++++ tests/users/mocks.py | 8 ++++- tests/users/test_users.py | 5 ++- 7 files changed, 101 insertions(+), 8 deletions(-) diff --git a/app/books/views.py b/app/books/views.py index c11087c..90c2f87 100644 --- a/app/books/views.py +++ b/app/books/views.py @@ -33,8 +33,8 @@ def get(self, request, book_id): except ObjectDoesNotExist: return JsonResponse({'message': 'Book doesn\'t exist'}, status=401) + @book_write_permission def delete(self, request, book_id): - try: removed_book = Book.objects.get(id=book_id) Book.objects.filter(id=book_id).delete() diff --git a/db.sqlite3 b/db.sqlite3 index 1eb8e8148e4e16b2a02f88e1215ed0e630db4cb7..badcc55a196c3f08ced48214af14d02f23445e20 100644 GIT binary patch delta 98 zcmV-o0G36L8Bcaa=J0e7)rqHh%100s}#01x{Q_7Cw7;1ARf<`3qxAs`kH zlPrI(1P|;027}&zx88pNCa@9)4k!Q*$PbSW+z(F-vYyeKT-aP^$ EG7Bpq*8l(j delta 98 zcmV-o0G36L8Bc99%I0d}!qqHh%J00$4$01x{Q_7Cw7;1Ajl)DPyfAs`kH zlPrI(1O)>D0fXLux88pNCa@9*4cY(?$PbSW+z(F;Y76t&f-aP^$ EG63@-=l}o! diff --git a/tests/books/mocks.py b/tests/books/mocks.py index c60893a..6c7f673 100644 --- a/tests/books/mocks.py +++ b/tests/books/mocks.py @@ -29,3 +29,10 @@ def __init__(self, test_book): def __getitem__(self, indices): return self.test_book + + +class MockUserAuthentication: + + def authenticate(self, request): + request.META['HTTP_CUSTOM_HEADER'] = [1] + return 'decoded_token', None diff --git a/tests/books/test_books.py b/tests/books/test_books.py index 7990240..1f2e5f3 100644 --- a/tests/books/test_books.py +++ b/tests/books/test_books.py @@ -1,7 +1,8 @@ from django.test import Client from django.urls import reverse from mock import PropertyMock, patch -from .mocks import BookMock +from app.auth import UserAuthentication +from .mocks import BookMock, MockUserAuthentication import mock from django.core.exceptions import ObjectDoesNotExist @@ -10,6 +11,7 @@ class TestBooksView: def setup(self) -> None: self.book_mock = BookMock() + UserAuthentication.authenticate = MockUserAuthentication.authenticate def test_get(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: @@ -43,6 +45,7 @@ class TestSingleBookView: def setup(self) -> None: self.book_mock = BookMock() + UserAuthentication.authenticate = MockUserAuthentication.authenticate def test_get_valid_book_id(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: @@ -65,7 +68,7 @@ def test_get_with_invalid_data(self): assert 401 == resp.status_code self.book_mock.objects.get.assert_called_with(id='1') - def test_delete(self): + def test_delete_existing_book(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: mock_books_objects.return_value = self.book_mock.objects @@ -78,7 +81,7 @@ def test_delete(self): self.book_mock.objects.filter.assert_called_with(id='1') self.book_mock.objects.filter().delete.assert_called() - def test_delete_book_do_not_exists(self): + def test_delete_book_not_existing_book(self): with patch('app.books.views.Book.objects', new_callable=PropertyMock) as mock_books_objects: mock_books_objects.return_value = self.book_mock.objects self.book_mock.objects.get = mock.Mock(side_effect=ObjectDoesNotExist) @@ -112,14 +115,17 @@ def test_update_book_do_not_exists(self): resp = Client().put( reverse('book', args=[1]), {'name': 'updated_name'}, - content_type='application/json') + content_type='application/json',) assert {'message': "Book doesn't exist"} == resp.json() assert 401 == resp.status_code self.book_mock.objects.filter.assert_called_with(id='1') def test_update_invalid_data(self): - resp = Client().put(reverse('book', args=[1])) + resp = Client().put(reverse( + 'book', args=[1]), + {}, + content_type='application/json') assert {'message': 'Invalid data'} == resp.json() assert 400 == resp.status_code diff --git a/tests/test_auth.py b/tests/test_auth.py index e69de29..c20dee1 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -0,0 +1,71 @@ +import pytest +import jwt +import mock +from app.auth import UserAuthentication +from rest_framework.test import APIRequestFactory +import app.auth +from rest_framework import exceptions + + +class TestUserAuthentication: + + def setup(self) -> None: + self.factory = APIRequestFactory() + + def test_authenticate_without_token(self): + request = self.factory.post('/users/', + {}, + HTTP_AUTHORIZATION='Bearer') + + with pytest.raises(exceptions.AuthenticationFailed) as e: + UserAuthentication().authenticate(request) + + assert 'Invalid token header. No credentials provided.' == str(e.value) + + def test_authenticate_too_many_token_words(self): + request = self.factory.post('/users/', + {}, + HTTP_AUTHORIZATION='Bearer 1 2') + + with pytest.raises(exceptions.AuthenticationFailed) as e: + UserAuthentication().authenticate(request) + + assert 'Invalid token header. Token string should not contain spaces.' == str(e.value) + + def test_authenticate_valid_token(self): + decoded_token = {'username': 'test username', + 'books_ids': [1]} + jwt.decode = mock.Mock(return_value=decoded_token) + request = self.factory.post('/users/', + {}, + HTTP_AUTHORIZATION='Bearer {}'.format('test_token')) + + result = UserAuthentication().authenticate(request) + + assert decoded_token, None == result + assert request.META['HTTP_CUSTOM_HEADER'] == [1] + jwt.decode.assert_called_with('test_token', app.auth.key, algorithm='HS256') + + def test_authenticate_expired_token(self): + jwt.decode = mock.Mock(side_effect=jwt.ExpiredSignatureError()) + request = self.factory.post('/users/', + {}, + HTTP_AUTHORIZATION='Bearer {}'.format('test_token')) + + with pytest.raises(exceptions.AuthenticationFailed) as e: + UserAuthentication().authenticate(request) + + assert 'Token was expired' == str(e.value) + jwt.decode.assert_called_with('test_token', app.auth.key, algorithm='HS256') + + def test_authenticate_invalid_token(self): + jwt.decode = mock.Mock(side_effect=jwt.DecodeError()) + request = self.factory.post('/users/', + {}, + HTTP_AUTHORIZATION='Bearer {}'.format('test_token')) + + with pytest.raises(exceptions.AuthenticationFailed) as e: + UserAuthentication().authenticate(request) + + assert 'Invalid token header.' == str(e.value) + jwt.decode.assert_called_with('test_token', app.auth.key, algorithm='HS256') diff --git a/tests/users/mocks.py b/tests/users/mocks.py index acf1242..3a3b231 100644 --- a/tests/users/mocks.py +++ b/tests/users/mocks.py @@ -6,7 +6,6 @@ class UserMock: def __init__(self): self.objects = UserMock.UserMockObjects() - # self.get_hash = mock.Mock(return_value='test_hash') class UserMockObjects: @@ -33,3 +32,10 @@ class EmptyFilterMock: def __len__(self): return 0 + + +class MockUserAuthentication: + + def authenticate(self, request): + request.META['HTTP_CUSTOM_HEADER'] = [1] + return 'decoded_token', None diff --git a/tests/users/test_users.py b/tests/users/test_users.py index 3dc9e30..7966374 100644 --- a/tests/users/test_users.py +++ b/tests/users/test_users.py @@ -1,9 +1,10 @@ from django.test import Client from django.urls import reverse from mock import PropertyMock, patch -from .mocks import UserMock, EmptyFilterMock +from .mocks import UserMock, EmptyFilterMock, MockUserAuthentication import mock from django.core.exceptions import ObjectDoesNotExist +from app.auth import UserAuthentication class TestUsersView: @@ -12,6 +13,7 @@ def setup(self) -> None: self.user_mock = UserMock() def test_get_when_users_arent_created_returns_empty_list(self): + UserAuthentication.authenticate = MockUserAuthentication.authenticate with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: mock_users_objects.return_value = self.user_mock.objects @@ -52,6 +54,7 @@ class TestSingleUserView: def setup(self) -> None: self.user_mock = UserMock() + UserAuthentication.authenticate = MockUserAuthentication.authenticate def test_get_valid_user_id(self): with patch('app.users.views.User.objects', new_callable=PropertyMock) as mock_users_objects: